You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于pion/webrtc转发H264直播流至浏览器播放异常问题

H264直播流转WebRTC播放异常问题修复方案

基于rtsp-simple-server与pion/webrtc库实现直播流转发至WebRTC时,浏览器播放出现异常。以下是问题代码及针对性修复建议:

原始问题代码

func (c *webrtcSession) runRead(ctx context.Context) error {
    c.ringBuffer, _ = ringbuffer.New(uint64(c.readBufferCount))
    h264FrameDuration := time.Millisecond * 33
    for {
        item, ok := c.ringBuffer.Pull()
        data := item.(*data)
        if c.videoTrack != nil && data.trackID == c.videoTrackID 
        {
            if data.h264NALUs == nil {
                continue
            }
            outBuf := []byte{}
            for _, nalu := range data.h264NALUs {
                outBuf = append(outBuf, []byte{0x00, 0x00, 0x00, 0x01}...)
                outBuf = append(outBuf, nalu...)
            }
            c.videoTrack.WriteSample(media.Sample{Data: outBuf, Duration:time.Duration(h264FrameDuration)})
        }
    }
}

核心问题点及修复措施

1. 未处理RingBuffer拉取失败与类型断言安全

原始代码未判断ringBuffer.Pull()的ok状态,也未校验item转*data的合法性,容易引发panic或无限循环。
修复:添加状态判断与类型校验:

item, ok := c.ringBuffer.Pull()
if !ok {
    return fmt.Errorf("ringbuffer closed")
}
data, ok := item.(*data)
if !ok {
    continue
}

2. 固定帧时长导致时序混乱

硬编码33ms帧时长,若原始流帧率不是30fps,会导致浏览器解码时序错误,出现卡顿、跳帧。
修复:从原始流获取真实帧率计算时长,或使用RTP时间戳转换:

// 示例:按30fps计算,建议从流元数据获取真实帧率
frameRate := 30
h264FrameDuration := time.Second / time.Duration(frameRate)

3. 缺少解码器初始化所需的SPS/PPS

浏览器WebRTC解码器需要先接收SPS(序列参数集)和PPS(图像参数集)才能正确解码IDR帧,原始代码未确保该逻辑。
修复:缓存并优先发送SPS/PPS,或在IDR帧前补充发送:

// 遍历NALU时标记类型
hasIDR := false
hasSPSPPS := false
for _, nalu := range data.h264NALUs {
    if len(nalu) < 1 {
        continue
    }
    naluType := nalu[0] & 0x1F
    switch naluType {
    case 7, 8: // SPS/PPS
        hasSPSPPS = true
        // 可选:缓存SPS/PPS备用
        // cacheSPSPPS(nalu, naluType)
    case 5: // IDR帧
        hasIDR = true
    }
    outBuf = append(outBuf, []byte{0x00,0x00,0x00,0x01}...)
    outBuf = append(outBuf, nalu...)
}

// 若IDR帧未携带SPS/PPS,补充发送缓存的参数集
if hasIDR && !hasSPSPPS {
    // sps, pps := getCachedSPSPPS()
    // if sps != nil && pps != nil {
    //     spsPPSBuf := append([]byte{0x00,0x00,0x00,0x01}, sps...)
    //     spsPPSBuf = append(spsPPSBuf, []byte{0x00,0x00,0x00,0x01}...)
    //     spsPPSBuf = append(spsPPSBuf, pps...)
    //     _ = c.videoTrack.WriteSample(media.Sample{Data: spsPPSBuf, Duration: 0})
    // }
}

4. 未监听上下文取消

循环未响应ctx.Done(),导致session关闭时无法退出,引发资源泄漏。
修复:在循环内添加上下文监听:

for {
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }
    // 后续拉取数据逻辑
}

修复后完整代码示例

import "fmt"

func (c *webrtcSession) runRead(ctx context.Context) error {
    ringBuf, err := ringbuffer.New(uint64(c.readBufferCount))
    if err != nil {
        return err
    }
    c.ringBuffer = ringBuf

    // 建议从直播流元数据获取真实帧率,此处为示例值
    frameRate := 30
    h264FrameDuration := time.Second / time.Duration(frameRate)

    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }

        item, ok := c.ringBuffer.Pull()
        if !ok {
            return fmt.Errorf("ringbuffer closed unexpectedly")
        }

        data, ok := item.(*data)
        if !ok {
            continue
        }

        if c.videoTrack == nil || data.trackID != c.videoTrackID {
            continue
        }

        if data.h264NALUs == nil || len(data.h264NALUs) == 0 {
            continue
        }

        outBuf := []byte{}
        hasIDR := false
        hasSPSPPS := false

        for _, nalu := range data.h264NALUs {
            if len(nalu) < 1 {
                continue
            }
            naluType := nalu[0] & 0x1F
            switch naluType {
            case 7: // SPS
            case 8: // PPS
                hasSPSPPS = true
            case 5: // IDR
                hasIDR = true
            }
            outBuf = append(outBuf, []byte{0x00, 0x00, 0x00, 0x01}...)
            outBuf = append(outBuf, nalu...)
        }

        // 确保解码器初始化所需的SPS/PPS已发送
        if hasIDR && !hasSPSPPS {
            // 此处需补充缓存的SPS/PPS发送逻辑,示例代码需根据实际场景实现
            // sps, pps := getCachedSPSPPS()
            // if sps != nil && pps != nil {
            //     spsPPSBuf := append([]byte{0x00,0x00,0x00,0x01}, sps...)
            //     spsPPSBuf = append(spsPPSBuf, []byte{0x00,0x00,0x00,0x01}...)
            //     spsPPSBuf = append(spsPPSBuf, pps...)
            //     if err := c.videoTrack.WriteSample(media.Sample{Data: spsPPSBuf, Duration: 0}); err != nil {
            //         return err
            //     }
            // }
        }

        if err := c.videoTrack.WriteSample(media.Sample{Data: outBuf, Duration: h264FrameDuration}); err != nil {
            return err
        }
    }
}

内容的提问来源于stack exchange,提问作者sureone

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 05:30:59