ffplay实现自定义滤镜小项目

本文介绍如何在FFplay中实现自定义视频滤镜,通过修改YUV数据达到特定视频处理效果,如局部涂黑。文章详细展示了定制过程,包括二进制文件解析及与视频帧数据的结合。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

需求

最近需要做一个基于ffplay的自定义滤镜显示,需要根据一个二进制bin文件,实际上是要取其中的一段流,根据其中的信息对视频进行涂黑处理,即当取到的流的一位为1时,涂黑视频的一个16*16的像素块。
放一个做好后得示例图,方便更直观的理解我的意图。
在这里插入图片描述

思考

因为涉及到这个小工具要播放本地视频,拉流播放等,所以就直接更改ffplay的源码,生成定制版本的ffplay。

本来想好好整理一篇文章的,但是东西太多了,后期直接放弃,想看怎么做的效果部分代码,直接翻到最下面看。

总体结构图

这里放一张雷神画的ffplay总体结构图,雷神文章
在这里插入图片描述
雷神的文章给了很大的帮助,因为使用的是ffmpeg4.3.1,所以与雷神的文章略有差异,但是原理是相同的。
另一篇文章流程图更为具体,而且也也符合现在的ffmpeg4.0,也可以看下ffplay流程图
然后为了熟悉整体的流程,自己画了一幅ffplay流程图。
ffplay流程图

main()函数

ffplay是作为一个小工具的方式对外提供的,其编译生成后存放在ffmpeg的bin目录下,ffplay是一个独立的播放器核心。
mian()函数调用

在main()函数内调用了以下几个函数:

avdevice_register_all():注册所有编码器和解码器。
avformat_network_init():初始化网络库,在官方的文档里陈述这个函数是可选的,由于ffplay涉及到拉流播放等,所以在ffplay内对其进行了初始化。这是一个avformat的一个接口,其对应的文档libavformat文档
show_banner():打印输出FFmpeg版本信息(编译时间,编译选项,类库信息等)。

show_banner效果图

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_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
wSDL窗口的宽
hSDL窗口的高
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或者多个

SDL_CreateRedner() Wiki

stream_open()

stream_open()的作用是打开输入的媒体。这个函数还是比较复杂的,包含了FFplay中各种线程的创建。它的函数调用结构如下图所示。
stream_open流程图

frame_queue_init()

其是结构体FrameQueue对外提供的队列初始化函数。
参考这两篇博文:ffplay frame queue分析1ffplay 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;

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值