diff --git a/js/app.js b/js/app.js index 8c11925f..10e44621 100644 --- a/js/app.js +++ b/js/app.js @@ -238,29 +238,30 @@ class Application { deriveTorrentPlaybackConfig, isValidMagnetUri, urlFirstEnabled: URL_FIRST_ENABLED, - analyticsCallbacks: { - "session-start": (detail) => { - const urlProvided = detail?.urlProvided ? "true" : "false"; - const magnetProvided = detail?.magnetProvided ? "true" : "false"; - const magnetUsable = detail?.magnetUsable ? "true" : "false"; - this.log( - `[playVideoWithFallback] Session start urlProvided=${urlProvided} magnetProvided=${magnetProvided} magnetUsable=${magnetUsable}` - ); - }, - fallback: (detail) => { - if (detail?.reason) { + analyticsCallbacks: { + "session-start": (detail) => { + const urlProvided = detail?.urlProvided ? "true" : "false"; + const magnetProvided = detail?.magnetProvided ? "true" : "false"; + const magnetUsable = detail?.magnetUsable ? "true" : "false"; this.log( - `[playVideoWithFallback] Falling back to WebTorrent (${detail.reason}).` + `[playVideoWithFallback] Session start urlProvided=${urlProvided} magnetProvided=${magnetProvided} magnetUsable=${magnetUsable}` ); - } + }, + fallback: (detail) => { + if (detail?.reason) { + this.log( + `[playVideoWithFallback] Falling back to WebTorrent (${detail.reason}).` + ); + } + }, + error: (detail) => { + if (detail?.error) { + this.log("[playVideoWithFallback] Playback error observed", detail.error); + } + }, }, - error: (detail) => { - if (detail?.error) { - this.log("[playVideoWithFallback] Playback error observed", detail.error); - } - }, - }, }); + this.activePlaybackResultPromise = null; this.activePlaybackSession = null; this.authService = @@ -7009,11 +7010,32 @@ class Application { * and falls back to WebTorrent when needed. */ async playVideoWithFallback({ url = "", magnet = "" } = {}) { - await this.waitForCleanup(); - this.cancelPendingViewLogging(); - const sanitizedUrl = typeof url === "string" ? url.trim() : ""; const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : ""; + const requestSignature = JSON.stringify({ + url: sanitizedUrl, + magnet: trimmedMagnet, + }); + + if ( + this.activePlaybackSession && + typeof this.activePlaybackSession.matchesRequestSignature === "function" && + this.activePlaybackSession.matchesRequestSignature(requestSignature) + ) { + this.log( + "[playVideoWithFallback] Duplicate playback request detected; reusing active session." + ); + if (this.activePlaybackResultPromise) { + return this.activePlaybackResultPromise; + } + if (typeof this.activePlaybackSession.getResult === "function") { + return this.activePlaybackSession.getResult(); + } + return { source: null }; + } + + await this.waitForCleanup(); + this.cancelPendingViewLogging(); let modalVideoEl = this.modalVideo; if (!modalVideoEl && this.videoModal) { @@ -7062,11 +7084,15 @@ class Application { if (refreshedModal) { this.modalVideo = refreshedModal; modalVideoEl = this.modalVideo; + this.applyModalLoadingPoster(); + } else { + this.applyModalLoadingPoster(); } const session = this.playbackService.createSession({ url: sanitizedUrl, magnet: trimmedMagnet, + requestSignature, videoElement: this.modalVideo, waitForCleanup: () => this.waitForCleanup(), cancelPendingViewLogging: () => this.cancelPendingViewLogging(), @@ -7082,6 +7108,7 @@ class Application { }); this.activePlaybackSession = session; + this.activePlaybackResultPromise = null; this.resetTorrentStats(); this.playSource = null; @@ -7150,6 +7177,7 @@ class Application { subscribe("finished", () => { if (this.activePlaybackSession === session) { this.activePlaybackSession = null; + this.activePlaybackResultPromise = null; } while (unsubscribers.length) { const off = unsubscribers.pop(); @@ -7166,7 +7194,9 @@ class Application { } }); - const result = await session.start(); + const startPromise = session.start(); + this.activePlaybackResultPromise = startPromise; + const result = await startPromise; if (!result || result.error) { return result; diff --git a/js/services/playbackService.js b/js/services/playbackService.js index 4551f5bf..b00cfa53 100644 --- a/js/services/playbackService.js +++ b/js/services/playbackService.js @@ -234,11 +234,20 @@ class PlaybackSession extends SimpleEventEmitter { this.options = options; this.watchdogCleanup = null; this.result = { source: null }; + this.finished = false; + this.startPromise = null; this.sanitizedUrl = typeof options.url === "string" ? options.url.trim() : ""; this.trimmedMagnet = typeof options.magnet === "string" ? options.magnet.trim() : ""; + this.requestSignature = + typeof options.requestSignature === "string" + ? options.requestSignature + : JSON.stringify({ + url: this.sanitizedUrl, + magnet: this.trimmedMagnet, + }); if (typeof service.deriveTorrentPlaybackConfig === "function") { this.playbackConfig = service.deriveTorrentPlaybackConfig({ @@ -273,6 +282,24 @@ class PlaybackSession extends SimpleEventEmitter { this.magnetProvided = !!this.playbackConfig.provided; } + isActive() { + return !this.finished; + } + + matchesRequestSignature(signature) { + if (!this.isActive()) { + return false; + } + if (typeof signature !== "string" || !signature) { + return false; + } + return signature === this.requestSignature; + } + + getResult() { + return this.result; + } + getPlaybackConfig() { return this.playbackConfig; } @@ -362,6 +389,14 @@ class PlaybackSession extends SimpleEventEmitter { } async start() { + if (this.startPromise) { + return this.startPromise; + } + this.startPromise = this.execute(); + return this.startPromise; + } + + async execute() { const { videoElement, waitForCleanup, @@ -376,6 +411,7 @@ class PlaybackSession extends SimpleEventEmitter { } = this.options; this.result = { source: null }; + this.finished = false; const detail = { urlProvided: !!this.sanitizedUrl, @@ -721,6 +757,7 @@ class PlaybackSession extends SimpleEventEmitter { return this.result; } finally { this.cleanupWatchdog(); + this.finished = true; if (this.result) { this.emit("finished", this.result); }