Merge pull request #1244 from Stremio/refactor/player-offload-subtitles-logic

Dev: Offload all subtitles logic from Player to dedicated hook
This commit is contained in:
Timothy Z. 2026-05-01 18:50:09 +03:00 committed by GitHub
commit dcf5173b22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 517 additions and 251 deletions

View file

@ -9,7 +9,7 @@ const { useTranslation } = require('react-i18next');
const { useRouteFocused } = require('stremio-router');
const { useServices, useGamepad } = require('stremio/services');
const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation');
const { onFileDrop, useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform, onShortcut } = require('stremio/common');
const { useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, useShell, usePlatform, onShortcut } = require('stremio/common');
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
const BufferingLoader = require('./BufferingLoader');
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
@ -26,6 +26,7 @@ const { default: SideDrawer } = require('./SideDrawer');
const usePlayer = require('./usePlayer');
const useStatistics = require('./useStatistics');
const useVideo = require('./useVideo');
const { default: useSubtitles } = require('./useSubtitles');
const styles = require('./styles');
const Video = require('./Video');
const { default: Indicator } = require('./Indicator/Indicator');
@ -90,13 +91,28 @@ const Player = ({ urlParams, queryParams }) => {
closeSideDrawer();
}, []);
const {
streamSubtitles,
allSubtitleTracks,
extraSubtitleTracks,
selectedExtraSubtitleTrackId,
subtitlesMenuProps,
} = useSubtitles({
player,
video,
settings,
streamStateChanged,
menusOpen,
closeMenus,
closeSubtitlesMenu,
toggleSubtitlesMenu,
});
const overlayHidden = React.useMemo(() => {
return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen;
}, [immersed, casting, video.state.paused, menusOpen]);
const nextVideoPopupDismissed = React.useRef(false);
const defaultSubtitlesSelected = React.useRef(false);
const lastSubtitleTrack = React.useRef(null);
const defaultAudioTrackSelected = React.useRef(false);
const playingOnExternalDevice = React.useRef(false);
const [error, setError] = React.useState(null);
@ -113,14 +129,6 @@ const Player = ({ urlParams, queryParams }) => {
const HOLD_DELAY = 200;
const onImplementationChanged = React.useCallback(() => {
video.setSubtitlesSize(settings.subtitlesSize);
video.setSubtitlesOffset(settings.subtitlesOffset);
video.setSubtitlesTextColor(settings.subtitlesTextColor);
video.setSubtitlesBackgroundColor(settings.subtitlesBackgroundColor);
video.setSubtitlesOutlineColor(settings.subtitlesOutlineColor);
}, [settings]);
const handleNextVideoNavigation = React.useCallback((deepLinks, bingeWatching, ended) => {
if (ended) {
if (bingeWatching) {
@ -178,33 +186,6 @@ const Player = ({ urlParams, queryParams }) => {
}
}, []);
const onSubtitlesTrackLoaded = React.useCallback(() => {
toast.show({
type: 'success',
title: t('PLAYER_SUBTITLES_LOADED'),
message: t('PLAYER_SUBTITLES_LOADED_EMBEDDED'),
timeout: 3000
});
}, []);
const onExtraSubtitlesTrackLoaded = React.useCallback((track) => {
toast.show({
type: 'success',
title: t('PLAYER_SUBTITLES_LOADED'),
message:
track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') :
track.local ? t('PLAYER_SUBTITLES_LOADED_LOCAL') :
t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
timeout: 3000
});
}, []);
const onExtraSubtitlesTrackAdded = React.useCallback((track) => {
if (track.local) {
video.setExtraSubtitlesTrack(track.id);
}
}, []);
const onPlayRequested = React.useCallback(() => {
playingOnExternalDevice.current = false;
video.setPaused(false);
@ -251,28 +232,6 @@ const Player = ({ urlParams, queryParams }) => {
video.setVideoScale(nextScale);
}, [video.state.videoScale]);
const onSubtitlesTrackSelected = React.useCallback((track) => {
defaultSubtitlesSelected.current = true;
video.setSubtitlesTrack(track?.id ?? null);
if (track) {
lastSubtitleTrack.current = { id: track.id, embedded: true };
}
streamStateChanged({
subtitleTrack: track ? { id: track.id, embedded: true, lang: track.lang } : null,
});
}, [streamStateChanged]);
const onExtraSubtitlesTrackSelected = React.useCallback((track) => {
defaultSubtitlesSelected.current = true;
video.setExtraSubtitlesTrack(track?.id ?? null);
if (track) {
lastSubtitleTrack.current = { id: track.id, embedded: false };
}
streamStateChanged({
subtitleTrack: track ? { id: track.id, embedded: false, lang: track.lang } : null,
});
}, [streamStateChanged]);
const onAudioTrackSelected = React.useCallback((id) => {
video.setAudioTrack(id);
streamStateChanged({
@ -282,37 +241,6 @@ const Player = ({ urlParams, queryParams }) => {
});
}, [streamStateChanged]);
const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
video.setSubtitlesDelay(delay);
streamStateChanged({ subtitleDelay: delay });
}, [streamStateChanged]);
const onIncreaseSubtitlesDelay = React.useCallback(() => {
const delay = video.state.extraSubtitlesDelay + 250;
onExtraSubtitlesDelayChanged(delay);
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
const onDecreaseSubtitlesDelay = React.useCallback(() => {
const delay = video.state.extraSubtitlesDelay - 250;
onExtraSubtitlesDelayChanged(delay);
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
const onSubtitlesSizeChanged = React.useCallback((size) => {
video.setSubtitlesSize(size);
streamStateChanged({ subtitleSize: size });
}, [streamStateChanged]);
const onUpdateSubtitlesSize = React.useCallback((delta) => {
const sizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(video.state.subtitlesSize);
const size = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, sizeIndex + delta))];
onSubtitlesSizeChanged(size);
}, [video.state.subtitlesSize, onSubtitlesSizeChanged]);
const onSubtitlesOffsetChanged = React.useCallback((offset) => {
video.setSubtitlesOffset(offset);
streamStateChanged({ subtitleOffset: offset });
}, [streamStateChanged]);
const onDismissNextVideoPopup = React.useCallback(() => {
closeNextVideoPopup();
nextVideoPopupDismissed.current = true;
@ -381,10 +309,6 @@ const Player = ({ urlParams, queryParams }) => {
event.nativeEvent.immersePrevented = true;
}, []);
onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, async (filename, buffer) => {
video.addLocalSubtitles(filename, buffer);
});
const onPlayPause = React.useCallback(() => {
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
if (video.state.paused) {
@ -466,13 +390,7 @@ const Player = ({ urlParams, queryParams }) => {
video.load({
stream: {
...player.stream.content,
subtitles: Array.isArray(player.selected.stream.subtitles) ?
player.selected.stream.subtitles.map((subtitles) => ({
...subtitles,
label: subtitles.label || subtitles.url
}))
:
[]
subtitles: streamSubtitles
},
autoplay: true,
time: player.libraryItem !== null &&
@ -501,16 +419,7 @@ const Player = ({ urlParams, queryParams }) => {
shellTransport: services.shell.active ? services.shell.transport : null,
});
}
}, [streamingServer.baseUrl, player.selected, player.stream, forceTranscoding, casting]);
React.useEffect(() => {
if (video.state.stream !== null) {
const tracks = player.subtitles.map((subtitles) => ({
...subtitles,
label: subtitles.label || subtitles.url
}));
video.addExtraSubtitlesTracks(tracks);
}
}, [player.subtitles, video.state.stream]);
}, [streamingServer.baseUrl, player.selected, player.stream, streamSubtitles, forceTranscoding, casting]);
React.useEffect(() => {
!seeking && timeChanged(video.state.time, video.state.duration, video.state.manifest?.name);
@ -546,48 +455,6 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [player.nextVideo, video.state.time, video.state.duration]);
// Auto subtitles track selection
React.useEffect(() => {
if (!defaultSubtitlesSelected.current) {
if (settings.subtitlesLanguage === null) {
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
defaultSubtitlesSelected.current = true;
return;
}
const savedTrackId = player.streamState?.subtitleTrack?.id;
const savedLang = player.streamState?.subtitleTrack?.lang;
const savedIsExternal = savedTrackId && player.streamState?.subtitleTrack?.embedded === false;
const subtitlesTrack =
savedTrackId ? findTrackById(video.state.subtitlesTracks, savedTrackId) :
savedLang ? findTrackByLang(video.state.subtitlesTracks, savedLang) :
findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
const extraSubtitlesTrack =
savedTrackId ? findTrackById(video.state.extraSubtitlesTracks, savedTrackId) :
savedLang ? findTrackByLang(video.state.extraSubtitlesTracks, savedLang) :
findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
if (subtitlesTrack && subtitlesTrack.id) {
if (video.state.selectedSubtitlesTrackId !== subtitlesTrack.id ||
video.state.selectedExtraSubtitlesTrackId !== null) {
video.setSubtitlesTrack(subtitlesTrack.id);
}
defaultSubtitlesSelected.current = true;
} else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
if (video.state.selectedExtraSubtitlesTrackId !== extraSubtitlesTrack.id ||
video.state.selectedSubtitlesTrackId !== null) {
video.setExtraSubtitlesTrack(extraSubtitlesTrack.id);
}
if (savedIsExternal) {
defaultSubtitlesSelected.current = true;
}
}
}
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId, player.streamState]);
// Auto audio track selection
React.useEffect(() => {
if (!defaultAudioTrackSelected.current) {
@ -603,30 +470,8 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [video.state.audioTracks, player.streamState]);
// Saved subtitles settings
React.useEffect(() => {
if (video.state.stream !== null) {
const delay = player.streamState?.subtitleDelay;
if (typeof delay === 'number') {
video.setSubtitlesDelay(delay);
}
const size = player.streamState?.subtitleSize;
if (typeof size === 'number') {
video.setSubtitlesSize(size);
}
const offset = player.streamState?.subtitleOffset;
if (typeof offset === 'number') {
video.setSubtitlesOffset(offset);
}
}
}, [video.state.stream, player.streamState]);
React.useEffect(() => {
defaultSubtitlesSelected.current = false;
defaultAudioTrackSelected.current = false;
lastSubtitleTrack.current = null;
nextVideoPopupDismissed.current = false;
playingOnExternalDevice.current = false;
// we need a timeout here to make sure that previous page unloads and the new one loads
@ -634,13 +479,6 @@ const Player = ({ urlParams, queryParams }) => {
setTimeout(() => isNavigating.current = false, 1000);
}, [video.state.stream]);
React.useEffect(() => {
if ((!Array.isArray(video.state.subtitlesTracks) || video.state.subtitlesTracks.length === 0) &&
(!Array.isArray(video.state.extraSubtitlesTracks) || video.state.extraSubtitlesTracks.length === 0)) {
closeSubtitlesMenu();
}
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
React.useEffect(() => {
if (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0) {
closeAudioMenu();
@ -753,40 +591,6 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [video.state.volume], !menusOpen);
onShortcut('subtitlesDelay', (combo) => {
combo === 1 ? onIncreaseSubtitlesDelay() : onDecreaseSubtitlesDelay();
}, [onIncreaseSubtitlesDelay, onDecreaseSubtitlesDelay], !menusOpen);
onShortcut('subtitlesSize', (combo) => {
combo === 1 ? onUpdateSubtitlesSize(1) : onUpdateSubtitlesSize(-1);
}, [onUpdateSubtitlesSize, onUpdateSubtitlesSize], !menusOpen);
onShortcut('toggleSubtitles', () => {
const isEnabled = video.state.selectedSubtitlesTrackId !== null || video.state.selectedExtraSubtitlesTrackId !== null;
if (isEnabled) {
if (video.state.selectedSubtitlesTrackId) {
lastSubtitleTrack.current = { id: video.state.selectedSubtitlesTrackId, embedded: true };
} else if (video.state.selectedExtraSubtitlesTrackId) {
lastSubtitleTrack.current = { id: video.state.selectedExtraSubtitlesTrackId, embedded: false };
}
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
} else {
const savedTrack = player.streamState?.subtitleTrack ?? lastSubtitleTrack.current;
if (savedTrack?.id) {
savedTrack.embedded ? video.setSubtitlesTrack(savedTrack.id) : video.setExtraSubtitlesTrack(savedTrack.id);
}
}
}, [player.streamState, video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId], !menusOpen);
onShortcut('subtitlesMenu', () => {
closeMenus();
if (video.state?.subtitlesTracks?.length > 0 || video.state?.extraSubtitlesTracks?.length > 0) {
toggleSubtitlesMenu();
}
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, toggleSubtitlesMenu]);
onShortcut('audioMenu', () => {
closeMenus();
if (video.state?.audioTracks?.length > 0) {
@ -951,18 +755,10 @@ const Player = ({ urlParams, queryParams }) => {
React.useEffect(() => {
video.events.on('error', onError);
video.events.on('ended', onEnded);
video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
video.events.on('implementationChanged', onImplementationChanged);
return () => {
video.events.off('error', onError);
video.events.off('ended', onEnded);
video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
video.events.off('implementationChanged', onImplementationChanged);
};
}, []);
@ -1035,8 +831,8 @@ const Player = ({ urlParams, queryParams }) => {
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player?.selected?.stream}
playbackDevices={playbackDevices}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
extraSubtitlesTracks={extraSubtitleTracks}
selectedExtraSubtitlesTrackId={selectedExtraSubtitleTrackId}
/>
</ContextMenu>
<HorizontalNavBar
@ -1067,7 +863,7 @@ const Player = ({ urlParams, queryParams }) => {
volume={video.state.volume}
muted={video.state.muted}
playbackSpeed={video.state.playbackSpeed}
subtitlesTracks={video.state.subtitlesTracks.concat(video.state.extraSubtitlesTracks)}
subtitlesTracks={allSubtitleTracks}
audioTracks={video.state.audioTracks}
metaItem={player.metaItem}
nextVideo={player.nextVideo}
@ -1128,24 +924,7 @@ const Player = ({ urlParams, queryParams }) => {
<Transition when={subtitlesMenuOpen} name={'fade'}>
<SubtitlesMenu
className={classnames(styles['layer'], styles['menu-layer'])}
subtitlesLanguage={settings.subtitlesLanguage}
interfaceLanguage={settings.interfaceLanguage}
subtitlesTracks={video.state.subtitlesTracks}
selectedSubtitlesTrackId={video.state.selectedSubtitlesTrackId}
subtitlesOffset={video.state.subtitlesOffset}
subtitlesSize={video.state.subtitlesSize}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
extraSubtitlesOffset={video.state.extraSubtitlesOffset}
extraSubtitlesDelay={video.state.extraSubtitlesDelay}
extraSubtitlesSize={video.state.extraSubtitlesSize}
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
onExtraSubtitlesSizeChanged={onSubtitlesSizeChanged}
{...subtitlesMenuProps}
/>
</Transition>
<Transition when={audioMenuOpen} name={'fade'}>
@ -1168,8 +947,8 @@ const Player = ({ urlParams, queryParams }) => {
className={classnames(styles['layer'], styles['menu-layer'])}
stream={player.selected?.stream}
playbackDevices={playbackDevices}
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
extraSubtitlesTracks={extraSubtitleTracks}
selectedExtraSubtitlesTrackId={selectedExtraSubtitleTrackId}
/>
</Transition>
</div>

92
src/routes/Player/useSubtitles.d.ts vendored Normal file
View file

@ -0,0 +1,92 @@
// Copyright (C) 2017-2026 Smart code 203358507
type SubtitleTrack = {
id: string,
lang: string,
label?: string | null,
origin?: string,
url?: string | null,
fallbackUrl?: string | null,
embedded?: boolean,
local?: boolean,
exclusive?: boolean,
buffer?: ArrayBuffer,
};
type SelectedSubtitleTrack = {
id: string,
embedded: boolean,
};
type VideoSubtitleState = {
stream: unknown | null,
subtitlesTracks: SubtitleTrack[],
selectedSubtitlesTrackId: string | null,
subtitlesOffset: number | null,
subtitlesSize: number | null,
extraSubtitlesTracks: SubtitleTrack[],
selectedExtraSubtitlesTrackId: string | null,
extraSubtitlesOffset: number | null,
extraSubtitlesDelay: number | null,
extraSubtitlesSize: number | null,
};
type VideoEvents = {
on: (event: string, listener: (...args: any[]) => void) => void,
off: (event: string, listener: (...args: any[]) => void) => void,
};
type VideoController = {
events: VideoEvents,
state: VideoSubtitleState,
addExtraSubtitlesTracks: (tracks: SubtitleTrack[]) => void,
addLocalSubtitles: (filename: string, buffer: ArrayBuffer) => void,
setSubtitlesTrack: (id: string | null) => void,
setExtraSubtitlesTrack: (id: string | null) => void,
setSubtitlesDelay: (delay: number) => void,
setSubtitlesSize: (size: number) => void,
setSubtitlesOffset: (offset: number) => void,
setSubtitlesTextColor: (color: string) => void,
setSubtitlesBackgroundColor: (color: string) => void,
setSubtitlesOutlineColor: (color: string) => void,
};
type UseSubtitlesArgs = {
player: Player,
video: VideoController,
settings: Settings,
streamStateChanged: (state: Partial<StreamState>) => void,
menusOpen: boolean,
closeMenus: () => void,
closeSubtitlesMenu: () => void,
toggleSubtitlesMenu: () => void,
};
type SubtitlesMenuProps = {
subtitlesLanguage: string | null,
interfaceLanguage: string,
subtitlesTracks: SubtitleTrack[],
selectedSubtitlesTrackId: string | null,
subtitlesOffset: number | null,
subtitlesSize: number | null,
extraSubtitlesTracks: SubtitleTrack[],
selectedExtraSubtitlesTrackId: string | null,
extraSubtitlesOffset: number | null,
extraSubtitlesDelay: number | null,
extraSubtitlesSize: number | null,
onSubtitlesTrackSelected: (track: SubtitleTrack | null) => void,
onExtraSubtitlesTrackSelected: (track: SubtitleTrack | null) => void,
onSubtitlesOffsetChanged: (offset: number) => void,
onSubtitlesSizeChanged: (size: number) => void,
onExtraSubtitlesOffsetChanged: (offset: number) => void,
onExtraSubtitlesDelayChanged: (delay: number) => void,
onExtraSubtitlesSizeChanged: (size: number) => void,
};
type UseSubtitlesResult = {
streamSubtitles: SubtitleTrack[],
allSubtitleTracks: SubtitleTrack[],
extraSubtitleTracks: SubtitleTrack[],
selectedExtraSubtitleTrackId: string | null,
subtitlesMenuProps: SubtitlesMenuProps,
};

View file

@ -0,0 +1,391 @@
// Copyright (C) 2017-2026 Smart code 203358507
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { CONSTANTS, languages, onFileDrop, onShortcut, useToast } from 'stremio/common';
const withFallbackLabels = (tracks?: SubtitleTrack[] | null): SubtitleTrack[] => {
if (!Array.isArray(tracks)) {
return [];
}
return tracks.map((track) => ({
...track,
label: track.label || track.url || '',
}));
};
const findTrackById = (tracks: SubtitleTrack[], id?: string | null) => {
if (!id) {
return undefined;
}
return tracks.find((track) => track.id === id);
};
const findTrackByLanguage = (tracks: SubtitleTrack[], language?: string | null) => {
if (!language) {
return undefined;
}
const languageCode = languages.toCode(language);
return tracks.find((track) => {
return track.lang === language || languages.toCode(track.lang) === languageCode;
});
};
const useSubtitles = ({
player,
video,
settings,
streamStateChanged,
menusOpen,
closeMenus,
closeSubtitlesMenu,
toggleSubtitlesMenu,
}: UseSubtitlesArgs): UseSubtitlesResult => {
const { t } = useTranslation();
const toast = useToast();
const videoRef = useRef(video);
const settingsRef = useRef(settings);
const defaultTrackSelected = useRef(false);
const lastSelectedTrack = useRef<SelectedSubtitleTrack | null>(null);
videoRef.current = video;
settingsRef.current = settings;
const streamSubtitles = useMemo(() => {
return withFallbackLabels(player.selected?.stream.subtitles);
}, [player.selected]);
const externalSubtitles = useMemo(() => {
return withFallbackLabels(player.subtitles);
}, [player.subtitles]);
const allTracks = useMemo(() => {
return video.state.subtitlesTracks.concat(video.state.extraSubtitlesTracks);
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
const hasTracks = allTracks.length > 0;
const applySubtitleStyle = useCallback(() => {
const currentSettings = settingsRef.current;
const currentVideo = videoRef.current;
currentVideo.setSubtitlesSize(currentSettings.subtitlesSize);
currentVideo.setSubtitlesOffset(currentSettings.subtitlesOffset);
currentVideo.setSubtitlesTextColor(currentSettings.subtitlesTextColor);
currentVideo.setSubtitlesBackgroundColor(currentSettings.subtitlesBackgroundColor);
currentVideo.setSubtitlesOutlineColor(currentSettings.subtitlesOutlineColor);
}, []);
const rememberTrack = useCallback((track: SubtitleTrack, embedded: boolean) => {
lastSelectedTrack.current = { id: track.id, embedded };
streamStateChanged({
subtitleTrack: {
id: track.id,
embedded,
lang: track.lang,
},
});
}, [streamStateChanged]);
const disableSubtitles = useCallback(() => {
defaultTrackSelected.current = true;
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
streamStateChanged({ subtitleTrack: null });
}, [streamStateChanged, video]);
const selectEmbeddedTrack = useCallback((track: SubtitleTrack | null) => {
if (!track) {
disableSubtitles();
return;
}
defaultTrackSelected.current = true;
video.setSubtitlesTrack(track.id);
rememberTrack(track, true);
}, [disableSubtitles, rememberTrack, video]);
const selectExtraTrack = useCallback((track: SubtitleTrack | null) => {
if (!track) {
disableSubtitles();
return;
}
defaultTrackSelected.current = true;
video.setExtraSubtitlesTrack(track.id);
rememberTrack(track, false);
}, [disableSubtitles, rememberTrack, video]);
const changeDelay = useCallback((delay: number) => {
video.setSubtitlesDelay(delay);
streamStateChanged({ subtitleDelay: delay });
}, [streamStateChanged, video]);
const increaseDelay = useCallback(() => {
changeDelay((video.state.extraSubtitlesDelay ?? 0) + 250);
}, [changeDelay, video.state.extraSubtitlesDelay]);
const decreaseDelay = useCallback(() => {
changeDelay((video.state.extraSubtitlesDelay ?? 0) - 250);
}, [changeDelay, video.state.extraSubtitlesDelay]);
const changeSize = useCallback((size: number) => {
video.setSubtitlesSize(size);
streamStateChanged({ subtitleSize: size });
}, [streamStateChanged, video]);
const updateSize = useCallback((delta: number) => {
const sizes = CONSTANTS.SUBTITLES_SIZES as number[];
const sizeIndex = sizes.indexOf(video.state.subtitlesSize ?? -1);
const nextIndex = Math.max(0, Math.min(sizes.length - 1, sizeIndex + delta));
changeSize(sizes[nextIndex]);
}, [changeSize, video.state.subtitlesSize]);
const changeOffset = useCallback((offset: number) => {
video.setSubtitlesOffset(offset);
streamStateChanged({ subtitleOffset: offset });
}, [streamStateChanged, video]);
onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, (filename: string, buffer: ArrayBuffer) => {
videoRef.current.addLocalSubtitles(filename, buffer);
});
useEffect(() => {
if (video.state.stream !== null) {
video.addExtraSubtitlesTracks(externalSubtitles);
}
}, [externalSubtitles, video.state.stream]);
useEffect(() => {
if (defaultTrackSelected.current) {
return;
}
if (settings.subtitlesLanguage === null) {
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
defaultTrackSelected.current = true;
return;
}
const savedTrack = player.streamState?.subtitleTrack;
const savedTrackId = savedTrack?.id;
const savedLanguage = savedTrack?.lang;
const savedExternalTrack = Boolean(savedTrackId && savedTrack?.embedded === false);
const embeddedTrack = savedTrackId ?
findTrackById(video.state.subtitlesTracks, savedTrackId)
:
findTrackByLanguage(video.state.subtitlesTracks, savedLanguage ?? settings.subtitlesLanguage);
const extraTrack = savedTrackId ?
findTrackById(video.state.extraSubtitlesTracks, savedTrackId)
:
findTrackByLanguage(video.state.extraSubtitlesTracks, savedLanguage ?? settings.subtitlesLanguage);
if (embeddedTrack?.id) {
if (video.state.selectedSubtitlesTrackId !== embeddedTrack.id ||
video.state.selectedExtraSubtitlesTrackId !== null) {
video.setSubtitlesTrack(embeddedTrack.id);
}
defaultTrackSelected.current = true;
return;
}
if (extraTrack?.id) {
if (video.state.selectedExtraSubtitlesTrackId !== extraTrack.id ||
video.state.selectedSubtitlesTrackId !== null) {
video.setExtraSubtitlesTrack(extraTrack.id);
}
if (savedExternalTrack) {
defaultTrackSelected.current = true;
}
}
}, [
player.streamState,
settings.subtitlesLanguage,
video.state.extraSubtitlesTracks,
video.state.selectedExtraSubtitlesTrackId,
video.state.selectedSubtitlesTrackId,
video.state.subtitlesTracks,
]);
useEffect(() => {
if (video.state.stream === null) {
return;
}
const delay = player.streamState?.subtitleDelay;
if (typeof delay === 'number') {
video.setSubtitlesDelay(delay);
}
const size = player.streamState?.subtitleSize;
if (typeof size === 'number') {
video.setSubtitlesSize(size);
}
const offset = player.streamState?.subtitleOffset;
if (typeof offset === 'number') {
video.setSubtitlesOffset(offset);
}
}, [player.streamState, video.state.stream]);
useEffect(() => {
defaultTrackSelected.current = false;
lastSelectedTrack.current = null;
}, [video.state.stream]);
useEffect(() => {
if (!hasTracks) {
closeSubtitlesMenu();
}
}, [closeSubtitlesMenu, hasTracks]);
useEffect(() => {
const onSubtitlesTrackLoaded = () => {
toast.show({
type: 'success',
title: t('PLAYER_SUBTITLES_LOADED'),
message: t('PLAYER_SUBTITLES_LOADED_EMBEDDED'),
timeout: 3000,
});
};
const onExtraSubtitlesTrackLoaded = (track: SubtitleTrack) => {
toast.show({
type: 'success',
title: t('PLAYER_SUBTITLES_LOADED'),
message: track.exclusive ?
t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE')
:
track.local ?
t('PLAYER_SUBTITLES_LOADED_LOCAL')
:
t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
timeout: 3000,
});
};
const onExtraSubtitlesTrackAdded = (track: SubtitleTrack) => {
if (track.local) {
videoRef.current.setExtraSubtitlesTrack(track.id);
}
};
video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
video.events.on('implementationChanged', applySubtitleStyle);
return () => {
video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
video.events.off('implementationChanged', applySubtitleStyle);
};
}, [applySubtitleStyle, t, toast, video.events]);
onShortcut('subtitlesDelay', (combo) => {
combo === 1 ? increaseDelay() : decreaseDelay();
}, [increaseDelay, decreaseDelay], !menusOpen);
onShortcut('subtitlesSize', (combo) => {
combo === 1 ? updateSize(1) : updateSize(-1);
}, [updateSize], !menusOpen);
onShortcut('toggleSubtitles', () => {
const subtitlesEnabled = video.state.selectedSubtitlesTrackId !== null ||
video.state.selectedExtraSubtitlesTrackId !== null;
if (subtitlesEnabled) {
if (video.state.selectedSubtitlesTrackId) {
lastSelectedTrack.current = {
id: video.state.selectedSubtitlesTrackId,
embedded: true,
};
} else if (video.state.selectedExtraSubtitlesTrackId) {
lastSelectedTrack.current = {
id: video.state.selectedExtraSubtitlesTrackId,
embedded: false,
};
}
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
return;
}
const savedTrack = player.streamState?.subtitleTrack ?? lastSelectedTrack.current;
if (savedTrack?.id) {
savedTrack.embedded ?
video.setSubtitlesTrack(savedTrack.id)
:
video.setExtraSubtitlesTrack(savedTrack.id);
}
}, [
player.streamState,
video.state.selectedExtraSubtitlesTrackId,
video.state.selectedSubtitlesTrackId,
], !menusOpen);
onShortcut('subtitlesMenu', () => {
closeMenus();
if (hasTracks) {
toggleSubtitlesMenu();
}
}, [closeMenus, hasTracks, toggleSubtitlesMenu]);
const menuProps = useMemo(() => ({
subtitlesLanguage: settings.subtitlesLanguage,
interfaceLanguage: settings.interfaceLanguage,
subtitlesTracks: video.state.subtitlesTracks,
selectedSubtitlesTrackId: video.state.selectedSubtitlesTrackId,
subtitlesOffset: video.state.subtitlesOffset,
subtitlesSize: video.state.subtitlesSize,
extraSubtitlesTracks: video.state.extraSubtitlesTracks,
selectedExtraSubtitlesTrackId: video.state.selectedExtraSubtitlesTrackId,
extraSubtitlesOffset: video.state.extraSubtitlesOffset,
extraSubtitlesDelay: video.state.extraSubtitlesDelay,
extraSubtitlesSize: video.state.extraSubtitlesSize,
onSubtitlesTrackSelected: selectEmbeddedTrack,
onExtraSubtitlesTrackSelected: selectExtraTrack,
onSubtitlesOffsetChanged: changeOffset,
onSubtitlesSizeChanged: changeSize,
onExtraSubtitlesOffsetChanged: changeOffset,
onExtraSubtitlesDelayChanged: changeDelay,
onExtraSubtitlesSizeChanged: changeSize,
}), [
changeDelay,
changeOffset,
changeSize,
selectEmbeddedTrack,
selectExtraTrack,
settings.interfaceLanguage,
settings.subtitlesLanguage,
video.state.extraSubtitlesDelay,
video.state.extraSubtitlesOffset,
video.state.extraSubtitlesSize,
video.state.extraSubtitlesTracks,
video.state.selectedExtraSubtitlesTrackId,
video.state.selectedSubtitlesTrackId,
video.state.subtitlesOffset,
video.state.subtitlesSize,
video.state.subtitlesTracks,
]);
return {
streamSubtitles,
allSubtitleTracks: allTracks,
extraSubtitleTracks: video.state.extraSubtitlesTracks,
selectedExtraSubtitleTrackId: video.state.selectedExtraSubtitlesTrackId,
subtitlesMenuProps: menuProps,
};
};
export default useSubtitles;

View file

@ -15,13 +15,16 @@ type MetaItemPlayer = MetaItemPreview & {
type SelectedStream = Stream & {
deepLinks: StreamDeepLinks,
subtitles?: Subtitle[],
};
type Subtitle = {
id: string,
lang: string,
origin: string,
url: string,
origin?: string,
url?: string | null,
fallbackUrl?: string | null,
label?: string | null,
};
type SeriesInfo = {
@ -32,6 +35,7 @@ type SeriesInfo = {
type SubtitlesTrackState = {
id: string,
embedded: boolean,
lang?: string,
};
type AudioTrackState = {
@ -39,7 +43,7 @@ type AudioTrackState = {
};
type StreamState = {
subtitleTrack?: SubtitlesTrackState,
subtitleTrack?: SubtitlesTrackState | null,
subtitleDelay?: number,
subtitleSize?: number,
subtitleOffset?: number,