[时间:2019-05] [状态:Open]

[关键词:字幕,libass,字幕渲染,ffmpeg, subtitles, video filter]

0 引言

libass库则是一个轻量级的对ASS/SSA格式字幕进行渲染的开源库。该库使用C编写,效率较高。据官方说明,libass和VSFilter兼容性最好~

libass依赖的第三方库是FreeType,FriBidi,NASM,Fontconfig(可选),HarfBuzz(可选)。

FreeType是libass使用的通用字体渲染库,也是很强大的库,作用是把系统的字库渲染成单张位图。

虽然官方源码提供的说明已经相对来说足够了,我编写此文主要的目的是学习下如何使用该库,并了解其基本构成。本文将包括:

  • libass如何编译
  • libass中demo源码解读
  • libass主要对外接口
  • ffmpeg中libass调用

1 libass编译

1.1 libass编译

首先从https://github.com/libass/libass.git中下载对应源码。

进入git所在的根目录,对应的可执行文件只有一个,autogen.sh,执行之。

这样会在当前目录下生成configure脚本,运行下面命令:

./configure

我的ubuntu(18.04)主机上遇到下面问题:

configure: error: Package requirements (fribidi >= 0.19.0) were not met:

No package 'fribidi' found

好吧。貌似是没有fribidi这个库,可以从https://github.com/fribidi/fribidi中下载对应源码,然后编译,命令类似:

./autogen.sh

./configure

make

sudo make install

然后重新运行以下命令:

./configure

make

这样基本编译完成。最终生成的libass.a位于./libass/.libs/目录下。

1.2 测试demo编译

当然在./test目录下还提供了一个libass库测试demo。为了编译之,我们需要将之前生成的libass.a拷贝到./test/.lib/目录下,并在./test目录下执行make命令,这样就会生成最后的可验证的测试程序(默认名字为test)。当然如果在编译中遇到错误,请按照错误提示添加确实的依赖项。比如我的主机遇到以下编译错误:

test-test.o: In function `write_png':
./test/test.c:53: undefined reference to `png_create_write_struct'
./test/test.c:54: undefined reference to `png_create_info_struct'
./test/test.c:57: undefined reference to `png_set_longjmp_fn'
./test/test.c:58: undefined reference to `png_destroy_write_struct'
./test/test.c:69: undefined reference to `png_init_io'
./test/test.c:70: undefined reference to `png_set_compression_level'
./test/test.c:72: undefined reference to `png_set_IHDR'
./test/test.c:76: undefined reference to `png_write_info'
./test/test.c:78: undefined reference to `png_set_bgr'
./test/test.c:84: undefined reference to `png_write_image'
./test/test.c:85: undefined reference to `png_write_end'
./test/test.c:86: undefined reference to `png_destroy_write_struct'
collect2: error: ld returned 1 exit status
Makefile:366: recipe for target 'test' failed
make: *** [test] Error 1

只要在Makefile中添加上png库的引用即可,即在LIBS最后添加-lpng

执行test程序,有以下输出:

$ ./test
usage: ./test <image file> <subtitle file> <time>

这三个参数中第一个是输出png的路径,第二个是ass字幕文件路径,第三个time是渲染字幕文件中指定时间点,浮点数,单位为秒。

我们可以使用下面ass字幕作为测试,我将其保存在test同目录下,命名为a.ass

[Script Info]
; Script generated by FFmpeg/Lavc58.14.100
ScriptType: v4.00+
PlayResX: 384
PlayResY: 288 [V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,16,&Hffffff,&Hffffff,&H0,&H0,0,0,0,0,100,100,0,0,1,1,0,2,10,10,10,0 [Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.10,0:00:00.20,Default,,0,0,0,,1st just ass text
Dialogue: 0,0:00:00.20,0:00:00.30,Default,,0,0,0,,2nd info to show
Dialogue: 0,0:00:00.30,0:00:00.40,Default,,0,0,0,,3rd for order check
Dialogue: 0,0:00:07.25,0:00:14.45,Default,,0,0,0,,4th for time check
Dialogue: 0,0:00:15.22,0:00:23.30,Default,,0,0,0,,5th endline

然后我们执行下面命令将会看到字幕渲染之后的png。

./test out.png a.ass 0 # 0s无字幕,生成灰色空白图

./test out.png a.ass 0.15 # 0.15s有字幕,图片下方居中显示第一行字幕

./test out.png a.ass 0.5 # 0.5s无字幕,生成灰色空白图

./test out.png a.ass 7.26 # 7.26s有字幕,图片下方显示第四行字幕

具体效果图建议实际尝试下,这里不截图了。

2 demo源码解析

libass库中的test/test.c整体逻辑比较简单,总共200行左右代码量。主要完成了三部分内容:

  • ass解析
  • 文本-->图形转换
  • 多图像叠加,rgb转png

具体实现代码如下:

// 初始化libass库
init(frame_w, frame_h); // ASS文件读取并解析
ASS_Track *track = ass_read_file(ass_library, subfile, NULL);
// 渲染指定时间点的字幕,结果保存在img中,这可能是一个图片列表
ASS_Image *img = ass_render_frame(ass_renderer, track, (int) (tm * 1000), NULL); // 多个ASS_Image合成为一个图片中
image_t *frame = gen_image(frame_w, frame_h);
blend(frame, img); // 反初始化
ass_free_track(track);
ass_renderer_done(ass_renderer);
ass_library_done(ass_library); // rgb保存为png
write_png(imgfile, frame);
free(frame->buffer);
free(frame);

3 libass库接口分析

libass库主要功能有两个:

  • ass解析
  • 字幕渲染

接口分为三类:

3.1 全局性接口

此类接口主要与ASS_Library相关,这是使用libass库必须打交道的结构体。主要包括以下几个常用接口:

int ass_library_version(void); // 获得库的版本号

// 这是使用ass库必须调用的第一个函数
ASS_Library *ass_library_init(void); // ass库卸载函数,一般在程序退出时调用
void ass_library_done(ASS_Library *priv); // 注册ass库消息回调函数
void ass_set_message_cb(ASS_Library *priv,
void (*msg_cb)(int level, const char *fmt, va_list args, void *data),
void *data); // 获取可用的字体库
void ass_get_available_font_providers(ASS_Library *priv,
ASS_DefaultFontProvider **providers,
size_t *size);

3.2 ass解析接口

此类接口与ASS_Track直接相关,我们可以称之为字幕轨,具体相关接口如下:

// 创建和释放ASS_Track
ASS_Track *ass_new_track(ASS_Library *);
void ass_free_track(ASS_Track *track); // 创建和释放style/event
int ass_alloc_style(ASS_Track *track);
int ass_alloc_event(ASS_Track *track); void ass_free_style(ASS_Track *track, int sid);
void ass_free_event(ASS_Track *track, int eid); // 解析ASS中的chunk数据
void ass_process_data(ASS_Track *track, char *data, int size);
void ass_process_chunk(ASS_Track *track, char *data, int size,
long long timecode, long long duration); // 清空所有event
void ass_flush_events(ASS_Track *track); // 使用本地文件或内存数据作为源创建ASS_Track
ASS_Track *ass_read_file(ASS_Library *library, char *fname,
char *codepage);
ASS_Track *ass_read_memory(ASS_Library *library, char *buf,
size_t bufsize, char *codepage);

3.3 字幕渲染接口

此类接口主要与ASS_Renderer有关,最终生成RGBA格式的ASS_Image。主要接口如下:

// 初始化及渲染结束
ASS_Renderer *ass_renderer_init(ASS_Library *);
void ass_renderer_done(ASS_Renderer *priv); // 给定时间点渲染文本为图片格式
ASS_Image *ass_render_frame(ASS_Renderer *priv, ASS_Track *track,
long long now, int *detect_change);

最后一个结构体是ASS_Image,我们需要理解其具体构成才能使用其中存储的数据,其定义如下:

// 由ass renderer产生的图像链表
typedef struct ass_image {
int w, h; // Bitmap width/height
int stride; // Bitmap stride
unsigned char *bitmap; // 1bpp stride*h alpha buffer
// Note: the last row may not be padded to
// bitmap stride!
uint32_t color; // Bitmap color and alpha, RGBA
int dst_x, dst_y; // Bitmap placement inside the video frame struct ass_image *next; // Next image, or NULL enum {
IMAGE_TYPE_CHARACTER,
IMAGE_TYPE_OUTLINE,
IMAGE_TYPE_SHADOW
} type; } ASS_Image;

4 ffmpeg中libass的使用

ffmpeg中有两个video filter是与libass库相关的,分别是asssubtitles。前者仅支持ass格式字幕,后者支持所有格式字幕(实际上是subtitles filter将其他格式字幕转化为ass字幕,然后调用libass库)。

此处以ass filter为例说明下。

相关源码位于libavfilter/vf_subtitles.c中。ass filter定义如下:

AVFilter ff_vf_ass = {
.name = "ass",
.description = NULL_IF_CONFIG_SMALL("Render ASS subtitles onto input video using the libass library."),
.priv_size = sizeof(AssContext),
.init = init_ass,
.uninit = uninit,
.query_formats = query_formats,
.inputs = ass_inputs,
.outputs = ass_outputs,
.priv_class = &ass_class,
};

其实这个filter的代码只有两个主要的函数,init_ass和uninit。下面我们依次查看下其实现代码:

uninit函数很简单,代码如下:

static av_cold void uninit(AVFilterContext *ctx)
{
AssContext *ass = ctx->priv;
// 全部是关于ass资源释放的逻辑
if (ass->track)
ass_free_track(ass->track);
if (ass->renderer)
ass_renderer_done(ass->renderer);
if (ass->library)
ass_library_done(ass->library);
}

init_ass函数代码如下:

static av_cold int init_ass(AVFilterContext *ctx)
{
AssContext *ass = ctx->priv;
int ret = init(ctx);// 这个函数完成libass初始化 if (ret < 0)
return ret; /* 初始化字体 */
ass_set_fonts(ass->renderer, NULL, NULL, 1, NULL, 1);
// 读取ass文件
ass->track = ass_read_file(ass->library, ass->filename, NULL);
if (!ass->track) {
av_log(ctx, AV_LOG_ERROR,
"Could not create a libass track when reading file '%s'\n",
ass->filename);
return AVERROR(EINVAL);
}
return 0;
} static av_cold int init(AVFilterContext *ctx)
{
AssContext *ass = ctx->priv; if (!ass->filename) {
av_log(ctx, AV_LOG_ERROR, "No filename provided!\n");
return AVERROR(EINVAL);
}
// 下面初始化基本上是使用libass库必须的
ass->library = ass_library_init();
if (!ass->library) {
av_log(ctx, AV_LOG_ERROR, "Could not initialize libass.\n");
return AVERROR(EINVAL);
}
ass_set_message_cb(ass->library, ass_log, ctx); ass_set_fonts_dir(ass->library, ass->fontsdir); ass->renderer = ass_renderer_init(ass->library);
if (!ass->renderer) {
av_log(ctx, AV_LOG_ERROR, "Could not initialize libass renderer.\n");
return AVERROR(EINVAL);
} return 0;
}

还有两个重要的函数隐藏在ass_inputs数组中,定义如下:

static const AVFilterPad ass_inputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
.filter_frame = filter_frame,
.config_props = config_input,
.needs_writable = 1,
},
{ NULL }
};

第一个是config_input函数,用于设置ass字幕输出格式,实现如下:

static int config_input(AVFilterLink *inlink)
{
AssContext *ass = inlink->dst->priv; ff_draw_init(&ass->draw, inlink->format, ass->alpha ? FF_DRAW_PROCESS_ALPHA : 0); ass_set_frame_size (ass->renderer, inlink->w, inlink->h);
if (ass->original_w && ass->original_h)
ass_set_aspect_ratio(ass->renderer, (double)inlink->w / inlink->h,
(double)ass->original_w / ass->original_h);
if (ass->shaping != -1)
ass_set_shaper(ass->renderer, ass->shaping); return 0;
}

第二个是filter_frame函数,用于获得字幕帧。实现如下:

static void overlay_ass_image(AssContext *ass, AVFrame *picref,
const ASS_Image *image)
{
for (; image; image = image->next) {
uint8_t rgba_color[] = {AR(image->color), AG(image->color), AB(image->color), AA(image->color)};
FFDrawColor color;
ff_draw_color(&ass->draw, &color, rgba_color);
ff_blend_mask(&ass->draw, &color,
picref->data, picref->linesize,
picref->width, picref->height,
image->bitmap, image->stride, image->w, image->h,
3, 0, image->dst_x, image->dst_y);
}
} static int filter_frame(AVFilterLink *inlink, AVFrame *picref)
{
AVFilterContext *ctx = inlink->dst;
AVFilterLink *outlink = ctx->outputs[0];
AssContext *ass = ctx->priv;
int detect_change = 0;
double time_ms = picref->pts * av_q2d(inlink->time_base) * 1000;
ASS_Image *image = ass_render_frame(ass->renderer, ass->track,
time_ms, &detect_change); if (detect_change)
av_log(ctx, AV_LOG_DEBUG, "Change happened at time ms:%f\n", time_ms); overlay_ass_image(ass, picref, image); return ff_filter_frame(outlink, picref);
}

相信在了解libass对外接口及demo逻辑之后,直接阅读上述代码并没有什么难度。

5 小结

看完libass的头文件,发现libass库本身很清晰,对外接口简单易懂,值得推荐。如果有任何不对的地方,欢迎指正。

本文整理并介绍了如何编译libass库,及其主要对外接口,并说明了ffmpeg中如何使用libass库的。仅供后续参考。

6 参考资料

  1. github-libass
  2. ffmpeg加入libass
  3. CentOS6.2下编译xbmc
  4. libass-0.14.0

libass简明教程的更多相关文章

  1. 2013 duilib入门简明教程 -- 第一个程序 Hello World(3)

    小伙伴们有点迫不及待了么,来看一看Hello World吧: 新建一个空的win32项目,新建一个main.cpp文件,将以下代码复制进去: #include <windows.h> #i ...

  2. 2013 duilib入门简明教程 -- 部分bug (11)

     一.WindowImplBase的bug     在第8个教程[2013 duilib入门简明教程 -- 完整的自绘标题栏(8)]中,可以发现窗口最大化之后有两个问题,     1.最大化按钮的样式 ...

  3. 2013 duilib入门简明教程 -- 部分bug 2 (14)

        上一个教程中提到了ActiveX的Bug,即如果主窗口直接用变量生成,则关闭窗口时会产生崩溃            如果用new的方式生成,则不会崩溃,所以给出一个临时的快速解决方案,即主窗口 ...

  4. 2013 duilib入门简明教程 -- 自绘控件 (15)

        在[2013 duilib入门简明教程 -- 复杂控件介绍 (13)]中虽然介绍了界面设计器上的所有控件,但是还有一些控件并没有被放到界面设计器上,还有一些常用控件duilib并没有提供(比如 ...

  5. 2013 duilib入门简明教程 -- 事件处理和消息响应 (17)

        界面的显示方面就都讲完啦,下面来介绍下控件的响应.     前面的教程只讲了按钮和Tab的响应,即在Notify函数里处理.其实duilib还提供了另外一种响应的方法,即消息映射DUI_BEG ...

  6. 2013 duilib入门简明教程 -- FAQ (19)

        虽然前面的教程几乎把所有的知识点都罗列了,但是有很多问题经常在群里出现,所以这里再次整理一下.     需要注意的是,在下面的问题中,除了加上XML属性外,主窗口必须继承自WindowImpl ...

  7. Mac安装Windows 10的简明教程

    每次在Mac上安装Windows都是一件非常痛苦的事情,曾经为了装Win8把整台Mac的硬盘数据都弄丢了,最后通过龟速系统恢复模式恢复了MacOSX(50M电信光纤下载了3天才把系统下载完),相信和我 ...

  8. Docker简明教程

    Docker简明教程 [编者的话]使用Docker来写代码更高效并能有效提升自己的技能.Docker能打包你的开发环境,消除包的依赖冲突,并通过集装箱式的应用来减少开发时间和学习时间. Docker作 ...

  9. 2013 duilib入门简明教程 -- 总结 (20)

        duilib的入门系列就到尾声了,再次提醒下,Alberl用的duilib版本是SVN上第个版本,时间是2013.08.15~       这里给出Alberl最后汇总的一个工程,戳我下载,效 ...

随机推荐

  1. scrapy机制mark(基于twisted)

    twisted twisted管理了所有的异步任务 Twisted的主线程是单线程的,即reactor线程: 而这些io耗时操作会在线程池中运行,不再twisted主线程中运行,即通过线程池来执行异步 ...

  2. JAVA基础系列:Arrays.binarySearch二分查找

    首先,binarySearch方法为二分法查找,所以数组必须是有序的或者是用sort()方法排序之后的 1)  binarySearch(Object[] a, Object key) a: 要搜索的 ...

  3. Visual Studio 调试系列9 调试器提示和技巧

    系列目录     [已更新最新开发文章,点击查看详细] 01 固定数据提示 如果你在调试时,经常将鼠标悬停在数据提示上,就可能想固定变量的数据提示,方便自己随时查看. 即使在重新启动后,固定的变量也能 ...

  4. 在Azure DevOps Server(TFS)上集成Python环境,实现持续集成和发布

    Python和Azure DevOps Server Python是一种计算机程序设计语言.是一种动态的.面向对象的脚本语言,最初主要为系统运维人员编写自动化脚本,在实际应用中,Python已经在前端 ...

  5. Python连载31-threading的使用

    一. 例子:我们对传参是有要求的必须传入一个元组,否则报错 二. import _thread as thread import time def loop1(in1): print("St ...

  6. Data Science: An overview

    Week 1 Data Science: An overview Objective: 1.Is data science the same as statistics or analysis? st ...

  7. vue中js文件中export常见方法及使用

    js文件一般放静态资源或动态资源,我将在这两种不同资源的请求下以不同的方式export出来 第一种在js中使用function 动态资源 在vue文件中引用调用这些方法 import进来 在生命周期函 ...

  8. python实现AES加密

    pip install pycryptodomex 需要安装pycryptodomex模块 aes加密 from Cryptodome.Cipher import AES from binascii ...

  9. Redis(序)应用场景

    前言 在阅读了<大型网站技术架构:核心原理与案例分析>书后,稍微了解了Redis在大型网站架构中的应用场景和目的. 大型网站都是从小用户量,小流量的网站演变过来的,在小型网站的架构之初,L ...

  10. MySQL如何定位并优化慢查询sql

    1.如何定位并优化慢查询sql a.根据慢日志定位慢查询sql SHOW VARIABLES LIKE '%query%'      查询慢日志相关信息 slow_query_log 默认是off关闭 ...