Files
RetroArch/samples/gfx/gfx_thumbnail_preview/mkfixtures.py
T
LibretroAdmin a4172fdafd CI: the lanes Ubuntu 26.04 broke, and the formats warnings
Built with ffmpeg 8, libaom 3.13, x265 4.1, glibc 2.43 and clang 21,
five lanes failed on the 26.04 runners:

- AV1 fixtures: a monochrome sequence comes out full range with no
  matrix whatever is asked for; the oracle takes the colour ffprobe
  reads instead of assuming it.
- Preview audio oracle: ffmpeg 8 leaves a timestamp gap at every seam
  of a -stream_loop copy, which the mixer honours and ffmpeg's raw
  decode does not. trailing_large loops its picture and encodes its
  sound as one track.
- rh265 multi-slice oracle: x265 4.1 writes unreadable slice headers
  when two slices are encoded on several threads; those cases encode
  on one worker.
- vkms: the kernel follows the runner's own release, its modules may
  sit under /usr/lib/modules, and modules-extra is fetched only where
  it exists.
- clang -std=c89 -pedantic: glibc's const-generic strchr/strstr under
  _GNU_SOURCE are _Generic; -Wno-c11-extensions goes wherever
  -Wno-c99-extensions already does.

ci_paths filters a file at the top of a samples tree by its own path.
rh264 and rh265 lose the parameters no instantiation reads, the 8-bit
H.264 kernels mark the depth they ignore, and the AV1 WebM fixtures
leave out the MP4 box writer they never use.
2026-11-05 17:36:02 +01:00

536 lines
23 KiB
Python

