[ffmpeg] 多输入滤波同步方式(framesync)
滤波也不总是单一的输入,也存在对多个输入流进行滤波的需求,最常见的就是对视频添加可视水印,水印的组成通常为原视频以及作为水印的图片或者小动画,在ffmpeg中可以使用overlay滤波器进行水印添加。
对于多视频流输入的滤波器,ffmpeg提供了一个名为framesync的处理方案。framesync为滤波器分担了不同线路的输入的帧同步任务,并为滤波器提供同步过后的帧,使得滤波器专注于滤波处理。
Extend Mode
由于各个视频流可能长短不一,可能起始或者结束时间也不同,为了应对由此产生的各种需求,framesync为每个输入流的起始以及结束都提供了3种可选的扩展方式
Mode | before(流开始前) | after(流结束后) |
EXT_STOP | 在这个流开始前的这段时间不可以进行滤波处理。如果有多个流都指定了before=EXT_STOP,那么以时间线最后的流为准。 | 在这个流结束后滤波处理必须停止。如果有多个流都指定了after=EXT_STOP,那么以时间线最前的流为准。 |
EXT_NULL | 其余的流可以在缺少了该流的情况下执行滤波处理。 | 其余的流可以在缺少了该流的情况下执行滤波处理。 |
EXT_INFINITY | 在这个流开始前的这段时间,提供这一个流的第一帧给滤波器进行处理。 | 在这个流结束后的这段时间,提供这一个流的最后一帧给滤波器进行处理。 |
Sync
在framesync所提供的同步服务中,滤波器可以为输入流设置同步等级,同步等级最高的输入流会被当作同步基准。
如上图所示,不同的输入流可能有不同的帧率,因此有必要对输入的流进行同步。上面的例子中,input stream 1的同步级别最高,因此以该流为同步基准,即每次得到input stream 1的帧时,可以进行滤波处理。滤波处理所提供的帧为各个流最近所获得的帧,在上面的例子中,当input stream 1获得序号为2的帧时,input stream 2刚刚所获得的帧序号为3,input stream 3刚刚所获得的帧序号为1,因此滤波时framesync所提供的帧分别为stream 1的2、stream 2的3、stream 3的1。
Example
滤波器调用framesync需要执行如下代码:
typedef struct Context {
FFFrameSync fs; //Context involves FFFrameSync
} Context; static int process_frame(FFFrameSync *fs)
{
Context *s = fs->opaque; AVFrame *in1, *in2, *in3;
int ret; //get frame before filtering
if ((ret = ff_framesync_get_frame(&s->fs, 0, &in1, 0)) < 0 ||
(ret = ff_framesync_get_frame(&s->fs, 1, &in2, 0)) < 0 ||
(ret = ff_framesync_get_frame(&s->fs, 2, &in3, 0)) < 0) //filtering
} //Before filtering, we can only get timebase in function config_output.
//See avfilter_config_links
static int config_output(AVFilterLink *outlink)
{
FFFrameSyncIn *in; ret = ff_framesync_init(&s->fs, ctx, 3); //init framesync
if (ret < 0)
return ret; //set inputs parameter: timebase, sync level, before mode, after mode
in = s->fs.in;
in[0].time_base = srclink1->time_base;
in[1].time_base = srclink2->time_base;
in[2].time_base = srclink3->time_base;
in[0].sync = 2;
in[0].before = EXT_STOP;
in[0].after = EXT_STOP;
in[1].sync = 1;
in[1].before = EXT_NULL;
in[1].after = EXT_INFINITY;
in[2].sync = 1;
in[2].before = EXT_NULL;
in[2].after = EXT_INFINITY; //save Context to fs.opaque which will be used on filtering
s->fs.opaque = s; //filtering function
s->fs.on_event = process_frame; return ff_framesync_configure(&s->fs); //framesync configure
} static int activate(AVFilterContext *ctx)
{
RemapContext *s = ctx->priv;
return ff_framesync_activate(&s->fs); //call filtering function if frame ready
} static av_cold void uninit(AVFilterContext *ctx)
{
RemapContext *s = ctx->priv; ff_framesync_uninit(&s->fs);
} static const AVFilterPad remap_inputs[] = {
{
.name = "source 1",
.type = AVMEDIA_TYPE_VIDEO,
.config_props = config_input,
},
{
.name = "source 2",
.type = AVMEDIA_TYPE_VIDEO,
},
{
.name = "source 3",
.type = AVMEDIA_TYPE_VIDEO,
},
{ NULL }
}; static const AVFilterPad remap_outputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
.config_props = config_output,
},
{ NULL }
};
可以发现使用framesync有如下要求:
- 在滤波器的参数结构体(Context)内包含FFFramesync结构体。
- 在进行滤波处理时,调用ff_framesync_get_frame来获得framesync同步后的帧。
- 在config_output时或之前调用ff_framesync_init来进行framesync初始化。
- 在config_output时设置各个输入的time base,extend mode,sync level,并调用ff_framesync_configure进行配置。
- 在config_output时或之前设置fs->opaque=context(参数结构体),用于后续滤波处理。
- 在config_output时或之前设置用于回调的滤波处理函数fs->on_event=process_frame。
- 在activate时调用ff_framesync_activate。在该函数内部如果frame ready,就会执行回调函数。
framesync的同步实现
framesync的同步实现主要集中在ff_framesync_activate所调用的framesync_advance函数当中。
static int framesync_advance(FFFrameSync *fs)
{
while (!(fs->frame_ready || fs->eof)) {
ret = consume_from_fifos(fs);
if (ret <= 0)
return ret;
}
return 0;
}
framesync_advance内是一个循环,退出该循环需要满足任意如下一个条件:
- fs->frame_ready==1。代表接下来可以执行滤波处理。
- fs->eof==1。代表结束整个滤波处理。
- ret = consume_from_fifos(fs) <= 0。返回值小于0代表出错;返回值等于0代表目前无法都从所有的输入流中得到帧。
从consume_from_fifos开始分析,我们将会对framesync的同步机制有详细的了解。
static int consume_from_fifos(FFFrameSync *fs)
{
AVFilterContext *ctx = fs->parent;
AVFrame *frame = NULL;
int64_t pts;
unsigned i, nb_active, nb_miss;
int ret, status; nb_active = nb_miss = 0;
for (i = 0; i < fs->nb_in; i++) {
if (fs->in[i].have_next || fs->in[i].state == STATE_EOF)
continue;
nb_active++;
ret = ff_inlink_consume_frame(ctx->inputs[i], &frame);
if (ret < 0)
return ret;
if (ret) {
av_assert0(frame);
framesync_inject_frame(fs, i, frame);
} else {
ret = ff_inlink_acknowledge_status(ctx->inputs[i], &status, &pts);
if (ret > 0) {
framesync_inject_status(fs, i, status, pts);
} else if (!ret) {
nb_miss++;
}
}
}
if (nb_miss) {
if (nb_miss == nb_active && !ff_outlink_frame_wanted(ctx->outputs[0]))
return FFERROR_NOT_READY;
for (i = 0; i < fs->nb_in; i++)
if (!fs->in[i].have_next && fs->in[i].state != STATE_EOF)
ff_inlink_request_frame(ctx->inputs[i]);
return 0;
}
return 1;
}
在consume_from_fifos返回1代表目前已经从所有的输入流中获得了帧。
- 如果已经从某个输入获得了帧,则不需要再次去获取。
- 如果某个输入流还未获得帧,则会调用ff_inlink_comsume_frame尝试从输入link中获取帧。
- 如果得到了帧,就会调用framesync_inject_frame把从输入流中获得的帧存放在fs->in[i].frame_next中,并用fs->in[i].have_next表示第i个输入流已经获得了帧。
- 如果没有获得帧,则调用ff_inlink_acknowledge_status检查是否出错或者EOF,是则表明该输入流结束,不是则表明前面的滤波器实例无法为我们提供帧。
- 由于无法获得我们所需要的帧,因此要调用ff_inlink_request_frame向前面的滤波器实例发出请求。
- 只有当从所有的输入流都得到帧后,consume_from_fifos才会返回1。
consume_from_fifos返回1的时候,所有输入流的帧缓存fs->in[i].frame_next都存储了一帧,该帧缓存标志fs->in[i].have_next的值都为1。然后进行下列同步处理:
static int framesync_advance(FFFrameSync *fs)
{
unsigned i;
int64_t pts;
int ret; while (!(fs->frame_ready || fs->eof)) {
ret = consume_from_fifos(fs);
if (ret <= 0)
return ret; pts = INT64_MAX;
for (i = 0; i < fs->nb_in; i++) //get the least pts frame
if (fs->in[i].have_next && fs->in[i].pts_next < pts)
pts = fs->in[i].pts_next;
if (pts == INT64_MAX) {
framesync_eof(fs);
break;
}
for (i = 0; i < fs->nb_in; i++) {
if (fs->in[i].pts_next == pts ||
(fs->in[i].before == EXT_INFINITY &&
fs->in[i].state == STATE_BOF)) {
av_frame_free(&fs->in[i].frame);
fs->in[i].frame = fs->in[i].frame_next; //move from frame_next to frame
fs->in[i].pts = fs->in[i].pts_next;
fs->in[i].frame_next = NULL;
fs->in[i].pts_next = AV_NOPTS_VALUE;
fs->in[i].have_next = 0;
fs->in[i].state = fs->in[i].frame ? STATE_RUN : STATE_EOF;
if (fs->in[i].sync == fs->sync_level && fs->in[i].frame)//the highest level frame
fs->frame_ready = 1;
if (fs->in[i].state == STATE_EOF &&
fs->in[i].after == EXT_STOP)
framesync_eof(fs);
}
}
if (fs->frame_ready)
for (i = 0; i < fs->nb_in; i++)
if ((fs->in[i].state == STATE_BOF &&
fs->in[i].before == EXT_STOP))
fs->frame_ready = 0;
fs->pts = pts;
}
return 0;
}
这里我们把frame_next当作从上一滤波器实例中获取的帧缓存,frame当作接下来会用于进行滤波处理的帧缓存。
- 从所缓存的帧(frame_next)中提取pts最小的一帧。
- 存放到用于提供给滤波器的缓存中(frame = frame_next)。
- 把这一帧所在输入流帧缓存设置为空(frame_next = NULL)。
- 如果这一帧所在的输入流是同步级别最高的流,表明此时在frame中该同步级别最高的流所输入的帧的pts最大,符合我们前面的同步描述,因此设置frame_ready = 1,表明接下来可以进行滤波处理。
- 如果这一帧所在的输入流不是同步级别最高的流,则需要继续执行下一循环(执行consume_from_fifos)。
以我们前面所展示的图片为例
每次都把frame_next中pts最小的一帧放入frame时,同时也表明在frame中新所放入的一帧永远是pts最大的一帧。当被放入到frame中的帧是属于最高同步等级的输入流的时候,可以执行滤波处理。如果我们把这一帧的pts定义为同步pts,此时其余的输入流中的帧的pts尽管比同步pts小,不过也是各自输入流中最大的,这与我们前面所说的同步处理是一致的。
framesync的实现总结来说就是循环执行:
- 从输入流中提取帧填补空缺的frame_next。
- 当所有输入流的frame_next都被写入帧后(即所有输入流的have_next都为1)consume_from_fifos才会返回1,然后进行各个流之间的pts比较。
- 接下来把pts最小的帧从frame_next存入frame,如此一来该frame_next又会出现空缺。
这种实现方式能保证所有的帧都是以pts从小到大由frame_next移入frame的,能防止帧被遗漏。
[ffmpeg] 多输入滤波同步方式(framesync)的更多相关文章
- Ffmpeg和SDL如何同步音频
ong> 同步音頻 现在我们已经有了一个比较像样的播放器.所以让我们看一下还有哪些零碎的东西没处理.上次,我们掩饰了一点同步问题,也就是同步音频到视频而不是其它的同步方式.我们将采用和视频一样的 ...
- FFmpeg--如何同步音视频的解决方案
如何同步视频 PTS和DTS 幸运的是,音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息在里面.音频流有采样,视频流有每秒的帧率.然而,如果我们只是简单的通过数帧和乘以帧率的方式来同步视频 ...
- C# 的TCP Socket (同步方式)
简单的c# TCP通讯(TcpListener) C# 的TCP Socket (同步方式) C# 的TCP Socket (异步方式) C# 的tcp Socket设置自定义超时时间 C# TCP ...
- java笔记--关于线程同步(7种同步方式)
关于线程同步(7种方式) --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3897440.html"谢谢-- 为何要使用同步? ...
- 如何在silverlight中以同步方式 获取sharepoint2013站点的当前登录账号
最近有个项目用到了silverlight要同步方式获取当前登录账号.异步的方式无法跟其他应用结合.主要先后顺序问题.但是silverlight非常不好获取到当前登录账号.即使获取到了也是异步方式获取. ...
- Linux多线程同步方式
当多个线程共享相同的内存时,需要确保每个线程看到一致的数据视图,当多个线程同时去修改这片内存时,就可能出现偏差,得到与预期不符合的值.为啥需要同步,一件事情逻辑上一定是有序的,即使在并发环境下:而操作 ...
- linux线程间同步方式汇总
抽空做了下linux所有线程间同步方式的汇总(原生的),包含以下几个: 1, mutex 2, condition variable 3, reader-writer lock 4, spin loc ...
- Windows Phone 同步方式获取网络类型
原文:Windows Phone 同步方式获取网络类型 在Windows Phone 开发中有时候需要获取设备当前连接网络的类型,是Wifi,还是2G,3G,或者4G,SDK中提供获取网络类型的API ...
- Java之线程安全中的三种同步方式
一个程序在运行起来时,会转换为进程,通常含有多个线程. 通常情况下,一个进程中的比较耗时的操作(如长循环.文件上传下载.网络资源获取等),往往会采用多线程来解决. 比如,现实生活中,银行取钱问题.火车 ...
随机推荐
- MIPI DSI之DBI DPI含义和区别(3-1)
一.MIPI MIPI(Mobile Industry Processor Interface/移动工业处理器接口)是2003年由ARM.Nokia.ST 等公司成立联盟并为移动应用处理器制定的一个开 ...
- 把项目中那些恶心的无处存储的大块数据都丢到FastDFS之快速搭建
在我们开发项目的时候,经常会遇到大块数据的问题(2M-100M),比如说保存报表中1w个人的ID号,他就像一个肿瘤一样,存储在服务器哪里都 觉得恶心,放在redis,mongodb中吧,一下子 ...
- 几分钟搞定redis存储session共享——设计实现
前面我们写过C#在redis中存储常用的5种数据类型demo,没看过的可以点击电梯直达:https://www.cnblogs.com/xiongze520/p/10267804.html 我们上一篇 ...
- Java面试 32个核心必考点完全解析
目录 课程预习 1.1 课程内容分为三个模块 1.2 换工作面临问题 1.3 课程特色 课时1:技术人职业发展路径 1.1 工程师发展路径 1.2 常见技术岗位划分 1.3 面试岗位选择 1.4 常见 ...
- Android视频录制从不入门到入门系列教程(三)————视频方向
运行Android视频录制从不入门到入门系列教程(二)————显示视频图像中的Demo后,我们应该能发现视频的方向是错误的. 由于Android中,Camera给我们的视频图片的原始方向是下图这个样子 ...
- 阿里云服务器部署Java Web项目全过程
最近需要将一个Java Web项目部署到服务器上,方便多人共享访问.这也是我第一次接触服务器之类的东西,也花了一点时间,最终总算部署成功,写下一篇文章记录以便日后回顾. 购买服务器 第一步当然是需要购 ...
- [20190416]exclusive latch测试脚本.txt
[20190416]exclusive latch测试脚本.txt --//昨天做了shared latch的测试脚本,今天完善exclusive latch测试脚本,上个星期的测试我是手工执行的.- ...
- Aspnet mvc移除WebFormViewEngine
为了提高mvc的速度,在Global.asax中移除WebFormViewEngine protected void Application_Start() { RemoveWebFormEngine ...
- Oracle 安装步骤、安装中错误处理、完整卸载
/*************************************************以下ORACLE服务端安装************************************* ...
- [idea] SpringBoot整合swagger2实现CRUD
一:创建SpringBoot ,在pom.xml文件中加入jar包 <dependency> <groupId>io.springfox</groupId> < ...