docs
MLT++ C++ 绑定

MLT++ C++ 绑定

MLT++

这个 mlt 子项目为 MLT 库提供了 C++ 封装。

用法

在 Makefile 中使用以下定义来编译并链接 mlt++:

CXXFLAGS=`pkg-config --cflags mlt-framework-7` -Wall
LDFLAGS=-lmlt++-7 `pkg-config --libs mlt-framework-7`

各个类的头文件可以显式地逐个包含,即:

#include <mlt++/MltProducer.h>
etc

或者也可以用以下语句一次性全部包含:

#include <mlt++/Mlt.h>

所有定义都放在 Mlt 命名空间中,并紧密遵循 C 的 命名约定。映射总是遵循如下模式:

工厂方法(Factory methods):

  • mlt_factory_init ==> Mlt::Factory::init
  • mlt_factory_producer ==> Mlt::Factory::producer
  • mlt_factory_filter ==> Mlt::Factory::filter
  • mlt_factory_link ==> Mlt::Factory::link
  • mlt_factory_transition ==> Mlt::Factory::transition
  • mlt_factory_consumer ==> Mlt::Factory::consumer
  • mlt_factory_close ==> Mlt::Factory::close

注意:用 Factory 构造服务模块是可选的。

类型:

  • mlt_chain ==> Mlt::Chain
  • mlt_properties ==> Mlt::Properties
  • mlt_frame ==> Mlt::Frame
  • mlt_link ==> Mlt::Link
  • mlt_service ==> Mlt::Service
  • mlt_producer ==> Mlt::Producer
  • mlt_filter ==> Mlt::Filter
  • mlt_transition ==> Mlt::Transition
  • mlt_consumer ==> Mlt::Consumer

方法:

  • mlt_type_method ==> Mlt::Type.method
  • 例如:mlt_playlist_append ==> Mlt::Playlist.append

父类的方法可以直接在子类上使用。

此外,你可以指定:

using namespace Mlt;

以避免强制使用 Mlt:: 前缀。

枚举器和宏直接复用自 C 库。

类层次结构

当前已映射的对象在下面的层次结构中展示:

Factory
  Properties
    Frame
    Service
    Consumer
    Field
    Filter
    Multitrack
    Producer
      Chain
      Link
      Playlist
      Tractor
      Transition

特殊情况

使用封装对象时需要多加小心。

以返回某个服务模块的直接消费器的 C 函数为例:

mlt_service mlt_service_consumer( mlt_service );

它映射为:

Mlt::Service *Mlt::Service.consumer( );

注意你拿到的是一个对象——它绝不是原来的 C++ 对象,而是一个 封装对象。这样做是为了与 C API 保持一致,因为 C API 可能 实例化 C 实例——因此不能假设所有 mlt 服务模块实例都存在 对应的 C++ 对象。

因此,你必须删除这些对象。原对象不会 受到影响。不过,所有其他修改(对属性或连接状态的修改) 都会反映在原对象上。

这种方式排除了使用 RTTI 来确定对象 真实类型的可能——这只能通过解析对象的属性来完成。

对象可能是无效的——使用前务必使用 is_valid 方法检查有效性。

局限性

定义新服务模块的机制被有意排除在 C++ 封装之外——这样做是为了确保所构造的服务模块 网络可以被序列化,并被基于 C API 的现有应用程序 (如 melted)使用。

Hello World

mlt++ 封装是 mlt C 库的 C++ 封装。因此,它 为底层库提供干净的 C++ 访问方式。

一个使用示例如下:

#include <mlt++/Mlt.h>
using namespace Mlt;

int main( void )
{
    Factory::init( );
    Profile profile;
    Producer p( profile, "pango:", "Hello World" );
    Consumer c( profile, "sdl2" );
    c.connect( p );
    c.run( );
    return 0;
}

这是一个相当典型的 mlt++ 用法示例——创建一个『生产器』(一个 产生『帧』的对象),创建一个『消费器』(一个消费 帧的对象),把它们连接起来,启动消费器并等待完成(这里 我们只是等待用户关闭窗口)。

在这个例子中,我们用『sdl2』消费器构造一个作为消费器的 窗口(SDL 是一个标准可移植库,为加速视频显示 和音频提供平台无关的访问),并使用『pango』 生产器生成带有『Hello World』字样的帧(pango 是 gtk 工具箱的一个库)。

这个例子的要旨在于表明 mlt 利用现有库来 提供其功能——这使得框架本身非常小巧。

请注意,mlt 的设计目标是嵌入到 GUI 或服务器类应用程序中—— 典型应用中不会像示例展示的那样等待消费器停止。

到目前为止,我们已经介绍了 Producer 和 Consumer 这两个 mlt 类。我们会在教程后面更详细地讲解它们,但眼下先 简要介绍其余的类。

播放列表(Playlists)

另一个简单的类是 Playlist(播放列表)——它是 Producer 的直接 扩展,允许你维护一个生产器对象的列表。

作为一个展示 Playlist 用法的简单例子,我们把上面的例子 改造成一个可播放多个视频或音频文件的应用程序。

#include <mlt++/Mlt.h>
using namespace Mlt;

