=================================================================
音视频入门基础:FLV专题系列文章:
音视频入门基础:FLV专题(2)——使用FFmpeg命令生成flv文件
音视频入门基础:FLV专题(3)——FLV header简介
音视频入门基础:FLV专题(4)——使用flvAnalyser工具分析FLV文件
音视频入门基础:FLV专题(5)——FFmpeg源码中,判断某文件是否为FLV文件的实现
音视频入门基础:FLV专题(6)——FFmpeg源码中,解码FLV header的实现
音视频入门基础:FLV专题(7)——Tag header简介
音视频入门基础:FLV专题(8)——FFmpeg源码中,解码Tag header的实现
音视频入门基础:FLV专题(9)——Script Tag简介
音视频入门基础:FLV专题(10)——Script Tag实例分析
音视频入门基础:FLV专题(11)——FFmpeg源码中,解析SCRIPTDATASTRING类型的ScriptDataValue的实现
音视频入门基础:FLV专题(12)——FFmpeg源码中,解析DOUBLE类型的ScriptDataValue的实现
音视频入门基础:FLV专题(13)——FFmpeg源码中,解析任意Type值的SCRIPTDATAVALUE类型的实现
音视频入门基础:FLV专题(14)——FFmpeg源码中,解码Script Tag的实现
音视频入门基础:FLV专题(15)——Video Tag简介
音视频入门基础:FLV专题(16)——FFmpeg源码中,解码Video Tag的VideoTagHeader的实现
音视频入门基础:FLV专题(17)——FFmpeg源码中,提取Video Tag的VIDEODATA的实现
音视频入门基础:FLV专题(18)——Audio Tag简介
音视频入门基础:FLV专题(19)——FFmpeg源码中,解码Audio Tag的AudioTagHeader,并提取AUDIODATA的实现
音视频入门基础:FLV专题(20)——FFmpeg源码中,获取FLV文件major_brand、minor_version、compatible_brands、encoder、Duration的实现
音视频入门基础:FLV专题(21)——FFmpeg源码中,获取FLV文件音频信息的实现(上)
音视频入门基础:FLV专题(22)——FFmpeg源码中,获取FLV文件音频信息的实现(中)
音视频入门基础:FLV专题(23)——FFmpeg源码中,获取FLV文件音频信息的实现(下)
音视频入门基础:FLV专题(24)——FFmpeg源码中,获取FLV文件视频信息的实现
音视频入门基础:FLV专题(25)——通过FFprobe显示FLV文件每个packet的信息
=================================================================
一、引言
在《音视频入门基础:FLV专题(9)——Script Tag简介》中对Script Tag进行了简介,本文讲述FFmpeg源码中是怎样解码FLV文件的Script Tag,拿到里面的信息。
二、flv_read_packet函数
从《音视频入门基础:FLV专题(8)——FFmpeg源码中,解码Tag header的实现》可以知道,FFmpeg源码中使用flv_read_packet函数来读取每个Tag的信息,该函数的前半部分实现了解码Tag header,获取其TagType属性的功能。然后根据TagType属性的值,判断该Tag为音频Tag、视频Tag还是脚本Tag。根据Tag的类型分别执行不同的解码操作:
if (type == FLV_TAG_TYPE_AUDIO) {
//...
} else if (type == FLV_TAG_TYPE_VIDEO) {
//...
}else if (type == FLV_TAG_TYPE_META) {
//...
}else{
//...
}
//...
从《音视频入门基础:FLV专题(9)——Script Tag简介》可以知道:
Script Tag = Tag header + SCRIPTDATA
未加密的情况下,SCRIPTDATA = ScriptTagBody
而FFmpeg源码(截止源码版本7.0.1)会统一把FLV文件当做未加密来处理。
所以FFmpeg源码中:Script Tag = Tag header + ScriptTagBody
如果在flv_read_packet函数的前半部分判断出该Tag为Script Tag(脚本Tag),flv_read_packet函数中会执行如下逻辑解码Script Tag的ScriptTagBody:
else if (type == FLV_TAG_TYPE_META) {
stream_type=FLV_STREAM_TYPE_SUBTITLE;
if (size > 13 + 1 + 4) { // Header-type metadata stuff
int type;
meta_pos = avio_tell(s->pb);
type = flv_read_metabody(s, next);
if (type == 0 && dts == 0 || type < 0) {
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");
return flv_data_packet(s, pkt, dts, next);
} else if (type == TYPE_ONCAPTION) {
return flv_data_packet(s, pkt, dts, next);
} else if (type == TYPE_UNKNOWN) {
stream_type = FLV_STREAM_TYPE_DATA;
}
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;
//...
leave:
//...
return ret;
}
下面我们分析上述代码块中解码ScriptTagBody的原理。
三、flv_read_packet函数中解码ScriptTagBody的原理
从 《音视频入门基础:FLV专题(8)——FFmpeg源码中,解码Tag header的实现》中可以知道,上述代码块中,局部变量size存贮该Tag的Tag data的长度,当该Tag为Script Tag时,它的Tag data就是ScriptTagBody,所以此时局部变量size存贮ScriptTagBody的长度:
else if (type == FLV_TAG_TYPE_META) {
stream_type=FLV_STREAM_TYPE_SUBTITLE;
if (size > 13 + 1 + 4) { // Header-type metadata stuff
//...
}
从《音视频入门基础:FLV专题(9)——Script Tag简介》可以知道:
ScriptTagBody = Name + Value
ScriptTagBody的Name = 1字节的值为2的Type + 2字节的StringLength + 可变长的StringData
ScriptTagBody的Value = 1字节的值为8的Type + 4字节的ECMAArrayLength + Variables数组 + 3字节的终止符(值固定为0,0,9)
FLV文件中必然有一个名称为“onMetadata”的Script Tag。当Script Tag为“onMetadata”时,ScriptTagBody的Name的StringData必为10个字节(“onMetadata”总共10个字节)。
所以当Script Tag为“onMetadata”时,ScriptTagBody的Name = 1字节的值为2的Type + 2字节的StringLength + 10字节的StringData(“onMetadata”),这时ScriptTagBody的Name固定为13字节。
ScriptTagBody的Value = 1字节的值为8的Type + 4字节的ECMAArrayLength + Variables数组 + 3字节的终止符(值固定为0,0,9),ScriptTagBody的Value必然不小于(1 + 4 + 3)个字节。
所以整个ScriptTagBody必然不小于(13 + 1 + 4 + 3)个字节。
所以flv_read_packet函数解码ScriptTagBody时,首先判断ScriptTagBody长度是否大于(13 + 1 + 4)个字节,如果不大于,不执行解码ScriptTagBody的操作。[个人认为FFmpeg源码中if (size > 13 + 1 + 4)这部分判断不够严谨,没有考虑到3字节终止符的情况,应该改为if (size > 13 + 1 + 4 + 3)]:
if (size > 13 + 1 + 4) { // Header-type metadata stuff
//...
}
通过avio_tell函数得到该Script Tag的ScriptTagBody的第一个字节相对于FLV文件的文件首的偏移字节数,赋值给局部变量meta_pos。关于avio_tell函数的用法可以参考:《FFmpeg源码:avio_tell函数分析》:
meta_pos = avio_tell(s->pb);
执行语句:
int type;
//...
type = flv_read_metabody(s, next);
flv_read_metabody函数定义如下。可以看到flv_read_metabody函数中,首先通过语句:type = avio_r8(ioc)得到该Tag的ScriptTagBody的Name中的Type,然后通过语句:amf_get_string(ioc, buffer, sizeof(buffer))得到ScriptTagBody的Name中的StringData。最后通过语句:amf_parse_object(s, astream, vstream, buffer, next_pos, 0)解析ScriptTagBody的Value。从《音视频入门基础:FLV专题(13)——FFmpeg源码中,解析任意Type值的SCRIPTDATAVALUE类型的实现》中可以知道,当Script Tag的名称为“onMetadata”时,通过amf_parse_object函数可以解析出部分元数据属性(duration、videodatarate、audiodatarate等)。当amf_parse_object函数解析成功时,flv_read_metabody函数返回0:
static int flv_read_metabody(AVFormatContext *s, int64_t next_pos)
{
FLVContext *flv = s->priv_data;
AMFDataType type;
//...
// first object needs to be "onMetaData" string
type = avio_r8(ioc);
if (type != AMF_DATA_TYPE_STRING ||
amf_get_string(ioc, buffer, sizeof(buffer)) < 0)
return TYPE_UNKNOWN;
//...
// 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;
}
所以flv_read_packet函数中,如果Script Tag的名称为“onMetadata”,并且解析ScriptTagBody的Value成功,type的值为0:
type = flv_read_metabody(s, next);
该Tag为Script Tag时Tag header中的Timestamp必为0,所以dts为0。所以满足条件“type == 0 && dts == 0”,执行下面大括号中的内容。执行语句:goto skip:
if (type == 0 && dts == 0 || type < 0) {
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;
}
从《音视频入门基础:FLV专题(8)——FFmpeg源码中,解码Tag header的实现》中可以知道,局部变量next为该Tag对应的PreviousTagSize相对于文件首的偏移(单位为字节)。通过avio_seek函数把AVIOContext的文件位置指针移动到该Tag对应的PreviousTagSize处,关于avio_seek函数的用法可以参考:《FFmpeg源码:avio_seek函数分析》。如果移动失败或者实际移动到的位置不是PreviousTagSize,表示出错了,flv_read_packet函数返回AVERROR_INVALIDDATA。如果没有出错,flv_read_packet函数返回FFERROR_REDO:
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;
//...
leave:
//...
return ret;
宏FFERROR_REDO定义如下,表示数据被消耗但被丢弃(被忽略的流或垃圾数据)。当使用flv_read_packet函数读取名称为“onMetadata”的Script Tag时,因为该Tag不是音频Tag或视频Tag,不包含实际的音视频数据,所以读取成功时flv_read_packet函数会返回FFERROR_REDO:
/**
* Returned by demuxers to indicate that data was consumed but discarded
* (ignored streams or junk data). The framework will re-call the demuxer.
*/
#define FFERROR_REDO FFERRTAG('R','E','D','O')
四、总结
1.flv_read_packet函数的作用是读取每个Tag的信息,即解码某个Tag。该函数首先会解码Tag header,获取其TagType。然后根据TagType的值,判断该Tag为音频Tag、视频Tag还是脚本Tag。根据Tag的类型分别执行不同的解码操作。
2.该Tag为Script Tag(脚本Tag)的情况下,flv_read_packet函数返回一个负数(FFERROR_REDO)是正常的,表示数据被消耗但被丢弃。