滤波也不总是单一的输入,也存在对多个输入流进行滤波的需求,最常见的就是对视频添加可视水印,水印的组成通常为原视频以及作为水印的图片或者小动画,在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有如下要求:

  1. 在滤波器的参数结构体(Context)内包含FFFramesync结构体。
  2. 在进行滤波处理时,调用ff_framesync_get_frame来获得framesync同步后的帧。
  3. 在config_output时或之前调用ff_framesync_init来进行framesync初始化。
  4. 在config_output时设置各个输入的time base,extend mode,sync level,并调用ff_framesync_configure进行配置。
  5. 在config_output时或之前设置fs->opaque=context(参数结构体),用于后续滤波处理。
  6. 在config_output时或之前设置用于回调的滤波处理函数fs->on_event=process_frame。
  7. 在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代表目前已经从所有的输入流中获得了帧。

  1. 如果已经从某个输入获得了帧,则不需要再次去获取。
  2. 如果某个输入流还未获得帧,则会调用ff_inlink_comsume_frame尝试从输入link中获取帧。
  3. 如果得到了帧,就会调用framesync_inject_frame把从输入流中获得的帧存放在fs->in[i].frame_next中,并用fs->in[i].have_next表示第i个输入流已经获得了帧。
  4. 如果没有获得帧,则调用ff_inlink_acknowledge_status检查是否出错或者EOF,是则表明该输入流结束,不是则表明前面的滤波器实例无法为我们提供帧。
  5. 由于无法获得我们所需要的帧,因此要调用ff_inlink_request_frame向前面的滤波器实例发出请求。
  6. 只有当从所有的输入流都得到帧后,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当作接下来会用于进行滤波处理的帧缓存。

  1. 从所缓存的帧(frame_next)中提取pts最小的一帧。
  2. 存放到用于提供给滤波器的缓存中(frame = frame_next)。
  3. 把这一帧所在输入流帧缓存设置为空(frame_next = NULL)。
  4. 如果这一帧所在的输入流是同步级别最高的流,表明此时在frame中该同步级别最高的流所输入的帧的pts最大,符合我们前面的同步描述,因此设置frame_ready = 1,表明接下来可以进行滤波处理。
  5. 如果这一帧所在的输入流不是同步级别最高的流,则需要继续执行下一循环(执行consume_from_fifos)。

以我们前面所展示的图片为例

每次都把frame_next中pts最小的一帧放入frame时,同时也表明在frame中新所放入的一帧永远是pts最大的一帧。当被放入到frame中的帧是属于最高同步等级的输入流的时候,可以执行滤波处理。如果我们把这一帧的pts定义为同步pts,此时其余的输入流中的帧的pts尽管比同步pts小,不过也是各自输入流中最大的,这与我们前面所说的同步处理是一致的。

framesync的实现总结来说就是循环执行:

  1. 从输入流中提取帧填补空缺的frame_next。
  2. 当所有输入流的frame_next都被写入帧后(即所有输入流的have_next都为1)consume_from_fifos才会返回1,然后进行各个流之间的pts比较。
  3. 接下来把pts最小的帧从frame_next存入frame,如此一来该frame_next又会出现空缺。

这种实现方式能保证所有的帧都是以pts从小到大由frame_next移入frame的,能防止帧被遗漏。

[ffmpeg] 多输入滤波同步方式(framesync)的更多相关文章

  1. Ffmpeg和SDL如何同步音频

    ong> 同步音頻 现在我们已经有了一个比较像样的播放器.所以让我们看一下还有哪些零碎的东西没处理.上次,我们掩饰了一点同步问题,也就是同步音频到视频而不是其它的同步方式.我们将采用和视频一样的 ...

  2. FFmpeg--如何同步音视频的解决方案

    如何同步视频 PTS和DTS 幸运的是,音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息在里面.音频流有采样,视频流有每秒的帧率.然而,如果我们只是简单的通过数帧和乘以帧率的方式来同步视频 ...

  3. C# 的TCP Socket (同步方式)

    简单的c# TCP通讯(TcpListener) C# 的TCP Socket (同步方式) C# 的TCP Socket (异步方式) C# 的tcp Socket设置自定义超时时间 C# TCP ...

  4. java笔记--关于线程同步(7种同步方式)

    关于线程同步(7种方式) --如果朋友您想转载本文章请注明转载地址"http://www.cnblogs.com/XHJT/p/3897440.html"谢谢-- 为何要使用同步? ...

  5. 如何在silverlight中以同步方式 获取sharepoint2013站点的当前登录账号

    最近有个项目用到了silverlight要同步方式获取当前登录账号.异步的方式无法跟其他应用结合.主要先后顺序问题.但是silverlight非常不好获取到当前登录账号.即使获取到了也是异步方式获取. ...

  6. Linux多线程同步方式

    当多个线程共享相同的内存时,需要确保每个线程看到一致的数据视图,当多个线程同时去修改这片内存时,就可能出现偏差,得到与预期不符合的值.为啥需要同步,一件事情逻辑上一定是有序的,即使在并发环境下:而操作 ...

  7. linux线程间同步方式汇总

    抽空做了下linux所有线程间同步方式的汇总(原生的),包含以下几个: 1, mutex 2, condition variable 3, reader-writer lock 4, spin loc ...

  8. Windows Phone 同步方式获取网络类型

    原文:Windows Phone 同步方式获取网络类型 在Windows Phone 开发中有时候需要获取设备当前连接网络的类型,是Wifi,还是2G,3G,或者4G,SDK中提供获取网络类型的API ...

  9. Java之线程安全中的三种同步方式

    一个程序在运行起来时,会转换为进程,通常含有多个线程. 通常情况下,一个进程中的比较耗时的操作(如长循环.文件上传下载.网络资源获取等),往往会采用多线程来解决. 比如,现实生活中,银行取钱问题.火车 ...

随机推荐

  1. etcd v3集群备份和恢复

    官方文档 https://github.com/etcd-io/etcd/blob/master/Documentation/op-guide/recovery.md 一.运行3个etcd节点 我们用 ...

  2. Flutter 即学即用系列博客——08 MethodChannel 实现 Flutter 与原生通信

    背景 前面我们讲了很多 Flutter 相关的知识点,但是我们并没有介绍怎样实现 Flutter 与原生的通信. 比如我在 Flutter UI 上面点击了一个按钮,我希望原生做一些处理,那么原生怎么 ...

  3. Flutter map 妙用及 .. 使用

    前言 本篇文章对于熟悉 flutter 或者 dart 的小伙伴来说可能觉得比较简单,但是对于初学者或者没用过的小伙伴还是有些收获的. 背景 说到 map 妙用的发现,还要归功于 Tooltip 的研 ...

  4. sql 修改、更新、替换 某个字段的部分内容(转载)

    来源:https://blog.csdn.net/jiangnanqbey/article/details/81304834 1*.需求 将表(Ws_FormMain)的字段(order_Number ...

  5. java中求质数(素数)的问题

    这篇笔记讲讲关于java中质数的问题. 一.什么是质数(素数)? 定义:质数又称素数.一个大于1的自然数,除了1和它自身外,不能被其他自然数整除的数叫做质数:否则称为合数.它可以有无限个数. 二.ja ...

  6. 2015年第六届蓝桥杯javaB组 试题 答案 解析

    1.三角形面积 如图1所示.图中的所有小方格面积都是1. 那么,图中的三角形面积应该是多少呢? 请填写三角形的面积.不要填写任何多余内容或说明性文字. ##### 答案 : 28   ### 2.立方 ...

  7. PHP面向对象和面向过程

    编程界不论是PHP.Python都有这么两种编程思想,oop面向对象.pop面向过程,这两种在功能上没有多大区别,但是在代码编写上却有很大区别.今天就记录一下面向对象和面向过程的优点.缺点.定义.实现 ...

  8. 第十三课 CSS外观及样式的应用 css学习3

    一.1.color: 文本颜色 预定义文本颜色值,如red,blue等 十六进制的颜色值 #fff白色 建议常用的表示方法 RGB代码,如红色可以表示为rgb(255,0,0)或rgb(100%,%0 ...

  9. java:合并两个排序的链表(递归+非递归)

    //采用不带头结点的链表 非递归实现 public static ListNode merge(ListNode list1,ListNode list2){ if(list1==null) retu ...

  10. Docker + Sonarqube 环境搭建

    Sonar概述 Sonar 是一个用于代码质量管理的开放平台.通过插件机制,Sonar 可以集成不同的测试工具,代码分析工具,以及持续集成工具. 一,postgres 数据库下载 docker pul ...