Event lineup player
The event page supports an organizer-selected event_stream or
performer_sequence mode. The latter starts with following enabled, but makes no
media connection until the viewer presses Play. New-session controls appear only
inside the existing doors-open/start/end watch window. The shared clock refreshes
on its normal interval and when document visibility changes. An already-started
session stays mounted after the window closes. It requires no media worker,
relay, provider key, or paid entitlement.
Ownership and updates
EventPublicPage retains its server-provided content until one event-scoped
events.getPublicBySlug subscription has an authoritative result. That projection
updates both the roster links and the watch component. A null projection removes
the page content and releases playback. Cancellation, disabling watch, and mode
changes unmount the lineup session. There is no subscription per performer.
EventLineupSession owns one current source and at most one prepared next source.
A generation check runs before creating an asynchronous source and again before
accepting playback results. Projection edits, manual selection and disposal
invalidate prepared requests. A pending current connection reconciles its captured
slot again when accepted, so a unique replacement key remains the active final
slot for overtime pause/resume. Pause also invalidates initial transport imports and
source play attempts awaiting audio-context resume. Sources are owned before
awaiting playback, so hiding a source or unmounting releases them even when
resume or play completion never resolves. Exact slot keys retain source identity. Replaced
keys require exactly one matching performer slug, authored start and selected
normalized stream ID. A hidden or changed selected source stops the session and
requires viewer action. An unchanged stream may retain its connection while its
slot key changes.
A save rejected for an unavailable stream selection shows Stream unavailable.
The editor marks the submitted explicit stream choices invalid until changed or
resubmitted. The backend error identifies the selection category, not a slot.
The existing standalone VrcdnStreamPlayer API is unchanged. It and the observed
event source share attachVrcdnTransport and releaseVrcdnTransport. Event playback
uses different buffered-failure handling, with the existing VrcdnPlayerControls
and play poster. Transport setup failure releases partially attached players;
teardown attempts every transport step even if an earlier step throws.
Following policy
Wall time selects schedule candidates. Monotonic performance.now() timestamps
measure audio progress and failure intervals. The two clock domains are never
subtracted. Current playback retains its source across schedule boundaries.
A healthy source is never cut off because its posted end has passed.
Only the immediate next slot can be prepared. Missing, freeform and unplayable slots are barriers. Out-of-order or simultaneous starts adjacent to the current or candidate slot block that transition, including a candidate with a same-time sibling. Ambiguity later in the lineup does not block an earlier unambiguous transition. Eligibility is the later of current end and next start, minus 120 seconds. Ordered overlaps use that same later boundary for an established current selection. Joining during simultaneous active intervals still refuses an ambiguous selection. If current end is absent, next start provides the handoff boundary only. An immediate next slot whose effective interval has ended is no longer a candidate for preparation or handoff. Its explicit end, or the following slot's start when its end is absent, closes that interval. A different-source slot remains a barrier, so following never skips ahead to a later performer. A prepared source is released if that interval ends while the healthy current source continues overtime.
Consecutive slots with the same normalized stream ID advance logical identity at
max(current.endAt ?? next.startAt, next.startAt). They retain the current media
connection and never prepare a duplicate. After a delayed or hidden observation,
following can catch up across consecutive expired same-source slots. Different,
missing or unplayable sources and ambiguous ordering still block that traversal.
Each decoded source routes through an analyser and source gain before the shared viewer gain. Prepared source gain is zero. Three consecutive progressing samples are required before it is ready to replace the current source. Viewer mute and volume affect output gain, not silence measurements.
UI subscribers receive a new snapshot only when a snapshot field changes. Media observation continues every 100 ms regardless of UI notifications.
The measured policy is below -90 dBFS for 1000 ms, sampled every 100 ms. Continuous stopped playback requires at least 1000 ms. Healthy decoded progress cancels failure evidence, including when bytes remain buffered after transport failure or clean EOF. EOF alone neither releases buffered playback nor advances the lineup. A genuine long silent musical break can satisfy the silence rule; it does not prove that the broadcaster has finished.
Retries use 1, 2, 4, then 10 second intervals. Each successful handoff resets the
preparation retry count and deadline for the next candidate. Replacing a prepared connection also
observes the first retry interval to avoid immediate reconnect bursts after
pause/resume or edits. A pending video.play() promise cannot block retries.
Failed next-source attempts retain the current source. Pause disables transition
decisions and releases preparation. Current-source play rejection leaves the shared
Play action. A rejected prepared
source remains muted and owned, with automatic reconnects suspended. The
Enable playback control retries that exact source under a viewer gesture while
current audio continues. Another gesture supersedes a pending retry on the same
source. Only the latest attempt can change pending or blocked state, and handoff
waits for that attempt to settle. Successful retry must still produce three progressing
samples before normal handoff is allowed. Projection changes, manual selection,
pause, candidate expiry, hidden-document release and disposal clear rejection
state and invalidate pending retry callbacks.
flowchart LR
Event[Event page or direct event link] --> Play[Play]
Play --> Current[Current audio continues]
Current --> Rejected[Prepared playback rejected]
Rejected --> Enable[Enable playback]
Enable --> Ready[Prepared source decodes while muted]
Ready --> Handoff[Normal failure or silence handoff]
Manual selection disables following. Return to live re-evaluates the scheduled slot. Resuming following also re-evaluates it, except an already-active final slot can resume after posted event end, including while its transport is being recovered. The reconciled selected slot retains this identity through failed or pending connections. Removed or changed sources lose it. New sessions after event end are refused. A lineup with no playable source offers no initial player. An already-started session keeps its recovery controls when its sources become unavailable. There is no automatic wraparound.
Browser visibility
Hidden documents deliberately stop analysis, preparation and handoff decisions; current audio can continue. Visibility changes and audio-context state changes clear evidence. The first active sample after a reset does not inherit old timers. Sampling gaps over 500 ms also reset evidence. This is foreground automatic following, not a claim of background switching or OS-suspension support.
Verification
The guarded /playwright/event-lineup-live route mounts the actual public page,
query hook and player. Only its Convex transport boundary is mocked. Backend
event-playback-projection.test.ts tests the actual public query and projection,
including hidden links, removed sources, cancellation and unpublished events.
Only the guarded fixture overrides schedule time. The loopback MPEG-TS server
provides audible, quiet, connected silent, musical-break and unavailable sources.
Browser tests also map the canonical event-stream fetch to loopback inside the
test page. This tests URL resolution and the existing player without contacting a
live provider.
Run with the same local fixture server and environment variables documented in
event-playback-proof.md, replacing the test filename
with event-lineup-live.spec.ts. The test explicitly launches Chromium and Firefox
and attaches screenshots, browser messages, connection statistics and visibility
observations. Each proof-server invocation generates private MPEG-TS files in a
unique directory and removes that directory after stopping its media processes.
Simultaneous invocations cannot truncate another server's transport inputs. Generated artifacts remain ignored under
apps/web/playwright-artifacts/.
Safari is unavailable on the Windows test host. A mobile viewport verifies layout, not Safari/iOS audio or hardware-volume behavior. Live-provider CORS/encoders and a human listening comparison require separate evidence. Native visibility results must be read from the recorded browser attachment, not inferred from headless foreground playback.
The September 15 product runs passed Chromium and Firefox lifecycle and recovery
cases. Both peaked at two connections and ended with zero connections and FFmpeg
children. Chromium minimization and Firefox foreground-tab attempts both left
document.visibilityState as visible on this headless host. Real native hidden
window behavior therefore remains unverified. The dev Issues indicator in the
controlled-failure screenshots corresponds to injected HTTP 503 and EarlyEOF
transport errors; neither run recorded an uncaught page error.
One isolated-fixture Firefox run reported an uncaught error with the message
Object. A focused follow-up with stack and rejection diagnostics passed, but
the earlier error remains unclassified. The failed report is preserved as
playwright-artifacts/task-5-isolated-final.json, with its extracted failure in
event-playback-proof/firefox-unclassified-error.json. The clean follow-up does
not establish that this error is fixed.
Transport cancellation ownership
The shared transport uses mpegts.js 1.8.1's supported customLoader interface.
VrcdnFetchLoader owns fetch, the response reader, and all cancellation promises.
It keeps range/seek headers, configured headers, credentials, referrer policy,
redirect notification, byte offsets, content length, EOF and HTTP/read errors.
The mpegts parser, media-source player, stash buffering and public controls remain
unchanged. This loader serves the current HTTP(S) VRCDN MPEG-TS callers with Fetch
and ReadableStream; it does not add WebSocket or legacy XHR transport support.
A deterministic Firefox browser test held a naturally fulfilled response until after player unmount. The original library then called body.cancel without returning or handling its promise, producing an uncaught native AbortError from FetchStreamLoader.open. The maintained loader handles rejected cancellation only for its owned cleanup; active HTTP/read errors still reach the player error callback. No global rejection filter is installed. Reader cancellation and late response fulfillment are generation-scoped so obsolete callbacks cannot change a new request.
This reproduces and fixes a concrete teardown leak consistent with the historical Object symptom. The historical recording did not retain the exception fields, so its exact identity remains unprovable. Preserve that failed artifact alongside the cancellation RED/GREEN evidence; a clean rerun alone is not the explanation.
The final local verification ledger records the connected authored-event serialization test, final editor snapshots and remaining operational evidence.