前言

本文基于PineAppRtc开源项目https://github.com/thfhongfeng/PineAppRtc

因为一个需求,我们需要将WebRtc发送过来的视频流中转出去,所以就研究一下WebRtc是如何接收视频数据并进行处理渲染的,于是有了这篇文章。

数据接收

在使用webrtc进行即时通话时,双方连接上后,会根据参数创建一个PeerConnection连接对象,具体代码在PeerConnectionClient类中,这个是需要自己来实现的。这个连接的作用来进行推拉流的。

我们在PeerConnectionClient中可以找到PCObserver,它实现了PeerConnection.Observer这个接口。在它的onAddStream回调中

if (stream.videoTracks.size() == 1) {mRemoteVideoTrack = stream.videoTracks.get(0);mRemoteVideoTrack.setEnabled(mRenderVideo);for (VideoRenderer.Callbacks remoteRender : mRemoteRenders) {mRemoteVideoTrack.addRenderer(new VideoRenderer(remoteRender));}
}

可以看到为remoteVideoTrack添加了VideoRenderer,这个VideoRenderer就是处理接受到的视频数据的

VideoRenderer的构造函数中传入的是VideoRenderer.Callbacks,它是一个接口,我们以其中一个实现SurfaceViewRenderer为例,它的回调函数renderFrame代码如下

public void renderFrame(I420Frame frame) {this.updateFrameDimensionsAndReportEvents(frame);this.eglRenderer.renderFrame(frame);
}

这个I420Frame就是封装后的接收到的视频数据。

绘制

renderFrame中执行了eglRenderer.renderFrame开始进行绘制

public void renderFrame(I420Frame frame) {...synchronized(this.handlerLock) {...synchronized(this.frameLock) {...this.pendingFrame = frame;this.renderThreadHandler.post(this.renderFrameRunnable);}}...
}

frame赋值给pendingFrame,然后post一个runnable,这个runnable代码如下

private final Runnable renderFrameRunnable = new Runnable() {public void run() {EglRenderer.this.renderFrameOnRenderThread();}};

可以看到执行了renderFrameOnRenderThread函数:

private void renderFrameOnRenderThread() {Object var2 = this.frameLock;I420Frame frame;synchronized(this.frameLock) {...frame = this.pendingFrame;this.pendingFrame = null;}if (this.eglBase != null && this.eglBase.hasSurface()) {...int[] yuvTextures = shouldUploadYuvTextures ? this.yuvUploader.uploadYuvData(frame.width, frame.height, frame.yuvStrides, frame.yuvPlanes) : null;if (shouldRenderFrame) {GLES20.glClearColor(0.0F, 0.0F, 0.0F, 0.0F);GLES20.glClear(16384);if (frame.yuvFrame) {this.drawer.drawYuv(yuvTextures, drawMatrix, drawnFrameWidth, drawnFrameHeight, 0, 0, this.eglBase.surfaceWidth(), this.eglBase.surfaceHeight());} else {this.drawer.drawOes(frame.textureId, drawMatrix, drawnFrameWidth, drawnFrameHeight, 0, 0, this.eglBase.surfaceWidth(), this.eglBase.surfaceHeight());}...}this.notifyCallbacks(frame, yuvTextures, texMatrix, shouldRenderFrame);VideoRenderer.renderFrameDone(frame);} else {this.logD("Dropping frame - No surface");VideoRenderer.renderFrameDone(frame);}}

将I420Frame加载成int[],然后通过drawer的对应的drawXxx函数进行绘制.

拦截处理

所以我们如果要自己处理接收的数据,就需要自行实现一个VideoRenderer.Callbacks,将其封装到VideoRenderer中并add到mRemoteVideoTrack上。

那么还有一个问题,I420Frame如何转成原生数据呢?

我发现VideoRenderer.Callbacks的另外一个实现VideoFileRenderer。如果要写入文件,一定会以原生数据的形式写入的,它的部分代码

