【C#】使用ffmpeg image2pipe将图片保存为mp4视频

文章目录

需求

在正式开始之前,先介绍下我的需求是怎么样的,基于此需求如何使用ffmpeg实现。仅供参考。

需求点:

  1. 将图片保存为视频
  2. 图片数量不是固定的,是由上游的webrtc传下的帧数据,转成的bitmap。所以只要webrtc开着,图片流就一直会有。
  3. 每帧图像的间隔时间依赖于不同的网络环境,所以不是固定的时间间隔。

实现

在使用原生ffmpeg之前,笔者使用了几个第三方的nuget库,如:FFmpeg.AutoGen、Xabe.FFmpeg、Accord.Video.FFMPEG。前两个库要么只支持将文件夹里现有的图片保存为mp4,要么不支持设置每帧的PTS,导致生成的mp4播放速度太快。最后选用了Accord.Video.FFMPEG,这个库能满足上述的三个需求点。无奈此库已长期不维护,当上游的FPS>15时,WriteVideoFrame方法抛出异常的频率会大大提升,导致内存泄漏,而且当前帧也会被丢掉。

然后项目使用的是.net452,一时半会版本也升级不上去,这就过滤大多数的nuget库。最后,只能使用的原生的ffmpeg了。
ffmpeg只是提供了一个exe,并没有官方的API可供我们调用,只提供了一大堆的参数说明,真是令人头大。经过不断的看文档和搜索调试之后,发现配置以下参数可以达到我们的需求。

-f image2pipe -use_wallclock_as_timestamps 1 -i - -c:v libx264 -pix_fmt yuv420p -vsync passthrough -maxrate 5000k  -an -y 123.mp4

以下对各参数做个简单介绍:

  • image2pipe:使用图片管道,我们可以将图片数据一直往管道里塞,ffmpeg会不断将其添加到mp4文件中。用来满足需求1和2.
  • use_wallclock_as_timestamps 1:开启此选项,ffmpeg就会将接收此图片的时间作为该帧的timestamp。这样生成的MP4播放速度才正常,满足需求3.
  • pix_fmt yuv420p:设置像素格式,解决生成的视频无法使用windows media player播放的问题。
  • -vsync passthrough:可以理解为动态帧率,根据上游的帧率来决定输出mp4的帧率。默认有以下几个选项:
    • passthrough :使用帧原始的timestamp.
    • cfr (1):根据输出帧率的配置,进行自动插帧(上游帧率小于输入帧率)或者丢帧(上游帧率大于输入帧率)。
    • vfr (2):类似于passthrough, 不过当两帧具有timestamp时会丢弃其中一个。
    • drop:类似于passthrough,只不过会丢弃帧原始的timstamp,然后重新生成符合帧率要求的timestamp。
    • auto (-1):默认行为。在cfr和vfr之前自动选择。
  • maxrate:设置最大比特率
  • 123.mp4:保存的文件名或者路径,注意里面不要有空格。

最后的C#代码如下,我们需要使用Process类来启动ffmpeg.exe。

public class FfmpegToVideoService 
{
        private bool _isRunning = false;
        private int _fps;
        private readonly Process _proc;

        /// <summary>
        /// Bitmap保存为MP4
        /// </summary>
        /// <param name="filePath">mp4要保存的路径,如:D:\\a\b\123.mp4</param>
        /// <param name="maxBitRate">最大比特率,单位K</param>
        public FfmpegToVideoService(string filePath,int maxBitRate = 5000)
        {
            var formattedPath = Path.GetFullPath(filePath);
            _proc = new Process();
            //-pix_fmt yuv420p -movflags +faststart  -r {30}  -i pipe:.bmp -r {_fps} -timecode 00:00:00.000
            //-vsync cfr自动差值 vfr根据timestamp,重复的丢弃    passthrough根据timestamp重复的不丢  -vsync passthrough
            //-r 30 入帧出帧都是30
            _proc.StartInfo.FileName = @"ffmpeg.exe";
            _proc.StartInfo.Arguments = $"-f image2pipe -use_wallclock_as_timestamps 1 -i - -c:v libx264 -pix_fmt yuv420p -vsync passthrough -maxrate {maxBitRate}k  -an -y {formattedPath}";
            _proc.StartInfo.WorkingDirectory = CommonFunctions.BasePath;
            _proc.StartInfo.UseShellExecute = false;
            _proc.StartInfo.RedirectStandardInput = true;
            _proc.StartInfo.RedirectStandardOutput = true;
            _proc.Start();
        }

		// 将Bitmap数据写入管道
        private void SendToPipe(Bitmap bitmap)
        {
            if (_proc.StartInfo.RedirectStandardInput)
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    bitmap.Save(ms, ImageFormat.Png);
                    ms.WriteTo(_proc.StandardInput.BaseStream);
                }
            }
        }

        /// <summary>
        /// 异步线程启动服务
        /// </summary>
        public override void StartAsync()
        {
            _isRunning = true;
        }

        /// <summary>
        /// 停止服务
        /// </summary>
        public override void Stop()
        {
            _isRunning = false;
            try
            {
                _proc.StartInfo.RedirectStandardInput = false;
                _proc.StartInfo.RedirectStandardOutput = false;
                _proc.StandardInput.Close();
                _proc.StandardOutput.Close();
                _proc.Close();
            }
            catch (Exception ex)
            {
                Log.Error(ex, "");
            }
        }

        /// <summary>
        /// 添加item
        /// </summary>
        /// <param name="item"></param>
        public override void Add(FrameInfo item)
        {
            if(_isRunning)
            {
                SendToPipe(item.Bitmap);
            }
        }
    }

  1. https://trac.ffmpeg.org/wiki/Slideshow
  2. https://ffmpeg.org/ffmpeg.html#filter_005foption
  3. https://stackoverflow.com/questions/60977555/adding-current-time-as-timestamp-in-h-264-raw-stream-with-few-frames
  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 11
    评论
你可以使用 FFmpeg.AutoGen 库和 .NET 6.0 来将 MP4 换为视频流。下面是一个简单的示例: ```C# using System; using System.Drawing; using System.Drawing.Imaging; using FFmpeg.AutoGen; class Program { static void Main(string[] args) { // 初始化 FFmpegffmpeg.av_register_all(); ffmpeg.avcodec_register_all(); ffmpeg.avformat_network_init(); // 打开 MP4 文件 var inputUrl = "path/to/your/file.mp4"; var inputFormatContext = OpenInputFile(inputUrl); var videoStreamIndex = FindVideoStreamIndex(inputFormatContext); // 查找视频解码器并打开它 var videoCodec = ffmpeg.avcodec_find_decoder(inputFormatContext->streams[videoStreamIndex]->codecpar->codec_id); var videoCodecContext = ffmpeg.avcodec_alloc_context3(videoCodec); ffmpeg.avcodec_parameters_to_context(videoCodecContext, inputFormatContext->streams[videoStreamIndex]->codecpar); ffmpeg.avcodec_open2(videoCodecContext, videoCodec, null); // 创建一个视频流 var videoStream = new VideoStream(videoCodecContext->width, videoCodecContext->height); // 读取视频帧并将其写入视频流 var packet = ffmpeg.av_packet_alloc(); var frame = ffmpeg.av_frame_alloc(); while (ffmpeg.av_read_frame(inputFormatContext, packet) >= 0) { if (packet->stream_index == videoStreamIndex) { ffmpeg.avcodec_send_packet(videoCodecContext, packet); while (ffmpeg.avcodec_receive_frame(videoCodecContext, frame) == 0) { // 将当前视频帧写入视频流 var bitmap = ConvertFrameToBitmap(frame); var frameData = BitmapToByteArray(bitmap); videoStream.Write(frameData); } } ffmpeg.av_packet_unref(packet); } ffmpeg.av_packet_free(&packet); ffmpeg.av_frame_free(&frame); // 关闭输入文件和视频解码器 ffmpeg.avcodec_close(videoCodecContext); ffmpeg.avcodec_free_context(&videoCodecContext); ffmpeg.avformat_close_input(&inputFormatContext); // 视频流写入完成 videoStream.Complete(); } // 打开输入文件 static AVFormatContext* OpenInputFile(string url) { var formatContext = ffmpeg.avformat_alloc_context(); if (ffmpeg.avformat_open_input(&formatContext, url, null, null) != 0) { throw new Exception("Could not open input file."); } if (ffmpeg.avformat_find_stream_info(formatContext, null) < 0) { throw new Exception("Could not find stream information."); } return formatContext; } // 查找视频流索引 static int FindVideoStreamIndex(AVFormatContext* formatContext) { for (int i = 0; i < formatContext->nb_streams; i++) { if (formatContext->streams[i]->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO) { return i; } } throw new Exception("Could not find video stream."); } // 将视频换为位图 static Bitmap ConvertFrameToBitmap(AVFrame* frame) { var bitmap = new Bitmap(frame->width, frame->height, PixelFormat.Format24bppRgb); var data = bitmap.LockBits(new Rectangle(0, 0, frame->width, frame->height), ImageLockMode.WriteOnly, PixelFormat.Format24bppRgb); ffmpeg.av_image_copy_to_buffer((byte**)data.Scan0, data.Stride * data.Height, frame->data, frame->linesize, (AVPixelFormat)frame->format, frame->width, frame->height, 1); bitmap.UnlockBits(data); return bitmap; } // 将位图换为字节数组 static byte[] BitmapToByteArray(Bitmap bitmap) { var encoder = new JpegBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(bitmap)); using (var stream = new MemoryStream()) { encoder.Save(stream); return stream.ToArray(); } } } ``` 在这个示例中,我们使用 FFmpeg.AutoGen 库来打开 MP4 文件,并从视频流中读取每一帧。对于每一帧,我们将其换为位图,然后将位图换为字节数组,并将其写入视频流中。 请注意,这只是一个简单的示例,你可能需要根据你的具体需求进行修改。另外,由于 FFmpeg.AutoGen 库是一个底层库,需要一定的编程经验才能使用

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

JimCarter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值