Upload Validation Pipeline
Upload validation protects data consistency and payment accuracy. A session should upload only after local artifacts prove that the recording is coherent.
The current implementation lives in RecordingArtifactValidator and is called by RecordingSessionFinalizer before files are registered for upload.
Required artifacts
Pure-v2 sessions require this local triplet:
| Artifact | Current path rule |
|---|---|
| MP4 | Completed recording path, for example <base>.mp4. |
| CSV | Same folder and base name as MP4, with _input.csv suffix. |
| JSON | Same folder and base name as MP4, with _meta.json suffix. |
Audio is muxed into the MP4 as AAC. There is no separate audio sidecar — the legacy _audio.ogg artifact was removed in EC-173 (commit af48e44, 2026-05-06) and the finalizer no longer references it.
Current validation config
These are the default values in code today:
| Setting | Current value | Effect |
|---|---|---|
minVideoSizeBytes | 1,048,576 bytes | MP4 files below 1 MiB are blocked as mp4_too_small. This value is sourced from UploaderConfig when available. |
minVideoDurationSeconds | 1.0 second | MP4 duration below 1 second is blocked as mp4_too_short. |
targetMuxedFps | 60.0 FPS | Product target used for FPS validation diagnostics. |
minMuxedFps | 60.0 FPS | Nominal minimum. |
minMuxedFpsAbsoluteTolerance | 2.0 FPS | Allows a 2 FPS tolerance from target. |
minMuxedFpsTargetRatio | 0.95 | Allows a 95% target-ratio floor. |
| Effective muxed FPS floor | 58.0 FPS | Computed as min(60, max(60 - 2, 60 * 0.95)). |
durationToleranceSeconds | 5.0 seconds | Minimum duration mismatch tolerance. |
durationToleranceRatio | 0.15 | Relative duration mismatch tolerance. |
| Effective duration tolerance | max(5s, reference_duration * 0.15) | reference_duration is the larger absolute duration being compared. |
Validation order
The validator short-circuits on the first blocking failure:
- Validate MP4 existence, regular-file status, size, frame counters, readable duration, and minimum duration.
- Validate CSV existence, header, parseability, timestamp monotonicity, JSON
event_args, lifecycle boundaries, and positive effective duration. - Validate JSON existence and parseability. If duration or muxed-frame fields are present, validate those fields.
- Validate muxed FPS from JSON metadata when both
duration_sandframe_count_muxedare available. - Compare CSV duration with MP4 duration.
- Compare JSON duration with MP4 duration when JSON duration is available.
- Return
uploadablewith diagnosticsmp4_csv_json_valid.
Only after this succeeds does RecordingSessionFinalizer register the three local files in SQLite as mp4, csv, and json.
MP4 checks
The MP4 gate currently validates:
- Path exists and is a regular file.
- File size is at least
1,048,576bytes. - Final capture stats are not both zero:
framesCaptured == 0 && framesEncoded == 0blocks asmp4_no_frames. - MP4 duration is readable from
mvhd; if needed, fragmented MP4 duration is reconstructed frommoof/trundata. moovandmoofpayloads over64 MiBare rejected while reading duration.- Duration must be at least
1.0second.
CSV checks
The CSV contract is exact:
frame_number,timestamp,event_type,event_args
Rows must have exactly four fields. timestamp must parse as a finite number and must be monotonic; equal timestamps are allowed, decreasing timestamps are blocked. event_args must parse as JSON when present.
Lifecycle duration uses VIDEO_START and VIDEO_END when both exist. If those are missing, the validator falls back to START and END. VIDEO_PAUSE and VIDEO_RESUME intervals between VIDEO_START and VIDEO_END are subtracted from CSV duration. If the CSV is paused when VIDEO_END arrives, the final paused interval is subtracted up to VIDEO_END.
The resulting CSV duration must be positive.
JSON checks
The JSON file is mandatory and must parse. Current field behavior is:
| Field | Current behavior |
|---|---|
frame_count_muxed | Optional, but if present it must be an integer greater than zero. Otherwise the result is mp4_no_frames. |
duration_s | Optional, but if present it must be finite and greater than zero. Otherwise the result is json_duration_mismatch. |
start_unix_s + end_unix_s | Used as duration fallback when duration_s is absent. end_unix_s must be greater than start_unix_s. |
Muxed FPS validation only runs when JSON has a valid duration and frame_count_muxed > 0. The computed value is:
muxed_fps = frame_count_muxed / duration_s
Since EC-360 this check is observe-only: when muxed_fps < 58.0 the
condition is appended to the validation diagnostics as
mp4_fps_below_threshold (observe-only) and surfaces in the
recording.validation telemetry, but the session remains uploadable.
(Until 4.1.10 it blocked the upload, and EC-346 then deleted the footage;
field feedback reversed that.) The honest real-vs-CFR frame split is recorded
in the _meta.json fps_breakdown block, so low-quality sessions are
filtered downstream instead of being destroyed on the device.
Uploader readiness checks
After validation and SQLite registration, the uploader still checks file readiness before transfer:
| Check | Current value |
|---|---|
| Exclusive file open succeeds | Required |
| File stability age | 2 seconds since last write |
| MP4 minimum size | 1,048,576 bytes |
| Max retries | 3 |
| Bandwidth limit | 0 means unlimited |
| Pause while recording | true |
These uploader checks are not a substitute for the triplet validator. They only protect transfer readiness.
Current failure reasons
These are the actual reason values returned by the validator today:
| Reason | Meaning |
|---|---|
mp4_missing | MP4 path is missing, not a regular file, or cannot be statted. |
mp4_too_small | MP4 is smaller than minVideoSizeBytes, currently 1 MiB. |
mp4_no_frames | Final capture and encoded counters are zero, or JSON frame_count_muxed <= 0. |
mp4_duration_unreadable | MP4 duration cannot be read from mvhd or fragmented duration data. |
mp4_too_short | MP4 duration is below 1.0 second. |
csv_missing | CSV path is missing or not a regular file. |
csv_parse_error | CSV cannot be opened, is empty, has the wrong header, has malformed rows, invalid timestamps, or invalid event_args JSON. |
csv_non_monotonic_timestamps | A CSV timestamp is lower than the previous row timestamp. |
csv_missing_start | Neither VIDEO_START nor fallback START exists. |
csv_missing_end | Neither VIDEO_END nor fallback END exists. |
csv_duration_mismatch | CSV duration is non-positive or differs from MP4 beyond max(5s, 15%). |
json_missing | JSON metadata path is missing or not a regular file. |
json_parse_error | JSON cannot be opened or parsed. |
json_duration_mismatch | JSON duration fields are invalid or differ from MP4 beyond max(5s, 15%). |
mp4_fps_below_threshold is no longer a block reason: since EC-360 a
JSON-derived muxed FPS below the 58.0 floor is recorded in the diagnostics
of an otherwise-uploadable result (see "JSON checks" above).
Optional-automatic upload — the held status (EC-286)
By default a finished recording's artifacts are registered as pending and the
global FIFO uploader pump (getNextPendingFile, WHERE status='pending')
auto-uploads them. EC-286 adds an opt-out so the user can review artifacts —
especially the transcript — before they leave the device:
- A Settings toggle "Upload as soon as recording finishes" persists the
auto_upload_enabledapp setting (absent ⇒ treated astrue, so existing installs are unaffected). - When the toggle is off,
RecordingSessionFinalizerregisters each artifact with statusheldinstead ofpending. Because the pump only claimspending, held files are excluded from upload by construction — the uploader itself is unchanged. - Transcription also becomes on-demand in this mode: the recording-stop
observer skips its auto-enqueue, and the user triggers transcription per
session (
POST /api/transcribe/run). - Releasing a held recording flips
held → pendingand wakes the pump (POST /api/uploader/releasefor one session, or all = "Upload all"). - Review happens in the in-app transcript editor (EC-384, playroll-ui):
the Activity row's pencil opens a modal that renders the transcript as
ordered sentences with timestamps (never raw JSON), with inline editing,
Ctrl+F find and Ctrl+R replace. Saving rewrites only the per-segment
textfields, regenerates the aggregatetext(same join rule as the cppbuild_transcript_document()), and writes atomically, so the upload pump ships the edited file when the user releases it. Files the editor can't parse fall back to the EC-286 alpha flow: open with the OS default app.
The held status is terminal-until-released: validation still ran at
finalize time, so a released file is already known-consistent.
Important invariant
Partial uploads are not acceptable for new sessions. If validation returns blocked, no MP4, CSV, or JSON file should be registered for upload.
For a valid pure-v2 session, server-side upload records should match the required artifact count: three upload records per uploadable session.
If validation fails, the session is marked as failed with the granular block reason. If the failure is mp4_no_frames, or final stats show no frames, the finalizer discards the local MP4, CSV, and JSON metadata. That is the only deletion case: every other blocked artifact stays on disk for inspection and recovery. (EC-346 / 4.1.10 briefly extended deletion to the low-fps / under-delivered family — mp4_fps_below_threshold, csv_duration_mismatch, mp4_too_short; EC-360 reverted that after field feedback.)