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'\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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM