[时间: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. 洛谷 P1910 L国的战斗之间谍

    洛谷 P1910 L国的战斗之间谍 传送门 思路 二维背包模板题 三维肯定会爆掉,所以换二维 代码 #include <bits/stdc++.h> #define N 1111 usin ...

  2. Spring Boot Admin2.X监控的服务context-path问题

    在使用Spring Boot Admin进行监控时,如果被监控的服务没有加context-path的话是不会有任何问题的,一旦服务加了context-path的配置,监控就会失败. 下图是正常情况的显 ...

  3. python实现的WebSocket客户端

    code #coding=utf- import json import time from websocket import create_connection ws = create_connec ...

  4. Python学习笔记系列

    1.小甲鱼 python 学习系列笔记

  5. 获取 Rancher 中 Prometheus 的数据

    1.需求 在 rancher 应用商店添加集群监控,会安装 prometheus.grafana:需要从 prometheus 的 api 中收集 pod 的一些信息. 查看grafana 配置的数据 ...

  6. lock、tryLock和lockInterruptibly的差別

    lock():若lock被thread A取得,thread B会进入block状态,直到取得lock:tryLock():若当下不能取得lock,thread就会放弃,可以设置一个超时时间参数,等待 ...

  7. [转帖]tcpdump详细教程

    tcpdump详细教程 https://www.jianshu.com/p/d9162722f189 tcpdump tcpdump - dump traffic on a network tcpdu ...

  8. Mysql 错误 ERROR 1 (HY000) at line 1: Can't create/write to file '/home/kaizenly/cfg_dict.csv' (Errcode: 13 - Permission denied)

    [1]问题描述 (1)执行SQL语句: use billing; select * from cfg_dict into outfile '/home/kaizenly/cfg_dict.csv' f ...

  9. R语言dai xie

    R语言,Python长期招代写,作业量充足,需要一定英文能力,价格满意.有意者请留言联系,谢谢

  10. Redis(八) LRU Cache

    Redis(八)-- LRU Cache 在计算机中缓存可谓无所不在,无论还是应用还是操作系统中,为了性能都需要做缓存.然缓存必然与缓存算法息息相关,LRU就是其中之一.笔者在最先接触LRU是大学学习 ...