P2P

WebRTC 的 P2P 连接,核心就是两个端点之间直接建立音视频数据通道,不经过中转服务器(除了信令和 NAT 穿透辅助)。
在 metaRTC 这套代码里,P2P 的实现主要分为以下几个步骤:

信令交换(SDP/ICE)

  • 通过信令服务器(可以是 SRS、Janus、WHIP 等)交换SDP(会话描述)和ICE Candidate(候选地址)。
  • 代码里相关的类有:YangPeerConnection7、YangRtcPublish、YangMetaConnection 等。

建立 ICE 连接

  • 通过 ICE 协议,尝试各种网络路径(本地IP、公网IP、STUN/TURN服务器)建立点对点连接。
  • 代码里 YangPeerConnection7 负责 ICE 相关的回调和状态管理。

DTLS 握手

  • 建立安全的传输通道,双方通过 DTLS(基于 UDP 的 TLS)完成密钥协商。
  • 代码里会检查 DTLS 状态,只有握手完成后才会真正传输音视频数据。

SRTP/RTP 传输音视频

  • 音视频数据经过编码(H264/H265/Opus等),打包成 RTP 包,通过 SRTP(加密的 RTP)直接在两个 peer 之间传输。
  • 代码里 YangPeerConnection7、YangMetaConnection、YangRtcConnection、pushH264/pushH265 等模块负责数据的打包和发送。

代码结构简化图

信令交换
YangPeerConnection7
ICE 连接建立
DTLS 握手
SRTP/RTP 传输
对端 Peer 收到数据

camera采集流程

采集线程的启动流程如下:

采集线程的本质

在 YangVideoCaptureLinux 这类采集类中,采集线程其实就是通过调用其 run() 方法启动的,而 run() 方法内部会调用 startLoop(),进入主循环不断采集摄像头数据。

采集线程的启动位置

采集线程的启动,通常是在上层通过 start() 方法或者直接新建线程来实现的。

在 metaRTC 这套代码里,采集线程的启动一般发生在如下流程:

  • YangPushCapture::startVideoCapture()
    这个方法会判断 m_videoCapture 是否存在且未启动,然后调用 m_videoCapture->start()。

  • YangVideoCapture 及其子类(如 YangVideoCaptureLinux)通常继承自某个线程基类(如 YangThread 或直接用 std::thread/QThread),其 start() 方法会新建线程并自动调用 run()。

代码链路举例

以 Linux 为例,流程大致如下:

  1. 上层调用
    YangPushCapture::startVideoCapture()
    if(m_videoCapture && !m_videoCapture->m_isStart) m_videoCapture->start();
    
  2. 线程启动
    start() 方法新建线程,线程入口是 run()。
  3. 进入主循环
    run() 方法内部调用 startLoop(),不断采集摄像头数据。
相关代码片段
  • YangPushCapture.cpp

    void YangPushCapture::startVideoCapture(){
        if(m_videoCapture && !m_videoCapture->m_isStart) m_videoCapture->start();
    }
    
  • YangVideoCapture.cpp

    void YangVideoCapture::run() {
        m_isStart = 1;
        startLoop();
        m_isStart = 0;
    }
    

采集线程的启动是通过 YangPushCapture::startVideoCapture() 间接调用 m_videoCapture->start() 实现的,start() 会新建线程并自动执行 run(),最终进入 startLoop() 进行摄像头数据的持续采集。

采集接口

startLoop

在 YangVideoCaptureLinux 里,startLoop() 的作用是:

启动摄像头采集主循环,不断从摄像头设备读取视频帧,并把帧数据写入缓冲区。

主要流程
  1. 初始化缓冲区:把所有 V4L2 的 buffer 都入队(VIDIOC_QBUF)。
  2. 启动视频流:调用 VIDIOC_STREAMON 开始采集。
  3. 主循环:用 select 等待摄像头设备有新数据可读。
  4. 读取帧数据:每次有新数据,调用 read_buffer(),从设备取出一帧,并通过 m_vhandle->putBuffer(...) 写入 YangVideoBuffer。
  5. 循环直到采集停止。
