> 文章列表 > MLT 视频编辑框架简介(二):框架设计简述

MLT 视频编辑框架简介(二):框架设计简述

MLT 视频编辑框架简介(二):框架设计简述

系列文章目录

  • MLT 视频编辑框架简介(一)编译与 demo 运行

文章目录

  • 系列文章目录
  • 0. 前言
  • 1. 基本概述
  • 2. 使用
    • Hello World
    • Factories
    • Service 属性
    • PlayList
    • Filters
    • Attached Filters
    • Introduction the Mix
    • Practicalities and Optimisations
    • Multiple Tracks and Transitions
    • Chain And Link
  • 3. 结构与设计
    • Class Hierarchy
    • mlt_properties
    • mlt_deque
    • mlt_pool
    • mlt_frame
  • 总结

0. 前言

作者在 Framework Design 中阐述了 MTL 框架的设计思想。本文对这篇文档进行总结和梳理,选择重点内容进行详细说明。

1. 基本概述

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,它们之间的通信包括三个阶段:

  1. 从上游获取 MLT frame
  2. 从 MLT frame 中获取视频帧
  3. 从 MLT frame 中获取音频帧

MLT采用了 “Lazy Evalution”,即在调用获取视频帧和音频帧方法之前,不需要从源中提取图像和音频。从本质上讲,Consumer 从上游获取数据,意味着 Consumer 通常需要启动额外的线程来抱歉实时的吞吐量。

2. 使用

Hello World

下面是 MTL 中实现视频播放的示例,我们逐一分析。

#include <stdio.h>
#include <unistd.h>
#include <framework/mlt.h>int main(int argc, char *argv[])
{// Initialise the factoryif (mlt_factory_init(NULL)){// Create a profilemlt_profile profile = mlt_profile_init(NULL);// Create the default consumermlt_consumer hello = mlt_factory_consumer(profile, NULL, NULL);// Create via the default producermlt_producer world = mlt_factory_producer(profile, NULL, argv[1]);// Connect the producer to the consumermlt_consumer_connect(hello, mlt_producer_service(world));// Start the consumermlt_consumer_start(hello);// Wait for the consumer to terminatewhile (!mlt_consumer_is_stopped(hello)) {sleep(1);}// Close the consumermlt_consumer_close(hello);// Close the producermlt_producer_close(world);// Close the profilemlt_profile_close(profile);// Close the factorymlt_factory_close();}else{// Report an error during initializationfprintf(stderr, "Unable to locate factory modules\\n");}// End of programreturn 0;
}
  • 使用 MLT 的第一个步骤是初始化工厂,即调用 mlt_factory_init 方法,该方法传入的是一个文件夹路径,这个文件夹里头有各种 .so,每个 so 都是 MLT 中的一个模块。如果传入的是 NULL 参数,MLT 会从环境变量 MLT_REPOSITORY 中获取默认的模块文件夹路径。
  • 所有的 Consumer、Producer、Filter 等都注册在工厂中,在 mlt_factory_init 调用成功后,你便可以通过 mlt_factory_consumermlt_factory_producer 等函数来创建对应的 service。
  • mlt_factory_producer 函数用于创建 producer,它接收三个参数:
    1. profile,即视频的相关配置,包括视频大小、fps 等。
    2. service name,即 producer 的名字。你可以在 Producers Plugins 中找到 MLT 支持的所有 producer 信息。
    3. resource,即创建 producer 所需要的资源。例如你创建一个叫 avformat 的 producer,用于视频解码。那么 resource 就是视频的路径。
  • mlt_factory_consumer 函数用于创建 consumer,它接收三个参数:
    1. profile,即视频的相关配置,包括视频大小、fps 等。
    2. service name,即 consumer 的名字。你可以在 Consumers Plugins
    3. input,可选的创建参数,通常是一个字符串。你可以在各个模块的 factory.c 中查看 xxx_init 函数,来判断 input 参数的使用情况。
  • 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 的窗口。
  • 此外,默认的 Producer 或者 Consumer 可以通过环境变量来修改,例如:
    • 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/dv1394file.avi 文件广播到 DV 设备上

