分类
开发计划

如何降低v4l2rtspserver的延时?

您想在命令 v4l2rtspserver -f H264 -W 1920 -H 1080 -F 60 -P 8554 /dev/video0 的基础上进一步优化延迟,包括修改源码层面的缓冲策略和零拷贝机制。

命令行优化

首先,在您的命令基础上添加低延迟参数:

v4l2rtspserver -f H264 -W 1920 -H 1080 -F 60 -P 8554 -Q 1 -c /dev/video0 &
  • -Q 1: 将帧队列设置为 1,最小化缓冲 1
  • -c: 禁用 SPS/PPS 配置帧重复 2

源码级优化建议

1. 降低 select() 超时时间

位置: src/V4L2DeviceSource.cpp:104-105 3

当前捕获线程使用 1 秒超时,这会增加轮询延迟。 4

修改:

tv.tv_sec=0;
tv.tv_usec=1000;  // 1ms,更激进的轮询

2. 优化队列触发机制

位置: src/V4L2DeviceSource.cpp:268-279 5

当前实现在队列满时才丢弃旧帧。对于 -Q 1 配置,可以修改为立即替换策略:

修改:

void V4L2DeviceSource::queueFrame(char * frame, int frameSize, const timeval &tv, char * allocatedBuffer) 
{
    pthread_mutex_lock (&m_mutex);

    // 对于 queueSize=1,直接替换而不是等待队列满
    if (m_queueSize == 1 && !m_captureQueue.empty()) {
        delete m_captureQueue.front();
        m_captureQueue.pop_front();
    }

    while (m_captureQueue.size() >= m_queueSize) {
        delete m_captureQueue.front();
        m_captureQueue.pop_front();
    }

    m_captureQueue.push_back(new Frame(frame, frameSize, tv, allocatedBuffer)); 
    pthread_mutex_unlock (&m_mutex);

    envir().taskScheduler().triggerEvent(m_eventTriggerId, this);
}

3. 零拷贝优化已实现

系统已经实现了零拷贝设计: 6

  • splitFrames() 返回指向原始缓冲区的指针,不进行数据拷贝 7
  • 只有最后一个帧负责释放 allocatedBuffer 8
  • Frame 结构体使用指针而非拷贝 9

4. 使用 MMAP 而非 READWRITE

确保使用内存映射模式(默认),避免使用 -r 参数: 10

MMAP 模式 (IOTYPE_MMAP) 提供真正的零拷贝,直接访问内核缓冲区。

5. 增大 OutPacketBuffer 大小

位置: inc/DeviceSourceFactory.h:44-47 11

对于 1920×1080@60fps H.264,可能需要更大的缓冲区。系统已自动调整,但可以预先设置:

修改 (在 main.cpp 的服务器创建前):

// 在创建 replicator 之前
OutPacketBuffer::maxSize = 2000000;  // 2MB,适应高码率

6. 禁用日志输出

生产环境中移除 -v 参数,日志 I/O 会增加延迟: 12

7. 使用非阻塞模式

确保不使用 -B 参数,保持非阻塞模式: 13

非阻塞模式 (O_NONBLOCK) 避免 I/O 等待。

8. 优化 H.264 解析

位置: H.264 解析在 H264_V4L2DeviceSource::splitFrames() 中进行 14

使用 -c 参数已经禁用了配置帧重复,这是最优配置。系统会自动检测 H.264 格式并使用专用源: 15

完整优化命令

v4l2rtspserver -f H264 -W 1920 -H 1080 -F 60 -P 8554 -Q 1 -c -t 5 /dev/video0 &

参数说明:
-Q 1: 最小队列
-c: 禁用配置重复
-t 5: 降低 RTCP 超时到 5 秒

性能验证

使用详细日志测量实际延迟:

v4l2rtspserver -vv -f H264 -W 1920 -H 1080 -F 60 -P 8554 -Q 1 -c /dev/video0

查看 diff: 字段显示的毫秒延迟。 16

Notes

  • 系统已经实现了零拷贝设计,通过 MMAP 和指针传递实现
  • 主要优化点是队列大小和轮询超时
  • 60fps 高帧率下,网络带宽和客户端解码能力也是关键因素
  • 建议客户端使用 ffplay -fflags nobuffer -flags low_delay 或 VLC 的 --network-caching=0 参数

Wiki pages you might want to explore:
Source Code Organization (mpromonet/v4l2rtspserver)
Architecture (mpromonet/v4l2rtspserver)
Video Capture and Processing (mpromonet/v4l2rtspserver)

Citations

File: main.cpp (L80-80)

    int queueSize = 5;

File: main.cpp (L87-87)

    V4l2IoType ioTypeIn  = IOTYPE_MMAP;

File: main.cpp (L89-89)

    int openflags = O_RDWR | O_NONBLOCK; 

File: main.cpp (L95-95)

    bool repeatConfig = true;

File: src/V4L2DeviceSource.cpp (L100-106)

    while (!stop) 
    {
        int fd = m_device->getFd();
        FD_SET(fd, &fdset);
        tv.tv_sec=1;
        tv.tv_usec=0;   
        int ret = select(fd+1, &fdset, NULL, NULL, &tv);

File: src/V4L2DeviceSource.cpp (L164-167)

            timeval diff;
            timersub(&curTime,&(frame->m_timestamp),&diff);

            LOG(DEBUG) << "deliverFrame\ttimestamp:" << curTime.tv_sec << "." << curTime.tv_usec << "\tsize:" << fFrameSize <<"\tdiff:" <<  (diff.tv_sec*1000+diff.tv_usec/1000) << "ms\tqueue:" << m_captureQueue.size();      

File: src/V4L2DeviceSource.cpp (L241-262)

void V4L2DeviceSource::processFrame(char * frame, int frameSize, const timeval &ref) 
{
    timeval tv;
    gettimeofday(&tv, NULL);                                                
    timeval diff;
    timersub(&tv,&ref,&diff);

    std::list< std::pair<unsigned char*,size_t> > frameList = this->splitFrames((unsigned char*)frame, frameSize);
    while (!frameList.empty())
    {
        std::pair<unsigned char*,size_t>& item = frameList.front();
        size_t size = item.second;
        char* allocatedBuffer = NULL;
        if (frameList.size() == 1) {
            // last frame will release buffer
            allocatedBuffer = frame;
        }
        queueFrame((char*)item.first,size,ref,allocatedBuffer);
        frameList.pop_front();

        LOG(DEBUG) << "queueFrame\ttimestamp:" << ref.tv_sec << "." << ref.tv_usec << "\tsize:" << size <<"\tdiff:" <<  (diff.tv_sec*1000+diff.tv_usec/1000) << "ms";       
    }           

File: src/V4L2DeviceSource.cpp (L268-279)

    pthread_mutex_lock (&m_mutex);
    while (m_captureQueue.size() >= m_queueSize)
    {
        LOG(DEBUG) << "Queue full size drop frame size:"  << (int)m_captureQueue.size() ;       
        delete m_captureQueue.front();
        m_captureQueue.pop_front();
    }
    m_captureQueue.push_back(new Frame(frame, frameSize, tv, allocatedBuffer)); 
    pthread_mutex_unlock (&m_mutex);

    // post an event to ask to deliver the frame
    envir().taskScheduler().triggerEvent(m_eventTriggerId, this);

File: inc/V4L2DeviceSource.h (L36-47)

        struct Frame
        {
            Frame(char* buffer, int size, timeval timestamp, char * allocatedBuffer = NULL) : m_buffer(buffer), m_size(size), m_timestamp(timestamp), m_allocatedBuffer(allocatedBuffer) {};
            Frame(const Frame&);
            Frame& operator=(const Frame&);
            ~Frame()  { delete [] m_allocatedBuffer; };

            char* m_buffer;
            unsigned int m_size;
            timeval m_timestamp;
            char* m_allocatedBuffer;
        };

File: inc/DeviceSourceFactory.h (L24-26)

            if (format == V4L2_PIX_FMT_H264)
            {
                source = H264_V4L2DeviceSource::createNew(*env, devCapture, outfd, queueSize, captureMode, repeatConfig, false);

File: inc/DeviceSourceFactory.h (L44-47)

                // extend buffer size if needed
                if (devCapture->getBufferSize() > OutPacketBuffer::maxSize)
                {
                    OutPacketBuffer::maxSize = devCapture->getBufferSize();

File: inc/H264_V4l2DeviceSource.h (L30-30)

        virtual std::list< std::pair<unsigned char*,size_t> > splitFrames(unsigned char* frame, unsigned frameSize);
./v4l2rtspserver -Q 1 -c /dev/video0

-Q 1: 将帧队列设置为 1,最小化缓冲延迟
-c: 禁用 SPS/PPS 配置帧重复,减少数据量

您的需求是使用 v4l2rtspserver 从 UVC H.264 摄像头读取并转发视频数据,无需重新编码,并实现最低延迟。

核心配置

v4l2rtspserver 已经原生支持 H.264 直通(passthrough)模式,无需重新编码。 1 系统会自动检测 H.264 格式并使用 H264_V4L2DeviceSource 进行处理。 2

推荐的启动命令

./v4l2rtspserver -Q 1 -c /dev/video0

参数说明:
-Q 1: 将帧队列设置为 1,最小化缓冲延迟 3
-c: 禁用 SPS/PPS 配置帧重复,减少数据量 4

代码层面的优化

1. 降低 select() 超时时间

src/V4L2DeviceSource.cpp 的捕获线程中,select() 超时设置为 1 秒: 5

修改建议: 将 tv.tv_sec=1 改为 tv.tv_sec=0tv.tv_usec=10000 (10ms),减少轮询延迟:

tv.tv_sec=0;
tv.tv_usec=10000;  // 10ms

2. 禁用配置帧重复逻辑

H.264 处理中,系统会在 IDR 帧前插入 SPS/PPS: 6

使用 -c 参数可以禁用此功能,但如果需要在代码层面强制禁用,可以在创建 H264_V4L2DeviceSource 时将 repeatConfig 参数设为 false: 7

3. 队列管理优化

当队列满时,系统会丢弃最旧的帧: 8

对于超低延迟场景,可以考虑修改队列逻辑,在队列有数据时立即触发发送,而不是等待队列填满。

H.264 数据流处理

v4l2rtspserver 对 H.264 的处理流程:

  1. NAL 单元解析: 系统会解析 H.264 NAL 单元,识别 SPS(类型 7)、PPS(类型 8)和 IDR 帧(类型 5) 9

  2. SDP 参数生成: 自动提取 SPS/PPS 并生成 Base64 编码的 SDP 参数 10

  3. RTP 封装: 使用 Live555 的 H264VideoRTPSink 进行 RTP 封装 11

整个过程不涉及重新编码,只是对 H.264 码流进行解析和 RTP 封装。

验证延迟

启用详细日志查看实际延迟:

./v4l2rtspserver -vv -Q 1 -c /dev/video0

日志会显示每帧的时间戳差异: 12

Notes

  • v4l2rtspserver 原生支持 H.264 直通,无需任何编码配置
  • 主要延迟来源是帧队列缓冲和网络传输
  • 客户端播放器的缓冲策略也会影响端到端延迟,建议使用低延迟播放器如 VLC (使用 --network-caching=0 参数)
  • UVC 摄像头本身的编码延迟无法通过 v4l2rtspserver 优化

Wiki pages you might want to explore:
Video Capture and Processing (mpromonet/v4l2rtspserver)
Format Support and Codec Handling (mpromonet/v4l2rtspserver)

Citations

File: inc/H264_V4l2DeviceSource.h (L18-23)

class H264_V4L2DeviceSource : public H26X_V4L2DeviceSource
{
    public:             
        static H264_V4L2DeviceSource* createNew(UsageEnvironment& env, DeviceInterface * device, int outputFd, unsigned int queueSize, CaptureMode captureMode, bool repeatConfig, bool keepMarker) {
            return new H264_V4L2DeviceSource(env, device, outputFd, queueSize, captureMode, repeatConfig, keepMarker);
        }

File: inc/DeviceSourceFactory.h (L22-27)

        static FramedSource* createFramedSource(UsageEnvironment* env, int format, DeviceInterface* devCapture, int queueSize = 5, V4L2DeviceSource::CaptureMode captureMode = V4L2DeviceSource::CAPTURE_INTERNAL_THREAD, int outfd = -1, bool repeatConfig = true) {
            FramedSource* source = NULL;
            if (format == V4L2_PIX_FMT_H264)
            {
                source = H264_V4L2DeviceSource::createNew(*env, devCapture, outfd, queueSize, captureMode, repeatConfig, false);
            }

File: inc/V4L2DeviceSource.h (L114-120)

        std::list<Frame*> m_captureQueue;
        Stats m_in;
        Stats m_out;
        EventTriggerId m_eventTriggerId;
        int m_outfd;
        DeviceInterface * m_device;
        unsigned int m_queueSize;

File: src/H264_V4l2DeviceSource.cpp (L37-60)

        switch (frameType&0x1F)                 
        {
            case 7: LOG(INFO) << "SPS size:" << size << " bufSize:" << bufSize; m_sps.assign((char*)buffer,size); m_pps.clear(); break;
            case 8: LOG(INFO) << "PPS size:" << size << " bufSize:" << bufSize; m_pps.assign((char*)buffer,size); break;
            case 5: LOG(INFO) << "IDR size:" << size << " bufSize:" << bufSize; 
                if (m_repeatConfig && !m_sps.empty() && !m_pps.empty())
                {
                    frameList.push_back(std::pair<unsigned char*,size_t>((unsigned char*)m_sps.c_str(), m_sps.size()));
                    frameList.push_back(std::pair<unsigned char*,size_t>((unsigned char*)m_pps.c_str(), m_pps.size()));
                }
                if (!m_sps.empty() && !m_pps.empty()) {
                    pthread_mutex_lock (&m_lastFrameMutex);
                    m_lastFrame.assign(H264marker, sizeof(H264marker));
                    m_lastFrame.append(m_sps.c_str(), m_sps.size());
                    m_lastFrame.append(H264marker, sizeof(H264marker));
                    m_lastFrame.append(m_pps.c_str(), m_pps.size());
                    m_lastFrame.append(H264marker, sizeof(H264marker));
                    m_lastFrame.append((char*)buffer, size);
                    pthread_mutex_unlock (&m_lastFrameMutex);
                }
            break;
            default: 
                break;
        }

File: src/H264_V4l2DeviceSource.cpp (L64-77)

            u_int32_t profile_level_id = 0;                 
            if (m_sps.size() >= 4) profile_level_id = (((unsigned char)m_sps[1])<<16)|(((unsigned char)m_sps[2])<<8)|((unsigned char)m_sps[3]); 

            char* sps_base64 = base64Encode(m_sps.c_str(), m_sps.size());
            char* pps_base64 = base64Encode(m_pps.c_str(), m_pps.size());       

            std::ostringstream os; 
            os << "profile-level-id=" << std::hex << std::setw(6) << std::setfill('0') << profile_level_id;
            os << ";sprop-parameter-sets=" << sps_base64 <<"," << pps_base64;
            m_auxLine.assign(os.str());

            delete [] sps_base64;
            delete [] pps_base64;
        }

File: src/V4L2DeviceSource.cpp (L102-106)

        int fd = m_device->getFd();
        FD_SET(fd, &fdset);
        tv.tv_sec=1;
        tv.tv_usec=0;   
        int ret = select(fd+1, &fdset, NULL, NULL, &tv);

File: src/V4L2DeviceSource.cpp (L142-152)

        pthread_mutex_lock (&m_mutex);
        if (m_captureQueue.empty())
        {
            LOG(DEBUG) << "Queue is empty";     
        }
        else
        {               
            timeval curTime;
            gettimeofday(&curTime, NULL);           
            Frame * frame = m_captureQueue.front();
            m_captureQueue.pop_front();

File: src/V4L2DeviceSource.cpp (L164-167)

            timeval diff;
            timersub(&curTime,&(frame->m_timestamp),&diff);

            LOG(DEBUG) << "deliverFrame\ttimestamp:" << curTime.tv_sec << "." << curTime.tv_usec << "\tsize:" << fFrameSize <<"\tdiff:" <<  (diff.tv_sec*1000+diff.tv_usec/1000) << "ms\tqueue:" << m_captureQueue.size();      

File: src/ServerMediaSubsession.cpp (L55-58)

    else if (format == "video/H264")
        {
        videoSink = H264VideoRTPSink::createNew(env, rtpGroupsock,rtpPayloadTypeIfDynamic);
    }