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

如何将SIPSorcery接收的WebRTC视频帧转存为WebM/MP4文件?

解决方案:SIPSorcery接收WebRTC视频帧并保存为WebM/MP4

核心认知:你收到的帧不需要先解码

OnVideoFrameReceived回调拿到的是编码后的原始媒体帧(VP8帧或H264的NAL单元),直接用这些数据封装成对应容器格式即可,无需先解码(解码反而会增加性能开销,除非你需要处理原始像素数据比如保存图片)。


1. 保存VP8帧为WebM文件

WebM原生支持VP8编码,推荐用FFmpeg.AutoGen库完成容器封装:

步骤

  • 在PeerConnection协商完成、拿到视频分辨率/帧率后,初始化WebM输出上下文
  • 在SaveFrameToFullRecordingStream中,将收到的VP8帧写入输出流
  • 录制结束时写入文件尾并释放资源

简化代码示例

private AVFormatContext* _outputFormatContext;
private AVStream* _videoStream;
private long _frameCount = 0;

// 初始化WebM输出(需传入实际视频参数)
private void InitWebMOutput(string filePath, int width, int height, int fps)
{
    ffmpeg.av_register_all();
    ffmpeg.avformat_alloc_output_context2(&_outputFormatContext, null, "webm", filePath);
    
    _videoStream = ffmpeg.avformat_new_stream(_outputFormatContext, null);
    var codecCtx = _videoStream->codec;
    codecCtx->codec_id = AVCodecID.AV_CODEC_ID_VP8;
    codecCtx->codec_type = AVMediaType.AVMEDIA_TYPE_VIDEO;
    codecCtx->width = width;
    codecCtx->height = height;
    codecCtx->time_base = new AVRational { num = 1, den = fps };
    
    if ((_outputFormatContext->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0)
    {
        ffmpeg.avio_open(&_outputFormatContext->pb, filePath, ffmpeg.AVIO_FLAG_WRITE);
    }
    
    ffmpeg.avformat_write_header(_outputFormatContext, null);
}

private void SaveFrameToFullRecordingStream(IPEndPoint arg1, uint arg2, byte[] frame, VideoFormat format)
{
    if (format != VideoFormatEnum.VP8) return;
    
    var pkt = ffmpeg.av_packet_alloc();
    ffmpeg.av_new_packet(pkt, frame.Length);
    Marshal.Copy(frame, 0, pkt->data, frame.Length);
    
    pkt->stream_index = _videoStream->index;
    pkt->pts = ffmpeg.av_rescale_q(_frameCount++, _videoStream->codec->time_base, _videoStream->time_base);
    
    ffmpeg.av_interleaved_write_frame(_outputFormatContext, pkt);
    ffmpeg.av_packet_free(&pkt);
}

// 结束录制时调用
private void FinishWebMRecording()
{
    ffmpeg.av_write_trailer(_outputFormatContext);
    if ((_outputFormatContext->oformat->flags & ffmpeg.AVFMT_NOFILE) == 0)
    {
        ffmpeg.avio_close(_outputFormatContext->pb);
    }
    ffmpeg.avformat_free_context(_outputFormatContext);
}

2. 保存H264帧为MP4文件

H264需要先提取关键帧中的SPS/PPS参数集,再用MP4Sharp这类轻量库封装:

关键要点

  • 从H264 NAL单元中识别并提取SPS(类型7)、PPS(类型8)
  • 确保写入帧时按时间戳顺序,关键帧优先写入参数集

简化代码示例

private MP4File _mp4File;
private Track _videoTrack;

// 初始化MP4输出(需传入SPS/PPS和视频参数)
private void InitMP4Output(string filePath, int width, int height, byte[] sps, byte[] pps)
{
    _mp4File = MP4File.Create(filePath);
    _videoTrack = _mp4File.AddH264VideoTrack(width, height, 1.0 / 30); // 30fps示例
    _videoTrack.AddSPS(sps);
    _videoTrack.AddPPS(pps);
}

private void SaveFrameToFullRecordingStream(IPEndPoint arg1, uint arg2, byte[] frame, VideoFormat format)
{
    if (format != VideoFormatEnum.H264) return;
    
    // 判断是否为关键帧(H264 NAL类型5为关键帧)
    bool isKeyFrame = (frame[0] & 0x1F) == 5;
    
    // 写入帧,时间戳按帧率计算
    _videoTrack.WriteSample(frame, isKeyFrame, 1.0 / 30);
}

// 结束录制时调用
private void FinishMP4Recording()
{
    _mp4File.Dispose();
}

3. 保存为图片(需解码帧)

如果要保存单帧图片,才需要用到OnVideoSinkDecodedSample回调,这里拿到的是解码后的YUV帧,可转成Bitmap保存:

private void VideoSink_OnVideoSinkDecodedSample(uint arg1, VideoSample sample)
{
    var bitmap = new Bitmap(sample.Width, sample.Height, PixelFormat.Format24bppRgb);
    var rect = new Rectangle(0, 0, sample.Width, sample.Height);
    var bmpData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb);
    
    // 调用YUV转RGB工具方法(可自行实现或用第三方库)
    ConvertNV12ToRGB(sample.RawSample, bmpData.Scan0, sample.Width, sample.Height);
    
    bitmap.UnlockBits(bmpData);
    bitmap.Save($"frame_{DateTime.Now.Ticks}.png", ImageFormat.Png);
}

常见问题排查

  • OnVideoSinkDecodedSample无回调:确认已正确初始化VideoSink,且PeerConnection视频流协商成功
  • 视频无法播放:检查容器封装参数(分辨率、帧率、编码格式)是否与接收帧匹配,确保SPS/PPS已正确写入
  • 帧顺序混乱:WebRTC乱序传输的帧会被SIPSorcery自动排序,封装时只需按时间戳写入即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 22:55:07