[時間:2018-11] [狀態:Open]
[關鍵詞:多媒體,字幕,文本,ffplay,FFmpeg,subtitle]
0 綜述
字幕是指電影、電視,以及戲劇、歌劇等舞台作品中出現的各種用途的文字,如版權標識、片名字幕、演(職)員表、說明字幕、歌詞字幕、對白字幕等。這些字幕按照影片放映時出現的先后順序而分為片頭字幕、片間字幕和片尾字幕。一般情況下,片頭、片尾字幕疊印在畫面上,而對白、歌詞等字幕一般出現在屏幕下方,戲劇等舞台傷口則顯示於舞台兩側或上方。
字幕與聲音語言相比,聲音語言有一定的局限性:有聲無形,轉瞬即逝,不易引起人們的注意,有時不易聽懂。如人物的語言和戲詞,有的因口音或語種的原因,受眾便很難聽清或聽懂,加上字幕就可以彌補這種局限性。因此,字幕與聲音和畫面相比,具有獨特的功能。
字幕的作用,主要是將語音內容以文字方式顯示,以幫助聽力較弱的觀眾理解節目內容。另外,對於不同語言的觀眾,只有通過字幕才能理解影片內容。而在中國,不同地區語言的發音差別很大,不能正確理解普通話的人很多。但文字寫法的差異並不大,看到普通話的文字后人們大都都能理解。所以,近年來華語圈的影視作品中,對應普通話(或方言)的字幕大多被附加在節目中。
本文主要內容是整理下目前常見的字幕格式,之后介紹下ffplay中字幕渲染的主要邏輯。
撰寫此文的主要目標在於整理下我在2018年的對字幕方面的主要理解和積累。
1 常見字幕格式及分類
通常字幕分類有兩個標准:基於文本的或基於圖片的、嵌入容器的或獨立存在的。
還有一種分類方式是按照字幕的表現方式,分為三類:
- 硬字幕。是將字幕疊加到視頻畫面上。因為這種字幕與視頻畫面是一體的,所以具有最佳的兼容性,只要能夠播放視頻,就能顯示字幕。缺點是字幕占據視頻畫面,破壞了視頻內容,而且不可取消、不可編輯更改。(嚴格意義上來說這已經不算字幕了,添加字幕之后視頻基本無法恢復,字幕也無法提取出來,甚至從容器來看根本就存在字幕流的概念。)
- 外掛字幕。將字幕做成一個獨立文件(字幕文件有多種格式)。這類字幕的優點是不破壞視頻畫面,可隨時根據需要更換字幕語言,並且可隨時編輯字幕內容。缺點是播放較為復雜,需要支持字幕播放的播放器支持。
- 軟字幕。是指通過某種方式將外掛字幕與視頻打包在一起,下載、復制時僅需復制一個文件即可。如DVD中的VOB文件,高清視頻封裝格式MKV、TS、AVI等。這類型文件一般可以同時封裝多種字幕文件,播放時通過播放器選擇所需字幕,非常方便。在需要的時候,還可以將字幕分離出來進行編輯修改或替換。
當然,你還可以按照實際用途划分。但在談及字幕格式時,通常我們指的是目前比較流行的字幕封裝格式。
字幕格式共分為兩類:圖形數據格式和文本數據格式。
1.1 圖形數據格式
這類字幕數據以圖片方式呈現,文件體積較大,不易於修改,有時亦稱為“硬字幕”。多用於非PC環境,例如DVD播放器、電視或視頻會議等。
- SUB格式
SUB格式的字幕數據由字幕圖片文件(.sub文檔)和字幕索引文件(.idx文檔)組成。一個.sub文檔可同時包含多個語言的字幕,由.idx進行調用。常見於DVD-VIDEO,但在DVD中,這兩個文件被集成到VOB內,需要通過軟件分離VOB來獲取字幕文件。
1.2 文本數據格式
這類字幕數據以文本格式呈現,文件體積較小,可直接用Windows自帶的記事本功能進行修改。
- SRT格式
SRT(Subripper)是最簡單的文本字幕格式,擴展名為.srt,其組成為:一行字幕序號,一行時間代碼,一行字幕數據。如:
45
00:02:52,184 --00:02:53,617
慢慢來
這表示:第45個字幕,顯示時間從該影片開始的第2分52.184秒到第2分53.617秒,內容為:慢慢來
-
SSA、ASS格式
SSA(Sub Station Alpha)是為了解決SRT過於簡單的字幕功能而開發的高級字幕格式,其擴展名為.SSA。采用SSA V4腳本語言,能實現豐富的字幕功能,除了能設定不同字幕數據的大小和位置外,更能實現動態文本和水印等復雜的功能。
ASS(Advanced SubStation Alpha)為是更高級的SSA版本,采用SSA V4+腳本語言編寫。它包含了所有SSA的所有特性,它可以將任何簡單的文本轉變成為卡拉OK的字幕樣式,數個項目旨在創建這些腳本。ASS的特點在於它比普通的SSA更為規范,如ASS的編程風格。 -
webvtt
此格式是在網站上配合HTML5視頻使用的字幕格式。它是基於SRT格式的,但並不完全兼容(更多資料參考w3c)。示例如下:
WEBVTT
Kind: captions
Language: en
1
00:00:00,264 --> 00:00:24,537
line 1
line 2
2
00:00:00,306 --> 00:00:04,306
line 1
line 2
line 3
line 4
3
00:00:30,544 --> 00:00:32,545
line 1
2 ffplay中字幕渲染邏輯
對於基於圖片的字幕,在視頻顯示時可以直接將字幕圖片疊加到畫面上。對於基於文本的字幕,需要通過其他技術先將文本轉化為可渲染的單元,然后渲染到播放畫面上。
FFmpeg,作為一個通用的播放框架,其中提供了對字幕的處理邏輯。本部分將重點介紹下ffplay中針對字幕的處理邏輯。
注意:我撰寫本文是FFmpeg最新的release是V4.1。
FFmpeg中添加了字幕的專用結構體,如下:
enum AVSubtitleType {
SUBTITLE_NONE,
SUBTITLE_BITMAP, ///< A bitmap, pict will be set
/**
* Plain text, the text field must be set by the decoder and is
* authoritative. ass and pict fields may contain approximations.
*/
SUBTITLE_TEXT,
/**
* Formatted text, the ass field must be set by the decoder and is
* authoritative. pict and text fields may contain approximations.
*/
SUBTITLE_ASS,
};
typedef struct AVSubtitleRect {
int x; ///< top left corner of pict, undefined when pict is not set
int y; ///< top left corner of pict, undefined when pict is not set
int w; ///< width of pict, undefined when pict is not set
int h; ///< height of pict, undefined when pict is not set
int nb_colors; ///< number of colors in pict, undefined when pict is not set
#if FF_API_AVPICTURE
attribute_deprecated
AVPicture pict;
#endif
/**
* data+linesize for the bitmap of this subtitle.
* Can be set for text/ass as well once they are rendered.
*/
uint8_t *data[4];
int linesize[4];
enum AVSubtitleType type;
char *text; ///< 0 terminated plain UTF-8 text
/**
* 0 terminated ASS/SSA compatible event line.
* The presentation of this is unaffected by the other values in this
* struct.
*/
char *ass;
int flags;
} AVSubtitleRect;
typedef struct AVSubtitle {
uint16_t format; /* 0 = graphics */
uint32_t start_display_time; /* relative to packet pts, in ms */
uint32_t end_display_time; /* relative to packet pts, in ms */
unsigned num_rects;
AVSubtitleRect **rects;
int64_t pts; ///< Same as packet pts, in AV_TIME_BASE
} AVSubtitle;
從上述結構來看,FFmpeg支持三種類型的字幕:位圖、普通文本以及ASS。每個AVSubtitle包含多個AVSubtitleRect,每個AVSubtitleRect中都有自己的字幕信息,比如文本內容或者圖片格式。
ffplay要實現字幕流的播放,首先需要decoder支持,對應的在ffplay源碼中有subtitle_thread線程專門用於字幕流解碼,其代碼如下:
static int subtitle_thread(void *arg)
{
VideoState *is = arg;
Frame *sp;
int got_subtitle;
double pts;
for (;;) {
// 從解碼后字幕流隊列中取一可用幀
if (!(sp = frame_queue_peek_writable(&is->subpq)))
return 0;
// 解碼 AVPacket-> AVSubtitle
if ((got_subtitle = decoder_decode_frame(&is->subdec, NULL, &sp->sub)) < 0)
break;
pts = 0;
/* 這里指定了僅支持圖片格式的字幕,文本字幕解碼之后直接丟棄了 */
if (got_subtitle && sp->sub.format == 0) {
if (sp->sub.pts != AV_NOPTS_VALUE)
pts = sp->sub.pts / (double)AV_TIME_BASE;
sp->pts = pts;
sp->serial = is->subdec.pkt_serial;
sp->width = is->subdec.avctx->width;
sp->height = is->subdec.avctx->height;
sp->uploaded = 0;
/* 將解碼之后的AVSubtitle放入隊列中 */
frame_queue_push(&is->subpq);
} else if (got_subtitle) {
avsubtitle_free(&sp->sub);
}
}
return 0;
}
字幕解碼之后的圖像數據需要通過視頻渲染線程才能最終被看到。其實現代碼如下:
// @video_image_display function
Frame *sp = NULL;
if (is->subtitle_st) { // 有字幕流的前提下
// 檢查並獲取字幕隊列的數據
if (frame_queue_nb_remaining(&is->subpq) > 0) {
sp = frame_queue_peek(&is->subpq);
// 根據當前視頻幀的時間戳計算字幕幀是否需要顯示
if (vp->pts >= sp->pts + ((float) sp->sub.start_display_time / 1000)) {
if (!sp->uploaded) {
uint8_t* pixels[4];
int pitch[4];
int i;
if (!sp->width || !sp->height) {
sp->width = vp->width;
sp->height = vp->height;
}
if (realloc_texture(&is->sub_texture, SDL_PIXELFORMAT_ARGB8888, sp->width, sp->height, SDL_BLENDMODE_BLEND, 1) < 0)
return;
// 將所有AVSubtitleRect合成到一個SDL_Texture中
for (i = 0; i < sp->sub.num_rects; i++) {
AVSubtitleRect *sub_rect = sp->sub.rects[i];
sub_rect->x = av_clip(sub_rect->x, 0, sp->width );
sub_rect->y = av_clip(sub_rect->y, 0, sp->height);
sub_rect->w = av_clip(sub_rect->w, 0, sp->width - sub_rect->x);
sub_rect->h = av_clip(sub_rect->h, 0, sp->height - sub_rect->y);
is->sub_convert_ctx = sws_getCachedContext(is->sub_convert_ctx,
sub_rect->w, sub_rect->h, AV_PIX_FMT_PAL8,
sub_rect->w, sub_rect->h, AV_PIX_FMT_BGRA,
0, NULL, NULL, NULL);
if (!is->sub_convert_ctx) {
av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");
return;
}
if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)pixels, pitch)) {
sws_scale(is->sub_convert_ctx, (const uint8_t * const *)sub_rect->data, sub_rect->linesize,
0, sub_rect->h, pixels, pitch);
SDL_UnlockTexture(is->sub_texture);
}
}
sp->uploaded = 1;
}
} else
sp = NULL;
}
}
// ... 省略部分代碼
if (sp) { // 渲染字幕流
SDL_RenderCopy(renderer, is->sub_texture, NULL, &rect);
}
以上是ffplay中對字幕處理的兩個主要邏輯:解碼、渲染。
還有一部分關於字幕原始幀的丟幀邏輯,位於video_refresh
函數中,通常會在seek之后觸發,有興趣的讀者可以自行研究。
從上面的介紹來看,ffplay並不支持文本字幕的顯示,真正要顯示文本的話需要借助於filter,比如subtitles或ass。用法如下:
./ffplay zuimei.mp4 -vf "subtitles=zuimei.lrc" -x 800 -y 600
由此可見,此處filter基本上實現了文本到圖像的轉換。
3 后續本系列內容提要
- 淺析LRC歌詞文件格式,其中包含歌詞播放邏輯
- SRT字幕流格式
- ASS字幕格式
- FFmpeg中的字幕demuxer的實現
- libass庫用法
- webvtt字幕格式
4 總結
本文主要是對目前常見的字幕格式做了簡單總結,並基於ffplay的代碼介紹了其字幕渲染的主要邏輯,僅供參考。
有任何錯誤或遺漏的地方,歡迎提出。