作者在 Framework Design 中阐述了 MTL 框架的设计思想。本文对这篇文档进行总结和梳理,选择重点内容进行详细说明。
MTL 是一个用 C 写的库(它其实也提供了 C++ 接口),它采用 Producer/Consumer 设计模式进行开发。
在 MTL 中最常见的「图结构」是 Producer 连接另一个 Consumer。Consumer 从 Producer 中请求一个 MLT frame,然后消费这个 MLT frame,最后释放 MLT frame。
+--------+ +--------+
|Producer|-->|Consumer|
+--------+ +--------+
在 MLT 框架中,Producer 负责生成一个 MLT frame 对象,而 Consumer 负责消费一个 MLT frame。MLT frame 本质上提供了解码后的视频帧和音频帧数据。
Filters 可以插入在 Producer 和 Consumer 之间:
+--------+ +------+ +--------+
|Producer|-->|Filter|-->|Consumer|
+--------+ +------+ +--------+
MLT Service 包括 Producers、Filters、Transitions 和 Consumers。从面向对象的角度看, Service 是它们的基类。
相互连接的两个 Service,它们之间的通信包括三个阶段:
MLT采用了 “Lazy Evalution”,即在调用获取视频帧和音频帧方法之前,不需要从源中提取图像和音频。从本质上讲,Consumer 从上游获取数据,意味着 Consumer 通常需要启动额外的线程来抱歉实时的吞吐量。
下面是 MTL 中实现视频播放的示例,我们逐一分析。
#include
#include
#include
int main(int argc, char *argv[])
{
// Initialise the factory
if (mlt_factory_init(NULL))
{
// Create a profile
mlt_profile profile = mlt_profile_init(NULL);
// Create the default consumer
mlt_consumer hello = mlt_factory_consumer(profile, NULL, NULL);
// Create via the default producer
mlt_producer world = mlt_factory_producer(profile, NULL, argv[1]);
// Connect the producer to the consumer
mlt_consumer_connect(hello, mlt_producer_service(world));
// Start the consumer
mlt_consumer_start(hello);
// Wait for the consumer to terminate
while (!mlt_consumer_is_stopped(hello)) {
sleep(1);
}
// Close the consumer
mlt_consumer_close(hello);
// Close the producer
mlt_producer_close(world);
// Close the profile
mlt_profile_close(profile);
// Close the factory
mlt_factory_close();
}
else
{
// Report an error during initialization
fprintf(stderr, "Unable to locate factory modules\n");
}
// End of program
return 0;
}
mlt_factory_init
方法,该方法传入的是一个文件夹路径,这个文件夹里头有各种 .so,每个 so 都是 MLT 中的一个模块。如果传入的是 NULL
参数,MLT 会从环境变量 MLT_REPOSITORY
中获取默认的模块文件夹路径。mlt_factory_init
调用成功后,你便可以通过 mlt_factory_consumer
、mlt_factory_producer
等函数来创建对应的 service。mlt_factory_producer
函数用于创建 producer,它接收三个参数:
mlt_factory_consumer
函数用于创建 consumer,它接收三个参数:
mlt_factory_producer
的 service name 为 NULL 时,它将创建默认的 Producer,那就是 loader。loader 根据 resource 类型(其实是后缀)来聪明地创建合适的 Producer,并自动地添加上一些 Filters( rescale,resize 和 resample)以满足 Consumer 的要求。总之 loader 用起来是很方便的。mlt_factory_consumer
的 service name 为 NULL 时,它将创建默认的 Consumer,那就是 sdl2,它将用于视频的播放。mlt_consumer_connect
用于连接两个 service。mlt_consumer_start
将启动 Consumer 进行数据消费。使用 while
循环等待 Consumer 消费结束。当 Consumer 是 SDL2 时,消费结束意味着你关闭了 SDL2 的窗口。MLT_CONSUMER=xml ./hello file.avi
,输出 xml 文件到终端MLT_CONSUMER=xml MLT_PRODUCER=avformat ./hello file.avi
使用 avformat 作为 Producer,并输出 xml 文件到终端MLT_CONSUMER=libdv ./hello file.avi > /dev/dv1394
将 file.avi
文件广播到 DV 设备上MLT 框架中所有的 service,都通过插件的形式载入。正如前面所说,每一个插件都是一个 so 文件。具体的,你可以在 MLT 源码的 “module” 目录下看到每个插件实现。mlt_factory_init
函数用于加载这些 so 文件。
Consumer、Producer、Filter等它们都继承自 Service,而 Service 继承自 Properties。Properties 支持各种属性的设置,包括字符串、数值、二进制数据等。使用方法:
mlt_properties properties = mlt_producer_properties( producer );
mlt_properties_set( properties, "name", "value" );
mlt_properties_set_int( properties, "name", 0 );
mlt_properties_set_double( properties, "name", 1.0 );
double v = mlt_properties_get_double(properties, "name");
Playlist 是一个组件,它可以将多个 Producer 按顺序组合成一个连续的序列。Playlist 提供了一个简单的线性编辑方式,允许您轻松地按顺序播放多个音视频片段,还可以在片段之间插入空白(blank)或其他特殊片段。注:PlayList 本身是一个 Producer,因此你可以将一个 PlayList 插入在另一个 PlayList 上。
例如,你要连续播放一些列文件,那么使用 PlayList 即可:
mlt_producer create_playlist( int argc, char **argv )
{
// We're creating a playlist here
mlt_playlist playlist = mlt_playlist_init( );
// Loop through each of the arguments
int i = 0;
for ( i = 1; i < argc; i ++ )
{
// Create the producer
mlt_producer producer = mlt_factory_producer( NULL, argv[ i ] );
// Add it to the playlist
mlt_playlist_append( playlist, producer );
// Close the producer (see below)
mlt_producer_close( producer );
}
// Return the playlist as a producer
return mlt_playlist_producer( playlist );
}
Filter 是对输入帧执行某种操作的组件。这些操作可以包括更改颜色、添加特效、调整音量等。Filter 可以连接到 Producer 或其他 Filter,从而形成一个处理链,以便按顺序应用多个效果。例如:
// Create a producer from something
mlt_producer producer = mlt_factory_producer( ... );
// Create a consumer from something
mlt_consumer consumer = mlt_factory_consumer( ... );
// Create a greyscale filter
mlt_filter filter = mlt_factory_filter( "greyscale", NULL );
// Connect the filter to the producer
mlt_filter_connect( filter, mlt_producer_service( producer ), 0 );
// Connect the consumer to filter
mlt_consumer_connect( consumer, mlt_filter_service( filter ) );
mlt_filter_connect
最后一个参数叫 index
,有些 producer 有多个 track,index
指定了具体的 track。
所有 Service 都能做 Attached Filters 的操作。这个功能很方便,例如你有一个 PlayList,它又 3 个 producer,你希望在第二个 producer 上添加一个 Filter。那么可以这么做:
// Create a producer
mlt_producer producer0 = mlt_factory_producer( NULL, clip );
mlt_producer producer1 = mlt_factory_producer( NULL, clip );
mlt_producer producer2 = mlt_factory_producer( NULL, clip );
// Create a filter
mlt_filter filter = mlt_factory_filter( "greyscale" );
// Create a playlist
mlt_playlist playlist = mlt_playlist_init( );
// Attach the filter to the producer
mlt_service_attach( producer1, filter );
// Construct a playlist with various cuts from the producer
mlt_playlist_append_io( producer0, 0, 99 );
mlt_playlist_append_io( producer1, 450, 499 );
mlt_playlist_append_io( producer2, 200, 399 );
// We can close the producer and filter now
mlt_producer_close( producer );
mlt_filter_close( filter );
MLT 中转场怎么做?本章来告诉你。假设有这样一个 PlayList:
+-+----------------------+----------------------------+-+
|X|A |B |X|
+-+----------------------+----------------------------+-+
假设「X」是一个长度为 50 帧的空白片段。如果你播放这个 PlayList,播放完 50 帧空白画面后,会突然的切换到 A 画面,播放完 A 后又突然切换到 B,最后又切换到空白画面。我们希望引入转场,让视频片段之间的切换更丝滑,引入后:
+-+---------------------+-+------------------------+-+
|X|A |A|B |B|
|A| |B| |X|
+-+---------------------+-+------------------------+-+
引入转场后,PlayList 的播放长度变短了,这是符合预期的。你可以使用 mlt_playlist_mix
在两个 clip 之间插入一个转场:
// Create a transition
mlt_transition transition = mlt_factor_transition( "luma", NULL );
// Mix the first and second clips for 50
mlt_playlist_mix( playlist, 0, 50, transition );
// Close the transition
mlt_transition_close( transition );
注意,mlt_playlist_mix
将会在 PlayList 上新增一个 clip,因此如果你需要继续为其他 clip 添加转场,需要注意下标的变化。如果你需要为所有 clip 都添加转场,你可以这么做:
// Get the number of clips on the playlist
int i = mlt_playlist_count( );
// Iterate through them in reverse order
while ( i -- )
{
// Create a transition
mlt_transition transition = mlt_factor_transition( "luma", NULL );
// Mix the first and second clips for 50
mlt_playlist_mix( playlist, i, 50, transition );
// Close the transition
mlt_transition_close( transition );
}
考虑一个问题:如果你在两个 Clip 上引入一个转场,且这两个 clip 其实引用的是同一个视频(producer),那么会怎样?为了实现转场,这个 producer 会不断地进行 seek,一会 seek 到 clip A 的位置进行解码,一会 seek 到 clip B 的位置进行解码。这会导致处理速度变慢,特别是在实时场景下,会导致播放卡顿。
MLT 提供了 mlt_producer_optimise( mlt_playlist_producer( playlist ) );
方法,让你来解决这个问题。mlt_producer_optimise
将对这种情况做 producer 的拷贝,以便让 producer 分别处理各自的 clip。
下面介绍 MLT 中处理多轨道的方法和思路。这里展示了一个多轨道(Multitrack)的可视化表示,与非线性编辑器(NLE)展示它的方式类似:
+-----------------+ +-----------------------+
0: |a1 | |a2 |
+---------------+-+--------------------------+-+---------------------+
1: |b1 |
+------------------------------+
MLT 有一个 Multitrack 对象,它继承自 Producer,但如果你将 Multitrack 与一个 Consumer 连接,会发现它不能正常工作。这种现象的原因是消费者从其连接的生产者那里获取一帧,而 Multitrack 会为每个轨道提供一帧,那么 Consumer 要使用哪个轨道的上的数据呢?必须要有某种机制确保所有帧都从 Multitrack 中提取出来,并选择正确的帧进行传递。
因此,MLT 提供了一个 Multitrack 的包装器,称为“Tractor”。Tractor 的任务是确保所有轨道平均地提取帧,输出正确的帧,并提供类似生产者的行为。
因此,一个 Multitrack 有 Tractor 驱动,Tractor 从 Multitrack 上拉取(pull)每个轨道上的数据,
+----------+
|multitrack|
| +------+ | +-------+
| |track0|-|--->|tractor|
| +------+ | |\ |
| | | \ |
| +------+ | | \ |
| |track1|-|--->|---o---|--->
| +------+ | | / |
| | | / |
| +------+ | |/ |
| |track2|-|--->| |
| +------+ | +-------+
+----------+
结合 Multitrack 和 Tractor 后,你可以将一个 Tractor 连接到 Consumer,它能够正常工作了。Multitrack 的每个 Track 上有一个 Producer,它是 PlayList 或者另一个 Tractor。
接下来,我们希望在 Multitrack 和 Tractor 之间插入过滤器和过渡效果。我们可以直接在 Tractor 和 Multitrack 之间插入过滤器,但这涉及大量左右生产者和消费者的连接和断开连接操作,因此我们希望能自动化这个过程。于是我们引入 “Field” (田地)的概念。我们在“田地”中“种植”过滤器和过渡效果,Tractor 则负责将 Multitrack(可以将其视为联合收割机)拉过田地,并产生一束帧(这里用了一个幽默的比喻,表示帧)。
+----------+
|multitrack|
| +------+ | +-------------+ +-------+
| |track0|-|--->|field |--->|tractor|
| +------+ | | | |\ |
| | | filters | | \ |
| +------+ | | and | | \ |
| |track1|-|--->| transitions |--->|---o---|--->
| +------+ | | | | / |
| | | | | / |
| +------+ | | | |/ |
| |track2|-|--->| |--->| |
| +------+ | +-------------+ +-------+
+----------+
因此,多轨的处理逻辑是这样的:
从本质上讲,这就是它在消费者眼中的样子:
+-----------------------------------------------+
|tractor +--------------------------+ |
| +----------+ | +-+ +-+ +-+ +-+ | |
| |multitrack| | |f| |f| |t| |t| | |
| | +------+ | | |i| |i| |r| |r| | |
| | |track0|-|--->| |l|- ->|l|- ->|a|--->|a|\| |
| | +------+ | | |t| |t| |n| |n| | |
| | | | |e| |e| |s| |s| |\ |
| | +------+ | | |r| |r| |i| |i| | \|
| | |track1|-|- ->| |0|--->|1|--->|t|--->|t|-|--o--->
| | +------+ | | | | | | |i| |i| | /|
| | | | | | | | |o| |o| |/ |
| | +------+ | | | | | | |n| |n| | |
| | |track2|-|- ->| | |- ->| |--->|0|- ->|1|/| |
| | +------+ | | | | | | | | | | | |
| +----------+ | +-+ +-+ +-+ +-+ | |
| +--------------------------+ |
+-----------------------------------------------+
这部分没看明白,且 Link 模块非常少,先略过。
mlt_properties
├─ mlt_frame
└─ mlt_service
├─ mlt_producer
│ ├─ mlt_playlist
│ ├─ mlt_tractor
│ ├─ mlt_chain
│ └─ mlt_link
├─ mlt_filter
├─ mlt_transition
└─ mlt_consumer
mlt_deque
mlt_pool
mlt_factory
在这个图示中,我们可以清晰地看到 MLT 中的各个模块以及它们之间的层次关系。例如,mlt_frame 继承自 mlt_properties,而 mlt_playlist、mlt_tractor、mlt_chain 和 mlt_link 都继承自 mlt_producer。同时,mlt_filter、mlt_transition 和 mlt_consumer 都继承自 mlt_service。mlt_deque、mlt_pool 和 mlt_factory 是独立的模块,它们没有层次关系。
MLT 的 properties 类是 frame 和 service 类的基类。它提供了一个高效的查找表,用于存储各种类型的信息,如字符串、整数、浮点值和指向数据和数据结构的指针。所有的属性都由唯一的字符串索引。
创建属性集、设置属性值、获取属性值等基本操作很简单:
// 1. 创建一个新的空属性集
mlt_properties properties = mlt_properties_new();
// 2. 给属性 "hello" 赋值 "world"
mlt_properties_set(properties, "hello", "world");
// 3. 获取并打印 "hello" 的值
printf("%s\n", mlt_properties_get(properties, "hello"));
属性对象可以处理从字符串到其他类型的反序列化,也可以处理到字符串的序列化。为了显示属性集中的所有名称/值对,可以遍历它们:
for (i = 0; i < mlt_properties_count(properties); i++)
printf("%s = %s\n", mlt_properties_get_name(properties, i),
mlt_properties_get_value(properties, i));
属性还用于保存指向内存的指针,这是通过 set_data 调用完成的:
uint8_t *image = malloc(size);
mlt_properties_set_data(properties, "image", image, size, NULL, NULL);
// 获取数据指针
image = mlt_properties_get_data(properties, "image", &size);
值得注意的是,分配给属性的内存在属性对象关闭后仍然存在,除非指定了一个析构函数:
mlt_properties_set_data(properties, "image", image, size, free, NULL);
Properties 类还提供了一些更高级的功能,如从另一个属性对象继承所有可序列化值,或在一个属性集上设置的值自动反映在另一个属性集上:
mlt_properties_inherit(this, that);
mlt_properties_mirror(this, that);
mlt_deque 是 MLT 框架中的一个双端队列(deque)实现,它结合了栈和队列的功能。在 MLT 中,栈操作主要用于反向波兰表示法 (RPN) 的图像和音频操作以及内存池管理,而队列操作则用于消费者基类以及其他消费者实现可能需要的队列。
mlt_pool 是 MLT 框架提供的内存池 API,可以作为 malloc/realloc/free 功能的替代。malloc/free 操作在处理大块内存(如图像)时效率较低。mlt_pool 的设计是维护一个栈列表,每个 2^n 字节(n 介于 8 和 31 之间)有一个栈。调用 alloc 时,请求的大小四舍五入到下一个 2^n,检索该大小的栈,并弹出或创建一个项目(如果栈为空)。从程序员的角度来看,API 与传统的 malloc/realloc/free 调用相同:
void *mlt_pool_alloc(int size);
void *mlt_pool_realloc(void *ptr, int size);
void mlt_pool_release(void *release);
mlt_frame 是一个关键组件,包括帧、属性、图像栈和音频栈。帧的生命周期可以分为以下几个阶段:
+-------+-----------------+---------------------+----------------+
| Stage | Producer | Filter | Consumer |
+-------+-----------------+---------------------+----------------+
| 0.0 | | | Request frame |
| 0.1 | | Receives request | |
| | | Request frame | |
| 0.2 | Receives request| | |
| | Generates frame | | |
| | for current pos.| | |
| | Increments pos. | | |
| 0.3 | | Receives frame | |
| | | Updates frame | |
| 0.4 | | | Receives frame |
+-------+-----------------+---------------------+----------------+
在这个过程中,生产者创建帧并设置帧的速度和位置属性。过滤器负责处理图像和音频,将数据和方法推送到栈中。接下来,当消费者调用 frame_get_image 和 frame_get_audio 时,过滤器的 filter_get 方法会被自动调用。在这个阶段,过滤器会更新帧的图像和音频。
+-------+-----------------+---------------------+----------------+
| Stage | Producer | Filter | Consumer |
+-------+-----------------+---------------------+----------------+
| 1.0 | | | frame_get_image|
| 1.1 | | filter_get_image: | |
| | | pop data2, data1 | |
| | | frame_get_image | |
| 1.2 | producer_get_image | | |
| | Generates image | | |
| 1.3 | | Receives image | |
| | | Updates image | |
| 1.4 | | | Receives image |
+-------+-----------------+---------------------+----------------+
+-------+-----------------+---------------------+----------------+
| Stage | Producer | Filter | Consumer |
+-------+-----------------+---------------------+----------------+
| 2.0 | | | frame_get_audio|
| 2.1 | | filter_get_audio: | |
| | | pop data | |
| | | frame_get_audio | |
| 2.2 | producer_get_audio | | |
| | Generates audio | | |
| 2.3 | | Receives audio | |
| | | Updates audio | |
| 2.4 | | | Receives audio |
+-------+-----------------+---------------------+----------------+
最后,消费者完成对帧的处理后,会关闭帧。帧有一些默认属性,例如位置、速度、图像、音频等。消费者还可以附加一些属性来影响帧的默认行为。例如,可以附加 test_card_producer、consumer_aspect_ratio 和 rescale.interp 属性以控制帧的测试图像生成和缩放方法。
这里需要注意的是,消费者可能不会针对每个给定的帧评估图像和音频,特别是在实时环境中。此外,normalized_width 和 normalized_height 属性用于确保效果始终如一地处理为 PAL 或 NTSC,无论消费者或生产者的图像宽度/高度请求如何。
test_image 和 test_audio 标志用于确定何时应生成图像和音频。生产者实现可能会提供其他属性,过滤器、转换器和消费者也可以添加其他属性以传达特定请求。这些属性在 modules.txt 中有详细文档。
总之,mlt_frame 是一个关键组件,用于在生产者、过滤器和消费者之间处理和传递图像和音频数据。帧的生命周期和属性可以灵活地进行管理,以满足不同场景的需求。在处理图像和音频时,需要注意生产者、过滤器和消费者之间的协作和数据传递顺序。
本文对 Framework Design 进行了说明与解读,希望能够帮助正在学习 MLT 框架的你。