关键代码片段
void YangVideoCaptureLinux::startLoop() {
    // 1. 入队所有缓冲区
    // 2. 启动视频流
    // 3. 主循环,不断 select + read_buffer
    while (m_isloop) {
        // 等待数据
        r = select(m_vd_id + 1, &fds, NULL, NULL, &tv);
        // 读取一帧
        read_buffer();
    }
}

startLoop 在哪里被调用?
  • 直接在 YangVideoCapture 的 run() 方法里被调用:
void YangVideoCapture::run() {
    m_isStart = 1;
    startLoop();
    m_isStart = 0;
}
  • 采集线程启动时会调用 run(),从而进入 startLoop()。

  • 采集线程的启动一般由上层的 startVideoCapture() 或类似方法触发。


写入缓冲区

摄像头采集线程把每一帧视频数据写入 YangVideoBuffer(如 m_out_videoBuffer、m_pre_videoBuffer)的核心代码在 YangVideoCaptureHandle 这个类里,具体如下:

1. 采集线程调用 putBuffer 或相关方法

  • 采集到一帧数据后,都会调用 YangVideoCaptureHandle::putBuffer 或类似方法。
//libmetartc7/src/yangcapture/linux/YangVideoCaptureLinux.cpp
  if (m_vhandle)
      m_vhandle->putBuffer(m_timestatmp, m_user_buffer[m_buf.index].start, m_user_buffer[m_buf.index].length);

2. putBuffer 内部会调用 putBuffers

  • putBuffer 负责处理格式转换、时间戳等,然后调用 putBuffers:

    void YangVideoCaptureHandle::putBuffer(int64_t pstamtime, uint8_t *pBuffer, int32_t BufferLen) {
        // ... 省略格式转换 ...
        m_videoFrame.payload = tmp;
        m_videoFrame.nb = m_bufLen;
        putBuffers();
        // ...
    }
    

3. putBuffers 把帧写入各个缓冲区

  • 这里会把当前帧写入 m_pre_videoBuffer、m_out_videoBuffer 等:

    void YangVideoCaptureHandle::putBuffers() {
        if(m_pre_videoBuffer) m_pre_videoBuffer->putVideo(&m_videoFrame);
        if(m_isFilm && m_film_out_videoBuffer) m_film_out_videoBuffer->putVideo(&m_videoFrame);
        if (m_isCapture && m_out_videoBuffer) {
            m_out_videoBuffer->putVideo(&m_videoFrame);
        }
        if (m_isLivingCaptrue && m_living_out_videoBuffer){
            m_living_out_videoBuffer->putVideo(&m_videoFrame);
        }
    }
    

m_pre_videoBuffer、m_out_videoBufferbuffer 的作用

1. m_pre_videoBuffer
  • 全称:预处理视频缓冲区(Pre Video Buffer)
  • 作用:
    • 主要用于本地预览,也就是把采集到的原始摄像头帧,直接显示在本地界面上(比如 Qt 窗口)。
    • 有时也可以作为编码前的中转区,便于后续处理(如加水印、旋转等)。
  • 典型用法:
    • 在渲染线程(如 YangRecordThread::render())里,从 m_pre_videoBuffer 取出一帧,显示到本地窗口。
    • 不参与网络推流。

2. m_out_videoBuffer
  • 全称:输出视频缓冲区(Out Video Buffer)
  • 作用:
    • 主要用于编码和推流,也就是把采集到的帧送给编码器,编码后再推送到远端。
    • 这个 buffer 是编码器的输入源。
  • 典型用法:
    • 编码线程会不断从 m_out_videoBuffer 取出帧,进行 H264/H265 等编码,然后编码后的数据再送到网络推流模块。
    • 只参与推流,不直接用于本地预览。

为什么有两个buffer?
  • 解耦:本地预览和推流编码是两条独立的链路,互不影响。即使推流卡住,本地预览也能流畅显示。
  • 效率:本地预览通常不需要编码,直接显示原始帧更快;推流则需要编码压缩,流程更复杂。
  • 灵活性:可以在预处理 buffer 做一些本地特效、调试等,不影响推流。