#!/usr/bin/env python3
"""Build the MP4 layouts the preview path has to survive.
Every fault this sample pins came from a container LAYOUT, not from a
codec: where the moov sits relative to the media, whether the media
starts past 4 GiB, whether the moov fits the resident head. So the
fixtures vary exactly that, and nothing else.
The multi-gigabyte ones are sparse: a 64-bit `mdat` header with a hole
in the middle, so a 7.28 GB fixture occupies ~40 MB on disk and costs
nothing to generate in CI. Sample offsets are rewritten to co64 where
the pad pushes them past 4 GiB.
Needs ffmpeg for the seed clip; everything after that is box surgery.
"""
import os
import json
import struct
import subprocess
import sys
from webm_audio_padding import pad as pad_webm_audio, insert_void
CONTAINERS = {b'moov', b'trak', b'mdia', b'minf', b'stbl', b'edts', b'udta'}
def parse(buf, start, end):
out, pos = [], start
while pos + 8 <= end:
sz = struct.unpack('>I', buf[pos:pos + 4])[0]
typ = buf[pos + 4:pos + 8]
hdr = 8
if sz == 1:
sz = struct.unpack('>Q', buf[pos + 8:pos + 16])[0]
hdr = 16
elif sz == 0:
sz = end - pos
if sz < hdr or pos + sz > end:
break
out.append((pos, sz, typ, hdr))
pos += sz
return out
def rewrite(buf, start, end, shift):
"""Shift every chunk offset, promoting stco to co64."""
parts = []
for (pos, sz, typ, hdr) in parse(buf, start, end):
body = buf[pos + hdr:pos + sz]
if typ in (b'stco', b'co64'):
w = 4 if typ == b'stco' else 8
fmt = '>I' if w == 4 else '>Q'
n = struct.unpack('>I', body[4:8])[0]
offs = [struct.unpack(fmt, body[8 + w * i:8 + w * (i + 1)])[0]
+ shift for i in range(n)]
nb = body[0:4] + struct.pack('>I', n) \
+ b''.join(struct.pack('>Q', o) for o in offs)
parts.append(struct.pack('>I', len(nb) + 8) + b'co64' + nb)
elif typ in CONTAINERS:
nb = rewrite(buf, pos + hdr, pos + sz, shift)
parts.append(struct.pack('>I', len(nb) + 8) + typ + nb)
else:
parts.append(buf[pos:pos + sz])
return b''.join(parts)
def boxes(path):
buf = open(path, 'rb').read()
return buf, {t: (p, s, h) for (p, s, t, h) in parse(buf, 0, len(buf))}
def audio_large_packet(src, dst, packet_bytes=3 * 1024 * 1024):
"""Audio-only test: a video sample larger than the old audio window."""
buf, bx = boxes(src)
mp, ms, mh = bx[b'mdat']
vp, vs, vh = bx[b'moov']
first = [None, None]
def find(start, end, video=False):
for pos, sz, typ, hdr in parse(buf, start, end):
body = buf[pos + hdr:pos + sz]
if typ in CONTAINERS:
find(pos + hdr, pos + sz,
b'vide' in body if typ == b'trak' else video)
elif video and typ == b'stco':
first[0] = struct.unpack_from('>I', body, 8)[0]
elif video and typ == b'stsz':
first[1] = struct.unpack_from('>I', body, 12)[0]
find(vp + vh, vp + vs)
cut = first[0] + first[1]
pad = packet_bytes - first[1]
def grow(start, end, video=False):
parts = []
for pos, sz, typ, hdr in parse(buf, start, end):
body = bytearray(buf[pos + hdr:pos + sz])
if typ in CONTAINERS:
body = grow(pos + hdr, pos + sz,
b'vide' in body if typ == b'trak' else video)
elif typ == b'stco':
count = struct.unpack_from('>I', body, 4)[0]
for i in range(count):
off = struct.unpack_from('>I', body, 8 + i * 4)[0]
if off >= cut:
struct.pack_into('>I', body, 8 + i * 4, off + pad)
elif video and typ == b'stsz':
struct.pack_into('>I', body, 12, first[1] + pad)
parts.append(struct.pack('>I', len(body) + 8) + typ + body)
return b''.join(parts)
# Unreferenced media padding keeps the fixture's bitrate-sized window
# above the large sample; the AAC track itself is unchanged.
tail = 4 * 1024 * 1024
moov = grow(vp + vh, vp + vs)
with open(dst, 'wb') as out:
out.write(buf[:mp] + struct.pack('>I', ms + pad + tail) + b'mdat')
out.write(buf[mp + mh:cut] + bytes(pad) + buf[cut:mp + ms])
out.write(bytes(tail) + struct.pack('>I', len(moov) + 8) + b'moov' + moov)
def seed(dst, secs, w, h, vbr):
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=%dx%d:r=30' % (w, h),
'-f', 'lavfi', '-i', 'sine=f=440:r=48000',
'-t', str(secs), '-c:v', 'libx264', '-preset', 'ultrafast',
'-b:v', vbr, '-pix_fmt', 'yuv420p',
'-c:a', 'aac', '-b:a', '64k', '-ac', '2', dst])
def trailing_padded(src, dst, target):
"""Trailing moov, media before a sparse pad -> moov past 4 GiB."""
buf, bx = boxes(src)
mp, ms, mh = bx[b'mdat']
vp, vs, vh = bx[b'moov']
payload = buf[mp + mh:mp + ms]
pad = target - (mp + 16 + len(payload) + vs)
if pad <= 0:
raise SystemExit('target too small')
shift = (16 - mh) + pad
newmoov = rewrite(buf, vp + vh, vp + vs, shift)
newmoov = struct.pack('>I', len(newmoov) + 8) + b'moov' + newmoov
with open(dst, 'wb') as o:
o.write(buf[:mp])
o.write(struct.pack('>I', 1) + b'mdat'
+ struct.pack('>Q', 16 + pad + len(payload)))
o.seek(mp + 16 + pad)
o.write(payload)
o.write(newmoov)
def leading_padded(src, dst, target):
"""Leading moov, media after a sparse pad -> media past 4 GiB."""
subprocess.check_call(['ffmpeg', '-v', 'error', '-y', '-i', src,
'-c', 'copy', '-movflags', '+faststart',
dst + '.fs.mp4'])
buf, bx = boxes(dst + '.fs.mp4')
fp, fs, fh = bx[b'ftyp']
vp, vs, vh = bx[b'moov']
mp, ms, mh = bx[b'mdat']
payload = buf[mp + mh:mp + ms]
# stco -> co64 grows the moov, and the grown size feeds back into
# where the media lands: size it once with a dummy shift, then use
# that length to place everything.
probe = rewrite(buf, vp + vh, vp + vs, 0)
probe = struct.pack('>I', len(probe) + 8) + b'moov' + probe
moov_end = fs + len(probe)
pad = target - (moov_end + 16 + len(payload))
if pad <= 0:
raise SystemExit('target too small')
shift = (moov_end + 16 + pad) - (mp + mh)
newmoov = rewrite(buf, vp + vh, vp + vs, shift)
newmoov = struct.pack('>I', len(newmoov) + 8) + b'moov' + newmoov
if len(newmoov) != len(probe):
raise SystemExit('moov size unstable under rewrite')
with open(dst, 'wb') as o:
o.write(buf[fp:fp + fs])
o.write(newmoov)
o.write(struct.pack('>I', 1) + b'mdat'
+ struct.pack('>Q', 16 + pad + len(payload)))
o.seek(moov_end + 16 + pad)
o.write(payload)
os.unlink(dst + '.fs.mp4')
def anim_webp(dst, secs, w, h, fps):
"""Lossless animated WebP, one ANMF per frame. Lossless so the
frames are large (hundreds of KB at 720p): the windowed-open test
needs a file well past the head + lookahead + margin the feeder is
allowed to keep resident, or a whole-file load would be
indistinguishable from a windowed one by RSS."""
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=%dx%d:r=%d' % (w, h, fps),
'-t', str(secs), '-c:v', 'libwebp_anim', '-lossless', '1',
'-compression_level', '0', '-loop', '0', dst])
def anim_webp_big_frame(src, dst, frame_bytes):
"""Rewrite an animated WebP so its SECOND frame is 'frame_bytes'
long: the frame's VP8L payload is preceded by an unknown sub-chunk
of padding, which the frame decoder skips. The frame then spans
more than the feeder's lookahead, which is the case next_span
exists for - without it the window could never cover the frame and
the animation would sit at the wall forever."""
b = bytearray(open(src, 'rb').read())
pos, n = 12, 0
while pos + 8 <= len(b):
tag = bytes(b[pos:pos + 4])
sz = struct.unpack('<I', b[pos + 4:pos + 8])[0]
if tag == b'ANMF':
n += 1
if n == 2:
pad = frame_bytes - sz
if pad < 8:
raise SystemExit('frame already that large')
pad -= 8
pad += pad & 1
chunk = b'PADx' + struct.pack('<I', pad) + bytes(pad)
ins = pos + 8 + 16 # after the 16-byte ANMF header
b[ins:ins] = chunk
b[pos + 4:pos + 8] = struct.pack('<I', sz + len(chunk))
b[4:8] = struct.pack('<I', len(b) - 8)
break
pos += 8 + sz + (sz & 1)
open(dst, 'wb').write(b)
def still_webp(dst, w, h):
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=%dx%d:r=1' % (w, h),
'-frames:v', '1', '-c:v', 'libwebp', '-lossless', '1',
'-compression_level', '0', dst])
def apng(dst, w, h, frames, fps):
"""APNG: the same shape as the WebP - large lossless frames, past the
window the feeder may keep. Written here rather than by ffmpeg, whose
APNG encoder (6.1) keeps only the rectangles that change between
frames: from 30 s of testsrc2 it made 33 frames and 3.4 MiB, inside
the window, and the windowed test had nothing to window. The frames
are seeded noise, which no encoder shrinks, so the size is the frame
count times the frame and the same on every host."""
import random
import zlib
def chunk(t, body):
return (struct.pack('>I', len(body)) + t + body
+ struct.pack('>I', zlib.crc32(t + body) & 0xffffffff))
rng = random.Random(0x5eed)
row = w * 3
delay = struct.pack('>HH', 1, fps)
seq = 0
out = [b'\x89PNG\r\n\x1a\n',
chunk(b'IHDR', struct.pack('>IIBBBBB', w, h, 8, 2, 0, 0, 0)),
chunk(b'acTL', struct.pack('>II', frames, 0))]
for f in range(frames):
noise = rng.getrandbits(8 * row * h).to_bytes(row * h, 'little')
raw = b''.join(b'\x00' + noise[y * row:(y + 1) * row]
for y in range(h))
data = zlib.compress(raw, 1)
out.append(chunk(b'fcTL', struct.pack('>IIIII', seq, w, h, 0, 0)
+ delay + b'\x00\x00'))
seq += 1
if f == 0:
out.append(chunk(b'IDAT', data))
else:
out.append(chunk(b'fdAT', struct.pack('>I', seq) + data))
seq += 1
out.append(chunk(b'IEND', b''))
with open(dst, 'wb') as fp:
fp.write(b''.join(out))
def apng_dispose_previous(dst):
"""A tiny hand-built APNG exercising DISPOSE_PREVIOUS with
alpha-blended sub-frames: the region a frame saves for restoring
later lives outside the canvas, and a channel-order switch must
convert it too, or the restore stamps the old order back in."""
import zlib
def chunk(t, body):
return (struct.pack('>I', len(body)) + t + body
+ struct.pack('>I', zlib.crc32(t + body) & 0xffffffff))
def raw(w, h, pix):
rows = b''
for y in range(h):
rows += b'\x00' + b''.join(struct.pack('BBBB', *pix(x, y))
for x in range(w))
return zlib.compress(rows)
W = H = 8
seq = [0]
def fctl(w, h, x, y, dispose, blend):
s = struct.pack('>IIIIIHHBB', seq[0], w, h, x, y, 1, 10,
dispose, blend)
seq[0] += 1
return chunk(b'fcTL', s)
def fdat(data):
s = struct.pack('>I', seq[0]) + data
seq[0] += 1
return chunk(b'fdAT', s)
out = b'\x89PNG\r\n\x1a\n'
out += chunk(b'IHDR', struct.pack('>IIBBBBB', W, H, 8, 6, 0, 0, 0))
out += chunk(b'acTL', struct.pack('>II', 4, 0))
# frame 0: full canvas, opaque gradient, dispose NONE (it is what
# frame 1's PREVIOUS restores, so it must stay on the canvas)
out += fctl(W, H, 0, 0, 0, 0)
out += chunk(b'IDAT', raw(W, H, lambda x, y: (x * 30, y * 30, 200, 255)))
# frame 1: 4x4 patch at (2,2), half-transparent, blend OVER, dispose PREVIOUS
out += fctl(4, 4, 2, 2, 2, 1)
out += fdat(raw(4, 4, lambda x, y: (250, 20 + x * 40, 20 + y * 40, 128)))
# frame 2: 4x4 patch at (0,4), opaque-ish, blend OVER, dispose NONE
out += fctl(4, 4, 0, 4, 0, 1)
out += fdat(raw(4, 4, lambda x, y: (10, 240, 60 + x * 20, 200)))
# frame 3: 2x2 patch, blend SOURCE
out += fctl(2, 2, 5, 1, 0, 0)
out += fdat(raw(2, 2, lambda x, y: (90, 90, 250, 90)))
out += chunk(b'IEND', b'')
open(dst, 'wb').write(out)
def make_aac_group(out):
"""Small AAC case can be refreshed without the large image fixtures."""
os.makedirs(out, exist_ok=True)
j = lambda n: os.path.join(out, n)
# Add the small new container case independently of the cached large
# fixture set. AAC in Matroska has CodecDelay but no Opus pre-skip.
if not os.path.exists(j('audio_group_aac.mkv')):
seed = j('audio_group_aac_seed.mkv')
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y', '-f', 'lavfi', '-i',
'testsrc2=size=64x48:rate=12:duration=4', '-f', 'lavfi', '-i',
'sine=frequency=440:sample_rate=48000:duration=4',
'-ac', '2', '-c:v', 'libvpx-vp9', '-g', '12', '-auto-alt-ref', '0',
'-c:a', 'aac', seed])
insert_void(seed, j('audio_group_aac.mkv'), after_picture=13, in_group=True)
os.unlink(seed)
def main():
out = sys.argv[1] if len(sys.argv) > 1 else '.'
os.makedirs(out, exist_ok=True)
j = lambda n: os.path.join(out, n)
BIG = 7817178301 # the size that started all of this
make_aac_group(out)
# The workflow rebuilds the fixtures before each sanitizer pass so
# a clean cannot leave the run globbing nothing; the outputs are
# deterministic, so an existing set is reused rather than encoded
# again (the lossless animations are the expensive part).
if all(os.path.exists(j(n)) for n in ('long_video.mp4', 'anim_lossless.webp',
'still_lossless.webp', 'anim_lossless.png',
'anim_dispose_prev.png', 'trailing_large.mp4',
'trailing_huge.mp4', 'leading_huge.mp4', 'trailing_small.mp4',
'audio_large_packet.m4a', 'audio_seek_gap.m4a',
'audio_group_opus.webm', 'audio_group_vorbis.webm', 'audio_group_aac.mkv',
'video_gap_head.webm', 'video_gap_pin.webm', 'video_group.webm',
'vp9_tiles.webm', 'hevc_wpp.mp4', 'bframes_h264.mp4',
'bframes_hevc.mp4', 'hevc_nowpp.mp4', 'mbaff_h264.mp4')):
print('fixtures present, not rebuilt')
return
# Frame-indexed animations for the windowed open, each just past
# what the feeder may keep resident (head + lookahead + margin +
# two huge pages, ~27 MB): lossless 720p frames, the WebP with a
# second frame padded past the feeder's lookahead, plus a still
# WebP the probe must reject from its first chunk. Kept as small
# as the checks allow - every byte is decoded three times under
# the sanitizers.
anim_webp(j('anim_lossless_base.webp'), 17, 1280, 720, 10)
anim_webp_big_frame(j('anim_lossless_base.webp'),
j('anim_lossless.webp'), 12 * 1024 * 1024)
os.unlink(j('anim_lossless_base.webp'))
still_webp(j('still_lossless.webp'), 1920, 1080)
apng(j('anim_lossless.png'), 1280, 720, 14, 10)
apng_dispose_previous(j('anim_dispose_prev.png'))
# A video longer than the feeder's fixed window span (4+8+8 MiB),
# for the bitrate-sized feed check: 60 s at 4 Mbit/s is 30 MB.
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=640x360:r=30', '-t', '60',
'-c:v', 'libx264', '-preset', 'ultrafast', '-b:v', '4M',
'-pix_fmt', 'yuv420p', '-an', '-movflags', '+faststart',
j('long_video.mp4')])
# VP9 in four tile columns (1280 px wide, tile-columns 2), for the
# threaded-decode oracle: tile columns are what the decoder spreads
# over threads, and a frame coded as one column would not exercise
# it. A second of test pattern, inter frames included.
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=1280x360:r=30', '-t', '1',
'-c:v', 'libvpx-vp9', '-tile-columns', '2', '-lag-in-frames', '0',
'-b:v', '400k', '-pix_fmt', 'yuv420p', '-an',
j('vp9_tiles.webm')])
# HEVC with wavefront parallel processing (x265's default), for the
# same oracle: the CTB rows are what the decoder spreads over
# threads. A second of test pattern, six CTB rows at 360 lines.
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=640x360:r=30', '-t', '1',
'-c:v', 'libx265', '-x265-params', 'wpp=1:log-level=none',
'-b:v', '400k', '-pix_fmt', 'yuv420p', '-tag:v', 'hvc1', '-an',
j('hevc_wpp.mp4')])
# Two seconds with three non-reference B-frames between every
# pair of references and no B-pyramid, so about two thirds of the
# pictures are ones nothing predicts from. The catch-up oracle
# drops those and checks the pictures it still shows are the ones
# a full decode shows, byte for byte.
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=160x120:r=30', '-t', '2',
'-c:v', 'libx264', '-preset', 'veryfast', '-bf', '3',
'-b-pyramid', 'none', '-g', '30', '-x264-params', 'b-adapt=0',
'-pix_fmt', 'yuv420p', '-an',
j('bframes_h264.mp4')])
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=160x120:r=30', '-t', '2',
'-c:v', 'libx265', '-x265-params',
'bframes=3:b-pyramid=0:b-adapt=0:keyint=30:log-level=none',
'-pix_fmt', 'yuv420p', '-tag:v', 'hvc1', '-an',
j('bframes_hevc.mp4')])
# HEVC without the wavefront, taller than one CTB row and with a
# partial last row, SAO and deblocking on: the decoder's row hook
# runs on this one - the loop filters a CTB row behind the decode
# - which the WPP fixtures never exercise.
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=320x200:r=30', '-t', '2',
'-c:v', 'libx265', '-x265-params',
'wpp=0:frame-threads=1:sao=1:deblock=1:bframes=3:b-pyramid=1:keyint=30:log-level=none',
'-pix_fmt', 'yuv420p', '-tag:v', 'hvc1', '-an',
j('hevc_nowpp.mp4')])
# Interlaced H.264 (x264's MBAFF), B-frames and CABAC: pair
# scanning in the decoder, and with pictures in flight the rows of
# a MBAFF picture published only at its completion.
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y',
'-f', 'lavfi', '-i', 'testsrc2=s=320x240:r=30', '-t', '2',
'-c:v', 'libx264', '-preset', 'veryfast', '-bf', '3', '-g', '30',
'-x264-params', 'tff=1:b-adapt=0',
'-pix_fmt', 'yuv420p', '-an',
j('mbaff_h264.mp4')])
# Prepared WebM audio: both codecs, including pause-before-adoption.
for codec, name in [('libopus', 'audio_seek_opus.weba'),
('libvorbis', 'audio_seek_vorbis.weba')]:
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y', '-f', 'lavfi', '-i',
'sine=frequency=440:sample_rate=48000:duration=16',
'-ac', '2', '-c:a', codec, '-f', 'webm',
'-cluster_time_limit', '1000', j(name)])
# Interleaved blocks beyond the audio window's permanent head. The
# padded video payload is only for demux/audio residency acceptance.
for codec in ['opus', 'vorbis']:
seed_path = j('audio_seek_' + codec + '_seed.webm')
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y', '-f', 'lavfi', '-i',
'testsrc2=size=64x48:rate=12:duration=4',
'-f', 'lavfi', '-i',
'sine=frequency=440:sample_rate=48000:duration=4',
'-ac', '2', '-c:v', 'libvpx-vp9', '-g', '12', '-auto-alt-ref', '0',
'-c:a', 'lib' + codec, seed_path])
pad_webm_audio(seed_path, j('audio_seek_' + codec + '_window.webm'))
insert_void(seed_path, j('audio_group_' + codec + '.webm'),
after_picture=13, in_group=True)
os.unlink(seed_path)
# Legal padding gaps both inside and beyond the permanent head. Noise
# keeps picture forty's coded input outside that head, so the second
# fixture exercises an actual moving-window packet pin.
noise_seed = j('video_gap_seed.webm')
subprocess.check_call([
'ffmpeg', '-v', 'error', '-y', '-f', 'lavfi', '-i',
'testsrc2=size=640x360:rate=30:duration=3',
'-vf', 'noise=alls=100:allf=t+u', '-c:v', 'libvpx',
'-deadline', 'realtime', '-cpu-used', '8', '-qmin', '1', '-qmax', '1',
'-b:v', '100M', '-g', '30', '-auto-alt-ref', '0', '-an', noise_seed])
packets = json.loads(subprocess.check_output([
'ffprobe', '-v', 'error', '-select_streams', 'v:0',
'-show_entries', 'packet=pos,size', '-of', 'json', noise_seed],
text=True))['packets']
assert int(packets[39]['pos']) > 4 * 1024 * 1024
assert max(int(packet['size']) for packet in packets) <= 640 * 360 * 4
insert_void(noise_seed, j('video_gap_head.webm'), after_picture=2)
insert_void(noise_seed, j('video_gap_pin.webm'), after_picture=40)
insert_void(noise_seed, j('video_group.webm'), after_picture=31, in_group=True)
os.unlink(noise_seed)
seed(j('seed_small.mp4'), 3, 640, 360, '300k')
seed(j('seed_4k.mp4'), 3, 3840, 2160, '400k')
# trailing moov, small and fully resident
os.replace(j('seed_small.mp4'), j('trailing_small.mp4'))
audio_large_packet(j('trailing_small.mp4'), j('audio_large_packet.m4a'))
audio_large_packet(j('trailing_small.mp4'), j('audio_seek_gap.m4a'),
32 * 1024 * 1024)
# trailing moov well past any plausible head. The picture loops; the
# sound is one track encoded whole, since a looped copy carries each
# loop's priming frame and, from ffmpeg 8, a timestamp gap at every
# seam, which a decode that honours timestamps fills and ffmpeg's
# raw decode, the audio oracle's reference, does not
subprocess.check_call(['ffmpeg', '-v', 'error', '-y', '-stream_loop',
'60', '-i', j('seed_4k.mp4'),
'-f', 'lavfi', '-i', 'sine=f=440:r=48000',
'-map', '0:v', '-map', '1:a', '-c:v', 'copy',
'-c:a', 'aac', '-b:a', '64k', '-ac', '2',
'-shortest', j('trailing_large.mp4')])
# trailing moov at 7.28 GB, co64 offsets past 4 GiB
trailing_padded(j('seed_4k.mp4'), j('trailing_huge.mp4'), BIG)
# leading moov at 7.28 GB, media past 4 GiB
leading_padded(j('seed_4k.mp4'), j('leading_huge.mp4'), BIG)
for f in ('seed_4k.mp4',):
if os.path.exists(j(f)):
os.unlink(j(f))
for f in sorted(os.listdir(out)):
if f.endswith(('.mp4', '.webp', '.png')):
p = j(f)
print('%-24s %13d bytes %s on disk'
% (f, os.path.getsize(p),
subprocess.check_output(['du', '-h', p])
.split()[0].decode()))
if __name__ == '__main__':
main()