FFmpeg 不完全实战

FFmpeg 不完全实战

2 年前 · 来自专栏 超级码力
本文会介绍一些FFmpeg的常用命令(也包含FFplay,FFprobe)。笔者在一开始接触FFmpeg时也是从网上找一些文章来看,但都是零零散散,也不知为什么是这个命令,最近为了完成一些任务啃了啃FFmpeg的文档,总结了一些常用且实用的命令。

阅读之前

本文介绍的内容不是从零开始的,不会教你去安装,也不会特别难,但是有点长。建议阅读本文之前阅读一下以下2篇文章:

关于音视频的格式

如何查看一个媒体文件的详细信息

ffprobe 是一个用来收集媒体文件信息的工具,可以以指定的格式打印出媒体文件信息。你可以用ffprobe查看视频,音频甚至是一张图片的信息。

ffprobe -loglevel quiet -of json -show_format -show_streams video.webm
ffprobe -v quiet -of json -show_format -show_streams video.webm
ffprobe -v 8 -of json -show_format -show_streams video.webm
# -v = -loglevel 日志的级别,常用的参数就是 -v quiet / -v 8,这样就可以获得一个比较干净的输出。
# -of = -print_format 输出的格式,json应该算是可读性比较高的,也可以选择 xml, csv 等。
# -show_format 打印格式信息
# -show_streams 打印流信息

拓展:如何比较两个文件的差异

这里会用到 vimdiff,如果会用 vim 的同学可以尝试一下。

vimdiff 的基本用法是:

vimdiff file1 file2

所以比较两个媒体文件的差异还是比较简单的:

vimdiff <(ffprobe -v quiet -of json -show_format -show_streams video1.mp4) <(ffprobe -v quiet -of json -show_format -show_streams video2.mp4)

常用的音视频格式

阮一峰老师的文章中讲的不错,不再赘述。

常用的视频分辨率

FFmpeg官网中提供了一组分辨率的值:

也有一些文章中提到另外一组分辨率的值:

其实不必太纠结,一般用视频高度就可以了,本文中后面的处理也是这样做的。

常用的音频采样率

目前比较常用的 44,100 Hz 和 48,000 Hz,即 44.1k 和 48k。

  • 22,050 Hz - 无线电广播所用采样率
  • 44,100 Hz - 音频CD,也常用于MPEG-1音频(VCD, SVCD, MP3)所用采样率
  • 48,000 Hz - miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率
  • 96,000或者192,000 Hz - DVD-Audio、一些LPCM DVD音轨、Blu-ray Disc(蓝光光盘)音轨、和HD-DVD(高清晰度DVD)音轨所用所用采样率

一般转码

终于进入正题,开始划重点:

$ ffmpeg \
[全局参数] \
[输入文件参数] \
-i [输入文件] \
[输出文件参数] \
[输出文件]

这个命令结构是从阮一峰老师那里拿过来的,对于一般的转码这个命令结构已经够用了。实际上,ffmpeg支持多个输入,多个输出(这里有个印象就好)。

格式的变换

ffmpeg -i video.webm video.mp4
ffmpeg -y -v quiet -i video.webm video.mp4

将webm格式的视频转换成mp4。很简单是不是。

# -y 表示输出结果文件存在时直接覆盖,不会询问
# -n 表示输出结果文件存在时终止转换
# -v 前面说过,是日志级别,-v quiet 就是什么也不输出,包括 banner
# -hide_banner 可以不输出 banner

加一些参数,指定音频编码,视频编码

ffmpeg -i video.webm -c:a aac -c:v h264 video.mp4
# -c = -codec 指定编码器
# -c:a 指定音频编码 -c:a aac 指定音频为 aac
# -c:v 指定视频编码 -c:v h264 指定视频为 h264
# -c:a copy 直接复制 音频编码
# -c:v copy 直接复制 视频编码
# -c copy 直接复制音频视频编码

分辨率的变换

指定分辨率

ffmpeg -i video.mp4 -vf scale=1280:720 video_1280_720.mp4
ffmpeg -i video.mp4 -vf scale=852:480 video_852_480.mp4
# -vf = -filter:v 这个参数在后面还要用到很多次

当然还可以只指定宽或者高,而对应的高或者宽自动计算出来

