feat(replays): play video-backed sessions behind a replay engine seam - #462
Open
Makisuo wants to merge 1 commit into
Open
feat(replays): play video-backed sessions behind a replay engine seam#462Makisuo wants to merge 1 commit into
Makisuo wants to merge 1 commit into
Conversation
The player hardcoded `new Replayer(...)`, so rrweb was the only thing it could ever play. Mobile sessions record H.264 segments wrapped in rrweb-shaped events — the chunk pipeline carries them fine, but rrweb finds no DOM to rebuild and renders nothing. Engine construction, the clock, seeking and letterboxing now sit behind `ReplayEngine`, with two implementations: the existing rrweb Replayer (moved over unchanged, including its negative-baseline clock guard) and a video engine that decodes each base64 segment to a Blob URL and drives one `<video>` element. Because every segment opens on an IDR keyframe, an offset resolves to (segment, offset-within-segment) exactly — a seek into a gap between segments snaps forward to the next real footage. Nothing below the context changed: the surface, transport, editor timeline, events rail and chunk loader are untouched, and the existing player tests pass as written. Two correctness fixes fall out of supporting both formats: - Idle derivation treated every event as an instant. A video session emits ~one event per 30s segment, so each quiet segment read as a 30s idle band and skip-idle (on by default) collapsed the recording to nothing. A segment now spans its duration, so only real gaps — the recorder stopping while the app is backgrounded — stay collapsible. - Marker extraction only matched MouseInteractions.Click, so touch sessions produced no click markers. TouchStart now counts, which also fixes mobile-web rrweb sessions that never had markers either. Format selection rides `maple.session.replay_format` in the existing ResourceAttributes map, so there is no warehouse migration and no wire change: the detail query already ships the whole map as JSON, and the route prefetches it, so the engine is chosen before any chunk is fetched. An absent key means rrweb — every session recorded before the marker existed is a browser recording. Also stop `normalizeEvents` from running JSON.stringify over both sides of every adjacent pair; with a base64 MP4 inside each event that serialized hundreds of KB per comparison on every range decode.
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.
Why
The web player hardcoded
new Replayer(...), so rrweb was the only thing it could ever play. Mobile sessions record H.264 segments wrapped in rrweb-shaped events — the chunk pipeline carries them untouched, but rrweb finds no DOM to rebuild and renders nothing.What
Engine construction, the clock, seeking and letterboxing now sit behind a
ReplayEngineinterface (apps/web/src/components/replays/engine/), with two implementations:Replayermoved over unchanged, including its config, theapplyScaleletterboxing math, and the negative-baselineTimeclock guard (that quirk is rrweb's, not the player's, so it belongs in the engine).<video>. Since every segment opens on an IDR keyframe, an offset resolves to (segment, offset-within-segment) exactly; a seek landing in a gap between segments snaps forward to the next real footage. Object URLs are held for the current and next segment only — the base64 already sits in the loader's event array, so a decoded Blob is a second copy.Nothing below the context changed.
replay-player.tsx,replay-editor-timeline.tsx,replay-timeline.ts,session-events-panel.tsx,replay-range.tsanduse-replay-chunk-loader.tsare byte-identical, and the existing player + transport tests pass as written — that's the evidence the refactor preserved behaviour, not just a claim.Two bugs found along the way
deriveMetaonly matchedMouseInteractions.Click(2), neverTouchStart(7). Also a latent fix for mobile-web rrweb sessions, which have never shown markers either.Separately,
normalizeEventsranJSON.stringifyover both sides of every adjacent pair; with an MP4 inside each event that serialized hundreds of KB per comparison on every range decode. It now rejects on timestamp + type first.Format discriminator
maple.session.replay_format("rrweb" | "video") rides the existingResourceAttributesmap, so there is no warehouse migration and no wire change: the detail query already ships the whole map as JSON and the route loader already prefetches it, so the engine is chosen before any chunk is fetched. Deliberately did not touchsessionReplaysListQuery— that would mean four duplicated projections plus acatalog.sqlregeneration for a list badge nobody asked for.An absent key means
rrweb; every session recorded before the marker existed is a browser recording. The browser SDK stamps"rrweb"inbuildSessionMetaRow's shared base object — placement matters, sincesession_replaysis a ReplacingMergeTree that replaces the whole row.Seeking needed no changes:
is_checkpointcomes from thex-maple-is-checkpointingest header, so an SDK that flags every segment gets exact per-segment anchoring fromcheckpointAtOrBeforefor free.Scope checks
apps/mobileis unaffected — it only calls/api/query-engine/*and/api/dashboards/, and never readssession_replaysor any replay route.FirstEventMsattempt was reverted because the deployed cluster rejected it (warehouse_schema_drift) and it tripped the local-CLI schema gate. A map key avoids all of that.Testing
144 tests across 13 replay files, plus
browser-session(109) and the effect-sdk session test. Scoped typecheck clean on@maple/weband@maple/browser-session.New coverage: segment extraction and offset→segment resolution (gaps, boundaries, past-the-end, negative), the video idle-band trap, touch vs mouse markers,
replayFormatfallbacks, and the format marker on both active and ended meta rows.The
<video>DOM path is intentionally not unit-tested — jsdom implements neitherURL.createObjectURLnorHTMLMediaElement.play/pause, so such a test would only exercise its own stubs. The segment math is pure and covered directly instead.Not done
bun run test/bun typecheckwere skipped (authored late, scoped commands only) — worth a run in CI/morning.eventsOverrideprovider prop injects a synthetic mobile chunk without touching the network.maple.session.replay_format: "video"in its meta row andx-maple-is-checkpoint: 1per segment. Unverified from this repo.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.