需求
最近需要做一个基于ffplay的自定义滤镜显示,需要根据一个二进制bin文件,实际上是要取其中的一段流,根据其中的信息对视频进行涂黑处理,即当取到的流的一位为1时,涂黑视频的一个16*16的像素块。
放一个做好后得示例图,方便更直观的理解我的意图。
思考
因为涉及到这个小工具要播放本地视频,拉流播放等,所以就直接更改ffplay的源码,生成定制版本的ffplay。
本来想好好整理一篇文章的,但是东西太多了,后期直接放弃,想看怎么做的效果部分代码,直接翻到最下面看。
总体结构图
这里放一张雷神画的ffplay总体结构图,雷神文章
雷神的文章给了很大的帮助,因为使用的是ffmpeg4.3.1,所以与雷神的文章略有差异,但是原理是相同的。
另一篇文章流程图更为具体,而且也也符合现在的ffmpeg4.0,也可以看下ffplay流程图
然后为了熟悉整体的流程,自己画了一幅ffplay流程图。
main()函数
ffplay是作为一个小工具的方式对外提供的,其编译生成后存放在ffmpeg的bin目录下,ffplay是一个独立的播放器核心。
在main()函数内调用了以下几个函数:
avdevice_register_all():注册所有编码器和解码器。
avformat_network_init():初始化网络库,在官方的文档里陈述这个函数是可选的,由于ffplay涉及到拉流播放等,所以在ffplay内对其进行了初始化。这是一个avformat的一个接口,其对应的文档libavformat文档
show_banner():打印输出FFmpeg版本信息(编译时间,编译选项,类库信息等)。
parse_options():解析输入的命令。
SDL_Init():SDL初始化,FFPlay中的视频与音频都是使用到了SDL
SDL_CreateWindow():创建SDL窗口,Wiki地址
SDL_CreateRedner()为SDL窗口创建SDL渲染上下文,SDL_CreateRenderer Wiki地址,SDL_RendererFlags Wiki地址
stream_open ():打开输入媒体。
event_loop():处理键盘事件与视频刷新,个人观点认为是主线程循环
avdevice_register_all()
这是Libavdevice库内的一个函数,使用libavdevice读取数据和直接打开视频文件比较类似。因为系统的设备也被FFmpeg认为是一种输入的格式(即AVInputFormat)。
使用libavdevice的时候需要包含其头文件:
#include “libavdevice/avdevice.h”
然后,在程序中需要注册libavdevice:
avdevice_register_all();
具体使用avdevice的应用参考雷神的文章:最简单的基于FFmpeg的AVDevice例子(读取摄像头)
值得注意的是很多以前的资料都是用**av_register_all()**进行初始化,但是现在这个API貌似已经早被取消了,在ffmpeg种未见,见以下所示的API变更提示:
2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h
Deprecate use of av_register_input_format(), av_register_output_format(),
av_register_all(), av_iformat_next(), av_oformat_next().
Add av_demuxer_iterate(), and av_muxer_iterate().
此提示位于:
ffmpeg/doc/APIchanges
avformat_network_init()
使用ffmpeg类库进行开发的时候,打开流媒体(或本地文件)的函数是avformat_open_input()。
其中打开网络流的话,前面要加上函数avformat_network_init()。
所以显而易见如果想通过rtsp拉流或者使用网络时,要运行网络流的初始化函数avformat_network_init(),然后再进行流的拉取事宜。
雷神在FFMPEG类库打开流媒体的方法中,对此进行了阐述,并做了一个rtsp拉流的小例子,可以看下。
show_banner()
打印输出FFmpeg版本信息(编译时间,编译选项,类库信息等)。
实际上就是在程序运行开始时打印出相关的版本信息,编译选项信息等。
parse_options()
参考雷神文章:ffplay.c函数结构简单分析
parse_options()解析全部输入选项。即将输入命令“ffplay -f h264 test.264”中的“-f”这样的命令解析出来。其函数调用结构如下图所示。需要注意的是,FFplay(ffplay.c)的parse_options()和FFmpeg(ffmpeg.c)中的parse_options()实际上是一样的。
parse_options()会循环调用parse_option()直到所有选项解析完毕。
parse_option()
解析一个输入选项。具体的解析步骤不再赘述。
OptionDef结构体
FFmpeg和ffplay的每一个选项信息存储在一个OptionDef结构体中。定义如下:
typedef struct OptionDef {
const char *name;
int flags;
#define HAS_ARG 0x0001
#define OPT_BOOL 0x0002
#define OPT_EXPERT 0x0004
#define OPT_STRING 0x0008
#define OPT_VIDEO 0x0010
#define OPT_AUDIO 0x0020
#define OPT_INT 0x0080
#define OPT_FLOAT 0x0100
#define OPT_SUBTITLE 0x0200
#define OPT_INT64 0x0400
#define OPT_EXIT 0x0800
#define OPT_DATA 0x1000
#define OPT_PERFILE 0x2000 /* the option is per-file (currently ffmpeg-only).
implied by OPT_OFFSET or OPT_SPEC */
#define OPT_OFFSET 0x4000 /* option is specified as an offset in a passed optctx */
#define OPT_SPEC 0x8000 /* option is to be stored in an array of SpecifierOpt.
Implies OPT_OFFSET. Next element after the offset is
an int containing element count in the array. */
#define OPT_TIME 0x10000
#define OPT_DOUBLE 0x20000
#define OPT_INPUT 0x40000
#define OPT_OUTPUT 0x80000
union {
void *dst_ptr;
int (*func_arg)(void *, const char *, const char *);
size_t off;
} u;
const char *help;
const char *argname;
} OptionDef;
其中的重要字段:
name:用于存储选项的名称。例如“i”,“f”,“codec”等等。
flags:存储选项值的类型。例如:HAS_ARG(包含选项值),OPT_STRING(选项值为字符串类型),OPT_TIME(选项值为时间类型。
u:存储该选项的处理函数。
help:选项的说明信息。
FFmpeg使用一个名称为options,类型为OptionDef的数组存储所有的选项。有一部分通用选项存储在cmdutils_common_opts.h中。这些选项对于FFmpeg,FFplay以及FFprobe都适用。
fftools/cmdutils.h内容如下:
{ "L", OPT_EXIT, { .func_arg = show_license }, "show license" }, \
{ "h", OPT_EXIT, { .func_arg = show_help }, "show help", "topic" }, \
{ "?", OPT_EXIT, { .func_arg = show_help }, "show help", "topic" }, \
{ "help", OPT_EXIT, { .func_arg = show_help }, "show help", "topic" }, \
{ "-help", OPT_EXIT, { .func_arg = show_help }, "show help", "topic" }, \
{ "version", OPT_EXIT, { .func_arg = show_version }, "show version" }, \
{ "buildconf", OPT_EXIT, { .func_arg = show_buildconf }, "show build configuration" }, \
{ "formats", OPT_EXIT, { .func_arg = show_formats }, "show available formats" }, \
{ "muxers", OPT_EXIT, { .func_arg = show_muxers }, "show available muxers" }, \
{ "demuxers", OPT_EXIT, { .func_arg = show_demuxers }, "show available demuxers" }, \
{ "devices", OPT_EXIT, { .func_arg = show_devices }, "show available devices" }, \
{ "codecs", OPT_EXIT, { .func_arg = show_codecs }, "show available codecs" }, \
{ "decoders", OPT_EXIT, { .func_arg = show_decoders }, "show available decoders" }, \
{ "encoders", OPT_EXIT, { .func_arg = show_encoders }, "show available encoders" }, \
{ "bsfs", OPT_EXIT, { .func_arg = show_bsfs }, "show available bit stream filters" }, \
{ "protocols", OPT_EXIT, { .func_arg = show_protocols }, "show available protocols" }, \
{ "filters", OPT_EXIT, { .func_arg = show_filters }, "show available filters" }, \
{ "pix_fmts", OPT_EXIT, { .func_arg = show_pix_fmts }, "show available pixel formats" }, \
{ "layouts", OPT_EXIT, { .func_arg = show_layouts }, "show standard channel layouts" }, \
{ "sample_fmts", OPT_EXIT, { .func_arg = show_sample_fmts }, "show available audio sample formats" }, \
{ "colors", OPT_EXIT, { .func_arg = show_colors }, "show available color names" }, \
{ "loglevel", HAS_ARG, { .func_arg = opt_loglevel }, "set logging level", "loglevel" }, \
{ "v", HAS_ARG, { .func_arg = opt_loglevel }, "set logging level", "loglevel" }, \
{ "report", 0, { .func_arg = opt_report }, "generate a report" }, \
{ "max_alloc", HAS_ARG, { .func_arg = opt_max_alloc }, "set maximum size of a single allocated block", "bytes" }, \
{ "cpuflags", HAS_ARG | OPT_EXPERT, { .func_arg = opt_cpuflags }, "force specific cpu flags", "flags" }, \
{ "hide_banner", OPT_BOOL | OPT_EXPERT, {&hide_banner}, "do not show program banner", "hide_banner" },
options数组的定义位于ffplay.c中,如下所示:
static const OptionDef options[] = {
CMDUTILS_COMMON_OPTIONS
{ "x", HAS_ARG, { .func_arg = opt_width }, "force displayed width", "width" },
{ "y", HAS_ARG, { .func_arg = opt_height }, "force displayed height", "height" },
{ "s", HAS_ARG | OPT_VIDEO, { .func_arg = opt_frame_size }, "set frame size (WxH or abbreviation)", "size" },
{ "fs", OPT_BOOL, { &is_full_screen }, "force full screen" },
{ "an", OPT_BOOL, { &audio_disable }, "disable audio" },
{ "vn", OPT_BOOL, { &video_disable }, "disable video" },
{ "sn", OPT_BOOL, { &subtitle_disable }, "disable subtitling" },
{ "ast", OPT_STRING | HAS_ARG | OPT_EXPERT, { &wanted_stream_spec[AVMEDIA_TYPE_AUDIO] }, "select desired audio stream", "stream_specifier" },
{ "vst", OPT_STRING | HAS_ARG | OPT_EXPERT, { &wanted_stream_spec[AVMEDIA_TYPE_VIDEO] }, "select desired video stream", "stream_specifier" },
{ "sst", OPT_STRING | HAS_ARG | OPT_EXPERT, { &wanted_stream_spec[AVMEDIA_TYPE_SUBTITLE] }, "select desired subtitle stream", "stream_specifier" },
{ "ss", HAS_ARG, { .func_arg = opt_seek }, "seek to a given position in seconds", "pos" },
{ "t", HAS_ARG, { .func_arg = opt_duration }, "play \"duration\" seconds of audio/video", "duration" },
{ "bytes", OPT_INT | HAS_ARG, { &seek_by_bytes }, "seek by bytes 0=off 1=on -1=auto", "val" },
{ "seek_interval", OPT_FLOAT | HAS_ARG, { &seek_interval }, "set seek interval for left/right keys, in seconds", "seconds" },
{ "nodisp", OPT_BOOL, { &display_disable }, "disable graphical display" },
{ "noborder", OPT_BOOL, { &borderless }, "borderless window" },
{ "alwaysontop", OPT_BOOL, { &alwaysontop }, "window always on top" },
{ "volume", OPT_INT | HAS_ARG, { &startup_volume}, "set startup volume 0=min 100=max", "volume" },
{ "f", HAS_ARG, { .func_arg = opt_format }, "force format", "fmt" },
{ "pix_fmt", HAS_ARG | OPT_EXPERT | OPT_VIDEO, { .func_arg = opt_frame_pix_fmt }, "set pixel format", "format" },
{ "stats", OPT_BOOL | OPT_EXPERT, { &show_status }, "show status", "" },
{ "fast", OPT_BOOL | OPT_EXPERT, { &fast }, "non spec compliant optimizations", "" },
{ "genpts", OPT_BOOL | OPT_EXPERT, { &genpts }, "generate pts", "" },
{ "drp", OPT_INT | HAS_ARG | OPT_EXPERT, { &decoder_reorder_pts }, "let decoder reorder pts 0=off 1=on -1=auto", ""},
{ "lowres", OPT_INT | HAS_ARG | OPT_EXPERT, { &lowres }, "", "" },
{ "sync", HAS_ARG | OPT_EXPERT, { .func_arg = opt_sync }, "set audio-video sync. type (type=audio/video/ext)", "type" },
{ "autoexit", OPT_BOOL | OPT_EXPERT, { &autoexit }, "exit at the end", "" },
{ "exitonkeydown", OPT_BOOL | OPT_EXPERT, { &exit_on_keydown }, "exit on key down", "" },
{ "exitonmousedown", OPT_BOOL | OPT_EXPERT, { &exit_on_mousedown }, "exit on mouse down", "" },
{ "loop", OPT_INT | HAS_ARG | OPT_EXPERT, { &loop }, "set number of times the playback shall be looped", "loop count" },
{ "framedrop", OPT_BOOL | OPT_EXPERT, { &framedrop }, "drop frames when cpu is too slow", "" },
{ "infbuf", OPT_BOOL | OPT_EXPERT, { &infinite_buffer }, "don't limit the input buffer size (useful with realtime streams)", "" },
{ "window_title", OPT_STRING | HAS_ARG, { &window_title }, "set window title", "window title" },
{ "left", OPT_INT | HAS_ARG | OPT_EXPERT, { &screen_left }, "set the x position for the left of the window", "x pos" },
{ "top", OPT_INT | HAS_ARG | OPT_EXPERT, { &screen_top }, "set the y position for the top of the window", "y pos" },
#if CONFIG_AVFILTER
{ "vf", OPT_EXPERT | HAS_ARG, { .func_arg = opt_add_vfilter }, "set video filters", "filter_graph" },
{ "af", OPT_STRING | HAS_ARG, { &afilters }, "set audio filters", "filter_graph" },
#endif
{ "rdftspeed", OPT_INT | HAS_ARG| OPT_AUDIO | OPT_EXPERT, { &rdftspeed }, "rdft speed", "msecs" },
{ "showmode", HAS_ARG, { .func_arg = opt_show_mode}, "select show mode (0 = video, 1 = waves, 2 = RDFT)", "mode" },
{ "default", HAS_ARG | OPT_AUDIO | OPT_VIDEO | OPT_EXPERT, { .func_arg = opt_default }, "generic catch all option", "" },
{ "i", OPT_BOOL, { &dummy}, "read specified file", "input_file"},
{ "codec", HAS_ARG, { .func_arg = opt_codec}, "force decoder", "decoder_name" },
{ "acodec", HAS_ARG | OPT_STRING | OPT_EXPERT, { &audio_codec_name }, "force audio decoder", "decoder_name" },
{ "scodec", HAS_ARG | OPT_STRING | OPT_EXPERT, { &subtitle_codec_name }, "force subtitle decoder", "decoder_name" },
{ "vcodec", HAS_ARG | OPT_STRING | OPT_EXPERT, { &video_codec_name }, "force video decoder", "decoder_name" },
{ "autorotate", OPT_BOOL, { &autorotate }, "automatically rotate video", "" },
{ "find_stream_info", OPT_BOOL | OPT_INPUT | OPT_EXPERT, { &find_stream_info },
"read and decode the streams to fill missing information with heuristics" },
{ "filter_threads", HAS_ARG | OPT_INT | OPT_EXPERT, { &filter_nbthreads }, "number of filter threads per graph" },
{ NULL, },
};
所以当我们基于ffplay或者ffmpeg去做定制功能时,如果需要加入自己定制的命令行参数,可以在此进行加入。
SDL_Init()
SDL_Init()用于初始化SDL。FFplay中视频的显示和声音的播放都用到了SDL。
SDL_CreateWindow()
创建SDL窗口。
函数原型:
DL_Window * SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags);
参数:
参数 | 释义 |
---|---|
title | 窗口标题,使用UTF-8编码 |
x | 窗口左上起始点的x坐标,常使用SDL_WINDOWPOS_CENTERED或者 SDL_WINDOWPOS_UNDEFINED |
y | 窗口左上起始点的y坐标,常使用SDL_WINDOWPOS_CENTERED或者 SDL_WINDOWPOS_UNDEFINED |
w | SDL窗口的宽 |
h | SDL窗口的高 |
flags | 窗口标志,0,1或者多个 |
返回创建窗口,失败时返回空指针
SDL_CreateWindow() Wiki
SDL_CreateRedner()
为SDL窗口创建SDL渲染上下文
函数原型:
SDL_Renderer * SDL_CreateRenderer(SDL_Window * window, int index, Uint32 flags);
参数:
参数 | 释义 |
---|---|
window | 显示渲染的窗口,即SDL_CreateWindow()创建并返回的窗口 |
index | 要初始化的呈现驱动程序的索引,或-1初始化支持请求标志的第一个驱动程序 |
flags | 窗口标志,0,1或者多个 |
stream_open()
stream_open()的作用是打开输入的媒体。这个函数还是比较复杂的,包含了FFplay中各种线程的创建。它的函数调用结构如下图所示。
frame_queue_init()
其是结构体FrameQueue对外提供的队列初始化函数。
参考这两篇博文:ffplay frame queue分析1,ffplay packet queue分析2
ffplay用frame queue保存解码后的数据。
首先定义了一个结构体Frame用于保存一帧视频画面、音频或者字幕:
typedef struct Frame {
AVFrame *frame; //视频或音频的解码数据
AVSubtitle sub; //解码的字幕数据
int serial;
double pts; /* 帧的时间戳 */
double duration; /* 帧的估计持续时间 */
int64_t pos; /* 输入文件中帧的字节位置 */
int width;
int height;
int format;
AVRational sar;
int uploaded;
int flip_v;
} Frame;
Frame的设计试意图用一个结构体“融合”3种数据:视频、音频、字幕,虽然AVFrame既可以表示视频又可以表示音频,但在融合字幕时又需要引入AVSubtitle,以及一些其他字段,如width/height等来补充AVSubtitle,所以整个结构体看起来很“拼凑”(甚至还有视频专用的flip_v字段)。这里先关注frame和sub字段即可。
接着设计了一个FrameQueue用于表示整个帧队列:
typedef struct FrameQueue {
Frame queue[FRAME_QUEUE_SIZE];//队列元素,用数组模拟队列
int rindex;//是读帧数据索引, 相当于是队列的队首
int windex;//是写帧数据索引, 相当于是队列的队尾
int size;//当前存储的节点个数(或者说,当前已写入的节点个数)
int max_size;//最大允许存储的节点个数
int keep_last;//是否要保留最后一个读节点
int rindex_shown;//当前节点是否已经显示
SDL_mutex *mutex;
SDL_cond *cond;
PacketQueue *pktq;//关联的PacketQueue
} FrameQueue;
FrameQueue不用数组实现队列(环形缓冲区)
从字段的定义上可以看出,FrameQueue的设计显然比PacketQueue要复杂。在深入代码分析之前,先给出其设计理念:
- 高效率的读写模型(回顾PacketQueue的设计,每次访问都需要加锁整个队列,锁范围很大)
- 高效的内存模型(节点内存以数组形式预分配,无需动态分配)
- 环形缓冲区设计,同时可以访问上一读节点
下面是开放的对外接口:
- frame_queue_init:初始化
- frame_queue_peek_last:获取当前播放器显示的帧
- frame_queue_peek:获取待显示的第一个帧
- frame_queue_peek_next:获取待显示的第二个帧
- frame_queue_peek_writable:获取queue中一块Frame大小的可写内存
- frame_queue_peek_readable:这方法和frame_queue_peek的作用一样, 都是获取待显示的第一帧
- frame_queue_push:推入一帧数据, 其实数据已经在调用这个方法前填充进去了, 这个方法的作用是将队列的写索引(也就是队尾)向后移, 还有将这个队列中的Frame的数量加一。
- frame_queue_next:将读索引(队头)后移一位, 还有将这个队列中的Frame的数量减一
- frame_queue_nb_remaining:返回队列中待显示帧的数目
- frame_queue_last_pos:返回正在显示的帧的position
- frame_queue_destory:释放Frame,释放互斥锁和互斥量
- frame_queue_unref_item:取消引用帧引用的所有缓冲区并重置帧字段,释放给定字幕结构中的所有已分配数据。
packet_queue_init()
PacketQueue操作提供以下方法:
- packet_queue_init:初始化
- packet_queue_destroy:销毁
- packet_queue_start:启用,设置abort_request为0,先放一个flush_pkt;
- packet_queue_abort:中止
- packet_queue_get:获取一个节点
- packet_queue_put:存入一个节点
- packet_queue_put_nullpacket:存入一个空节点
- packet_queue_flush:清除队列内所有的节点
ffplay用AVPacket保存解封装后的数据(压缩状态),即保存AVPacket,AVFrame用于存储解码后的音频或者视频数据。
ffplay首先定义了一个结构体AVFifoBuffer:
typedef struct AVFifoBuffer {
uint8_t *buffer;
uint8_t *rptr, *wptr, *end;
uint32_t rndx, wndx;
} AVFifoBuffer;
uint8_t * buffer 缓存区
uint8_t * rptr 读指针
uint8_t * wptr 写指针
uint8_t * end 末尾指针
uint32_t rndx 读索引
uint32_t wndx 写索引
接着定义另一个结构体PacketQueue:
/* 保存解封装后的数据,即保存AVPacket */
typedef struct PacketQueue {
AVFifoBuffer *pkt_list; ///< AV先进先出缓存器
int nb_packets; ///< 队列中一共有多少个节点
int size; ///< 队列所有节点字节总数,用于计算cache大小
int64_t duration; ///<队列所有节点的合计时长
int abort_request; ///< 是否要中止队列操作,用于安全快速退出播放
int serial; ///<序列号,和MyAVPacketList的serial作用相同,但改变的时序稍微有点不同
SDL_mutex *mutex; ///<用于维持PacketQueue的多线程安全(SDL_mutex可以按pthread_mutex_t理解)
SDL_cond *cond; ///<用于读、写线程相互通知(SDL_cond可以按pthread_cond_t理解)
} PacketQueue;
read_thread()
read_thread()调用了如下函数:
avformat_open_input():打开媒体。
avformat_find_stream_info():获得媒体信息。
av_dump_format():输出媒体信息到控制台。
stream_component_open():分别打开视频/音频/字幕解码线程。
refresh_thread():视频刷新线程。
av_read_frame():获取一帧压缩编码数据(即一个AVPacket)。
packet_queue_put():根据压缩编码数据类型的不同(视频/音频/字幕),放到不同的PacketQueue中。
refresh_thread()
refresh_thread()调用了如下函数:
SDL_PushEvent(FF_REFRESH_EVENT):发送FF_REFRESH_EVENT的SDL_Event
av_usleep():每两次发送之间,间隔一段时间。
stream_component_open()
stream_component_open()用于打开视频/音频/字幕解码的线程。stream_component_open()调用了如下函数:
avcodec_find_decoder():获得解码器。
avcodec_open2():打开解码器。
audio_open():打开音频解码。
SDL_PauseAudio(0):SDL中播放音频的函数。
video_thread():创建视频解码线程。
subtitle_thread():创建字幕解码线程。
packet_queue_start():初始化PacketQueue。
audio_open()调用了如下函数
SDL_OpenAudio():SDL中打开音频设备的函数。注意它是根据SDL_AudioSpec参数打开音频设备。SDL_AudioSpec中的callback字段指定了音频播放的回调函数sdl_audio_callback()。当音频设备需要更多数据的时候,会调用该回调函数。因此该函数是会被反复调用的。
下面来看一下SDL_AudioSpec中指定的回调函数sdl_audio_callback()。
sdl_audio_callback()调用了如下函数
audio_decode_frame():解码音频数据。
update_sample_display():当不显示视频图像,而是显示音频波形的时候,调用此函数。
audio_decode_frame()调用了如下函数
packet_queue_get():获取音频压缩编码数据(一个AVPacket)。
avcodec_decode_audio4():解码音频压缩编码数据(得到一个AVFrame)。
swr_init():初始化libswresample中的SwrContext。libswresample用于音频采样采样数据(PCM)的转换。
swr_convert():转换音频采样率到适合系统播放的格式。
swr_free():释放SwrContext。
video_thread()调用了如下函数
avcodec_alloc_frame():初始化一个AVFrame。
get_video_frame():获取一个存储解码后数据的AVFrame。
queue_picture():
get_video_frame()调用了如下函数
packet_queue_get():获取视频压缩编码数据(一个AVPacket)。
avcodec_decode_video2():解码视频压缩编码数据(得到一个AVFrame)。
queue_picture()调用了如下函数
SDL_LockYUVOverlay():锁定一个SDL_Overlay。
sws_getCachedContext():初始化libswscale中的SwsContext。Libswscale用于图像的Raw格式数据(YUV,RGB)之间的转换。注意sws_getCachedContext()和sws_getContext()功能是一致的。
sws_scale():转换图像数据到适合系统播放的格式。
SDL_UnlockYUVOverlay():解锁一个SDL_Overlay。
subtitle_thread()调用了如下函数
packet_queue_get():获取字幕压缩编码数据(一个AVPacket)。
avcodec_decode_subtitle2():解码字幕压缩编码数据。
event_loop()
FFplay再打开媒体之后,便会进入event_loop()函数,永远不停的循环下去。该函数用于接收并处理各种各样的消息。有点像Windows的消息循环机制。
根据event_loop()中SDL_WaitEvent()接收到的SDL_Event类型的不同,会调用不同的函数进行处理(从编程的角度来说就是一个switch()语法)。图中仅仅列举了几个例子:
SDLK_ESCAPE(按下“ESC”键):do_exit()。退出程序。
SDLK_f(按下“f”键):toggle_full_screen()。切换全屏显示。
SDLK_SPACE(按下“空格”键):toggle_pause()。切换“暂停”。
SDLK_DOWN(按下鼠标键):stream_seek()。跳转到指定的时间点播放。
SDL_VIDEORESIZE(窗口大小发生变化):SDL_SetVideoMode()。重新设置宽高。
FF_REFRESH_EVENT(视频刷新事件(自定义事件)):video_refresh()。刷新视频。
方案
由于本次的目标是要将指定的块涂黑,所以在操作时只要将目标部分的Y数据清零就可以。
所以可以在video_image_display函数内部将要显示的图片直接进行处理,然后将处理后的图片进行显示
static void video_image_display(VideoState *is)
{
Frame *vp;
Frame *sp = NULL;
SDL_Rect rect;
vp = frame_queue_peek_last(&is->pictq); //取要显示的视频帧 最后一帧
if (is->subtitle_st) { //字幕显示逻辑
if (frame_queue_nb_remaining(&is->subpq) > 0) {
sp = frame_queue_peek(&is->subpq);
if (vp->pts >= sp->pts + ((float) sp->sub.start_display_time / 1000)) {
if (!sp->uploaded) {
uint8_t* pixels[4];
int pitch[4];
int i;
if (!sp->width || !sp->height) {
sp->width = vp->width;
sp->height = vp->height;
}
if (realloc_texture(&is->sub_texture, SDL_PIXELFORMAT_ARGB8888, sp->width, sp->height, SDL_BLENDMODE_BLEND, 1) < 0)
return;
for (i = 0; i < sp->sub.num_rects; i++) {
AVSubtitleRect *sub_rect = sp->sub.rects[i];
sub_rect->x = av_clip(sub_rect->x, 0, sp->width );
sub_rect->y = av_clip(sub_rect->y, 0, sp->height);
sub_rect->w = av_clip(sub_rect->w, 0, sp->width - sub_rect->x);
sub_rect->h = av_clip(sub_rect->h, 0, sp->height - sub_rect->y);
is->sub_convert_ctx = sws_getCachedContext(is->sub_convert_ctx,
sub_rect->w, sub_rect->h, AV_PIX_FMT_PAL8,
sub_rect->w, sub_rect->h, AV_PIX_FMT_BGRA,
0, NULL, NULL, NULL);
if (!is->sub_convert_ctx) {
av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");
return;
}
if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)pixels, pitch)) {
sws_scale(is->sub_convert_ctx, (const uint8_t * const *)sub_rect->data, sub_rect->linesize,
0, sub_rect->h, pixels, pitch);
SDL_UnlockTexture(is->sub_texture);
}
}
sp->uploaded = 1;
}
} else
sp = NULL;
}
}
//将帧宽高按照sar最大适配到窗口
calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height, vp->width, vp->height, vp->sar);
if (!vp->uploaded) { //如果是重复显示上一帧,那么uploaded就是1
// if (upload_texture(&is->vid_texture, add_filter_deal(&is->add_filter, vp->frame), &is->img_convert_ctx) < 0)
if (upload_texture(&is->vid_texture,vp->frame, &is->img_convert_ctx) < 0)
return;
vp->uploaded = 1;
vp->flip_v = vp->frame->linesize[0] < 0;
}
set_sdl_yuv_conversion_mode(vp->frame);
SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL, vp->flip_v ? SDL_FLIP_VERTICAL : 0);
set_sdl_yuv_conversion_mode(NULL);
if (sp) {
#if USE_ONEPASS_SUBTITLE_RENDER
SDL_RenderCopy(renderer, is->sub_texture, NULL, &rect);
#else
int i;
double xratio = (double)rect.w / (double)sp->width;
double yratio = (double)rect.h / (double)sp->height;
for (i = 0; i < sp->sub.num_rects; i++) {
SDL_Rect *sub_rect = (SDL_Rect*)sp->sub.rects[i];
SDL_Rect target = {.x = rect.x + sub_rect->x * xratio,
.y = rect.y + sub_rect->y * yratio,
.w = sub_rect->w * xratio,
.h = sub_rect->h * yratio};
SDL_RenderCopy(renderer, is->sub_texture, sub_rect, &target);
}
#endif
}
}
仔细观察可以发现有一句注释:
// if (upload_texture(&is->vid_texture, add_filter_deal(&is->add_filter, vp->frame), &is->img_convert_ctx) < 0)
在这个位置进行处理就可以,add_filter_deal函数是我自己写的处理函数,最后返回一个视频帧。
这个位置是在进行显示前进行修改的,那么有没有其他的方式呢,当然后了,看下面:
static int video_thread(void *arg)
{
VideoState *is = arg;
AVFrame *frame = av_frame_alloc();
double pts;
double duration;
int ret;
AVRational tb = is->video_st->time_base;
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
#if CONFIG_AVFILTER
AVFilterGraph *graph = NULL;
AVFilterContext *filt_out = NULL, *filt_in = NULL;
int last_w = 0;
int last_h = 0;
enum AVPixelFormat last_format = -2;
int last_serial = -1;
int last_vfilter_idx = 0;
#endif
if (!frame)
return AVERROR(ENOMEM);
for (;;) {
ret = get_video_frame(is, frame);
if (ret < 0)
goto the_end;
if (!ret)
continue;
#if CONFIG_AVFILTER
if ( last_w != frame->width
|| last_h != frame->height
|| last_format != frame->format
|| last_serial != is->viddec.pkt_serial
|| last_vfilter_idx != is->vfilter_idx) {
av_log(NULL, AV_LOG_DEBUG,
"Video frame changed from size:%dx%d format:%s serial:%d to size:%dx%d format:%s serial:%d\n",
last_w, last_h,
(const char *)av_x_if_null(av_get_pix_fmt_name(last_format), "none"), last_serial,
frame->width, frame->height,
(const char *)av_x_if_null(av_get_pix_fmt_name(frame->format), "none"), is->viddec.pkt_serial);
avfilter_graph_free(&graph);
graph = avfilter_graph_alloc();
if (!graph) {
ret = AVERROR(ENOMEM);
goto the_end;
}
graph->nb_threads = filter_nbthreads;
if ((ret = configure_video_filters(graph, is, vfilters_list ? vfilters_list[is->vfilter_idx] : NULL, frame)) < 0) {
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
goto the_end;
}
filt_in = is->in_video_filter;
filt_out = is->out_video_filter;
last_w = frame->width;
last_h = frame->height;
last_format = frame->format;
last_serial = is->viddec.pkt_serial;
last_vfilter_idx = is->vfilter_idx;
frame_rate = av_buffersink_get_frame_rate(filt_out);
}
frame = add_filter_deal(&is->add_filter, frame);
ret = av_buffersrc_add_frame(filt_in, frame);//TODO
if (ret < 0)
goto the_end;
while (ret >= 0) {
is->frame_last_returned_time = av_gettime_relative() / 1000000.0;
ret = av_buffersink_get_frame_flags(filt_out, frame, 0);
if (ret < 0) {
if (ret == AVERROR_EOF)
is->viddec.finished = is->viddec.pkt_serial;
ret = 0;
break;
}
is->frame_last_filter_delay = av_gettime_relative() / 1000000.0 - is->frame_last_returned_time;
if (fabs(is->frame_last_filter_delay) > AV_NOSYNC_THRESHOLD / 10.0)
is->frame_last_filter_delay = 0;
tb = av_buffersink_get_time_base(filt_out);
#endif
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
ret = queue_picture(is, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
av_frame_unref(frame);
#if CONFIG_AVFILTER
if (is->videoq.serial != is->viddec.pkt_serial)
break;
}
#endif
if (ret < 0)
goto the_end;
}
the_end:
#if CONFIG_AVFILTER
avfilter_graph_free(&graph);
#endif
av_frame_free(&frame);
return 0;
}
里面有这么一句:
frame = add_filter_deal(&is->add_filter, frame);
是不是特别熟悉,看见了add_filter_deal函数,这是在读取并解码完视频帧时就直接进行YUV数据的修改。
其实处理视频数据时我们要分析要进行处理哪些数据,知道自己要处理哪些数据后,再清楚这个数据会在哪些地方出现,然后只要最后处理数据就可以了。
下面是需求的源码:
ypedef struct AddImgFilter {
int array_width; // 预设待处理视频帧的宽
int array_height; // 预设待处理视频帧的高
int slide_count_width; //处理视频帧 Y分量时行 行程
int slide_count_height; //处理视频帧 Y分量时列 行程
int array_len; //存储滤镜的数组长度
uint8_t *array; //指向数组的指针 滤镜数据存储
uint8_t buf[1024]; //缓存buffer
uint8_t enable_fileter;
AVFrame *frame; //存储处理函数的输入帧原始数据 然后进行处理与返回
} AddImgFilter;
static void get_bin_file(void)
{
uint8_t buf[1024];
buf[0] = 0x7e; // 0开始 帧头
buf[1] = 0x00; // 1开始 4字节 数据长度
buf[2] = 0x00;
buf[3] = 0x00;
buf[4] = 0x64;
buf[5] = 0x00; // 5开始 4字节 滤镜宽
buf[6] = 0x00;
buf[7] = 0x01;
buf[8] = 0xe0;
buf[9] = 0x00; // 9开始 4字节 滤镜高
buf[10] = 0x00;
buf[11] = 0x01;
buf[12] = 0x10;
buf[13] = 0x00; // 13开始 4字节 滤镜数组长度
buf[14] = 0x00;
buf[15] = 0x00;
buf[16] = 0x58;
memset(&buf[17], 0xaa, 88); // 17开始 n 字节 滤镜数组数据
memset(&buf[105], 0x7e, 1);
FILE *bin_file;
if((bin_file=fopen("filterbin.bin","wb")) != NULL)
{
fwrite(buf, 1, 106, bin_file);
fclose(bin_file);
}
else
printf( "\nCan not open the path: %s \n", "filterbin.bin");
}
// AddImgFilter 构造函数
static void add_filter_init(AddImgFilter *add_filter)
{
get_bin_file();
memset(add_filter, 0, sizeof(*add_filter));
add_filter->enable_fileter = 0; //关闭滤镜
FILE *bin_file;
if((bin_file=fopen("filterbin.bin","rb")) != NULL)
{
fread(add_filter->buf, sizeof(uint8_t), 1024, bin_file);
// for(int i = 0; i<1024; i++)
// printf("add_filter->buf[%d] %x\n", i, add_filter->buf[i]);
fclose(bin_file);
if(add_filter->buf[0] == 0x7e) //帧头
{
printf("add_filter 1byte ->buf[0] -> %x \n", add_filter->buf[0] );
int data_len = (add_filter->buf[1] << 24) | (add_filter->buf[2] << 16) | (add_filter->buf[3] << 8) | add_filter->buf[4];
printf("data_len 4byte -> %d\n", data_len);
if(add_filter->buf[data_len + 5] == 0x7e) //data_len 不包括帧尾(1字节),以及本身(4字节)
{
add_filter->array_width = (add_filter->buf[5] << 24) | (add_filter->buf[6] << 16) | (add_filter->buf[7] << 8) | add_filter->buf[8];
printf("add_filter->array_width 4byte -> %d\n", add_filter->array_width);
add_filter->array_height = (add_filter->buf[9] << 24) | (add_filter->buf[10] << 16) | (add_filter->buf[11] << 8) | add_filter->buf[12];
printf("add_filter->array_height 4byte -> %d\n", add_filter->array_height);
add_filter->array_len = (add_filter->buf[13] << 24) | (add_filter->buf[14] << 16) | (add_filter->buf[15] << 8) | add_filter->buf[16];
printf("add_filter->array_len 4byte -> %d\n", add_filter->array_len);
add_filter->array = &add_filter->buf[17];
printf("add_filter %dbyte ->buf[%d]-> %x\n",data_len , data_len + 5, add_filter->buf[data_len + 5]);
add_filter->enable_fileter = 1; //解析出正确滤镜信息 开启滤镜
}
}
// add_filter->format = AV_PIX_FMT_YUV420P; ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
add_filter->slide_count_width = 0;
add_filter->slide_count_height = 0;
}
else
printf( "\nCan not open the path: %s \n", "filterbin.bin");
}
// AddImgFilter 析构函数
static void add_filter_uninit(AddImgFilter *add_filter)
{
add_filter = NULL; // AddImgFilter 作为 VideoState 的成员,会在 VideoState 的析构函数内进行销毁
}
static void add_group_filter(AddImgFilter *add_filter, int tar_row, int tar_col)
{
for(int col = 0; col < 16; col++) // 列数 0~15
{
if(tar_col + col < add_filter->array_height)
{
for(int row = 0; row < 16; row++) //行数 0~15
{
if(tar_row + row < add_filter->array_width)
{
add_filter->frame->data[0][(tar_col + col) * add_filter->frame->linesize[0] + (tar_row + row)] = 0;
}
}
}
}
}
// AddImgFilter 帧处理函数
static AVFrame *add_filter_deal(AddImgFilter *add_filter, AVFrame *frame)
{
add_filter->frame = av_frame_clone(frame);
add_filter->slide_count_width = 0;
add_filter->slide_count_height = 0;
if(add_filter->enable_fileter == 1) //滤镜开启状态才进行处理
{
if(add_filter->array_width != add_filter->frame->width || add_filter->array_height != add_filter->frame->height)
{
add_filter->enable_fileter = 0;
printf("The width height of input video is different from that of setting filter, and the filter processing will not be carried out ");
}
for(int i = 0; i < add_filter->array_len ; i++)
{
for(int j = 7; j >= 0; j--)
{
if((add_filter->array[i] >> j) & 0x01)
{
add_group_filter(add_filter, add_filter->slide_count_width, add_filter->slide_count_height);
}
add_filter->slide_count_width = (add_filter->slide_count_width < add_filter->array_width)? add_filter->slide_count_width + 16 : 0;
add_filter->slide_count_height = (add_filter->slide_count_width == 0)? add_filter->slide_count_height + 16 : add_filter->slide_count_height;
}
}
}
return add_filter->frame;
}