使用FFmpeg来进行固定时间间隔截图(rtmp直播流)


问题描述

如何对一个直播流不停的进行截图?

对直播流进行截图是一件十分平常的事情,因为实际中需要通过截图来对视频内容进行审核。

一个简单的需求就是每隔一个固定时间间隔来从直播流中截取一张图片(jpeg或者png)。

这里总结一下如何使用FFmpeg工具来完成这件事。有两种使用方法,简单的就是直接通过CMD来调用FFmpeg进行截图, 复杂的就是用C语言来调用FFmpeg。

  • 使用CMD调用FFmpeg:简单,稳定,但是低效。
  • 使用C语言调用FFmpeg:复杂,但是非常高效。

使用CMD调用FFmpeg来截图


在固定时间点的截取一张图

先来最简单的,如何截取一个视频流的第一张图:

1
$ ffmpeg -i rtmp://192.168.90.43:2019/live/long -frames:v 1 ./snap/1.png

这里直接使用-frames:v 1参数就可以完成,截图一张图片后,便会结束。

考虑在一个时间节点截取一张图:

1
$ ffmpeg -ss 00:00:10 -i .\1080_60.flv -frames:v 1 ./snap/1.png

增加-ss 00:00:10参数,这里截取10秒处的一张图。


建立for循环来达到固定时间间隔截图

那么为了简单的达到固定时间间隔截图,可以使用for循环来完成,隔一段时间截一单张图:

1
2
3
4
for((;;)); do \
ffmpeg -i rtmp://192.168.90.43:2019/live/long -strftime 1 -frames:v 1 ./snap/%H-%M-%S.png \
sleep 10; \
done

这个方法十分简单,但是问题在于每次都要拉一次流,创建一次连接,虽然也不占多少CPU,但是感觉有毒。


使用帧率控制来达到固定时间间隔截取一张图

一条命令完成,可以通过控制帧率来达到,将帧率设置在0.1,也就是每10秒1帧:

1
$ ffmpeg -i rtmp://192.168.90.43:2019/live/long -vf fps=0.1 ./snap/%00d.png

如果想要调整图片分辨率可以同时scale来完成:

1
2
$ ffmpeg -i rtmp://192.168.90.43:2019/live/long \
-filter_complex "[0:v]fps=0.1[a];[a]scale=-1:720[out]" -map "[out]" -an ./snap/%00d.png

这样的方法的确可以达到固定时间间隔截图,但是问题在于FFmpeg会一直进行解码,CPU占用很高,且非常没有必要。

另外还有通过Select滤镜来选择frame来进行截图,但是实际上不仅很难用,而且也会持续解码,占用CPU。


(曲线救国)先保存成TS文件,再从TS文件上截图

上面的方法中,使用for循环来截图相对来说是最健康的,因为它基本不占啥CPU,但是每次重新创建远程连接也挺耗资源的。

那么可以先通过一个FFmpeg进程来拉取流,并保存成为本地的TS文件,然后再进行for循环来对TS文件进行截图, 这样就能免得每次创建远程连接。

下面是一个简单的Shell脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
#!/bin/bash
input=""
interval=5
outdir="./screens"
segmdir="tmp"
height=-1
outfmt="jpg"
invalid=false
while getopts ":i:t:d:h:f:" opt
do
case $opt in
i)
input=$OPTARG
;;
t)
interval=$OPTARG
;;
d)
outdir=$OPTARG
;;
h)
height=$OPTARG
;;
f)
outfmt=$OPTARG
;;
?)
invalid=true
;;
esac
done
if [ "$input" == "" ] ; then invalid=true ; fi
if [[ "$outfmt" != "jpg" && "$outfmt" != "png" ]] ; then invalid=true ; fi
if [ $invalid == true ]
then
echo "Invalid input"
exit 1
fi
echo "input = $input"
echo "interval = $interval"
echo "outdir = $outdir"
echo "screen fmt = $outfmt"
echo "screenshot height = $height"
if [ ! -d "$outdir" ] ; then mkdir -p $outdir ; fi
segmdir="$outdir/$segmdir"
if [ ! -d "$segmdir" ] ; then mkdir -p $segmdir ; fi
trap " echo "" ; echo "exit..." ; rm -r $segmdir ; exit " SIGHUP SIGINT SIGQUIT
ffmpeg -i $input -f segment -segment_list $segmdir/list \
-segment_list_size 3 -segment_time $interval -segment_wrap 3 \
-codec copy $segmdir/%01d.mp4 \
> /dev/null 2>&1 &
sleep `expr $interval / 2`
idx=1
while true
do
seg=`cat $segmdir/list | awk 'END {print}'`
if [ "$seg" != "" ]
then
echo "$seg $idx.$outfmt"
if [ "$outfmt" == "png" ] ; then
ffmpeg -i $segmdir/$seg -vf "scale=-1:$height" -c:v png -an -frames:v 1 -y $outdir/$idx.$outfmt > /dev/null 2>&1 &
elif [ "$outfmt" == "jpg" ] ; then
ffmpeg -i $segmdir/$seg -vf "scale=-1:$height" -f image2 -q:v 2 -an -frames:v 1 -y $outdir/$idx.$outfmt > /dev/null 2>&1 &
fi
let idx++
fi
sleep $interval
done

这个方法虽然每次都要重新开启线程,关闭线程,而且还相当于在进行录制,但是基本避免了最大的开销(解码,建立远程连接)。


使用C语言调用FFmpeg

很明显,上面的方法虽然进化得越来越好,但是始终是不完美的。

要想达到完美,看来只能通过C语言来调用FFmpeg,这样就能将整个编解码过程控制在自己手里。

使用C语言调用FFmpeg来进行固定时间间隔截图(rtmp直播流)


参考

FFmpeg Documentation

---------------------------------END---------------------------------