1"""Video clips on a run's timeline: listing, matching, and serving.
3Run: cd webviewer && .venv/bin/python -m pytest backend/test_video.py -q
8from datetime
import datetime
9from zoneinfo
import ZoneInfo
12from fastapi.testclient
import TestClient
14from .
import config, export_cache, main, video
16TZ =
"America/Los_Angeles"
19def box(kind: bytes, payload: bytes) -> bytes:
20 return struct.pack(
">I4s", 8 + len(payload), kind) + payload
23def mp4(seconds: float, version: int = 0, scale: int = 1000, tail: bytes = b
"") -> bytes:
24 """A file with just enough mp4 for a duration: ftyp, moov/mvhd, then some 'mdat'."""
26 mvhd = bytes([1, 0, 0, 0]) + struct.pack(
">QQIQ", 0, 0, scale, int(seconds * scale))
28 mvhd = bytes([0, 0, 0, 0]) + struct.pack(
">IIII", 0, 0, scale, int(seconds * scale))
30 return box(b
"ftyp", b
"isom" + bytes(4)) +
box(b
"moov",
box(b
"mvhd", mvhd)) +
box(b
"mdat", tail
or bytes(64))
33def epoch(y, mo, d, h, mi, s) -> float:
34 return datetime(y, mo, d, h, mi, s, tzinfo=ZoneInfo(TZ)).timestamp()
39 monkeypatch.setattr(config,
"VIDEO_DIR", tmp_path)
40 monkeypatch.setattr(config,
"VIDEO_TZ", TZ)
41 video._dur_cache.clear()
42 video._timing_cache.clear()
46def put(rec_dir, folder: str, stream: str, seconds: float) ->
None:
47 (rec_dir / folder).mkdir(exist_ok=
True)
48 (rec_dir / folder / f
"{stream}.mp4").write_bytes(
mp4(seconds))
55 (tmp_path /
"a.mp4").write_bytes(
mp4(12.5))
56 (tmp_path /
"b.mp4").write_bytes(
mp4(7.25, version=1, scale=90000))
57 assert video.mp4_duration(tmp_path /
"a.mp4") == pytest.approx(12.5)
58 assert video.mp4_duration(tmp_path /
"b.mp4") == pytest.approx(7.25)
63 (tmp_path /
"cut.mp4").write_bytes(whole[:30])
64 (tmp_path /
"text.mp4").write_bytes(b
"not a movie at all")
65 (tmp_path /
"nomoov.mp4").write_bytes(
box(b
"ftyp", b
"isom" + bytes(4)) +
box(b
"mdat", bytes(32)))
66 for name
in (
"cut.mp4",
"text.mp4",
"nomoov.mp4"):
67 assert video.mp4_duration(tmp_path / name)
is None
70def trak(handler: bytes, scale: int, dur: int, samples: int) -> bytes:
71 mdhd = bytes([0, 0, 0, 0]) + struct.pack(
">IIII", 0, 0, scale, dur) + bytes(4)
72 hdlr = bytes(4) + bytes(4) + handler + bytes(12) + b
"\0"
73 stsz = bytes(4) + struct.pack(
">II", 0, samples)
74 stbl =
box(b
"stbl",
box(b
"stsz", stsz))
75 return box(b
"trak",
box(b
"mdia",
box(b
"mdhd", mdhd) +
box(b
"hdlr", hdlr) +
box(b
"minf", stbl)))
79 mvhd = bytes([0, 0, 0, 0]) + struct.pack(
">IIII", 0, 0, 1000, 10000) + bytes(80)
81 moov =
box(b
"moov",
box(b
"mvhd", mvhd) +
trak(b
"soun", 8000, 80000, 500) +
trak(b
"vide", 90000, 900000, 250))
82 (tmp_path /
"v.mp4").write_bytes(
box(b
"ftyp", b
"isom" + bytes(4)) + moov)
83 assert video.mp4_frame_seconds(tmp_path /
"v.mp4") == pytest.approx(1 / 25)
85 (tmp_path /
"a.mp4").write_bytes(
box(b
"moov",
box(b
"mvhd", mvhd) +
trak(b
"soun", 8000, 80000, 500)))
86 assert video.mp4_frame_seconds(tmp_path /
"a.mp4")
is None
87 (tmp_path /
"x.mp4").write_bytes(
mp4(10))
88 assert video.mp4_frame_seconds(tmp_path /
"x.mp4")
is None
95 put(rec_dir,
"2026-11-07_140322_record",
"cam1", 30)
96 put(rec_dir,
"2026-11-07_141000_save",
"cam1", 300)
97 by_kind = {c.kind: c
for c
in video.list_clips()}
98 assert by_kind[
"record"].start ==
epoch(2026, 11, 7, 14, 3, 22)
99 assert by_kind[
"record"].end ==
epoch(2026, 11, 7, 14, 3, 52)
101 assert by_kind[
"save"].end ==
epoch(2026, 11, 7, 14, 10, 0)
102 assert by_kind[
"save"].start ==
epoch(2026, 11, 7, 14, 5, 0)
103 assert all(c.approx
for c
in by_kind.values())
107 put(rec_dir,
"2026-07-01_120000_record",
"cam1", 5)
108 (clip,) = video.list_clips()
110 assert clip.start == datetime(2026, 7, 1, 19, 0, 0, tzinfo=ZoneInfo(
"UTC")).timestamp()
114 put(rec_dir,
"2026-11-07_140322_record",
"cam1", 30)
115 (rec_dir /
"2026-11-07_140322_record" /
".cam2").mkdir()
116 (rec_dir /
"2026-11-07_140322_record" /
"half.mp4").write_bytes(
mp4(10)[:20])
117 (rec_dir /
"2026-11-07_140322_record" /
"bad.name.mp4").write_bytes(
mp4(10))
118 (rec_dir /
"random-folder").mkdir()
119 (rec_dir /
"random-folder" /
"cam1.mp4").write_bytes(
mp4(10))
120 assert [(c.recording, c.stream)
for c
in video.list_clips()] == [
121 (
"2026-11-07_140322_record",
"cam1")
126 monkeypatch.setattr(config,
"VIDEO_DIR",
None)
127 assert not video.available()
and video.list_clips() == []
128 monkeypatch.setattr(config,
"VIDEO_DIR", tmp_path /
"missing")
129 assert not video.available()
and video.list_clips() == []
136 (rec_dir / folder / f
"{stream}.json").write_text(json.dumps(timing))
140 folder =
"2026-11-07_140322_record"
141 put(rec_dir, folder,
"cam1", 30)
142 t0 =
epoch(2026, 11, 7, 14, 3, 21) + 0.6
144 "version": 1,
"durationS": 30,
"latencyMs": 0,
146 "pieces": [{
"fileS": 0,
"wallMs": t0 * 1000}, {
"fileS": 20,
"wallMs": (t0 + 52) * 1000}],
148 (clip,) = video.list_clips()
149 assert not clip.approx
150 assert clip.start == pytest.approx(t0, abs=1e-3)
151 assert clip.end == pytest.approx(t0 + 62, abs=1e-3)
152 assert clip.as_json()[
"pieces"] == [
153 {
"fileS": 0.0,
"wall": pytest.approx(t0, abs=1e-3)}, {
"fileS": 20.0,
"wall": pytest.approx(t0 + 52, abs=1e-3)}
158 folder =
"2026-11-07_140322_record"
159 put(rec_dir, folder,
"cam1", 30)
160 g0 =
epoch(2026, 11, 7, 14, 3, 21)
162 "version": 1,
"durationS": 30,
164 "clockOffsetMs": 2500,
"clockUncertaintyMs": 3,
165 "pieces": [{
"fileS": 0,
"wallMs": g0 * 1000}, {
"fileS": 10,
"wallMs": (g0 + 10) * 1000}],
167 (clip,) = video.list_clips()
168 assert clip.start == pytest.approx(g0 - 2.5, abs=1e-3)
169 assert clip.pieces[1][1] == pytest.approx(g0 + 7.5, abs=1e-3)
170 assert clip.end == pytest.approx(g0 + 27.5, abs=1e-3)
172 assert (j[
"clockOffset"], j[
"clockUncertainty"]) == (2.5, 0.003)
176 folder =
"2026-11-07_140322_record"
177 put(rec_dir, folder,
"cam1", 30)
178 g0 =
epoch(2026, 11, 7, 14, 3, 21)
179 put_timing(rec_dir, folder,
"cam1", {
"version": 1,
"durationS": 30,
"pieces": [{
"fileS": 0,
"wallMs": g0 * 1000}]})
180 (clip,) = video.list_clips()
181 assert clip.start == pytest.approx(g0, abs=1e-3)
and clip.clock_offset
is None
184@pytest.mark.parametrize(
"bad", [
186 {
"version": 2,
"durationS": 30,
"pieces": [{
"fileS": 0,
"wallMs": 1}]},
187 {
"version": 1,
"durationS": 30,
"pieces": []},
188 {
"version": 1,
"durationS": 30,
"pieces": [{
"fileS": 5,
"wallMs": 1}]},
189 {
"version": 1,
"durationS": 30,
"pieces": [{
"fileS": 0,
"wallMs": 1}, {
"fileS": 0,
"wallMs": 2}]},
190 {
"version": 1,
"durationS": 30,
"pieces": [{
"fileS": 0,
"wallMs": 1}, {
"fileS": 31,
"wallMs": 2}]},
191 {
"version": 1,
"pieces": [{
"fileS": 0,
"wallMs": 1}]},
194 folder =
"2026-11-07_140322_record"
195 put(rec_dir, folder,
"cam1", 30)
196 (rec_dir / folder /
"cam1.json").write_text(bad
if isinstance(bad, str)
else json.dumps(bad))
197 (clip,) = video.list_clips()
198 assert clip.approx
and clip.start ==
epoch(2026, 11, 7, 14, 3, 22)
202 put(rec_dir,
"2026-11-07_140322_record",
"cam1", 30)
203 (clip,) = video.list_clips()
204 assert clip.as_json()[
"pieces"] == [{
"fileS": 0.0,
"wall": clip.start}]
211 put(rec_dir,
"2026-11-07_140000_record",
"before", 10)
212 put(rec_dir,
"2026-11-07_140055_record",
"straddle", 10)
213 put(rec_dir,
"2026-11-07_140130_record",
"inside", 10)
214 put(rec_dir,
"2026-11-07_140500_save",
"after-save", 60)
215 put(rec_dir,
"2026-11-07_150000_record",
"later", 10)
216 t0, t1 =
epoch(2026, 11, 7, 14, 1, 0),
epoch(2026, 11, 7, 14, 4, 30)
217 got = sorted(c.stream
for c
in video.clips_overlapping(t0, t1))
218 assert got == [
"after-save",
"inside",
"straddle"]
226 run =
"daq_20261107_140000"
227 t0, t1 =
epoch(2026, 11, 7, 14, 1, 0),
epoch(2026, 11, 7, 14, 4, 30)
228 monkeypatch.setattr(export_cache,
"is_cached",
lambda r: r == run)
232 lambda r: {
"t_min": t0,
"t_max": t1,
"sensor_t_min": t0 + 0.5,
"sensor_t_max": t1 - 0.5},
234 return TestClient(main.app), run
239 put(rec_dir,
"2026-11-07_140130_record",
"cam1", 10)
240 put(rec_dir,
"2026-11-07_150000_record",
"cam1", 10)
241 r = c.get(f
"/api/runs/{run}/video")
242 assert r.status_code == 200
244 assert body[
"available"]
is True
245 assert [(x[
"recording"], x[
"stream"])
for x
in body[
"clips"]] == [(
"2026-11-07_140130_record",
"cam1")]
246 assert body[
"clips"][0][
"end"] - body[
"clips"][0][
"start"] == pytest.approx(10)
251 monkeypatch.setattr(config,
"VIDEO_DIR",
None)
252 assert c.get(f
"/api/runs/{run}/video").
json() == {
"available":
False,
"clips": []}
257 assert c.get(
"/api/runs/daq_20261107_150000/video").status_code == 409
262 folder =
"2026-11-07_140130_record"
263 (rec_dir / folder).mkdir()
264 data =
mp4(10, tail=bytes(range(256)) * 40)
265 (rec_dir / folder /
"cam1.mp4").write_bytes(data)
266 r = c.get(f
"/api/video/{folder}/cam1.mp4", headers={
"Range":
"bytes=100-199"})
267 assert r.status_code == 206
268 assert r.content == data[100:200]
269 assert r.headers[
"content-type"] ==
"video/mp4"
270 assert c.get(f
"/api/video/{folder}/cam1.mp4").content == data
278 put(rec_dir,
"2026-11-07_140130_record",
"cam1", 10)
279 url =
"/api/video/2026-11-07_140130_record/cam1.mp4"
280 for r
in (c.get(url), c.get(url, headers={
"Range":
"bytes=0-9"})):
281 directives = [d.strip()
for d
in r.headers[
"cache-control"].
split(
",")]
282 assert "private" in directives
285@pytest.mark.parametrize(
288 "/api/video/2026-11-07_140130_record/cam2.mp4",
289 "/api/video/not-a-recording/cam1.mp4",
290 "/api/video/2026-11-07_140130_record/..%2F..%2Fsecret.mp4",
291 "/api/video/..%2Fetc/passwd.mp4",
296 put(rec_dir,
"2026-11-07_140130_record",
"cam1", 10)
297 (rec_dir /
"secret.mp4").write_bytes(
mp4(10))
298 assert c.get(path).status_code == 404
a class to store JSON values
static std::vector< string_t > split(const string_t &reference_string)
split the string input to reference tokens
test_gateway_clock_offset_is_taken_off_so_clips_sit_on_the_daq_clock(rec_dir)
test_duration_none_for_unfinished_or_foreign_files(tmp_path)
float epoch(y, mo, d, h, mi, s)
test_listing_skips_what_is_not_a_finished_clip(rec_dir)
test_frame_period_from_the_video_track(tmp_path)
test_timing_file_places_the_clip_exactly_and_spans_its_gap(rec_dir)
rec_dir(tmp_path, monkeypatch)
bytes mp4(float seconds, int version=0, int scale=1000, bytes tail=b"")
None put_timing(rec_dir, folder, stream, timing)
test_run_video_unindexed_is_409(client)
test_no_measured_offset_means_the_clocks_are_taken_as_equal(rec_dir)
None put(rec_dir, str folder, str stream, float seconds)
test_clip_is_not_stored_by_a_shared_cache(client, rec_dir)
bytes trak(bytes handler, int scale, int dur, int samples)
test_approximate_clip_serves_one_piece(rec_dir)
test_overlap_keeps_partial_clips_and_drops_outside_ones(rec_dir)
test_duration_from_mvhd_v0_and_v1(tmp_path)
client(rec_dir, monkeypatch)
test_clip_served_with_range(client, rec_dir)
test_run_video_lists_overlapping_clips(client, rec_dir)
test_no_mount_means_no_clips(monkeypatch, tmp_path)
bytes box(bytes kind, bytes payload)
test_clip_refuses_anything_not_listed(client, rec_dir, path)
test_record_starts_at_its_name_save_ends_at_its_name(rec_dir)
test_folder_name_is_read_in_the_gateway_zone_not_utc(rec_dir)
test_a_timing_file_that_does_not_parse_falls_back_to_the_name(rec_dir, bad)
test_run_video_without_mount_says_unavailable(client, monkeypatch)