ffmpeg -i video.mp4 -vf scale=-1:720 video_1280_720.mp4
ffmpeg -i video.mp4 -vf scale=1280:-1 video_1280_720.mp4
-1 就表示这个值是缺省值,可以自动计算出来

YUV 编码的坑 -1 与 -2

但是这里有个坑,就是yuv编码中,分辨率必须是偶数。所以当缺省的宽或者高计算出来是一个奇数的话,就会报错。这个时候我们可以使用-2来代替-1,这样计算出来的结果会取偶数,实际上,我更推荐从一开始就用-2。

# 比如原始分辨率是 1920 * 1080
ffmpeg -i video.mp4 -vf scale=-1:480 video_480.mp4
# 使用 -1 计算就会报错,因为算出来的是 853 * 480
ffmpeg -i video.mp4 -vf scale=-2:480 video_480.mp4
# 使用 -2 计算就不会报错,因为算出来的是 852 * 480

其他参数的变换

音频可以调整的参数:音频编码、音频声道数、音频采样率、音频码率

视频可以调整的参数:视频编码、视频码率、视频分辨率、视频每秒帧数

关于码率,有一篇文章 《如何计算视频最佳码率》 可以参考一下,个人不太懂,如果有懂的可以推荐一下。

ffmpeg -i video.webm -c:a aac -ac 2 -ar 48000 -b:a 128k -c:v h264 -b:v 1000k -vf 'scale=1920:1080,fps=30' video.mp4
-c:a aac 指定音频编码aac
-ac 2 声道双声道
-ar 48k 采样率
-b:a 128k = -ab 128k 指定音频码率,推荐使用 -b:a
-c:v h264 指定视频编码h264
-b:v 128k = -vb 1000k 指定视频频码率,推荐使用 -b:v
-vf 'scale=1920:1080,fps=30' 指定分辨率 每秒帧数

合并视频/拼接视频

合并视频/拼接视频要求视频合适尽可能一致,否则合成的视频可能会无法播放。

常用的方式是将需要拼接的视频文件路径写入一个文件,然后用 FFmpeg concat 方法拼接:

echo "file $PWD/video-1.mp4" >> videos.txt
echo "file $PWD/video-2.mp4" >> videos.txt
echo "file $PWD/video-3.mp4" >> videos.txt
ffmpeg -f concat -safe 0 -i videos.txt -c copy video-concat-123.mp4

当然通过Linux命令的技巧是可以写成一个命令的:

ffmpeg -f concat -safe 0 -i <(echo "file $PWD/video-1.mp4"; echo "file $PWD/video-2.mp4"; echo "file $PWD/video-3.mp4";) -c copy video-concat-123.mp4

合并视频也是可以进行转码的

ffmpeg -f concat -safe 0 -i <(echo "file $PWD/video-1.webm"; echo "file $PWD/video-2.webm"; echo "file $PWD/video-3.webm";) -c:a aac -c:v h264 video-concat-123.mp4

后面会提到如何给拼接并转码成HLS格式的视频

音频加图片变成视频

ffmpeg -loop 1 -i cover.jpg -i input.mp3 -c:v libx264 -c:a aac -b:a 192k -shortest output.mp4

上面命令中,有两个输入文件,一个是封面图片 cover.jpg ,另一个是音频文件 input.mp3 -loop 1 参数表示图片无限循环, -shortest 参数表示音频文件结束,输出视频就结束。

添加水印

给视频添加水印可真是个比较麻烦的事情,这里也仅仅是列出了一些比较常用的类型,举一反三加阅读文档就可以了。

一个图片水印,一个位置

这种算是比较简单的了

ffmpeg -i video.mp4 -vf "movie=watermark.png,scale=100:100[logo];[in][logo]overlay=main_w-overlay_w-10:10[out]" video-logo-01.mp4
ffmpeg -i video.mp4 -vf "movie=watermark.png,scale=128:128[logo];[in][logo]overlay=W-w-50:50[out]" video-logo-02.mp4
ffmpeg -i video.mp4 -vf "movie=watermark.png,scale=128:128[logo];[in][logo]overlay=W-w-50:H-h-50[out]" video-logo-03.mp4

这里用到了前面用过的 -vf 参数

movie=watermark.png,scale=100:100[logo] 
# scale 表示 这个图片会被缩放到 100 x 100 的大小

这个表示我输入一个图片,我把他叫做 logo

[in][logo]overlay=main_w-overlay_w-10:10[out]

