Fix wrong recording length (8:12 video listed as 1:40) #4

Merged
smmmquader merged 2 commits from fix/recording-duration into main 2026-08-08 15:40:14 -04:00
Member

/v/keyword-research is 8:12 of video but was listed as 1:40.

What was actually wrong

Nothing was lost — I parsed the stored WebM directly and the last cluster timecode is 492,092 ms (8m12s) across 132.5 MB. The player is fine too: on the live page player.duration resolves to 493.05 s once seekfix runs.

The bad number is the stored duration, which is what the dashboard badge renders (dashboard.html, v.duration|duration). MediaRecorder writes a streaming WebM with no Duration element, so the length we saved came from the recorder's own tick loop instead of from the media:

tickTimer = setInterval(() => { if (!paused) elapsedMs += now - last; ... }, 250);

That accumulator only advances while its callback fires. Browsers throttle and outright freeze timers in hidden/occluded tabs — which is exactly what a screen recorder's own tab is for the whole recording — so the saved length can end up far short of the real one.

Changes

  • recorder.js — timer derived from wall-clock marks (recStartMs, pausedTotalMs) instead of per-tick accumulation, so a frozen tab can't under-count.
  • recorder.js — save the real media length: the review player already recovers it from the recorded blob via fixVideoSeek, so use that and keep the wall-clock timer only as a fallback.
  • watch.html / watch.js — pass the stored length to the player as data-duration, so the client can tell when the browser's guess is wrong.
  • seekfix.js — also run the recovery when the browser reports a finite but wrong duration; it previously no-opped on any finite value. Recovery is attempted at most once per element so a wrong stored duration can't cause a seek loop, plus a timeout so the playhead is never left parked at the end.

Verification

  • WebM cluster-timecode parse of the live file → 8m12s (the media is intact).
  • Live page inspection → player.duration 493.05 s, seekfix fires and settles in ~2 s.
  • node --check on all three touched JS files.
  • State-machine tests for seekfix (fake <video>): Infinity recovers · wrong-finite corrects · bad hint → one seek, no loop · agreeing duration no-ops · restoreTo honoured. All pass.

Note

This is read-side only for new recordings — nothing needs re-uploading. Existing recordings keep their stored (possibly wrong) duration until backfilled; the length shown on the watch page itself is already correct.


Follow-up: length taken from the container, and a backfill

The client's timer is no longer the source of truth at all.

app/webmprobe.py reads the real length straight from the WebM — the last Cluster's Timecode plus the largest block offset inside it, scaled by TimecodeScale. It reads only the head and a 4 MB tail, so probing a 130 MB recording touches a few MB, not the whole file. (Lives under app/ because the Dockerfile only copies app/, templates/, static/ and schema.sql into the image.)

  • complete_upload() now prefers the probed duration over whatever the browser reported, falling back to it only when the file can't be read.
  • Backfill for recordings already stored with a wrong value:
docker compose exec lumo python -m app.webmprobe            # dry run - lists what would change
docker compose exec lumo python -m app.webmprobe --apply    # write the corrections

Dry run by default; --tolerance controls what counts as wrong (default 1 s).

Verification

  • Probe run against the live 8:13 recording returns 493.052 s — exactly the value the browser resolves.
  • Malformed, empty, truncated and garbage input all return None instead of raising, so the upload path falls back cleanly.
  • py_compile clean on the touched Python.
`/v/keyword-research` is **8:12** of video but was listed as **1:40**. ## What was actually wrong Nothing was lost — I parsed the stored WebM directly and the last cluster timecode is **492,092 ms (8m12s)** across 132.5 MB. The player is fine too: on the live page `player.duration` resolves to **493.05 s** once `seekfix` runs. The bad number is the **stored** duration, which is what the dashboard badge renders (`dashboard.html`, `v.duration|duration`). MediaRecorder writes a streaming WebM with no Duration element, so the length we saved came from the recorder's own tick loop instead of from the media: ```js tickTimer = setInterval(() => { if (!paused) elapsedMs += now - last; ... }, 250); ``` That accumulator only advances while its callback fires. Browsers throttle and outright freeze timers in hidden/occluded tabs — which is exactly what a screen recorder's own tab is for the whole recording — so the saved length can end up far short of the real one. ## Changes - **`recorder.js`** — timer derived from wall-clock marks (`recStartMs`, `pausedTotalMs`) instead of per-tick accumulation, so a frozen tab can't under-count. - **`recorder.js`** — save the **real media length**: the review player already recovers it from the recorded blob via `fixVideoSeek`, so use that and keep the wall-clock timer only as a fallback. - **`watch.html` / `watch.js`** — pass the stored length to the player as `data-duration`, so the client can tell when the browser's guess is wrong. - **`seekfix.js`** — also run the recovery when the browser reports a *finite but wrong* duration; it previously no-opped on any finite value. Recovery is attempted **at most once** per element so a wrong stored duration can't cause a seek loop, plus a timeout so the playhead is never left parked at the end. ## Verification - WebM cluster-timecode parse of the live file → 8m12s (the media is intact). - Live page inspection → `player.duration` 493.05 s, `seekfix` fires and settles in ~2 s. - `node --check` on all three touched JS files. - State-machine tests for `seekfix` (fake `<video>`): Infinity recovers · wrong-finite corrects · **bad hint → one seek, no loop** · agreeing duration no-ops · `restoreTo` honoured. All pass. ## Note This is read-side only for new recordings — nothing needs re-uploading. **Existing** recordings keep their stored (possibly wrong) duration until backfilled; the length shown on the watch page itself is already correct. --- ## Follow-up: length taken from the container, and a backfill The client's timer is no longer the source of truth at all. **`app/webmprobe.py`** reads the real length straight from the WebM — the last Cluster's `Timecode` plus the largest block offset inside it, scaled by `TimecodeScale`. It reads only the head and a 4 MB tail, so probing a 130 MB recording touches a few MB, not the whole file. (Lives under `app/` because the Dockerfile only copies `app/`, `templates/`, `static/` and `schema.sql` into the image.) - **`complete_upload()`** now prefers the probed duration over whatever the browser reported, falling back to it only when the file can't be read. - **Backfill** for recordings already stored with a wrong value: ```bash docker compose exec lumo python -m app.webmprobe # dry run - lists what would change docker compose exec lumo python -m app.webmprobe --apply # write the corrections ``` Dry run by default; `--tolerance` controls what counts as wrong (default 1 s). ### Verification - Probe run against the live 8:13 recording returns **493.052 s** — exactly the value the browser resolves. - Malformed, empty, truncated and garbage input all return `None` instead of raising, so the upload path falls back cleanly. - `py_compile` clean on the touched Python.
MediaRecorder writes a streaming WebM with no Duration element, so the length
we stored came from the recorder's own tick loop rather than the media.

- recorder.js: derive the timer from wall-clock marks instead of accumulating
  per tick. Browsers freeze timers in hidden/occluded tabs - which is exactly
  what a screen recorder's own tab is - and a tick that never fires silently
  under-counts.
- recorder.js: save the real media length (the review player recovers it from
  the recorded blob via fixVideoSeek) and keep the wall-clock timer only as a
  fallback.
- watch.html / watch.js: pass the stored duration to the player as
  data-duration so the client can tell when the browser's guess is wrong.
- seekfix.js: also run the recovery when the browser reports a finite but
  wrong duration; previously it no-opped on any finite value. Recovery is
  attempted at most once per element so a wrong stored duration cannot loop,
  with a timeout so the playhead is never left parked at the end.

Read-side only for existing recordings - nothing needs re-uploading.
app/webmprobe.py reads the true length straight from the container: the last
Cluster's Timecode plus the largest block offset inside it, using TimecodeScale
from the header. Only the head and a 4 MB tail are read, so probing a 130 MB
recording touches a few MB rather than the whole file. Verified against a live
8:13 recording: probe returns 493.052s, matching exactly what the browser
resolves. Malformed/short input returns None rather than raising.

- complete_upload() now prefers the probed duration over the value the browser
  reported, falling back to it when the file cannot be read. The client timer is
  no longer trusted as the source of truth.
- CLI to correct recordings already stored with a wrong duration:
    docker compose exec lumo python -m app.webmprobe            # dry run
    docker compose exec lumo python -m app.webmprobe --apply    # write changes
  Dry run by default, --tolerance to control what counts as wrong.

Lives under app/ because the Dockerfile only copies app/, templates/, static/
and schema.sql into the image.
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
QuaderEnterprises/lumo!4
No description provided.