本篇是 《播放器网络视频数据读取过程详解》 的延续部分,我们回顾一下上一篇。
回顾前面内容
我们详细分析了自定义的协议是如何以静态方式、注册到 IJKPLAYER 协议profile中。
本篇分析打开自定义协议、读取数据流数据并匹配解封装函数,也即是说本篇要把私有协议、私有数据封装
的数据流模式,实现在ijkplayer中播放流程说清楚。
static int init_input(AVFormatContext *s, const char *filename,
AVDictionary **options)
{
if ((ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, ///> 1.2. 此函数是 io_open() 打开文件或网络socket
options)) < 0)
return ret;
if (s->iformat)
return 0;
return av_probe_input_buffer2(s->pb, &s->iformat, filename, ///> 1.3 读数据流的第一帧数据
s, 0, s->format_probesize);
}
在 init_input() 函数中调用了 io_open() 函数,
ret = s->io_open(s, &s->pb, filename, AVIO_FLAG_READ | s->avio_flags, options);
这个函数其根本是执行此函数
static int io_open_default(AVFormatContext *s, AVIOContext **pb,
const char *url, int flags, AVDictionary **options)
在该函数中调用函数
ffio_open_whitelist(pb, url, flags, &s->interrupt_callback, options, s->protocol_whitelist, s->protocol_blacklist);
匹配通讯协议,并根据通讯建立通讯连接。前面文章已经描述此过程,再次我们简单回顾。
第一 解封装器匹配过程
int ffio_open_whitelist(AVIOContext **s, const char *filename, int flags,
const AVIOInterruptCB *int_cb, AVDictionary **options,
const char *whitelist, const char *blacklist
)
{
URLContext *h;
int err;
///> 此函数匹配 filename 中协议类别,创建通讯对象和关联结构体初始化。
err = ffurl_open_whitelist(&h, filename, flags, int_cb, options, whitelist, blacklist, NULL);
if (err < 0)
return err;
err = ffio_fdopen(s, h); ///> 调用 ffio_fdopen 函数,分配AVIOContext的io buffer,
if (err < 0) {
ffurl_close(h);
return err;
}
return 0;
}
int ffio_fdopen(AVIOContext **s, URLContext *h)
{
AVIOInternal *internal = NULL;
uint8_t *buffer = NULL;
int buffer_size, max_packet_size;
av_log(NULL, AV_LOG_INFO, "%s/%s(), LINE:%d \n",__FILE__, __func__, __LINE__);
max_packet_size = h->max_packet_size;
if (max_packet_size) {
buffer_size = max_packet_size; /* no need to bufferize more than one packet */
} else {
buffer_size = IO_BUFFER_SIZE;
}
buffer = av_malloc(buffer_size); ///> 用于预取视频数据的缓存, io buffer大小为32k
if (!buffer)
return AVERROR(ENOMEM);
internal = av_mallocz(sizeof(*internal)); ///> AVIOInternal指针, 指向URLContext
if (!internal)
goto fail;
internal->h = h;
///> 此函数动态设置 read_packet 指针指向的函数为 io_read_packet() 函数。
*s = avio_alloc_context(buffer, buffer_size, h->flags & AVIO_FLAG_WRITE, ///> 分配AVIOContext, 设置read_packet、write_packet、seek函数,
internal, io_read_packet, io_write_packet, io_seek);///> 对应为io_read_packet、io_write_packet、io_see
if (!*s)
goto fail;
av_log(NULL, AV_LOG_INFO, "%s/%s(), LINE:%d \n",__FILE__, __func__, __LINE__);
(*s)->protocol_whitelist = av_strdup(h->protocol_whitelist);
if (!(*s)->protocol_whitelist && h->protocol_whitelist) {
avio_closep(s);
goto fail;
}
(*s)->protocol_blacklist = av_strdup(h->protocol_blacklist);
if (!(*s)->protocol_blacklist && h->protocol_blacklist) {
avio_closep(s);
goto fail;
}
(*s)->direct = h->flags & AVIO_FLAG_DIRECT;
av_log(NULL, AV_LOG_INFO, "%s/%s(), LINE:%d \n",__FILE__, __func__, __LINE__);
(*s)->seekable = h->is_streamed ? 0 : AVIO_SEEKABLE_NORMAL;
(*s)->max_packet_size = max_packet_size;
(*s)->min_packet_size = h->min_packet_size;
if(h->prot) {
(*s)->read_pause = io_read_pause;
(*s)->read_seek = io_read_seek;
if (h->prot->url_read_seek)
(*s)->seekable |= AVIO_SEEKABLE_TIME;
}
(*s)->short_seek_get = io_short_seek;
(*s)->av_class = &ff_avio_class;
return 0;
fail:
av_log(NULL, AV_LOG_INFO, "%s/%s(), LINE:%d \n",__FILE__, __func__, __LINE__);
av_freep(&internal);
av_freep(&buffer);
return AVERROR(ENOMEM);
}
此 open_fdopen() 函数主要工作内容建立 AVIOContext 对象,并把对象关联到播放器对象上,就完成通讯协议
各部分的初始化工作,通讯链接已经建立起来。
接下来我们分析读取数据流第一帧数据 probe data 和数据流解封装的过程,在 init_input() 函数中,执行
io_open() 函数后,就调用下面这个函数。
///> av_probe_input_buffer2(s->pb, &s->iformat, filename,s, 0, s->format_probesize); ///> 1.3 读数据流的第一帧数据
int av_probe_input_buffer2(AVIOContext *pb, AVInputFormat **fmt,
const char *filename, void *logctx,
unsigned int offset, unsigned int max_probe_size)
{
AVProbeData pd = {
filename ? filename : "" };
uint8_t *buf = NULL;
int ret = 0, probe_size, buf_offset = 0;
int score = 0;
int ret2;
if (!max_probe_size)
max_probe_size = PROBE_BUF_MAX;
else if (max_probe_size < PROBE_BUF_MIN) {
av_log(logctx, AV_LOG_ERROR,
"Specified probe size value %u cannot be < %u\n", max_probe_size, PROBE_BUF_MIN);
return AVERROR(EINVAL);
}
if (offset >= max_probe_size)
return AVERROR(EINVAL);
if (pb->av_class) {
///> av_class 指针执行当前 编解码类对象
uint8_t *mime_type_opt = NULL;
char *semi;
av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type_opt); ///> 获取当前av_class对象的mime_type内容
pd.mime_type = (const char *)mime_type_opt;
semi = pd.mime_type ? strchr(pd.mime_type, ';') : NULL;
if (semi) {
*semi = '\0';
}
}
#if 0
if (!*fmt && pb->av_class && av_opt_get(pb, "mime_type", AV_OPT_SEARCH_CHILDREN, &mime_type) >= 0 && mime_type) {
if (!av_strcasecmp(mime_type, "audio/aacp")) {
*fmt = av_find_input_format("aac");
}
av_freep(&mime_type);
}
#endif
///> 此循环体是在 max_probe_size 范围内容,查找可以使用的解封装对象。
for (probe_size = PROBE_BUF_MIN; probe_size <= max_probe_size && !*fmt;
probe_size = FFMIN(probe_size << 1,FFMAX(max_probe_size, probe_size + 1)))
{
score = probe_size < max_probe_size ? AVPROBE_SCORE_RETRY : 0;
/* Read probe data.buf缓存默认大小为2k+32 */
if ((ret = av_reallocp(&buf, probe_size + AVPROBE_PADDING_SIZE)) < 0) ///> extra allocated bytes at the end of the probe buffer
goto fail;
if ((ret = avio_read(pb, buf + buf_offset, ///> 1.3.1 此函数最终调用 s->read_packet(s->opaque, buf, size);
probe_size - buf_offset)) < 0) ///> 读取 probe_size 长度数据到 buf 中供 av_probe_input_format2() 用
{
/* Fail if error was not end of file, otherwise, lower score. */
if (ret != AVERROR_EOF)
goto fail;
score = 0;
ret = 0; /* error was end of file, nothing read */
}
buf_offset += ret;
if (buf_offset < offset)
continue;
pd.buf_size = buf_offset - offset;
pd.buf = &buf[offset]; ///> 1.3.2 将buf缓存保存到AVProbeData中
memset(pd.buf + pd.buf_size, 0, AVPROBE_PADDING_SIZE);
/* Guess file format. 根据AVPorbeData中的数据,探测出输入视频的容器格式,
即 AVInputFormat,ffmpeg支持的AVInputFormat定义在 demuxer_list.c 中
*/
*fmt = av_probe_input_format2(&pd, 1, &score); ///> 1.3.3 根据 pd.buf 中数据匹配 demuxer 格式
if (*fmt) {
/* This can only be true in the last iteration. */
if (score <= AVPROBE_SCORE_RETRY) {
av_log(logctx, AV_LOG_WARNING,
"Format %s detected only with low score of %d, "
"misdetection possible!\n", (*fmt)->name, score);
} else
av_log(logctx, AV_LOG_DEBUG,
"Format %s probed with size=%d and score=%d\n",
(*fmt)->name, probe_size, score);
#if 0
FILE *f = fopen("probestat.tmp", "ab");
fprintf(f, "probe_size:%d format:%s score:%d filename:%s\n", probe_size, (*fmt)->name, score, filename);
fclose(f);
#endif
}
}
if (!*fmt)
ret = AVERROR_INVALIDDATA;
fail:
/* Rewind. Reuse probe buffer to avoid seeking. */
ret2 = ffio_rewind_with_probe_data(pb, &buf, buf_offset); ///> 1.3.4 匹配到解封装器后,把probe中数据转移
if (ret >= 0)
ret = ret2;
av_freep(&pd.mime_type);
return ret < 0 ? ret : score;
}
根据上面的代码我们知道数据流格式匹配由函数 av_probe_input_format2() 处理。
至此我们先总结一下这个逻辑:
- 函数 av_probe_input_buffer2() 在 max_probe_size 数据长度内,匹配最优的解封装器;
- 成功就返回 AVInputFormat *fmt 指向的对象;(假设返回的是 ff_mov_demuxer 解封装器).
- 函数 init_input() 执行完返回到 avformat_open_input() 中,此函数后面就开始匹配解码器,在成功就进入连续播放的状态。
接下来 看 ff_mov_demuxer 解封装器是如何实现,此源码路径: libavformat / mov.c
#define OFFSET(x) offsetof(MOVContext, x)
#define FLAGS AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_DECODING_PARAM
static const AVOption mov_options[] = {
{
"use_absolute_path",
"allow using absolute path when opening alias, this is a possible security issue",
OFFSET(use_absolute_path), AV_OPT_TYPE_BOOL, {
.i64 = 0},
0, 1, FLAGS},
{
"seek_streams_individually",
"Seek each stream individually to the to the closest point",
OFFSET(seek_individually), AV_OPT_TYPE_BOOL, {
.i64 = 1 },
0, 1, FLAGS},
{
"ignore_editlist", "Ignore the edit list atom.", OFFSET(ignore_editlist), AV_OPT_TYPE_BOOL, {
.i64 = 0},
0, 1, FLAGS},
{
"advanced_editlist",
"Modify the AVIndex according to the editlists. Use this option to decode in the order specified by the edits.",
OFFSET(advanced_editlist), AV_OPT_TYPE_BOOL, {
.i64 = 1},
0, 1, FLAGS},
{
"ignore_chapters", "", OFFSET(ignore_chapters), AV_OPT_TYPE_BOOL, {
.i64 = 0},
0, 1, FLAGS},
{
"use_mfra_for",
"use mfra for fragment timestamps",
OFFSET(use_mfra_for), AV_OPT_TYPE_INT, {
.i64 = FF_MOV_FLAG_MFRA_AUTO},
-1, FF_MOV_FLAG_MFRA_PTS, FLAGS,
"use_mfra_for"},
{
"auto", "auto", 0, AV_OPT_TYPE_CONST, {
.i64 = FF_MOV_FLAG_MFRA_AUTO}, 0, 0,
FLAGS, "use_mfra_for" },
{
"dts", "dts", 0, AV_OPT_TYPE_CONST, {
.i64 = FF_MOV_FLAG_MFRA_DTS}, 0, 0,
FLAGS, "use_mfra_for" },
{
"pts", "pts", 0, AV_OPT_TYPE_CONST, {
.i64 = FF_MOV_FLAG_MFRA_PTS}, 0, 0,
FLAGS, "use_mfra_for" },
{
"export_all", "Export unrecognized metadata entries", OFFSET(export_all),
AV_OPT_TYPE_BOOL, {
.i64 = 0 }, 0, 1, .flags = FLAGS },
{
"export_xmp", "Export full XMP metadata", OFFSET(export_xmp),
AV_OPT_TYPE_BOOL, {
.i64 = 0 }, 0, 1, .flags = FLAGS },
{
"activation_bytes", "Secret bytes for Audible AAX files", OFFSET(activation_bytes),
AV_OPT_TYPE_BINARY, .flags = AV_OPT_FLAG_DECODING_PARAM },
{
"audible_fixed_key", // extracted from libAAX_SDK.so and AAXSDKWin.dll files!
"Fixed key used for handling Audible AAX files", OFFSET(audible_fixed_key),
AV_OPT_TYPE_BINARY, {
.str="77214d4b196a87cd520045fd20a51d67"},
.flags = AV_OPT_FLAG_DECODING_PARAM },
{
"decryption_key", "The media decryption key (hex)", OFFSET(decryption_key), AV_OPT_TYPE_BINARY, .flags = AV_OPT_FLAG_DECODING_PARAM },
{
"enable_drefs", "Enable external track support.", OFFSET(enable_drefs), AV_OPT_TYPE_BOOL,
{
.i64 = 0}, 0, 1, FLAGS },
{
NULL },
};
static const AVClass mov_class = {
.class_name = "mov,mp4,m4a,3gp,3g2,mj2",
.item_name = av_default_item_name,
.option = mov_options,
.version = LIBAVUTIL_VERSION_INT,
};
AVInputFormat ff_mov_demuxer = {
.name = "mov,mp4,m4a,3gp,3g2,mj2",
.long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"),
.priv_class = &mov_class,
.priv_data_size = sizeof(MOVContext),
.extensions = "mov,mp4,m4a,3gp,3g2,mj2",
.read_probe = mov_probe,
.read_header = mov_read_header,
.read_packet = mov_read_packet,
.read_close = mov_read_close,
.read_seek = mov_read_seek,
.flags = AVFMT_NO_BYTE_SEEK,
};
ff_mov_demuxer 解封装器支持的格式"mov,mp4,m4a,3gp,3g2,mj2",我们先看 AVoption 定义如下:
/**
* {"auto", "auto", 0, AV_OPT_TYPE_CONST, {.i64 = FF_MOV_FLAG_MFRA_AUTO}, 0, 0, FLAGS, "use_mfra_for" },
* 上面的数据对应到结构体中,如下。
*/
typedef struct AVOption {
const char *name; //> "auto",
const char *help; //> "auto",
int offset; //> 0,
enum AVOptionType type; //> AV_OPT_TYPE_CONST,
union {
int64_t i64;
double dbl;
const char *str;
AVRational q;
} default_val; //> {.i64 = FF_MOV_FLAG_MFRA_AUTO},
double min; //> 0,
double max; //> 0,
int flags; //> FLAGS,
const char *unit; //> "use_mfra_for"
} AVOption;
程序在 AVInputFormat ff_mov_demuxer 定义时并没有见到给成员 mime_type 赋值;
而程序 av_probe_input_format2() 中在用,我们看看是如何使用的。
第二 . probe input 如何分解数据流格式
我们看下面的这个结构体,前面注解很清晰是 数据格式匹配。
/**
* This structure contains the data a format has to probe a file.
*/
typedef struct AVProbeData {
const char *filename; ///> 字符串指针,与硬件平台结构有关。
unsigned char *buf; /**< Buffer must have AVPROBE_PADDING_SIZE of extra allocated bytes filled with zero. */
int buf_size; /**< Size of buf except extra allocated bytes */
const char *mime_type; /**< mime_type, when known. */
} AVProbeData;
接着看函数 av_probe_input_format2() 函数做了什么事情呢。
///> *fmt = av_probe_input_format2(&pd, 1, &score);
AVInputFormat *av_probe_input_format2(AVProbeData *pd, int is_opened, int *score_max)
{
int score_ret;
AVInputFormat *fmt = av_probe_input_format3(pd, is_opened, &score_ret);
if (score_ret > *score_max) {
*score_max = score_ret;
return fmt;
} else
return NULL;
}
AVInputFormat *av_probe_input_format3(AVProbeData *pd, int is_opened,
int *score_ret)
{
AVProbeData lpd = *pd;
AVInputFormat *fmt1 = NULL, *fmt;
int score, score_max = 0;
const static uint8_t zerobuffer[AVPROBE_PADDING_SIZE]; ///> AVPROBE_PADDING_SIZE = 32 bytes
enum nodat {
NO_ID3,
ID3_ALMOST_GREATER_PROBE,
ID3_GREATER_PROBE,
ID3_GREATER_MAX_PROBE,
} nodat = NO_ID3;
if (!lpd.buf)
lpd.buf = (unsigned char *) zerobuffer;
///> 上面读取的第一帧数据是放到pb.buf中,直接解析读取到数据流内容。
if (lpd.buf_size > 10 && ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC)) {
///> 2.1 匹配ID3V2特征头 10 bytes,
int id3len = ff_id3v2_tag_len(lpd.buf);
if (lpd.buf_size > id3len + 16) {
if (lpd.buf_size < 2LL*id3len + 16)
nodat = ID3_ALMOST_GREATER_PROBE;
lpd.buf += id3len;
lpd.buf_size -= id3len;
} else if (id3len >= PROBE_BUF_MAX) {
nodat = ID3_GREATER_MAX_PROBE;
} else
nodat = ID3_GREATER_PROBE;
}
fmt = NULL;
while ((fmt1 = av_iformat_next(fmt1))) {
///> 2.2 遍历解复用器列表
if (!is_opened == !(fmt1->flags & AVFMT_NOFILE) && strcmp(fmt1->name, "image2"))
continue;
score = 0;
if (fmt1->read_probe) {
score = fmt1->read_probe(&lpd); ///> 2.3 调用解封装器的 read_probe,通过解析数据流内容,获取视频的得分值
if (score)
av_log(NULL, AV_LOG_TRACE, "Probing %s score:%d size:%d\n", fmt1->name, score, lpd.buf_size);
if (fmt1->extensions && av_match_ext(lpd.filename, fmt1->extensions)) {
///> ff_mov_demuxer 解封装器扩展 "mov,mp4,m4a,3gp,3g2,mj2",
switch (nodat) {
///> filename 字符中有 mp4,就可以匹配上这个解封装器。
case NO_ID3:
score = FFMAX(score, 1);
break;
case ID3_GREATER_PROBE:
case ID3_ALMOST_GREATER_PROBE:
score = FFMAX(score, AVPROBE_SCORE_EXTENSION / 2 - 1);
break;
case ID3_GREATER_MAX_PROBE:
score = FFMAX(score, AVPROBE_SCORE_EXTENSION);
break;
}
}
} else if (fmt1->extensions) {
if (av_match_ext(lpd.filename, fmt1->extensions)) ///> 直接匹配扩展明
score = AVPROBE_SCORE_EXTENSION;
}
if (av_match_name(lpd.mime_type, fmt1->mime_type)) {
///> 匹配 mime_type 类型
if (AVPROBE_SCORE_MIME > score) {
av_log(NULL, AV_LOG_DEBUG, "Probing %s score:%d increased to %d due to MIME type\n", fmt1->name, score, AVPROBE_SCORE_MIME);
score = AVPROBE_SCORE_MIME;
}
}
if (score > score_max) {
score_max = score;
fmt = fmt1;
} else if (score == score_max)
fmt = NULL;
}
if (nodat == ID3_GREATER_PROBE)
score_max = FFMIN(AVPROBE_SCORE_EXTENSION / 2 - 1, score_max);
*score_ret = score_max;
return fmt;
}
///> 入口参数: ff_id3v2_match(lpd.buf, ID3v2_DEFAULT_MAGIC) //ID3v2_DEFAULT_MAGIC = "ID3"
///> 此函数在 id3v2.c 中,源码路径 libavformat/id3v2.c ,此文件中主要是解封装的源码内容。
int ff_id3v2_match(const uint8_t *buf, const char *magic)
{
return buf[0] == magic[0] && // I
buf[1] == magic[1] && // D
buf[2] == magic[2] && // 3
buf[3] != 0xff &&
buf[4] != 0xff &&
(buf[6] & 0x80) == 0 && // length << 21
(buf[7] & 0x80) == 0 && // length << 14
(buf[8] & 0x80) == 0 && // length << 7
(buf[9] & 0x80) == 0; // length
}
///> 计算帧数据长度。
int ff_id3v2_tag_len(const uint8_t *buf)
{
int len = ((buf[6] & 0x7f) << 21) +
((buf[7] & 0x7f) << 14) +
((buf[8] & 0x7f) << 7) +
(buf[9] & 0x7f) +
ID3v2_HEADER_SIZE;
if (buf[5] & 0x10)
len += ID3v2_HEADER_SIZE;
return len;
}
///> 2.2 遍历解封装器
/** head of registered input format linked list */
static AVInputFormat *first_iformat = NULL;
/** head of registered output format linked list */
static AVOutputFormat *first_oformat = NULL;
static AVInputFormat **last_iformat = &first_iformat; ///> 输入 解封装格式链表的 first_iformat 节点, 全局遍历是 AVInputFormat格式的数组集合
static AVOutputFormat **last_oformat = &first_oformat; ///> 输出 封装格式链表 first_oformat 节点
AVInputFormat *av_iformat_next(const AVInputFormat *f)
{
if (f)
return f->next;
else
return first_iformat;
}
我们在看看 ID3V2.3 标签头结构格式定义,如下:
--------------------------------------------------------------------
名称 字节 说明
--------------------------------------------------------------------
Header 3 ID3V2.3 标识符"ID3"的Ascii码,否则认为没有ID3V2.3
Ver 1 版本号,=03
Revision 1 副版本号,=00
flag 1 标志字节,一般没意义,=00
Size 4 标签内容长度,高位在前,不包括标签头的10个字节
---------------------------------------------------------------------
说明:
- Size 字段的计算公式如下(从左至右):
size =字节1的值×&H200000+字节2的值×&H4000+字节3的值×&H80+字节4的值
- 如果所有标签帧的总长度<标签内容长度,则须用0填满。
至此我们基本清楚匹配 解封装器的过程,函数 ff_id3v2_match() 根据 ID3v2 的格式头进行匹配,
检测数据流是否为 ID3V2 流格式,扩展名匹配成功就确认此解封装器可以使用。
总结一下:
- IJKPLAYER 中支持的解封装器都添加到 以 first_iformat 为首的链表中;
- 封装器都添加到 以 first_oformat 为首的链表中;
- 用户可以参考 其他解封装器、或封装器程序、实现私有解封装或封装的方法。
第三. 接下来详细分析从数据流提取信息
我们还以 ID3v2 封装流格式为例,前面根据标签头已经匹配到 ff_mov_demuxer 解封装器,接下来程序从 init_input() 返回到
avformat_open_input 函数继续执行,如下:
int avformat_open_input(){
if ((ret = init_input(s, filename, &tmp)) < 0) ///> 1. 通过 filename 初始化 AVFormatContext 的 input 相关参数
goto fail; ///> 处理ID3V2的首帧数据
s->probe_score = ret;
...... 省略相关的代码
/* e.g. AVFMT_NOFILE formats will not have a AVIOContext */
if (s->pb)
ff_id3v2_read_dict(s->pb, &s->internal->id3v2_meta, ID3v2_DEFAULT_MAGIC, &id3v2_extra_meta); ///> 此程序调用 id3v2_parse() 函数
if (id3v2_extra_meta) {
if (!strcmp(s->iformat->name, "mp3") || !strcmp(s->iformat->name, "aac") ||
!strcmp(s->iformat->name, "tta")) {
if ((ret = ff_id3v2_parse_apic(s, &id3v2_extra_meta)) < 0) ///> 处理帧标识为 APIC 的第二帧数据
goto fail;
if ((ret = ff_id3v2_parse_chapters(s, &id3v2_extra_meta)) < 0) ///>
goto fail;
} else
av_log(s, AV_LOG_DEBUG, "demuxer does not support additional id3 data, skipping\n");
}
ff_id3v2_free_extra_meta(&id3v2_extra_meta);
if ((ret = avformat_queue_attached_pictures(s)) < 0)
goto fail;
update_stream_avctx(s);
for (i = 0; i < s->nb_streams; i++)
s->streams[i]->internal->orig_codec_id = s->streams[i]->codecpar->codec_id;
}
///> 到此 avformat_open_input() 打开输入流函数就执行结束,程序返回至 read_thread() 线程中。
///> 建立新流
int ff_id3v2_parse_apic(AVFormatContext *s, ID3v2ExtraMeta **extra_meta)
{
ID3v2ExtraMeta *cur;
for (cur = *extra_meta; cur; cur = cur->next) {
ID3v2ExtraMetaAPIC *apic;
AVStream *st;
if (strcmp(cur->tag, "APIC"))
continue;
apic = cur->data;
if (!(st = avformat_new_stream(s, NULL)))
return AVERROR(ENOMEM);
st->disposition |= AV_DISPOSITION_ATTACHED_PIC;
st->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
st->codecpar->codec_id = apic->id;
if (AV_RB64(apic->buf->data) == 0x89504e470d0a1a0a)
st->codecpar->codec_id = AV_CODEC_ID_PNG;
if (apic->description[0])
av_dict_set(&st->metadata, "title", apic->description, 0);
av_dict_set(&st->metadata, "comment", apic->type, 0);
av_init_packet(&st->attached_pic);
st->attached_pic.buf = apic->buf;
st->attached_pic.data = apic->buf->data;
st->attached_pic.size = apic->buf->size - AV_INPUT_BUFFER_PADDING_SIZE;
st->attached_pic.stream_index = st->index;
st->attached_pic.flags |= AV_PKT_FLAG_KEY;
apic->buf = NULL;
}
return 0;
}
///>
int ff_id3v2_parse_chapters(AVFormatContext *s, ID3v2ExtraMeta **extra_meta)
{
int ret = 0;
ID3v2ExtraMeta *cur;
AVRational time_base = {
1, 1000};
ID3v2ExtraMetaCHAP **chapters = NULL;
int num_chapters = 0;
int i;
// since extra_meta is a linked list where elements are prepended,
// we need to reverse the order of chapters
for (cur = *extra_meta; cur; cur = cur->next) {
ID3v2ExtraMetaCHAP *chap;
if (strcmp(cur->tag, "CHAP"))
continue;
chap = cur->data;
if ((ret = av_dynarray_add_nofree(&chapters, &num_chapters, chap)) < 0)
goto end;
}
for (i = 0; i < (num_chapters / 2); i++) {
ID3v2ExtraMetaCHAP *right;
int right_index;
right_index = (num_chapters - 1) - i;
right = chapters[right_index];
chapters[right_index] = chapters[i];
chapters[i] = right;
}
for (i = 0; i < num_chapters; i++) {
ID3v2ExtraMetaCHAP *chap;
AVChapter *chapter;
chap = chapters[i];
chapter = avpriv_new_chapter(s, i, time_base, chap->start, chap->end, chap->element_id);
if (!chapter)
continue;
if ((ret = av_dict_copy(&chapter->metadata, chap->meta, 0)) < 0)
goto end;
}
end:
av_freep(&chapters);
return ret;
}
我们接下来在看 ID3v2 标签帧的结构,如下:
表4:标签帧的结构
----------------------------------------------------------
名称 字节 说明
----------------------------------------------------------
FrameID 4 帧标识符的Ascii码,常用标识符的意义见表5
Size 4 帧内容及编码方式的合计长度,高位在前
Flags 2 标志,只使用了6位,详见表6,一般均=0
encode 4 帧内容所用的编码方式。许多帧没有此项
帧内容 至少 1 个字节
----------------------------------------------------------
说明:
①Size的计算同上。
②标签帧之间没有特殊的分隔符,要得到一个完整的标签帧内容必须先从帧头中得到帧内容长度。
③encode 有 4 个可能值:
0:表示帧内容字符用 ISO-8859-1 编码;
1:表示帧内容字符用 UTF-16LE 编码;
2:表示帧内容字符用 UTF-16BE 编码;
3:表示帧内容字符用 UTF-8 编码(仅ID3V2.4才支持)
但经常看到的是"eng"这样的字符形式,它表示帧内容所使用的自然语言为英语。也许 D3V2 标签帧进化到现在,encode 已经用“自然语言”取代了“编码方式”。
⑤帧内容均为字符串,常以 00 开头。
表5:标签帧标识符的意义
---------------------------------------
名称 意义
---------------------------------------
AENC: 音频加密技术
APIC: 附加描述
COMM: 注释,相当于ID3v1的Comment
COMR: 广告
ENCR: 加密方法注册
ETC0: 事件时间编码
GEOB: 常规压缩对象
GRID: 组识别注册
IPLS: 复杂类别列表
MCDI: 音乐CD标识符
MLLT: MPEG位置查找表格
OWNE: 所有权
PRIV: 私有
PCNT: 播放计数
POPM: 普通仪表
POSS: 位置同步
RBUF: 推荐缓冲区大小
RVAD: 音量调节器
RVRB: 混响
SYLT: 同步歌词或文本
SYTC: 同步节拍编码
TALB: 专辑,相当于ID3v1的Album
TBPM: 每分钟节拍数
TCOM: 作曲家
TCON: 流派(风格),见表2
TCOP: 版权
TDAT: 日期
TDLY: 播放列表返录
TENC: 编码
TEXT: 歌词作者
TFLT: 文件类型
TIME: 时间
TIT1: 内容组描述
TIT2: 标题,相当于ID3v1的Title
TIT3: 副标题
TKEY: 最初关键字
TLAN: 语言
TLEN: 长度
TMED: 媒体类型
TOAL: 原唱片集
TOFN: 原文件名
TOLY: 原歌词作者
TOPE: 原艺术家
TORY: 最初发行年份
TOWM: 文件所有者(许可证者)
TPE1: 艺术家相当于ID3v1的Artist
TPE2: 乐队
TPE3: 指挥者
TPE4: 翻译(记录员、修改员)
TPOS: 作品集部分
TPUB: 发行人
TRCK: 音轨(曲号),相当于ID3v1的Track
TRDA: 录制日期
TRSN: Intenet电台名称
TRSO: Intenet电台所有者
TSIZ: 大小
TSRC: ISRC(国际的标准记录代码)
TSSE: 编码使用的软件(硬件设置)
TYER: 年代,相当于ID3v1的Year
TXXX: 年度
UFID: 唯一的文件标识符
USER: 使用条款
USLT: 歌词
WCOM: 广告信息
WCOP: 版权信息
WOAF: 官方音频文件网页
WOAR: 官方艺术家网页
WOAS: 官方音频原始资料网页
WORS: 官方互联网无线配置首页
WPAY: 付款
WPUB: 出版商官方网页
WXXX: 用户定义的URL链接
---------------------------------------
说明:
①帧内容是数字的,都用 Ascii 字符表示。
②有的 TCON(风格、流派)的帧内容是直接用字符串表示的,如“genre”,而有的则是用编号表示的,如“28 31 32 29”就是用字符串“(12)”表示 12 号风格,我们在解析的时候要注意。
③TRCK(音轨)的帧内容格式是:N/M。其中,分母表示专辑中共有 M 首歌曲,分子表示专辑中的第 N 首曲。
表6:标签帧中Flags标志的意义
----------------------------------------------------
位址 意义
----------------------------------------------------
0 标签保护标志,如设置表示此帧作废
1 文件保护标志,如设置表示此帧作废
2 只读标志,如设置表示此帧不能修改
3 压缩标志,如设置表示1个字节存放2个BCD码表示数字
4 加密标志
5 组标志,如设置表示此帧和其它的某帧是一组
----------------------------------------------------
至此总结以下:
- 当 url 中有 .MP4 关键字时,匹配到的是 ff_mov_demuxer 解封装器。
- ID3v2 的数据流是由 “标签头 + 标签帧1 + … + 标签帧n”,
在 init_input() 函数处理标签头,ff_id3v2_parse_apic() 处理标签帧,建立数据流。 - 通过 avformat_queue_attached_pictures() 把标签帧解析的数据放入 raw_packet_buffer 的队尾。
- 函数 update_stream_avctx(s) 刷新流相关参数,接下来给数据流配置 codec_id 赋值指定解码器。
- 函数 avformat_open_input() 执行结束、返回到 read_thread() 线程函数中。
下一篇文章 《ijkplayer 代码走读之 h264 解封装器应用详解》,通过实例再次阐述 IJKPLAYER 的解协议、解封装的
实现逻辑。