框架概览
前言
MLT 是一个为电视广播设计的多媒体框架。因此,它提供了一种可插拔的架构, 以便纳入新的音频/视频源、滤镜、转场和播放设备。
该框架提供了所有 MLT 应用程序和服务模块(service)所定义的基础结构与 实用功能。
就其自身而言,该框架提供的更多只是‘抽象类’以及管理资源的工具, 例如内存、属性、动态对象加载和服务实例化。
本文档大致分为 3 个部分。第一部分提供 MLT 的基本概览,第二部分展示 其使用方式,最后一部分展示结构与设计,重点在于如何扩展系统。
目标读者
本文档作为该框架的‘路线图’提供,任何希望在 MLT 层面开发代码的人 都应将其视为必读内容。
这包括:
- 框架维护者;
- 模块开发者;
- 应用开发者;
- 所有对 MLT 感兴趣的人。
本文档的重点在于解释公共接口,而非实现细节。
对于 MLT 客户端/服务端集成而言,本文并非必读—— 请查阅 libmvsp.txt 和 mvsp.txt 以了解更多相关内容。
基本概览
基本设计信息
MLT 使用 C 语言编写。
该框架除了标准 C99 和 POSIX 库之外没有任何依赖。
它遵循基本的面向对象设计范式,因此其设计在很大程度上松散地基于 生产者/消费者(Producer/Consumer)设计模式。
它对音频和视频特效(FX)的应用采用逆波兰表示法(Reverse Polish Notation)。
该框架被设计为色彩空间中立——不过当前实现的模块是非常面向 8bit YUV422 的。 理论上,这些模块可以被完全替换。
阅读本文档剩余部分时,默认你已对这些术语有模糊的了解。
结构与流程
MLT ‘网络’的一般结构很简单,就是把一个‘生产器(producer)’连接到一个 ‘消费器(consumer)’:
+--------+ +--------+
|Producer|-->|Consumer|
+--------+ +--------+典型的消费器向生产器请求 MLT Frame(帧)对象,对它们做一些处理, 处理完一帧后就将其关闭。
关于这里使用的生产器/消费器术语,一个常见的混淆在于:消费器也可能 ‘生产’某些东西。例如,libdv 消费器产出 DV,而 libdv 生产器看起来是在 消费 DV。然而,这些命名约定仅指 MLT Frame 的生产者和消费者。
换句话说——生产器产出 MLT Frame 对象,消费器消费 MLT Frame 对象。
一个 MLT Frame 本质上提供一幅未压缩的图像及其关联的音频采样。
滤镜也可以放置在生产器和消费器之间:
+--------+ +------+ +--------+
|Producer|-->|Filter|-->|Consumer|
+--------+ +------+ +--------+服务(service)是生产器、滤镜、转场和消费器的统称。
相互连接的消费器与生产器或服务之间的通信分 3 个阶段进行:
- 获取帧(get the frame)
- 获取图像(get the image)
- 获取音频(get the audio)
MLT 采用‘惰性求值(lazy evaluation)’——在 get image 和 get audio 方法 被调用之前,无需从源中提取图像和音频。
本质上,消费器从与其连接的对象拉取数据——这意味着线程化通常属于 消费器实现的范畴,并且消费器类提供了一些基本功能以确保实时吞吐量。
使用方法
Hello World
在进入框架架构的细节之前,先给出一个可用的使用示例。
下面这段代码简单地提供了一个媒体播放器:
#include <stdio.h>
#include <unistd.h>
#include <framework/mlt.h>
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;
}这是一个简单的例子——它没有提供任何 Seek 功能或运行时配置选项。
任何 MLT 应用程序的第一步都是工厂(factory)初始化——这确保环境已配置 完成、MLT 能够正常工作。工厂将在下文更详细地介绍。
所有服务都通过工厂实例化,如上面的 mlt_factory_consumer 和 mlt_factory_producer 调用所示。滤镜和转场也有类似的工厂。所有标准服务的 详情请参阅 services.txt。
这里请求的默认值是一种特殊情况——传入 NULL 表示使用默认的生产器和 消费器。
默认生产器是 “loader”。该生产器通过匹配文件名来定位要使用的服务, 并为加载的内容附加‘归一化滤镜(normalizing filters)’(如缩放器、 去隔行器、重采样器和场归一化器)——这些滤镜确保消费器得到它所请求的东西。
默认消费器是 “sdl2”。loader 与 sdl2 的组合将提供一个媒体播放器。
在这个例子中,我们先连接生产器,然后启动消费器。接着我们等待直到消费器 停止(在本例中,是由用户关闭 SDL 窗口的动作触发的),最后关闭消费器、 生产器和工厂,然后退出应用程序。
注意消费器是带线程的——在启动之后、停止或关闭消费器之前, 总是需要等待某种事件。
另外请注意,你可以像这样覆盖默认值:
MLT_CONSUMER=xml ./hello file.avi
这将在 stdout 上生成一个 XML 文档。
MLT_CONSUMER=xml MLT_PRODUCER=avformat ./hello file.avi
这将直接使用 avformat 生产器播放视频,从而绕过归一化处理。
MLT_CONSUMER=libdv ./hello file.avi > /dev/dv1394
如果你运气好,这也许会即时、实时地把 file.avi 转换为 DV 并输出到你的 DV 设备。
工厂(Factories)
如 ‘Hello World’ 示例所示,工厂用于创建服务对象。
框架本身不提供任何服务——服务以插件结构的形式提供。插件按‘模块(module)’ 组织,一个模块可以提供多种不同类型的服务。
一旦工厂初始化完成,所有已配置的服务都可供使用。
mlt_factory_prefix() 返回已安装模块目录所在路径。这个路径可以在 mlt_factory_init 调用中直接指定,也可以通过 MLT_REPOSITORY 环境变量 指定;如果两者都没有提供,则默认为 install 前缀/shared/mlt/modules。
mlt_environment() 提供对一组 name=value 键值对的只读访问,如下表所示:
| 名称 | 描述 | 取值 |
|---|---|---|
| MLT_NORMALISATION | 系统的归一化标准 | PAL 或 NTSC |
| MLT_PRODUCER | 默认生产器 | “loader” 或其他 |
| MLT_CONSUMER | 默认消费器 | “sdl2” 或其他 |
| MLT_TEST_CARD | 默认测试卡生产器 | 任意生产器 |
这些值从同名的环境变量初始化。
如上所示,生产器既可以使用‘默认归一化’生产器创建,也可以按名称请求。 滤镜和转场则总是按名称请求——它们没有‘默认’的概念。
服务属性
所有服务都有自己的一组属性,可以通过操作这些属性来影响其行为。
要在服务上设置属性,我们需要获取与它关联的 properties 对象。对于生产器, 这通过调用以下方法完成:
mlt_properties properties = mlt_producer_properties( producer );所有服务都有与之类似的关联方法。
获取之后,就可以直接在该对象上设置和读取属性,例如:
mlt_properties_set( properties, "name", "value" );关于 properties 对象的更完整描述见下文。
播放列表(Playlists)
到目前为止,我们展示的都是简单的生产器/消费器配置——下一个阶段是把 生产器组织进播放列表。
假设我们是在 Hello World 示例的基础上做改造,希望把若干文件排入 播放队列,即:
hello *.avi
我们不直接调用 mlt_factory_producer,而是创建一个名为 create_playlist 的新函数。该函数负责创建播放列表、创建每个生产器并将其追加到播放列表。
mlt_producer create_playlist( int argc, char **argv )
{
// We're creating a playlist here
mlt_playlist playlist = mlt_playlist_init( );
// We need the playlist properties to ensure clean up
mlt_properties properties = mlt_playlist_properties( playlist );
// 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 );
}注意我们在 append 之后关闭了生产器。实际上,我们做的是关闭我们自己对它的 引用——播放列表在 append 和 insert 时会创建它自己的引用,并且在播放列表 被销毁时会关闭它的引用[*]。
另外请注意,如果你追加同一个生产器的多个实例,将会创建对它的多个引用。
现在,我们只需把 main 函数中的这几行:
// Create a normalized producer
mlt_producer world = mlt_factory_producer( NULL, argv[ 1 ] );替换为:
// Create a playlist
mlt_producer world = create_playlist( argc, argv );我们就拥有了播放多个片段的方法。
[*] 这一引用功能是在 mlt 0.1.2 中引入的——它与早期向播放列表对象的属性中 注册引用和析构函数的机制 100% 兼容。
滤镜(Filters)
在生产器和消费器之间插入滤镜,只需实例化各个滤镜,把第一个连到生产器, 下一个连到前一个滤镜,最后一个滤镜连到消费器即可。
例如:
// 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 ) );与生产器和消费器一样,滤镜也可以通过它们的 properties 对象来操作—— 可以调用 mlt_filter_properties 方法并按需设置属性。
滤镜连接时的额外一个参数非常重要,它决定了滤镜操作所在的‘轨道(track)’。 对于基本生产器和播放列表,只有一条轨道(0),正如你在下一节将看到的, 即使是多轨道也只有一个单轨道输出。
附加滤镜(Attached Filters)
所有服务都可以有附加滤镜。
考虑下面的例子:
// Create a producer
mlt_producer producer = mlt_factory_producer( NULL, clip );
// Get the service object of the producer
mlt_producer service = mlt_producer_service( producer );
// 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( producer, filter );
// Construct a playlist with various cuts from the producer
mlt_playlist_append_io( producer, 0, 99 );
mlt_playlist_append_io( producer, 450, 499 );
mlt_playlist_append_io( producer, 200, 399 );
// We can close the producer and filter now
mlt_producer_close( producer );
mlt_filter_close( filter );播放时,对于播放列表中来自该生产器的每一帧,greyscale 滤镜都会被执行。
此外,每个剪切(cut)还可以有自己附加的滤镜,它们在生产器的滤镜之后执行。 例如:
// Create a new filter
filter = mlt_factory_filter( "invert", NULL );
// Get the second 'clip' in the playlist
producer = mlt_playlist_get_clip( 1 );
// Get the service object of the clip
service = mlt_producer_service( producer );
// Attach the filter
mlt_service_attach( producer, filter );
// Close the filter
mlt_filter_close( filter );甚至播放列表本身也可以有附加滤镜:
// Create a new filter
filter = mlt_factory_filter( "watermark", "+Hello.txt" );
// Get the service object of the playlist
service = mlt_playlist_service( playlist );
// Attach the filter
mlt_service_attach( service, filter );
// Close the filter
mlt_filter_close( filter );当然,播放列表本身作为一个生产器,也可以被剪切并放到另一个播放列表上, 滤镜可以附加到那些剪切上,或者附加到新播放列表本身,依此类推,无穷无尽。
附加滤镜的主要优点是它们保持附加状态,不会遇到因条目插入而挤占挪动 已计算好的 in/out 点所带来的维护难题——如果你在多轨道场 (multitrack field,见下文)中专门使用 connect 或 insert 来添加分离式(detached)滤镜,这将是一个重大问题。
引入 mix(混合)
mix 是在播放列表上相邻片段之间引入转场的最简单方式。
考虑下面的播放列表:
+-+----------------------+----------------------------+-+
|X|A |B |X|
+-+----------------------+----------------------------+-+假设 ‘X’ 是一段 50 帧长的‘黑场片段’。
播放时,你会先看到 50 帧黑场,然后硬切到 A,接着硬切到 B,最后再切回黑场。
我们的目标是把这个播放列表转换成类似这样的形式:
+-+---------------------+-+------------------------+-+
|X|A |A|B |B|
|A| |B| |X|
+-+---------------------+-+------------------------+-+其中引用了两个片段的部分代表一个转场。注意第二个播放列表的表示比第一个 更短——这是预料之中的——两个片段之间一个 50 帧的转场会使结果的播放时长 减少 50 帧。
这是通过 mlt_playlist_mix 方法完成的。所以,假设你得到了原始图示中的 播放列表,要做第一个 mix,你可以这样做:
// 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 );这就得到了第一个转场,接下来你可以用类似的技术对片段 1 和 2 做 mix。 注意这会创建一个新片段,所以下一次 mix 将发生在 3 和 4 之间。
作为一个通用技巧,为了简化对下一个片段索引的了解,你可能会发现下面 这种写法更简单:
// 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 );
}还有其他技术,比如在当前片段和新创建的片段之间使用 mlt_playlist_join(可以通过比较 mix 调用前后播放列表的长度来判断 是否创建了新片段)。
在内部,mlt_playlist_mix 调用会生成一个 tractor(牵引机)和一个 multitrack(多轨),如下文所述。与附加滤镜一样,当你往播放列表中插入 条目时,mix 让事情变得非常简单。
还要注意,它允许更简单的用户界面——不必强制使用复杂的多轨对象, 你可以在单条轨道上完成很多操作。因此,额外的轨道可以用来引入音频配音、 混音或合成,它们独立定位,且不受其他轨道上操作的影响。不过,如果你想要 传统 NLE 那种轰轰烈烈、令人困惑、最终令人抓狂的体验,那部分功能也 照样提供 ;-).
实用性与优化
在前两节中,我介绍了一些旨在简化 MLT 使用的强大功能。然而,一个普遍问题 出现了——当你在同一段视频素材的两个剪切之间引入转场时会发生什么?
熟悉视频压缩的人都知道,seek 从性能角度看并不总是没有代价的。所以如果你 恰好需要从同一个片段取两帧来做转场,处理开销将会非常大,结果几乎必然 非常糟糕,尤其是在实时渲染的场景下……
那我们怎么绕过这个问题呢?
其实很简单——在修改之后对顶层对象调用 mlt_producer_optimise,MLT 会 自行决定如何处理。在内部,它会确定整个对象中最大重叠实例数,并按需要 创建克隆(clone)和分配克隆索引。
在上面的 mix 例子中,你只需在 mix 调用完成后调用:
// Optimize the playlist
mlt_producer_optimise( mlt_playlist_producer( playlist ) );注意,这一优化会自动应用于反序列化的 MLT XML。
多轨道与转场
MLT 对多轨道的处理方式由两条要求决定:
- 消费器和生产器需要通过单一帧相互通信;
- 希望能够序列化并操作一个‘网络’(或者你愿意称之为滤镜图 filter graph)。
我们可以按照 NLE 呈现的方式来想象多轨:
+-----------------+ +-----------------------+
0: |a1 | |a2 |
+---------------+-+--------------------------+-+---------------------+
1: |b1 |
+------------------------------+轨道 0 和轨道 1 的重叠区域(大概)会有某种转场——如果没有转场, b1 和 b2 的帧会在重叠区域显示(即:默认情况下,编号较高的轨道优先于 编号较低的轨道)。
MLT 有一个 multitrack 对象,但它不是一个意义上的生产器——把它直接连到 消费器上并不能让一切正常工作。消费器会完全把它当作普通生产器对待,而在 上面这个 multitrack 的例子中,除了片段之间的转场之外,你永远不会看到 来自轨道 1 的任何内容——a1 和 a2 之间的空隙会显示测试帧。
出现这种情况的原因是:消费器从与其连接的生产器每次拉取一帧,而 multitrack 会为每条轨道各提供一帧。必须由某个地方的某个机制确保从 multitrack 拉取 所有帧,并选出正确的一帧传递下去。
因此,MLT 为 multitrack 提供了一个包装器,称为‘tractor(牵引机)’, tractor 的职责是确保所有轨道被均匀地拉取、输出正确的帧,并表现出 类生产器(producer-like)的行为。
这样,multitrack 在概念上由 tractor ‘拉取’,如图所示:
+----------+
|multitrack|
| +------+ | +-------+
| |track0|-|--->|tractor|
| +------+ | |\ |
| | | \ |
| +------+ | | \ |
| |track1|-|--->|---o---|--->
| +------+ | | / |
| | | / |
| +------+ | |/ |
| |track2|-|--->| |
| +------+ | +-------+
+----------+有了这两者的组合,我们现在可以把 multitrack 连接到消费器了。 最后一张非测试卡(test card)图像会被获取并传递下去。
轨道可以是生产器、播放列表,甚至可以是其他 tractor。
现在我们希望 multitrack 与 tractor 之间插入滤镜和转场。我们可以直接把 滤镜插入 tractor 和 multitrack 之间来实现,但这就涉及大量左右逢源地连接 和重新连接生产器、消费器,而且看起来我们应该能够自动化这一过程才算公平。
于是,遵循我们的农业主题,‘field(场)’的概念诞生了。我们把滤镜和转场 ‘种(plant)’在场里,tractor 像联合收割机一样把 multitrack 驶过这片场 (想象一下收割机 :-)),然后生产出一‘捆(bail)’(抱歉——开玩笑的—— 是一帧 :-))。
在概念上,我们可以这样看它:
+----------+
|multitrack|
| +------+ | +-------------+ +-------+
| |track0|-|--->|field |--->|tractor|
| +------+ | | | |\ |
| | | filters | | \ |
| +------+ | | and | | \ |
| |track1|-|--->| transitions |--->|---o---|--->
| +------+ | | | | / |
| | | | | / |
| +------+ | | | |/ |
| |track2|-|--->| |--->| |
| +------+ | +-------------+ +-------+
+----------+所以,我们需要先创建 tractor,然后从中获取 multitrack 和 field 对象。 我们可以填充这些对象,最后把 tractor 连接到消费器。
本质上,消费器看到的样子是这样的:
+-----------------------------------------------+
|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|/| |
| | +------+ | | | | | | | | | | | |
| +----------+ | +-+ +-+ +-+ +-+ | |
| +--------------------------+ |
+-----------------------------------------------+希望一个例子能把这一点讲清楚。
假设我们想在 hello world 示例上叠加一个‘水印(watermark)’。我们已经把 示例扩展成播放多个片段,现在我们要在左上角放置一个基于文字的水印, 内容为 ‘Hello World’:
mlt_producer create_tracks( int argc, char **argv )
{
// Create the tractor
mlt_tractor tractor = mlt_tractor_new( );
// Obtain the field
mlt_field field = mlt_tractor_field( tractor );
// Obtain the multitrack
mlt_multitrack multitrack = mlt_tractor_multitrack( tractor );
// Create a transition to blend tracks
mlt_transition transition = mlt_factory_transition( "qtblend", "10%/10%:15%x15%" );
// Create track 0
mlt_producer track0 = create_playlist( argc, argv );
// Create the watermark track - note we NEED loader for scaling here
mlt_producer track1 = mlt_factory_producer( "loader", "pango" );
// Get the length of track0
mlt_position length = mlt_producer_get_playtime( track0 );
// Set the properties of track1
mlt_properties properties = mlt_producer_properties( track1 );
mlt_properties_set( properties, "text", "Hello\nWorld" );
mlt_properties_set_position( properties, "in", 0 );
mlt_properties_set_position( properties, "out", length - 1 );
mlt_properties_set_position( properties, "length", length );
mlt_properties_set_int( properties, "a_track", 0 );
mlt_properties_set_int( properties, "b_track", 1 );
// Now set the properties on the transition
properties = mlt_transition_properties( transition );
mlt_properties_set_position( properties, "in", 0 );
mlt_properties_set_position( properties, "out", length - 1 );
// Add our tracks to the multitrack
mlt_multitrack_connect( multitrack, track0, 0 );
mlt_multitrack_connect( multitrack, track1, 1 );
// Now plant the transition
mlt_field_plant_transition( field, transition, 0, 1 );
// Close our references
mlt_producer_close( track0 );
mlt_producer_close( track1 );
mlt_transition_close( transition );
// Return the tractor
return mlt_tractor_producer( tractor );
}现在,我们只需把 main 函数中的这几行:
// Create a playlist
mlt_producer world = create_playlist( argc, argv );替换为:
// Create a watermarked playlist
mlt_producer world = create_tracks( argc, argv );我们就拥有了一个能播放多个片段、并带上一个极其碍眼水印的方法—— 这正是世界所需要的,对吧?;-)
顺便说一句,用更简单的做法也能达到同样效果:把 watermark 滤镜插入 生产器和消费器之间即可。
Chain 与 Link
只要你的处理不依赖当前处理帧以外的其他帧,滤镜就是操作图像和音频的 简单方式。但有些处理需要来自前一帧或后一帧的数据,以便对未来采样进行 插值或执行时域处理。这类处理的例子有音频重采样或视频去隔行。 还有一些处理可能想访问完全不相关的帧,比如时间重映射(time remapping)。 滤镜无法请求生产器 seek 到某一特定帧,因为滤镜不知道给它提供帧的生产器 是谁。这正是 chain(链)和 link(链接)大显身手之处。
chain 是一种生产器,为标准生产器提供包装器。作为包装器,它会把属性 向下传递给封装的源生产器。link 是另一种生产器,可以连接到一个‘next’ (下一个)生产器。那个 next 生产器可以是 chain 中的源生产器,也可以是 另一个 link。由于 link 是生产器,它们具有 mlt_producer_seek() 函数。 又由于它们知道‘next’生产器,它们可以在 chain 中的下一个生产器/link 上 调用 mlt_producer_seek()。
与滤镜类似,帧从第一个 link 被拉取,沿着链条一路向下,直到帧请求 到达生产器。但与滤镜不同的是,每个 link 都可以在 chain 中的下一个 生产器/link 上执行 seek。它甚至可以多次 seek 并请求多帧,如果它需要 多帧数据的话。
+------------------------------------------------+
|chain |
| +----------+ +-------+ +-------+ +-------+ |
| | producer |->| link2 |->| link1 |->| link0 |--|->
| +----------+ +-------+ +-------+ +-------+ |
+------------------------------------------------+与滤镜类似,link 可以被设计成具有专门用途、注册到服务仓库中, 并通过工厂加载。
归一化 link 可以通过调用 mlt_chain_attach_normalizers() 加载到 chain 上。 这类似于使用 loader 生产器把归一化滤镜添加到生产器上。像其他生产器一样, chain 上也可以附加滤镜。但最好使用归一化 link 而不是归一化滤镜。事实上, mlt_chain_attach_normalizers() 会从源生产器上移除任何归一化滤镜。
结构与设计
类层次结构
mlt 框架由一个面向对象的类层次结构组成,包含以下公共类和抽象:
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_properties
properties 类是 frame 类和 service 类的基类。
它被设计用于为各类信息提供高效的查找表,例如字符串、整数、浮点值, 以及指向数据和数据结构的指针。
所有属性都通过唯一的字符串索引。
properties 最基本的用法如下:
// 1. Create a new, empty properties set;
mlt_properties properties = mlt_properties_new( );
// 2. Assign the value "world" to the property "hello";
mlt_properties_set( properties, "hello", "world" );
// 3. Retrieve and print the value of "hello";
printf( "%s\n", mlt_properties_get( properties, "hello" ) );
// 4. Reassign "hello" to "world!";
mlt_properties_set( properties, "hello", "world!" );
// 5. Retrieve and print the value of "hello";
printf( "%s\n", mlt_properties_get( properties, "hello" ) );
// 6. Assign the value "0" to "int";
mlt_properties_set( properties, "int", "0" );
// 7. Retrieve and print the integer value of "int";
printf( "%d\n", mlt_properties_get_int( properties, "int" ) );
// 8. Assign the integer value 50 to "int2";
mlt_properties_set_int( properties, "int2", 50 );
// 9. Retrieve and print the double value of "int2";
printf( "%s\n", mlt_properties_get( properties, "int2" ) );步骤 2 到 5 表明‘名称’是唯一的——对已存在的‘名称’执行 set 操作会改变 其值,同时释放与前一个值关联的内存。注意,通过这种方式也可以改变类型。
步骤 6 和 7 表明 properties 对象处理从字符串的反序列化。设置的是字符串 值 “0”,取回的是整数值 0。
步骤 8 和 9 表明 properties 对象处理向字符串的序列化。
要显示 properties 中的所有 name/value 对,可以对它们进行遍历:
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 ) );注意,属性是按照设置时的顺序取回的。
properties 还用于保存指向内存的指针。这通过 set_data 调用完成:
uint8_t *image = malloc( size );
mlt_properties_set_data( properties, "image", image, size, NULL, NULL );在这个例子中,我们指定该指针可以通过后续调用 get_data 从 properties 中取回:
image = mlt_properties_get_data( properties, "image", &size );或者:
image = mlt_properties_get_data( properties, "image", NULL );如果我们不想获取大小。
这里有两点:
- 除非你指定析构函数,否则在 properties 对象关闭后,分配的内存仍然存在。 在上述情况下,可以这样做到:
mlt_properties_set_data( properties, "image", image, size, free, NULL );当 properties 被关闭,或者 “image” 的值被更改时,析构函数会被调用。
- mlt_properties_get 返回的字符串值为 NULL。 通常你不会想把图像序列化为字符串,但其他结构可能需要这种功能—— 如有需要,可以把序列化函数作为最后一个参数指定 (声明为 char *serialize( void * ))。
properties 还提供一些更高级的用法能力。
它能够从另一个 properties 对象继承所有可序列化的值:
mlt_properties_inherit( this, that );它能够将设置在本对象上的属性镜像(mirror)到另一组 properties 上:
mlt_properties_mirror( this, that );此调用之后,所有设置在本对象上的可序列化值都会被传递到 that 上。
mlt_deque
堆栈和队列是 MLT 框架中的关键组件。出于我们一贯的懒脾气,我们选择实现 一个‘双端队列(deque,Double Ended Queue)’——它把两者的功能都封装在内。
堆栈操作用于若干场景:
- 逆波兰表示法(RPN)图像和音频操作
- 内存池化
队列操作用于:
- 消费器基类;
- 消费器实现可能还需要额外的队列。
mlt_pool
MLT 框架通过 mlt_pool API 提供内存池能力。一旦初始化完成,它们可以看作 malloc/realloc/free 功能的直接替换。
这个 API 背后的背景是:malloc/free 操作效率出了名地低,尤其是在处理 大块内存(比如一幅图像)时。在 linux 上,malloc 针对小于 128k 的内存分配 做了优化——这些大小或更小的内存块会保留在进程堆中以便后续复用,从而 绕过对小内存块反复分配/释放的内核调用。然而,大于该值的内存块需要 内核调用,这对性能有不利影响。
mlt_pool 的设计很简单——持有一组堆栈的列表——每 2^n 字节有一个堆栈 (其中 n 介于 8 和 31 之间)。当调用 alloc 时,请求的大小会被向上取整到 下一个 2^n,取出对应大小的堆栈,从中弹出一个项目,如果堆栈为空则新建一个。
每个项目都有一个‘头部(header)’,位于返回地址之前的紧邻位置—— 它保存该项目所属的‘堆栈’。
当释放一个项目时,我们读取头部,取得堆栈,把它压回去。
因此,从程序员的角度看,该 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
frame 对象的本质定义如下:
| frame |
| properties |
| image stack |
| audio stack |
帧的生命周期可以表示如下:
| 阶段 | 生产器 | 滤镜 | 消费器 |
| 0.0 | 请求帧 | ||
| 0.1 | 收到请求 | ||
| 请求帧 | |||
| 0.2 | 收到请求 | ||
| 为当前位置 | |||
| 生成帧 | |||
| 递增位置 | |||
| 0.3 | 收到帧 | ||
| 更新帧 | |||
| 0.4 | 收到帧 |
注意,在收到帧之前,滤镜和消费器都对‘位置’没有任何概念。速度和位置是 生产器的属性,生产器在创建帧对象时把它们赋给帧。
这里的步骤 0.3 非常关键——如果滤镜判定这帧与它有关,那么它就应按需 操作图像和/或音频堆栈以及属性。
假设滤镜同时处理图像和音频,那么它应该把数据和方法压入负责处理的堆栈。 这可以通过 mlt_frame_push_image 和音频方法完成。为了表明滤镜对这帧 注册了兴趣,堆栈中应该包含:
image stack:
[ producer_get_image ] [ data1 ] [ data2 ] [ filter_get_image ]
audio stack:
[ producer_get_audio ] [ data ] [ filter_get_audio ]当消费器对帧调用 get_image 时,filter_get 系列方法会被自动调用。
| 阶段 | 生产器 | 滤镜 | 消费器 |
| 1.0 | frame_get_image | ||
| 1.1 | filter_get_image: | ||
| 弹出 data2 和 data1 | |||
| frame_get_image | |||
| 1.2 | producer_get_image | ||
| 生成图像 | |||
| 1.3 | 收到图像 | ||
| 更新图像 | |||
| 1.4 | 收到图像 |
显然,如果滤镜对图像不感兴趣,它就应当不去动堆栈,这样消费器就会 直接从生产器获取图像。
音频的处理方式类似,如下所示:
| 阶段 | 生产器 | 滤镜 | 消费器 |
| 2.0 | frame_get_audio | ||
| 2.1 | filter_get_audio: | ||
| 弹出 data | |||
| frame_get_audio | |||
| 2.2 | producer_get_audio | ||
| 生成音频 | |||
| 2.3 | 收到音频 | ||
| 更新音频 | |||
| 2.4 | 收到音频 |
最后,当消费器用完这帧时,应当将其关闭。
注意消费器未必会对任何给定帧同时处理图像和音频,在实时环境中尤其如此。 参见下文的‘实时考量(Realtime Considerations)’。
默认情况下,一帧具有以下属性:
| 名称 | 描述 | 取值 |
| position | 生产器的帧位置 | 0 到 n |
| speed | 生产器的速度 | double |
| image | 生成的图像 | NULL 或指针 |
| alpha | 生成的 alpha 掩膜 | NULL 或指针 |
| width | 图像的宽度 | |
| height | 图像的高度 | |
| normalised_width | 图像的归一化宽度 | 720 |
| normalised_height | 图像的归一化高度 | 576 或 480 |
| progressive | 指示逐行/隔行 | 0 或 1 |
| top_field_first | 指示上场优先 | 0 或 1 |
| audio | 生成的音频 | NULL 或指针 |
| frequency | 音频的采样频率 | |
| channels | 音频的声道 | |
| samples | 音频的采样 | |
| aspect_ratio | 图像的采样宽高比 | double |
| test_image | 用于指示无图像可用 | 0 或 1 |
| test_audio | 用于指示无音频可用 | 0 或 1 |
消费器可以附加以下属性,它们会影响帧的默认行为:
| test_card_producer | 从此处合成测试图像 | NULL 或指针 |
| consumer_aspect_ | 对测试卡生产器应用此宽高比 | double |
| ratio | ||
| rescale.interp | 测试图像使用此缩放方法 | “string” |
虽然其中大多数基本不言自明,但 normalised_width 和 normalised_height 的值需要稍作解释。之所以需要它们,是为了确保特效无论消费器或生产器请求 什么样的宽高,都始终按照 PAL 或 NTSC 一致地处理。
test_image 和 test_audio 标志用于决定何时应当合成图像和音频。
生产器实现还可以提供额外的属性,滤镜、转场和消费器也可以添加额外属性 以传达特定请求。这些记录在 modules.txt 中。
mlt_service
service 基类扩展了 properties,允许 0 到 m 个输入和 0 到 n 个输出, 表示如下:
+-----------+
- ->| |- ->
- ->| Service |- ->
- ->| |
+-----------+
| properties|
+-----------+service 的后代会对输入和输出如何连接施加限制,并提供一组基本属性。 通常,service 实例会被其后代封装起来,以便后代确保其连接规则得到遵守。
service 在构造时不定义任何属性。需要注意,生产器、滤镜和转场可能会被 序列化(例如通过 xml 消费器),因此应当注意区分可序列化属性和瞬态属性。 这里使用的约定是给瞬态属性加下划线前缀。
通常,只有 service 的直接后代需要调用这些方法,我们鼓励开发者在定义 新服务时使用这些扩展。
mlt_producer
生产器有 0 个输入和 1 个输出:
+-----------+
| |
| Producer |--->
| |
+-----------+
| service |
+-----------+生产器为文件读取器、管道、流或任何其他图像/音频输入提供了一种抽象。
实例化时,生产器具有以下属性:
| 名称 | 描述 | 取值 |
| mlt_type | 生产器的类型 | mlt_producer |
| position | 生产器的帧位置 | 0 到 n |
| speed | 生产器的速度 | double |
| fps | 输出的每秒帧数 | 25 或 29.97 |
| in | 以帧计的开始点 | 0 到 length - 1 |
| out | 以帧计的结束点 | in 到 length - 1 |
| length | 输入以帧计的长度 | 0 到 n |
| aspect_ratio | 源的宽高比 | 0 到 n |
| eof | 片段结尾行为 | “pause” 或 “loop” |
| resource | 构造函数参数(即文件名) | “" |
生产器实现还可以提供额外的属性。