注意,本文待查证!

简介

最近在使用bilibili直播姬的时候,想在局域网同时观看保存下来的直播流,本文记录flv的文件格式,将保存下的flv文件作为直播流时的golang实现及踩的一些坑。

flv文件结构

本节引用来自众多文章:

flv文件结构简单,这里记录一下flv文件的基本结构,以便日后完善功能使用。

flv

flv头 flv内容
9 bytes ? bytes

flv头

签名 版本 媒体标识位 flv头长度
3 bytes 1 byte 1 byte 4 bytes

签名:固定使用FLV(0x46 0x4c 0x56)。

版本:当前版本号1(0x01)。

媒体标识位:

保留 音频标识 保留 视频标识
5 bits 1 bit 1 bit 1 bit

保留:必须是0

音频/视频标示:有则为1

flv头长度:必须为9(0x09)。

实例:

46 4C 56 01 05 00 00 00 09

flv内容

前一tag的长度 tag头 tag内容
4 bytes 11 bytes ? bytes

前一tag的长度:首tag为0(0x00 0x00 0x00 0x00),其他的为上一个tag头+tag内容的长度。

tag头:

tag信息 tag内容长度 相对时间戳 扩展时间戳 流id
1 byte 3 bytes 3 bytes 1 byte 3 bytes

tag信息:

保留 加密标识 tag类型
2 bits 1 bit 5 bits

保留:必须是0

加密标识:加密1、未加密0

tag类型:Script脚本1 0010、视频0 1001、音频0 1000

实例:

未加密的
    Script脚本`0x12`
    视频`0x09`
    音频`0x08`

tag内容长度:记录tag内容的长度。

相对时间戳:相对第一个tag的时间戳,单位毫秒。第一个Script脚本、视频、音频tag的时间戳为0(0x00 0x00 0x00)

扩展时间戳:当相对时间戳不够用时,可将此字节拼接到相对时间戳之前,以此进行扩展。

流id:固定为0(0x00 0x00 0x00)

实例:

12 00 02 AF 00 00 00 00 00 00 00
...
00 00 02 BA

tag内容

上述提到有3种tag。其中Script脚本是用来记录一些视频的基本信息,由于此处暂时没使用到,留待日后研究。

视频tag

tag类型为视频时,tag内容还具有视频头。

视频:

帧类型 编码类型 视频数据
4 bits 4 bits ? bits

帧类型:有以下5种。

  • 关键帧1(0x1)
  • 中间帧2(0x2)
  • 一次性中间帧3(0x3)
  • 生成关键帧4(0x4)
  • 视频信息帧5(0x5)

编码类型:有若干种详见官方文档,其中常用的有AVC H2647(0x7)。

视频数据:当编码类型为AVC时,其具有AVC头部,但此处暂未用到,日后补充。

实例:末尾的17

09 00 00 2D 00 00 00 00 00 00 00 17
音频tag

tag类型为音频时,tag内容还具有音频头。

音频:

音频格式 采样率 位宽 通道 音频数据
4 bits 2 bits 1 bit 1 bit ? bits

音频格式:有若干种,详见官方文档,其中常用的有AAC10(0xa)。

采样率:有以下4种。

  • 5.5kHz0(00)
  • 11kHz1(01)
  • 22kHz2(10)
  • 44kHz3(11)

位宽:8bit:0(0)、16bit:1(1)。

通道:单声道:0(0)、双声道:1(1)。

音频数据:当音频格式为AAC时,有AAC头,此处暂未用到,日后补充。

实例:末尾的AF

08 00 00 04 00 00 00 00 00 00 00 AF

golang转flv直播流

本节暂时仅针对bilibili直播流保存中再转直播流的情况,其他情况需要再做研究,如已下载完成的直播流转直播,会Read到许多EOF,暂时直接使用http.FileServer进行服务。

1
http.FileServer(http.Dir(base_dir)).ServeHTTP(w,r)

本节转换后的flv直播流在播放时会有音画不同步的情况,详见issues。后续会forkflv.js来尝试解决此问题。

根据对上述flv格式的了解,可以得出直播流的生成方法:获取最新的视频关键帧,并将它之前的音视频tag全部去除,直接拼接到第一个音/视频tag后即可。同时为了形成流,而不是变成直接下载,需要使用到Flush

flv处理

完整代码见flvDecode.go,内含简单注释,下面进行解释。以下代码还处于开发中,可能有不合理处不要介意,仅介绍思路。

首先打开文件:

1
2
3
4
5
6
//file
f,err := os.OpenFile(path,os.O_RDONLY,0644)
if err != nil {
    return err
}
defer f.Close()

复制flv头及首个前一个tag大小,其中streamChan为一个golang channel,用于后续从中读取数据并发送到客户端。

1
2
3
4
5
6
7
//get flv header(9byte) + FirstTagSize(4byte)
{
    buf := make([]byte, flv_header_size+previou_tag_size)
    if _,err := f.Read(buf);err != nil {return err}
    if bytes.Index(buf,flv_header_sign) != 0 {return errors.New(`no flv`)}
    streamChan <- buf
}

其中flv_header_size=9previou_tag_size=4flv_header_sign=[]byte{0x46,0x4c,0x56}是根据flv格式标准定义的值,分别表示flv头长度、前一tag长度、flv签名FLV