int main( int argc, char **argv )
{
    Factory::init( );
    Profile profile;
    Playlist list( profile );
    for ( int i = 1; i < argc; i ++ )
    {
        Producer p( profile, argv[i] );
        if ( p.is_valid( ) )
            list.append( p );
    }
    Consumer c( profile, "sdl2" );
    c.connect( list );
    c.run( );
    return 0;
}

现在你可以这样运行该程序:

./player *.avi *.mp3 *.jpg etc

在这个例子中,我们只是简单地把生产器追加进去来构造播放列表。 请注意,虽然 Producer 的作用域仅限于内部 的 for 循环,我们却可以放心地把它加入播放列表——这是因为 所有 mlt 对象都维护引用计数,在引用全部消失之前 对象不会真正被销毁。在这个例子中,当 list 对象 离开作用域时,我们创建的所有生产器都会自动 被销毁。

滤镜(Filters)

到目前为止,我们展示了如何加载和播放媒体。我们简要 介绍了 Playlist 容器,现在该开始操作一些东西了……

下一个例子中,我要给视频加一个『水印』 (watermark)——广播机构用水印来标识频道品牌,通常 由某种徽标组成。我们就用黑色文字配半透明 红色背景。

#include <mlt++/Mlt.h>
using namespace Mlt;

int main( int argc, char **argv )
{
    Factory::init( );
    Profile profile;
    Playlist list( profile );
    for ( int i = 1; i < argc; i ++ )
    {
        Producer p( profile, argv[i] );
        if ( p.is_valid( ) )
            list.append( p );
    }
    Filter f( profile, "watermark", "pango:" );
    f.set( "producer.text", "MLT++" );
    f.set( "producer.fgcolour", "0x000000ff" );
    f.set( "producer.bgcolour", "0xff000080" );
    list.attach( f );
    Consumer c( profile, "sdl2" );
    c.connect( list );
    c.run( );
    return 0;
}

请注意,watermark 滤镜复用了我们在第一个例子中展示的 『pango』生产器。事实上,这里可以用任何生产器——如果你想 用一幅图或一段视频,只需构造滤镜时把它的完整路径 作为第二个参数传入即可。

我们用 set 方法来操作滤镜——这个方法 在第一个例子中也出现过。

最后,我们把滤镜附加到播放列表上。这确保从 播放列表获取的所有帧都会被加上水印。

剪切(Cuts)

当你把一个片段添加到播放列表时,会创建一个 cut(剪切)对象——它只是 生产器的一个封装,覆盖指定的 in 和 out 入出点。

每当你从播放列表中取出一个片段时,得到的总是一个 cut 对象。 这允许你把滤镜附加到生产器的特定部分,并且即使该 cut 在 播放列表中的位置发生变化,滤镜也会保持与之正确 关联。

生产器和 cut 的行为通常是相同的,但如果你需要 区分它们,可以使用:

if ( producer.is_cut( ) )

而要获取一个 cut 的父对象,可以使用:

Producer parent = producer.parent_cut( );

直接附加到父对象上的滤镜会在附加到 cut 的任何 滤镜之前执行。

Tractor(牵引器)

tractor 是一个允许操纵多条视频和音频轨道的对象。

暂时偏离我们一直在折腾的播放器例子,假设我们想做类似 给视频配音(dub)的事情。这 是非常简单就能做到的:

Tractor *dub( char *video_file, char *audio_file )
{
    Profile profile;
    Tractor *tractor = new Tractor( profile );
    Producer video( profile, video_file );
    Producer audio( profile, audio_file );
    tractor->set_track( video, 0 );
    tractor->set_track( audio, 1 );
    return tractor;
}

需要做的就是这些——你现在可以把返回的对象连接到 消费器,或加入播放列表,甚至把它作为一条轨道添加到另一个 tractor。

转场(Transition)

现在假设我们想混合两条轨道之间的音频——为此,我们 需要引入转场(transition)的概念。mlt 中的转场是一种服务 模块,它把两个生产器的帧组合起来生成新的帧。

Tractor *mix( char *video_file, char *audio_file )
{
    Profile profile;
    Tractor *tractor = new Tractor( profile );
    Transition mix( profile, "mix" );
    Producer video( profile, video_file );
    Producer audio( profile, audio_file );
    tractor.set_track( video, 0 );
    tractor.set_track( audio, 1 );
    tractor.field.plant_transition( mix, 0, 1 );
    return tractor;
}

这样返回的 tractor 就会混合原始视频的音频和 音频文件的音频。

Mix(混合)

有一个便捷函数简化了在播放列表上相邻 cut 之间 应用转场的过程。这通常往往优于自行构造 tractor 和转场的配置。

要在播放列表的第一个和第二个 cut 之间应用一个 25 帧的 luma 转场,你可以使用:

Transition luma( profile, "luma" );
playlist.mix( 0, 25, luma );

事件(Events)

通常,应用程序需要在 mlt++ 对象发生变化时得到通知。 这支撑了诸如撤销/重做管理、时间线控件中的项目 渲染,以及应用程序所需的其他许多操作类型的服务。

