前言
本文基于PineAppRtc开源项目github.com/thfhongfeng…
因为一个需求,我们需要将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格式;但是接收的数据则不同。如果我们要拿到这些数据进行处理,就需要我们自己进行转码,转到通用的格式后再处理。