Merge pull request #2294 from PR0M3TH3AN/refactor-playback-strategy-12158012329543805223

Refactor playback strategy logic into PlaybackStrategyService
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-02-09 23:30:52 -05:00
commit 144aa652ec
3 files changed
+422 -267

No files matched your search

+72 -267
View File
@@ -448,290 +448,95 @@ export function createPlaybackCoordinator(deps) {
* and falls back to WebTorrent when needed.
*/
async playVideoWithFallback(options = {}) {
const { url = "", magnet = "", trigger, forcedSource } = options || {};
const playbackStrategyService = this.playbackStrategyService;
if (!playbackStrategyService) {
this.showError("Playback strategy service is not available.");
return { source: null, error: new Error("Service missing") };
}
emit("playback-decision", {
method: forcedSource || (magnet ? "webtorrent" : "url"), // heuristic
details: {
url: Boolean(url),
magnet: Boolean(magnet),
forcedSource,
return playbackStrategyService.play(options, {
getModalVideo: () => this.modalVideo,
setModalVideo: (videoElement) => {
this.modalVideo = videoElement;
},
});
const hasTrigger = Object.prototype.hasOwnProperty.call(
options || {},
"trigger"
);
if (hasTrigger) {
this.setLastModalTrigger(trigger);
}
const sanitizedUrl = typeof url === "string" ? url.trim() : "";
const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : "";
const previousSource = this.playSource || null;
const requestSignature = JSON.stringify({
url: sanitizedUrl,
magnet: trimmedMagnet,
forcedSource,
});
const modalVideoIsConnected = (() => {
if (!this.modalVideo) {
return false;
}
if (typeof this.modalVideo.isConnected === "boolean") {
return this.modalVideo.isConnected;
}
const ownerDocument = this.modalVideo.ownerDocument ||
(typeof document !== "undefined" ? document : null);
if (ownerDocument?.contains) {
try {
return ownerDocument.contains(this.modalVideo);
} catch (error) {
devLogger.warn(
"[playVideoWithFallback] Failed to determine modal video connection state",
error,
);
getVideoModalElement: () =>
this.videoModal && typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null,
setVideoModalElement: (videoElement) => {
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(videoElement);
}
}
return true;
})();
},
cleanupVideoModalPoster: () => {
if (
this.videoModal &&
typeof this.videoModal.clearPosterCleanup === "function"
) {
this.videoModal.clearPosterCleanup();
}
},
ensureVideoModalReady: (opts) => this.ensureVideoModalReady(opts),
teardownVideoElement: (videoEl, opts) =>
this.teardownVideoElement(videoEl, opts),
applyModalLoadingPoster: () => this.applyModalLoadingPoster(),
const shouldReuseActiveSession =
modalVideoIsConnected &&
this.activePlaybackSession &&
typeof this.activePlaybackSession.matchesRequestSignature === "function" &&
this.activePlaybackSession.matchesRequestSignature(requestSignature);
if (shouldReuseActiveSession) {
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();
if (
previousSource === "torrent" &&
sanitizedUrl &&
this.playbackService &&
this.playbackService.torrentClient &&
typeof this.playbackService.torrentClient.cleanup === "function"
) {
try {
this.log(
"[playVideoWithFallback] Previous playback used WebTorrent; cleaning up before preparing hosted session.",
);
await this.playbackService.torrentClient.cleanup();
} catch (error) {
devLogger.warn(
"[playVideoWithFallback] Pre-playback torrent cleanup threw:",
error,
);
}
}
let modalVideoEl = this.modalVideo;
const modalVideoFromController =
this.videoModal && typeof this.videoModal.getVideoElement === "function"
? this.videoModal.getVideoElement()
: null;
if (modalVideoFromController && modalVideoFromController !== modalVideoEl) {
modalVideoEl = modalVideoFromController;
this.modalVideo = modalVideoEl;
}
const modalVideoConnected = Boolean(
modalVideoEl && modalVideoEl.isConnected,
);
if (!modalVideoEl || !modalVideoConnected) {
try {
const { videoElement } = await this.ensureVideoModalReady({
ensureVideoElement: true,
});
modalVideoEl = videoElement;
this.modalVideo = modalVideoEl;
} catch (error) {
this.log(
"[playVideoWithFallback] Failed to load video modal before playback:",
error
);
this.showError("Could not prepare the video player. Please try again.");
return { source: null, error };
}
}
if (!modalVideoEl) {
const error = new Error("Video element is not ready for playback.");
this.log(
"[playVideoWithFallback] Video element missing after modal load attempt."
);
this.showError("Video player is not ready yet. Please try again.");
return { source: null, error };
}
if (
this.videoModal &&
typeof this.videoModal.clearPosterCleanup === "function"
) {
try {
this.videoModal.clearPosterCleanup();
} catch (err) {
devLogger.warn(
"[playVideoWithFallback] video modal poster cleanup threw:",
err
);
}
}
const refreshedModal = this.teardownVideoElement(modalVideoEl, {
replaceNode: true,
});
if (refreshedModal) {
if (
this.videoModal &&
typeof this.videoModal.setVideoElement === "function"
) {
this.videoModal.setVideoElement(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(),
clearActiveIntervals: () => this.clearActiveIntervals(),
showModalWithPoster: () => this.showModalWithPoster(),
teardownVideoElement: (videoEl, options) =>
this.teardownVideoElement(videoEl, options),
probeUrl: (candidateUrl) => this.probeUrl(candidateUrl),
playViaWebTorrent: (magnetUri, options) =>
this.playViaWebTorrent(magnetUri, options),
playViaWebTorrent: (magnetUri, opts) =>
this.playViaWebTorrent(magnetUri, opts),
autoplay: () => this.autoplayModalVideo(),
unsupportedBtihMessage: UNSUPPORTED_BTITH_MESSAGE,
forcedSource,
});
this.activePlaybackSession = session;
this.activePlaybackResultPromise = null;
showError: (msg) => this.showError(msg),
setLastModalTrigger: (trigger) => this.setLastModalTrigger(trigger),
this.resetTorrentStats();
this.playSource = null;
resetTorrentStats: () => this.resetTorrentStats(),
setPlaySource: (source) => {
this.playSource = source;
},
setActivePlaybackSession: (session) => {
this.activePlaybackSession = session;
},
setActivePlaybackResultPromise: (promise) => {
this.activePlaybackResultPromise = promise;
},
const playbackConfig = session.getPlaybackConfig();
const magnetForPlayback = session.getMagnetForPlayback();
const fallbackMagnet = session.getFallbackMagnet();
const magnetProvided = session.getMagnetProvided();
if (this.currentVideo) {
this.currentVideo.magnet = magnetForPlayback;
this.currentVideo.normalizedMagnet = magnetForPlayback;
this.currentVideo.normalizedMagnetFallback = fallbackMagnet;
if (playbackConfig?.infoHash && !this.currentVideo.legacyInfoHash) {
this.currentVideo.legacyInfoHash = playbackConfig.infoHash;
}
this.currentVideo.torrentSupported = !!magnetForPlayback;
}
this.currentMagnetUri = magnetForPlayback || null;
// this.setCopyMagnetState(!!magnetForPlayback); // Removed
const unsubscribers = [];
const subscribe = (eventName, handler) => {
const off = session.on(eventName, handler);
unsubscribers.push(off);
};
subscribe("status", ({ message } = {}) => {
if (this.videoModal) {
this.videoModal.updateStatus(
typeof message === "string" ? message : ""
);
}
});
subscribe("video-prepared", ({ videoElement } = {}) => {
if (videoElement && videoElement !== this.modalVideo) {
this.modalVideo = videoElement;
}
});
subscribe("view-logging-request", ({ videoElement } = {}) => {
if (videoElement) {
this.preparePlaybackLogging(videoElement);
}
});
subscribe("poster-remove", ({ reason } = {}) => {
this.forceRemoveModalPoster(reason || "playback");
});
subscribe("sourcechange", ({ source } = {}) => {
this.playSource = source || null;
const usingTorrent = source === "torrent";
if (this.videoModal) {
this.videoModal.setTorrentStatsVisibility?.(usingTorrent);
}
});
subscribe("error", ({ error, message } = {}) => {
const displayMessage =
typeof message === "string"
? message
: error && error.message
? `Playback error: ${error.message}`
: "Playback error";
this.showError(displayMessage);
});
subscribe("finished", () => {
if (this.activePlaybackSession === session) {
this.activePlaybackSession = null;
this.activePlaybackResultPromise = null;
}
while (unsubscribers.length) {
const off = unsubscribers.pop();
if (typeof off === "function") {
try {
off();
} catch (err) {
devLogger.warn(
"[playVideoWithFallback] Listener cleanup error:",
err
);
updateCurrentVideoMetadata: (metadata) => {
if (this.currentVideo) {
this.currentVideo.magnet = metadata.magnet;
this.currentVideo.normalizedMagnet = metadata.normalizedMagnet;
this.currentVideo.normalizedMagnetFallback =
metadata.normalizedMagnetFallback;
if (metadata.legacyInfoHash && !this.currentVideo.legacyInfoHash) {
this.currentVideo.legacyInfoHash = metadata.legacyInfoHash;
}
this.currentVideo.torrentSupported = metadata.torrentSupported;
}
}
this.currentMagnetUri = metadata.magnet || null;
},
updateVideoModalStatus: (message) => {
if (this.videoModal) {
this.videoModal.updateStatus(message);
}
},
preparePlaybackLogging: (videoElement) =>
this.preparePlaybackLogging(videoElement),
forceRemoveModalPoster: (reason) => this.forceRemoveModalPoster(reason),
setTorrentStatsVisibility: (visible) => {
if (this.videoModal) {
this.videoModal.setTorrentStatsVisibility?.(visible);
}
},
});
const startPromise = session.start();
this.activePlaybackResultPromise = startPromise;
const result = await startPromise;
if (!result || result.error) {
return result;
}
emit("playback-started", {
method: result.source,
details: { startedAt: Date.now() },
});
return result;
},
async playVideoByEventId(eventId, playbackHint = {}) {
+345
View File
@@ -0,0 +1,345 @@
import { emit } from "../embedDiagnostics.js";
/**
* PlaybackStrategyService orchestrates the decision-making process for video playback.
* It determines whether to reuse an existing session, manages the lifecycle of the
* playback session, and coordinates between the PlaybackService (mechanism) and
* the Application (state/UI).
*/
export class PlaybackStrategyService {
constructor({ playbackService, logger }) {
this.playbackService = playbackService;
this.logger = logger || console;
this.activePlaybackSession = null;
this.activePlaybackResultPromise = null;
this.playSource = null;
}
log(...args) {
if (this.logger && typeof this.logger.log === "function") {
const prefix = "[PlaybackStrategy]";
if (typeof args[0] === "string") {
this.logger.log(`${prefix} ${args[0]}`, ...args.slice(1));
} else {
this.logger.log(prefix, ...args);
}
}
}
/**
* Orchestrates the playback flow.
*
* @param {object} options - Playback options (url, magnet, trigger, forcedSource).
* @param {object} context - Application context providing UI and State access.
* @returns {Promise<object>} The result of the playback attempt.
*/
async play(options = {}, context = {}) {
const { url = "", magnet = "", trigger, forcedSource } = options || {};
emit("playback-decision", {
method: forcedSource || (magnet ? "webtorrent" : "url"), // heuristic
details: {
url: Boolean(url),
magnet: Boolean(magnet),
forcedSource,
},
});
const hasTrigger = Object.prototype.hasOwnProperty.call(options || {}, "trigger");
if (hasTrigger && typeof context.setLastModalTrigger === "function") {
context.setLastModalTrigger(trigger);
}
const sanitizedUrl = typeof url === "string" ? url.trim() : "";
const trimmedMagnet = typeof magnet === "string" ? magnet.trim() : "";
const previousSource = this.playSource || null;
const requestSignature = JSON.stringify({
url: sanitizedUrl,
magnet: trimmedMagnet,
forcedSource,
});
const modalVideoIsConnected = (() => {
const modalVideo = context.getModalVideo ? context.getModalVideo() : null;
if (!modalVideo) {
return false;
}
if (typeof modalVideo.isConnected === "boolean") {
return modalVideo.isConnected;
}
const ownerDocument = modalVideo.ownerDocument ||
(typeof document !== "undefined" ? document : null);
if (ownerDocument?.contains) {
try {
return ownerDocument.contains(modalVideo);
} catch (error) {
this.log("Failed to determine modal video connection state", error);
}
}
return true;
})();
const shouldReuseActiveSession =
modalVideoIsConnected &&
this.activePlaybackSession &&
typeof this.activePlaybackSession.matchesRequestSignature === "function" &&
this.activePlaybackSession.matchesRequestSignature(requestSignature);
if (shouldReuseActiveSession) {
this.log("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 };
}
if (typeof context.waitForCleanup === "function") {
await context.waitForCleanup();
}
if (typeof context.cancelPendingViewLogging === "function") {
context.cancelPendingViewLogging();
}
if (
previousSource === "torrent" &&
sanitizedUrl &&
this.playbackService &&
this.playbackService.torrentClient &&
typeof this.playbackService.torrentClient.cleanup === "function"
) {
try {
this.log("Previous playback used WebTorrent; cleaning up before preparing hosted session.");
await this.playbackService.torrentClient.cleanup();
} catch (error) {
this.log("Pre-playback torrent cleanup threw:", error);
}
}
let modalVideoEl = context.getModalVideo ? context.getModalVideo() : null;
const modalVideoFromController =
context.getVideoModalElement ? context.getVideoModalElement() : null;
if (modalVideoFromController && modalVideoFromController !== modalVideoEl) {
modalVideoEl = modalVideoFromController;
if (context.setModalVideo) {
context.setModalVideo(modalVideoEl);
}
}
const modalVideoConnected = Boolean(modalVideoEl && modalVideoEl.isConnected);
if (!modalVideoEl || !modalVideoConnected) {
try {
if (typeof context.ensureVideoModalReady === "function") {
const { videoElement } = await context.ensureVideoModalReady({
ensureVideoElement: true,
});
modalVideoEl = videoElement;
if (context.setModalVideo) {
context.setModalVideo(modalVideoEl);
}
}
} catch (error) {
this.log("Failed to load video modal before playback:", error);
if (typeof context.showError === "function") {
context.showError("Could not prepare the video player. Please try again.");
}
return { source: null, error };
}
}
if (!modalVideoEl) {
const error = new Error("Video element is not ready for playback.");
this.log("Video element missing after modal load attempt.");
if (typeof context.showError === "function") {
context.showError("Video player is not ready yet. Please try again.");
}
return { source: null, error };
}
if (context.cleanupVideoModalPoster) {
try {
context.cleanupVideoModalPoster();
} catch (err) {
this.log("video modal poster cleanup threw:", err);
}
}
let refreshedModal = null;
if (typeof context.teardownVideoElement === "function") {
refreshedModal = context.teardownVideoElement(modalVideoEl, {
replaceNode: true,
});
}
if (refreshedModal) {
if (context.setVideoModalElement) {
context.setVideoModalElement(refreshedModal);
}
if (context.setModalVideo) {
context.setModalVideo(refreshedModal);
}
modalVideoEl = refreshedModal; // Ensure we use the new element
if (typeof context.applyModalLoadingPoster === "function") {
context.applyModalLoadingPoster();
}
} else {
if (typeof context.applyModalLoadingPoster === "function") {
context.applyModalLoadingPoster();
}
}
// Double check that modalVideoEl is current (it might have been replaced)
modalVideoEl = context.getModalVideo ? context.getModalVideo() : modalVideoEl;
const session = this.playbackService.createSession({
url: sanitizedUrl,
magnet: trimmedMagnet,
requestSignature,
videoElement: modalVideoEl,
waitForCleanup: context.waitForCleanup,
cancelPendingViewLogging: context.cancelPendingViewLogging,
clearActiveIntervals: context.clearActiveIntervals,
showModalWithPoster: context.showModalWithPoster,
teardownVideoElement: context.teardownVideoElement,
probeUrl: context.probeUrl,
playViaWebTorrent: context.playViaWebTorrent,
autoplay: context.autoplay,
unsupportedBtihMessage: context.unsupportedBtihMessage,
forcedSource,
});
this.activePlaybackSession = session;
if (typeof context.setActivePlaybackSession === "function") {
context.setActivePlaybackSession(session);
}
this.activePlaybackResultPromise = null;
if (typeof context.setActivePlaybackResultPromise === "function") {
context.setActivePlaybackResultPromise(null);
}
if (typeof context.resetTorrentStats === "function") {
context.resetTorrentStats();
}
this.playSource = null;
if (typeof context.setPlaySource === "function") {
context.setPlaySource(null);
}
const playbackConfig = session.getPlaybackConfig();
const magnetForPlayback = session.getMagnetForPlayback();
const fallbackMagnet = session.getFallbackMagnet();
// const magnetProvided = session.getMagnetProvided();
if (context.updateCurrentVideoMetadata) {
context.updateCurrentVideoMetadata({
magnet: magnetForPlayback,
normalizedMagnet: magnetForPlayback,
normalizedMagnetFallback: fallbackMagnet,
legacyInfoHash: playbackConfig?.infoHash || null,
torrentSupported: !!magnetForPlayback
});
}
// this.currentMagnetUri = magnetForPlayback || null; // Logic is in context if needed
const unsubscribers = [];
const subscribe = (eventName, handler) => {
const off = session.on(eventName, handler);
unsubscribers.push(off);
};
subscribe("status", ({ message } = {}) => {
if (typeof context.updateVideoModalStatus === "function") {
context.updateVideoModalStatus(typeof message === "string" ? message : "");
}
});
subscribe("video-prepared", ({ videoElement } = {}) => {
if (videoElement && videoElement !== (context.getModalVideo ? context.getModalVideo() : null)) {
if (context.setModalVideo) {
context.setModalVideo(videoElement);
}
}
});
subscribe("view-logging-request", ({ videoElement } = {}) => {
if (videoElement && typeof context.preparePlaybackLogging === "function") {
context.preparePlaybackLogging(videoElement);
}
});
subscribe("poster-remove", ({ reason } = {}) => {
if (typeof context.forceRemoveModalPoster === "function") {
context.forceRemoveModalPoster(reason || "playback");
}
});
subscribe("sourcechange", ({ source } = {}) => {
this.playSource = source || null;
if (typeof context.setPlaySource === "function") {
context.setPlaySource(this.playSource);
}
const usingTorrent = source === "torrent";
if (typeof context.setTorrentStatsVisibility === "function") {
context.setTorrentStatsVisibility(usingTorrent);
}
});
subscribe("error", ({ error, message } = {}) => {
const displayMessage =
typeof message === "string"
? message
: error && error.message
? `Playback error: ${error.message}`
: "Playback error";
if (typeof context.showError === "function") {
context.showError(displayMessage);
}
});
subscribe("finished", () => {
if (this.activePlaybackSession === session) {
this.activePlaybackSession = null;
if (typeof context.setActivePlaybackSession === "function") {
context.setActivePlaybackSession(null);
}
this.activePlaybackResultPromise = null;
if (typeof context.setActivePlaybackResultPromise === "function") {
context.setActivePlaybackResultPromise(null);
}
}
while (unsubscribers.length) {
const off = unsubscribers.pop();
if (typeof off === "function") {
try {
off();
} catch (err) {
this.log("Listener cleanup error:", err);
}
}
}
});
const startPromise = session.start();
this.activePlaybackResultPromise = startPromise;
if (typeof context.setActivePlaybackResultPromise === "function") {
context.setActivePlaybackResultPromise(startPromise);
}
const result = await startPromise;
if (!result || result.error) {
return result;
}
emit("playback-started", {
method: result.source,
details: { startedAt: Date.now() },
});
return result;
}
}
export default PlaybackStrategyService;
+5
View File
@@ -6,6 +6,7 @@ import { createWatchHistoryRenderer } from "../historyView.js";
import WatchHistoryController from "./watchHistoryController.js";
import WatchHistoryTelemetry from "../services/watchHistoryTelemetry.js";
import PlaybackService from "../services/playbackService.js";
import PlaybackStrategyService from "../services/playbackStrategyService.js";
import AuthService from "../services/authService.js";
import DiscussionCountService from "../services/discussionCountService.js";
import CommentThreadService from "../services/commentThreadService.js";
@@ -350,6 +351,10 @@ export default class ApplicationBootstrap {
},
},
});
app.playbackStrategyService = new PlaybackStrategyService({
playbackService: app.playbackService,
logger: devLogger,
});
app.activePlaybackResultPromise = null;
app.activePlaybackSession = null;