zoukankan      html  css  js  c++  java
  • libass简明教程

    [时间: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'
    ",
                   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!
    ");
            return AVERROR(EINVAL);
        }
    	// 下面初始化基本上是使用libass库必须的
        ass->library = ass_library_init();
        if (!ass->library) {
            av_log(ctx, AV_LOG_ERROR, "Could not initialize libass.
    ");
            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.
    ");
            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
    ", 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
  • 相关阅读:
    PE文件捆绑实现二:(远程线程注入)
    C++中Vector清空
    ttrss更新到最新版本后发访问非80和443端口规避
    Git配置https_proxy访问github失败
    Haproxy配置拦截指定src的连接
    synology git管理程序添加
    ActiveMQ深入浅出系列 (一)
    sl4fj日志级别
    HTTP上传文件解析
    linux下jcmd无法获取jvmdump
  • 原文地址:https://www.cnblogs.com/tocy/p/subtitle-format-libass-tutorial.html
Copyright © 2011-2022 走看看