feat(web): add sprite sheet scrub previews for video progress bar#1731
Open
mstublefield wants to merge 1 commit intoCapSoftware:mainfrom
Open
feat(web): add sprite sheet scrub previews for video progress bar#1731mstublefield wants to merge 1 commit intoCapSoftware:mainfrom
mstublefield wants to merge 1 commit intoCapSoftware:mainfrom
Conversation
During background processing, the media server now generates a sprite sheet (grid of frames at 2-second intervals) and a WebVTT file mapping time ranges to sprite coordinates. The playlist API serves the VTT with a fresh signed sprite URL injected on each request. On the client side, a <track kind="metadata" label="thumbnails"> element feeds into media-chrome's built-in preview infrastructure, which was already wired up in the vendored media-player.tsx but had no data source. Zero changes to the vendored file. Graceful fallback: before sprites are ready (processing in progress, older videos), the tooltip shows time-only. After sprites load, hover previews show the actual frame at the hovered position. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| progress: 93, | ||
| message: "Generating preview sprites...", | ||
| }); | ||
| sendWebhook(getJob(jobId)!); |
Contributor
There was a problem hiding this comment.
sendWebhook not awaited (inconsistent with processVideoAsync)
The sprite-generation webhook call in muxSegmentsAsync is fire-and-forget, while the equivalent call in processVideoAsync (line 781) uses await. If sendWebhook rejects, the rejection is silently dropped here. The thumbnail block in this same function (line 1373) has the same pattern, but the divergence from processVideoAsync should be intentional and consistent.
Suggested change
| sendWebhook(getJob(jobId)!); | |
| await sendWebhook(getJob(jobId)!); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/media-server/src/routes/video.ts
Line: 1394
Comment:
**`sendWebhook` not awaited (inconsistent with `processVideoAsync`)**
The sprite-generation webhook call in `muxSegmentsAsync` is fire-and-forget, while the equivalent call in `processVideoAsync` (line 781) uses `await`. If `sendWebhook` rejects, the rejection is silently dropped here. The thumbnail block in this same function (line 1373) has the same pattern, but the divergence from `processVideoAsync` should be intentional and consistent.
```suggestion
await sendWebhook(getJob(jobId)!);
```
How can I resolve this? If you propose a fix, please make it concise.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds proper scrub preview thumbnails when hovering over the video progress bar on shared Cap video pages. Previously, the hover tooltip either showed the current playback frame (not the hovered position) or a red "Error" image for custom bucket users.
#xywhcoordinatesfileType=thumbnails-vtthandler in the playlist API serves the VTT with a fresh signed sprite URL injected on each request — works for both default and custom R2/S3 buckets<track kind="metadata" label="thumbnails">— just needed a data source. The existingspriteStyleandgetThumbnailpaths inmedia-player.tsxare untouchedGraceful fallback
Both recording modes supported
Instant and studio recordings converge on the same processing pipeline. Sprite generation is best-effort — failures are caught and logged but don't fail the processing job.
Test plan
🤖 Generated with Claude Code
Greptile Summary
This PR adds server-side sprite sheet generation during video processing and wires up a WebVTT-based seek preview in the media player. The FFmpeg sprite generation, VTT placeholder approach, signed-URL injection in the playlist API, and graceful 404 fallback are all cleanly implemented.
Confidence Score: 5/5
Safe to merge — sprite generation is best-effort and failures don't affect core video playback.
All findings are P2: a missing type union entry in the webhook handler (silently handled at runtime by the default case) and an un-awaited webhook call in mux-segments (consistent with pre-existing thumbnail webhook pattern). No data loss, no broken playback path, no security concerns.
apps/web/app/api/webhooks/media-server/progress/route.ts — add
"generating_sprites"to the payload type and an explicit mapping case.Important Files Changed
generateSpriteSheetandformatVttTimestamphelpers; sprite generation logic is correct with proper timeout, chunked stdout reading, and a__SPRITE_URL__placeholder for deferred URL resolution.processVideoAsyncandmuxSegmentsAsync; sprite generation is best-effort with try/catch, butsendWebhookis not awaited in themuxSegmentsAsyncsprite block (unlike inprocessVideoAsync)."generating_sprites"phase is silently handled by thedefaultcase inmapPhaseToDbPhasebut is missing from theProgressWebhookPayloadtype union, creating a type/runtime mismatch.thumbnails-vtthandler fetches the stored VTT, replaces__SPRITE_URL__with a fresh signed URL, and serves withno-storecache headers — correctly handles both default and custom S3 buckets.<track kind="metadata" label="thumbnails">element pointing to the new VTT endpoint; gracefully falls back to time-only display when VTT is unavailable (404).recording-complete.tschange.sprites/sprite.jpgandsprites/thumbnails.vtt, forwarding them through to the mux-segments payload cleanly.Sequence Diagram
sequenceDiagram participant Client as Browser participant Playlist as /api/playlist participant S3 as S3 / R2 participant MediaServer as Media Server participant Webhook as /api/webhooks/progress MediaServer->>S3: Upload result.mp4 MediaServer->>MediaServer: generateSpriteSheet(result.mp4) MediaServer->>S3: PUT sprites/sprite.jpg (presigned) MediaServer->>S3: PUT sprites/thumbnails.vtt (presigned, __SPRITE_URL__ placeholder) MediaServer->>Webhook: POST phase=generating_sprites (mapped to processing) MediaServer->>Webhook: POST phase=complete Client->>Playlist: GET /api/playlist?fileType=thumbnails-vtt Playlist->>S3: getObject(sprites/thumbnails.vtt) S3-->>Playlist: VTT with __SPRITE_URL__ Playlist->>S3: getSignedObjectUrl(sprites/sprite.jpg) S3-->>Playlist: signed URL (fresh) Playlist-->>Client: VTT with real signed URL (no-store) Client->>S3: GET sprites/sprite.jpg (signed URL, on hover) S3-->>Client: sprite imageComments Outside Diff (1)
apps/web/app/api/webhooks/media-server/progress/route.ts, line 14-64 (link)"generating_sprites"is sent by the media server but is absent fromProgressWebhookPayload["phase"]and has no explicit case inmapPhaseToDbPhase. At runtime thedefaultbranch silently maps it to"processing", which is correct, but the TypeScript type is misleading and any future addition of cases beforedefaultcould miss this phase.And in
mapPhaseToDbPhase, add an explicit case:Prompt To Fix With AI
Prompt To Fix All With AI
Reviews (1): Last reviewed commit: "feat(web): add sprite sheet scrub previe..." | Re-trigger Greptile
(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!