浅析WebRtc中视频数据的接收和渲染流程
前言
本文基于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數組
總結
通過分析可以發現,在WebRtc中傳輸視頻數據的時候用的是i420格式的,當然采集發送時候這個庫在底層自動將原始數據轉成i420格式;但是接收的數據則不同。如果我們要拿到這些數據進行處理,就需要我們自己進行轉碼,轉到通用的格式后再處理。
關注公眾號:BennuCTech,發送“電子書”獲取經典學習資料。
總結
以上是生活随笔為你收集整理的浅析WebRtc中视频数据的接收和渲染流程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅析WebRtc中视频数据的收集和发送流
- 下一篇: 简单聊聊Glide的内存缓存