248 lines
8.2 KiB
Python
248 lines
8.2 KiB
Python
"""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_epoch_reset_discards_predictive_state_but_keeps_decoder_object(fake_av):
|
|
decoder = decoder_module.FragmentDecoder()
|
|
init, frame = fixture()
|
|
decoder.configure(init)
|
|
decoder.decode(frame)
|
|
decoder.reset(init)
|
|
assert decoder.frames == 0 and decoder._next_dts is None
|
|
decoder.decode(fixture(dts=500)[1])
|
|
assert decoder.frames == 1 and decoder._next_dts == 600
|
|
decoder.reset(init)
|
|
with pytest.raises(decoder_module.StreamingDecodeError, match="random-access"):
|
|
decoder.decode(fixture(dts=600, sync=False)[1])
|
|
with pytest.raises(decoder_module.StreamingDecodeError, match="failed decoder"):
|
|
decoder.reset(init)
|
|
|
|
|
|
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
|