zoukankan      html  css  js  c++  java
  • 浅析字幕流

    [时间: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
    ");
                            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,比如subtitlesass。用法如下:

    ./ffplay zuimei.mp4 -vf "subtitles=zuimei.lrc" -x 800 -y 600

    由此可见,此处filter基本上实现了文本到图像的转换。

    3 后续本系列内容提要

    • 浅析LRC歌词文件格式,其中包含歌词播放逻辑
    • SRT字幕流格式
    • ASS字幕格式
    • FFmpeg中的字幕demuxer的实现
    • libass库用法
    • webvtt字幕格式

    4 总结

    本文主要是对目前常见的字幕格式做了简单总结,并基于ffplay的代码介绍了其字幕渲染的主要逻辑,仅供参考。

    有任何错误或遗漏的地方,欢迎提出。

    4.1 参考资料

    1. 字幕格式-wiki
    2. subtitle-wiki
    3. 字幕基础:字幕介绍、字幕种类及常见格式
    4. subtile-formats
    5. ffmpeg-doc
  • 相关阅读:
    [leetcode ]429. N-ary Tree Level Order Traversale (easy)
    [leetcode] 559. Maximum Depth of N-ary Tree (easy)
    [leetcode] 406. Queue Reconstruction by Height (medium)
    [leetcode] 238. Product of Array Except Self (medium)
    [leetcode] 94. Binary Tree Inorder Traversal
    [leetcode] 621. Task Scheduler(medium)
    [leetcode] 309. Best Time to Buy and Sell Stock with Cooldown(medium)
    为窗口设置图标
    关闭住主窗口
    窗口居中显示
  • 原文地址:https://www.cnblogs.com/tocy/p/subtitle-stream-overview.html
Copyright © 2011-2022 走看看