mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
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.
536 lines
23 KiB
Python
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()
|