如何将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
相关产品推荐
相关产品推荐