4. 代码举例
  • 写入(采集线程):
    if(m_pre_videoBuffer) m_pre_videoBuffer->putVideo(&m_videoFrame);
    if(m_isCapture && m_out_videoBuffer) m_out_videoBuffer->putVideo(&m_videoFrame);
    
  • 读取(本地预览):
    uint8_t* t_vb = m_pre_videoBuffer->getVideoRef(&m_frame);
    
  • 读取(编码推流):
    uint8_t* t_vb = m_out_videoBuffer->getVideoRef(&m_frame);
    

  • m_pre_videoBuffer:本地预览用,让你能在界面上看到摄像头画面。
  • m_out_videoBuffer:编码推流用,把视频帧送给编码器和网络,发给对方。

每一帧摄像头数据采集后,最终都是通过 YangVideoCaptureHandle::putBuffers() 写入 YangVideoBuffer,而 putBuffers() 会把帧分发到 m_out_videoBuffer、m_pre_videoBuffer 等缓冲区。

编码

编码线程从 m_out_videoBuffer 取帧并进行 H264/H265 编码的核心代码在 YangVideoEncoderHandle 里,具体如下:

1. 设置输入缓冲区

编码线程会通过 setInVideoBuffer 方法,把 m_out_videoBuffer 传进来:

void YangVideoEncoderHandle::setInVideoBuffer(YangVideoBuffer *plist) {
    m_in_videoBuffer = plist;
}

2. 编码线程主循环

编码线程的主循环在 YangVideoEncoderHandle::startLoop():

void YangVideoEncoderHandle::startLoop() {
    m_isConvert = 1;
    // ... 省略参数准备 ...
    while (m_isConvert == 1) {
        if (m_in_videoBuffer->size() == 0) {
            yang_usleep(20000);
            continue;
        }
        uint8_t *tmpsrc = m_in_videoBuffer->getVideoRef(&videoFrame);
        if(!tmpsrc) continue;
        // ... 格式转换等 ...
        t_Encoder->encode(&videoFrame, this); // 进行H264/H265编码
    }
    // ... 资源释放 ...
}
  • 这里的 m_in_videoBuffer 就是 m_out_videoBuffer,即采集线程写入的 buffer。
  • getVideoRef(&videoFrame) 从 buffer 取出一帧,填充到 videoFrame 结构体。

3. 编码实现

编码器对象(如 YangOpenH264Encoder、YangVideoEncoderFfmpeg 等)会把 videoFrame 编码成 H264/H265 数据。
例如软编码(OpenH264):

int32_t YangOpenH264Encoder::encode(YangFrame* pframe, YangEncoderCallback* pcallback) {
    // ... 编码 ...
    if (pcallback)
        pcallback->onVideoData(pframe); // 编码后回调
}
  • 采集线程:写入 m_out_videoBuffer
  • 编码线程:startLoop() 里不断 getVideoRef() 取帧,encode() 编码
  • 编码后的数据:通过回调/缓冲区传递给推流模块

