[时间: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的代码介绍了其字幕渲染的主要逻辑,仅供参考。
有任何错误或遗漏的地方,欢迎提出。