fix: refresh stale proxy token on video error
This commit is contained in:
@@ -243,6 +243,40 @@ describe("browser player flow", () => {
|
|||||||
assert.equal(modules.streamUrlForMode("sub", "best"), "");
|
assert.equal(modules.streamUrlForMode("sub", "best"), "");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("refreshes stale current stream token from episode data", async () => {
|
||||||
|
modules.state.elements.video.currentTime = 64;
|
||||||
|
modules.state.playback.modeSources = {
|
||||||
|
sub: { token: "stale-token", type: "m3u8", qualities: ["720p"], subtitles: [] },
|
||||||
|
};
|
||||||
|
globalThis.fetch = ((url: string) => {
|
||||||
|
fetchCalls.push({ url });
|
||||||
|
return Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: () =>
|
||||||
|
Promise.resolve({
|
||||||
|
mode_sources: {
|
||||||
|
sub: { token: "fresh-token", type: "m3u8", qualities: ["720p"], subtitles: [] },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
} as Response);
|
||||||
|
}) as typeof fetch;
|
||||||
|
|
||||||
|
const refreshed = await modules.refreshCurrentModeSource();
|
||||||
|
|
||||||
|
assert.equal(refreshed, true);
|
||||||
|
assert.deepEqual(
|
||||||
|
fetchCalls.map((call) => call.url),
|
||||||
|
["/api/watch/episode/42/3?mode=sub"],
|
||||||
|
);
|
||||||
|
assert.equal(modules.state.playback.modeSources.sub?.token, "fresh-token");
|
||||||
|
assert.equal(
|
||||||
|
modules.state.elements.video.src,
|
||||||
|
"/watch/proxy/stream?mode=sub&token=fresh-token&hls=1",
|
||||||
|
);
|
||||||
|
assert.equal(modules.state.playback.pendingSeekTime, 64);
|
||||||
|
});
|
||||||
|
|
||||||
test("saves progress on pause and after scrub mouseup", async () => {
|
test("saves progress on pause and after scrub mouseup", async () => {
|
||||||
modules.setupProgress();
|
modules.setupProgress();
|
||||||
modules.state.elements.video.currentTime = 30;
|
modules.state.elements.video.currentTime = 30;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { setupQuality, updateQualityOptions } from "./quality";
|
|||||||
import { setupSkip, updateSkipButton, updateAutoSkipButton } from "./skip";
|
import { setupSkip, updateSkipButton, updateAutoSkipButton } from "./skip";
|
||||||
import { setupSegmentEditor } from "./skip/editor";
|
import { setupSegmentEditor } from "./skip/editor";
|
||||||
import { resolveActiveSegments, renderSegments } from "./skip/segments";
|
import { resolveActiveSegments, renderSegments } from "./skip/segments";
|
||||||
|
import { refreshCurrentModeSource } from "./source";
|
||||||
import { state, initState, showEndState, hideEndState } from "./state";
|
import { state, initState, showEndState, hideEndState } from "./state";
|
||||||
import { safeLocalStorage } from "./storage";
|
import { safeLocalStorage } from "./storage";
|
||||||
import { setupSubtitles, updateSubtitleOptions, updateSubtitleRender } from "./subtitles";
|
import { setupSubtitles, updateSubtitleOptions, updateSubtitleRender } from "./subtitles";
|
||||||
@@ -280,6 +281,27 @@ const initPlayer = async (): Promise<void> => {
|
|||||||
},
|
},
|
||||||
{ signal },
|
{ signal },
|
||||||
);
|
);
|
||||||
|
let sourceRefreshInFlight = false;
|
||||||
|
state.elements.video.addEventListener(
|
||||||
|
"error",
|
||||||
|
() => {
|
||||||
|
if (sourceRefreshInFlight) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sourceRefreshInFlight = true;
|
||||||
|
refreshCurrentModeSource(signal)
|
||||||
|
.catch((error) => {
|
||||||
|
if (error instanceof DOMException && error.name === "AbortError") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error("failed to refresh video source:", error);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
sourceRefreshInFlight = false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
// update progress bar during buffering
|
// update progress bar during buffering
|
||||||
state.elements.video.addEventListener(
|
state.elements.video.addEventListener(
|
||||||
"progress",
|
"progress",
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { state } from "./state";
|
import { state } from "./state";
|
||||||
|
import { safeLocalStorage } from "./storage";
|
||||||
|
import { isRecord, parseModeSources } from "./validate";
|
||||||
|
import { loadVideoSource } from "./video";
|
||||||
|
|
||||||
export const streamUrlForMode = (mode: string, quality?: string): string => {
|
export const streamUrlForMode = (mode: string, quality?: string): string => {
|
||||||
const src = state.playback.modeSources[mode];
|
const src = state.playback.modeSources[mode];
|
||||||
@@ -16,3 +19,30 @@ export const streamUrlForMode = (mode: string, quality?: string): string => {
|
|||||||
|
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const refreshCurrentModeSource = async (signal?: AbortSignal): Promise<boolean> => {
|
||||||
|
const mode = state.playback.currentMode;
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/watch/episode/${state.episode.malID}/${encodeURIComponent(state.episode.current)}?mode=${encodeURIComponent(mode)}`,
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`mode source refresh failed with status ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: unknown = await res.json();
|
||||||
|
if (!isRecord(data)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refreshedSource = parseModeSources(data.mode_sources)[mode];
|
||||||
|
if (!refreshedSource?.token) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.playback.modeSources = { ...state.playback.modeSources, [mode]: refreshedSource };
|
||||||
|
|
||||||
|
const preferredQuality = safeLocalStorage.getItem("mal:preferred-quality") || "best";
|
||||||
|
loadVideoSource(streamUrlForMode(mode, preferredQuality), refreshedSource.type);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user