书接上回,我们继续来解析FLV文件的内容,这次要解析的是元数据Tag的内容,需要注意的是不是每个FLV文件都有这个Tag的。
在有些教程中,元数据Tag也被称作Script Tag。在官方文档中其实是称作Data Tag,其中的内容称为ScriptDataObject。不管叫什么,你需要知道的是我们在讨论同一个东西。
ScriptDataObject的编码格式是AMF,全称Action Message Format。也是Adobe出品,一共有两个版本,为了区分,将先前的版本称为AMF0,新的版本称为AMF3。FLV中用到的主要是AMF0。
AMF0
AMF编码的基本套路就是类型+数据
,具体的格式后面详细介绍。AMF0一共定义了17种类型,如下表所示。
值 | 类型 | 说明 |
---|---|---|
0x00 | Number | 8字节浮点数 |
0x01 | Boolean | 布尔 |
0x02 | String | 字符串 |
0x03 | Object | 对象,键值对 |
0x04 | MovieClip | 保留,未使用 |
0x05 | Null | 空 |
0x06 | Undefined | 未定义类型 |
0x07 | Reference | 引用类型 |
0x08 | ECMA array | ECMA数组,键值对 |
0x09 | Object end | 对象和ECMA数组结束标志 |
0x0A | Strict array | 普通数组 |
0x0B | Date | 日期 |
0x0C | Long string | 长字符串 |
0x0D | Unsupported | 不支持类型 |
0x0E | Recordset | 保留,未使用 |
0x0F | Xml document | xml文档 |
0x10 | Typed object | 有类型对象 |
0x11 | Avmplus Object | 从AMF0切换到AMF3 |
以上是完整的AMF0类型,来自AMF0官方文档。而在FLV文档中,实际只有前12中类型,因此在这里我们也只对前12种类型做出说明。
AMF Number
AMF的Number类型都是浮点数,占8字节,格式是类型+值
。比如以下示例表示的就是数字10。
00 40 24 00 00 00 00 00 00
AMF Boolean
布尔类型的格式与数字类型相同,也是类型+值
的形式。布尔类型用一个字节表示,00
表示false
,01
表示true
。
AMF String
AMF中的字符串都是变长的,基本格式为类型+长度+值
。比如下面的例子表示的是字符串onMetaData
。
00 0A 6F 6E 4D 65 74 61 44 61 74 61
AMF Object
AMF对象类型实际上是键值对集合,它以0x000009
结束,其中0x0000
是空字符串,0x09
是AMF对象和ECMA数组的结束标识。ECMA数组和对象类型非常相似,都是键值对,唯一的区别是ECMA数组多了数组长度字段。
AMF对象的基本格式是类型+属性+值+...+0x000009
。其中属性是字符串类型,值可以是任意类型,遵循AMF类型+值
的基本套路,解析过程根据类型而定。
AMF Reference
AMF引用类型的基本格式是类型+值
,其中值是2字节无符号整数。它表示的是对某个变量的引用,目的是减少重复的数据传输带来的网络带宽。
AMF ECMA Array
ECMA数组也是键值对集合,以0x000009
结束,它与Object类型的唯一区别是在类型后面有一个长度字段,表示键值对数量。其基本格式为类型+size+键+值+...+0x000009
,键依然是字符串类型,值可以是任意类型,遵循类型+值
的基本套路。
AMF Object End
表示AMF对象和ECMA数组类型的结束。对象和ECMA数组其实是键值对集合,键的类型是字符串。AMF对象和ECMA数组的结束其实是空字符串+AMF Object End标识
,所以很多教程都认为对象和ECAM数组是以0x000009
结束的,但其实前面两字节的0x0000
表示的是一个空字符串。它的格式还是键+值
,只不过结尾部分键是空字符串,也就是0x0000
,值是结束标识,也就是0x09
。这也是为什么只有对象和ECMA数组有结束标识,而Strict Array没有的原因。
AMF Strict Array
StrictArray才是真正意义上的数组,其基本格式为类型+size+元素+...
,它并没有结束标志,这里的元素也是类型+值
的格式。
AMF Date
基本格式为类型+时间戳+时区
,其中时间戳是8字节浮点数,也就是Double类型,时区是2字节有符号整数,因为它代表的是失去偏移,可正可负。但是官方并不建议使用时区,而是建议设置为0。
AMF Long String
AMF Long String与String的区别在于前者的长度字段占4字节,是32位无符号整数,基本格式还是类型+长度+值
。
实战
介绍完AMF0数据类型的基本格式,下面可是实战练习。新建amf.go
文件,同样这里也需到导入"gitee.com/lJOSVDE/stream"
这个工具。
首先我们定义一些需要用到的常量。
const (
AMF_NUMBER = 0x00
AMF_BOOLEAN = 0x01
AMF_STRING = 0x02
AMF_OBJECT = 0x03
AMF_MOVIECLIP = 0x04 //reserved
AMF_NULL = 0x05
AMF_UNDEFINED = 0x06
AMF_REFERENCE = 0x07
AMF_ECMA_ARRAY = 0x08
AMF_OBJECT_END = 0x09
AMF_STRICT_ARRAY = 0x0A
AMF_DATE = 0x0B
AMF_LONG_STRING = 0x0C
AMF_UNSUPPORTED = 0x0D
AMF_RECORDSET = 0x0E //reserved
AMF_XML_DOCUMENT = 0x0F
AMF_TYPE_OBJECT = 0x10
AMF_AMF3 = 0x11
)
var (
ERR_UNSUPPORT = errors.New("unsupport amf type")
ERR_NOT_IMPLEMENT = errors.New("not implement")
ERR_AMF_TYPE_MISMATCH = errors.New("amf type mismatch")
ERR_EXPECT_OBJECT_END = errors.New("expect amf object end")
)
接下来我们需要一个可以读取任意类型数的函数。
func DecodeAMF(s stream.Stream) (v interface{}, err error) {
var amfType byte
if err = s.Byte(&amfType).Error(); err != nil {
return
}
switch amfType {
case AMF_NUMBER:
v, err = DecodeAMFNumber(s)
case AMF_BOOLEAN:
v, err = DecodeAMFBool(s)
case AMF_STRING:
v, err = DecodeAMFString(s)
case AMF_OBJECT:
v, err = DecodeAMFObject(s)
case AMF_NULL:
case AMF_UNDEFINED:
case AMF_REFERENCE:
v, err = DecodeAMFRefrence(s)
case AMF_ECMA_ARRAY:
v, err = DecodeAMFECMAArray(s)
case AMF_OBJECT_END:
err = ERR_OBJECT_ENDED
case AMF_STRICT_ARRAY:
v, err = DecodeAMFStrictArray(s)
case AMF_DATE:
v, err = DecodeAMFDate(s)
case AMF_LONG_STRING:
v, err = DecodeAMFLongString(s)
default:
err = ERR_UNSUPPORT
}
return
}
下面我们来实现具体的解码函数。
// 解码AMF数字类型
func DecodeAMFNumber(s stream.Stream) (v float64, err error) {
err = s.F64(&v).Error()
return
}
// 解码AMF布尔类型
func DecodeAMFBool(s stream.Stream) (v bool, err error) {
err = s.Bool(&v).Error()
return
}
// 解码AMF字符串类型
func DecodeAMFString(s stream.Stream) (v string, err error) {
var length uint16
if err = s.U16(&length).Error(); err != nil {
return
}
if length == 0 {
return "", nil
}
return s.String(int(length))
}
// 解码AMF对象类型
func DecodeAMFObject(s stream.Stream) (v map[string]interface{}, err error) {
v = make(map[string]interface{})
for err == nil {
var property string
var value interface{}
if property, err = DecodeAMFString(s); err != nil {
continue
}
if value, err = DecodeAMF(s); err != nil {
continue
}
v[property] = value
}
if errors.Is(err, ERR_OBJECT_ENDED) {
err = nil
}
return
}
// 解码AMF引用类型
func DecodeAMFRefrence(s stream.Stream) (v uint16, err error) {
err = s.U16(&v).Error()
return
}
// 解码AMF ECMA数组类型
func DecodeAMFECMAArray(s stream.Stream) (v map[string]interface{}, err error) {
var size uint32
if err = s.U32(&size).Error(); err != nil {
return
}
v = make(map[string]interface{}, size)
for i := 0; i < int(size); i++ {
var key string
var val interface{}
if key, err = DecodeAMFString(s); err != nil {
return
}
if val, err = DecodeAMF(s); err != nil {
return
}
v[key] = val
}
var end uint32
if err = s.U24(&end).Error(); err != nil {
return
}
if end != 0x09 {
err = ERR_EXPECT_OBJECT_END
}
return
}
// 解码AMF数组类型
func DecodeAMFStrictArray(s stream.Stream) (v []interface{}, err error) {
var size uint32
if err = s.U32(&size).Error(); err != nil {
return
}
v = make([]interface{}, size)
for i := 0; i < int(size); i++ {
if item, err := DecodeAMF(s); err != nil {
break
} else {
v[i] = item
}
}
return
}
// 解码AMF日期类型
func DecodeAMFDate(s stream.Stream) (v int64, err error) {
var zone int16
if err = s.I64(&v).I16(&zone).Error(); err != nil {
return
}
if zone != 0 {
//TODO:计算时区
}
return
}
// 解码AMF长字符串类型
func DecodeAMFLongString(s stream.Stream) (v string, err error) {
var length uint32
if err = s.U32(&length).Error(); err != nil {
return
}
if length == 0 {
return "", nil
}
return s.String(int(length))
}
以上就是AMF解码的全部代码,下面我们还需要定义一个结构来表示FLV Data Tag。
type FlvDataTag struct {
Header FlvTagHeader
Method string
MateData map[string]interface{}
}
在FLV Data Tag中,一般有两部分内容,一个是字符串onMetaData
,第二部分就是元数据,类型是ECMA数组。元数据的内容并不是完全固定的,也就是说不同的FLV文件它们的元数据从键值对的数量上来说都有可能是不同的。常见的元数据有以下这些。
属性 | 类型 | 说明 |
---|---|---|
duration | float64 | 时长(秒) |
width | float64 | 视频宽(像素) |
height | float64 | 视频高(像素) |
videodatarate | float64 | 码率(kbps) |
framerate | float64 | 帧率 |
videocodecid | float64 | 视频编码格式 |
audiosamplerate | float64 | 音频采样频率 |
audiosamplesize | float64 | 音频采样分辨率 |
stereo | bool | 是否是立体声 |
audiocodecid | float64 | 音频编码格式 |
filesize | float64 | 文件字节数 |
最后我们将【Go】FLV文件解析(一)中的FlvTag
转化成FlvDataTag
。
func DecodeFlvDataTag(t FlvTag) (d FlvDataTag, err error) {
d.Header = t.Header
var val interface{}
var ok bool
if val, err = DecodeAMF(t.Data); err != nil {
return
} else if d.Method, ok = val.(string); !ok {
err = FLV_FMT_ERROR
return
}
if val, err = DecodeAMF(t.Data); err != nil {
return
} else if d.MateData, ok = val.(map[string]interface{}); !ok {
err = FLV_FMT_ERROR
return
}
return
}
以上就是FLV Data Tag解析的全部内容,下一期我们继续音频Tag的解析。