aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-10-03 10:29:59 +0200
committerChristophe Besson <cbesson@gmail.com>2026-10-03 14:24:54 +0200
commitf73476d71f9b7268650c257db0eed3ab49b94849 (patch)
tree03dcd37787b66f32563ac4dc8cf93e7a94765d60 /packages/meshbay-hub/src/meshbay_hub/static
parent57758895bc7b0f7f21497194e81172f43d634852 (diff)
downloadmeshbay-f73476d71f9b7268650c257db0eed3ab49b94849.tar.gz
fix(client): a seek's first segments are held while it lands, not dropped
They are the new stream, header first. Dropped, a cast relay restarted at the landing got no ftyp/moov and the receiver gave up; they are now replayed in order once reinitAt/resumeAt is done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-player.js37
1 files changed, 36 insertions, 1 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js
index 2a4d2ea..4fa652b 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/video-player.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/video-player.js
@@ -438,6 +438,10 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) {
const castRestartPendingRef = useRef(false);
const castDeviceRef = useRef(null);
const castRestartGenRef = useRef(0);
+ // Segments of the *new* stream that arrive while its seek is still landing
+ // (`reinitAt`/`resumeAt` wait on the SourceBuffer). Null when not holding.
+ const heldRef = useRef(null);
+ const holdGenRef = useRef(0);
const landingPlayheadRef = useRef(false);
// The current Screen Wake Lock sentinel, if the browser granted one — see
// the effect below. Null on any platform/context that does not support it,
@@ -764,6 +768,7 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) {
ranges: describeRanges(),
});
awaitingInitRef.current = true;
+ heldRef.current = null; holdGenRef.current++;
// A seek supersedes a resume that had been asked for and not landed:
// this one empties the buffer, and taking the resume branch on its
// `stream_init` would leave the film we navigated away from in place.
@@ -798,6 +803,7 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) {
});
resumingRef.current = true;
awaitingInitRef.current = true;
+ heldRef.current = null; holdGenRef.current++;
seekTargetRef.current = null;
outstandingRef.current = STREAM_WINDOW;
console.log('[resume] request', +target.toFixed(1));
@@ -1064,7 +1070,23 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) {
castRestartPendingRef.current = true;
}
const land = resuming ? resumeAt : reinitAt;
- land(msg.start || 0).catch(() => {
+ // What follows this message on the ordered channel is the new
+ // stream, its header first. The landing waits on the SourceBuffer,
+ // and those segments used to arrive in that gap and be dropped as
+ // the old film's: the player kept its header from the start of the
+ // film and never noticed, but a cast relay restarted here got a
+ // stream beginning at a moof — no ftyp, no moov — and the receiver
+ // gave up on it within a second (measured on a phone). So they are
+ // held, and taken in order once the landing is done.
+ const hold = ++holdGenRef.current;
+ heldRef.current = [];
+ land(msg.start || 0).then(async () => {
+ while (holdGenRef.current === hold && heldRef.current && heldRef.current.length) {
+ await consumeSegment(heldRef.current.shift());
+ }
+ if (holdGenRef.current === hold) heldRef.current = null;
+ }).catch(() => {
+ if (holdGenRef.current === hold) heldRef.current = null;
setError(t('video.err_transport'));
setPhase('error');
});
@@ -1178,6 +1200,12 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) {
// for credit that cannot come. A race, which is why the same seek
// worked twice and hung on the third.
outstandingRef.current = Math.max(0, outstandingRef.current - 1);
+ // The new stream, arriving while its seek lands: kept, not dropped.
+ // Counted above already, so the replay must not count it again.
+ if (heldRef.current) {
+ if (!msg.file_id || msg.file_id === entry.id) heldRef.current.push(msg);
+ return;
+ }
if (awaitingInitRef.current) {
console.log('[seek] dropping segment (awaitingInit), outstanding:', outstandingRef.current);
}
@@ -1190,6 +1218,13 @@ function VideoPlayer({ entry, transportRef, gekRef, onClose, onDownload }) {
// would otherwise be decrypted against the wrong file — which fails,
// loudly, in the console, for something that is simply not ours.
if (msg.file_id && msg.file_id !== entry.id) return;
+ await consumeSegment(msg);
+ };
+
+ // Everything a segment goes through once it is known to belong to the
+ // stream being played: by arrival, or replayed after a landing.
+ const consumeSegment = async (msg) => {
+ if (cancelled) return;
try {
const plaintext = await window.MeshBayCrypto.decryptChunkBin(
gekRef.current, entry.id, msg.segment_index, msg.nonce, msg.ct);