FFmpeg Idioms 2

Posted on | 3257 words | ~7 mins
Linux FFmpeg Video

上一篇文章中,我们已经熟悉了FFmpeg相关的基本概念。这篇中,我们就介绍一些常见的FFmpeg用法,比如:格式转码,抽取关键帧,分段录制rtsp流等。

1. 解封装和编解码

将mov文件转为mp4文件

1$ ffmpeg -i a.mov a.mp4

等同于使用-f参数指定输出媒体类型(封装格式)

1$ ffmpeg -i a.move -f mp4 a.dat

-f支持的格式,可用如下命令获得:

 1$ ffmpeg -formats
 2ffmpeg version N-97037-gba698a23c6 Copyright (c) 2000-2020 the FFmpeg developers
 3built with gcc 7 (Ubuntu 7.5.0-3ubuntu1~18.04)
 4configuration: --prefix=/usr/local --enable-shared --disable-optimizations --disable-x86asm --enable-libx264 --enable-gpl --disable-stripping --enable-debug=3 --extra-cflags=-gstabs+
 5libavutil      56. 42.101 / 56. 42.101
 6libavcodec     58. 76.100 / 58. 76.100
 7libavformat    58. 42.100 / 58. 42.100
 8libavdevice    58.  9.103 / 58.  9.103
 9libavfilter     7. 77.100 /  7. 77.100
10libswscale      5.  6.101 /  5.  6.101
11libswresample   3.  6.100 /  3.  6.100
12libpostproc    55.  6.100 / 55.  6.100
13File formats:
14D. = Demuxing supported
15.E = Muxing supported
16--
17D  3dostr          3DO STR
18...
19E mov             QuickTime / MOV
20D  mov,mp4,m4a,3gp,3g2,mj2 QuickTime / MOV
21E mp2             MP2 (MPEG audio layer 2)
22DE mp3             MP3 (MPEG audio layer 3)
23E mp4             MP4 (MPEG-4 Part 14)
24D  mpc             Musepack
25D  mpc8            Musepack SV8
26DE mpeg            MPEG-1 Systems / MPEG program stream
27E mpeg1video      raw MPEG-1 video
28E mpeg2video      raw MPEG-2 video
29DE mpegts          MPEG-TS (MPEG-2 Transport Stream)
30D  mpegtsraw       raw MPEG-TS (MPEG-2 Transport Stream)
31D  mpegvideo       raw MPEG video
32DE mpjpeg          MIME multipart JPEG    

如果只想查看支持的muxer或者demuxer,可以分别使用ffmpeg -muxersffmpeg -demuxers。若需要知道当前机器支持哪些编解码器,可用如下命令:

 1# 显示编解码器
 2$ ffmpeg -codecs 
 3
 4# 只显示解码器
 5$ ffmpeg -decoders 
 6
 7# 只显示编码器
 8$ ffmpeg -encoders 
 9
10# 帧图像的像素格式
11# ffmpeg -pix_fmts 

除了以上和格式相关的内容,还有一些其他信息可以输出,例如:

1$ ffmpeg -devices # 显示所有ffmpeg可使用的获取媒体或者输出媒体的设备,例如:v4l2

不过不同的device会有不同的参数,以及不同的格式支持。可用ffplay(FFmpeg中的另一个工具)来查看设备支持的格式,例如:

1$ ffplay -f video4linux2 -list_formats all /dev/video0 # 查看第一个摄像头支持的分辨率
2[video4linux2,v4l2 @ 0x7f0c94000b80] Compressed:       mjpeg :          Motion-JPEG : 1280x720 320x180 320x240 352x288 424x240 640x360 640x480 848x480 960x540
3[video4linux2,v4l2 @ 0x7f0c94000b80] Raw       :     yuyv422 :           YUYV 4:2:2 : 1280x720 320x180 320x240 352x288 424x240 640x360 640x480 848x480 960x540

2. 流选择

mp4,mov,mkv是媒体封装格式,封装了各种不同的流,例如:视频流,音频流,字幕,甚至自定义流。我们在转存,切割等操作时,可以使用-map参数来选择部分流处理。

1$ ffmpeg -i a.mkv a.mov

当我们执行上述命令时,其实并没有把a.mkv中所有的流都输出到a.mov, 而是从a.mkv中找到一个最高质量的视频流和一个最高质量的音频流,保存到a.mov。借用 FFmpeg Wiki Map <http://trac.ffmpeg.org/wiki/Map>_ 上的例子,对-map进一步讲解一下。假设a.mkv中包含多个流:

  1. #0:0 h264视频流
  2. #0:1 德语语音流
  3. #0:2 英语语音流
  4. #0:3 中文字幕
  5. #0:4 英文字幕

FFmpeg支持多个输入媒体和多个输出媒体,因此**#0:0中第一个0指第一个输入媒体,第二个0**指第一个流

如果我们希望得到一个包含英语语音和中文字幕的视频文件,且英语语音需要输出两种不同的格式,128kbps的mp3和96kbps的aac,则:

1$ ffmpeg -i a.mkv -map 0:0 -map 0:2 -map 0:2 -map 0:3 -c:v copy -c:a:0 libmp3lame -b:a:0 128k -c:a:1 libfaac -b:a:1 96k -c:s copy output.mkv

一些要点:

  • 因为不需要德语语音流和英文字幕,命令行中没有出现-map 0:1-map 0:4
  • 因为要输出两个英语语音流,命令行中出现了两次-map 0:2
  • output.mkv中流的顺序和-map出现的顺序一致,故最终输出文件中流依次是h264视频流,mp3格式英文语音流,acc格式英语语音流,中文字幕 --c:v copy等同于-c:v:0 copy,-codec:v copy,-vcodec copy, 在这个例子中的作用是:第一个视频输出流无需编解码,直接copy-map 0:0中的内容 --c:a:0 libmp3lame第一个音频输出流转为mp3格式 --b:a:0 128k第一个音频输出流按128kbps码率进行输出

有了 -map, 我们可以做很多事情:

1# 将流倒序输出
2$ ffmpeg -i a.mkv -map 0:4 -map 0:3 -map 0:2 -map 0:1 -map 0:0 -c copy output.mkv 
3
4# 仅输出视频流
5$ ffmpeg -i a.mkv -map 0:0 -c copy output.mkv 
6
7# 复制所有的流,但是音频流全部转为acc格式
8$ ffmpeg -i a.mkv -map 0 -c copy -c:a libfaac output.mkv 

3. 视频文件切割

忽略所有细节,最简单粗暴的方法就是

1$ ffmpeg -ss 00:01:00 -i a.mp4 -to 00:02:00 output.mp4

以上命令,从a.mp4中切割出一段1分钟长的视频,保存到output.mp4,其中-ss表示起始时间,-to表示结束时间。

1$ ffmpeg -ss 00:01:00 -i a.mp4 -t 00:01:00 output.mp4

和上面一条命令输出相同,只不过使用了 -i来指定输出的视频长度,而不是结尾的时间点。

但是以上操作会解码之后再编码,导致过程会很耗时间。所以一般会使用-c copy直接将流原封不同的复制到输出流。

1$ ffmpeg -ss 00:01:00 -i a.mp4 -t 00:01:00 -c copy output.mp4

因为不解码,所以起始时间00:01:00这个时间点上的数据不一定是I帧,结尾时间00:02:00这个时间点上的数据有可能是B帧,如下图:

GOP

当output.mp4解码播放时,因为第一帧不是I,最后一帧是B帧,导致开始和结尾有些帧无法完成解码,出现“黑屏”现象。那么如何既快速切割,又避免“黑屏”影响呢?一共有三个方案:

3.1. 方案1

为了确保00:01:00到00:02:00之间的视频没有黑屏,可以设置-ss为更早的时间,设置-t为更大的时间跨度。如下图,在00:00:30开始切割,并在00:02:30结束切割:

GOP

因为第一帧依旧不是I,最后一帧依旧是B,所以还是会有黑屏。不过在第一帧到00:01:00间有个I帧,这样00:01:00开始的帧会被正确解码同时00:02:00到最后一帧之间有个P帧,也能确保00:02:00可以正确解码。这个方法没有根本上解决问题,不能避免“黑屏”现象。但是保障了目标时间段内视频解码正常。不失为“简单粗暴”但有效的方法。

3.2. 方案2

对视频处理两遍。第一遍强行在00:01:00和00:02:00两个位置加入I帧。

1$ ffmpeg -i a.mp4 -force_key_frames 00:01:00,00:02:00 -y output.mp4

加入后帧分布如图:

GOP

然后按常规切割方法,得到的视频就不会有黑屏了。但请注意,使用-force_key_frames不要指定太多时间点。该命令会编解码视频,速度会受影响。如果插入I帧的点过多,速度会很慢。

1$ ffmpeg -ss 00:01:00 -i output.mp4 -t 00:01:00 -c copy output2.mp4

3.3. 方案3

类似于方案1。唯一不同的是,先使用ffprobe(和ffplay一样是FFmpeg工具之一)输出所有I帧位置。然后选择00:01:00前面第一个I帧时间点,和00:02:00后面第一个I帧时间点,进行切割。这样切割出的视频和方案1相比,不再有“黑屏”,但因为要跑两遍(第一遍输出I帧位置),所以速度慢于方案1。

 1$ ffprobe -select_streams v -show_frames -show_entries frame=pkt_pts_time,pict_type -v quiet a.mp4
 2[FRAME]
 3media_type=video
 4stream_index=0
 5key_frame=0
 6pkt_pts=10556937
 7pkt_pts_time=117.299300
 8pkt_dts=10556937
 9pkt_dts_time=117.299300
