将碎片化的 MP4 转换为 MP4

Convert fragmented MP4 to MP4

我正在尝试从 trafficview.org 抓取视频帧,但似乎无法弄清楚如何解码数据。

我根据这方面的教程编写了几行代码 websocket_client 以访问实时流式 websocket 并直接接收消息。

我已经监控了通过 Chrome 上的网络选项卡传入的消息,并且还深入研究了下面代码的输出,并且我相当确定数据以碎片化的 MP4 形式流入。以下是前 100 个左右 byte/messages:

b'\xfa\x00\x02\x86\xf1B\xc0\x1e\x00\x00\x00\x18ftypiso5\x00\x00\x02\x00iso6mp41\x00\x00\x02jmoov\x00\x00\x00lmvhd\x00\x00\x00\x00\xdb\x7f\xeb\xb2\xdb\x7f\xeb\xb2\x00\x00\x03\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'

在整个输出中,有很多 moof 和 mdat 对。假设我让这段代码 运行 持续 30 秒,我怎样才能将这个原始字节字符串转换成 mp4 文件?

import json

from websocket import create_connection

url = 'wss://cctv.trafficview.org:8420/DDOT_CAPTOP_13.vod?progressive'

headers = json.dumps({
    'Accept-Encoding': 'gzip, deflate, br',
    'Accept-Language': 'en-US,en;q=0.9',
    'Cache-Control': 'no-cache',
    'Connection': 'Upgrade',
    'Host': 'cctv.trafficview.org:8420',
    'Origin': 'https://trafficview.org',
    'Pragma': 'no-cache',
    'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits',
    'Sec-WebSocket-Key': 'FzWbrsoHFsJWzvWGJ04ffw==',
    'Sec-WebSocket-Version': '13',
    'Upgrade': 'websocket',
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36',
})

ws = create_connection(url, headers=headers)

# Then send a message through the tunnel
ws.send('ping')

# Here you will view the message return from the tunnel
flag = 3000
output = b''
while flag > 0:
    output += ws.recv()
    flag -= 1

更新:我已经修改了 stack-overflow 上的一些代码,据说可以通过管道输入 fmp4 数据并将其转换为帧。为此,我注意到 websocket 输出的前 16 个字节与我检查过的其他 mp4 文件不一致。所以我先trim前16个字节。我也不知道这些文件中的一个应该如何结束,所以我 trim 直到文件的最后一个 moof。

下面的代码可以很好地读取 mp4 header(也在下面),但无法解码任何字节。

output = output[8:]

import re
moof_locs = [m.start() for m in re.finditer(b'moof', output)]

output = output[:moof_locs[-1]-1]

import subprocess as sp
import shlex

width, height = 640, 480

# FFmpeg input PIPE: WebM encoded data as stream of bytes.
# FFmpeg output PIPE: decoded video frames in BGR format.
process = sp.Popen(shlex.split('/usr/bin/ffmpeg -i pipe: -f hls -hls_segment_type fmp4 -c h264 -an -sn pipe:'), stdin=sp.PIPE, stdout=sp.PIPE, bufsize=10**8)
process.stdin.write(output)
process.stdin.close()
in_bytes = process.stdout.read(width * height * 3)
in_frame = (np.frombuffer(in_bytes, np.uint8).reshape([height, width, 3]))

ffmpeg 的输出:

[mov,mp4,m4a,3gp,3g2,mj2 @ 0x994600] Could not find codec parameters for stream 0 (Video: h264 (avc1 / 0x31637661), none, 640x480): unspecified pixel format
Consider increasing the value for the 'analyzeduration' and 'probesize' options
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'pipe:':
  Metadata:
    major_brand     : iso5
    minor_version   : 512
    compatible_brands: iso6mp41
    creation_time   : 2020-09-11T13:40:21.000000Z
  Duration: N/A, bitrate: N/A
    Stream #0:0(und): Video: h264 (avc1 / 0x31637661), none, 640x480, 1k tbr, 1k tbn, 2k tbc (default)
    Metadata:
      creation_time   : 2020-09-11T13:40:21.000000Z
      encoder         : EvoStream Media Server
Stream mapping:
  Stream #0:0 -> #0:0 (h264 (native) -> h264 (libx264))
Finishing stream 0:0 without any data written to it.
Nothing was written into output file 0 (pipe:), because at least one of its streams received no packets.
frame=    0 fps=0.0 q=0.0 Lsize=       0kB time=-577014:32:22.77 bitrate=  -0.0kbits/s speed=N/A    
video:0kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: unknown
Output file is empty, nothing was encoded (check -ss / -t / -frames parameters if used)

更新 2:

检查来自 websocket 的流后,我意识到每条消息都以一个特定的整数开头,该整数在 trafficview 的 javascript 代码中定义。这些代码的顺序总是相同的,它们如下所示:

Header MOOV (250)
    PBT Begin (249)
        Video Buffer (252)
        Header MOOF (251)
        Header MOOF (251)
        Header MOOF (251)
        Header MDAT (254)
    PBT End (255)

    PBT Begin (249)
    Continues Forever

其中一些标签始终相同,例如 249 条消息始终为 f900 0000,而 255 条消息始终为 ff00 0000。

我猜测 249 和 255 消息通常不在碎片化的 mp4 或 hls 流中,因此我认为我需要使用此标记信息来建立正确的从零开始的文件格式。

ws = create_connection(url, headers=headers)
# Then send a message through the tunnel
ws.send('ping')

start = timeit.default_timer()
flag = True
output = []
while flag:
    output.append(ws.recv())
    if timeit.default_timer() - start > 90:
        flag = False

result = output[0][8:]

for msg in output[1:]:
    if msg[0] == 249:
        moofmdat = b''
        moof = b''
        continue

    if msg[0] == 252:
        vidbuf = msg[4:]

    if msg[0] == 251:
        moof += msg[4:]

    if msg[0] == 254:
        mdat = msg[4:]

    if msg[0] == 255:
        moofmdat += moof
        moofmdat += mdat
        moofmdat += vidbuf
        result += moofmdat

with open('test.mp4', 'wb') as file:
    file.write(result)

想通了。 MOOV header 有 8 个字节的不必要信息必须删除。每条附加消息(除了 PBT_Begin 和 PBT_End 之外)都有 4 个字节的播放器特定数据。只需要清理每条消息并按正确的顺序放置。然后将原始字节保存为 mp4 和瞧,在 vlc 中播放的视频。