refactor(perception): decode bounded fMP4 without source file dependencies
This commit is contained in:
@@ -0,0 +1,231 @@
|
||||
"""Synthetic boxes and fake codec only; real decoding belongs on Worker 006."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link import media_fragments as mp4
|
||||
from k1link.perception import streaming_decoder as decoder_module
|
||||
|
||||
|
||||
def box(kind, data=b""):
|
||||
return (len(data) + 8).to_bytes(4, "big") + kind + data
|
||||
|
||||
|
||||
def u32(value):
|
||||
return value.to_bytes(4, "big")
|
||||
|
||||
|
||||
def fixture(*, dts=0, sync=True, size=5, offset_delta=0, count=1, trun_flags=1):
|
||||
tkhd = box(b"tkhd", bytes(12) + u32(1))
|
||||
mdhd = box(b"mdhd", bytes(12) + u32(1000))
|
||||
hdlr = box(b"hdlr", bytes(8) + b"vide")
|
||||
init = box(b"moov", box(b"trak", tkhd + box(b"mdia", mdhd + hdlr)))
|
||||
tfhd = box(
|
||||
b"tfhd", b"\x00\x02\x00\x38" + u32(1) + u32(100) + u32(size) + u32(0 if sync else 0x10000)
|
||||
)
|
||||
tfdt = box(b"tfdt", bytes(4) + u32(dts))
|
||||
mfhd = box(b"mfhd", bytes(8))
|
||||
|
||||
def moof(offset):
|
||||
trun = box(b"trun", u32(trun_flags) + u32(count) + u32(offset))
|
||||
return box(b"moof", mfhd + box(b"traf", tfhd + tfdt + trun))
|
||||
|
||||
return init, moof(len(moof(0)) + 8 + offset_delta) + box(b"mdat", b"frame")
|
||||
|
||||
|
||||
def test_shared_parser_and_direct_sample_are_source_neutral():
|
||||
init, fragment = fixture()
|
||||
timing = mp4.video_timing(init, mp4.ParseBudget(128, 1))
|
||||
frame = mp4.video_fragment_timing(fragment, timing, mp4.ParseBudget(128, 1))
|
||||
assert (timing.track_id, timing.timescale) == (1, 1000)
|
||||
assert (frame.base_decode_time, frame.duration_units, frame.random_access) == (0, 100, True)
|
||||
sample = mp4.single_sample_payload(fragment, timing)
|
||||
assert sample == b"frame" and sample.obj is fragment
|
||||
script = (
|
||||
"import sys; import k1link.perception.streaming_decoder; "
|
||||
"assert 'k1link.sessions' not in sys.modules; "
|
||||
"assert 'k1link.compute' not in sys.modules; assert 'av' not in sys.modules"
|
||||
)
|
||||
subprocess.run([sys.executable, "-c", script], check=True, timeout=5)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"changes,match",
|
||||
[
|
||||
({"size": 4}, "size"),
|
||||
({"offset_delta": 1}, "offset"),
|
||||
({"count": 2}, "one sample"),
|
||||
({"trun_flags": 0x801}, "composition"),
|
||||
],
|
||||
)
|
||||
def test_direct_sample_rejects_guessed_offsets_or_sizes(changes, match):
|
||||
init, fragment = fixture(**changes)
|
||||
with pytest.raises(mp4.Mp4IntegrityError, match=match):
|
||||
mp4.single_sample_payload(fragment, mp4.video_timing(init, mp4.ParseBudget()))
|
||||
|
||||
|
||||
def test_parser_bounds_and_truncation():
|
||||
init, fragment = fixture(count=2)
|
||||
timing = mp4.video_timing(init, mp4.ParseBudget())
|
||||
with pytest.raises(mp4.Mp4IntegrityError, match="sample budget"):
|
||||
mp4.video_fragment_timing(fragment, timing, mp4.ParseBudget(128, 1))
|
||||
with pytest.raises(mp4.Mp4IntegrityError, match="box budget"):
|
||||
mp4.video_timing(init, mp4.ParseBudget(1, 1))
|
||||
with pytest.raises(mp4.Mp4IntegrityError, match="box size"):
|
||||
mp4.single_sample_payload(fragment[:-1], timing)
|
||||
with pytest.raises(mp4.Mp4IntegrityError, match="moof then mdat"):
|
||||
mp4.single_sample_payload(fragment + box(b"free"), timing)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_av(monkeypatch):
|
||||
class Image:
|
||||
shape, nbytes = (600, 800, 3), 1_440_000
|
||||
|
||||
def setflags(self, *, write):
|
||||
assert write is False
|
||||
|
||||
class Codec:
|
||||
has_b_frames = False
|
||||
|
||||
def open(self):
|
||||
pass
|
||||
|
||||
def decode(self, packet):
|
||||
self.calls.append(packet)
|
||||
return (
|
||||
[]
|
||||
if self.future
|
||||
else [
|
||||
SimpleNamespace(
|
||||
width=self.width,
|
||||
height=600,
|
||||
pts=packet.pts + self.pts_delta,
|
||||
key_frame=self.key_frame,
|
||||
to_ndarray=lambda **kwargs: Image(),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
codec = Codec()
|
||||
codec.calls, codec.future, codec.width, codec.pts_delta, codec.key_frame = (
|
||||
[],
|
||||
False,
|
||||
800,
|
||||
0,
|
||||
True,
|
||||
)
|
||||
|
||||
class Streams(list):
|
||||
@property
|
||||
def video(self):
|
||||
return self
|
||||
|
||||
class Container:
|
||||
def __enter__(self):
|
||||
return SimpleNamespace(
|
||||
streams=Streams(
|
||||
[
|
||||
SimpleNamespace(
|
||||
codec_context=SimpleNamespace(
|
||||
name="h264", width=800, height=600, extradata=b"config"
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
def __exit__(self, *args):
|
||||
pass
|
||||
|
||||
opens = []
|
||||
|
||||
def open_container(stream, **kwargs):
|
||||
opens.append(stream.getvalue())
|
||||
return Container()
|
||||
|
||||
av = SimpleNamespace(
|
||||
__version__="18.0.0",
|
||||
open=open_container,
|
||||
CodecContext=SimpleNamespace(create=lambda *args: codec),
|
||||
Packet=lambda raw: SimpleNamespace(raw=bytes(raw)),
|
||||
)
|
||||
original = decoder_module.importlib.import_module
|
||||
monkeypatch.setattr(
|
||||
decoder_module.importlib,
|
||||
"import_module",
|
||||
lambda name: av if name == "av" else original(name),
|
||||
)
|
||||
return codec, av, opens
|
||||
|
||||
|
||||
def test_persistent_codec_emits_current_frame_without_demux_or_flush(fake_av):
|
||||
codec, _, opens = fake_av
|
||||
init, fragment = fixture()
|
||||
decoder = decoder_module.FragmentDecoder()
|
||||
decoder.configure(init)
|
||||
for sequence in range(3):
|
||||
decoder.decode(fixture(dts=sequence * 100)[1])
|
||||
assert len(opens) == 1 and len(codec.calls) == decoder.frames == 3
|
||||
assert codec.thread_count == 1 and codec.thread_type == "SLICE"
|
||||
assert all(p.raw == b"frame" and p.pts == p.dts for p in codec.calls)
|
||||
decoder.close()
|
||||
with pytest.raises(decoder_module.StreamingDecodeError, match="failed"):
|
||||
decoder.decode(fragment)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"mutation,match",
|
||||
[
|
||||
(("future", True), "future"),
|
||||
(("has_b_frames", True), "reordering"),
|
||||
(("width", 801), "raster"),
|
||||
(("pts_delta", 1), "identity"),
|
||||
(("key_frame", False), "keyframe"),
|
||||
],
|
||||
)
|
||||
def test_ambiguous_native_output_poisons_decoder(fake_av, mutation, match):
|
||||
codec, _, _ = fake_av
|
||||
setattr(codec, *mutation)
|
||||
decoder = decoder_module.FragmentDecoder()
|
||||
init, fragment = fixture()
|
||||
decoder.configure(init)
|
||||
with pytest.raises(decoder_module.StreamingDecodeError, match=match):
|
||||
decoder.decode(fragment)
|
||||
assert decoder.failed and decoder.frames == 0
|
||||
|
||||
|
||||
def test_codec_gap_does_not_use_later_frame_as_current(fake_av):
|
||||
codec, _, _ = fake_av
|
||||
decoder = decoder_module.FragmentDecoder()
|
||||
decoder.configure(fixture()[0])
|
||||
decoder.decode(fixture()[1])
|
||||
with pytest.raises(decoder_module.StreamingDecodeError, match="discontinuous"):
|
||||
decoder.decode(fixture(dts=200)[1])
|
||||
assert len(codec.calls) == 1 and decoder.failed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("case", ["no-init", "reinit", "non-keyframe", "oversized", "version"])
|
||||
def test_invalid_stream_setup_fails_closed(fake_av, case):
|
||||
_, av, _ = fake_av
|
||||
if case == "version":
|
||||
av.__version__ = "other"
|
||||
with pytest.raises(decoder_module.StreamingDecodeError, match="version"):
|
||||
decoder_module.FragmentDecoder()
|
||||
return
|
||||
decoder = decoder_module.FragmentDecoder()
|
||||
if case != "no-init":
|
||||
decoder.configure(fixture()[0])
|
||||
with pytest.raises(decoder_module.StreamingDecodeError):
|
||||
if case == "reinit":
|
||||
decoder.configure(fixture()[0])
|
||||
else:
|
||||
decoder.decode(
|
||||
b"x" * (1024 * 1024 + 1)
|
||||
if case == "oversized"
|
||||
else fixture(sync=case != "non-keyframe")[1]
|
||||
)
|
||||
assert decoder.failed
|
||||
Reference in New Issue
Block a user