`YangVideoEncoderHandle::startLoop()

YangVideoEncoderHandle::startLoop() 是在 YangVideoEncoderHandle::run() 里被调用的,而 run() 是编码线程的主函数。

  1. YangPushPublish::startVideoEncoding()
    → YangPushEncoder::startVideoEncoder()
    → YangVideoEncoderHandle::start()
    → 新线程启动,自动调用 YangVideoEncoderHandle::run()
    → run() 里调用 startLoop(),开始编码循环

YangVideoEncoderHandle

pcallback->onVideoData(pframe); 这个回调的配置,其实就是编码线程本身,也就是 YangVideoEncoderHandle 这个对象。
在 YangVideoEncoderHandle::startLoop() 里有:

t_Encoder->encode(&videoFrame, this);

这里的 this 就是当前的 YangVideoEncoderHandle 对象。
YangVideoEncoderHandle 继承了 YangEncoderCallback(或者实现了 onVideoData 这个接口),所以可以作为回调传进去。

回调的作用
  • 编码器编码完一帧后,调用 pcallback->onVideoData(pframe)。
  • 实际上就是调用 YangVideoEncoderHandle::onVideoData(pframe)。
  • 这个函数里会把编码后的帧写入编码后缓冲区(m_out_videoBuffer->putEVideo(pframe)),供推流线程读取。
// YangVideoEncoderHandle.cpp
void YangVideoEncoderHandle::onVideoData(YangFrame* pframe) {
    if (pframe->nb > 4) {
    /*- 这是一个回调函数,当视频编码器(如 x264、FFmpeg 等)成功编码一帧视频后,就会调用这个函数。- 参数 pframe 包含了编码好的视频数据(比如 H264 NALU)*/
        m_out_videoBuffer->putEVideo(pframe);
        //- m_out_videoBuffer 是YangPushEncoder类的m_out_videoBuffer
    }
}

pcallback->onVideoData(pframe) 的回调对象就是 YangVideoEncoderHandle,也就是编码线程自己,编码线程通过实现回调接口来接收编码后的数据。

推流

推流线程/模块读取编码帧

  • 推流核心类(如 YangRtcPublish)会有一个循环,不断从 YangVideoEncoderBuffer(也就是 m_out_videoBuffer)里取出编码好的帧。
  • 取到编码帧后,推流模块会把数据打包成 RTP/WHIP/WebRTC 等协议包,最终通过 UDP/TCP 发送到远端。
    on_video 函数的具体定义位于 YangRtcPublish 类中,文件路径为 demo/metapushstream7/yangpush/YangRtcPublish.cpp。以下是该函数的相关代码片段:
void YangRtcPublish::startLoop() {
    // ... 省略其他代码 ...

    while (m_isConvert == 1) {
        // ... 省略其他代码 ...

        if (m_in_videoBuffer && m_in_videoBuffer->size() > 0) {
            videoFrame.payload = m_in_videoBuffer->getEVideoRef(&videoFrame);

            if (videoFrame.frametype == YANG_Frametype_I) {
                // ... 处理元数据 ...

                data.setVideoFrametype(data.context, YANG_Frametype_Spspps);
                data.setMetaTimestamp(data.context, videoFrame.pts);
                ret = stream->on_video(data.getVideoFrame(data.context));

                // ... 处理视频帧 ...
            }

            data.setVideoData(data.context, &videoFrame, videoType);
            ret = stream->on_video(data.getVideoFrame(data.context));

            // ... 处理连接状态 ...
        }
    }
    // ... 省略其他代码 ...
}
  • on_video 函数的调用发生在 startLoop 方法中,主要用于将视频帧发送到远程对等方。
  • 该函数会检查视频缓冲区是否有可用的帧,并处理这些帧的元数据,然后通过 stream->on_video 方法将其发送出去。
  • stream就是YangPeerConnection7的类指针
int32_t YangPeerConnection7::on_video(YangFrame* videoFrame){
    return m_conn.on_video(&m_peer, videoFrame);
    //`m_conn` 是 `YangMetaConnection` 这个**结构体**的对象。
}

YangMetaConnection

m_conn 是 YangMetaConnection 这个结构体的对象。
在 YangPeerConnection7 的声明(include/yangrtc/YangPeerConnection7.h)里:

class YangPeerConnection7 {
    // ...
private:
    YangMetaConnection m_conn;
    // ...
};

YangMetaConnection 是一个结构体(C风格),定义在 include/yangrtc/YangMetaConnection.h:

typedef struct {
    // 一堆函数指针,比如
    int32_t  (*on_video)(YangPeer* peer,YangFrame* videoFrame);
    // ...
} YangMetaConnection;

它本质上是一个函数指针表,类似于接口或虚表,里面的函数指针在初始化时会被赋值为具体的实现函数。

SPS/PPS帧
H264帧
H265帧
C++: m_conn.on_video(&peer, &frame)
C: yang_pc_onVideo
C: yang_rtcconn_onVideo
帧类型判断
发送视频元数据
session->pushH264->on_video
session->pushH265->on_video
远端Peer收到视频数据
Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