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

简介: 本文基于PineAppRtc开源项目github.com/thfhongfeng…因为一个需求,我们需要将WebRtc发送过来的视频流中转出去,所以就研究一下WebRtc是如何接收视频数据并进行处理渲染的,于是有了这篇文章。

前言


本文基于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格式;但是接收的数据则不同。如果我们要拿到这些数据进行处理,就需要我们自己进行转码,转到通用的格式后再处理。



目录
相关文章
|
弹性计算 监控 网络协议
ecs资源监控操作
监控阿里云ECS服务器资源分为7步:登录阿里云控制台,进入ECS管理界面,选择要监控的实例,查看基础监控数据,通过云监控服务获取详细图表、配置报警规则,可选安装云监控插件获取OS级数据,最后定期审查优化资源配置。通过这些步骤,确保系统稳定运行并及时处理问题。如需帮助,参考官方文档或联系阿里云支持。
571 3
|
编解码 移动开发 流计算
【开源视频联动物联网平台】流媒体传输协议HLS,FLV的功能和特点
【开源视频联动物联网平台】流媒体传输协议HLS,FLV的功能和特点
511 2
|
Web App开发 编解码 监控
【开源视频联动物联网平台】推流,拉流,转发,转码?
【开源视频联动物联网平台】推流,拉流,转发,转码?
1384 2
|
7月前
|
Web App开发 算法 安全
《拆解WebRTC:NAT穿透的探测逻辑与中继方案》
本文深入解析了WebRTC应对NAT穿透的技术体系。NAT因类型多样(完全锥形、受限锥形、端口受限锥形、对称NAT)给端到端通信带来挑战,而WebRTC通过STUN服务器探测公网地址与NAT类型,借助ICE协议规划多路径(本地地址、公网反射地址、中继地址)并验证连接,TURN服务器则作为中继保障通信。文章还探讨了多层NAT、运营商级NAT等复杂场景的应对策略,揭示WebRTC通过探测、协商与中继实现可靠通信的核心逻辑,展现其在网络边界中寻找连接路径的技术智慧。
356 7
|
3月前
|
数据采集 SQL 人工智能
评估工程正成为下一轮 Agent 演进的重点
AI系统因不确定性需重构评估体系,评估工程正从人工经验走向自动化。通过LLM-as-a-Judge、奖励模型与云监控2.0等技术,实现对Agent输出的可量化、可追溯、闭环优化的全周期评估,构建AI质量护城河。(238字)
|
编译器 C++
简述构造函数、拷贝构造函数、深拷贝浅拷贝、析构函数
简述构造函数、拷贝构造函数、深拷贝浅拷贝、析构函数
|
设计模式 Java Android开发
安卓应用开发中的内存泄漏检测与修复
【9月更文挑战第30天】在安卓应用开发过程中,内存泄漏是一个常见而又棘手的问题。它不仅会导致应用运行缓慢,还可能引发应用崩溃,严重影响用户体验。本文将深入探讨如何检测和修复内存泄漏,以提升应用性能和稳定性。我们将通过一个具体的代码示例,展示如何使用Android Studio的Memory Profiler工具来定位内存泄漏,并介绍几种常见的内存泄漏场景及其解决方案。无论你是初学者还是有经验的开发者,这篇文章都将为你提供实用的技巧和方法,帮助你打造更优质的安卓应用。
element UI实现输入建议下拉列表 —— el-select filterable可筛选的下拉列表 or 带输入建议的输入框 el-autocomplete ?
element UI实现输入建议下拉列表 —— el-select filterable可筛选的下拉列表 or 带输入建议的输入框 el-autocomplete ?
899 0
|
Python
【已解决】如何用正则提取小括号的内容
【已解决】如何用正则提取小括号的内容
474 0
|
C++ 编译器
C++ - 虚基类、虚函数与纯虚函数
虚基类       在说明其作用前先看一段代码 class A{public:    int iValue;};class B:public A{public:    void bPrintf(){cout
3045 0