Manifest Format
manifest.json is the single interface between the two halves of the system. The app validates it strictly on import and refuses anything ambiguous.
You do not normally write this by hand — the preparation script produces it. This page is for troubleshooting an import error or building your own tooling.
Structure
Section titled “Structure”{ "schema_version": 1, "sport": "badminton_singles", "source": { "id": "2026-08-10-club-a", "filename": "IMG_4471.MOV", "fps": 119.88, "width": 1920, "height": 1080, "recorded_at": "2026-08-10T19:22:00+08:00", "camera": { "viewpoint": "receiver", "height_m": 1.7, "notes": "centre line, 0.8 m behind baseline" }, "opponent_id": "opp_003", "consent": true }, "shots": [ { "id": "s_0001", "contact_frame": 4127, "contact_time_ms": 34426.09, "zone": "BL", "shot_type": "clear", "detector": { "source": "manual", "confidence": 1.0, "confirmed_by_human": false }, "clip": { "path": "clips/s_0001.mp4", "duration_ms": 2500, "lead_in_ms": 2500 } } ]}Field reference
Section titled “Field reference”source
Section titled “source”| Field | Notes |
|---|---|
id | Identifies the footage set. Shown in the app’s Footage list |
fps | Exact frame rate from container metadata, as a float. Must be 1–1000 |
recorded_at | ISO 8601 |
camera.viewpoint | receiver | side | corner | other. Anything but receiver triggers a warning |
opponent_id | Optional |
consent | Absent is treated as false and warned about |
shots[]
Section titled “shots[]”| Field | Notes |
|---|---|
id | Unique within the manifest |
contact_frame | Integer frame index of racket–shuttle contact |
contact_time_ms | Must equal contact_frame / fps × 1000 within one frame |
zone | Must be a code the sport config defines |
shot_type | Optional, nullable, unused in analysis |
detector.source | tracknet_v3 | tracknet_v4 | manual |
detector.confirmed_by_human | Set to true after annotation in the app |
clip.path | Relative to the manifest. Absolute paths and .. are rejected |
clip.duration_ms | Must be > 0. The clip ends on the contact frame |
clip.lead_in_ms | Must not exceed duration_ms |
Invariants
Section titled “Invariants”Every clip ends exactly on its contact frame. Occlusion is stopping playback early, never trimming a file.
contact_time_ms must agree with contact_frame at the declared fps. The app checks this and rejects the import if they disagree by more than one frame — a mismatch means one of them is wrong, and there is no safe way to guess which.
Clip encoding
Section titled “Clip encoding”H.264, same frame rate as source, CRF 20, -g 15 (short GOP), faststart, audio stripped.
The short GOP matters: it keeps decoding predictable so the player can be prerolled reliably before each trial.
Import errors
Section titled “Import errors”The app reports these and aborts the import:
| Error | Cause |
|---|---|
| Unsupported manifest schema version | Script and app versions differ. Re-cut with the matching script |
| Manifest is for sport “X” but “Y” was expected | Wrong --sport when cutting |
| Zone code not defined | Wrong zone codes for the sport |
| contact_time_ms disagrees with contact_frame | Frame rate was assumed rather than read |
| Duplicate shot id / clip path | Malformed shot list |
| This video has already been imported | Delete the existing footage first |