docs
框架概览

框架概览

前言

MLT 是一个为电视广播设计的多媒体框架。因此,它提供了一种可插拔的架构, 以便纳入新的音频/视频源、滤镜、转场和播放设备。

该框架提供了所有 MLT 应用程序和服务模块(service)所定义的基础结构与 实用功能。

就其自身而言,该框架提供的更多只是‘抽象类’以及管理资源的工具, 例如内存、属性、动态对象加载和服务实例化。

本文档大致分为 3 个部分。第一部分提供 MLT 的基本概览,第二部分展示 其使用方式,最后一部分展示结构与设计,重点在于如何扩展系统。

目标读者

本文档作为该框架的‘路线图’提供,任何希望在 MLT 层面开发代码的人 都应将其视为必读内容。

这包括:

  1. 框架维护者;
  2. 模块开发者;
  3. 应用开发者;
  4. 所有对 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 对多轨道的处理方式由两条要求决定:

  1. 消费器和生产器需要通过单一帧相互通信;
  2. 希望能够序列化并操作一个‘网络’(或者你愿意称之为滤镜图 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 );

如果我们不想获取大小。

这里有两点:

  1. 除非你指定析构函数,否则在 properties 对象关闭后,分配的内存仍然存在。 在上述情况下,可以这样做到:
mlt_properties_set_data( properties, "image", image, size, free, NULL );

当 properties 被关闭,或者 “image” 的值被更改时,析构函数会被调用。

  1. 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.0frame_get_image
1.1filter_get_image:
弹出 data2 和 data1
frame_get_image
1.2producer_get_image
生成图像
1.3收到图像
更新图像
1.4收到图像

显然,如果滤镜对图像不感兴趣,它就应当不去动堆栈,这样消费器就会 直接从生产器获取图像。

音频的处理方式类似,如下所示:

阶段生产器滤镜消费器
2.0frame_get_audio
2.1filter_get_audio:
弹出 data
frame_get_audio
2.2producer_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构造函数参数(即文件名)“"

生产器实现还可以提供额外的属性。