接着获取Script脚本tag、第一个视频tag、第一个音频tag。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
for {
    t := getTag(f)
    if t.Tag == script_tag {
        streamChan <- *t.Buf
    } else if t.Tag == video_tag {
        if !first_video_tag {
            first_video_tag = true
            streamChan <- *t.Buf
        }

        if t.FirstByte & 0xf0 == 0x10 {
            if len(last_keyframe_video_offsets) > 2 {
                // last_timestamps = append(last_timestamps[1:], t.Timestamp)
                last_keyframe_video_offsets = append(last_keyframe_video_offsets[1:], t.Offset)
            } else {
                // last_timestamps = append(last_timestamps, t.Timestamp)
                last_keyframe_video_offsets = append(last_keyframe_video_offsets, t.Offset)
            }
        }
    } else if t.Tag == audio_tag {
        if !first_audio_tag {
            first_audio_tag = true
            streamChan <- *t.Buf
        }
    } else {//eof_tag 
        break;
    }
}

其中

script_tag=0x12video_tag=0x09audio_tag=0x08为三种tag的flv定义的字节串。

first_video_tagfirst_audio_tag为局部bool变量,获取到第一个视频、音频tag后设为true。不再将后续视频,音频tag发送给客户。

last_keyframe_video_offsets为局部int64数组,用来储存最后几个关键帧的位置。int64是由f.Seek返回的位置是int64类型决定的。

t.FirstByte & 0xf0 == 0x10t.FirstByte是视频头,通过位相与,为真时即为关键帧。

这里使用了getTag方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
type flv_tag struct {
    Tag byte
    Offset int64
    Timestamp int32
    PreSize int32
    FirstByte byte
    Buf *[]byte
}

//get tag func
var getTag = func(f *os.File)(t flv_tag){
    t.Offset,_ = f.Seek(0,1)//获取当前tag位置
    Buf := []byte{}
    t.Buf = &Buf//返回指针避免再次分配内存

    buf := make([]byte, tag_header_size)//读取tag头
    if size,err := f.Read(buf);err != nil || size == 0 {
        t.Tag = eof_tag//eof_tag为读取到结尾的标志,或许当前tag还没下载完成
        return
    }
    Buf = append(Buf, buf...)//将tag头储存到buf中,准备返回
    t.Tag = buf[0]//获取当前tag类型
    t.Timestamp = F.Btoi32([]byte{buf[7],buf[4],buf[5],buf[6]},0)//获取当前tag时间戳,Btoi32为4为字节转int32

    size := F.Btoi32(append([]byte{0x00},buf[1:4]...),0)//获取tag内容长度

    data := make([]byte, size)
    if size,err := f.Read(data);err != nil || size == 0 {
        t.Tag = eof_tag
        return
    }
    t.FirstByte = data[0]//获取当前内容的第一个字节,视频头/音频头

    pre_tag := make([]byte, previou_tag_size)//前一tag长度
    if size,err := f.Read(pre_tag);err != nil || size == 0 {
        t.Tag = eof_tag
        return
    } 
    t.PreSize = F.Btoi32(pre_tag,0)//前一tag长度
    
    Buf = append(Buf, append(data, pre_tag...)...)
    // if t.PreSize == 0{fmt.Println(t.Tag,size,data[size:])}

    return
}

将当前位置定位到最后几个关键帧

1
2
3
//seed to the second last tag
if len(last_keyframe_video_offsets) == 0 {flvlog.L(`W: `,`no keyframe`);return errors.New(`no keyframe`)}
f.Seek(last_keyframe_video_offsets[0],0)

下面进行对tag逐个进行复制

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//copy
{
    last_available_offset := last_keyframe_video_offsets[0]//最后一个可用tag,万一发生错误,将回溯到此位置

    // last_Timestamp := last_timestamps[0]
    for {
        //退出
        select {
        case <-cancel:return nil;//客户退出了
        default:;
        }
        t := getTag(f)//逐个获取tag
        if t.Tag == eof_tag {//获取到了个残缺tag
            f.Seek(last_available_offset,0)
            time.Sleep(time.Second)
            continue
        } else if t.PreSize == 0 {//这是个无效的tag,或许读取发生了错误
            f.Seek(last_available_offset,0)
            f.Seek(seachtag(f),1)//seachtag返回可用tag位置
            continue
        } else if t.Tag == video_tag {
            // if t.FirstByte & 0xf0 == 0x10 {
            // 	video_keyframe_speed = t.Timestamp - last_video_keyframe_timestramp
            // 	fmt.Println(`video_keyframe_speed`,video_keyframe_speed)
            // 	last_video_keyframe_timestramp = t.Timestamp
            // }
            streamChan <- *t.Buf
        } else if t.Tag == audio_tag {
            streamChan <- *t.Buf
        } else if t.Tag != script_tag {//不复制Script脚本tag
            ;
        }
        
        last_available_offset = t.Offset//刷新最后可用tag

    }
}

http部分

完整代码见Reply/F.go,其中的核心部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
flusher, flushSupport := w.(http.Flusher);
if flushSupport {flusher.Flush()}//先进行这步,否则将可能会变成直接下载

var (
    err error
)
for err == nil {
    if b := <- byteC;len(b) != 0 {
        _,err = w.Write(b)
    } else {
        break
    }
    if flushSupport {flusher.Flush()}
}