例如下面这个例子:

class Xml
{
    private:
        Consumer consumer;
        Tractor &tractor;
    public:
        Xml( MltTractor &tractor ) :
            tractor( tractor ),
            consumer( *tractor.profile(), "xml" )
        {
            consumer.connect( tractor );
            tractor.listen( tractor, "producer-changed", 
                            ( mlt_listener )Xml::listener );
        }
        
        static void listener( Properties *tractor, Xml *object )
        {
            object->activate( );
        }
        void activate( )
        {
            consumer.start( );
        }
};

现在,每次 tractor 被改动时,其 XML 表示都会被输出到 stderr。

剧终人散……

信不信由你,以上对 mlt++ 中你通常会打交道的类做了 一个相当完整的概述。显然,实际内容比这要多一些—— 有几个内部类被略过了(特别是 Properties 和 Service 基类)。下一节将涵盖上述全部内容, 但会更详细得多……

深入探究

上一节旨在带大家走马观花地浏览主要的框架类。 本节将带你走上风景线,细细看来。

介绍基类

mlt 中的服务模块(Services)是 Producer(生产器)、Filter(滤镜)、 Transition(转场)和 Consumer(消费器)的总称。Service 也是 所有这些类所继承的基类。它提供了前面各节 示例中一直展示的基本连接能力。

属性(Properties)是我们与服务模块通信的主要方式—— 本质上,它提供了对具名值的 get/set 方法。所有服务模块都继承自 Properties。

Properties(属性)

Properties 提供了与服务模块通信的通用机制—— 通过 Properties 接口,我们能够操作和序列化 服务模块的状态。

例如,要把所有属性转储到 stdout,你可以使用类似 这样的代码:

void dump( Properties &properties )
{
    for ( int i = 0; i < properties.count( ); i ++ )
        cout << Properties.get_name( i ) << " = " << Properties.get( i ) << endl;
}

请注意,properties 对象会处理类型转换,所以下面 这样是可以的:

properties.set( "hello", "10.5" );
int hello_int = properties.get_int( "hello" );
double hello_double = properties.get_double( "hello" );

还提供了一些便捷方法用于检查或序列化属性 对象。

例如:

properties.debug( );

会把所有可序列化的属性以如下形式报告到 stderr:

Object: [ ref=1, in=0, out=0, track=0, u=75, v=150, _unique_id=15, 
mlt_type=filter, mlt_service=sepia ]

服务模块(Services)

通常,所有服务模块都通过各自具体类的 构造函数来构造。很多时候,你拿到的是 Service 对象而不是 它们的具体类型。为了访问扩展类的接口, 你需要创建一个引用。

例如,给定一个任意的 Service 对象,你可以用 type 方法 确定它的类型——它会返回一个『service_type』, 取值有 producer_type、filter_type 等。或者,你也可以 创建一个封装对象并检查其有效性。

bool do_we_have_a_producer( Service &service )
{
    Producer producer( service );
    return producer.is_valid( );
}

服务器与 MLT XML 文档

出于各种原因,你可能想把一个生产器序列化为字符串。 为此,你只需指定要写入的属性:

Profile profile;
Consumer xml( profile, "xml", "buffer" );
xml.connect( producer );
xml.start( );
buffer = xml.get( "buffer" );

名字可以随便取,并且可以通过『resource』属性 来更改。任何含有『.』的名字都被视为文件。因此, 你可以用 xml 消费器为同一个 MLT 对象存储多个 实例——如果你在编辑应用程序中想提供 撤销/重做功能,这会很有用。

如果你收到一个 XML 文档形式的字符串,并想把它 发送到服务器,可以使用:

Profile profile;
Consumer client( profile, "mvsp", "localhost:5250" );
client.set( "xml", buffer );
client.start( );

如果你需要从 XML 字符串获得一个 MLT 对象:

Profile profile;
Producer producer( profile, "xml-string", buffer );

下面是一个扩展服务器的可运行示例:

class ShotcutServer : public Melted
{
    public:
        ShotcutServer( char *id, int port ) :
            Melted( id, port )
        {
        }

        void set_receive_doc( bool doc )
        {
            set( "push-parser-off", doc );
        }

        // Reject all commands other than push/receive
        Response *execute( char *command )
        {
            mvsp_response response = mvsp_response_init( );
            mvsp_response_set_error( response, 400, "Not OK" );
            return new Response( response );
        }

        // Push document handler
        Response *received( char *command, char *doc )
        {
            mvsp_response response = mvsp_response_init( );
            // Use doc in some way and assign Response
            if ( doc != NULL )
                mvsp_response_set_error( response, 200, "OK" );
            return new Response( response );
        }

        // Push service handler
        Response *push( char *command, Service *service )
        {
            mvsp_response response = mvsp_response_init( );
            // Use service in some way and assign Response
            if ( service != NULL )
                mvsp_response_set_error( response, 200, "OK" );
            return new Response( response );
        }
};

注意:如果你要把它集成到 GUI 应用程序中,请记住 execute、received 和 push 方法是从一个线程中调用的——在与 UI 交互之前,务必遵守你的 GUI 工具包的加锁要求。