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, "\xef\xbb\xbf", 3)) { // 跳过UTF-8 BOM头
offset += 3;
}
while(p->buf[offset] == '\n' || p->buf[offset] == '\r') {
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 = '\0';
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等)这样实现主要考虑是出于基于文本的字幕通常占用内存较少。
参考资料
FFmpeg中subtitle demuxer实现的更多相关文章
- FFmpeg中HLS文件解析源码
不少人都在找FFmpeg中是否有hls(m3u8)解析的源码,其实是有的.就是ffmpeg/libavformat/hlsproto.c,它依赖的文件也在那个目录中. 如果要是单纯想解析HLS的话,建 ...
- ffmpeg中av_log的实现分析
[时间:2017-10] [状态:Open] [关键词:ffmpeg,avutil,av_log, 日志输出] 0 引言 FFmpeg的libavutil中的日志输出的接口整体比较少,但是功能还是不错 ...
- FFmpeg 中AVPacket的使用
AVPacket保存的是解码前的数据,也就是压缩后的数据.该结构本身不直接包含数据,其有一个指向数据域的指针,FFmpeg中很多的数据结构都使用这种方法来管理数据. AVPacket的使用通常离不开下 ...
- ffmpeg中的sws_scale算法性能测试
经常用到ffmpeg中的sws_scale来进行图像缩放和格式转换,该函数可以使用各种不同算法来对图像进行处理.以前一直很懒,懒得测试和甄 别应该使用哪种算法,最近的工作时间,很多时候需要等待别人.忙 ...
- ffmpeg 中添加264支持
转自:http://blog.sina.com.cn/s/blog_513f4e8401011yuq.html ffmpeg 中带有264的解码,没有编码,需要添加x264: 参考百度上的“windo ...
- 零基础学习视频解码之FFMpeg中比较重要的函数以及数据结构
http://www.cnblogs.com/tanlon/p/3879081.html 在正式开始解码练习前先了解下关于FFmpeg中比较重要的函数以及数据结构. 1. 数据结构: (1) AVF ...
- iOS平台在ffmpeg中使用librtmp
转载请注明出处:http://www.cnblogs.com/fpzeng/p/3202344.html 系统版本:OS X 10.8 一.在iOS平台上交叉编译librtmp librtmp lin ...
- 【转】ffmpeg中的sws_scale算法性能测试
经常用到ffmpeg中的sws_scale来进行图像缩放和格式转换,该函数可以使用各种不同算法来对图像进行处理.以前一直很懒,懒得测试和甄别应该使用哪种算法,最近的工作时间,很多时候需要等待别人.忙里 ...
- FFMPEG中最关键的结构体之间的关系
FFMPEG中结构体很多.最关键的结构体可以分成以下几类: a) 解协议(http,rtsp,rtmp,mms) AVIOContext,URLProtocol,URLContext主要 ...
随机推荐
- angular笔记_6
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...
- YOLO系列:YOLO v1深度解析
声明一点,我是工程应用人员,此文章仅适合算法应用工程师. 1.首先 先看一下YOLO的整体结构: 2.其次 看一下YOLO的工作过程: (1) 将原图划分为SxS的网格.如果一个目标的中心落入某个格子 ...
- H5图片压缩上传
1.所用到技术 HTML5 API:filereader.canvas 以及 formdata 目前来说,HTML5的各种新API都在移动端的webkit上得到了较好的实现.本次使用到的FileRea ...
- Manacher学习笔记
目录 code(伪) Manacher算法 可在 \(O(n)\)的时间内求出一个字符串以每个位置为中心的最长回文子串. 原理:根据之前预处理出的回文串长度求得新的回文串长度 我们可以通过在字符中加上 ...
- HNOI 2017
题目链接 我还是按bzoj AC数量排序做的 4827 这个其实如果推一下(求每个值)式子会发现是个卷积,然后FFT就好了 4826 记不太清了,可以求出每个点左右第一个比他的的点的位置,将点对看成平 ...
- 潭州课堂25班:Ph201805201 django 项目 第九课 图片验证码前台实现,判断用户是否注册功能实现 (课堂笔记)
u胎代码实现 : 针对每一个 app 写个 js 脚本, 先给 users 的 app 应用创建个 js:在指定目录下的 js 文件夹下,创建 users 文件夹,下创建 suth.js ,图片验证 ...
- HDU5518 : John's Fences
求出平面图的对偶图,那么需要选择一些环,使得这些环可以异或出所有环. 对于两个不同的区域,需要用一个代价最小的环把它们区分开,这对应最小割. 那么求出对偶图的最小割树,所有树边之和就是把所有区域都区分 ...
- BZOJ4422 : [Cerc2015]Cow Confinement
从右往左扫描线,用线段树维护扫描线上每一个点能达到的花的数量,并支持最近篱笆的查询. 对于一朵花,找到它上方最近的篱笆,那么它对这中间的每头牛的贡献都是$1$. 当扫到一个篱笆的右边界时,这中间的答案 ...
- PAT基础6-10
6-10 阶乘计算升级版 (20 分) 本题要求实现一个打印非负整数阶乘的函数. 函数接口定义: void Print_Factorial ( const int N ); 其中N是用户传入的参数,其 ...
- 获取SQL server 中的表和说明
SELECT 表名 = case when a.colorder = 1 then d.name else '' end, 表说明 = case w ...