【Go】FLV文件解析(一)

这是一个系列教程,一是为了解释FLV文件的结构,二是为了练习Go语言,希望大家多多支持。

在实战编码之前,我们需要首先了解FLV文件的格式。FLV是adobe出品的视频封装格式,注意它只是封装格式,不是编码格式。做为第一节的内容,我们不会过度深入音视频数据的编码,这部分内容以后会再讲。

FLV文件格式

FLV文件由FLV Header和FLV Body组成,FLV Body又由许多Tag组成,Tag里面可能是视频、音频或脚本。这里所说的脚本并不是可执行脚本,而是视频的一些元信息。在每一个Tag的前面还有一个重要信息,就是前一个Tag的大小。它们看起来如下图。

为什么Tag之间会记录上一个Tag大小呢?为的是方便回溯,有了前一个Tag大小,我们就可以轻松回溯到前一个Tag了。

FLV Header

1版本的FLV Header共9个字节,目前应该都是这个版本。FLV Header结构如下图。

FLV头的前三个字节分别是FLV三个字母的ASIIC码,可以由此来判断一个文件是否是FLV文件。紧接着的一个字节是FLV文件版本,下一个字节是是一个flag标志,其中第8个比特表示是否包含视频,第6个比特表示是否包含音频,其余比特必须是0,最后是FLV头的大小,占4个字节。

FLV Tag

FLV的Tag也由Tag Header和Tag Data组成,Tag Header共计11字节,结构如下图。

FLV Tag有三种类型,它们有相同的Tag Header,不同的是Tag Data中的数据类型。

  • 8:音频Tag,Tag Data中是音频数据。

  • 9:视频Tag,Tag Data中是视频数据。

  • 18:元数据Tag,Tag Data中是视频元信息。

Tag Data大小占3字节,表示Tag Data的长度,不包括Tag Header在内。

关于时间戳,我们要搞清楚以下3个问题:

  1. 时间戳的单位是是什么?

  2. 时间戳的含义是什么?

  3. 时间戳如何计算?

第一个问题,根据官方文档的描述,时间戳的单位是毫秒。

第二个问题,时间戳表示的是相对于第一个Tag时间戳的偏移量,因此第一个Tag的时间戳永远是0。

第三个问题,时间戳在这里被分成了两部分,一个3字节的时间戳和1字节的扩展时间戳,完整的时间戳应该是一个32位无符号整数,计算的时候,扩展时间戳是高8位,时间戳是低24位,在拼时间戳的时候要注意这一点。

流ID实际上源于RTMP协议的多路复用,因为FLV只支持一路流,因此流ID总是0。

基本的FLV结构就介绍到这里,下面开始代码实操。

实战

首先我们需要一个工具提供基础的解码功能,你可以通过导入"gitee.com/lJOSVDE/stream"这个库来使用这个工具。另外我们使用的系统是小端序。

新建一个Go项目以及flv.go文件,首先我们定义两个全局变量。

var (
    _FLV_         = [3]byte{'F', 'L', 'V'}
    FLV_FMT_ERROR = errors.New("flv format error")
)

接下来定义FLV Header和FLV Tag的结构。

// flv头部
type FlvHeader struct {
    flv      [3]byte
    Version  uint8
    HasAudio bool
    HasVideo bool
    Size     uint32
}

// flv tag
type FlvTag struct {
    Header FlvTagHeader
    Data   stream.Stream
}

// flv tag头部
type FlvTagHeader struct {
    TagType    uint8
    DataSize   uint32
    Timestamp  uint32
    StreamID   uint32
    PreTagSzie uint32
}

这里我们将FlvTagData字段也定义为Stream类型,方便之后进一步的解析。然后需要准备一个FLV文件,然后写一个读取文件的函数。

func ReadFlv(path string) (Stream, error) {
    if name == "" {
        return nil, errors.New("empty flv name")
    }
    bs, err := ioutil.ReadFile(name)
    if err != nil {
        return nil, err
    }
    return stream.NewByteStream(bs), nil
}

下面来读取FLV头部。

// 读取FLV Header
func DecodeFlvHeader(s stream.Stream) (h FlvHeader, err error) {
	// 读取FLV
	err = s.Byte(&h.flv[0]).Byte(&h.flv[1]).Byte(&h.flv[2]).Error()
	if h.flv != _FLV_ || err != nil {
		err = FLV_FMT_ERROR
		return
	}
	var flag byte
	// 读取FLV版本、标识符、头部大小
	err = s.U8(&h.Version).Byte(&flag).U32(&h.Size).Error()
	h.HasAudio = flag&0x04 == 0x04
	h.HasVideo = flag&0x01 == 0x01
	return
}

接下来读取FLV Tag。

// 读取一个FLV Tag
func DecodeFlvTag(s stream.Stream) (t FlvTag, err error) {
	var externTimestamp uint8
	err = s.
		U32(&t.Header.PreTagSzie). //前一个Tag大小
		U8(&t.Header.TagType).     //Tag类型
		U24(&t.Header.DataSize).   //Tag Data大小
		U24(&t.Header.Timestamp).  //时间戳
		U8(&externTimestamp).      //扩展时间戳
		U24(&t.Header.StreamID).   //流ID
		Error()
	if err != nil {
		return
	}
  //拼完整时间戳
	if externTimestamp != 0 {
		t.Header.Timestamp |= uint32(externTimestamp) << 24
	}
	//读取Tag Data
	t.Data, err = s.Produce(int(t.Header.DataSize))
	return
}

为了方便观察和调试,我们再给FlvTag实现Stringer接口。

func (f FlvTag) String() string {
	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("tag header: %+v\ntag body:", f.Header))
	for i, b := range f.Data.Raw() {
		if i&0x0F == 0 {
			sb.WriteByte('\n')
		}
		sb.WriteString(fmt.Sprintf("%2X ", b))
	}
	return sb.String()
}

到这里,flv.go的内容就基本完成了,接下来写个main函数来试试吧。

func main() {
	s, _ := ReadFlv("b.flv")
	h, _ := DecodeFlvHeader(s)
	fmt.Printf("%+v\n", h)
	st, _ := DecodeFlvTag(s)
	fmt.Println(st)
}

以上就是本期的全部内容,你可以亲手试一试如何循环读完所有Tag。

下一期我们探索Tag Data的内部乾坤。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值