FLV 视频格式解析

如今视频直播非常火热,就需要对视频格式的知识做些储备,FLV(即Flash Video)这一视频格式是最简单的,通过对它的文件格式的研究,可以对这些应用于网络传输的视频格式,也即流媒体有一些基本的了解。

FLV

流媒体简介

流式媒体是按照时间顺序依次下载播放的媒体形式,依托于Flash这一强大播放器插件,可以实现边下载边播放,比如在优酷等视频平台观看节目时,并不需要把视频完整下载下来就可以播放,而这些视频网站都有Flash插件的支持;与之对应的是BT下载的视频,很难做到_顺序下载_,另外还要看播放器是否支持解码部分视频,来达到边下载边播放的目的,你可以尝试播放一个没有下载完的视频,拖动进度条是可以找到能看的片段,但很少能从视频开头开始。

从上面可以看出,流媒体传输最重要的特性是要按时间顺序传输。打个比方,如果流媒体是一列火车,火车进站就必须严格按照“火车头-第一节车厢-第二节车厢…”的方式进站,乱了顺序可不行 :P

FLV格式解析

FLV是流媒体视频传输格式中最最简单的一个,下面我们从格式方面入手,看看它是如何保证按时间顺序传播视频文件的。

为了方便理解,先上一张示意图:

flv示意图

火车头,也即FLV视频的第一部分,是用来表明自己身份的标识,“大家好,我叫FLV”,仅仅使用9个字节,其中3个字节是名字“FLV”,一个字节标识版本号,目前都为0,一个字节用来标识FLV里面是否包含音频和视频,最后用4个字节标识这个“火车头”,即整个头文件的长度。

使用vim打开一个flv文件,使用%!xxd以16进制方式浏览,可以看到文件的开头如下图:

flv文件的前9个字节

前9个字节0x46 | 0x4c | 0x56 | 0x01 | 0x05 | 0x00 | 0x00 | 0x00 | 0x09,前3个是FLV三个字符的ASCII码,之后的0x01表示版本号,0x05表示有音频也有视频,如果仅有音频,则是0x04,如果仅有视频,则是0x01,最后的4个字节0x00 0x00 0x00 0x09表示了这个header长度为9。

有了上面这些知识,我们可以仅仅读取文件的前9个字节,就可以判断这个视频是否是FLV,下面是一小段golang语言的代码:

package main

import (
    "fmt"
    "os"
    "encoding/binary"
    "bytes"
)

func main() {
    // 打开flv文件
    f, err := os.Open("test.flv")
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    // 读取前9个字节
    header := make([]byte, 9, 9)
    n, err := f.Read(header)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }

    if n != 9 {
        fmt.Println("read header length not equal 9")
        os.Exit(1)
    }

    // 验证
    if string(header[:3]) != "FLV" {
        fmt.Println("header not start with FLV")
        os.Exit(1)
    }

    var version uint8
    if err := binary.Read(bytes.NewReader(header[3:4], binary.BigEndian, &version); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    if version != 1 {
        fmt.Println("version not equal 1")
        os.Exit(1)
    }

    var flags uint8
    if err := binary.Read(bytes.NewReader(header[4:5], binary.BigEndian, &flags); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    switch flags {
    case 1, 4, 5:
        break;
    default:
        fmt.Println("invalid audio and video flags")
        os.Exit(1)
    }

    var offset uint32
    if err := binary.Read(bytes.NewReader(header[5:]), binary.BigEndian, &offset); err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    if offset != 9 {
        fmt.Println("invalid offset")
        os.Exit(1)
    }

    fmt.Println("this file is a flv")
}

得到FLV的头部并成功判断是FLV文件之后,就可以接收后面的数据了。后面的数据结构如下:

-------------------------
|  Previous Tag Size    |
-------------------------
|          Tag          |
-------------------------
|  Previous Tag Size    |
-------------------------
|          Tag          |
-------------------------
|  Previous Tag Size    |
-------------------------
|          Tag          |
-------------------------
|  Previous Tag Size    |
-------------------------

其中,每一段具体的数据叫做一个Tag,就如同上面图画中的一节节车厢;而每个Tag前面都会有四个字节用于表示前面的Tag大小,即Previous Tag Size。这个值可以用于验证数据的完整性,即播放器在接收完一个Tag后,可以再接收4个字节,得到前面的Tag的大小,判断是否一致。要知道,网络传输数据很容易发生数据丢失的情况。

Tag有三种类型,除了音频、视频,还有一种Script Data类型,音视频数据好理解,就是一段一段封装好的片段,Script Data这个类型的Tag一般都是作为第一个Tag出现。

每个Tag也包含了表示Tag信息的Header和具体数据的Data,结构如下所示:

-------------------------
|       Tag Header      |
-------------------------
|       Tag  Data       |
-------------------------

其中Tag Header由11个字节组成:

  • 第1个字节:表示类型,0x08表示音频,0x09表示视频,0x12表示Script Data;
  • 第2-4字节:表示当前Tag Data的长度;
  • 第5-7字节:表示Tag的时间戳,以毫秒为单位;
  • 第8字节:时间戳的扩展字节,用于扩充上面的时间戳;
  • 第9-11字节:表示Stream ID,总为0。

由此可见,读取了11个字节后,我们就知道了接下来的Tag Data是什么类型,对应的长度是多少(这个决定了接下来读取的数据中有多少数据是Tag Data的),对应的时间戳是多少(这个决定了播放的顺序),读完接下来的Tag Data之后,再通过Previous Tag Data来验证下数据的长度。

Tag Data中就是具体的音视频数据了,因为涉及到具体的音视频格式,这里就不一一展开了。

具体的分析FLV的代码,我使用golang写了一部分,放在了githubgo-flv,后续还会继续完善。