10best_effort_timestamp=10556937
11best_effort_timestamp_time=117.299300
12pkt_duration=3000
13pkt_duration_time=0.033333
14pkt_pos=8791358
15pkt_size=163
16width=1280
17height=720
18pix_fmt=yuvj420p
19sample_aspect_ratio=1:1
20pict_type=P
21coded_picture_number=2347
22display_picture_number=0
23interlaced_frame=0
24top_field_first=0
25repeat_pict=0
26color_range=pc
27color_space=bt709
28color_primaries=bt709
29color_transfer=bt709
30chroma_location=center
31[/FRAME]
32[FRAME]
33media_type=video
34stream_index=0
35key_frame=0
36pkt_pts=10559937
37pkt_pts_time=117.332633
38pkt_dts=10559937
39pkt_dts_time=117.332633
40best_effort_timestamp=10559937
41best_effort_timestamp_time=117.332633
42pkt_duration=6000
43pkt_duration_time=0.066667
44pkt_pos=8791521
45pkt_size=126
46width=1280
47height=720
48pix_fmt=yuvj420p
49sample_aspect_ratio=1:1
50pict_type=P
51coded_picture_number=2348
52display_picture_number=0
53interlaced_frame=0
54top_field_first=0
55repeat_pict=0
56color_range=pc
57color_space=bt709
58color_primaries=bt709
59color_transfer=bt709
60chroma_location=center
61[/FRAME]

输出太多不是很容易找到需要的I帧,增加几个控制参数再试一下

 1$ ffprobe -select_streams v -show_frames -show_entries frame=pkt_pts_time,pict_type -of csv -v quiet 2020-05-02_00-29-59.mp4 | grep I
 2frame,0.000000,I
 3frame,1.533333,I
 4frame,3.133322,I
 5...
 6frame,57.533000,I
 7frame,59.132989,I
 8frame,60.732978,I
 9frame,62.332967,I
10...
11frame,111.932667,I
12frame,113.532656,I
13frame,115.132644,I
14frame,116.732633,I
15frame,118.332633,I
16frame,119.932622,I
17frame,121.532611,I
18frame,123.132600,I
19frame,124.732589,I
20frame,126.332578,I
21frame,127.932567,I
22...

pkt_pts_time参数单位为秒,因为00:01:00 (60)前的第一个I帧是59.132989秒;00:02:00 (120)后的第一个I帧是121.532611。因此提取命令为:

1$ ffmpeg -ss 00:00:59 -i a.mp4 -to 00:02:01 output.mp4

4. 保存视频流

当我们将一个rtsp流保存为视频文件时,通常不进行编解码工作,而是使用流媒体本身的编码直接保存为文件。这样对资源占用最低。

1$ ffmpeg -rtsp_transport tcp -i rtsp://127.0.0.1 -map 0 -c copy dump.mp4
  • -rtsp_transport tcp使用tcp进行视频流读取
  • -c copy将rtsp流内容原封copy下来。该命令对CPU和内存占用率最低 --map 0将整个流都保存下来

不过一般为了方便后续处理,会将视频流按固定时间段大小,存成多个文件

1$ ffmpeg -rtsp_transport tcp -i rtsp://127.0.0.1 -map 0 -c copy -f segment -segment_list list.csv -strftime 1 -segment_time 3600 -segment_format mp4 "%Y-%m-%d_%H-%M-%S.mp4"
  • -f segment对视频流分割保存
  • -segment_list list.csv切割后的文件名都保存到list.csv中
  • -strftime 1切割后的文件名使用时间戳来命名
  • -segment_time 3600每3600秒保存成一个新文件
  • -segment_format mp4输出为mp4文件
  • %Y-%m-%d_%H-%M-%S.mp4保存为文件名类似于2020-05-05_20-12-11.mp4的文件

最后,大家可能会有一个疑问:使用-f segment分割出的视频会不会有“黑屏”现象?即FFmpeg使用非I帧进行切割?

答案是“不会”。这部分核心代码的实现在libavformat/segment.csegment.c方法。从下面的代码片段即可看出,只有在pkt->flags & AV_PKT_FLAG_KEY(即当前packet对应的是关键帧AV_PKT_FLAG_KEY),或者seg->break_non_keyframes(即允许不在关键帧处切割)两个条件至少有一个为真时,才会进行切割操作。

 1if (pkt->stream_index == seg->reference_stream_index &&
 2    (pkt->flags & AV_PKT_FLAG_KEY || seg->break_non_keyframes) &&
 3    (seg->segment_frame_count > 0 || seg->write_empty) &&
 4    (seg->cut_pending || seg->frame_count >= start_frame ||
 5        (pkt->pts != AV_NOPTS_VALUE &&
 6        av_compare_ts(pkt->pts, st->time_base,
 7                    end_pts - seg->time_delta, AV_TIME_BASE_Q) >= 0))) {
 8    /*sanitize end time in case last packet didn't have a defined duration*/
 9    if (seg->cur_entry.last_duration == 0)
10        seg->cur_entry.end_time = (double)pkt->pts*av_q2d(st->time_base);
11
12    if ((ret = segment_end(s, seg->individual_header_trailer, 0)) < 0)
13        goto fail;
14
15    if ((ret = segment_start(s, seg->individual_header_trailer)) < 0)
16        goto fail;