这个表示 我用在输入 [in] 上添加 logo,添加的位置为 overlay=main_w-overlay_w-10:10

这里需要解释:overlay=x=50:y=50 这表示给logo设置一个位置,这里的 x 和 y 可以省略,即

overlay=50:50

这表示视频左上角坐标为 (0, 0), 向右,向下 50 像素。

而 main_w 和 W 一样表示 视频的宽度,main_h 和 H 一样表示视频高度,overlay_w-10 和 w-10 一样,表示横向的偏移量,overlay_h-10 和 h-10 一样 表示纵向的偏移量。

overlay=W-w-50:50 就表示,视频左上角坐标为 (0, 0), 向右 视频宽度减去偏移量50的像素,向下 50 像素,即从右上角向左向下 50 像素。

同理:

# 左上角
overlay=50:50
# 右上角
overlay=W-w-50:50
# 左下角
overlay=50:H-h-50
# 右下角
overlay=W-w-50:H-h-50

一个图片水印,多个位置变换

这里给出一个例子,一个logo三个位置交替出现,读懂需要一点编程的思维

ffmpeg -i video.mp4 -vf "movie=watermark0.png,scale=80:80[watermark0];movie=watermark0.png,scale=80:80[watermark1];movie=watermark0.png,scale=80:80[watermark2];[in][watermark0]overlay=x='if(gte(mod(t,30),0)*lt(mod(t,30),10),W-w-80,NAN)':y=80[tmp],[tmp][watermark1]overlay=x='if(gte(mod(t,30),10)*lt(mod(t,30),20),80,NAN)':y=80[tmp],[tmp][watermark2]overlay=x='if(gte(mod(t,30),20)*lt(mod(t,30),30),W-w-80,NAN)':y=H-h-80[out]" video-watermark.mp4

这里输入的一张图片被分成了三个输入 watermark0 watermark1 watermark2

逻辑是在 0-10 秒显示 watermark0, 10-20 秒显示 watermark1, 20-30 秒显示 watermark2

这个时间可以自己定义,用到的是 ffmpeg 中 if gte lt mod 等运算符,和默认的时间 t

中间的输出用 tmp 表示,同时也作为下一个水印的输入。

多个图片水印,变换

有了上面的例子,下面这个应该好理解了

# 三个logo交替出现
ffmpeg -i video.mp4 -vf "movie=watermark0.png,scale=150:150[watermark0];movie=watermark1.png,scale=150:150[watermark1];movie=watermark3.png,scale=150:150[watermark2];[in][watermark0]overlay=x='if(gte(mod(t,30),0)*lt(mod(t,30),10),W-w-80,NAN)':y=80[tmp],[tmp][watermark1]overlay=x='if(gte(mod(t,30),10)*lt(mod(t,30),20),W-w-80,NAN)':y=80[tmp],[tmp][watermark2]overlay=x='if(gte(mod(t,30),20)*lt(mod(t,30),30),W-w-80,NAN)':y=80[out]" video-watermark.mp4

这里输入的三张图片分别为 watermark0 watermark1 watermark2

跑马灯

跑马灯水印实际上就是图片位置x,y随着时间t的变化,可以写成函数

# 从左到右
ffmpeg -i video.mp4 -vf "movie=watermark0.png,scale=128:128[logo];[in][logo]overlay=x='mod(t*20,W)':50[out]" video-logo.mp4
ffmpeg -i video.mp4 -vf "movie=watermark0.png,scale=128:128[logo];[in][logo]overlay=x='mod(t*20,W+128)-128':50[out]" video-logo.mp4
# 从左上到右下
ffmpeg -i video.mp4 -vf "movie=watermark0.png,scale=128:128[logo];[in][logo]overlay=x='mod(t*20,W)':y='mod(t*20,H)'[out]" video-logo.mp4
ffmpeg -i video.mp4 -vf "movie=watermark0.png,scale=128:128[logo];[in][logo]overlay=x='mod(t*20,W+128)-128':y='mod(t*20,H+128)-128'[out]" video-logo.mp4

半透明水印

ffmpeg -i video.mp4 -vf "movie=watermark0.png,scale=128:128,colorkey=0x000000:0.6:1.0[logo];[in][logo]overlay=W-w-50:50[out]" video-logo.mp4

文字水印

