一、FLV格式介绍:
是Adobe发布的一种可以用于直播也可以用于点播的封装格式,以FLVTAG的形式存在,每一个TAG都是独立存在的,我们先用FlvAnalyzer.exe打开一个flv视频看看。
从上图可以看到FLV包括文件头(File Header)和文件体(File Body)两部分,其中文件体由一系列的Tag组成。
1. 文件头
Header 部分记录了FLV的类型、版本等信息,是FLV的开头。一般差不多占9bytes。具体格式如下:
a. 文件标识(3B):总是为”FLV”, 0x46 0x4c 0x56
b. 版本(1B):目前为0x01
c. 流信息(1B):文件的标志位说明。前5位保留,必须为0;第6位为音频Tag:1表示有音频;第七位保留,为0; 第8位为视频Tag:1表示有视频
d. Header长度(4B):整个Header的长度,一般为9(版本为0x01时);大于9表示下面还有扩展信息。即0x00000009。
2. 文件体
文件体由一系列的Tag组成。其中,每个Tag前面还包含了Previous Tag Size字段,表示前面一个Tag的大小,其中第一个Tag的Previous Tag Size为0。Tag的类型可以是视频、音频和Script,每个Tag只能包含以上三种类型的数据中的一种。
3. Tag
每个Tag由也是由两部分组成的:Tag Header和Tag Data。Tag Header里存放的是当前Tag的类型、数据区(Tag Data)长度等信息,具体如下:
Tag类型(1):0x08:音频; 0x09:视频; 0x12:脚本; 其他:保留
数据区长度(3):数据区的长度
时间戳(3):整数,单位是毫秒。对于脚本型的tag总是0 (CTS)
时间戳扩展(1):将时间戳扩展为4bytes,代表高8位。很少用到
StreamsID(3):总是0
数据区(由数据区长度决定):数据实体
下面是三个Tag类型说明:
Audio Tag Data结构(音频类型) :音频Tag Data区域开始的第一个字节包含了音频数据的参数信息,从第二个字节开始为音频流数据。
video Tag Data结构(视频类型):视频Tag Data开始的第一个字节包含视频数据的参数信息,从第二个字节开始为视频流数据。
Script Tag Data结构(脚本类型、帧类型):该类型Tag又被称为MetaData Tag,存放一些关于FLV视频和音频的元信息,比如:duration、width、height等。通常该类型Tag会作为FLV文件的第一个tag,并且只有一个,跟在File Header后。
二、FLV格式使用
文章开头我们有提到 flv可以用于直播也可以用于点播的封装格式。之前的H264编码有讲到I帧(关键帧),P帧,B帧。关键帧可以独立解码出一张图片并且解码P,B也是需要I的。所以在点播,直播/点播的时候用户是从任意时间看的,而不是从文件头开始。这样当用户定位到某个时间的时候就需要寻找最近的I帧,从这里开始解码播放。所以我们在实际应用中,特别是现在直播的应用中,我们往往需要向FLV格式中写入关键帧索引,并将这些索引文件写在Metadata 中。
1. 使用FFmpeg生成带关键索引信息的FLV
ffmpeg -i U1.mp4 -c copy -f flv -flvflags add_keyframe_index U1.flv
2.使用ffprobe -v trace -i U1.flv 查看信息:
[NULL @ 0x556298fe7dc0] Opening 'U1.flv' for reading
[file @ 0x556298fe8940] Setting default whitelist 'file,crypto,data'
Probing flv score:100 size:2048
[flv @ 0x556298fe7dc0] Format flv probed with size=2048 and score=100
[flv @ 0x556298fe7dc0] Before avformat_find_stream_info() pos: 13 bytes read:32768 seeks:0 nb_streams:0
[flv @ 0x556298fe7dc0] type:18, size:1904, last:-1, dts:0 pos:21
[flv @ 0x556298fe7dc0] keyframe stream hasn't been created
[flv @ 0x556298fe7dc0] type:9, size:45, last:-1, dts:0 pos:1940
[flv @ 0x556298fe7dc0] keyframe filepositions = 2016 times = 0
[flv @ 0x556298fe7dc0] keyframe filepositions = 411562 times = 6000
[flv @ 0x556298fe7dc0] keyframe filepositions = 711285 times = 12000
[flv @ 0x556298fe7dc0] keyframe filepositions = 921593 times = 18000
[flv @ 0x556298fe7dc0] keyframe filepositions = 1126035 times = 24000
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
Duration: 00:07:01.96, start: 0.000000, bitrate: 507 kb/s
Stream #0:0, 41, 1/1000: Video: h264 (Main), 1 reference frame, yuv420p(progressive, left), 576x432 [SAR 1:1 DAR 4:3], 0/1, 468 kb/s, 25 fps, 25 tbr, 1k tbn, 50 tbc
Stream #0:1, 35, 1/1000: Audio: aac (HE-AAC), 44100 Hz, stereo, fltp, 32 kb/s
[h264 @ 0x556299016cc0] nal_unit_type: 7(SPS), nal_ref_idc: 3
[h264 @ 0x556299016cc0] nal_unit_type: 8(PPS), nal_ref_idc: 3
[AVIOContext @ 0x556298ff0d40] Statistics: 163840 bytes read, 0 seeks
crl6@crl6-VirtualBox:~/work/AudioVideo$
三、ffmpeg源码分析
static int flv_read_header(AVFormatContext *s)
{
int flags;
FLVContext *flv = s->priv_data;
int offset;
int pre_tag_size = 0;
/* Actual FLV data at 0xe40000 in KUX file */
if(!strcmp(s->iformat->name, "kux"))
avio_skip(s->pb, 0xe40000);
//跳过FLV和版本号
avio_skip(s->pb, 4);
//读取一个字节,得到Flags
flags = avio_r8(s->pb);
//检查音频流或视频流
flv->missing_streams = flags & (FLV_HEADER_FLAG_HASVIDEO | FLV_HEADER_FLAG_HASAUDIO);
s->ctx_flags |= AVFMTCTX_NOHEADER;
//读取4个字节,也就是HeaderSize
offset = avio_rb32(s->pb);
//偏移4个字节
avio_seek(s->pb, offset, SEEK_SET);
/* Annex E. The FLV File Format
* E.3 TheFLVFileBody
* Field Type Comment
* PreviousTagSize0 UI32 Always 0
* */
//读取4个字节,也就是我们的body的第一个 previous tag size
pre_tag_size = avio_rb32(s->pb);
if (pre_tag_size) {
av_log(s, AV_LOG_WARNING, "Read FLV header error, input file is not a standard flv format, first PreviousTagSize0 always is 0\n");
}
s->start_time = 0;
flv->sum_flv_tag_size = 0;
flv->last_keyframe_stream_index = -1;
return 0;
}
static int flv_read_packet(AVFormatContext *s, AVPacket *pkt)
{
FLVContext *flv = s->priv_data;
int ret, i, size, flags;
enum FlvTagType type;
int stream_type=-1;
int64_t next, pos, meta_pos;
int64_t dts, pts = AV_NOPTS_VALUE;
int av_uninit(channels);
int av_uninit(sample_rate);
AVStream *st = NULL;
int last = -1;
int orig_size;
retry:
/* pkt size is repeated at end. skip it */
//得到目前所在的位置
pos = avio_tell(s->pb);
//读取一个字节,也就是我们Tag Header中的Type
//每个NALU的第一个byte & 0x1f就可以得出它的类型
type = (avio_r8(s->pb) & 0x1F);
//读取3个字节,得到Tag Data的大小
orig_size =
size = avio_rb24(s->pb);
//得到总的Tag的大小
flv->sum_flv_tag_size += size + 11;
//读取3个字节,得到Timestamp
dts = avio_rb24(s->pb);
//再读取一个字节,得到Timestamp_ex,然后拼接为dts
dts |= (unsigned)avio_r8(s->pb) << 24;
av_log(s, AV_LOG_TRACE, "type:%d, size:%d, last:%d, dts:%"PRId64" pos:%"PRId64"\n", type, size, last, dts, avio_tell(s->pb));
if (avio_feof(s->pb))
return AVERROR_EOF;
//跳过三个字节,也就是StreamID
avio_skip(s->pb, 3); /* stream id, always 0 */
flags = 0;
if (flv->validate_next < flv->validate_count) {
int64_t validate_pos = flv->validate_index[flv->validate_next].pos;
if (pos == validate_pos) {
if (FFABS(dts - flv->validate_index[flv->validate_next].dts) <=
VALIDATE_INDEX_TS_THRESH) {
flv->validate_next++;
} else {
clear_index_entries(s, validate_pos);
flv->validate_count = 0;
}
} else if (pos > validate_pos) {
clear_index_entries(s, validate_pos);
flv->validate_count = 0;
}
}
//如果tag data的size为0,就跳到leave
if (size == 0) {
ret = FFERROR_REDO;
goto leave;
}
next = size + avio_tell(s->pb);
//如果是audio
if (type == FLV_TAG_TYPE_AUDIO) {
stream_type = FLV_STREAM_TYPE_AUDIO;
//读取一个字节,得到音频信息
flags = avio_r8(s->pb);
size--;
} else if (type == FLV_TAG_TYPE_VIDEO) {
stream_type = FLV_STREAM_TYPE_VIDEO;
//得到视频信息
flags = avio_r8(s->pb);
size--;
//如果是video info/command frame
if ((flags & FLV_VIDEO_FRAMETYPE_MASK) == FLV_FRAME_VIDEO_INFO_CMD)
goto skip;
} else if (type == FLV_TAG_TYPE_META) { //如果是元数据
stream_type=FLV_STREAM_TYPE_DATA;
if (size > 13 + 1 + 4) { // Header-type metadata stuff
int type;
meta_pos = avio_tell(s->pb);
//读取metabody 1
type = flv_read_metabody(s, next);
if (type == 0 && dts == 0 || type < 0 || type == TYPE_UNKNOWN) {
if (type < 0 && flv->validate_count &&
flv->validate_index[0].pos > next &&
flv->validate_index[0].pos - 4 < next
) {
av_log(s, AV_LOG_WARNING, "Adjusting next position due to index mismatch\n");
next = flv->validate_index[0].pos - 4;
}
goto skip;
} else if (type == TYPE_ONTEXTDATA) {
avpriv_request_sample(s, "OnTextData packet");
//2
return flv_data_packet(s, pkt, dts, next);
} else if (type == TYPE_ONCAPTION) {
return flv_data_packet(s, pkt, dts, next);
}
avio_seek(s->pb, meta_pos, SEEK_SET);
}
} else {
av_log(s, AV_LOG_DEBUG,
"Skipping flv packet: type %d, size %d, flags %d.\n",
type, size, flags);
skip:
if (avio_seek(s->pb, next, SEEK_SET) != next) {
// This can happen if flv_read_metabody above read past
// next, on a non-seekable input, and the preceding data has
// been flushed out from the IO buffer.
av_log(s, AV_LOG_ERROR, "Unable to seek to the next packet\n");
return AVERROR_INVALIDDATA;
}
ret = FFERROR_REDO;
goto leave;
}
/* skip empty data packets */
if (!size) {
ret = FFERROR_REDO;
goto leave;
}
/* now find stream */
//找到相应的codec
for (i = 0; i < s->nb_streams; i++) {
st = s->streams[i];
if (stream_type == FLV_STREAM_TYPE_AUDIO) {
if (st->codecpar->codec_type == AVMEDIA_TYPE_AUDIO &&
(s->audio_codec_id || flv_same_audio_codec(st->codecpar, flags)))
break;
} else if (stream_type == FLV_STREAM_TYPE_VIDEO) {
if (st->codecpar->codec_type == AVMEDIA_TYPE_VIDEO &&
(s->video_codec_id || flv_same_video_codec(st->codecpar, flags)))
break;
} else if (stream_type == FLV_STREAM_TYPE_DATA) {
if (st->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE)
break;
}
}
if (i == s->nb_streams) {
static const enum AVMediaType stream_types[] = {AVMEDIA_TYPE_VIDEO, AVMEDIA_TYPE_AUDIO, AVMEDIA_TYPE_SUBTITLE};
//如果没有就创建
st = create_stream(s, stream_types[stream_type]);
if (!st)
return AVERROR(ENOMEM);
}
av_log(s, AV_LOG_TRACE, "%d %X %d \n", stream_type, flags, st->discard);
if ((s->pb->seekable & AVIO_SEEKABLE_NORMAL) &&
((flags & FLV_VIDEO_FRAMETYPE_MASK) == FLV_FRAME_KEY ||
stream_type == FLV_STREAM_TYPE_AUDIO))
av_add_index_entry(st, pos, dts, size, 0, AVINDEX_KEYFRAME);
if ( (st->discard >= AVDISCARD_NONKEY && !((flags & FLV_VIDEO_FRAMETYPE_MASK) == FLV_FRAME_KEY || (stream_type == FLV_STREAM_TYPE_AUDIO)))
||(st->discard >= AVDISCARD_BIDIR && ((flags & FLV_VIDEO_FRAMETYPE_MASK) == FLV_FRAME_DISP_INTER && (stream_type == FLV_STREAM_TYPE_VIDEO)))
|| st->discard >= AVDISCARD_ALL
) {
avio_seek(s->pb, next, SEEK_SET);
ret = FFERROR_REDO;
goto leave;
}
// if not streamed and no duration from metadata then seek to end to find
// the duration from the timestamps
if ((s->pb->seekable & AVIO_SEEKABLE_NORMAL) &&
(!s->duration || s->duration == AV_NOPTS_VALUE) &&
!flv->searched_for_end) {
int size;
const int64_t pos = avio_tell(s->pb);
// Read the last 4 bytes of the file, this should be the size of the
// previous FLV tag. Use the timestamp of its payload as duration.
int64_t fsize = avio_size(s->pb);
retry_duration:
avio_seek(s->pb, fsize - 4, SEEK_SET);
size = avio_rb32(s->pb);
if (size > 0 && size < fsize) {
// Seek to the start of the last FLV tag at position (fsize - 4 - size)
// but skip the byte indicating the type.
avio_seek(s->pb, fsize - 3 - size, SEEK_SET);
if (size == avio_rb24(s->pb) + 11) {
uint32_t ts = avio_rb24(s->pb);
ts |= avio_r8(s->pb) << 24;
if (ts)
s->duration = ts * (int64_t)AV_TIME_BASE / 1000;
else if (fsize >= 8 && fsize - 8 >= size) {
fsize -= size+4;
goto retry_duration;
}
}
}
avio_seek(s->pb, pos, SEEK_SET);
flv->searched_for_end = 1;
}
//如果是音频流
if (stream_type == FLV_STREAM_TYPE_AUDIO) {
int bits_per_coded_sample;
channels = (flags & FLV_AUDIO_CHANNEL_MASK) == FLV_STEREO ? 2 : 1;
sample_rate = 44100 << ((flags & FLV_AUDIO_SAMPLERATE_MASK) >>
FLV_AUDIO_SAMPLERATE_OFFSET) >> 3;
bits_per_coded_sample = (flags & FLV_AUDIO_SAMPLESIZE_MASK) ? 16 : 8;
if (!st->codecpar->channels || !st->codecpar->sample_rate ||
!st->codecpar->bits_per_coded_sample) {
st->codecpar->channels = channels;
st->codecpar->channel_layout = channels == 1
? AV_CH_LAYOUT_MONO
: AV_CH_LAYOUT_STEREO;
st->codecpar->sample_rate = sample_rate;
st->codecpar->bits_per_coded_sample = bits_per_coded_sample;
}
if (!st->codecpar->codec_id) {
//设置音频编解码
flv_set_audio_codec(s, st, st->codecpar,
flags & FLV_AUDIO_CODECID_MASK);
flv->last_sample_rate =
sample_rate = st->codecpar->sample_rate;
flv->last_channels =
channels = st->codecpar->channels;
} else {
AVCodecParameters *par = avcodec_parameters_alloc();
if (!par) {
ret = AVERROR(ENOMEM);
goto leave;
}
par->sample_rate = sample_rate;
par->bits_per_coded_sample = bits_per_coded_sample;
flv_set_audio_codec(s, st, par, flags & FLV_AUDIO_CODECID_MASK);
sample_rate = par->sample_rate;
avcodec_parameters_free(&par);
}
} else if (stream_type == FLV_STREAM_TYPE_VIDEO) { //如果是视频流
//设置视频编解码
int ret = flv_set_video_codec(s, st, flags & FLV_VIDEO_CODECID_MASK, 1);
if (ret < 0)
return ret;
size -= ret;
} else if (stream_type == FLV_STREAM_TYPE_DATA) { //如果是Script流
st->codecpar->codec_id = AV_CODEC_ID_TEXT;
}
//如果是这几个格式
if (st->codecpar->codec_id == AV_CODEC_ID_AAC ||
st->codecpar->codec_id == AV_CODEC_ID_H264 ||
st->codecpar->codec_id == AV_CODEC_ID_MPEG4) {
//读取1个字节,得到包类型,可以参考h264的结构来看
int type = avio_r8(s->pb);
size--;
if (size < 0) {
ret = AVERROR_INVALIDDATA;
goto leave;
}
if (st->codecpar->codec_id == AV_CODEC_ID_H264 || st->codecpar->codec_id == AV_CODEC_ID_MPEG4) {
// sign extension
//如果3个字节,得到cts
int32_t cts = (avio_rb24(s->pb) + 0xff800000) ^ 0xff800000;
//通过cts得到pts
pts = dts + cts;
if (cts < 0) { // dts might be wrong
if (!flv->wrong_dts)
av_log(s, AV_LOG_WARNING,
"Negative cts, previous timestamps might be wrong.\n");
flv->wrong_dts = 1;
} else if (FFABS(dts - pts) > 1000*60*15) {
av_log(s, AV_LOG_WARNING,
"invalid timestamps %"PRId64" %"PRId64"\n", dts, pts);
dts = pts = AV_NOPTS_VALUE;
}
}
if (type == 0 && (!st->codecpar->extradata || st->codecpar->codec_id == AV_CODEC_ID_AAC ||
st->codecpar->codec_id == AV_CODEC_ID_H264)) {
AVDictionaryEntry *t;
if (st->codecpar->extradata) {
if ((ret = flv_queue_extradata(flv, s->pb, stream_type, size)) < 0)
return ret;
ret = FFERROR_REDO;
goto leave;
}
if ((ret = flv_get_extradata(s, st, size)) < 0)
return ret;
/* Workaround for buggy Omnia A/XE encoder */
t = av_dict_get(s->metadata, "Encoder", NULL, 0);
if (st->codecpar->codec_id == AV_CODEC_ID_AAC && t && !strcmp(t->value, "Omnia A/XE"))
st->codecpar->extradata_size = 2;
if (st->codecpar->codec_id == AV_CODEC_ID_AAC && 0) {
MPEG4AudioConfig cfg;
if (avpriv_mpeg4audio_get_config(&cfg, st->codecpar->extradata,
st->codecpar->extradata_size * 8, 1) >= 0) {
st->codecpar->channels = cfg.channels;
st->codecpar->channel_layout = 0;
if (cfg.ext_sample_rate)
st->codecpar->sample_rate = cfg.ext_sample_rate;
else
st->codecpar->sample_rate = cfg.sample_rate;
av_log(s, AV_LOG_TRACE, "mp4a config channels %d sample rate %d\n",
st->codecpar->channels, st->codecpar->sample_rate);
}
}
ret = FFERROR_REDO;
goto leave;
}
}
/* skip empty data packets */
if (!size) {
ret = FFERROR_REDO;
goto leave;
}
// 获得AVPacket并初始化
ret = av_get_packet(s->pb, pkt, size);
if (ret < 0)
return ret;
pkt->dts = dts;
pkt->pts = pts == AV_NOPTS_VALUE ? dts : pts;
pkt->stream_index = st->index;
pkt->pos = pos;
if (flv->new_extradata[stream_type]) {
uint8_t *side = av_packet_new_side_data(pkt, AV_PKT_DATA_NEW_EXTRADATA,
flv->new_extradata_size[stream_type]);
if (side) {
memcpy(side, flv->new_extradata[stream_type],
flv->new_extradata_size[stream_type]);
av_freep(&flv->new_extradata[stream_type]);
flv->new_extradata_size[stream_type] = 0;
}
}
if (stream_type == FLV_STREAM_TYPE_AUDIO &&
(sample_rate != flv->last_sample_rate ||
channels != flv->last_channels)) {
flv->last_sample_rate = sample_rate;
flv->last_channels = channels;
ff_add_param_change(pkt, channels, 0, sample_rate, 0, 0);
}
if ( stream_type == FLV_STREAM_TYPE_AUDIO ||
((flags & FLV_VIDEO_FRAMETYPE_MASK) == FLV_FRAME_KEY) ||
stream_type == FLV_STREAM_TYPE_DATA)
pkt->flags |= AV_PKT_FLAG_KEY;
leave:
last = avio_rb32(s->pb);
if (last != orig_size + 11 && last != orig_size + 10 &&
!avio_feof(s->pb) &&
(last != orig_size || !last) && last != flv->sum_flv_tag_size &&
!flv->broken_sizes) {
av_log(s, AV_LOG_ERROR, "Packet mismatch %d %d %d\n", last, orig_size + 11, flv->sum_flv_tag_size);
avio_seek(s->pb, pos + 1, SEEK_SET);
ret = resync(s);
av_packet_unref(pkt);
if (ret >= 0) {
goto retry;
}
}
return ret;
}
static int flv_read_metabody(AVFormatContext *s, int64_t next_pos)
{
FLVContext *flv = s->priv_data;
AMFDataType type;
AVStream *stream, *astream, *vstream;
AVStream av_unused *dstream;
AVIOContext *ioc;
int i;
// only needs to hold the string "onMetaData".
// Anything longer is something we don't want.
char buffer[32];
astream = NULL;
vstream = NULL;
dstream = NULL;
ioc = s->pb;
// first object needs to be "onMetaData" string
//读取8个字节,也就是AMF包
type = avio_r8(ioc);
if (type != AMF_DATA_TYPE_STRING ||
amf_get_string(ioc, buffer, sizeof(buffer)) < 0)
return TYPE_UNKNOWN;
if (!strcmp(buffer, "onTextData"))
return TYPE_ONTEXTDATA;
if (!strcmp(buffer, "onCaption"))
return TYPE_ONCAPTION;
if (!strcmp(buffer, "onCaptionInfo"))
return TYPE_ONCAPTIONINFO;
if (strcmp(buffer, "onMetaData") && strcmp(buffer, "onCuePoint")) {
av_log(s, AV_LOG_DEBUG, "Unknown type %s\n", buffer);
return TYPE_UNKNOWN;
}
// find the streams now so that amf_parse_object doesn't need to do
// the lookup every time it is called.
for (i = 0; i < s->nb_streams; i++) {
stream = s->streams[i];
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
vstream = stream;
flv->last_keyframe_stream_index = i;
} else if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
astream = stream;
if (flv->last_keyframe_stream_index == -1)
flv->last_keyframe_stream_index = i;
}
else if (stream->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE)
dstream = stream;
}
// parse the second object (we want a mixed array)
if (amf_parse_object(s, astream, vstream, buffer, next_pos, 0) < 0)
return -1;
return 0;
}
static int flv_data_packet(AVFormatContext *s, AVPacket *pkt,
int64_t dts, int64_t next)
{
AVIOContext *pb = s->pb;
AVStream *st = NULL;
char buf[20];
int ret = AVERROR_INVALIDDATA;
int i, length = -1;
int array = 0;
//读取1个字节,得到AMF包的类型
switch (avio_r8(pb)) {
case AMF_DATA_TYPE_ARRAY:
array = 1;
case AMF_DATA_TYPE_MIXEDARRAY:
avio_seek(pb, 4, SEEK_CUR);
case AMF_DATA_TYPE_OBJECT:
break;
default:
goto skip;
}
//读取AMF包中的数组
while (array || (ret = amf_get_string(pb, buf, sizeof(buf))) > 0) {
//得到key
AMFDataType type = avio_r8(pb);
if (type == AMF_DATA_TYPE_STRING && (array || !strcmp(buf, "text"))) {
//得到数组长度
length = avio_rb16(pb);
//获得数组内容
ret = av_get_packet(pb, pkt, length);
if (ret < 0)
goto skip;
else
break;
} else {
if ((ret = amf_skip_tag(pb, type)) < 0)
goto skip;
}
}
if (length < 0) {
ret = AVERROR_INVALIDDATA;
goto skip;
}
for (i = 0; i < s->nb_streams; i++) {
st = s->streams[i];
if (st->codecpar->codec_type == AVMEDIA_TYPE_SUBTITLE)
break;
}
if (i == s->nb_streams) {
st = create_stream(s, AVMEDIA_TYPE_SUBTITLE);
if (!st)
return AVERROR(ENOMEM);
st->codecpar->codec_id = AV_CODEC_ID_TEXT;
}
pkt->dts = dts;
pkt->pts = dts;
pkt->size = ret;
pkt->stream_index = st->index;
pkt->flags |= AV_PKT_FLAG_KEY;
skip:
avio_seek(s->pb, next + 4, SEEK_SET);
return ret;
}