mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-10 03:01:45 +00:00
Player: offload all subtitles logic to hook
This commit is contained in:
parent
856f612f23
commit
a1d42f686e
4 changed files with 517 additions and 251 deletions
|
|
@ -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
92
src/routes/Player/useSubtitles.d.ts
vendored
Normal 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,
|
||||
};
|
||||
391
src/routes/Player/useSubtitles.ts
Normal file
391
src/routes/Player/useSubtitles.ts
Normal 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;
|
||||
10
src/types/models/Player.d.ts
vendored
10
src/types/models/Player.d.ts
vendored
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue