[playback] harden WebTorrent streaming + add forced-source test deep links

WebTorrent (handleTorrentStream) hardening:
- Resolve only when playback is actually viable (loadeddata/canplay/playing),
  not the instant streamTo() is called. A dead/too-slow swarm is no longer
  reported as success, so the caller's fallback can run.
- Pre-playback torrent errors reject (caller falls back); post-playback errors
  go to an onPlaybackError hook instead of a dead reject().
- Mid-stream stall watchdog surfaces an onStall hook ("waiting for peers").
- Fix a <video> listener leak: stream listeners are tracked and torn down
  before the next stream and on cleanup() (the modal reuses one element).
- Stream the largest decodable file from a multi-file torrent and deselect the
  rest (wider ext set: mp4/m4v/webm/mkv/mov/ogv/ogg) so the swarm isn't spent
  downloading files we never play.
playbackCoordinator wires onStall/onPlaybackError to user-facing status.

Forced-source deep links (test affordance):
- The main app honors ?v=<nevent>&playback=torrent|url (mirrors the embed
  player) to force a playback path; a missing source shows a clear message.
- The video share menu gains "Copy WebTorrent URL" / "Copy CDN URL" items that
  copy the forced-source deep link (disabled with a reason when unavailable).

Tests: webtorrent-handlers (resolve-on-readiness, pre/post-ready errors, stall
fires/clears, largest-file select/deselect, no listener accumulation),
playback-forced-source (param normalization), videoMenuRenderers (copy items +
availability). All mutation-verified.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-18 12:15:27 -04:00
1 parent 577ea5af15
commit 4e5bfaa6af
7 files changed
+678 -21

No files matched your search

+31
View File
@@ -3290,6 +3290,35 @@ export class VideoModal {
);
}
/**
* Build a `?v=<nevent>&playback=<source>` deep link for the given video that
* forces a specific playback path (used by the share-menu "Copy WebTorrent
* URL" / "Copy CDN URL" items). Returns "" if a link can't be built.
* @param {object} video
* @param {'torrent'|'url'} playback
* @returns {string}
*/
buildForcedSourceShareUrl(video, playback) {
const eventId =
video && typeof video.id === "string" ? video.id.trim() : "";
if (!eventId) {
return "";
}
const win = this.window || globalThis;
const neventEncode = win?.NostrTools?.nip19?.neventEncode;
const loc = win?.location;
if (typeof neventEncode !== "function" || !loc) {
return "";
}
try {
const nevent = neventEncode({ id: eventId });
const base = `${loc.origin}${loc.pathname}`;
return `${base}?v=${encodeURIComponent(nevent)}&playback=${playback}`;
} catch (error) {
return "";
}
}
setupModalSharePopover() {
if (!this.playerModal || !this.shareBtn) {
return;
@@ -3319,6 +3348,8 @@ export class VideoModal {
hasSigner: this.shareNostrAuthState.hasSigner,
hasMagnet: Boolean(this.modalMoreMenuContext.playbackMagnet), // Re-use magnet from context
hasCdn: Boolean(this.modalMoreMenuContext.playbackUrl),
buildForcedSourceUrl: (playback) =>
this.buildForcedSourceShareUrl(this.activeVideo, playback),
});
if (panel) {
container.appendChild(panel);
+72
View File
@@ -316,6 +316,13 @@ export function createVideoShareMenuPanel({
hasSigner = false,
hasMagnet = false,
hasCdn = false,
// Optional builder for forced-source test deep links: fn('torrent'|'url') ->
// a `?v=<nevent>&playback=...` URL (or "" when unavailable). When provided,
// the menu offers "Copy WebTorrent URL" / "Copy CDN URL" items that copy a
// link which always plays via that source — handy for testing one path
// consistently. `clipboard` is injectable for tests.
buildForcedSourceUrl = null,
clipboard = null,
designSystem = null,
} = {}) {
const doc = resolveDocument(documentRef);
@@ -370,6 +377,71 @@ export function createVideoShareMenuPanel({
cdnBtn.title = "No CDN link available";
}
// Forced-source test deep links. Self-contained: they copy a link via the
// injected builder + clipboard rather than routing through the shared menu
// dispatch, so they work regardless of how the share popover is wired.
if (typeof buildForcedSourceUrl === "function") {
const resolveClipboard = () =>
clipboard ||
(typeof navigator !== "undefined" ? navigator.clipboard : null);
const wireForcedCopy = (button, playback, available, unavailableTitle) => {
if (!available) {
button.disabled = true;
button.classList.add("opacity-50", "cursor-not-allowed");
button.title = unavailableTitle;
return;
}
const originalText = button.textContent;
button.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
const url = buildForcedSourceUrl(playback) || "";
if (!url) {
return;
}
const clip = resolveClipboard();
try {
if (clip && typeof clip.writeText === "function") {
await clip.writeText(url);
}
button.textContent = "Copied!";
} catch (error) {
button.textContent = "Copy failed";
}
if (typeof setTimeout === "function") {
setTimeout(() => {
button.textContent = originalText;
}, 1200);
}
});
};
const torrentLinkBtn = appendMenuAction(doc, list, {
text: "Copy WebTorrent URL",
action: "copy-webtorrent-url",
dataset: { eventId: metadata.id },
});
wireForcedCopy(
torrentLinkBtn,
"torrent",
hasMagnet,
"No WebTorrent source for this video",
);
const cdnLinkBtn = appendMenuAction(doc, list, {
text: "Copy CDN URL",
action: "copy-cdn-url",
dataset: { eventId: metadata.id },
});
wireForcedCopy(
cdnLinkBtn,
"url",
hasCdn,
"No CDN source for this video",
);
}
return panel;
}