Files
bitvid/tests/fleet-playback-fallback.test.mjs

208 lines
6.2 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import "./test-helpers/setup-localstorage.mjs";
import { createPlaybackCoordinator } from "../js/app/playbackCoordinator.js";
import { PlaybackStrategyService } from "../js/services/playbackStrategyService.js";
import { PlaybackService } from "../js/services/playbackService.js";
import { deriveTorrentPlaybackConfig } from "../js/playbackUtils.js";
import { isValidMagnetUri } from "../js/utils/magnetValidators.js";
// Real coordinator/strategy/session/magnet helpers; only IO and modal rendering
// are substituted. This proves orchestration, not native decoding or WebTorrent.
const primary = "https://localhost/dead.webm";
const mirror = "https://localhost/mirror.webm";
const seed = "https://localhost/original.webm";
const hash = "ABCDEF0123456789abcdef0123456789ABCDEF01";
const magnet = `magnet:?xt=urn:btih:${hash}&ws=${encodeURIComponent(seed)}`;
class ControlledMedia extends EventTarget {
dataset = {};
isConnected = true;
src = "";
readyState = 0;
networkState = 0;
currentTime = 0;
paused = true;
error = null;
constructor(onPlay) {
super();
this.onPlay = onPlay;
}
pause() {
this.paused = true;
}
load() {
this.error = null;
}
removeAttribute(name) {
if (name === "src") this.src = "";
}
async play() {
this.onPlay(this);
}
}
async function runScenario(mode) {
const probes = [],
mediaStarts = [],
torrents = [],
errors = [],
statuses = [];
const torrentInstance = { fixture: "alternate-IO" };
const video = new ControlledMedia((element) => {
mediaStarts.push(element.src);
if (element.src === mirror) {
element.dispatchEvent(new Event("playing"));
} else {
// Repeated failures must still lead to just one alternate selection.
element.dispatchEvent(new Event("error"));
element.dispatchEvent(new Event("stalled"));
}
});
const service = new PlaybackService({
deriveTorrentPlaybackConfig,
isValidMagnetUri
});
const strategy = new PlaybackStrategyService({
playbackService: service,
logger: { log() {} }
});
const app = {
modalVideo: video,
currentVideo: {},
playbackStrategyService: strategy,
videoModal: { updateStatus: (message) => statuses.push(message) },
waitForCleanup: async () => {},
cancelPendingViewLogging() {},
clearActiveIntervals() {},
showModalWithPoster() {},
teardownVideoElement: (element) => element,
applyModalLoadingPoster() {},
resetTorrentStats() {},
preparePlaybackLogging() {},
autoplayModalVideo() {},
forceRemoveModalPoster() {},
probeUrl: async (url) => {
probes.push(url);
return mode === "media-error" || url === mirror
? { outcome: "ok", status: 200 }
: { outcome: "bad", status: 404 };
},
playViaWebTorrent: async (uri, options) => {
torrents.push({ uri, options });
if (mode === "both-fail") throw new Error("local alternate unavailable");
return torrentInstance;
},
showError: (message) => errors.push(message)
};
const result = await createPlaybackCoordinator({}).playVideoWithFallback.call(
app,
{
url: primary,
magnet,
...(mode === "mirror" ? { sources: [primary, mirror, mirror] } : {})
}
);
assert.equal(
app.activePlaybackSession,
null,
"finished session released from app"
);
assert.equal(
app.activePlaybackResultPromise,
null,
"finished promise released from app"
);
assert.equal(strategy.activePlaybackSession, null);
assert.equal(strategy.activePlaybackResultPromise, null);
assert.equal(service.currentSession.isActive(), false);
// Stale media notifications after settlement must not start another alternate.
video.dispatchEvent(new Event("error"));
video.dispatchEvent(new Event("stalled"));
return {
result,
probes,
mediaStarts,
torrents,
errors,
statuses,
app,
torrentInstance
};
}
function assertTorrentHandoff(torrents) {
assert.equal(torrents.length, 1, "alternate selected exactly once");
const { uri, options } = torrents[0];
assert.equal(
uri.split("&")[0],
`magnet:?xt=urn:btih:${hash}`,
"literal original xt/hash preserved"
);
const hints = uri
.split("&")
.filter((part) => part.startsWith("ws="))
.map((part) => decodeURIComponent(part.slice(3)));
assert.ok(hints.includes(seed), "original webseed preserved");
assert.ok(
hints.includes(primary),
"hosted webseed preserved in normalized magnet"
);
assert.equal(options.hostedUrlWebSeed, primary);
assert.deepEqual(options.urlList, [seed, primary]);
}
for (const mode of ["probe-404", "media-error", "both-fail", "mirror"]) {
test(
`actual playback orchestration: ${mode}`,
{ timeout: 2000 },
async () => {
const {
result,
probes,
mediaStarts,
torrents,
errors,
statuses,
app,
torrentInstance
} = await runScenario(mode);
assert.deepEqual(
probes,
mode === "mirror" ? [primary, mirror] : [primary]
);
assert.deepEqual(
mediaStarts,
mode === "media-error" ? [primary] : mode === "mirror" ? [mirror] : []
);
if (mode === "mirror") {
assert.equal(result.source, "url");
assert.equal(app.playSource, "url");
assert.equal(
torrents.length,
0,
"viable hosted mirror prevents torrent fallback"
);
assert.deepEqual(errors, []);
assert.ok(statuses.includes("✅ Streaming from hosted URL"));
} else {
assertTorrentHandoff(torrents);
if (mode === "both-fail") {
assert.equal(result.source, null);
assert.ok(result.error instanceof Error);
assert.equal(app.playSource, null);
assert.equal(errors.length, 1, "one surfaced terminal error");
assert.match(errors[0], /^Playback error:/);
} else {
assert.equal(result.source, "torrent");
assert.equal(result.torrentInstance, torrentInstance);
assert.equal(app.playSource, "torrent");
assert.deepEqual(errors, []);
assert.ok(statuses.includes("Switching to WebTorrent..."));
}
}
}
);
}