Factories

MLT 框架中所有的 service,都通过插件的形式载入。正如前面所说,每一个插件都是一个 so 文件。具体的,你可以在 MLT 源码的 “module” 目录下看到每个插件实现。mlt_factory_init 函数用于加载这些 so 文件。

Service 属性

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

Playlist 是一个组件,它可以将多个 Producer 按顺序组合成一个连续的序列。Playlist 提供了一个简单的线性编辑方式,允许您轻松地按顺序播放多个音视频片段,还可以在片段之间插入空白(blank)或其他特殊片段。注:PlayList 本身是一个 Producer,因此你可以将一个 PlayList 插入在另一个 PlayList 上。
例如,你要连续播放一些列文件,那么使用 PlayList 即可:

mlt_producer create_playlist( int argc, char **argv )
{// We're creating a playlist heremlt_playlist playlist = mlt_playlist_init( );// Loop through each of the argumentsint i = 0;for ( i = 1; i < argc; i ++ ){// Create the producermlt_producer producer = mlt_factory_producer( NULL, argv[ i ] );// Add it to the playlistmlt_playlist_append( playlist, producer );// Close the producer (see below)mlt_producer_close( producer );}// Return the playlist as a producerreturn mlt_playlist_producer( playlist );
}

Filters

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。

Attached Filters

所有 Service 都能做 Attached Filters 的操作。这个功能很方便,例如你有一个 PlayList,它又 3 个 producer,你希望在第二个 producer 上添加一个 Filter。那么可以这么做:

	// Create a producermlt_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 filtermlt_filter filter = mlt_factory_filter( "greyscale" );// Create a playlistmlt_playlist playlist = mlt_playlist_init( );// Attach the filter to the producermlt_service_attach( producer1, filter );// Construct a playlist with various cuts from the producermlt_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 nowmlt_producer_close( producer );mlt_filter_close( filter );

Introduction the Mix

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 transitionmlt_transition transition = mlt_factor_transition( "luma", NULL );// Mix the first and second clips for 50mlt_playlist_mix( playlist, i, 50, transition );// Close the transitionmlt_transition_close( transition );
}

Practicalities and Optimisations

考虑一个问题:如果你在两个 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。

Multiple Tracks and Transitions

下面介绍 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|-|--->|             |--->|       |
| +------+ |    +-------------+    +-------+
+----------+

因此,多轨的处理逻辑是这样的:

  1. 创建一个 Tractor
  2. 通过 Tractor 获取到 Multitrack 和 Field
  3. 放置 Producer 到 Multitrack 的 Track 上,按需添加
  4. 通过 Field 添加 Filter 和 Transition
  5. 链接 Tractor 到 Consumer,进行消费

从本质上讲,这就是它在消费者眼中的样子:

+-----------------------------------------------+
|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|/|  |
| | +------+ |    | | |    | |    | |    | | |  |
| +----------+    | +-+    +-+    +-+    +-+ |  |
|                 +--------------------------+  |
+-----------------------------------------------+	

Chain And Link

这部分没看明白,且 Link 模块非常少,先略过。

3. 结构与设计

Class Hierarchy

mlt_properties
├─ mlt_frame
└─ mlt_service├─ mlt_producer│  ├─ mlt_playlist│  ├─ mlt_tractor│  ├─ mlt_chain│  └─ mlt_link├─ mlt_filter├─ mlt_transition└─ mlt_consumermlt_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

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 框架中的一个双端队列(deque)实现,它结合了栈和队列的功能。在 MLT 中,栈操作主要用于反向波兰表示法 (RPN) 的图像和音频操作以及内存池管理,而队列操作则用于消费者基类以及其他消费者实现可能需要的队列。

mlt_pool

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

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 框架的你。