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 工具包的加锁要求。