zoukankan      html  css  js  c++  java
  • FFmpeg中subtitle demuxer实现

    [时间:2019-01] [状态:Open]
    [关键词:字幕,ffmpeg,subtitle,demuxer,源码]

    0 引言

    本文重心在于FFmpeg中subtitle demuxer的实现逻辑。
    在阅读本文前,笔者希望你对FFmpeg中libavformat的实现有一定了解(可以参考我之前的博文FFmpeg框架分析,最起码知道demuxer的主要接口)。
    同时笔者也希望你对主流的字幕格式有一定了解,包括LRC、SRT、ASS、WebVTT。

    这是我的“浅析字幕流”系列第四篇文章,其他文章链接如下:

    1 LRC demuxer的实现

    本文将以其中一种字幕格式——LRC的实现作为示例,说明FFmpeg内部对subtitle的解析逻辑,其他字幕实现逻辑差不多吧。有兴趣的可以自行查看代码。
    对应的demuxer实现文件是libavformat/lrcdec.c
    我们首先看看lrc_demuxer的定义部分:

    AVInputFormat ff_lrc_demuxer = {
        .name           = "lrc",
        .long_name      = NULL_IF_CONFIG_SMALL("LRC lyrics"),
        .priv_data_size = sizeof (LRCContext),
        .read_probe     = lrc_probe,
        .read_header    = lrc_read_header,
        .read_packet    = lrc_read_packet,
        .read_close     = lrc_read_close,
        .read_seek2     = lrc_read_seek
    };
    

    不过从实际代码逻辑来看,有两个比较大的函数(probe和read_header),其他的都很简单。先看一下LRC内部的上下文数据结构的定义:

    typedef struct LRCContext {
        FFDemuxSubtitlesQueue q;
        int64_t ts_offset; // offset metadata item
    } LRCContext;
    

    只有一个添加的结构FFDemuxSubtitlesQueue,基本上空的。
    我们再看一下后面三个函数的实现:

    static int lrc_read_packet(AVFormatContext *s, AVPacket *pkt)
    {
        LRCContext *lrc = s->priv_data;
        return ff_subtitles_queue_read_packet(&lrc->q, pkt);
    }
    
    static int lrc_read_seek(AVFormatContext *s, int stream_index,
                             int64_t min_ts, int64_t ts, int64_t max_ts, int flags)
    {
        LRCContext *lrc = s->priv_data;
        return ff_subtitles_queue_seek(&lrc->q, s, stream_index,
                                       min_ts, ts, max_ts, flags);
    }
    
    static int lrc_read_close(AVFormatContext *s)
    {
        LRCContext *lrc = s->priv_data;
        ff_subtitles_queue_clean(&lrc->q);
        return 0;
    }
    

    基本上都是一行代码,直接将实现逻辑转接到ff_subtitles_*函数上。(关于此系列函数第二节将会详细介绍)

    我们这里要介绍的第一个函数是probe,代码如下:

    static int lrc_probe(AVProbeData *p)
    {
        int64_t offset = 0;
        int64_t mm;
        uint64_t ss, cs;
        const AVMetadataConv *metadata_item;
    
        if(!memcmp(p->buf, "xefxbbxbf", 3)) { // 跳过UTF-8 BOM头
            offset += 3;
        }
        while(p->buf[offset] == '
    ' || p->buf[offset] == '
    ') {
            offset++;
        }
        if(p->buf[offset] != '[') {// 第一个字符必须是'['
            return 0;
        }
        offset++;
        // 不在ff_lrc_metadata_conv中的特定字段
        if(!memcmp(p->buf + offset, "offset:", 7)) {
            return 40;
        }
    	// [mm:ss.xx] 这是LRC中的时间格式
        if(sscanf(p->buf + offset, "%"SCNd64":%"SCNu64".%"SCNu64"]",
                  &mm, &ss, &cs) == 3) {
            return 50;
        }
        /*
    	const AVMetadataConv ff_lrc_metadata_conv[] = {
    	    {"ti", "title"}, {"al", "album"},
    	    {"ar", "artist"}, {"au", "author"},
    	    {"by", "creator"}, {"re", "encoder"},
    	    {"ve", "encoder_version"}, {0, 0}
    	};
    	*/
        for(metadata_item = ff_lrc_metadata_conv;
            metadata_item->native; metadata_item++) {
            size_t metadata_item_len = strlen(metadata_item->native);
            if(p->buf[offset + metadata_item_len] == ':' &&
               !memcmp(p->buf + offset, metadata_item->native, metadata_item_len)) {
                return 40;
            }
        }
        return 5; // Give it 5 scores since it starts with a bracket
    }
    

    probe函数基本上是根据LRC格式的特征字段进行格式探测,由于LRC本身没有明确的格式标记,所以这里仅仅是探测,给出置信值,并没有确认。在ASS或WebVTT中是有特定的格式标记的。
    下面是read_header函数的实现代码:

    static int lrc_read_header(AVFormatContext *s)
    {
        LRCContext *lrc = s->priv_data;
        AVBPrint line;
        AVStream *st;
    	// 先创建AVStream并初始化部分信息
        st = avformat_new_stream(s, NULL);
        if(!st) {
            return AVERROR(ENOMEM);
        }
        avpriv_set_pts_info(st, 64, 1, 1000);
        lrc->ts_offset = 0;
        st->codecpar->codec_type = AVMEDIA_TYPE_SUBTITLE;
        st->codecpar->codec_id   = AV_CODEC_ID_TEXT;
        av_bprint_init(&line, 0, AV_BPRINT_SIZE_UNLIMITED);
    
    	// 注意这个循环会把整个LRC文件全部读完
        while(!avio_feof(s->pb)) {
            int64_t pos = read_line(&line, s->pb);// 读一行数据
            int64_t header_offset = find_header(line.str); // 查找是否是ID标签
            if(header_offset >= 0) { // ID标签解析,格式为 ID:msg
                char *comma_offset = strchr(line.str, ':');
                if(comma_offset) {
                    char *right_bracket_offset = strchr(line.str, ']');
                    if(!right_bracket_offset) {
                        continue;
                    }
    
                    *right_bracket_offset = *comma_offset = '';
                    if(strcmp(line.str + 1, "offset") ||
                       sscanf(comma_offset + 1, "%"SCNd64, &lrc->ts_offset) != 1) {
                        av_dict_set(&s->metadata, line.str + 1, comma_offset + 1, 0);
                    }
                    *comma_offset = ':';
                    *right_bracket_offset = ']';
                }
    
            } else { // 时间标签 + 歌词
                AVPacket *sub;
                int64_t ts_start = AV_NOPTS_VALUE;
                int64_t ts_stroffset = 0;
                int64_t ts_stroffset_incr = 0;
                int64_t ts_strlength = count_ts(line.str); // 找到时间标签的起始位置
    			// 读取时间戳,并偏移到给歌词起始位置
                while((ts_stroffset_incr = read_ts(line.str + ts_stroffset,
                                                   &ts_start)) != 0) {
                    ts_stroffset += ts_stroffset_incr;
    				// 将实际歌词信息插入到队列中
                    sub = ff_subtitles_queue_insert(&lrc->q, line.str + ts_strlength,
                                                    line.len - ts_strlength, 0);
                    if(!sub) {
                        return AVERROR(ENOMEM);
                    }
                    sub->pos = pos;
                    sub->pts = ts_start - lrc->ts_offset; // 时间戳在此赋值
                    sub->duration = -1;
                }
            }
        }
    	// subtitle读取完毕,会做一些字幕重排及调整
        ff_subtitles_queue_finalize(s, &lrc->q);
        ff_metadata_conv_ctx(s, NULL, ff_lrc_metadata_conv);
        av_bprint_finalize(&line, NULL);
        return 0;
    }
    

    从上述实现来看,LRC demuxer是在read_header中直接读取了所有歌词信息,并保存到字幕队列中。后续所有处理都通过该队列完成。

    总结一下,在LRC demuxer中调用了以下几个API:

    • ff_subtitles_queue_read_packet
    • ff_subtitles_queue_seek
    • ff_subtitles_queue_clean
    • ff_subtitles_queue_insert
    • ff_subtitles_queue_finalize

    下一小节我们将围绕这几个函数展开。

    2 ff_subtitles_queue_*接口实现

    首先我们看一下FFDemuxSubtitlesQueue的定义

    enum sub_sort {
        SUB_SORT_TS_POS = 0,    ///< 排序顺序为:时间戳,之后是位置
        SUB_SORT_POS_TS,        ///< 排序顺序为:位置,之后是时间戳
    };
    typedef struct {
        AVPacket *subs;         ///< subtitles数据包数组
        int nb_subs;            ///< 已存储数据包个数
        int allocated_size;     ///< 已分配数组长度
        int current_sub_idx;    ///< 目前正在读的数据包的索引
        enum sub_sort sort;     ///< subtitle排序算法
        int keep_duplicates;    ///< set to 1 to keep duplicated subtitle events
    } FFDemuxSubtitlesQueue;
    

    先说明下,通常ffmpeg内部的接口是不加锁的,因为从设计上来说,ff_subtitles_queue_*需要保证在同一个线程内调用,否则可能存在多线程同步的问题。

    2.1 ff_subtitles_queue_read_packet

    读包逻辑相对简答,基本是从队列中读取缓存数据。代码如下:

    int ff_subtitles_queue_read_packet(FFDemuxSubtitlesQueue *q, AVPacket *pkt)
    {
        AVPacket *sub = q->subs + q->current_sub_idx;
    
        if (q->current_sub_idx == q->nb_subs)
            return AVERROR_EOF;
        if (av_packet_ref(pkt, sub) < 0) {
            return AVERROR(ENOMEM);
        }
    
        pkt->dts = pkt->pts;
        q->current_sub_idx++; // 这里更新读取位置索引
        return 0;
    }
    

    2.2 ff_subtitles_queue_seek

    seek逻辑跟read_packet类似,主要是根据时间戳,直接找到seek之后的读取位置即可。代码如下:

    // 二分查找最接近时间ts的索引位置
    static int search_sub_ts(const FFDemuxSubtitlesQueue *q, int64_t ts)
    {
        int s1 = 0, s2 = q->nb_subs - 1;
    
        if (s2 < s1)
            return AVERROR(ERANGE);
    
        for (;;) {
            int mid;
    
            if (s1 == s2)
                return s1;
            if (s1 == s2 - 1)
                return q->subs[s1].pts <= q->subs[s2].pts ? s1 : s2;
            mid = (s1 + s2) / 2;
            if (q->subs[mid].pts <= ts)
                s1 = mid;
            else
                s2 = mid;
        }
    }
    
    int ff_subtitles_queue_seek(FFDemuxSubtitlesQueue *q, AVFormatContext *s, int stream_index,
                                int64_t min_ts, int64_t ts, int64_t max_ts, int flags)
    {
        if (flags & AVSEEK_FLAG_BYTE) {
            return AVERROR(ENOSYS);
        } else if (flags & AVSEEK_FLAG_FRAME) { // 按照帧编号执行seek
            if (ts < 0 || ts >= q->nb_subs)
                return AVERROR(ERANGE);
            q->current_sub_idx = ts;
        } else { // 通常seek都会进入此分支
            int i, idx = search_sub_ts(q, ts);
            int64_t ts_selected;
    
            if (idx < 0)
                return idx;
    		// 继续缩小范围,找到比min_ts大,比max_tx小的位置
            for (i = idx; i < q->nb_subs && q->subs[i].pts < min_ts; i++)
                if (stream_index == -1 || q->subs[i].stream_index == stream_index)
                    idx = i;
            for (i = idx; i > 0 && q->subs[i].pts > max_ts; i--)
                if (stream_index == -1 || q->subs[i].stream_index == stream_index)
                    idx = i;
    
            ts_selected = q->subs[idx].pts;
            if (ts_selected < min_ts || ts_selected > max_ts)
                return AVERROR(ERANGE);
    
            /* 处理在时间上重叠的字幕数据包 */
            for (i = idx - 1; i >= 0; i--) {
                int64_t pts = q->subs[i].pts;
                if (q->subs[i].duration <= 0 ||
                    (stream_index != -1 && q->subs[i].stream_index != stream_index))
                    continue;
                if (pts >= min_ts && pts > ts_selected - q->subs[i].duration)
                    idx = i;
                else
                    break;
            }
    
            q->current_sub_idx = idx;
        }
        return 0;
    }
    

    2.3 ff_subtitles_queue_clean

    clean函数主要完成动态申请内存的释放。具体代码如下:

    void ff_subtitles_queue_clean(FFDemuxSubtitlesQueue *q)
    {
        int i;
    
        for (i = 0; i < q->nb_subs; i++)
            av_packet_unref(&q->subs[i]);
        av_freep(&q->subs);
        q->nb_subs = q->allocated_size = q->current_sub_idx = 0;
    }
    

    2.4 ff_subtitles_queue_finalize

    finalize函数主要是完成字幕数据包的排序和后处理,调用此接口表示所有字幕已经读取完了。代码如下:

    void ff_subtitles_queue_finalize(void *log_ctx, FFDemuxSubtitlesQueue *q)
    {
        int i;
    	// 按照给定策略对队列中的数据包排序
        qsort(q->subs, q->nb_subs, sizeof(*q->subs),
              q->sort == SUB_SORT_TS_POS ? cmp_pkt_sub_ts_pos
                                         : cmp_pkt_sub_pos_ts);
        for (i = 0; i < q->nb_subs; i++)
            if (q->subs[i].duration < 0 && i < q->nb_subs - 1)
                q->subs[i].duration = q->subs[i + 1].pts - q->subs[i].pts;
    
        if (!q->keep_duplicates) // 剔除重复数据包
            drop_dups(log_ctx, q);
    }
    

    2.5 ff_subtitles_queue_insert

    insert函数完成字幕数据的插入,并分配相关内存。代码如下:

    AVPacket *ff_subtitles_queue_insert(FFDemuxSubtitlesQueue *q,
                                        const uint8_t *event, size_t len, int merge)
    {
        AVPacket *subs, *sub;
    
        if (merge && q->nb_subs > 0) {
            /* merge with previous event */
    
            int old_len;
            sub = &q->subs[q->nb_subs - 1];
            old_len = sub->size;
            if (av_grow_packet(sub, len) < 0)
                return NULL;
            memcpy(sub->data + old_len, event, len);
        } else { // 多数基于文本的字幕都会进入这个逻辑分支
            /* new event */
    
            if (q->nb_subs >= INT_MAX/sizeof(*q->subs) - 1)
                return NULL;
    		// 这个函数将保证q->subs中有足够的可用空间,不够的话自动扩展
            subs = av_fast_realloc(q->subs, &q->allocated_size,
                                   (q->nb_subs + 1) * sizeof(*q->subs));
            if (!subs)
                return NULL;
            q->subs = subs;
            sub = &subs[q->nb_subs++];
            if (av_new_packet(sub, len) < 0)
                return NULL;
            sub->flags |= AV_PKT_FLAG_KEY;
            sub->pts = sub->dts = 0;
            memcpy(sub->data, event, len);
        }
        return sub;
    }
    

    3 小结

    至此,我们已经基本上了解了FFmpeg内部对subtitle的解析逻辑,并且本文也以LRC为例做了说明。从整体来看,libavformat中对字幕解析的主要逻辑都集中在ff_subtitles_queue_*一系列API中。

    当然,我们可以在理解这个逻辑的基础上,将subitle的demuxer改成逐帧读取数据,类似其他demuxer的处理逻辑,仅在需要的时候读取数据包,而不是在read_header时全部读完。我认为FFmpeg中字幕相关的demuxer(LRC、ASS、SRT、WebVTT等)这样实现主要考虑是出于基于文本的字幕通常占用内存较少。

    参考资料

    1. libavformat-subtitles.c
    2. libavformat-lrcdec.c
  • 相关阅读:
    二进制安全(学习笔记)
    rsa
    bugku 隐写 笔记
    dvwa随学笔记
    实验吧 密码学 writeup
    实验吧 隐写 writeup
    实验吧 web题weiteup
    Java数据结构和算法 第二版 课后习题第三章
    自动化测试入门指南(3)-- 入门demo
    自动化测试入门指南(2)-- 环境搭建
  • 原文地址:https://www.cnblogs.com/tocy/p/subtitle-format-ffmpeg-demuxer.html
Copyright © 2011-2022 走看看