mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat: make default playback source configurable
- Added `DEFAULT_PLAYBACK_SOURCE` to `config/instance-config.js` to allow choosing between "url" (default) and "torrent" priority. - Updated `js/constants.js` to derive `URL_FIRST_ENABLED` flag from the configuration. - Refactored `PlaybackService.execute()` in `js/services/playbackService.js` to robustly support "Torrent First" logic with fallback to URL, while maintaining "URL First" with fallback to Torrent. - Added unit tests in `tests/services/playbackService_order.test.mjs` to verify playback order and fallback mechanics.
This commit is contained in:
1 parent
0c2afb2a2a
commit
c22037a575
5 files changed
+350
-102
No files matched your search
@@ -427,3 +427,12 @@ export const VIEW_COUNT_CACHE_TTL_MS = 5 * 60 * 1000;
|
||||
* still letting operators retry if a publish genuinely failed.
|
||||
*/
|
||||
export const ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS = 5 * 60;
|
||||
|
||||
/**
|
||||
* Preferred playback source strategy.
|
||||
*
|
||||
* Options:
|
||||
* - "url": Try CDN/URL first, fall back to WebTorrent.
|
||||
* - "torrent": Try WebTorrent first, fall back to CDN/URL.
|
||||
*/
|
||||
export const DEFAULT_PLAYBACK_SOURCE = "url";
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
VIEW_COUNT_BACKFILL_MAX_DAYS,
|
||||
VIEW_COUNT_CACHE_TTL_MS,
|
||||
ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS,
|
||||
DEFAULT_PLAYBACK_SOURCE,
|
||||
} from "../config/instance-config.js";
|
||||
|
||||
export const WATCH_HISTORY_VERSION_TAG_VALUE = "2";
|
||||
@@ -133,3 +134,4 @@ export { VIEW_COUNT_DEDUPE_WINDOW_SECONDS };
|
||||
export { VIEW_COUNT_BACKFILL_MAX_DAYS };
|
||||
export { VIEW_COUNT_CACHE_TTL_MS };
|
||||
export { ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS };
|
||||
export { DEFAULT_PLAYBACK_SOURCE };
|
||||
+2
-1
@@ -4,6 +4,7 @@ import {
|
||||
DEFAULT_TRUST_SEED_NPUBS as CONFIG_DEFAULT_TRUST_SEED_NPUBS,
|
||||
DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as CONFIG_DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD,
|
||||
DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as CONFIG_DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD,
|
||||
DEFAULT_PLAYBACK_SOURCE,
|
||||
IS_DEV_MODE,
|
||||
} from "./config.js";
|
||||
|
||||
@@ -144,7 +145,7 @@ export const DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD =
|
||||
export const DEFAULT_TRUST_SEED_NPUBS = SANITIZED_DEFAULT_TRUST_SEED_NPUBS;
|
||||
|
||||
const DEFAULT_FLAGS = Object.freeze({
|
||||
URL_FIRST_ENABLED: true, // try URL before magnet in the player
|
||||
URL_FIRST_ENABLED: DEFAULT_PLAYBACK_SOURCE !== "torrent", // try URL before magnet in the player
|
||||
FEATURE_WATCH_HISTORY_V2: true,
|
||||
FEATURE_PUBLISH_NIP71: false,
|
||||
FEATURE_HASHTAG_PREFERENCES: false,
|
||||
|
||||
+110
-101
@@ -550,57 +550,40 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
// -- Fallback Logic --
|
||||
// This function switches from the "URL" path to the "Torrent" path.
|
||||
let fallbackStarted = false;
|
||||
const startTorrentFallback = async (reason) => {
|
||||
if (fallbackStarted) {
|
||||
this.service.log(
|
||||
`[playVideoWithFallback] Duplicate fallback request ignored (${reason}).`
|
||||
);
|
||||
return null;
|
||||
const resetVideoElement = () => {
|
||||
if (!activeVideoEl) return;
|
||||
try {
|
||||
activeVideoEl.pause();
|
||||
} catch (err) {
|
||||
this.service.log("[playVideoWithFallback] Pause threw during reset:", err);
|
||||
}
|
||||
fallbackStarted = true;
|
||||
try {
|
||||
activeVideoEl.removeAttribute("src");
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
activeVideoEl.src = "";
|
||||
activeVideoEl.srcObject = null;
|
||||
try {
|
||||
activeVideoEl.load();
|
||||
} catch (err) {
|
||||
this.service.log("[playVideoWithFallback] Load threw during reset:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// --- Attempt Torrent Logic ---
|
||||
let torrentAttempted = false;
|
||||
const attemptTorrentPlayback = async (reason) => {
|
||||
if (torrentAttempted) return null;
|
||||
torrentAttempted = true;
|
||||
|
||||
this.cleanupWatchdog();
|
||||
cleanupHostedUrlStatusListeners();
|
||||
cleanupDebugListeners();
|
||||
|
||||
// Reset the video element state so WebTorrent can take over clean.
|
||||
if (activeVideoEl) {
|
||||
try {
|
||||
activeVideoEl.pause();
|
||||
} catch (err) {
|
||||
this.service.log(
|
||||
"[playVideoWithFallback] Ignoring pause error before torrent fallback:",
|
||||
err
|
||||
);
|
||||
}
|
||||
try {
|
||||
activeVideoEl.removeAttribute("src");
|
||||
} catch (err) {
|
||||
// ignore attribute removal errors
|
||||
}
|
||||
activeVideoEl.src = "";
|
||||
activeVideoEl.srcObject = null;
|
||||
try {
|
||||
activeVideoEl.load();
|
||||
} catch (err) {
|
||||
this.service.log(
|
||||
"[playVideoWithFallback] Ignoring load error before torrent fallback:",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
resetVideoElement();
|
||||
|
||||
if (!this.magnetForPlayback) {
|
||||
const message =
|
||||
"Hosted playback failed and no magnet fallback is available.";
|
||||
this.emit("status", { message });
|
||||
this.emit("sourcechange", { source: null });
|
||||
const error = new Error(message);
|
||||
this.service.handleAnalyticsEvent("error", { error });
|
||||
this.emit("error", { error, message });
|
||||
this.result = { source: null, error };
|
||||
// No magnet available to try
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -609,33 +592,29 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
this.emit("fallback", { reason });
|
||||
|
||||
if (typeof playViaWebTorrent !== "function") {
|
||||
const error = new Error("No torrent playback handler provided.");
|
||||
this.service.handleAnalyticsEvent("error", { error });
|
||||
this.emit("error", { error, message: error.message });
|
||||
this.result = { source: null, error };
|
||||
return null;
|
||||
throw new Error("No torrent playback handler provided.");
|
||||
}
|
||||
|
||||
// Delegate to the WebTorrent driver (see js/webtorrent.js interactions in app.js)
|
||||
const torrentInstance = await playViaWebTorrent(this.magnetForPlayback, {
|
||||
fallbackMagnet: this.fallbackMagnet,
|
||||
urlList: webSeedCandidates,
|
||||
});
|
||||
|
||||
this.service.handleAnalyticsEvent("sourcechange", { source: "torrent" });
|
||||
this.emit("sourcechange", { source: "torrent" });
|
||||
if (typeof autoplay === "function") {
|
||||
autoplay();
|
||||
}
|
||||
this.result = { source: "torrent", torrentInstance };
|
||||
return torrentInstance;
|
||||
|
||||
const result = { source: "torrent", torrentInstance };
|
||||
this.result = result;
|
||||
return result;
|
||||
};
|
||||
|
||||
// -- Primary Path: Direct URL --
|
||||
if (
|
||||
this.service.urlFirstEnabled &&
|
||||
httpsUrl &&
|
||||
forcedSource !== "torrent"
|
||||
) {
|
||||
// --- Attempt URL Logic ---
|
||||
const attemptHostedPlayback = async () => {
|
||||
if (!httpsUrl) return null;
|
||||
|
||||
this.emit("status", { message: "Checking hosted URL..." });
|
||||
this.service.log(
|
||||
`[playVideoWithFallback] Probing hosted URL ${httpsUrl} (readyState=${activeVideoEl.readyState} networkState=${activeVideoEl.networkState}).`
|
||||
@@ -644,20 +623,18 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
let hostedStatusResolved = false;
|
||||
const hostedStatusHandlers = [];
|
||||
const addHostedStatusListener = (eventName, handler, options) => {
|
||||
if (!activeVideoEl) {
|
||||
return;
|
||||
}
|
||||
if (!activeVideoEl) return;
|
||||
activeVideoEl.addEventListener(eventName, handler, options);
|
||||
hostedStatusHandlers.push([eventName, handler, options]);
|
||||
};
|
||||
|
||||
const markHostedUrlAsLive = () => {
|
||||
if (hostedStatusResolved) {
|
||||
return;
|
||||
}
|
||||
if (hostedStatusResolved) return;
|
||||
hostedStatusResolved = true;
|
||||
this.emit("status", { message: HOSTED_URL_SUCCESS_MESSAGE });
|
||||
cleanupHostedUrlStatusListeners();
|
||||
};
|
||||
|
||||
const maybeMarkHostedUrl = () => {
|
||||
if (
|
||||
hostedStatusResolved ||
|
||||
@@ -670,6 +647,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
markHostedUrlAsLive();
|
||||
}
|
||||
};
|
||||
|
||||
cleanupHostedUrlStatusListeners = () => {
|
||||
if (!hostedStatusHandlers.length || !activeVideoEl) {
|
||||
hostedStatusHandlers.length = 0;
|
||||
@@ -682,6 +660,7 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
hostedStatusHandlers.length = 0;
|
||||
cleanupHostedUrlStatusListeners = () => {};
|
||||
};
|
||||
|
||||
addHostedStatusListener("playing", markHostedUrlAsLive, { once: true });
|
||||
addHostedStatusListener("loadeddata", maybeMarkHostedUrl);
|
||||
addHostedStatusListener("canplay", maybeMarkHostedUrl);
|
||||
@@ -718,23 +697,18 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
|
||||
const playbackOutcomePromise = new Promise((resolve) => {
|
||||
outcomeResolver = (value) => {
|
||||
if (outcomeResolved) {
|
||||
return;
|
||||
}
|
||||
if (outcomeResolved) return;
|
||||
outcomeResolved = true;
|
||||
resolve(value);
|
||||
};
|
||||
});
|
||||
|
||||
// Install the watchdogs before we assign src and play.
|
||||
const attachWatchdogs = ({ stallMs = 8000 } = {}) => {
|
||||
this.registerWatchdogs(activeVideoEl, {
|
||||
stallMs,
|
||||
onSuccess: () => outcomeResolver({ status: "success" }),
|
||||
onFallback: (reason) => {
|
||||
if (autoplayBlocked && reason === "stall") {
|
||||
// If browser blocked autoplay, a stall is expected (video is paused).
|
||||
// Don't fallback yet; wait for user interaction.
|
||||
this.service.log(
|
||||
"[playVideoWithFallback] Autoplay blocked; waiting for user gesture before falling back."
|
||||
);
|
||||
@@ -742,10 +716,9 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
message: "Press play to start the hosted video.",
|
||||
});
|
||||
this.emit("autoplay-blocked", { reason });
|
||||
attachWatchdogs({ stallMs: 0 }); // Disable timer
|
||||
attachWatchdogs({ stallMs: 0 });
|
||||
return;
|
||||
}
|
||||
|
||||
outcomeResolver({ status: "fallback", reason });
|
||||
},
|
||||
});
|
||||
@@ -804,39 +777,74 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
this.emit("sourcechange", { source: "url" });
|
||||
cleanupHostedUrlStatusListeners();
|
||||
cleanupDebugListeners();
|
||||
this.result = { source: "url" };
|
||||
return this.result;
|
||||
const result = { source: "url" };
|
||||
this.result = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
// If we are here, the watchdog triggered a fallback.
|
||||
const fallbackReason =
|
||||
playbackOutcome?.reason || "watchdog-triggered";
|
||||
const torrentInstance = await startTorrentFallback(fallbackReason);
|
||||
this.result = torrentInstance
|
||||
? { source: "torrent", torrentInstance }
|
||||
: this.result;
|
||||
return this.result;
|
||||
// Fallback triggered
|
||||
const fallbackReason = playbackOutcome?.reason || "watchdog-triggered";
|
||||
return { status: "fallback", reason: fallbackReason };
|
||||
}
|
||||
|
||||
this.service.log(
|
||||
`[playVideoWithFallback] Hosted URL probe reported "${probeOutcome}"; deferring to WebTorrent.`
|
||||
);
|
||||
cleanupHostedUrlStatusListeners();
|
||||
cleanupDebugListeners();
|
||||
return { status: "fallback", reason: "probe-failed" };
|
||||
};
|
||||
|
||||
// --- Execution Flow ---
|
||||
|
||||
let tryUrlFirst = this.service.urlFirstEnabled;
|
||||
if (forcedSource === "url") tryUrlFirst = true;
|
||||
if (forcedSource === "torrent") tryUrlFirst = false;
|
||||
|
||||
if (tryUrlFirst) {
|
||||
if (httpsUrl) {
|
||||
const urlResult = await attemptHostedPlayback();
|
||||
if (urlResult && urlResult.source === "url") {
|
||||
return urlResult;
|
||||
}
|
||||
// URL Failed, try Torrent
|
||||
const fallbackReason = urlResult?.reason || "url-unavailable";
|
||||
if (this.magnetForPlayback) {
|
||||
return await attemptTorrentPlayback(fallbackReason);
|
||||
}
|
||||
} else if (this.magnetForPlayback) {
|
||||
// No URL, try Torrent
|
||||
return await attemptTorrentPlayback("url-missing");
|
||||
}
|
||||
} else {
|
||||
// Try Torrent First
|
||||
if (this.magnetForPlayback) {
|
||||
try {
|
||||
const torrentResult = await attemptTorrentPlayback("preference");
|
||||
if (torrentResult) return torrentResult;
|
||||
} catch (err) {
|
||||
this.service.log("[playVideoWithFallback] Torrent preference failed, trying URL:", err);
|
||||
// Clean up before trying URL
|
||||
if (
|
||||
this.service.torrentClient &&
|
||||
typeof this.service.torrentClient.cleanup === "function"
|
||||
) {
|
||||
await this.service.torrentClient.cleanup();
|
||||
}
|
||||
resetVideoElement();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to URL
|
||||
if (httpsUrl) {
|
||||
const urlResult = await attemptHostedPlayback();
|
||||
if (urlResult && urlResult.source === "url") {
|
||||
return urlResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- Secondary Path: Magnet / Torrent --
|
||||
if (this.magnetForPlayback) {
|
||||
const torrentInstance = await startTorrentFallback("magnet-primary");
|
||||
this.result = torrentInstance
|
||||
? { source: "torrent", torrentInstance }
|
||||
: this.result;
|
||||
return this.result;
|
||||
}
|
||||
|
||||
const message = this.magnetProvided && !this.magnetForPlayback
|
||||
? unsupportedBtihMessage
|
||||
: "No playable source found.";
|
||||
const message =
|
||||
this.magnetProvided && !this.magnetForPlayback
|
||||
? unsupportedBtihMessage
|
||||
: "No playable source found.";
|
||||
this.emit("status", { message });
|
||||
this.service.handleAnalyticsEvent("sourcechange", { source: null });
|
||||
this.emit("sourcechange", { source: null });
|
||||
@@ -848,9 +856,10 @@ class PlaybackSession extends SimpleEventEmitter {
|
||||
return this.result;
|
||||
} catch (error) {
|
||||
this.service.log("Error in playVideoWithFallback:", error);
|
||||
const message = error && error.message
|
||||
? `Playback error: ${error.message}`
|
||||
: "Playback error";
|
||||
const message =
|
||||
error && error.message
|
||||
? `Playback error: ${error.message}`
|
||||
: "Playback error";
|
||||
this.service.handleAnalyticsEvent("error", { error });
|
||||
this.emit("error", { error, message });
|
||||
this.result = { source: null, error };
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import { describe, test, mock, before, after, beforeEach, afterEach } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { PlaybackService } from "../../js/services/playbackService.js";
|
||||
|
||||
describe("PlaybackService Ordering", () => {
|
||||
let video;
|
||||
let window;
|
||||
|
||||
before(() => {
|
||||
// Mock global window/document
|
||||
global.window = {
|
||||
HTMLMediaElement: {
|
||||
HAVE_NOTHING: 0,
|
||||
HAVE_METADATA: 1,
|
||||
HAVE_CURRENT_DATA: 2,
|
||||
HAVE_FUTURE_DATA: 3,
|
||||
HAVE_ENOUGH_DATA: 4,
|
||||
},
|
||||
Event: class Event { constructor(type) { this.type = type; } },
|
||||
localStorage: {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {}
|
||||
}
|
||||
};
|
||||
global.document = {
|
||||
createElement: (tag) => {
|
||||
if (tag === 'video') return video;
|
||||
return {};
|
||||
}
|
||||
};
|
||||
window = global.window;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
delete global.window;
|
||||
delete global.document;
|
||||
});
|
||||
|
||||
describe("Playback Execution Order", () => {
|
||||
let playMock;
|
||||
let loadMock;
|
||||
let pauseMock;
|
||||
let eventListeners;
|
||||
|
||||
beforeEach(() => {
|
||||
mock.timers.enable({ apis: ['setTimeout'] });
|
||||
eventListeners = {};
|
||||
|
||||
video = {
|
||||
play: mock.fn(async () => {}),
|
||||
load: mock.fn(),
|
||||
pause: mock.fn(),
|
||||
removeAttribute: mock.fn(),
|
||||
addEventListener: (event, handler) => {
|
||||
if (!eventListeners[event]) eventListeners[event] = [];
|
||||
eventListeners[event].push(handler);
|
||||
},
|
||||
removeEventListener: (event, handler) => {
|
||||
if (eventListeners[event]) {
|
||||
eventListeners[event] = eventListeners[event].filter(h => h !== handler);
|
||||
}
|
||||
},
|
||||
dispatchEvent: (event) => {
|
||||
if (eventListeners[event.type]) {
|
||||
eventListeners[event.type].forEach(h => h(event));
|
||||
}
|
||||
},
|
||||
readyState: 0,
|
||||
networkState: 0,
|
||||
dataset: {},
|
||||
muted: false
|
||||
};
|
||||
|
||||
playMock = video.play;
|
||||
loadMock = video.load;
|
||||
pauseMock = video.pause;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.timers.reset();
|
||||
});
|
||||
|
||||
test("urlFirstEnabled=true: Tries URL first, succeeds", async () => {
|
||||
const service = new PlaybackService({
|
||||
logger: () => {},
|
||||
urlFirstEnabled: true,
|
||||
isValidMagnetUri: () => true,
|
||||
});
|
||||
|
||||
const probeUrl = mock.fn(async () => ({ outcome: "good", status: 200 }));
|
||||
const playViaWebTorrent = mock.fn(async () => ({ infoHash: "123" }));
|
||||
|
||||
const session = service.createSession({
|
||||
url: "https://example.com/vid.mp4",
|
||||
magnet: "magnet:?xt=urn:btih:123",
|
||||
videoElement: video,
|
||||
probeUrl,
|
||||
playViaWebTorrent,
|
||||
});
|
||||
|
||||
const startPromise = session.start();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
video.dispatchEvent(new window.Event("playing"));
|
||||
const result = await startPromise;
|
||||
|
||||
assert.equal(probeUrl.mock.callCount(), 1, "Should probe URL");
|
||||
assert.equal(playViaWebTorrent.mock.callCount(), 0, "Should NOT call torrent");
|
||||
assert.equal(result.source, "url");
|
||||
});
|
||||
|
||||
test("urlFirstEnabled=true: Tries URL first, fails, falls back to Torrent", async () => {
|
||||
const service = new PlaybackService({
|
||||
logger: () => {},
|
||||
urlFirstEnabled: true,
|
||||
isValidMagnetUri: () => true,
|
||||
});
|
||||
|
||||
// Probe returns error
|
||||
const probeUrl = mock.fn(async () => ({ outcome: "bad", status: 404 }));
|
||||
const playViaWebTorrent = mock.fn(async () => ({ infoHash: "123" }));
|
||||
|
||||
const session = service.createSession({
|
||||
url: "https://example.com/vid.mp4",
|
||||
magnet: "magnet:?xt=urn:btih:123",
|
||||
videoElement: video,
|
||||
probeUrl,
|
||||
playViaWebTorrent,
|
||||
});
|
||||
|
||||
const result = await session.start();
|
||||
|
||||
assert.equal(probeUrl.mock.callCount(), 1, "Should probe URL");
|
||||
assert.equal(playViaWebTorrent.mock.callCount(), 1, "Should fall back to torrent");
|
||||
assert.equal(result.source, "torrent");
|
||||
});
|
||||
|
||||
test("urlFirstEnabled=false: Tries Torrent first, succeeds", async () => {
|
||||
const service = new PlaybackService({
|
||||
logger: () => {},
|
||||
urlFirstEnabled: false,
|
||||
isValidMagnetUri: () => true,
|
||||
});
|
||||
|
||||
const probeUrl = mock.fn(async () => ({ outcome: "good", status: 200 }));
|
||||
const playViaWebTorrent = mock.fn(async () => ({ infoHash: "123" }));
|
||||
|
||||
const session = service.createSession({
|
||||
url: "https://example.com/vid.mp4",
|
||||
magnet: "magnet:?xt=urn:btih:123",
|
||||
videoElement: video,
|
||||
probeUrl,
|
||||
playViaWebTorrent,
|
||||
});
|
||||
|
||||
const result = await session.start();
|
||||
|
||||
assert.equal(playViaWebTorrent.mock.callCount(), 1, "Should call torrent first");
|
||||
assert.equal(probeUrl.mock.callCount(), 0, "Should NOT probe URL");
|
||||
assert.equal(result.source, "torrent");
|
||||
});
|
||||
|
||||
test("urlFirstEnabled=false: Tries Torrent first, fails (throws), falls back to URL", async () => {
|
||||
const service = new PlaybackService({
|
||||
logger: () => {},
|
||||
urlFirstEnabled: false,
|
||||
isValidMagnetUri: () => true,
|
||||
torrentClient: { cleanup: mock.fn() } // Mock cleanup
|
||||
});
|
||||
|
||||
const probeUrl = mock.fn(async () => ({ outcome: "good", status: 200 }));
|
||||
const playViaWebTorrent = mock.fn(async () => {
|
||||
throw new Error("Torrent failed");
|
||||
});
|
||||
|
||||
const session = service.createSession({
|
||||
url: "https://example.com/vid.mp4",
|
||||
magnet: "magnet:?xt=urn:btih:123",
|
||||
videoElement: video,
|
||||
probeUrl,
|
||||
playViaWebTorrent,
|
||||
});
|
||||
|
||||
const startPromise = session.start();
|
||||
|
||||
// Wait for async operations
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
||||
// Simulate successful playback for URL
|
||||
video.dispatchEvent(new window.Event("playing"));
|
||||
|
||||
const result = await startPromise;
|
||||
|
||||
assert.equal(playViaWebTorrent.mock.callCount(), 1, "Should call torrent first");
|
||||
assert.equal(probeUrl.mock.callCount(), 1, "Should fallback to probe URL");
|
||||
assert.equal(result.source, "url");
|
||||
});
|
||||
|
||||
test("forcedSource=torrent overrides urlFirstEnabled=true", async () => {
|
||||
const service = new PlaybackService({
|
||||
logger: () => {},
|
||||
urlFirstEnabled: true,
|
||||
isValidMagnetUri: () => true,
|
||||
});
|
||||
|
||||
const probeUrl = mock.fn();
|
||||
const playViaWebTorrent = mock.fn(async () => ({ infoHash: "123" }));
|
||||
|
||||
const session = service.createSession({
|
||||
url: "https://example.com/vid.mp4",
|
||||
magnet: "magnet:?xt=urn:btih:123",
|
||||
videoElement: video,
|
||||
probeUrl,
|
||||
playViaWebTorrent,
|
||||
forcedSource: "torrent"
|
||||
});
|
||||
|
||||
const result = await session.start();
|
||||
|
||||
assert.equal(playViaWebTorrent.mock.callCount(), 1);
|
||||
assert.equal(probeUrl.mock.callCount(), 0);
|
||||
assert.equal(result.source, "torrent");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user