mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-30 01:22:56 +01:00
fix(subtitles): guard track-list effect against stale async runs
The track-building effect in VideoContext reruns once api?.basePath and isCurrentSubImageBased settle. An earlier async run could resolve after a rerun and overwrite subtitleTracks/audioTracks with setTrack callbacks bound to a stale `api`, breaking external-subtitle identity matching. Add a cancellation token and route every state commit through guarded committers so all six commit points (offline-transcoded audio/subs, burned-in, and the online audio/subs paths) drop writes from a dead run, plus bail out right after the awaited getAudioTracks when cancelled.
This commit is contained in:
@@ -144,6 +144,19 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!tracksReady) return;
|
if (!tracksReady) return;
|
||||||
|
|
||||||
|
// Guard every state commit against stale runs: api?.basePath /
|
||||||
|
// isCurrentSubImageBased can flip mid-run and restart this effect, and an
|
||||||
|
// earlier async run (which captured an old `api`) must not finish later and
|
||||||
|
// overwrite the fresh track list with callbacks bound to stale closures.
|
||||||
|
// The cleanup flips `cancelled`, so any late commit from a dead run is dropped.
|
||||||
|
let cancelled = false;
|
||||||
|
const commitSubtitleTracks = (next: Track[]) => {
|
||||||
|
if (!cancelled) setSubtitleTracks(next);
|
||||||
|
};
|
||||||
|
const commitAudioTracks = (next: Track[]) => {
|
||||||
|
if (!cancelled) setAudioTracks(next);
|
||||||
|
};
|
||||||
|
|
||||||
const fetchTracks = async () => {
|
const fetchTracks = async () => {
|
||||||
// Check if this is offline transcoded content
|
// Check if this is offline transcoded content
|
||||||
// For transcoded offline content, only ONE audio track exists in the file
|
// For transcoded offline content, only ONE audio track exists in the file
|
||||||
@@ -169,10 +182,10 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
setAudioTracks(audio);
|
commitAudioTracks(audio);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: show no audio tracks if the stored track wasn't found
|
// Fallback: show no audio tracks if the stored track wasn't found
|
||||||
setAudioTracks([]);
|
commitAudioTracks([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For subtitles in transcoded offline content:
|
// For subtitles in transcoded offline content:
|
||||||
@@ -189,7 +202,7 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
(s) => s.Index === downloadedSubtitleIndex,
|
(s) => s.Index === downloadedSubtitleIndex,
|
||||||
);
|
);
|
||||||
if (burnedInSub && isImageBasedSubtitle(burnedInSub)) {
|
if (burnedInSub && isImageBasedSubtitle(burnedInSub)) {
|
||||||
setSubtitleTracks([
|
commitSubtitleTracks([
|
||||||
{
|
{
|
||||||
name: `${burnedInSub.DisplayTitle || "Unknown"} (burned in)`,
|
name: `${burnedInSub.DisplayTitle || "Unknown"} (burned in)`,
|
||||||
index: burnedInSub.Index ?? -1,
|
index: burnedInSub.Index ?? -1,
|
||||||
@@ -239,12 +252,13 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setSubtitleTracks(subs);
|
commitSubtitleTracks(subs);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// MPV track handling
|
// MPV track handling
|
||||||
const audioData = await playerControls.getAudioTracks().catch(() => null);
|
const audioData = await playerControls.getAudioTracks().catch(() => null);
|
||||||
|
if (cancelled) return;
|
||||||
const playerAudio = (audioData as MpvAudioTrack[]) ?? [];
|
const playerAudio = (audioData as MpvAudioTrack[]) ?? [];
|
||||||
|
|
||||||
const subs: Track[] = [];
|
const subs: Track[] = [];
|
||||||
@@ -355,11 +369,14 @@ export const VideoProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
|
|
||||||
// Already in jellyfin-web order (sorted iteration above); "Disable" stays
|
// Already in jellyfin-web order (sorted iteration above); "Disable" stays
|
||||||
// at the front (unshifted), local downloaded subs at the end.
|
// at the front (unshifted), local downloaded subs at the end.
|
||||||
setSubtitleTracks(subs);
|
commitSubtitleTracks(subs);
|
||||||
setAudioTracks(audio);
|
commitAudioTracks(audio);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchTracks();
|
fetchTracks();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
// api?.basePath: setTrack builds external-sub URLs from it — rebuild once the
|
// api?.basePath: setTrack builds external-sub URLs from it — rebuild once the
|
||||||
// API is ready so online externals don't resolve with undefined.
|
// API is ready so online externals don't resolve with undefined.
|
||||||
// isCurrentSubImageBased: setTrack closes over it for the transcode replacePlayer
|
// isCurrentSubImageBased: setTrack closes over it for the transcode replacePlayer
|
||||||
|
|||||||
Reference in New Issue
Block a user