mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-11 12:20:45 +00:00
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:
commit
dcf5173b22
4 changed files with 517 additions and 251 deletions
|
|
@ -9,7 +9,7 @@ const { useTranslation } = require('react-i18next');
|
||||||
const { useRouteFocused } = require('stremio-router');
|
const { useRouteFocused } = require('stremio-router');
|
||||||
const { useServices, useGamepad } = require('stremio/services');
|
const { useServices, useGamepad } = require('stremio/services');
|
||||||
const { useContentGamepadNavigation } = require('stremio/services/GamepadNavigation');
|
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 { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
|
||||||
const BufferingLoader = require('./BufferingLoader');
|
const BufferingLoader = require('./BufferingLoader');
|
||||||
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
|
const VolumeChangeIndicator = require('./VolumeChangeIndicator');
|
||||||
|
|
@ -26,6 +26,7 @@ const { default: SideDrawer } = require('./SideDrawer');
|
||||||
const usePlayer = require('./usePlayer');
|
const usePlayer = require('./usePlayer');
|
||||||
const useStatistics = require('./useStatistics');
|
const useStatistics = require('./useStatistics');
|
||||||
const useVideo = require('./useVideo');
|
const useVideo = require('./useVideo');
|
||||||
|
const { default: useSubtitles } = require('./useSubtitles');
|
||||||
const styles = require('./styles');
|
const styles = require('./styles');
|
||||||
const Video = require('./Video');
|
const Video = require('./Video');
|
||||||
const { default: Indicator } = require('./Indicator/Indicator');
|
const { default: Indicator } = require('./Indicator/Indicator');
|
||||||
|
|
@ -90,13 +91,28 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
closeSideDrawer();
|
closeSideDrawer();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
streamSubtitles,
|
||||||
|
allSubtitleTracks,
|
||||||
|
extraSubtitleTracks,
|
||||||
|
selectedExtraSubtitleTrackId,
|
||||||
|
subtitlesMenuProps,
|
||||||
|
} = useSubtitles({
|
||||||
|
player,
|
||||||
|
video,
|
||||||
|
settings,
|
||||||
|
streamStateChanged,
|
||||||
|
menusOpen,
|
||||||
|
closeMenus,
|
||||||
|
closeSubtitlesMenu,
|
||||||
|
toggleSubtitlesMenu,
|
||||||
|
});
|
||||||
|
|
||||||
const overlayHidden = React.useMemo(() => {
|
const overlayHidden = React.useMemo(() => {
|
||||||
return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen;
|
return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen;
|
||||||
}, [immersed, casting, video.state.paused, menusOpen]);
|
}, [immersed, casting, video.state.paused, menusOpen]);
|
||||||
|
|
||||||
const nextVideoPopupDismissed = React.useRef(false);
|
const nextVideoPopupDismissed = React.useRef(false);
|
||||||
const defaultSubtitlesSelected = React.useRef(false);
|
|
||||||
const lastSubtitleTrack = React.useRef(null);
|
|
||||||
const defaultAudioTrackSelected = React.useRef(false);
|
const defaultAudioTrackSelected = React.useRef(false);
|
||||||
const playingOnExternalDevice = React.useRef(false);
|
const playingOnExternalDevice = React.useRef(false);
|
||||||
const [error, setError] = React.useState(null);
|
const [error, setError] = React.useState(null);
|
||||||
|
|
@ -113,14 +129,6 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
|
|
||||||
const HOLD_DELAY = 200;
|
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) => {
|
const handleNextVideoNavigation = React.useCallback((deepLinks, bingeWatching, ended) => {
|
||||||
if (ended) {
|
if (ended) {
|
||||||
if (bingeWatching) {
|
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(() => {
|
const onPlayRequested = React.useCallback(() => {
|
||||||
playingOnExternalDevice.current = false;
|
playingOnExternalDevice.current = false;
|
||||||
video.setPaused(false);
|
video.setPaused(false);
|
||||||
|
|
@ -251,28 +232,6 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
video.setVideoScale(nextScale);
|
video.setVideoScale(nextScale);
|
||||||
}, [video.state.videoScale]);
|
}, [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) => {
|
const onAudioTrackSelected = React.useCallback((id) => {
|
||||||
video.setAudioTrack(id);
|
video.setAudioTrack(id);
|
||||||
streamStateChanged({
|
streamStateChanged({
|
||||||
|
|
@ -282,37 +241,6 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
});
|
});
|
||||||
}, [streamStateChanged]);
|
}, [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(() => {
|
const onDismissNextVideoPopup = React.useCallback(() => {
|
||||||
closeNextVideoPopup();
|
closeNextVideoPopup();
|
||||||
nextVideoPopupDismissed.current = true;
|
nextVideoPopupDismissed.current = true;
|
||||||
|
|
@ -381,10 +309,6 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
event.nativeEvent.immersePrevented = true;
|
event.nativeEvent.immersePrevented = true;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, async (filename, buffer) => {
|
|
||||||
video.addLocalSubtitles(filename, buffer);
|
|
||||||
});
|
|
||||||
|
|
||||||
const onPlayPause = React.useCallback(() => {
|
const onPlayPause = React.useCallback(() => {
|
||||||
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
|
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
|
||||||
if (video.state.paused) {
|
if (video.state.paused) {
|
||||||
|
|
@ -466,13 +390,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
video.load({
|
video.load({
|
||||||
stream: {
|
stream: {
|
||||||
...player.stream.content,
|
...player.stream.content,
|
||||||
subtitles: Array.isArray(player.selected.stream.subtitles) ?
|
subtitles: streamSubtitles
|
||||||
player.selected.stream.subtitles.map((subtitles) => ({
|
|
||||||
...subtitles,
|
|
||||||
label: subtitles.label || subtitles.url
|
|
||||||
}))
|
|
||||||
:
|
|
||||||
[]
|
|
||||||
},
|
},
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
time: player.libraryItem !== null &&
|
time: player.libraryItem !== null &&
|
||||||
|
|
@ -501,16 +419,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
shellTransport: services.shell.active ? services.shell.transport : null,
|
shellTransport: services.shell.active ? services.shell.transport : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [streamingServer.baseUrl, player.selected, player.stream, forceTranscoding, casting]);
|
}, [streamingServer.baseUrl, player.selected, player.stream, streamSubtitles, 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]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
!seeking && timeChanged(video.state.time, video.state.duration, video.state.manifest?.name);
|
!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]);
|
}, [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
|
// Auto audio track selection
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!defaultAudioTrackSelected.current) {
|
if (!defaultAudioTrackSelected.current) {
|
||||||
|
|
@ -603,30 +470,8 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
}
|
}
|
||||||
}, [video.state.audioTracks, player.streamState]);
|
}, [video.state.audioTracks, player.streamState]);
|
||||||
|
|
||||||
// Saved subtitles settings
|
|
||||||
React.useEffect(() => {
|
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;
|
defaultAudioTrackSelected.current = false;
|
||||||
lastSubtitleTrack.current = null;
|
|
||||||
nextVideoPopupDismissed.current = false;
|
nextVideoPopupDismissed.current = false;
|
||||||
playingOnExternalDevice.current = false;
|
playingOnExternalDevice.current = false;
|
||||||
// we need a timeout here to make sure that previous page unloads and the new one loads
|
// 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);
|
setTimeout(() => isNavigating.current = false, 1000);
|
||||||
}, [video.state.stream]);
|
}, [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(() => {
|
React.useEffect(() => {
|
||||||
if (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0) {
|
if (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0) {
|
||||||
closeAudioMenu();
|
closeAudioMenu();
|
||||||
|
|
@ -753,40 +591,6 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
}
|
}
|
||||||
}, [video.state.volume], !menusOpen);
|
}, [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', () => {
|
onShortcut('audioMenu', () => {
|
||||||
closeMenus();
|
closeMenus();
|
||||||
if (video.state?.audioTracks?.length > 0) {
|
if (video.state?.audioTracks?.length > 0) {
|
||||||
|
|
@ -951,18 +755,10 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
video.events.on('error', onError);
|
video.events.on('error', onError);
|
||||||
video.events.on('ended', onEnded);
|
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 () => {
|
return () => {
|
||||||
video.events.off('error', onError);
|
video.events.off('error', onError);
|
||||||
video.events.off('ended', onEnded);
|
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'])}
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||||
stream={player?.selected?.stream}
|
stream={player?.selected?.stream}
|
||||||
playbackDevices={playbackDevices}
|
playbackDevices={playbackDevices}
|
||||||
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
|
extraSubtitlesTracks={extraSubtitleTracks}
|
||||||
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
|
selectedExtraSubtitlesTrackId={selectedExtraSubtitleTrackId}
|
||||||
/>
|
/>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<HorizontalNavBar
|
<HorizontalNavBar
|
||||||
|
|
@ -1067,7 +863,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
volume={video.state.volume}
|
volume={video.state.volume}
|
||||||
muted={video.state.muted}
|
muted={video.state.muted}
|
||||||
playbackSpeed={video.state.playbackSpeed}
|
playbackSpeed={video.state.playbackSpeed}
|
||||||
subtitlesTracks={video.state.subtitlesTracks.concat(video.state.extraSubtitlesTracks)}
|
subtitlesTracks={allSubtitleTracks}
|
||||||
audioTracks={video.state.audioTracks}
|
audioTracks={video.state.audioTracks}
|
||||||
metaItem={player.metaItem}
|
metaItem={player.metaItem}
|
||||||
nextVideo={player.nextVideo}
|
nextVideo={player.nextVideo}
|
||||||
|
|
@ -1128,24 +924,7 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
<Transition when={subtitlesMenuOpen} name={'fade'}>
|
<Transition when={subtitlesMenuOpen} name={'fade'}>
|
||||||
<SubtitlesMenu
|
<SubtitlesMenu
|
||||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||||
subtitlesLanguage={settings.subtitlesLanguage}
|
{...subtitlesMenuProps}
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
<Transition when={audioMenuOpen} name={'fade'}>
|
<Transition when={audioMenuOpen} name={'fade'}>
|
||||||
|
|
@ -1168,8 +947,8 @@ const Player = ({ urlParams, queryParams }) => {
|
||||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||||
stream={player.selected?.stream}
|
stream={player.selected?.stream}
|
||||||
playbackDevices={playbackDevices}
|
playbackDevices={playbackDevices}
|
||||||
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
|
extraSubtitlesTracks={extraSubtitleTracks}
|
||||||
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
|
selectedExtraSubtitlesTrackId={selectedExtraSubtitleTrackId}
|
||||||
/>
|
/>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</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 & {
|
type SelectedStream = Stream & {
|
||||||
deepLinks: StreamDeepLinks,
|
deepLinks: StreamDeepLinks,
|
||||||
|
subtitles?: Subtitle[],
|
||||||
};
|
};
|
||||||
|
|
||||||
type Subtitle = {
|
type Subtitle = {
|
||||||
id: string,
|
id: string,
|
||||||
lang: string,
|
lang: string,
|
||||||
origin: string,
|
origin?: string,
|
||||||
url: string,
|
url?: string | null,
|
||||||
|
fallbackUrl?: string | null,
|
||||||
|
label?: string | null,
|
||||||
};
|
};
|
||||||
|
|
||||||
type SeriesInfo = {
|
type SeriesInfo = {
|
||||||
|
|
@ -32,6 +35,7 @@ type SeriesInfo = {
|
||||||
type SubtitlesTrackState = {
|
type SubtitlesTrackState = {
|
||||||
id: string,
|
id: string,
|
||||||
embedded: boolean,
|
embedded: boolean,
|
||||||
|
lang?: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
type AudioTrackState = {
|
type AudioTrackState = {
|
||||||
|
|
@ -39,7 +43,7 @@ type AudioTrackState = {
|
||||||
};
|
};
|
||||||
|
|
||||||
type StreamState = {
|
type StreamState = {
|
||||||
subtitleTrack?: SubtitlesTrackState,
|
subtitleTrack?: SubtitlesTrackState | null,
|
||||||
subtitleDelay?: number,
|
subtitleDelay?: number,
|
||||||
subtitleSize?: number,
|
subtitleSize?: number,
|
||||||
subtitleOffset?: number,
|
subtitleOffset?: number,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue