【C#】FFmpeg使用汇总
一、FFmpeg 常规处理流程
为什么使用ffmepg:
(1)vlc延时问题
使用vlc,即便优化参数,也有大概几百毫秒的延时,
string[] options = { ":network-caching=300", ":rtsp -tcp", ":no-audio" };// { ":network-caching=100", ":rtsp -tcp", ":no-audio" };
(2)和EasyPlayer性能对比,1. 稳定运行;2. 断网恢复后自动播放。
二、一些概念
1、DTS(Decoding Time Stamp, 解码时间戳)
表示压缩帧的解码时间。
2、PTS(Presentation Time Stamp, 显示时间戳)
表示将压缩帧解码后得到的原始帧的显示时间。
pts和dts的值指的是占多少个时间刻度(占多少个格子)。它的单位不是秒,而是时间刻度。只有pts与time_base两者结合在一起,才能表达出具体的时间是多少。
3、time_base
时间基(time_base)是时间戳(timestamp)的单位。
时间戳值乘以时间基,可以得到实际的时刻值(以秒等为单位)。
例如,如果一个视频帧的dts是40,pts是160,其time_base是1/1000秒,可算出:
解码时刻:40毫秒(40/1000),
显示时刻:160毫秒(160/1000)。
4、三种时间基
不同的封装格式具有不同的时间基。FFmepg 中有三种时间基:
(1)tbn
对应容器中的时间基(视频流的时间基)。值是 AVStream.time_base 的倒数;
(2)tbc
对应编解码器中的时间基。值是 AVCodecContext.time_base 的倒数;
AVCodecContext.time_base 是帧率(视频帧)的倒数,每帧时间戳递增 1,那么tbc就等于帧率;
编码过程中,应由用户设置好此参数。
解码过程中,此参数已过时,建议直接使用帧率倒数用作时间基。
实际使用时,在视频解码过程中,我们不使用 AVCodecContext.time_base,而用帧率倒数作时间基;
在视频编码过程中,我们将 AVCodecContext.time_base 设置为帧率的倒数。
(3)tbr
从视频流中猜算得到,可能是帧率或场率(帧率的 2 倍)
5、帧率
每秒编码进视频文件的帧数目。人类的眼睛需要每秒至少15帧才能将图像连贯在一起。
6、比特率
决定处理1s的编码流需要多少bit
7、采集顺序、编码顺序
采集顺序与显示顺序相同。编码顺序、传输顺序和解码顺序相同。
三、time_base详解
1、输入流与输出流中的 time_base
AVStream.time_base 是 AVPacket 中pts和dts的时间单位。输入流与输出流中 time_base 按如下方式确定:
(1)对于输入流
打开输入文件后,调用 avformat_find_stream_info()可获取到每个流中的 time_base;
(2)对于输出流
打开输出文件后,调用 avformat_write_header()可根据输出文件封装格式确定每个流的 time_base 并写入输出文件中。
2、封装格式中的 time_base
不同封装格式具有不同的时间基,在转封装(将一种封装格式转换为另一种封装格式)过程中,时间基转换相关代码如下:
av_read_frame(ifmt_ctx, &pkt); pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
下面的代码具有和上面代码相同的效果:
// 从输入文件中读取 packet av_read_frame(ifmt_ctx, &pkt); // 将 packet 中的各时间值从输入流封装格式时间基转换到输出流封装格式时间基 av_packet_rescale_ts(&pkt, in_stream->time_base, out_stream->time_base);
flv 封装格式的 time_base 为{1,1000},ts 封装格式的 time_base 为{1,90000}
对于同一个视频帧,它们时间基(tbn)不同因此时间戳(pkt_pts)也不同,但是计算出来的时刻值(pkt_pts_time)是相同的。
pkt_pts/1000 pkt_pts/90000
3、time_base不匹配的情况
AVFrame->pts和AVPacket->pts、AVPacket->dts的值,在解码/编码后,会经历短暂的time_base不匹配的情况:
(1)解码后,decoded_frame->pts的值使用AVStream->time_base为单位,后在AVFilter里面转换成以AVCodecContext->time_base为单位。
(2)编码后,pkt.pts、pkt.dts使用AVCodecContext->time_base为单位,后通过调用"av_packet_rescale_ts"转换为AVStream->time_base为单位。
4、解码时时间基转换
容器(文件层)中的时间基(AVStream.time_base)与编解码器上下文(视频层)里的时间基(AVCodecContex.time_base)不一样,解码编码过程中需要进行时间基转换。
(1)视频
视频按帧进行播放,所以原始视频帧时间基为 1/framerate。
视频解码前需要处理输入 AVPacket 中各时间参数,将输入容器中的时间基转换为 1/framerate 时间基;
视频编码后再处理输出 AVPacket 中各时间参数,将 1/framerate 时间基转换为输出容器中的时间基。
(2)音频
音频按采样点进行播放,所以原始音频帧时间为 1/sample_rate。
音频解码前需要处理输入 AVPacket 中各时间参数,将输入容器中的时间基转换为 1/sample_rate 时间基;
音频编码后再处理输出 AVPacket 中各时间参数,将 1/sample_rate 时间基转换为输出容器中的时间基。
如果引入音频 FIFO,从 FIFO 从读出的音频帧时间戳信息会丢失,需要使用 1/sample_rate 时间基重新为每一个音频帧生成 pts,然后再送入编码器。
5、时间相关参数学习链接
(1)关于FFmpeg编解码中涉及到的pts总结(关于时间戳和时间基)
(2)ffmpeg转码
(3)FFmpeg DTS、PTS和时间戳TIME_BASE详解
(4)FFMPEG中的pts与音视频同步的关系以及编解码过程中的注意事项
四、实例
1、用FFmpeg获取视频流+音频流的信息
https://zhuanlan.zhihu.com/p/577624174
2、存储
(1)使用FFmpeg接收RTSP流并存为mp4
http://events.jianshu.io/p/99efcfc74728
(2)通过ffmpeg把RGBA数据保存为mp4
https://blog.51cto.com/code/3034118
(3)ffmpeg读取rtsp并保存到mp4文件
https://blog.csdn.net/zhouyongku/article/details/38224045
(4)调用ffmpeg接口,将RTSP流保存为MP4的C代码
如果代码看不懂,参考以下链接了解函数作用:
(1) FFMpeg.AutoGen 讲解官方example代码:Main函数、解码
(2) FFmpeg打开输入文件
(3) FFmpeg解码流程
WPF、Winform的代码我都有,需要的话评论留邮箱。
五、用ffmpeg.exe进程实现BitMap流实时存为avi视频文件
创建类:
public class FfmpegToVideo { public bool _isRunning = false; private int _fps; private readonly Process _proc; public FfmpegToVideo(string filePath, int maxBitRate = 5000) { string ffmpeg = string.Format("{0}ffmpeg.exe", AppDomain.CurrentDomain.BaseDirectory); var formattedPath = Path.GetFullPath(filePath); this.KillProcess("ffmpeg"); _proc = new Process(); ProcessStartInfo startInfo = new ProcessStartInfo(ffmpeg); startInfo.FileName = ffmpeg; //-f image2 -i c:\temp\d.jpg -vcodec libx264 test.mp4 startInfo.Arguments = string.Format("-f image2pipe -use_wallclock_as_timestamps 1 -i - -c:v libx264 -pix_fmt yuv420p -vsync passthrough -maxrate {0}k -an -y {1}", maxBitRate, formattedPath); //startInfo.Arguments = "-f image2pipe -i pipe:.bmp -vcodec libx264 -maxrate {0}k -bt 10 -r {1} -an -y {2}"; //+ rtmp; startInfo.WorkingDirectory = System.AppDomain.CurrentDomain.SetupInformation.ApplicationBase; startInfo.UseShellExecute = false; startInfo.CreateNoWindow = true; startInfo.RedirectStandardInput = true; startInfo.RedirectStandardOutput = true; startInfo.WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden; _proc = System.Diagnostics.Process.Start(startInfo); } public void StartAsync() { _isRunning = true; } /// <summary> /// 停止服务 /// </summary> public void Stop() { _isRunning = false; try { _proc.StartInfo.RedirectStandardInput = false; _proc.StartInfo.RedirectStandardOutput = false; _proc.StandardInput.Close(); _proc.StandardOutput.Close(); _proc.Close(); //this.KillProcess("ffmpeg"); } catch (Exception ex) { } } /// <summary> /// 添加item /// </summary> /// <param name="item"></param> public void Add(Bitmap bitmap) { if (_isRunning) { try { if (_proc.StartInfo.RedirectStandardInput) { using (MemoryStream ms = new MemoryStream()) { bitmap.Save(ms, ImageFormat.Bmp); ms.Seek(0, SeekOrigin.Begin); //ms.WriteTo(_proc.StandardInput.BaseStream); _proc.StandardInput.BaseStream.Write(ms.ToArray(), 0, ms.ToArray().Length); } } } catch { } } } /// <summary> /// 杀掉指定程序进程 /// </summary> /// <param name="processName"></param> private void KillProcess(string processName) { //获得进程对象,以用来操作 System.Diagnostics.Process myproc = new System.Diagnostics.Process(); //得到所有打开的进程 try { //获得需要杀死的进程名 foreach (Process thisproc in Process.GetProcessesByName(processName)) { //立即杀死进程 thisproc.Kill(); } } catch (Exception Exc) { throw new Exception("", Exc); } } }
调用:
FfmpegToVideo FfmpegObj; public void SaveStart(string path) {
//SavePath = @"F:\out.avi"; FfmpegObj = new FfmpegToVideo(path); FfmpegObj.StartAsync(); } public void SaveFinish() { if (FfmpegObj != null) { FfmpegObj.Stop(); } } public void AddFunc(Bitmap CurrentBitMap) { this.Invoke((EventHandler)delegate { //保存文件 if (FfmpegObj._isRunning) { if (CurrentBitMap != null) { FfmpegObj.Add(CurrentBitMap); } } }); } //在要获取BitMap的地方循环调用: AddFunc(CurrentBitMap);
参考1:https://blog.csdn.net/catshitone/article/details/126930470
参考2:https://blog.csdn.net/dgnankai/article/details/130583320
六、问题汇总
1、保存的mp4文件不能用Windows Media Player播放
https://blog.csdn.net/qq_39170782/article/details/111907521
我是直接用数据流解码前的数据存到文件的,格式已经是已经是yuv420p,但还是不能直接用Windows Media Player播放,但用VSPlay、VLC等可以播放,该问题未解决。
2、保存的mp4文件用FFmpeg解码后播放速度变快
但用VSPlay、VLC等播放时速度正常。试过修改时间基、PTS、DTS等参数都无效,最终是用VLC来做视频回放功能了。