ffmpeg -i video.mp4 -vf "drawtext=fontfile=simhei.ttf:text='Github is Great!':x='mod(t*20,W)':y='mod(t*10,H)':fontsize=24:fontcolor=white:shadowy=2" video-watermark-text.mp4
ffmpeg -i video.mp4 -vf "drawtext=text='Github is Great!':x='mod(t*20,W)':y='mod(t*10,H)':fontsize=24:fontcolor=white:shadowy=2" video-watermark-text.mp4
ffmpeg -i video.mp4 -vf "drawtext=fontfile=SourceHanSerifCN-Regular.otf:text='GitHub真棒':x='mod(t*20,W)':y='mod(t*10,H)':fontsize=24:fontcolor=white:shadowy=2" video-watermark-text.mp4
ffmpeg -i video.mp4 -vf "drawtext=fontfile=SourceHanSerifCN-Regular.otf:text='这个水印真厉害!!!哈哈哈!!!':x='mod(t*20,W)':y='mod(t*10+470,H)':fontsize=24:fontcolor=white:shadowx=2:shadowy=2" video-watermark-text.mp4
  • fontfile 字体文件,如果水印内容包含中文,需要中文字体
  • text 水印内容
  • fontsize 字体大小
  • fontcolor 字体颜色
  • shadowx、shadowy 阴影

图片和文字水印并存

ffmpeg -i video.mp4 -vf "movie=watermark0.png,scale=150:150[watermark0];movie=watermark2.png,scale=150:150[watermark1];[in][watermark0]overlay=x='if(lt(mod(t,20),10),50,NAN)':y=50[tmp],[tmp][watermark1]overlay=x='if(gt(mod(t,20),10),W-w-50,NAN)':y=50[tmp],[tmp]drawtext=fontfile=simhei.ttf:text='Github is Great!':x='mod(t*20,W)':y='mod(t*10,H)':fontsize=24:fontcolor=white:shadowx=2:shadowy=2[out]" video-watermark.mp4

切片(转HLS)

关于m3u8文件

M3U8文件是指UTF-8编码格式的M3U文件。M3U文件是记录了一个索引纯文本文件,打开它时播放软件并不是播放它,而是根据它的索引找到对应的音视频文件的网络地址进行在线播放。

M3U是一种播放多媒体列表的文件格式,它的设计初衷是为了播放音频文件,比如MP3,但是越来越多的软件现在用来播放视频文件列表,M3U也可以指定在线流媒体音频源。很多播放器和软件都支持M3U文件格式。

拓展:一个下载视频的小技巧

youtube-dl

直接转码切片

ffmpeg -i video.webm -c:a aac -ac 2 -c:v h264 -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time+append_list' -hls_time 15 -hls_segment_filename "hls/%06d.ts" hls/index.m3u8

这个命令里面有太多的参数了,实际上平时不会用到这么多。

前面讲过 -c:a aac -ac 2 -c:v h264 是指定音视频格式的参数,实际操作中,如果 音频编码是 aac,视频编码是 h264 可以直接使用 -c copy 参数,不改变编码,加快切片速度。

  • -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb 转换HLS或者MP4或者FLV时最好加上这两个选项
  • -f hls 指定格式为 hls
  • -hls_playlist_type vod 指定视频为点播 同时会默认设置 -hls_list_size 0,点播的m3u8索引中ts数目不限制
  • -hls_flags 'split_by_time+append_list' 默认情况下,切片时长可能会长短不齐,split_by_time 可以使时间尽可能的相同,append_list是以追加的方式添加在m3u8文件末尾,这也是拼接HLS视频的一种方式
  • -hls_time 15 每个切片的长度
  • -hls_segment_filename "hls/%06d.ts" 切片的文件名 %06d 表示 6位数字,从 0 开始,前面不足补 0

切片输出不同分辨率

ffmpeg -i video.mp4 -c copy -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time+append_list' -hls_time 15 "hls/1080/%06d.ts" hls/1080/index.m3u8
ffmpeg -i video.mp4 -vf scale=1280:720 -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time+append_list' -hls_time 15 -hls_segment_filename "hls/720/%06d.ts" hls/720/index.m3u8
ffmpeg -i video.mp4 -vf scale=852:480 -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time+append_list' -hls_time 15 -hls_segment_filename "hls/480/%06d.ts" hls/480/index.m3u8

同时输出多种分辨率

ffmpeg -i video.mp4 \
-c copy -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time' -hls_time 15 -hls_segment_filename "hls/1080/%06d.ts" hls/1080/index.m3u8 \
-vf scale=-2:720 -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time' -hls_time 15 -hls_segment_filename "hls/720/%06d.ts" hls/720/index.m3u8 \
-vf scale=-2:480 -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time' -hls_time 15 -hls_segment_filename "hls/480/%06d.ts" hls/480/index.m3u8

同时输出多种分辨率的另一种方法

ffmpeg -i video.mp4 \
-filter_complex "[v:0]split=3[vtemp001][vtemp002][vout1080];[vtemp001]scale=w=1280:h=720[vout720];[vtemp002]scale=w=852:h=480[vout480]" \
-bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time' \
-hls_time 15 hls/multi/%06d_%v.ts \
-map "[vout1080]" \
-map "[vout720]" \
-map "[vout480]" \
-map a:0 \
-map a:0 \
-map a:0 \
-var_stream_map "v:0,a:0 v:1,a:1 v:2,a:2" \
-master_pl_name index.m3u8 \
hls/multi/index_%v.m3u8

截取指定时长切片(试看功能)

ffmpeg -ss 0 -t 300 -i video.webm -c:a aac -ac 2 -c:v h264 -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time' -hls_time 15 -hls_segment_filename "hls/%06d.ts" hls/index.m3u8

-ss 0 -t 300 表示从 0 秒开始,-t 时长

多个视频合并成一个视频切片

ffmpeg -f concat -safe 0 -i <(echo "file $PWD/video-1.mp4"; echo "file $PWD/video-2.mp4"; echo "file $PWD/video-3.mp4";) -c copy -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time' -hls_time 15 -hls_segment_filename "hls/%06d.ts" hls/index-123.m3u8

这种拼接方法在不同格式视频(不同分辨率,码率,采样率)时可能会出现错误,

可以采用另一种方法转换HLS

# 使用追加的模式 append_list
ffmpeg -y -hide_banner -i video.webm -c:a aac -c:v h264 -vf 'scale=720:-2' -movflags faststart -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time+append_list' -hls_time 10 -hls_segment_filename hls/%d.ts hls/index.m3u8
ffmpeg -y -hide_banner -i video.mp4 -c:a aac -c:v h264 -vf 'scale=720:-2' -movflags faststart -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time+append_list' -hls_time 10 -hls_segment_filename hls/%d.ts hls/index.m3u8
ffmpeg -y -hide_banner -i video.mp4 -c:a aac -c:v h264 -vf 'scale=720:-2' -movflags faststart -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_playlist_type vod -hls_flags 'split_by_time+append_list' -hls_time 10 -hls_segment_filename hls/%d.ts hls/index.m3u8

切片的加密(转HLS加密)

HLS 加密需要一个密钥 key,一个初始向量 iv

openssl rand 16 > enc.key
touch enc.keyinfo
echo "https://example.com/enc.key" > enc.keyinfo
echo "enc.key" >> enc.keyinfo
echo $(openssl rand -hex 16) >> enc.keyinfo
ffmpeg -y -i video.mp4 -c copy -bsf:a aac_adtstoasc -bsf:v h264_mp4toannexb -f hls -hls_key_info_file enc.keyinfo -hls_playlist_type vod -hls_flags 'split_by_time' -hls_time 15 -hls_segment_filename "hls-encrypted/%6d.ts" video-encrypted.m3u8

openssl rand 16 生成的是一个乱码的字符串 openssl rand -hex 16 输出的是转成 16 进制的字符串

如果要存储密钥和向量可以都用 16 进制,在 Java 里可以这样写:

import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomUtils;
import java.nio.charset.StandardCharsets;
// 用于存储在数据库中
String key = new String(Hex.encodeHex(RandomUtils.nextBytes(16)));
String iv = new String(Hex.encodeHex(RandomUtils.nextBytes(16)));
// 用于转码时使用
File encKeyFile = new File("enc.key");
File encKeyInfoFile = new File("enc.keyinfo");
FileUtils.writeByteArrayToFile(encKeyFile, Hex.decodeHex(key));
FileUtils.writeStringToFile(encKeyInfoFile, "https://example.com/enc.key\n", StandardCharsets.UTF_8, false);
FileUtils.writeStringToFile(encKeyInfoFile, "enc.key\n", StandardCharsets.UTF_8, true);