Skip to content

fix(media-server): recover web recordings with damaged frames instead of failing them - #3527

Merged
richiemcilroy merged 7 commits into
mainfrom
fix/media-server-recover-damaged-webm
Oct 29, 2026
Merged

richiemcilroy merged 7 commits into
mainfrom
fix/media-server-recover-damaged-webm

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Web recorder uploads (raw-upload.webm) are checked with a strict ffmpeg decode (-xerror -err_detect explode) before conversion. That decode stops at the first bad packet, so a single damaged VP9 frame failed the whole recording, however long it was. The repair and resilient-transcode paths further down never ran. This PR keeps the check, but a damaged frame now triggers recovery instead of an outright failure.

flowchart TD
    A[raw WebM] --> B{strict decode}
    B -- clean --> P[probe + transcode]
    B -- no video stream --> N["error: recording has no video"]
    B -- cannot open --> D["error: damaged or unreadable"]
    B -- decode errors --> R[probe + transcode with recovery]
    R --> C{output covers source duration?}
    C -- yes --> OK[publish]
    C -- no --> D
    R -- probe or transcode fails --> D
Loading
  • apps/media-server/src/lib/video-input-validation.ts:109 validateVideoInput now returns "clean" | "damaged" instead of throwing on every non-zero exit. It still throws when ffmpeg cannot open the input, and it throws a new, accurate message when the file has no video stream (:104). That happens when a browser recording stops before its first video frame. Those uploads used to be labelled "damaged".
  • apps/media-server/src/routes/video.ts:1610 A damaged source goes through the existing probe, repair and resilient transcode. Afterwards it gets the same duration check imported originals already use (isCompleteRecovery, :1680). If the recovered output falls short of the source duration, or if probing or transcoding fails (:1624), the job fails with the original "damaged or unreadable" message, so callers and the web app see no new error strings.
  • A damaged inter frame holds the picture until the next keyframe. The web recorder asks for two-second keyframes, so the visible effect is bounded to a short freeze instead of a lost recording.
  • Imported originals, MP4 inputs and clean WebM inputs take exactly the same path as before.
60 s VP9 + Opus WebM, one damaged inter frame Before After
/video/process result error, "damaged or unreadable" complete
Output duration none 60.007 s, video + audio
Decode errors in output n/a 0
Visible effect whole recording unavailable about 1.5 s held until the next keyframe

Cases in video-input-validation.integration.test.ts: damaged now completes through the route, damaged-truncated (a damaged frame plus a missing tail) is still rejected, and no-video is rejected with the new message before any transcode.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; the remaining test gap is non-blocking.

Findings

  1. P2 Missing-duration inputs remain untested ▶
Fix with agent prompt
### Issue 1
apps/media-server/src/__tests__/lib/video-input-validation.integration.test.ts:178-182
The new truncation test uses a seekable WebM produced by ffmpeg. Removing its tail leaves the original duration in the header. This misses inputs without a stored duration: `probeMedia` falls back to `computeDuration`, so the recovery check can use the shortened upload as its reference. Add a damaged, truncated WebM without stored duration and check that recovery does not silently accept the missing tail.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Damaged WebM recordings can reach recovery instead of failing at the first bad frame.

  • Recoverable WebM recordings can finish processing after a damaged frame.

Reviews (4) · Last reviewed commit: "test(media-server): cover recovered audi..." · Reviewed by Greptile

@vercel
vercel Bot temporarily deployed to Preview – cap-status October 28, 2026 23:39 Inactive
@vercel

vercel Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
cap-status Skipped Skipped Oct 29, 2026 12:21am UTC

Comment on lines +101 to +105
const corruptBytes = readFileSync(corrupt);
writeFileSync(
damagedTruncated,
corruptBytes.subarray(0, Math.floor(corruptBytes.length * 0.6)),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing-duration inputs remain untested

The new truncation test uses a seekable WebM produced by ffmpeg. Removing its tail leaves the original duration in the header. This misses inputs without a stored duration: probeMedia falls back to computeDuration, so the recovery check can use the shortened upload as its reference. Add a damaged, truncated WebM without stored duration and check that recovery does not silently accept the missing tail.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/__tests__/lib/video-input-validation.integration.test.ts
Line: 101-105

Comment:
**Missing-duration inputs remain untested**

The new truncation test uses a seekable WebM produced by ffmpeg. Removing its tail leaves the original duration in the header. This misses inputs without a stored duration: `probeMedia` falls back to `computeDuration`, so the recovery check can use the shortened upload as its reference. Add a damaged, truncated WebM without stored duration and check that recovery does not silently accept the missing tail.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Comment thread apps/media-server/src/routes/video.ts Outdated
@vercel
vercel Bot temporarily deployed to Preview – cap-status October 28, 2026 23:45 Inactive
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found 2 potential issues (2 medium).

Open in Capy

const outputPath = processed.outputFile.path;
updateJob(jobId, { outputTempFile: processed.outputFile });
if (options.importedOriginal) {
if (options.importedOriginal || recoveringDamagedSource) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Truncated streamed inputs supply their own shortened recovery target

Medium Severity

The reference-duration defect is in probeMedia and probeWithRepairFallback: when a streamed WebM has no stored Duration, probing computes duration from the packets still present, or from the repaired container. For a recording with a damaged frame and a missing tail, that yields the shortened duration; transcoding the surviving packets can then satisfy this newly enabled completeness check. Previously strict validation rejected that damaged recording. The workflow now publishes it as complete, overwrites the browser-reported duration, and deletes the raw upload through cleanupRawUpload. Carry the independently recorded duration into recovery and compare against it rather than treating an observed or repaired duration as proof of completeness. The new streamed fixture is not truncated, while the truncated fixture retains its original header duration.


Full-length audio can mask a shortened recovered video track

Medium Severity

The output-duration defect is in probeMedia: its single container-level duration does not establish the recovered video track's endpoint. For example, if corruption prevents decoding the last ten seconds of a sixty-second video while its audio remains intact, the non-strict processVideo transcode can succeed with fifty seconds of video and sixty seconds of audio. The MP4 still reports sixty seconds, so this newly enabled check accepts it and the workflow deletes the raw source, despite losing substantially more video than the intended short keyframe freeze. Compare recovered video-track coverage against the source video timeline, and check audio coverage separately when required, rather than using the longest container duration.

Open in Capy

@vercel
vercel Bot temporarily deployed to Preview – cap-status October 28, 2026 23:53 Inactive
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found 2 potential issues (2 medium).

Open in Capy

Comment thread apps/media-server/src/routes/video.ts Outdated
Comment thread apps/media-server/src/routes/video.ts Outdated
@vercel
vercel Bot temporarily deployed to Preview – cap-status October 29, 2026 00:21 Inactive
@richiemcilroy

Copy link
Copy Markdown
Member Author

hey @greptileai, please re-review the PR

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capy found no issues.

Open in Capy

@richiemcilroy
richiemcilroy merged commit 1897892 into main Oct 29, 2026
44 checks passed

This branch was successfully deployed

1 active and 1 inactive deployments
Preview – cap-web — f4043441 Deployed Oct 29, 2026 by vercel[bot]
Preview – cap-status — f4043441 Deployed Oct 29, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant