使用Python解析FLV文件的Scripts Tag

前言

前文链接:Python实现FLV视频拼接

为了拼接出一个没有瑕疵的 FLV 视频,仅修改音视频数据的时间戳还是不够的。为此,我们需要先对 FLV 视频的关键 Tag ——Scripts Tag 有一个较深的认识,然后便可以合理地修改 Scripts Tag。

解析FLV文件的Scripts Tag

首先,Scripts Tag 是由头部,主体和尾部组成,头部和尾部的结构十分简单,重点部分是主体。

正如 FLV 文件的数据是分块的,Scripts Tag 主体里的数据也是分块的。数据块有大有小,层层嵌套,无论怎么样变化,数据块的结构都可以表示为 A + (B) + C:

A:块的类型,1 字节,常见值有 0 (数字),1 (布尔值),2 (字符串),3 (对象),8 (字典),10 (数组);
B:字符串的长度( 2 字节)或数组的元素个数( 4 字节),不一定有,取决于块的类型;
C:块的数据。

Scripts Tag 主体通常只有 2 个数据块,主要信息都包括在第 2 个数据块中。
第 1 个数据块如下:

b'\x02\x00\nonMetaData'

A = \x02 = 2,这是个字符串数据块,B 存在,且 B 为 2 个字节
B = \x00\n = 10,表示字符串的长度为 10 字节
C = onMetaData

第 2 个数据块( C 很长,暂不列出):

b'\x08\x00\x00\x00\x1a' + C

A = \x08 = 8,这是个字典数据块,B 存在,且 B 为 4 个字节
B = \x00\x00\x00\x1a = 26,该值表示 C 含有 26 对键值
C:接下来解析

以 C 中含有的 2 个键值对作为解析示例:

b'\x00\x0bdescription\x02\x00\x12This is an example'

字典数据块中,因为每个键都是字符串,故其没有添加类型,前 2 个字节就直接表示字符串的长度,
\x00\x0b = 11,键 = description ,
接下来的字节则是键所对应的值,这是一个标准的数据块,
显而易见,A = \x02,B = \x00\x12,C = This is an example 。

-------------------------------------------------------------------------------------------------
b'\x00\x08duration\x00@kd\xc4\x9b\xa5\xe3T'

同理,\x00\x08 = 8,键 = duration ,该键对应的值,是一个数字数据块,
A = \x00,B 不存在,C 为 8 字节 Double 数,C = @kd\xc4\x9b\xa5\xe3T = 219.149 。

在了解上述内容后,基本上就可以解析出 Scripts Tag 里的信息了。

完整代码

import struct

class Reader(): # 阅读器,可以使我们方便地读取数据
    def __init__(self, content):
        self.content = content
        self.start = 0
        self.eof = False
        self.length = len(self.content)
        
    def read(self, n=1):
        if self.length > (self.start + n):
            out = self.content[self.start:self.start + n]
            self.start += n
        else:
            out = self.content[self.start:]
            self.eof = True
        return out


class ScriptsTag(Reader): # 解析 Scripts Tag 的类
    def begin(self):
        while not self.eof:
            type_ = ord(self.read(1))
            print(f'   ', end='')
            self.parse(type_)
    
    def parse(self, type_, end='\n'): # print 函数里的空格只是为了规范化输出,不必深究
        print(f' ({type_}) ', end='')
        
        if type_ == 0: # number
            value = struct.unpack('>d', self.read(8))[0] # 将 8 字节 Double 数转化成十进制数
            print(value, end=end)
            
        elif type_ == 1: # boolean 布尔数据块的结构为: A + C ( 1 字节)
            value = ord(self.read(1))
            print(value, end=end)
            
        elif type_ == 2: # string
            length = int.from_bytes(self.read(2), 'big')
            value = self.read(length).decode()
            print(value, end=end)
            
        elif type_ == 3: # object 对象数据块的结构和字典数据块的相似
            while not self.eof:
                print('\n\t   ', end='')
                self.parse(2, '')
                valueType = ord(self.read(1))
                self.parse(valueType)
            
        elif type_ == 9: # object end marker
            value = self.read(0).decode()
            print(value, end=end)
            
        elif type_ == 8: # ecma array 
            numElements = int.from_bytes(self.read(4), 'big')
            print(f'{numElements}')
            for i in range(numElements):
                keyLength = int.from_bytes(self.read(2), 'big')
                key = self.read(keyLength).decode()
                valueType = ord(self.read(1))
                print(f'        {key}:', end='')
                self.parse(valueType)
                
        elif type_ == 10: # strict array 数组数据块,其结构和字典数据块相似,只有值没有键
            numElements = int.from_bytes(self.read(4), 'big')
            print(f'{numElements}')
            for i in range(numElements):
                print(f'\t       ', end='')
                valueType = ord(self.read(1))
                self.parse(valueType)
                
        else:
            print(f'Unresloved type {type_}')
      
      
def parse_flv(flv): # 主函数
    with open(flv, 'rb') as f:
        content = f.read()
    reader = Reader(content)

    header = reader.read(13)

    while not reader.eof:
        tagHeader = reader.read(11)
        tagData = reader.read(int.from_bytes(tagHeader[1:4], 'big'))
        tagSize = reader.read(4)

        dataSize = int.from_bytes(tagHeader[1:4], 'big')
        timeStamp = int.from_bytes(tagHeader[4:7], 'big')
        
        # 判断 Tag 类型。注意,从字节串中截取的单个值为整数,故 tagHeader[0] 为 18,而不是 b'\x12'
        if tagHeader[0] == 18:
            tagType = 'scripts'
            print(tagType, dataSize, timeStamp)
            ScriptsTag(tagData).begin()
        elif tagHeader[0] == 9:
            tagType = 'video'
            # print(tagType, dataSize, timeStamp)
        elif tagHeader[0] == 8:
            tagType = 'audio'
            # print(tagType, dataSize, timeStamp)
        else:
            print('This may NOT be a .flv file!')
            return 

输出样例

样例很长,有大幅度删减。

scripts 2402 0
    (2) onMetaData
    (8) 26
        hasVideo: (1) 1
        duration: (0) 382.4
        datasize: (0) 45991684.0
        videosize: (0) 41086520.0
        framerate: (0) 23.981254347746496
        videodatarate: (0) 837.3441177432009
        width: (0) 1280.0
        height: (0) 720.0
        ......
        keyframes: (3) 
	    (2) filepositions (10) 99
	        (0) 2430.0
	        (0) 2510.0
	        ......
	        (0) 30690020.0
	        (0) 30828476.0

Scripts Tag 的数据解析后还是比较容易看出每个值的含义。在这里说一下, keyframes 表示的是关键帧,即我们观看视频快进时的每个时间节点。

扫描二维码关注公众号,回复: 8777124 查看本文章

如果我的文章对您有帮助,还请您点个赞,谢谢!

发布了2 篇原创文章 · 获赞 2 · 访问量 1636

猜你喜欢

转载自blog.csdn.net/Okery/article/details/104064251
tag