前文链接:Python实现FLV视频拼接
为了拼接出一个没有瑕疵的 FLV 视频,仅修改音视频数据的时间戳还是不够的。为此,我们需要先对 FLV 视频的关键 Tag ——Scripts Tag 有一个较深的认识,然后便可以合理地修改 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 表示的是关键帧,即我们观看视频快进时的每个时间节点。
如果我的文章对您有帮助,还请您点个赞,谢谢!