mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-06 12:52:26 +00:00
update
This commit is contained in:
1 parent
589925f1fe
commit
ef29bb6eb3
97 files changed
+46855
No files matched your search
+345
@@ -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;
|
||||
Reference in new issue
Block a user