public void renderFrame(final I420Frame frame) {this.renderThreadHandler.post(new Runnable() {public void run() {VideoFileRenderer.this.renderFrameOnRenderThread(frame);}});
}private void renderFrameOnRenderThread(I420Frame frame) {float frameAspectRatio = (float)frame.rotatedWidth() / (float)frame.rotatedHeight();float[] rotatedSamplingMatrix = RendererCommon.rotateTextureMatrix(frame.samplingMatrix, (float)frame.rotationDegree);float[] layoutMatrix = RendererCommon.getLayoutMatrix(false, frameAspectRatio, (float)this.outputFileWidth / (float)this.outputFileHeight);float[] texMatrix = RendererCommon.multiplyMatrices(rotatedSamplingMatrix, layoutMatrix);try {ByteBuffer buffer = nativeCreateNativeByteBuffer(this.outputFrameSize);if (frame.yuvFrame) {nativeI420Scale(frame.yuvPlanes[0], frame.yuvStrides[0], frame.yuvPlanes[1], frame.yuvStrides[1], frame.yuvPlanes[2], frame.yuvStrides[2], frame.width, frame.height, this.outputFrameBuffer, this.outputFileWidth, this.outputFileHeight);buffer.put(this.outputFrameBuffer.array(), this.outputFrameBuffer.arrayOffset(), this.outputFrameSize);} else {this.yuvConverter.convert(this.outputFrameBuffer, this.outputFileWidth, this.outputFileHeight, this.outputFileWidth, frame.textureId, texMatrix);int stride = this.outputFileWidth;byte[] data = this.outputFrameBuffer.array();int offset = this.outputFrameBuffer.arrayOffset();buffer.put(data, offset, this.outputFileWidth * this.outputFileHeight);int r;for(r = this.outputFileHeight; r < this.outputFileHeight * 3 / 2; ++r) {buffer.put(data, offset + r * stride, stride / 2);}for(r = this.outputFileHeight; r < this.outputFileHeight * 3 / 2; ++r) {buffer.put(data, offset + r * stride + stride / 2, stride / 2);}}buffer.rewind();this.rawFrames.add(buffer);} finally {VideoRenderer.renderFrameDone(frame);}}

可以看到得到的是I420Frame类,这个类里封装里视频数据,是i420格式的,且Y、U、V分别存储,可以看到yuvPlanes是一个ByteBuffer[]yuvPlanes[0]是Y,yuvPlanes[1]是U,yuvPlanes[2]是V

这些数据我们可能无法直接使用,所以需要进行转换,比如转成NV21格式。
我们知道NV21是YYYYVUVU这种格式,所以可以通过下面这个方法可以将其转成NV21格式的byte数组

public static byte[] convertLineByLine(org.webrtc.VideoRenderer.I420Frame src) {byte[] bytes = new byte[src.width*src.height*3/2];int i=0;for (int row=0; row<src.height; row++) {for (int col=0; col<src.width; col++) {bytes[i++] = src.yuvPlanes[0].get(col+row*src.yuvStrides[0]);}}for (int row=0; row<src.height/2; row++) {for (int col=0; col<src.width/2; col++) {bytes[i++] = src.yuvPlanes[2].get(col+row*src.yuvStrides[2]);bytes[i++] = src.yuvPlanes[1].get(col+row*src.yuvStrides[1]);}}return bytes;
}

总结

通过分析可以发现,在WebRtc中传输视频数据的时候用的是i420格式的,当然采集发送时候这个库在底层自动将原始数据转成i420格式;但是接收的数据则不同。如果我们要拿到这些数据进行处理,就需要我们自己进行转码,转到通用的格式后再处理。

关注公众号:BennuCTech,发送“电子书”获取经典学习资料。

浅析WebRtc中视频数据的接收和渲染流程相关推荐

  1. 浅析WebRtc中视频数据的收集和发送流程

    前言 本文是基于PineAppRtc开源项目https://github.com/thfhongfeng/PineAppRtc 因为一个需求,我们需要将一个视频流通过WebRtc发送出去,所以就研究一 ...

  2. 网卡驱动和队列层中的数据包接收

    一.从网卡说起 这并非是一个网卡驱动分析的专门文档,只是对网卡处理数据包的流程进行一个重点的分析.这里以Intel的e100驱动为例进行分析. 大多数网卡都是一个PCI设备,PCI设备都包含了一个标准 ...

  3. 使用Jrtplib实现RTP视频数据发送接收

    在网上下载获取Jrtplib包的源码,解压缩配置编译安装以后就可以使用,下载的代码包里面有example代码示例: 示例都是基于Jrtplib的RTP/RTCP协议栈实现,在设置Server端的时候, ...

  4. C#中POST数据和接收的几种方式

    POST方式提交数据,一种众所周知的方式: html页面中使用form表单提交,接收方式,使用Request.Form[""]或Request.QueryString[" ...

  5. linux如何使用鼠标数据的,浅析linux中鼠标数据读取

    001 #include 002 #include 003 #include 004 #include 005 #include 006 // 以下代码源自 007 // [需要在文本控制台下运行,才 ...

  6. 浅析 Hadoop 中的数据倾斜

    最近几次被问到关于数据倾斜的问题,这里找了些资料也结合一些自己的理解. 在并行计算中我们总希望分配的每一个task 都能以差不多的粒度来切分并且完成时间相差不大,但是集群中可能硬件不同,应用的类型不同 ...

  7. WebRTC视频数据流程分析

    本文来自<WebRTC Native开发实战>书籍作者许建林在LiveVideoStack线上分享中的内容,详细分析总结 WebRTC 的视频数据流程,并对大型项目如何快速上手:分析方法, ...

  8. 多路视频数据实时采集系统设计与实现

    多路视频数据实时采集系统设计与实现 常永亮   王霖萱  常馨蓉 摘要 面对越来越多的实时视频采集.播放的应用,如何能更加方便的操控视频采集,保证流畅的播放效果,成为近几年实时媒体流的一个重要研究方向 ...

  9. 在React中获取数据

    React初学者经常从不需要获取数据的应用开始.他们经常面临一个计数器,任务列表获取井字棋游戏应用.这是很好的,因为在开始学习React的时候,数据获取在你的应用中添加了另一层复杂度. 然而,有些时候 ...

最新文章

  1. excel模糊匹配两列文字_高效便捷的Word、Excel操作技巧
  2. 数据段、数据报、数据包、帧的区别与联系
  3. 怎么用class引入svg_【蓝湖指北】走向设计巅峰,从蓝湖 Sketch 插件开始,用它!...
  4. “生骨肉”概念加速奔跑,宠物行业真要起飞了?
  5. 重磅!深入调研证券行业,神策数据发布《财富管理数字化转型现状与趋势洞察》报告...
  6. phpbreak跳出几层循环_PHP跳出循环之“break”
  7. P1197-星球大战【并查集,图论】
  8. BZOJ-2005能量采集-数论函数
  9. 微服务、Kubernetes和无服务器之后,即将发生的……
  10. 信息学奥赛一本通 2040:【例5.7】筛选法找质数 (普通筛 线性筛)
  11. 二分法解决力扣374.猜数字大小 C语言
  12. 出现ESXi系统无法连接FreeNAS的情况?90%以上的人都做错了!
  13. react 创建组件
  14. 阵列信号处理笔记-阵列信号处理基础
  15. 听音乐用什么蓝牙耳机好?音质好的tws蓝牙耳机推荐
  16. C语言--确定到底谁是凶手
  17. 最详细的SQL注入语句相关的命令整理
  18. 一个女孩子居然做了十年硬件。​。。
  19. js实现web网页版台球游戏
  20. css变成块级元素_设置标签的css样式代码为“display:block”,标签将变为块级元素。( )...

热门文章

  1. android 开发时遇到的环境问题3--eclipse整个项目工程报错
  2. Installshield获取安装包版本的系统变量是IFX_PRODUCT_VERSION
  3. 转载 cFos vs cFosSpeed
  4. vue项目实现按需加载的3种方式
  5. 二柱子2.0编程总结
  6. 【转】Oracle执行计划解释
  7. 用JavaScript实现100以内自然数求和
  8. Trie实现(C++)
  9. request,response,session,application,out对象的常用调用的函数
  10. WebGL three.js学习笔记 6种类型的纹理介绍及应用