From 3b730a2bd816b8f0984cd6b2c7237d1fd8285920 Mon Sep 17 00:00:00 2001
From: Neeraj TK <64883030+GaryGosh@users.noreply.github.com>
Date: Sun, 11 Aug 2024 04:32:23 +0530
Subject: [PATCH 01/25] added shortkey to toggle caption. UX improvement.
---
src/routes/Player/Player.js | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index c7d94d82c..5a4b1cf71 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -515,6 +515,18 @@ const Player = ({ urlParams, queryParams }) => {
break;
}
+ case 'KeyC': {
+ if (!menusOpen && !nextVideoPopupOpen) {
+ if (video.state.selectedSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(null);
+ } else if (video.state.subtitlesTracks.length > 0) {
+ onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
+ } else if (video.state.extraSubtitlesTracks.length > 0) {
+ onExtraSubtitlesTrackSelected(video.state.extraSubtitlesTracks[0].id);
+ }
+ }
+ break;
+ }
case 'KeyI': {
closeMenus();
if (player.metaItem !== null && player.metaItem.type === 'Ready') {
From d2db62f33a4e3be6fdb1b4b9f8cab2c9fd18bad6 Mon Sep 17 00:00:00 2001
From: Neeraj TK <64883030+GaryGosh@users.noreply.github.com>
Date: Mon, 23 Sep 2024 20:05:36 +0530
Subject: [PATCH 02/25] retain last selected subtitle upon toggling
---
src/routes/Player/Player.js | 1796 +++++++++++++++++++----------------
1 file changed, 999 insertions(+), 797 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index 5a4b1cf71..894a9b466 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -1,812 +1,1014 @@
// Copyright (C) 2017-2023 Smart code 203358507
-const React = require('react');
-const PropTypes = require('prop-types');
-const classnames = require('classnames');
-const debounce = require('lodash.debounce');
-const langs = require('langs');
-const { useTranslation } = require('react-i18next');
-const { useRouteFocused } = require('stremio-router');
-const { useServices } = require('stremio/services');
-const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
-const BufferingLoader = require('./BufferingLoader');
-const VolumeChangeIndicator = require('./VolumeChangeIndicator');
-const Error = require('./Error');
-const ControlBar = require('./ControlBar');
-const NextVideoPopup = require('./NextVideoPopup');
-const StatisticsMenu = require('./StatisticsMenu');
-const InfoMenu = require('./InfoMenu');
-const OptionsMenu = require('./OptionsMenu');
-const VideosMenu = require('./VideosMenu');
-const SubtitlesMenu = require('./SubtitlesMenu');
-const SpeedMenu = require('./SpeedMenu');
-const usePlayer = require('./usePlayer');
-const useSettings = require('./useSettings');
-const useStatistics = require('./useStatistics');
-const useVideo = require('./useVideo');
-const styles = require('./styles');
-const Video = require('./Video');
+const React = require("react");
+const PropTypes = require("prop-types");
+const classnames = require("classnames");
+const debounce = require("lodash.debounce");
+const langs = require("langs");
+const { useTranslation } = require("react-i18next");
+const { useRouteFocused } = require("stremio-router");
+const { useServices } = require("stremio/services");
+const {
+ HorizontalNavBar,
+ useFullscreen,
+ useBinaryState,
+ useToast,
+ useStreamingServer,
+ withCoreSuspender,
+} = require("stremio/common");
+const BufferingLoader = require("./BufferingLoader");
+const VolumeChangeIndicator = require("./VolumeChangeIndicator");
+const Error = require("./Error");
+const ControlBar = require("./ControlBar");
+const NextVideoPopup = require("./NextVideoPopup");
+const StatisticsMenu = require("./StatisticsMenu");
+const InfoMenu = require("./InfoMenu");
+const OptionsMenu = require("./OptionsMenu");
+const VideosMenu = require("./VideosMenu");
+const SubtitlesMenu = require("./SubtitlesMenu");
+const SpeedMenu = require("./SpeedMenu");
+const usePlayer = require("./usePlayer");
+const useSettings = require("./useSettings");
+const useStatistics = require("./useStatistics");
+const useVideo = require("./useVideo");
+const styles = require("./styles");
+const Video = require("./Video");
const Player = ({ urlParams, queryParams }) => {
- const { t } = useTranslation();
- const { chromecast, shell, core } = useServices();
- const forceTranscoding = React.useMemo(() => {
- return queryParams.has('forceTranscoding');
- }, [queryParams]);
-
- const [player, videoParamsChanged, timeChanged, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
- const [settings, updateSettings] = useSettings();
- const streamingServer = useStreamingServer();
- const statistics = useStatistics(player, streamingServer);
- const video = useVideo();
- const routeFocused = useRouteFocused();
- const toast = useToast();
-
- const [casting, setCasting] = React.useState(() => {
- return chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED;
- });
-
- const [immersed, setImmersed] = React.useState(true);
- const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
- const [, , , toggleFullscreen] = useFullscreen();
-
- const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
- const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
- const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
- const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
- const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
- const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
- const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
-
- const menusOpen = React.useMemo(() => {
- return optionsMenuOpen || subtitlesMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
- }, [optionsMenuOpen, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
-
- const closeMenus = React.useCallback(() => {
- closeOptionsMenu();
- closeSubtitlesMenu();
- closeInfoMenu();
- closeSpeedMenu();
- closeVideosMenu();
- closeStatisticsMenu();
- }, []);
-
- const overlayHidden = React.useMemo(() => {
- return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen;
- }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
-
- const nextVideoPopupDismissed = React.useRef(false);
- const defaultSubtitlesSelected = React.useRef(false);
- const defaultAudioTrackSelected = React.useRef(false);
- const [error, setError] = React.useState(null);
-
- const onImplementationChanged = React.useCallback(() => {
- video.setProp('subtitlesSize', settings.subtitlesSize);
- video.setProp('subtitlesOffset', settings.subtitlesOffset);
- video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
- video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
- video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
- video.setProp('extraSubtitlesSize', settings.subtitlesSize);
- video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
- video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
- video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
- video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
- }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]);
-
- const onEnded = React.useCallback(() => {
- ended();
- if (player.nextVideo !== null) {
- onNextVideoRequested();
- } else {
- window.history.back();
- }
- }, [player.nextVideo, onNextVideoRequested]);
-
- const onError = React.useCallback((error) => {
- console.error('Player', error);
- if (error.critical) {
- setError(error);
- } else {
- toast.show({
- type: 'error',
- title: t('ERROR'),
- message: error.message,
- timeout: 3000
- });
- }
- }, []);
-
- 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') : t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
- timeout: 3000
- });
- }, []);
-
- const onPlayRequested = React.useCallback(() => {
- video.setProp('paused', false);
- }, []);
-
- const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []);
-
- const onPauseRequested = React.useCallback(() => {
- video.setProp('paused', true);
- }, []);
-
- const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []);
- const onMuteRequested = React.useCallback(() => {
- video.setProp('muted', true);
- }, []);
-
- const onUnmuteRequested = React.useCallback(() => {
- video.setProp('muted', false);
- }, []);
-
- const onVolumeChangeRequested = React.useCallback((volume) => {
- video.setProp('volume', volume);
- }, []);
-
- const onSeekRequested = React.useCallback((time) => {
- video.setProp('time', time);
- }, []);
-
- const onPlaybackSpeedChanged = React.useCallback((rate) => {
- video.setProp('playbackSpeed', rate);
- }, []);
-
- const onSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp('selectedSubtitlesTrackId', id);
- video.setProp('selectedExtraSubtitlesTrackId', null);
- }, []);
-
- const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp('selectedSubtitlesTrackId', null);
- video.setProp('selectedExtraSubtitlesTrackId', id);
- }, []);
-
- const onAudioTrackSelected = React.useCallback((id) => {
- video.setProp('selectedAudioTrackId', id);
- }, []);
-
- const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
- video.setProp('extraSubtitlesDelay', delay);
- }, []);
-
- const onSubtitlesSizeChanged = React.useCallback((size) => {
- updateSettings({ subtitlesSize: size });
- }, [updateSettings]);
-
- const onSubtitlesOffsetChanged = React.useCallback((offset) => {
- updateSettings({ subtitlesOffset: offset });
- }, [updateSettings]);
-
- const onDismissNextVideoPopup = React.useCallback(() => {
- closeNextVideoPopup();
- nextVideoPopupDismissed.current = true;
- }, []);
-
- const onNextVideoRequested = React.useCallback(() => {
- if (player.nextVideo !== null) {
- nextVideo();
-
- const deepLinks = player.nextVideo.deepLinks;
- if (deepLinks.metaDetailsStreams && deepLinks.player) {
- window.location.replace(deepLinks.metaDetailsStreams);
- window.location.href = deepLinks.player;
- } else {
- window.location.replace(deepLinks.player ?? deepLinks.metaDetailsStreams);
- }
- }
- }, [player.nextVideo]);
-
- const onVideoClick = React.useCallback(() => {
- if (video.state.paused !== null) {
- if (video.state.paused) {
- onPlayRequestedDebounced();
- } else {
- onPauseRequestedDebounced();
- }
- }
- }, [video.state.paused]);
-
- const onVideoDoubleClick = React.useCallback(() => {
- onPlayRequestedDebounced.cancel();
- onPauseRequestedDebounced.cancel();
- toggleFullscreen();
- }, [toggleFullscreen]);
-
- const onContainerMouseDown = React.useCallback((event) => {
- if (!event.nativeEvent.optionsMenuClosePrevented) {
- closeOptionsMenu();
- }
- if (!event.nativeEvent.subtitlesMenuClosePrevented) {
- closeSubtitlesMenu();
- }
- if (!event.nativeEvent.infoMenuClosePrevented) {
- closeInfoMenu();
- }
- if (!event.nativeEvent.speedMenuClosePrevented) {
- closeSpeedMenu();
- }
- if (!event.nativeEvent.videosMenuClosePrevented) {
- closeVideosMenu();
- }
- if (!event.nativeEvent.statisticsMenuClosePrevented) {
- closeStatisticsMenu();
- }
- }, []);
-
- const onContainerMouseMove = React.useCallback((event) => {
- setImmersed(false);
- if (!event.nativeEvent.immersePrevented) {
- setImmersedDebounced(true);
- } else {
- setImmersedDebounced.cancel();
- }
- }, []);
-
- const onContainerMouseLeave = React.useCallback(() => {
- setImmersedDebounced.cancel();
- setImmersed(true);
- }, []);
-
- const onBarMouseMove = React.useCallback((event) => {
- event.nativeEvent.immersePrevented = true;
- }, []);
-
- React.useEffect(() => {
- setError(null);
- if (player.selected === null) {
- video.unload();
- } else if (streamingServer.settings !== null && streamingServer.settings.type !== 'Loading' &&
- (player.selected.metaRequest === null || (player.metaItem !== null && player.metaItem.type !== 'Loading'))) {
- video.load({
- stream: {
- ...player.selected.stream,
- subtitles: Array.isArray(player.selected.stream.subtitles) ?
- player.selected.stream.subtitles.map((subtitles) => ({
- ...subtitles,
- label: subtitles.url
- }))
- :
- []
- },
- autoplay: true,
- time: player.libraryItem !== null &&
- player.selected.streamRequest !== null &&
- player.selected.streamRequest.path !== null &&
- player.libraryItem.state.video_id === player.selected.streamRequest.path.id ?
- player.libraryItem.state.timeOffset
- :
- 0,
- forceTranscoding: forceTranscoding || casting,
- maxAudioChannels: settings.surroundSound ? 32 : 2,
- streamingServerURL: streamingServer.baseUrl ?
- casting ?
- streamingServer.baseUrl
- :
- streamingServer.selected.transportUrl
- :
- null,
- seriesInfo: player.seriesInfo
- }, {
- chromecastTransport: chromecast.active ? chromecast.transport : null,
- shellTransport: shell.active ? shell.transport : null,
- });
- }
- }, [streamingServer.baseUrl, player.selected, player.metaItem, forceTranscoding, casting]);
- React.useEffect(() => {
- if (video.state.stream !== null) {
- const tracks = player.subtitles.map((subtitles) => ({
- ...subtitles,
- label: subtitles.url
- }));
- video.addExtraSubtitlesTracks(tracks);
- }
- }, [player.subtitles, video.state.stream]);
-
- React.useEffect(() => {
- video.setProp('subtitlesSize', settings.subtitlesSize);
- video.setProp('extraSubtitlesSize', settings.subtitlesSize);
- }, [settings.subtitlesSize]);
-
- React.useEffect(() => {
- video.setProp('subtitlesOffset', settings.subtitlesOffset);
- video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
- }, [settings.subtitlesOffset]);
-
- React.useEffect(() => {
- video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
- video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
- }, [settings.subtitlesTextColor]);
-
- React.useEffect(() => {
- video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
- video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
- }, [settings.subtitlesBackgroundColor]);
-
- React.useEffect(() => {
- video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
- video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
- }, [settings.subtitlesOutlineColor]);
-
- React.useEffect(() => {
- if (video.state.time !== null && !isNaN(video.state.time) &&
- video.state.duration !== null && !isNaN(video.state.duration) &&
- video.state.manifest !== null && typeof video.state.manifest.name === 'string') {
- timeChanged(video.state.time, video.state.duration, video.state.manifest.name);
- }
- }, [video.state.time, video.state.duration, video.state.manifest]);
-
- React.useEffect(() => {
- if (video.state.paused !== null) {
- pausedChanged(video.state.paused);
- }
- }, [video.state.paused]);
-
- React.useEffect(() => {
- videoParamsChanged(video.state.videoParams);
- }, [video.state.videoParams]);
-
- React.useEffect(() => {
- if (!!settings.bingeWatching && player.nextVideo !== null && !nextVideoPopupDismissed.current) {
- if (video.state.time !== null && video.state.duration !== null && video.state.time < video.state.duration && (video.state.duration - video.state.time) <= settings.nextVideoNotificationDuration) {
- openNextVideoPopup();
- } else {
- closeNextVideoPopup();
- }
- }
- }, [player.nextVideo, video.state.time, video.state.duration]);
-
- React.useEffect(() => {
- if (!defaultSubtitlesSelected.current) {
- const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
-
- const subtitlesTrack = findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
- const extraSubtitlesTrack = findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
-
- if (subtitlesTrack && subtitlesTrack.id) {
- onSubtitlesTrackSelected(subtitlesTrack.id);
- defaultSubtitlesSelected.current = true;
- } else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
- onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
- defaultSubtitlesSelected.current = true;
- }
- }
- }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
-
- React.useEffect(() => {
- if (!defaultAudioTrackSelected.current) {
- const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
- const audioTrack = findTrackByLang(video.state.audioTracks, settings.audioLanguage);
-
- if (audioTrack && audioTrack.id) {
- onAudioTrackSelected(audioTrack.id);
- defaultAudioTrackSelected.current = true;
- }
- }
- }, [video.state.audioTracks]);
-
- React.useEffect(() => {
- defaultSubtitlesSelected.current = false;
- defaultAudioTrackSelected.current = false;
- nextVideoPopupDismissed.current = false;
- }, [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) &&
- (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0)) {
- closeSubtitlesMenu();
- }
- }, [video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
-
- React.useEffect(() => {
- if (player.metaItem === null || player.metaItem.type !== 'Ready') {
- closeInfoMenu();
- closeVideosMenu();
- }
- }, [player.metaItem]);
-
- React.useEffect(() => {
- if (video.state.playbackSpeed === null) {
- closeSpeedMenu();
- }
- }, [video.state.playbackSpeed]);
-
- React.useEffect(() => {
- const toastFilter = (item) => item?.dataset?.type === 'CoreEvent';
- toast.addFilter(toastFilter);
- const onCastStateChange = () => {
- setCasting(chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED);
- };
- const onChromecastServiceStateChange = () => {
- onCastStateChange();
- if (chromecast.active) {
- chromecast.transport.on(
- cast.framework.CastContextEventType.CAST_STATE_CHANGED,
- onCastStateChange
- );
- }
- };
- const onCoreEvent = ({ event }) => {
- if (event === 'PlayingOnDevice') {
- onPauseRequested();
- }
- };
- chromecast.on('stateChanged', onChromecastServiceStateChange);
- core.transport.on('CoreEvent', onCoreEvent);
- onChromecastServiceStateChange();
- return () => {
- toast.removeFilter(toastFilter);
- chromecast.off('stateChanged', onChromecastServiceStateChange);
- core.transport.off('CoreEvent', onCoreEvent);
- if (chromecast.active) {
- chromecast.transport.off(
- cast.framework.CastContextEventType.CAST_STATE_CHANGED,
- onCastStateChange
- );
- }
- };
- }, []);
-
- React.useLayoutEffect(() => {
- const onKeyDown = (event) => {
- switch (event.code) {
- case 'Space': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
- if (video.state.paused) {
- onPlayRequested();
- } else {
- onPauseRequested();
- }
- }
-
- break;
- }
- case 'ArrowRight': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
- const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
- onSeekRequested(video.state.time + seekDuration);
- }
-
- break;
- }
- case 'ArrowLeft': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
- const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
- onSeekRequested(video.state.time - seekDuration);
- }
-
- break;
- }
- case 'ArrowUp': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume + 5);
- }
-
- break;
- }
- case 'ArrowDown': {
- if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume - 5);
- }
-
- break;
- }
- case 'KeyS': {
- closeMenus();
- if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) ||
- (Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0) ||
- (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0)) {
- toggleSubtitlesMenu();
- }
-
- break;
- }
- case 'KeyC': {
- if (!menusOpen && !nextVideoPopupOpen) {
- if (video.state.selectedSubtitlesTrackId !== null) {
- onSubtitlesTrackSelected(null);
- } else if (video.state.subtitlesTracks.length > 0) {
- onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
- } else if (video.state.extraSubtitlesTracks.length > 0) {
- onExtraSubtitlesTrackSelected(video.state.extraSubtitlesTracks[0].id);
- }
- }
- break;
- }
- case 'KeyI': {
- closeMenus();
- if (player.metaItem !== null && player.metaItem.type === 'Ready') {
- toggleInfoMenu();
- }
-
- break;
- }
- case 'KeyR': {
- closeMenus();
- if (video.state.playbackSpeed !== null) {
- toggleSpeedMenu();
- }
-
- break;
- }
- case 'KeyV': {
- closeMenus();
- if (player.metaItem !== null && player.metaItem.type === 'Ready' && player.metaItem?.content?.videos?.length > 0) {
- toggleVideosMenu();
- }
-
- break;
- }
- case 'KeyD': {
- closeMenus();
- if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
- toggleStatisticsMenu();
- }
-
- break;
- }
- case 'Escape': {
- closeMenus();
- break;
- }
- }
- };
- const onWheel = ({ deltaY }) => {
- if (deltaY > 0) {
- if (!menusOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume - 5);
- }
- } else {
- if (!menusOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume + 5);
- }
- }
- };
- if (routeFocused) {
- window.addEventListener('keydown', onKeyDown);
- window.addEventListener('wheel', onWheel);
- }
- return () => {
- window.removeEventListener('keydown', onKeyDown);
- window.removeEventListener('wheel', onWheel);
- };
- }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]);
-
- 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('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('implementationChanged', onImplementationChanged);
- };
- }, []);
-
- React.useLayoutEffect(() => {
- return () => {
- setImmersedDebounced.cancel();
- onPlayRequestedDebounced.cancel();
- onPauseRequestedDebounced.cancel();
- };
- }, []);
-
+ const { t } = useTranslation();
+ const { chromecast, shell, core } = useServices();
+ const forceTranscoding = React.useMemo(() => {
+ return queryParams.has("forceTranscoding");
+ }, [queryParams]);
+
+ const [
+ player,
+ videoParamsChanged,
+ timeChanged,
+ pausedChanged,
+ ended,
+ nextVideo,
+ ] = usePlayer(urlParams);
+ const [settings, updateSettings] = useSettings();
+ const streamingServer = useStreamingServer();
+ const statistics = useStatistics(player, streamingServer);
+ const video = useVideo();
+ const routeFocused = useRouteFocused();
+ const toast = useToast();
+
+ const [casting, setCasting] = React.useState(() => {
return (
-
-
- {
- !video.state.loaded ?
-
-

-
- :
- null
- }
- {
- (video.state.buffering || !video.state.loaded) && !error ?
-
- :
- null
- }
- {
- error !== null ?
-
- :
- null
- }
- {
- menusOpen ?
-
- :
- null
- }
- {
- video.state.volume !== null && overlayHidden ?
-
- :
- null
- }
-
-
- {
- nextVideoPopupOpen ?
-
- :
- null
- }
- {
- statisticsMenuOpen ?
-
- :
- null
- }
- {
- subtitlesMenuOpen ?
-
- :
- null
- }
- {
- infoMenuOpen ?
-
- :
- null
- }
- {
- speedMenuOpen ?
-
- :
- null
- }
- {
- videosMenuOpen ?
-
- :
- null
- }
- {
- optionsMenuOpen ?
-
- :
- null
- }
-
+ chromecast.active &&
+ chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED
);
+ });
+
+ const [immersed, setImmersed] = React.useState(true);
+ const setImmersedDebounced = React.useCallback(
+ debounce(setImmersed, 3000),
+ []
+ );
+ const [, , , toggleFullscreen] = useFullscreen();
+
+ const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] =
+ useBinaryState(false);
+ const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] =
+ useBinaryState(false);
+ const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
+ const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] =
+ useBinaryState(false);
+ const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] =
+ useBinaryState(false);
+ const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] =
+ useBinaryState(false);
+ const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] =
+ useBinaryState(false);
+
+ const menusOpen = React.useMemo(() => {
+ return (
+ optionsMenuOpen ||
+ subtitlesMenuOpen ||
+ infoMenuOpen ||
+ speedMenuOpen ||
+ videosMenuOpen ||
+ statisticsMenuOpen
+ );
+ }, [
+ optionsMenuOpen,
+ subtitlesMenuOpen,
+ infoMenuOpen,
+ speedMenuOpen,
+ videosMenuOpen,
+ statisticsMenuOpen,
+ ]);
+
+ const closeMenus = React.useCallback(() => {
+ closeOptionsMenu();
+ closeSubtitlesMenu();
+ closeInfoMenu();
+ closeSpeedMenu();
+ closeVideosMenu();
+ closeStatisticsMenu();
+ }, []);
+
+ const overlayHidden = React.useMemo(() => {
+ return (
+ immersed &&
+ !casting &&
+ video.state.paused !== null &&
+ !video.state.paused &&
+ !menusOpen &&
+ !nextVideoPopupOpen
+ );
+ }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
+
+ const nextVideoPopupDismissed = React.useRef(false);
+ const defaultSubtitlesSelected = React.useRef(false);
+ const defaultAudioTrackSelected = React.useRef(false);
+ const [error, setError] = React.useState(null);
+ const [previousSubtitlesTrackId, setPreviousSubtitlesTrackId] =
+ React.useState(null);
+
+ const onImplementationChanged = React.useCallback(() => {
+ video.setProp("subtitlesSize", settings.subtitlesSize);
+ video.setProp("subtitlesOffset", settings.subtitlesOffset);
+ video.setProp("subtitlesTextColor", settings.subtitlesTextColor);
+ video.setProp(
+ "subtitlesBackgroundColor",
+ settings.subtitlesBackgroundColor
+ );
+ video.setProp("subtitlesOutlineColor", settings.subtitlesOutlineColor);
+ video.setProp("extraSubtitlesSize", settings.subtitlesSize);
+ video.setProp("extraSubtitlesOffset", settings.subtitlesOffset);
+ video.setProp("extraSubtitlesTextColor", settings.subtitlesTextColor);
+ video.setProp(
+ "extraSubtitlesBackgroundColor",
+ settings.subtitlesBackgroundColor
+ );
+ video.setProp("extraSubtitlesOutlineColor", settings.subtitlesOutlineColor);
+ }, [
+ settings.subtitlesSize,
+ settings.subtitlesOffset,
+ settings.subtitlesTextColor,
+ settings.subtitlesBackgroundColor,
+ settings.subtitlesOutlineColor,
+ ]);
+
+ const onEnded = React.useCallback(() => {
+ ended();
+ if (player.nextVideo !== null) {
+ onNextVideoRequested();
+ } else {
+ window.history.back();
+ }
+ }, [player.nextVideo, onNextVideoRequested]);
+
+ const onError = React.useCallback((error) => {
+ console.error("Player", error);
+ if (error.critical) {
+ setError(error);
+ } else {
+ toast.show({
+ type: "error",
+ title: t("ERROR"),
+ message: error.message,
+ timeout: 3000,
+ });
+ }
+ }, []);
+
+ 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")
+ : t("PLAYER_SUBTITLES_LOADED_ORIGIN", { origin: track.origin }),
+ timeout: 3000,
+ });
+ }, []);
+
+ const onPlayRequested = React.useCallback(() => {
+ video.setProp("paused", false);
+ }, []);
+
+ const onPlayRequestedDebounced = React.useCallback(
+ debounce(onPlayRequested, 200),
+ []
+ );
+
+ const onPauseRequested = React.useCallback(() => {
+ video.setProp("paused", true);
+ }, []);
+
+ const onPauseRequestedDebounced = React.useCallback(
+ debounce(onPauseRequested, 200),
+ []
+ );
+ const onMuteRequested = React.useCallback(() => {
+ video.setProp("muted", true);
+ }, []);
+
+ const onUnmuteRequested = React.useCallback(() => {
+ video.setProp("muted", false);
+ }, []);
+
+ const onVolumeChangeRequested = React.useCallback((volume) => {
+ video.setProp("volume", volume);
+ }, []);
+
+ const onSeekRequested = React.useCallback((time) => {
+ video.setProp("time", time);
+ }, []);
+
+ const onPlaybackSpeedChanged = React.useCallback((rate) => {
+ video.setProp("playbackSpeed", rate);
+ }, []);
+
+ const onSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp("selectedSubtitlesTrackId", id);
+ video.setProp("selectedExtraSubtitlesTrackId", null);
+ }, []);
+
+ const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp("selectedSubtitlesTrackId", null);
+ video.setProp("selectedExtraSubtitlesTrackId", id);
+ }, []);
+
+ const onAudioTrackSelected = React.useCallback((id) => {
+ video.setProp("selectedAudioTrackId", id);
+ }, []);
+
+ const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
+ video.setProp("extraSubtitlesDelay", delay);
+ }, []);
+
+ const onSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp("selectedSubtitlesTrackId", id);
+ video.setProp("selectedExtraSubtitlesTrackId", null);
+ setPreviousSubtitlesTrackId(id);
+ }, []);
+
+ const onSubtitlesSizeChanged = React.useCallback(
+ (size) => {
+ updateSettings({ subtitlesSize: size });
+ },
+ [updateSettings]
+ );
+
+ const onSubtitlesOffsetChanged = React.useCallback(
+ (offset) => {
+ updateSettings({ subtitlesOffset: offset });
+ },
+ [updateSettings]
+ );
+
+ const onDismissNextVideoPopup = React.useCallback(() => {
+ closeNextVideoPopup();
+ nextVideoPopupDismissed.current = true;
+ }, []);
+
+ const onNextVideoRequested = React.useCallback(() => {
+ if (player.nextVideo !== null) {
+ nextVideo();
+
+ const deepLinks = player.nextVideo.deepLinks;
+ if (deepLinks.metaDetailsStreams && deepLinks.player) {
+ window.location.replace(deepLinks.metaDetailsStreams);
+ window.location.href = deepLinks.player;
+ } else {
+ window.location.replace(
+ deepLinks.player ?? deepLinks.metaDetailsStreams
+ );
+ }
+ }
+ }, [player.nextVideo]);
+
+ const onVideoClick = React.useCallback(() => {
+ if (video.state.paused !== null) {
+ if (video.state.paused) {
+ onPlayRequestedDebounced();
+ } else {
+ onPauseRequestedDebounced();
+ }
+ }
+ }, [video.state.paused]);
+
+ const onVideoDoubleClick = React.useCallback(() => {
+ onPlayRequestedDebounced.cancel();
+ onPauseRequestedDebounced.cancel();
+ toggleFullscreen();
+ }, [toggleFullscreen]);
+
+ const onContainerMouseDown = React.useCallback((event) => {
+ if (!event.nativeEvent.optionsMenuClosePrevented) {
+ closeOptionsMenu();
+ }
+ if (!event.nativeEvent.subtitlesMenuClosePrevented) {
+ closeSubtitlesMenu();
+ }
+ if (!event.nativeEvent.infoMenuClosePrevented) {
+ closeInfoMenu();
+ }
+ if (!event.nativeEvent.speedMenuClosePrevented) {
+ closeSpeedMenu();
+ }
+ if (!event.nativeEvent.videosMenuClosePrevented) {
+ closeVideosMenu();
+ }
+ if (!event.nativeEvent.statisticsMenuClosePrevented) {
+ closeStatisticsMenu();
+ }
+ }, []);
+
+ const onContainerMouseMove = React.useCallback((event) => {
+ setImmersed(false);
+ if (!event.nativeEvent.immersePrevented) {
+ setImmersedDebounced(true);
+ } else {
+ setImmersedDebounced.cancel();
+ }
+ }, []);
+
+ const onContainerMouseLeave = React.useCallback(() => {
+ setImmersedDebounced.cancel();
+ setImmersed(true);
+ }, []);
+
+ const onBarMouseMove = React.useCallback((event) => {
+ event.nativeEvent.immersePrevented = true;
+ }, []);
+
+ React.useEffect(() => {
+ setError(null);
+ if (player.selected === null) {
+ video.unload();
+ } else if (
+ streamingServer.settings !== null &&
+ streamingServer.settings.type !== "Loading" &&
+ (player.selected.metaRequest === null ||
+ (player.metaItem !== null && player.metaItem.type !== "Loading"))
+ ) {
+ video.load(
+ {
+ stream: {
+ ...player.selected.stream,
+ subtitles: Array.isArray(player.selected.stream.subtitles)
+ ? player.selected.stream.subtitles.map((subtitles) => ({
+ ...subtitles,
+ label: subtitles.url,
+ }))
+ : [],
+ },
+ autoplay: true,
+ time:
+ player.libraryItem !== null &&
+ player.selected.streamRequest !== null &&
+ player.selected.streamRequest.path !== null &&
+ player.libraryItem.state.video_id ===
+ player.selected.streamRequest.path.id
+ ? player.libraryItem.state.timeOffset
+ : 0,
+ forceTranscoding: forceTranscoding || casting,
+ maxAudioChannels: settings.surroundSound ? 32 : 2,
+ streamingServerURL: streamingServer.baseUrl
+ ? casting
+ ? streamingServer.baseUrl
+ : streamingServer.selected.transportUrl
+ : null,
+ seriesInfo: player.seriesInfo,
+ },
+ {
+ chromecastTransport: chromecast.active ? chromecast.transport : null,
+ shellTransport: shell.active ? shell.transport : null,
+ }
+ );
+ }
+ }, [
+ streamingServer.baseUrl,
+ player.selected,
+ player.metaItem,
+ forceTranscoding,
+ casting,
+ ]);
+ React.useEffect(() => {
+ if (video.state.stream !== null) {
+ const tracks = player.subtitles.map((subtitles) => ({
+ ...subtitles,
+ label: subtitles.url,
+ }));
+ video.addExtraSubtitlesTracks(tracks);
+ }
+ }, [player.subtitles, video.state.stream]);
+
+ React.useEffect(() => {
+ video.setProp("subtitlesSize", settings.subtitlesSize);
+ video.setProp("extraSubtitlesSize", settings.subtitlesSize);
+ }, [settings.subtitlesSize]);
+
+ React.useEffect(() => {
+ video.setProp("subtitlesOffset", settings.subtitlesOffset);
+ video.setProp("extraSubtitlesOffset", settings.subtitlesOffset);
+ }, [settings.subtitlesOffset]);
+
+ React.useEffect(() => {
+ video.setProp("subtitlesTextColor", settings.subtitlesTextColor);
+ video.setProp("extraSubtitlesTextColor", settings.subtitlesTextColor);
+ }, [settings.subtitlesTextColor]);
+
+ React.useEffect(() => {
+ video.setProp(
+ "subtitlesBackgroundColor",
+ settings.subtitlesBackgroundColor
+ );
+ video.setProp(
+ "extraSubtitlesBackgroundColor",
+ settings.subtitlesBackgroundColor
+ );
+ }, [settings.subtitlesBackgroundColor]);
+
+ React.useEffect(() => {
+ video.setProp("subtitlesOutlineColor", settings.subtitlesOutlineColor);
+ video.setProp("extraSubtitlesOutlineColor", settings.subtitlesOutlineColor);
+ }, [settings.subtitlesOutlineColor]);
+
+ React.useEffect(() => {
+ if (
+ video.state.time !== null &&
+ !isNaN(video.state.time) &&
+ video.state.duration !== null &&
+ !isNaN(video.state.duration) &&
+ video.state.manifest !== null &&
+ typeof video.state.manifest.name === "string"
+ ) {
+ timeChanged(
+ video.state.time,
+ video.state.duration,
+ video.state.manifest.name
+ );
+ }
+ }, [video.state.time, video.state.duration, video.state.manifest]);
+
+ React.useEffect(() => {
+ if (video.state.paused !== null) {
+ pausedChanged(video.state.paused);
+ }
+ }, [video.state.paused]);
+
+ React.useEffect(() => {
+ videoParamsChanged(video.state.videoParams);
+ }, [video.state.videoParams]);
+
+ React.useEffect(() => {
+ if (
+ !!settings.bingeWatching &&
+ player.nextVideo !== null &&
+ !nextVideoPopupDismissed.current
+ ) {
+ if (
+ video.state.time !== null &&
+ video.state.duration !== null &&
+ video.state.time < video.state.duration &&
+ video.state.duration - video.state.time <=
+ settings.nextVideoNotificationDuration
+ ) {
+ openNextVideoPopup();
+ } else {
+ closeNextVideoPopup();
+ }
+ }
+ }, [player.nextVideo, video.state.time, video.state.duration]);
+
+ React.useEffect(() => {
+ if (!defaultSubtitlesSelected.current) {
+ const findTrackByLang = (tracks, lang) =>
+ tracks.find(
+ (track) =>
+ track.lang === lang || langs.where("1", track.lang)?.[2] === lang
+ );
+
+ const subtitlesTrack = findTrackByLang(
+ video.state.subtitlesTracks,
+ settings.subtitlesLanguage
+ );
+ const extraSubtitlesTrack = findTrackByLang(
+ video.state.extraSubtitlesTracks,
+ settings.subtitlesLanguage
+ );
+
+ if (subtitlesTrack && subtitlesTrack.id) {
+ onSubtitlesTrackSelected(subtitlesTrack.id);
+ defaultSubtitlesSelected.current = true;
+ } else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
+ onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
+ defaultSubtitlesSelected.current = true;
+ }
+ }
+ }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
+
+ React.useEffect(() => {
+ if (!defaultAudioTrackSelected.current) {
+ const findTrackByLang = (tracks, lang) =>
+ tracks.find(
+ (track) =>
+ track.lang === lang || langs.where("1", track.lang)?.[2] === lang
+ );
+ const audioTrack = findTrackByLang(
+ video.state.audioTracks,
+ settings.audioLanguage
+ );
+
+ if (audioTrack && audioTrack.id) {
+ onAudioTrackSelected(audioTrack.id);
+ defaultAudioTrackSelected.current = true;
+ }
+ }
+ }, [video.state.audioTracks]);
+
+ React.useEffect(() => {
+ defaultSubtitlesSelected.current = false;
+ defaultAudioTrackSelected.current = false;
+ nextVideoPopupDismissed.current = false;
+ }, [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) &&
+ (!Array.isArray(video.state.audioTracks) ||
+ video.state.audioTracks.length === 0)
+ ) {
+ closeSubtitlesMenu();
+ }
+ }, [
+ video.state.audioTracks,
+ video.state.subtitlesTracks,
+ video.state.extraSubtitlesTracks,
+ ]);
+
+ React.useEffect(() => {
+ if (player.metaItem === null || player.metaItem.type !== "Ready") {
+ closeInfoMenu();
+ closeVideosMenu();
+ }
+ }, [player.metaItem]);
+
+ React.useEffect(() => {
+ if (video.state.playbackSpeed === null) {
+ closeSpeedMenu();
+ }
+ }, [video.state.playbackSpeed]);
+
+ React.useEffect(() => {
+ const toastFilter = (item) => item?.dataset?.type === "CoreEvent";
+ toast.addFilter(toastFilter);
+ const onCastStateChange = () => {
+ setCasting(
+ chromecast.active &&
+ chromecast.transport.getCastState() ===
+ cast.framework.CastState.CONNECTED
+ );
+ };
+ const onChromecastServiceStateChange = () => {
+ onCastStateChange();
+ if (chromecast.active) {
+ chromecast.transport.on(
+ cast.framework.CastContextEventType.CAST_STATE_CHANGED,
+ onCastStateChange
+ );
+ }
+ };
+ const onCoreEvent = ({ event }) => {
+ if (event === "PlayingOnDevice") {
+ onPauseRequested();
+ }
+ };
+ chromecast.on("stateChanged", onChromecastServiceStateChange);
+ core.transport.on("CoreEvent", onCoreEvent);
+ onChromecastServiceStateChange();
+ return () => {
+ toast.removeFilter(toastFilter);
+ chromecast.off("stateChanged", onChromecastServiceStateChange);
+ core.transport.off("CoreEvent", onCoreEvent);
+ if (chromecast.active) {
+ chromecast.transport.off(
+ cast.framework.CastContextEventType.CAST_STATE_CHANGED,
+ onCastStateChange
+ );
+ }
+ };
+ }, []);
+
+ React.useLayoutEffect(() => {
+ const onKeyDown = (event) => {
+ switch (event.code) {
+ case "Space": {
+ if (
+ !menusOpen &&
+ !nextVideoPopupOpen &&
+ video.state.paused !== null
+ ) {
+ if (video.state.paused) {
+ onPlayRequested();
+ } else {
+ onPauseRequested();
+ }
+ }
+
+ break;
+ }
+ case "ArrowRight": {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event.shiftKey
+ ? settings.seekShortTimeDuration
+ : settings.seekTimeDuration;
+ onSeekRequested(video.state.time + seekDuration);
+ }
+
+ break;
+ }
+ case "ArrowLeft": {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event.shiftKey
+ ? settings.seekShortTimeDuration
+ : settings.seekTimeDuration;
+ onSeekRequested(video.state.time - seekDuration);
+ }
+
+ break;
+ }
+ case "ArrowUp": {
+ if (
+ !menusOpen &&
+ !nextVideoPopupOpen &&
+ video.state.volume !== null
+ ) {
+ onVolumeChangeRequested(video.state.volume + 5);
+ }
+
+ break;
+ }
+ case "ArrowDown": {
+ if (
+ !menusOpen &&
+ !nextVideoPopupOpen &&
+ video.state.volume !== null
+ ) {
+ onVolumeChangeRequested(video.state.volume - 5);
+ }
+
+ break;
+ }
+ case "KeyS": {
+ closeMenus();
+ if (
+ (Array.isArray(video.state.subtitlesTracks) &&
+ video.state.subtitlesTracks.length > 0) ||
+ (Array.isArray(video.state.extraSubtitlesTracks) &&
+ video.state.extraSubtitlesTracks.length > 0) ||
+ (Array.isArray(video.state.audioTracks) &&
+ video.state.audioTracks.length > 0)
+ ) {
+ toggleSubtitlesMenu();
+ }
+
+ break;
+ }
+ case "KeyC": {
+ if (!menusOpen && !nextVideoPopupOpen) {
+ if (video.state.selectedSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(null);
+ } else if (previousSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(previousSubtitlesTrackId);
+ } else if (video.state.subtitlesTracks.length > 0) {
+ onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
+ } else if (video.state.extraSubtitlesTracks.length > 0) {
+ onExtraSubtitlesTrackSelected(
+ video.state.extraSubtitlesTracks[0].id
+ );
+ }
+ }
+ break;
+ }
+ case "KeyI": {
+ closeMenus();
+ if (player.metaItem !== null && player.metaItem.type === "Ready") {
+ toggleInfoMenu();
+ }
+
+ break;
+ }
+ case "KeyR": {
+ closeMenus();
+ if (video.state.playbackSpeed !== null) {
+ toggleSpeedMenu();
+ }
+
+ break;
+ }
+ case "KeyV": {
+ closeMenus();
+ if (
+ player.metaItem !== null &&
+ player.metaItem.type === "Ready" &&
+ player.metaItem?.content?.videos?.length > 0
+ ) {
+ toggleVideosMenu();
+ }
+
+ break;
+ }
+ case "KeyD": {
+ closeMenus();
+ if (
+ streamingServer.statistics !== null &&
+ streamingServer.statistics.type !== "Err" &&
+ player.selected &&
+ typeof player.selected.stream.infoHash === "string" &&
+ typeof player.selected.stream.fileIdx === "number"
+ ) {
+ toggleStatisticsMenu();
+ }
+
+ break;
+ }
+ case "Escape": {
+ closeMenus();
+ break;
+ }
+ }
+ };
+ const onWheel = ({ deltaY }) => {
+ if (deltaY > 0) {
+ if (!menusOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume - 5);
+ }
+ } else {
+ if (!menusOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume + 5);
+ }
+ }
+ };
+ if (routeFocused) {
+ window.addEventListener("keydown", onKeyDown);
+ window.addEventListener("wheel", onWheel);
+ }
+ return () => {
+ window.removeEventListener("keydown", onKeyDown);
+ window.removeEventListener("wheel", onWheel);
+ };
+ }, [
+ player.metaItem,
+ player.selected,
+ streamingServer.statistics,
+ settings.seekTimeDuration,
+ settings.seekShortTimeDuration,
+ routeFocused,
+ menusOpen,
+ nextVideoPopupOpen,
+ video.state.paused,
+ video.state.time,
+ video.state.volume,
+ video.state.audioTracks,
+ video.state.subtitlesTracks,
+ video.state.extraSubtitlesTracks,
+ video.state.playbackSpeed,
+ toggleSubtitlesMenu,
+ toggleInfoMenu,
+ toggleVideosMenu,
+ toggleStatisticsMenu,
+ ]);
+
+ 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("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("implementationChanged", onImplementationChanged);
+ };
+ }, []);
+
+ React.useLayoutEffect(() => {
+ return () => {
+ setImmersedDebounced.cancel();
+ onPlayRequestedDebounced.cancel();
+ onPauseRequestedDebounced.cancel();
+ };
+ }, []);
+
+ return (
+
+
+ {!video.state.loaded ? (
+
+

+
+ ) : null}
+ {(video.state.buffering || !video.state.loaded) && !error ? (
+
+ ) : null}
+ {error !== null ? (
+
+ ) : null}
+ {menusOpen ?
: null}
+ {video.state.volume !== null && overlayHidden ? (
+
+ ) : null}
+
+
+ {nextVideoPopupOpen ? (
+
+ ) : null}
+ {statisticsMenuOpen ? (
+
+ ) : null}
+ {subtitlesMenuOpen ? (
+
+ ) : null}
+ {infoMenuOpen ? (
+
+ ) : null}
+ {speedMenuOpen ? (
+
+ ) : null}
+ {videosMenuOpen ? (
+
+ ) : null}
+ {optionsMenuOpen ? (
+
+ ) : null}
+
+ );
};
Player.propTypes = {
- urlParams: PropTypes.shape({
- stream: PropTypes.string,
- streamTransportUrl: PropTypes.string,
- metaTransportUrl: PropTypes.string,
- type: PropTypes.string,
- id: PropTypes.string,
- videoId: PropTypes.string
- }),
- queryParams: PropTypes.instanceOf(URLSearchParams)
+ urlParams: PropTypes.shape({
+ stream: PropTypes.string,
+ streamTransportUrl: PropTypes.string,
+ metaTransportUrl: PropTypes.string,
+ type: PropTypes.string,
+ id: PropTypes.string,
+ videoId: PropTypes.string,
+ }),
+ queryParams: PropTypes.instanceOf(URLSearchParams),
};
const PlayerFallback = () => (
-
+
);
module.exports = withCoreSuspender(Player, PlayerFallback);
From 881c8080035d0deaabab67602d6ce9f4cf44257d Mon Sep 17 00:00:00 2001
From: Neeraj TK <64883030+GaryGosh@users.noreply.github.com>
Date: Mon, 23 Sep 2024 20:07:11 +0530
Subject: [PATCH 03/25] formatting revert
---
src/routes/Player/Player.js | 1722 ++++++++++++++++-------------------
1 file changed, 765 insertions(+), 957 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index 894a9b466..00071e02d 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -1,1014 +1,822 @@
// Copyright (C) 2017-2023 Smart code 203358507
-const React = require("react");
-const PropTypes = require("prop-types");
-const classnames = require("classnames");
-const debounce = require("lodash.debounce");
-const langs = require("langs");
-const { useTranslation } = require("react-i18next");
-const { useRouteFocused } = require("stremio-router");
-const { useServices } = require("stremio/services");
-const {
- HorizontalNavBar,
- useFullscreen,
- useBinaryState,
- useToast,
- useStreamingServer,
- withCoreSuspender,
-} = require("stremio/common");
-const BufferingLoader = require("./BufferingLoader");
-const VolumeChangeIndicator = require("./VolumeChangeIndicator");
-const Error = require("./Error");
-const ControlBar = require("./ControlBar");
-const NextVideoPopup = require("./NextVideoPopup");
-const StatisticsMenu = require("./StatisticsMenu");
-const InfoMenu = require("./InfoMenu");
-const OptionsMenu = require("./OptionsMenu");
-const VideosMenu = require("./VideosMenu");
-const SubtitlesMenu = require("./SubtitlesMenu");
-const SpeedMenu = require("./SpeedMenu");
-const usePlayer = require("./usePlayer");
-const useSettings = require("./useSettings");
-const useStatistics = require("./useStatistics");
-const useVideo = require("./useVideo");
-const styles = require("./styles");
-const Video = require("./Video");
+const React = require('react');
+const PropTypes = require('prop-types');
+const classnames = require('classnames');
+const debounce = require('lodash.debounce');
+const langs = require('langs');
+const { useTranslation } = require('react-i18next');
+const { useRouteFocused } = require('stremio-router');
+const { useServices } = require('stremio/services');
+const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common');
+const BufferingLoader = require('./BufferingLoader');
+const VolumeChangeIndicator = require('./VolumeChangeIndicator');
+const Error = require('./Error');
+const ControlBar = require('./ControlBar');
+const NextVideoPopup = require('./NextVideoPopup');
+const StatisticsMenu = require('./StatisticsMenu');
+const InfoMenu = require('./InfoMenu');
+const OptionsMenu = require('./OptionsMenu');
+const VideosMenu = require('./VideosMenu');
+const SubtitlesMenu = require('./SubtitlesMenu');
+const SpeedMenu = require('./SpeedMenu');
+const usePlayer = require('./usePlayer');
+const useSettings = require('./useSettings');
+const useStatistics = require('./useStatistics');
+const useVideo = require('./useVideo');
+const styles = require('./styles');
+const Video = require('./Video');
const Player = ({ urlParams, queryParams }) => {
- const { t } = useTranslation();
- const { chromecast, shell, core } = useServices();
- const forceTranscoding = React.useMemo(() => {
- return queryParams.has("forceTranscoding");
- }, [queryParams]);
+ const { t } = useTranslation();
+ const { chromecast, shell, core } = useServices();
+ const forceTranscoding = React.useMemo(() => {
+ return queryParams.has('forceTranscoding');
+ }, [queryParams]);
- const [
- player,
- videoParamsChanged,
- timeChanged,
- pausedChanged,
- ended,
- nextVideo,
- ] = usePlayer(urlParams);
- const [settings, updateSettings] = useSettings();
- const streamingServer = useStreamingServer();
- const statistics = useStatistics(player, streamingServer);
- const video = useVideo();
- const routeFocused = useRouteFocused();
- const toast = useToast();
+ const [player, videoParamsChanged, timeChanged, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
+ const [settings, updateSettings] = useSettings();
+ const streamingServer = useStreamingServer();
+ const statistics = useStatistics(player, streamingServer);
+ const video = useVideo();
+ const routeFocused = useRouteFocused();
+ const toast = useToast();
- const [casting, setCasting] = React.useState(() => {
- return (
- chromecast.active &&
- chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED
- );
- });
-
- const [immersed, setImmersed] = React.useState(true);
- const setImmersedDebounced = React.useCallback(
- debounce(setImmersed, 3000),
- []
- );
- const [, , , toggleFullscreen] = useFullscreen();
-
- const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] =
- useBinaryState(false);
- const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] =
- useBinaryState(false);
- const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
- const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] =
- useBinaryState(false);
- const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] =
- useBinaryState(false);
- const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] =
- useBinaryState(false);
- const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] =
- useBinaryState(false);
-
- const menusOpen = React.useMemo(() => {
- return (
- optionsMenuOpen ||
- subtitlesMenuOpen ||
- infoMenuOpen ||
- speedMenuOpen ||
- videosMenuOpen ||
- statisticsMenuOpen
- );
- }, [
- optionsMenuOpen,
- subtitlesMenuOpen,
- infoMenuOpen,
- speedMenuOpen,
- videosMenuOpen,
- statisticsMenuOpen,
- ]);
-
- const closeMenus = React.useCallback(() => {
- closeOptionsMenu();
- closeSubtitlesMenu();
- closeInfoMenu();
- closeSpeedMenu();
- closeVideosMenu();
- closeStatisticsMenu();
- }, []);
-
- const overlayHidden = React.useMemo(() => {
- return (
- immersed &&
- !casting &&
- video.state.paused !== null &&
- !video.state.paused &&
- !menusOpen &&
- !nextVideoPopupOpen
- );
- }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
-
- const nextVideoPopupDismissed = React.useRef(false);
- const defaultSubtitlesSelected = React.useRef(false);
- const defaultAudioTrackSelected = React.useRef(false);
- const [error, setError] = React.useState(null);
- const [previousSubtitlesTrackId, setPreviousSubtitlesTrackId] =
- React.useState(null);
-
- const onImplementationChanged = React.useCallback(() => {
- video.setProp("subtitlesSize", settings.subtitlesSize);
- video.setProp("subtitlesOffset", settings.subtitlesOffset);
- video.setProp("subtitlesTextColor", settings.subtitlesTextColor);
- video.setProp(
- "subtitlesBackgroundColor",
- settings.subtitlesBackgroundColor
- );
- video.setProp("subtitlesOutlineColor", settings.subtitlesOutlineColor);
- video.setProp("extraSubtitlesSize", settings.subtitlesSize);
- video.setProp("extraSubtitlesOffset", settings.subtitlesOffset);
- video.setProp("extraSubtitlesTextColor", settings.subtitlesTextColor);
- video.setProp(
- "extraSubtitlesBackgroundColor",
- settings.subtitlesBackgroundColor
- );
- video.setProp("extraSubtitlesOutlineColor", settings.subtitlesOutlineColor);
- }, [
- settings.subtitlesSize,
- settings.subtitlesOffset,
- settings.subtitlesTextColor,
- settings.subtitlesBackgroundColor,
- settings.subtitlesOutlineColor,
- ]);
-
- const onEnded = React.useCallback(() => {
- ended();
- if (player.nextVideo !== null) {
- onNextVideoRequested();
- } else {
- window.history.back();
- }
- }, [player.nextVideo, onNextVideoRequested]);
-
- const onError = React.useCallback((error) => {
- console.error("Player", error);
- if (error.critical) {
- setError(error);
- } else {
- toast.show({
- type: "error",
- title: t("ERROR"),
- message: error.message,
- timeout: 3000,
- });
- }
- }, []);
-
- const onSubtitlesTrackLoaded = React.useCallback(() => {
- toast.show({
- type: "success",
- title: t("PLAYER_SUBTITLES_LOADED"),
- message: t("PLAYER_SUBTITLES_LOADED_EMBEDDED"),
- timeout: 3000,
+ const [casting, setCasting] = React.useState(() => {
+ return chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED;
});
- }, []);
- const onExtraSubtitlesTrackLoaded = React.useCallback((track) => {
- toast.show({
- type: "success",
- title: t("PLAYER_SUBTITLES_LOADED"),
- message: track.exclusive
- ? t("PLAYER_SUBTITLES_LOADED_EXCLUSIVE")
- : t("PLAYER_SUBTITLES_LOADED_ORIGIN", { origin: track.origin }),
- timeout: 3000,
- });
- }, []);
+ const [immersed, setImmersed] = React.useState(true);
+ const setImmersedDebounced = React.useCallback(debounce(setImmersed, 3000), []);
+ const [, , , toggleFullscreen] = useFullscreen();
- const onPlayRequested = React.useCallback(() => {
- video.setProp("paused", false);
- }, []);
+ const [optionsMenuOpen, , closeOptionsMenu, toggleOptionsMenu] = useBinaryState(false);
+ const [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
+ const [infoMenuOpen, , closeInfoMenu, toggleInfoMenu] = useBinaryState(false);
+ const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
+ const [videosMenuOpen, , closeVideosMenu, toggleVideosMenu] = useBinaryState(false);
+ const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
+ const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
- const onPlayRequestedDebounced = React.useCallback(
- debounce(onPlayRequested, 200),
- []
- );
+ const menusOpen = React.useMemo(() => {
+ return optionsMenuOpen || subtitlesMenuOpen || infoMenuOpen || speedMenuOpen || videosMenuOpen || statisticsMenuOpen;
+ }, [optionsMenuOpen, subtitlesMenuOpen, infoMenuOpen, speedMenuOpen, videosMenuOpen, statisticsMenuOpen]);
- const onPauseRequested = React.useCallback(() => {
- video.setProp("paused", true);
- }, []);
+ const closeMenus = React.useCallback(() => {
+ closeOptionsMenu();
+ closeSubtitlesMenu();
+ closeInfoMenu();
+ closeSpeedMenu();
+ closeVideosMenu();
+ closeStatisticsMenu();
+ }, []);
- const onPauseRequestedDebounced = React.useCallback(
- debounce(onPauseRequested, 200),
- []
- );
- const onMuteRequested = React.useCallback(() => {
- video.setProp("muted", true);
- }, []);
+ const overlayHidden = React.useMemo(() => {
+ return immersed && !casting && video.state.paused !== null && !video.state.paused && !menusOpen && !nextVideoPopupOpen;
+ }, [immersed, casting, video.state.paused, menusOpen, nextVideoPopupOpen]);
- const onUnmuteRequested = React.useCallback(() => {
- video.setProp("muted", false);
- }, []);
+ const nextVideoPopupDismissed = React.useRef(false);
+ const defaultSubtitlesSelected = React.useRef(false);
+ const defaultAudioTrackSelected = React.useRef(false);
+ const [error, setError] = React.useState(null);
+ const [previousSubtitlesTrackId, setPreviousSubtitlesTrackId] = React.useState(null);
- const onVolumeChangeRequested = React.useCallback((volume) => {
- video.setProp("volume", volume);
- }, []);
- const onSeekRequested = React.useCallback((time) => {
- video.setProp("time", time);
- }, []);
+ const onImplementationChanged = React.useCallback(() => {
+ video.setProp('subtitlesSize', settings.subtitlesSize);
+ video.setProp('subtitlesOffset', settings.subtitlesOffset);
+ video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
+ video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
+ video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
+ video.setProp('extraSubtitlesSize', settings.subtitlesSize);
+ video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
+ video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
+ video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
+ video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
+ }, [settings.subtitlesSize, settings.subtitlesOffset, settings.subtitlesTextColor, settings.subtitlesBackgroundColor, settings.subtitlesOutlineColor]);
- const onPlaybackSpeedChanged = React.useCallback((rate) => {
- video.setProp("playbackSpeed", rate);
- }, []);
-
- const onSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp("selectedSubtitlesTrackId", id);
- video.setProp("selectedExtraSubtitlesTrackId", null);
- }, []);
-
- const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp("selectedSubtitlesTrackId", null);
- video.setProp("selectedExtraSubtitlesTrackId", id);
- }, []);
-
- const onAudioTrackSelected = React.useCallback((id) => {
- video.setProp("selectedAudioTrackId", id);
- }, []);
-
- const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
- video.setProp("extraSubtitlesDelay", delay);
- }, []);
-
- const onSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp("selectedSubtitlesTrackId", id);
- video.setProp("selectedExtraSubtitlesTrackId", null);
- setPreviousSubtitlesTrackId(id);
- }, []);
-
- const onSubtitlesSizeChanged = React.useCallback(
- (size) => {
- updateSettings({ subtitlesSize: size });
- },
- [updateSettings]
- );
-
- const onSubtitlesOffsetChanged = React.useCallback(
- (offset) => {
- updateSettings({ subtitlesOffset: offset });
- },
- [updateSettings]
- );
-
- const onDismissNextVideoPopup = React.useCallback(() => {
- closeNextVideoPopup();
- nextVideoPopupDismissed.current = true;
- }, []);
-
- const onNextVideoRequested = React.useCallback(() => {
- if (player.nextVideo !== null) {
- nextVideo();
-
- const deepLinks = player.nextVideo.deepLinks;
- if (deepLinks.metaDetailsStreams && deepLinks.player) {
- window.location.replace(deepLinks.metaDetailsStreams);
- window.location.href = deepLinks.player;
- } else {
- window.location.replace(
- deepLinks.player ?? deepLinks.metaDetailsStreams
- );
- }
- }
- }, [player.nextVideo]);
-
- const onVideoClick = React.useCallback(() => {
- if (video.state.paused !== null) {
- if (video.state.paused) {
- onPlayRequestedDebounced();
- } else {
- onPauseRequestedDebounced();
- }
- }
- }, [video.state.paused]);
-
- const onVideoDoubleClick = React.useCallback(() => {
- onPlayRequestedDebounced.cancel();
- onPauseRequestedDebounced.cancel();
- toggleFullscreen();
- }, [toggleFullscreen]);
-
- const onContainerMouseDown = React.useCallback((event) => {
- if (!event.nativeEvent.optionsMenuClosePrevented) {
- closeOptionsMenu();
- }
- if (!event.nativeEvent.subtitlesMenuClosePrevented) {
- closeSubtitlesMenu();
- }
- if (!event.nativeEvent.infoMenuClosePrevented) {
- closeInfoMenu();
- }
- if (!event.nativeEvent.speedMenuClosePrevented) {
- closeSpeedMenu();
- }
- if (!event.nativeEvent.videosMenuClosePrevented) {
- closeVideosMenu();
- }
- if (!event.nativeEvent.statisticsMenuClosePrevented) {
- closeStatisticsMenu();
- }
- }, []);
-
- const onContainerMouseMove = React.useCallback((event) => {
- setImmersed(false);
- if (!event.nativeEvent.immersePrevented) {
- setImmersedDebounced(true);
- } else {
- setImmersedDebounced.cancel();
- }
- }, []);
-
- const onContainerMouseLeave = React.useCallback(() => {
- setImmersedDebounced.cancel();
- setImmersed(true);
- }, []);
-
- const onBarMouseMove = React.useCallback((event) => {
- event.nativeEvent.immersePrevented = true;
- }, []);
-
- React.useEffect(() => {
- setError(null);
- if (player.selected === null) {
- video.unload();
- } else if (
- streamingServer.settings !== null &&
- streamingServer.settings.type !== "Loading" &&
- (player.selected.metaRequest === null ||
- (player.metaItem !== null && player.metaItem.type !== "Loading"))
- ) {
- video.load(
- {
- stream: {
- ...player.selected.stream,
- subtitles: Array.isArray(player.selected.stream.subtitles)
- ? player.selected.stream.subtitles.map((subtitles) => ({
- ...subtitles,
- label: subtitles.url,
- }))
- : [],
- },
- autoplay: true,
- time:
- player.libraryItem !== null &&
- player.selected.streamRequest !== null &&
- player.selected.streamRequest.path !== null &&
- player.libraryItem.state.video_id ===
- player.selected.streamRequest.path.id
- ? player.libraryItem.state.timeOffset
- : 0,
- forceTranscoding: forceTranscoding || casting,
- maxAudioChannels: settings.surroundSound ? 32 : 2,
- streamingServerURL: streamingServer.baseUrl
- ? casting
- ? streamingServer.baseUrl
- : streamingServer.selected.transportUrl
- : null,
- seriesInfo: player.seriesInfo,
- },
- {
- chromecastTransport: chromecast.active ? chromecast.transport : null,
- shellTransport: shell.active ? shell.transport : null,
+ const onEnded = React.useCallback(() => {
+ ended();
+ if (player.nextVideo !== null) {
+ onNextVideoRequested();
+ } else {
+ window.history.back();
}
- );
- }
- }, [
- streamingServer.baseUrl,
- player.selected,
- player.metaItem,
- forceTranscoding,
- casting,
- ]);
- React.useEffect(() => {
- if (video.state.stream !== null) {
- const tracks = player.subtitles.map((subtitles) => ({
- ...subtitles,
- label: subtitles.url,
- }));
- video.addExtraSubtitlesTracks(tracks);
- }
- }, [player.subtitles, video.state.stream]);
+ }, [player.nextVideo, onNextVideoRequested]);
- React.useEffect(() => {
- video.setProp("subtitlesSize", settings.subtitlesSize);
- video.setProp("extraSubtitlesSize", settings.subtitlesSize);
- }, [settings.subtitlesSize]);
+ const onError = React.useCallback((error) => {
+ console.error('Player', error);
+ if (error.critical) {
+ setError(error);
+ } else {
+ toast.show({
+ type: 'error',
+ title: t('ERROR'),
+ message: error.message,
+ timeout: 3000
+ });
+ }
+ }, []);
- React.useEffect(() => {
- video.setProp("subtitlesOffset", settings.subtitlesOffset);
- video.setProp("extraSubtitlesOffset", settings.subtitlesOffset);
- }, [settings.subtitlesOffset]);
+ const onSubtitlesTrackLoaded = React.useCallback(() => {
+ toast.show({
+ type: 'success',
+ title: t('PLAYER_SUBTITLES_LOADED'),
+ message: t('PLAYER_SUBTITLES_LOADED_EMBEDDED'),
+ timeout: 3000
+ });
+ }, []);
- React.useEffect(() => {
- video.setProp("subtitlesTextColor", settings.subtitlesTextColor);
- video.setProp("extraSubtitlesTextColor", settings.subtitlesTextColor);
- }, [settings.subtitlesTextColor]);
+ const onExtraSubtitlesTrackLoaded = React.useCallback((track) => {
+ toast.show({
+ type: 'success',
+ title: t('PLAYER_SUBTITLES_LOADED'),
+ message: track.exclusive ? t('PLAYER_SUBTITLES_LOADED_EXCLUSIVE') : t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
+ timeout: 3000
+ });
+ }, []);
- React.useEffect(() => {
- video.setProp(
- "subtitlesBackgroundColor",
- settings.subtitlesBackgroundColor
- );
- video.setProp(
- "extraSubtitlesBackgroundColor",
- settings.subtitlesBackgroundColor
- );
- }, [settings.subtitlesBackgroundColor]);
+ const onPlayRequested = React.useCallback(() => {
+ video.setProp('paused', false);
+ }, []);
- React.useEffect(() => {
- video.setProp("subtitlesOutlineColor", settings.subtitlesOutlineColor);
- video.setProp("extraSubtitlesOutlineColor", settings.subtitlesOutlineColor);
- }, [settings.subtitlesOutlineColor]);
+ const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []);
- React.useEffect(() => {
- if (
- video.state.time !== null &&
- !isNaN(video.state.time) &&
- video.state.duration !== null &&
- !isNaN(video.state.duration) &&
- video.state.manifest !== null &&
- typeof video.state.manifest.name === "string"
- ) {
- timeChanged(
- video.state.time,
- video.state.duration,
- video.state.manifest.name
- );
- }
- }, [video.state.time, video.state.duration, video.state.manifest]);
+ const onPauseRequested = React.useCallback(() => {
+ video.setProp('paused', true);
+ }, []);
- React.useEffect(() => {
- if (video.state.paused !== null) {
- pausedChanged(video.state.paused);
- }
- }, [video.state.paused]);
+ const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []);
+ const onMuteRequested = React.useCallback(() => {
+ video.setProp('muted', true);
+ }, []);
- React.useEffect(() => {
- videoParamsChanged(video.state.videoParams);
- }, [video.state.videoParams]);
+ const onUnmuteRequested = React.useCallback(() => {
+ video.setProp('muted', false);
+ }, []);
- React.useEffect(() => {
- if (
- !!settings.bingeWatching &&
- player.nextVideo !== null &&
- !nextVideoPopupDismissed.current
- ) {
- if (
- video.state.time !== null &&
- video.state.duration !== null &&
- video.state.time < video.state.duration &&
- video.state.duration - video.state.time <=
- settings.nextVideoNotificationDuration
- ) {
- openNextVideoPopup();
- } else {
+ const onVolumeChangeRequested = React.useCallback((volume) => {
+ video.setProp('volume', volume);
+ }, []);
+
+ const onSeekRequested = React.useCallback((time) => {
+ video.setProp('time', time);
+ }, []);
+
+ const onPlaybackSpeedChanged = React.useCallback((rate) => {
+ video.setProp('playbackSpeed', rate);
+ }, []);
+
+ const onSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp('selectedSubtitlesTrackId', id);
+ video.setProp('selectedExtraSubtitlesTrackId', null);
+ }, []);
+
+ const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp('selectedSubtitlesTrackId', null);
+ video.setProp('selectedExtraSubtitlesTrackId', id);
+ }, []);
+
+ const onAudioTrackSelected = React.useCallback((id) => {
+ video.setProp('selectedAudioTrackId', id);
+ }, []);
+
+ const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
+ video.setProp('extraSubtitlesDelay', delay);
+ }, []);
+
+ const onSubtitlesTrackSelected = React.useCallback((id) => {
+ video.setProp('selectedSubtitlesTrackId', id);
+ video.setProp('selectedExtraSubtitlesTrackId', null);
+ setPreviousSubtitlesTrackId(id);
+ }, []);
+
+ const onSubtitlesSizeChanged = React.useCallback((size) => {
+ updateSettings({ subtitlesSize: size });
+ }, [updateSettings]);
+
+ const onSubtitlesOffsetChanged = React.useCallback((offset) => {
+ updateSettings({ subtitlesOffset: offset });
+ }, [updateSettings]);
+
+ const onDismissNextVideoPopup = React.useCallback(() => {
closeNextVideoPopup();
- }
- }
- }, [player.nextVideo, video.state.time, video.state.duration]);
+ nextVideoPopupDismissed.current = true;
+ }, []);
- React.useEffect(() => {
- if (!defaultSubtitlesSelected.current) {
- const findTrackByLang = (tracks, lang) =>
- tracks.find(
- (track) =>
- track.lang === lang || langs.where("1", track.lang)?.[2] === lang
- );
+ const onNextVideoRequested = React.useCallback(() => {
+ if (player.nextVideo !== null) {
+ nextVideo();
- const subtitlesTrack = findTrackByLang(
- video.state.subtitlesTracks,
- settings.subtitlesLanguage
- );
- const extraSubtitlesTrack = findTrackByLang(
- video.state.extraSubtitlesTracks,
- settings.subtitlesLanguage
- );
-
- if (subtitlesTrack && subtitlesTrack.id) {
- onSubtitlesTrackSelected(subtitlesTrack.id);
- defaultSubtitlesSelected.current = true;
- } else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
- onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
- defaultSubtitlesSelected.current = true;
- }
- }
- }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
-
- React.useEffect(() => {
- if (!defaultAudioTrackSelected.current) {
- const findTrackByLang = (tracks, lang) =>
- tracks.find(
- (track) =>
- track.lang === lang || langs.where("1", track.lang)?.[2] === lang
- );
- const audioTrack = findTrackByLang(
- video.state.audioTracks,
- settings.audioLanguage
- );
-
- if (audioTrack && audioTrack.id) {
- onAudioTrackSelected(audioTrack.id);
- defaultAudioTrackSelected.current = true;
- }
- }
- }, [video.state.audioTracks]);
-
- React.useEffect(() => {
- defaultSubtitlesSelected.current = false;
- defaultAudioTrackSelected.current = false;
- nextVideoPopupDismissed.current = false;
- }, [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) &&
- (!Array.isArray(video.state.audioTracks) ||
- video.state.audioTracks.length === 0)
- ) {
- closeSubtitlesMenu();
- }
- }, [
- video.state.audioTracks,
- video.state.subtitlesTracks,
- video.state.extraSubtitlesTracks,
- ]);
-
- React.useEffect(() => {
- if (player.metaItem === null || player.metaItem.type !== "Ready") {
- closeInfoMenu();
- closeVideosMenu();
- }
- }, [player.metaItem]);
-
- React.useEffect(() => {
- if (video.state.playbackSpeed === null) {
- closeSpeedMenu();
- }
- }, [video.state.playbackSpeed]);
-
- React.useEffect(() => {
- const toastFilter = (item) => item?.dataset?.type === "CoreEvent";
- toast.addFilter(toastFilter);
- const onCastStateChange = () => {
- setCasting(
- chromecast.active &&
- chromecast.transport.getCastState() ===
- cast.framework.CastState.CONNECTED
- );
- };
- const onChromecastServiceStateChange = () => {
- onCastStateChange();
- if (chromecast.active) {
- chromecast.transport.on(
- cast.framework.CastContextEventType.CAST_STATE_CHANGED,
- onCastStateChange
- );
- }
- };
- const onCoreEvent = ({ event }) => {
- if (event === "PlayingOnDevice") {
- onPauseRequested();
- }
- };
- chromecast.on("stateChanged", onChromecastServiceStateChange);
- core.transport.on("CoreEvent", onCoreEvent);
- onChromecastServiceStateChange();
- return () => {
- toast.removeFilter(toastFilter);
- chromecast.off("stateChanged", onChromecastServiceStateChange);
- core.transport.off("CoreEvent", onCoreEvent);
- if (chromecast.active) {
- chromecast.transport.off(
- cast.framework.CastContextEventType.CAST_STATE_CHANGED,
- onCastStateChange
- );
- }
- };
- }, []);
-
- React.useLayoutEffect(() => {
- const onKeyDown = (event) => {
- switch (event.code) {
- case "Space": {
- if (
- !menusOpen &&
- !nextVideoPopupOpen &&
- video.state.paused !== null
- ) {
- if (video.state.paused) {
- onPlayRequested();
+ const deepLinks = player.nextVideo.deepLinks;
+ if (deepLinks.metaDetailsStreams && deepLinks.player) {
+ window.location.replace(deepLinks.metaDetailsStreams);
+ window.location.href = deepLinks.player;
} else {
- onPauseRequested();
+ window.location.replace(deepLinks.player ?? deepLinks.metaDetailsStreams);
}
- }
-
- break;
}
- case "ArrowRight": {
- if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
- const seekDuration = event.shiftKey
- ? settings.seekShortTimeDuration
- : settings.seekTimeDuration;
- onSeekRequested(video.state.time + seekDuration);
- }
+ }, [player.nextVideo]);
- break;
- }
- case "ArrowLeft": {
- if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
- const seekDuration = event.shiftKey
- ? settings.seekShortTimeDuration
- : settings.seekTimeDuration;
- onSeekRequested(video.state.time - seekDuration);
- }
-
- break;
- }
- case "ArrowUp": {
- if (
- !menusOpen &&
- !nextVideoPopupOpen &&
- video.state.volume !== null
- ) {
- onVolumeChangeRequested(video.state.volume + 5);
- }
-
- break;
- }
- case "ArrowDown": {
- if (
- !menusOpen &&
- !nextVideoPopupOpen &&
- video.state.volume !== null
- ) {
- onVolumeChangeRequested(video.state.volume - 5);
- }
-
- break;
- }
- case "KeyS": {
- closeMenus();
- if (
- (Array.isArray(video.state.subtitlesTracks) &&
- video.state.subtitlesTracks.length > 0) ||
- (Array.isArray(video.state.extraSubtitlesTracks) &&
- video.state.extraSubtitlesTracks.length > 0) ||
- (Array.isArray(video.state.audioTracks) &&
- video.state.audioTracks.length > 0)
- ) {
- toggleSubtitlesMenu();
- }
-
- break;
- }
- case "KeyC": {
- if (!menusOpen && !nextVideoPopupOpen) {
- if (video.state.selectedSubtitlesTrackId !== null) {
- onSubtitlesTrackSelected(null);
- } else if (previousSubtitlesTrackId !== null) {
- onSubtitlesTrackSelected(previousSubtitlesTrackId);
- } else if (video.state.subtitlesTracks.length > 0) {
- onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
- } else if (video.state.extraSubtitlesTracks.length > 0) {
- onExtraSubtitlesTrackSelected(
- video.state.extraSubtitlesTracks[0].id
- );
+ const onVideoClick = React.useCallback(() => {
+ if (video.state.paused !== null) {
+ if (video.state.paused) {
+ onPlayRequestedDebounced();
+ } else {
+ onPauseRequestedDebounced();
}
- }
- break;
}
- case "KeyI": {
- closeMenus();
- if (player.metaItem !== null && player.metaItem.type === "Ready") {
- toggleInfoMenu();
- }
+ }, [video.state.paused]);
- break;
+ const onVideoDoubleClick = React.useCallback(() => {
+ onPlayRequestedDebounced.cancel();
+ onPauseRequestedDebounced.cancel();
+ toggleFullscreen();
+ }, [toggleFullscreen]);
+
+ const onContainerMouseDown = React.useCallback((event) => {
+ if (!event.nativeEvent.optionsMenuClosePrevented) {
+ closeOptionsMenu();
}
- case "KeyR": {
- closeMenus();
- if (video.state.playbackSpeed !== null) {
- toggleSpeedMenu();
- }
-
- break;
+ if (!event.nativeEvent.subtitlesMenuClosePrevented) {
+ closeSubtitlesMenu();
}
- case "KeyV": {
- closeMenus();
- if (
- player.metaItem !== null &&
- player.metaItem.type === "Ready" &&
- player.metaItem?.content?.videos?.length > 0
- ) {
- toggleVideosMenu();
- }
-
- break;
+ if (!event.nativeEvent.infoMenuClosePrevented) {
+ closeInfoMenu();
}
- case "KeyD": {
- closeMenus();
- if (
- streamingServer.statistics !== null &&
- streamingServer.statistics.type !== "Err" &&
- player.selected &&
- typeof player.selected.stream.infoHash === "string" &&
- typeof player.selected.stream.fileIdx === "number"
- ) {
- toggleStatisticsMenu();
- }
-
- break;
+ if (!event.nativeEvent.speedMenuClosePrevented) {
+ closeSpeedMenu();
}
- case "Escape": {
- closeMenus();
- break;
+ if (!event.nativeEvent.videosMenuClosePrevented) {
+ closeVideosMenu();
}
- }
- };
- const onWheel = ({ deltaY }) => {
- if (deltaY > 0) {
- if (!menusOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume - 5);
+ if (!event.nativeEvent.statisticsMenuClosePrevented) {
+ closeStatisticsMenu();
}
- } else {
- if (!menusOpen && video.state.volume !== null) {
- onVolumeChangeRequested(video.state.volume + 5);
+ }, []);
+
+ const onContainerMouseMove = React.useCallback((event) => {
+ setImmersed(false);
+ if (!event.nativeEvent.immersePrevented) {
+ setImmersedDebounced(true);
+ } else {
+ setImmersedDebounced.cancel();
}
- }
- };
- if (routeFocused) {
- window.addEventListener("keydown", onKeyDown);
- window.addEventListener("wheel", onWheel);
- }
- return () => {
- window.removeEventListener("keydown", onKeyDown);
- window.removeEventListener("wheel", onWheel);
- };
- }, [
- player.metaItem,
- player.selected,
- streamingServer.statistics,
- settings.seekTimeDuration,
- settings.seekShortTimeDuration,
- routeFocused,
- menusOpen,
- nextVideoPopupOpen,
- video.state.paused,
- video.state.time,
- video.state.volume,
- video.state.audioTracks,
- video.state.subtitlesTracks,
- video.state.extraSubtitlesTracks,
- video.state.playbackSpeed,
- toggleSubtitlesMenu,
- toggleInfoMenu,
- toggleVideosMenu,
- toggleStatisticsMenu,
- ]);
+ }, []);
- 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("implementationChanged", onImplementationChanged);
+ const onContainerMouseLeave = React.useCallback(() => {
+ setImmersedDebounced.cancel();
+ setImmersed(true);
+ }, []);
- return () => {
- video.events.off("error", onError);
- video.events.off("ended", onEnded);
- video.events.off("subtitlesTrackLoaded", onSubtitlesTrackLoaded);
- video.events.off(
- "extraSubtitlesTrackLoaded",
- onExtraSubtitlesTrackLoaded
- );
- video.events.off("implementationChanged", onImplementationChanged);
- };
- }, []);
+ const onBarMouseMove = React.useCallback((event) => {
+ event.nativeEvent.immersePrevented = true;
+ }, []);
- React.useLayoutEffect(() => {
- return () => {
- setImmersedDebounced.cancel();
- onPlayRequestedDebounced.cancel();
- onPauseRequestedDebounced.cancel();
- };
- }, []);
+ React.useEffect(() => {
+ setError(null);
+ if (player.selected === null) {
+ video.unload();
+ } else if (streamingServer.settings !== null && streamingServer.settings.type !== 'Loading' &&
+ (player.selected.metaRequest === null || (player.metaItem !== null && player.metaItem.type !== 'Loading'))) {
+ video.load({
+ stream: {
+ ...player.selected.stream,
+ subtitles: Array.isArray(player.selected.stream.subtitles) ?
+ player.selected.stream.subtitles.map((subtitles) => ({
+ ...subtitles,
+ label: subtitles.url
+ }))
+ :
+ []
+ },
+ autoplay: true,
+ time: player.libraryItem !== null &&
+ player.selected.streamRequest !== null &&
+ player.selected.streamRequest.path !== null &&
+ player.libraryItem.state.video_id === player.selected.streamRequest.path.id ?
+ player.libraryItem.state.timeOffset
+ :
+ 0,
+ forceTranscoding: forceTranscoding || casting,
+ maxAudioChannels: settings.surroundSound ? 32 : 2,
+ streamingServerURL: streamingServer.baseUrl ?
+ casting ?
+ streamingServer.baseUrl
+ :
+ streamingServer.selected.transportUrl
+ :
+ null,
+ seriesInfo: player.seriesInfo
+ }, {
+ chromecastTransport: chromecast.active ? chromecast.transport : null,
+ shellTransport: shell.active ? shell.transport : null,
+ });
+ }
+ }, [streamingServer.baseUrl, player.selected, player.metaItem, forceTranscoding, casting]);
+ React.useEffect(() => {
+ if (video.state.stream !== null) {
+ const tracks = player.subtitles.map((subtitles) => ({
+ ...subtitles,
+ label: subtitles.url
+ }));
+ video.addExtraSubtitlesTracks(tracks);
+ }
+ }, [player.subtitles, video.state.stream]);
- return (
-
-
- {!video.state.loaded ? (
-
-

+ React.useEffect(() => {
+ video.setProp('subtitlesSize', settings.subtitlesSize);
+ video.setProp('extraSubtitlesSize', settings.subtitlesSize);
+ }, [settings.subtitlesSize]);
+
+ React.useEffect(() => {
+ video.setProp('subtitlesOffset', settings.subtitlesOffset);
+ video.setProp('extraSubtitlesOffset', settings.subtitlesOffset);
+ }, [settings.subtitlesOffset]);
+
+ React.useEffect(() => {
+ video.setProp('subtitlesTextColor', settings.subtitlesTextColor);
+ video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor);
+ }, [settings.subtitlesTextColor]);
+
+ React.useEffect(() => {
+ video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor);
+ video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor);
+ }, [settings.subtitlesBackgroundColor]);
+
+ React.useEffect(() => {
+ video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor);
+ video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor);
+ }, [settings.subtitlesOutlineColor]);
+
+ React.useEffect(() => {
+ if (video.state.time !== null && !isNaN(video.state.time) &&
+ video.state.duration !== null && !isNaN(video.state.duration) &&
+ video.state.manifest !== null && typeof video.state.manifest.name === 'string') {
+ timeChanged(video.state.time, video.state.duration, video.state.manifest.name);
+ }
+ }, [video.state.time, video.state.duration, video.state.manifest]);
+
+ React.useEffect(() => {
+ if (video.state.paused !== null) {
+ pausedChanged(video.state.paused);
+ }
+ }, [video.state.paused]);
+
+ React.useEffect(() => {
+ videoParamsChanged(video.state.videoParams);
+ }, [video.state.videoParams]);
+
+ React.useEffect(() => {
+ if (!!settings.bingeWatching && player.nextVideo !== null && !nextVideoPopupDismissed.current) {
+ if (video.state.time !== null && video.state.duration !== null && video.state.time < video.state.duration && (video.state.duration - video.state.time) <= settings.nextVideoNotificationDuration) {
+ openNextVideoPopup();
+ } else {
+ closeNextVideoPopup();
+ }
+ }
+ }, [player.nextVideo, video.state.time, video.state.duration]);
+
+ React.useEffect(() => {
+ if (!defaultSubtitlesSelected.current) {
+ const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
+
+ const subtitlesTrack = findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage);
+ const extraSubtitlesTrack = findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage);
+
+ if (subtitlesTrack && subtitlesTrack.id) {
+ onSubtitlesTrackSelected(subtitlesTrack.id);
+ defaultSubtitlesSelected.current = true;
+ } else if (extraSubtitlesTrack && extraSubtitlesTrack.id) {
+ onExtraSubtitlesTrackSelected(extraSubtitlesTrack.id);
+ defaultSubtitlesSelected.current = true;
+ }
+ }
+ }, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
+
+ React.useEffect(() => {
+ if (!defaultAudioTrackSelected.current) {
+ const findTrackByLang = (tracks, lang) => tracks.find((track) => track.lang === lang || langs.where('1', track.lang)?.[2] === lang);
+ const audioTrack = findTrackByLang(video.state.audioTracks, settings.audioLanguage);
+
+ if (audioTrack && audioTrack.id) {
+ onAudioTrackSelected(audioTrack.id);
+ defaultAudioTrackSelected.current = true;
+ }
+ }
+ }, [video.state.audioTracks]);
+
+ React.useEffect(() => {
+ defaultSubtitlesSelected.current = false;
+ defaultAudioTrackSelected.current = false;
+ nextVideoPopupDismissed.current = false;
+ }, [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) &&
+ (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0)) {
+ closeSubtitlesMenu();
+ }
+ }, [video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
+
+ React.useEffect(() => {
+ if (player.metaItem === null || player.metaItem.type !== 'Ready') {
+ closeInfoMenu();
+ closeVideosMenu();
+ }
+ }, [player.metaItem]);
+
+ React.useEffect(() => {
+ if (video.state.playbackSpeed === null) {
+ closeSpeedMenu();
+ }
+ }, [video.state.playbackSpeed]);
+
+ React.useEffect(() => {
+ const toastFilter = (item) => item?.dataset?.type === 'CoreEvent';
+ toast.addFilter(toastFilter);
+ const onCastStateChange = () => {
+ setCasting(chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED);
+ };
+ const onChromecastServiceStateChange = () => {
+ onCastStateChange();
+ if (chromecast.active) {
+ chromecast.transport.on(
+ cast.framework.CastContextEventType.CAST_STATE_CHANGED,
+ onCastStateChange
+ );
+ }
+ };
+ const onCoreEvent = ({ event }) => {
+ if (event === 'PlayingOnDevice') {
+ onPauseRequested();
+ }
+ };
+ chromecast.on('stateChanged', onChromecastServiceStateChange);
+ core.transport.on('CoreEvent', onCoreEvent);
+ onChromecastServiceStateChange();
+ return () => {
+ toast.removeFilter(toastFilter);
+ chromecast.off('stateChanged', onChromecastServiceStateChange);
+ core.transport.off('CoreEvent', onCoreEvent);
+ if (chromecast.active) {
+ chromecast.transport.off(
+ cast.framework.CastContextEventType.CAST_STATE_CHANGED,
+ onCastStateChange
+ );
+ }
+ };
+ }, []);
+
+ React.useLayoutEffect(() => {
+ const onKeyDown = (event) => {
+ switch (event.code) {
+ case 'Space': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
+ if (video.state.paused) {
+ onPlayRequested();
+ } else {
+ onPauseRequested();
+ }
+ }
+
+ break;
+ }
+ case 'ArrowRight': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
+ onSeekRequested(video.state.time + seekDuration);
+ }
+
+ break;
+ }
+ case 'ArrowLeft': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
+ const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration;
+ onSeekRequested(video.state.time - seekDuration);
+ }
+
+ break;
+ }
+ case 'ArrowUp': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume + 5);
+ }
+
+ break;
+ }
+ case 'ArrowDown': {
+ if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume - 5);
+ }
+
+ break;
+ }
+ case 'KeyS': {
+ closeMenus();
+ if ((Array.isArray(video.state.subtitlesTracks) && video.state.subtitlesTracks.length > 0) ||
+ (Array.isArray(video.state.extraSubtitlesTracks) && video.state.extraSubtitlesTracks.length > 0) ||
+ (Array.isArray(video.state.audioTracks) && video.state.audioTracks.length > 0)) {
+ toggleSubtitlesMenu();
+ }
+
+ break;
+ }
+ case 'KeyC': {
+ if (!menusOpen && !nextVideoPopupOpen) {
+ if (video.state.selectedSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(null);
+ } else if (previousSubtitlesTrackId !== null) {
+ onSubtitlesTrackSelected(previousSubtitlesTrackId);
+ } else if (video.state.subtitlesTracks.length > 0) {
+ onSubtitlesTrackSelected(video.state.subtitlesTracks[0].id);
+ } else if (video.state.extraSubtitlesTracks.length > 0) {
+ onExtraSubtitlesTrackSelected(video.state.extraSubtitlesTracks[0].id);
+ }
+ }
+ break;
+ }
+ case 'KeyI': {
+ closeMenus();
+ if (player.metaItem !== null && player.metaItem.type === 'Ready') {
+ toggleInfoMenu();
+ }
+
+ break;
+ }
+ case 'KeyR': {
+ closeMenus();
+ if (video.state.playbackSpeed !== null) {
+ toggleSpeedMenu();
+ }
+
+ break;
+ }
+ case 'KeyV': {
+ closeMenus();
+ if (player.metaItem !== null && player.metaItem.type === 'Ready' && player.metaItem?.content?.videos?.length > 0) {
+ toggleVideosMenu();
+ }
+
+ break;
+ }
+ case 'KeyD': {
+ closeMenus();
+ if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') {
+ toggleStatisticsMenu();
+ }
+
+ break;
+ }
+ case 'Escape': {
+ closeMenus();
+ break;
+ }
+ }
+ };
+ const onWheel = ({ deltaY }) => {
+ if (deltaY > 0) {
+ if (!menusOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume - 5);
+ }
+ } else {
+ if (!menusOpen && video.state.volume !== null) {
+ onVolumeChangeRequested(video.state.volume + 5);
+ }
+ }
+ };
+ if (routeFocused) {
+ window.addEventListener('keydown', onKeyDown);
+ window.addEventListener('wheel', onWheel);
+ }
+ return () => {
+ window.removeEventListener('keydown', onKeyDown);
+ window.removeEventListener('wheel', onWheel);
+ };
+ }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, menusOpen, nextVideoPopupOpen, video.state.paused, video.state.time, video.state.volume, video.state.audioTracks, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, video.state.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]);
+
+ 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('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('implementationChanged', onImplementationChanged);
+ };
+ }, []);
+
+ React.useLayoutEffect(() => {
+ return () => {
+ setImmersedDebounced.cancel();
+ onPlayRequestedDebounced.cancel();
+ onPauseRequestedDebounced.cancel();
+ };
+ }, []);
+
+ return (
+
+
+ {
+ !video.state.loaded ?
+
+

+
+ :
+ null
+ }
+ {
+ (video.state.buffering || !video.state.loaded) && !error ?
+
+ :
+ null
+ }
+ {
+ error !== null ?
+
+ :
+ null
+ }
+ {
+ menusOpen ?
+
+ :
+ null
+ }
+ {
+ video.state.volume !== null && overlayHidden ?
+
+ :
+ null
+ }
+
+
+ {
+ nextVideoPopupOpen ?
+
+ :
+ null
+ }
+ {
+ statisticsMenuOpen ?
+
+ :
+ null
+ }
+ {
+ subtitlesMenuOpen ?
+
+ :
+ null
+ }
+ {
+ infoMenuOpen ?
+
+ :
+ null
+ }
+ {
+ speedMenuOpen ?
+
+ :
+ null
+ }
+ {
+ videosMenuOpen ?
+
+ :
+ null
+ }
+ {
+ optionsMenuOpen ?
+
+ :
+ null
+ }
- ) : null}
- {(video.state.buffering || !video.state.loaded) && !error ? (
-
- ) : null}
- {error !== null ? (
-
- ) : null}
- {menusOpen ?
: null}
- {video.state.volume !== null && overlayHidden ? (
-
- ) : null}
-
-
- {nextVideoPopupOpen ? (
-
- ) : null}
- {statisticsMenuOpen ? (
-
- ) : null}
- {subtitlesMenuOpen ? (
-
- ) : null}
- {infoMenuOpen ? (
-
- ) : null}
- {speedMenuOpen ? (
-
- ) : null}
- {videosMenuOpen ? (
-
- ) : null}
- {optionsMenuOpen ? (
-
- ) : null}
-
- );
+ );
};
Player.propTypes = {
- urlParams: PropTypes.shape({
- stream: PropTypes.string,
- streamTransportUrl: PropTypes.string,
- metaTransportUrl: PropTypes.string,
- type: PropTypes.string,
- id: PropTypes.string,
- videoId: PropTypes.string,
- }),
- queryParams: PropTypes.instanceOf(URLSearchParams),
+ urlParams: PropTypes.shape({
+ stream: PropTypes.string,
+ streamTransportUrl: PropTypes.string,
+ metaTransportUrl: PropTypes.string,
+ type: PropTypes.string,
+ id: PropTypes.string,
+ videoId: PropTypes.string
+ }),
+ queryParams: PropTypes.instanceOf(URLSearchParams)
};
const PlayerFallback = () => (
-
+
);
module.exports = withCoreSuspender(Player, PlayerFallback);
From 5eb55d3aaf8df7180f0f7b1dc9c7c7678588a39a Mon Sep 17 00:00:00 2001
From: Neeraj TK <64883030+GaryGosh@users.noreply.github.com>
Date: Fri, 14 Nov 2025 17:45:45 +0530
Subject: [PATCH 04/25] Added the toggle subtitles shortcut
---
src/common/Shortcuts/shortcuts.json | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/common/Shortcuts/shortcuts.json b/src/common/Shortcuts/shortcuts.json
index 766288fb0..4f2d17db2 100644
--- a/src/common/Shortcuts/shortcuts.json
+++ b/src/common/Shortcuts/shortcuts.json
@@ -69,6 +69,11 @@
"label": "SETTINGS_SHORTCUT_SUBTITLES_DELAY",
"combos": [["G"], ["H"]]
},
+ {
+ "name": "toggleSubtitles",
+ "label": "SETTINGS_SHORTCUT_TOGGLE_SUBTITLES",
+ "combos": [["C"]]
+ },
{
"name": "subtitlesMenu",
"label": "SETTINGS_SHORTCUT_MENU_SUBTITLES",
From 17ee0e95e487ef730625760b1d7f6cc4fdace1c9 Mon Sep 17 00:00:00 2001
From: Tim
Date: Thu, 22 Jan 2026 11:21:09 +0100
Subject: [PATCH 05/25] fix(Player): side drawer layout on large screens
---
src/routes/Player/SideDrawer/SideDrawer.less | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less
index 0fe22f58a..6623bc283 100644
--- a/src/routes/Player/SideDrawer/SideDrawer.less
+++ b/src/routes/Player/SideDrawer/SideDrawer.less
@@ -3,6 +3,7 @@
@import (reference) '~stremio/common/screen-sizes.less';
:import('~stremio/components/MetaPreview/styles.less') {
+ description-container: description-container;
action-buttons-container: action-buttons-container;
}
@@ -57,9 +58,14 @@
.info {
padding: @padding;
overflow-y: auto;
- flex: 1;
.side-drawer-meta-preview {
+ .description-container {
+ display: -webkit-box;
+ -webkit-line-clamp: 4;
+ -webkit-box-orient: vertical;
+ }
+
.action-buttons-container {
padding-top: 0;
margin-top: 0;
@@ -91,10 +97,6 @@
@media @phone-landscape {
.side-drawer {
max-width: 50dvw;
-
- .info {
- flex: 1;
- }
}
}
From 67358359bf20c25beac6d9edea6a7891193185ae Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 14:38:59 +0200
Subject: [PATCH 06/25] refactor: remember the sub track
handle both external and embedded
---
src/routes/Player/Player.js | 47 +++++++++++++++++++++++++++++++------
1 file changed, 40 insertions(+), 7 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index 11a71aecf..fe6f9abde 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -90,10 +90,9 @@ const Player = ({ urlParams, queryParams }) => {
const nextVideoPopupDismissed = React.useRef(false);
const defaultSubtitlesSelected = React.useRef(false);
+ const lastSelectedSubtitleTrack = React.useRef(null);
const defaultAudioTrackSelected = React.useRef(false);
const [error, setError] = React.useState(null);
- const [previousSubtitlesTrackId, setPreviousSubtitlesTrackId] = React.useState(null);
-
const isNavigating = React.useRef(false);
@@ -256,11 +255,6 @@ const Player = ({ urlParams, queryParams }) => {
streamStateChanged({ subtitleDelay: delay });
}, [streamStateChanged]);
- const onSubtitlesTrackSelected = React.useCallback((id) => {
- video.setProp('selectedSubtitlesTrackId', id);
- video.setProp('selectedExtraSubtitlesTrackId', null);
- setPreviousSubtitlesTrackId(id);
- }, []);
const onIncreaseSubtitlesDelay = React.useCallback(() => {
const delay = video.state.extraSubtitlesDelay + 250;
onExtraSubtitlesDelayChanged(delay);
@@ -509,6 +503,14 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [video.state.stream, player.streamState]);
+ React.useEffect(() => {
+ if (video.state.selectedSubtitlesTrackId !== null) {
+ lastSelectedSubtitleTrack.current = { id: video.state.selectedSubtitlesTrackId, embedded: true };
+ } else if (video.state.selectedExtraSubtitlesTrackId !== null) {
+ lastSelectedSubtitleTrack.current = { id: video.state.selectedExtraSubtitlesTrackId, embedded: false };
+ }
+ }, [video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId]);
+
React.useEffect(() => {
defaultSubtitlesSelected.current = false;
defaultAudioTrackSelected.current = false;
@@ -677,6 +679,37 @@ const Player = ({ urlParams, queryParams }) => {
combo === 1 ? onUpdateSubtitlesSize(-1) : onUpdateSubtitlesSize(1);
}, [onUpdateSubtitlesSize, onUpdateSubtitlesSize]);
+ onShortcut('toggleSubtitles', () => {
+ const hasEmbedded = video.state.selectedSubtitlesTrackId !== null;
+ const hasExtra = video.state.selectedExtraSubtitlesTrackId !== null;
+ const last = lastSelectedSubtitleTrack.current;
+
+ if (hasEmbedded || hasExtra) {
+ video.setSubtitlesTrack(null);
+ video.setExtraSubtitlesTrack(null);
+ return;
+ }
+
+ if (last) {
+ const tracks = last.embedded ? video.state.subtitlesTracks : video.state.extraSubtitlesTracks;
+ if (tracks?.some((t) => t.id === last.id)) {
+ last.embedded ? onSubtitlesTrackSelected(last.id) : onExtraSubtitlesTrackSelected(last.id);
+ return;
+ }
+ }
+
+ const embeddedMatch = findTrackByLang(video.state.subtitlesTracks || [], settings.subtitlesLanguage);
+ if (embeddedMatch) {
+ onSubtitlesTrackSelected(embeddedMatch.id);
+ return;
+ }
+
+ const extraMatch = findTrackByLang(video.state.extraSubtitlesTracks || [], settings.subtitlesLanguage);
+ if (extraMatch) {
+ onExtraSubtitlesTrackSelected(extraMatch.id);
+ }
+ }, [video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, settings.subtitlesLanguage, onSubtitlesTrackSelected, onExtraSubtitlesTrackSelected]);
+
onShortcut('subtitlesMenu', () => {
closeMenus();
if (video.state?.subtitlesTracks?.length > 0 || video.state?.extraSubtitlesTracks?.length > 0) {
From ea5e302af72be2167ba735af5976b862f20188f8 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 14:48:52 +0200
Subject: [PATCH 07/25] refactor: simplfy subs handling
---
src/routes/Player/Player.js | 46 +++++++++++--------------------------
1 file changed, 14 insertions(+), 32 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index fe6f9abde..db34cbe3a 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -90,7 +90,7 @@ const Player = ({ urlParams, queryParams }) => {
const nextVideoPopupDismissed = React.useRef(false);
const defaultSubtitlesSelected = React.useRef(false);
- const lastSelectedSubtitleTrack = React.useRef(null);
+ const subtitlesEnabled = React.useRef(true);
const defaultAudioTrackSelected = React.useRef(false);
const [error, setError] = React.useState(null);
@@ -503,14 +503,6 @@ const Player = ({ urlParams, queryParams }) => {
}
}, [video.state.stream, player.streamState]);
- React.useEffect(() => {
- if (video.state.selectedSubtitlesTrackId !== null) {
- lastSelectedSubtitleTrack.current = { id: video.state.selectedSubtitlesTrackId, embedded: true };
- } else if (video.state.selectedExtraSubtitlesTrackId !== null) {
- lastSelectedSubtitleTrack.current = { id: video.state.selectedExtraSubtitlesTrackId, embedded: false };
- }
- }, [video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId]);
-
React.useEffect(() => {
defaultSubtitlesSelected.current = false;
defaultAudioTrackSelected.current = false;
@@ -680,35 +672,25 @@ const Player = ({ urlParams, queryParams }) => {
}, [onUpdateSubtitlesSize, onUpdateSubtitlesSize]);
onShortcut('toggleSubtitles', () => {
- const hasEmbedded = video.state.selectedSubtitlesTrackId !== null;
- const hasExtra = video.state.selectedExtraSubtitlesTrackId !== null;
- const last = lastSelectedSubtitleTrack.current;
+ const savedTrack = player.streamState?.subtitleTrack;
- if (hasEmbedded || hasExtra) {
+ if (subtitlesEnabled.current) {
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
- return;
- }
-
- if (last) {
- const tracks = last.embedded ? video.state.subtitlesTracks : video.state.extraSubtitlesTracks;
- if (tracks?.some((t) => t.id === last.id)) {
- last.embedded ? onSubtitlesTrackSelected(last.id) : onExtraSubtitlesTrackSelected(last.id);
- return;
+ } else if (savedTrack?.id) {
+ savedTrack.embedded ? onSubtitlesTrackSelected(savedTrack.id) : onExtraSubtitlesTrackSelected(savedTrack.id);
+ } else {
+ const embeddedMatch = findTrackByLang(video.state.subtitlesTracks || [], settings.subtitlesLanguage);
+ const extraMatch = findTrackByLang(video.state.extraSubtitlesTracks || [], settings.subtitlesLanguage);
+ if (embeddedMatch) {
+ onSubtitlesTrackSelected(embeddedMatch.id);
+ } else if (extraMatch) {
+ onExtraSubtitlesTrackSelected(extraMatch.id);
}
}
- const embeddedMatch = findTrackByLang(video.state.subtitlesTracks || [], settings.subtitlesLanguage);
- if (embeddedMatch) {
- onSubtitlesTrackSelected(embeddedMatch.id);
- return;
- }
-
- const extraMatch = findTrackByLang(video.state.extraSubtitlesTracks || [], settings.subtitlesLanguage);
- if (extraMatch) {
- onExtraSubtitlesTrackSelected(extraMatch.id);
- }
- }, [video.state.selectedSubtitlesTrackId, video.state.selectedExtraSubtitlesTrackId, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, settings.subtitlesLanguage, onSubtitlesTrackSelected, onExtraSubtitlesTrackSelected]);
+ subtitlesEnabled.current = !subtitlesEnabled.current;
+ }, [player.streamState, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, settings.subtitlesLanguage, onSubtitlesTrackSelected, onExtraSubtitlesTrackSelected]);
onShortcut('subtitlesMenu', () => {
closeMenus();
From 891147321039b52c6398e68d64a550380eedb22f Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 14:54:33 +0200
Subject: [PATCH 08/25] refactor: remove the block which is handled by useff
---
src/routes/Player/Player.js | 10 +---------
1 file changed, 1 insertion(+), 9 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index db34cbe3a..adeebd206 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -679,18 +679,10 @@ const Player = ({ urlParams, queryParams }) => {
video.setExtraSubtitlesTrack(null);
} else if (savedTrack?.id) {
savedTrack.embedded ? onSubtitlesTrackSelected(savedTrack.id) : onExtraSubtitlesTrackSelected(savedTrack.id);
- } else {
- const embeddedMatch = findTrackByLang(video.state.subtitlesTracks || [], settings.subtitlesLanguage);
- const extraMatch = findTrackByLang(video.state.extraSubtitlesTracks || [], settings.subtitlesLanguage);
- if (embeddedMatch) {
- onSubtitlesTrackSelected(embeddedMatch.id);
- } else if (extraMatch) {
- onExtraSubtitlesTrackSelected(extraMatch.id);
- }
}
subtitlesEnabled.current = !subtitlesEnabled.current;
- }, [player.streamState, video.state.subtitlesTracks, video.state.extraSubtitlesTracks, settings.subtitlesLanguage, onSubtitlesTrackSelected, onExtraSubtitlesTrackSelected]);
+ }, [player.streamState]);
onShortcut('subtitlesMenu', () => {
closeMenus();
From 1c441b9bc0213ea2eb1cc328afa96b2f0452b0b2 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 15:13:36 +0200
Subject: [PATCH 09/25] Update Player.js
---
src/routes/Player/Player.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index adeebd206..cc9985ac4 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -678,7 +678,8 @@ const Player = ({ urlParams, queryParams }) => {
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
} else if (savedTrack?.id) {
- savedTrack.embedded ? onSubtitlesTrackSelected(savedTrack.id) : onExtraSubtitlesTrackSelected(savedTrack.id);
+ video.setSubtitlesTrack(savedTrack?.id);
+ video.setExtraSubtitlesTrack(savedTrack?.id);
}
subtitlesEnabled.current = !subtitlesEnabled.current;
From d456adff0ea5d676b475a0f242fa72ba14d35e47 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 15:20:09 +0200
Subject: [PATCH 10/25] refactor: correctly set tracks
---
src/routes/Player/Player.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js
index cc9985ac4..54ad14bc8 100644
--- a/src/routes/Player/Player.js
+++ b/src/routes/Player/Player.js
@@ -678,8 +678,7 @@ const Player = ({ urlParams, queryParams }) => {
video.setSubtitlesTrack(null);
video.setExtraSubtitlesTrack(null);
} else if (savedTrack?.id) {
- video.setSubtitlesTrack(savedTrack?.id);
- video.setExtraSubtitlesTrack(savedTrack?.id);
+ savedTrack.embedded ? video.setSubtitlesTrack(savedTrack.id) : video.setExtraSubtitlesTrack(savedTrack.id);
}
subtitlesEnabled.current = !subtitlesEnabled.current;
From 3cad4910401ba5d4d8e17901f37f356dbc8fe551 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 15:20:21 +0200
Subject: [PATCH 11/25] chore: update translations
---
package.json | 2 +-
pnpm-lock.yaml | 10 +++++-----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/package.json b/package.json
index 0c9ca54ec..4f283d0ac 100644
--- a/package.json
+++ b/package.json
@@ -41,7 +41,7 @@
"react-i18next": "^15.1.3",
"react-is": "18.3.1",
"spatial-navigation-polyfill": "github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
- "stremio-translations": "github:Stremio/stremio-translations#7c0c337f32163aa13158bb90cd6133da43feafef",
+ "stremio-translations": "github:Stremio/stremio-translations#1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe",
"url": "0.11.4",
"use-long-press": "^3.2.0"
},
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 435a809e8..a55a4f3bd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -90,8 +90,8 @@ importers:
specifier: github:Stremio/spatial-navigation#64871b1422466f5f45d24ebc8bbd315b2ebab6a6
version: https://codeload.github.com/Stremio/spatial-navigation/tar.gz/64871b1422466f5f45d24ebc8bbd315b2ebab6a6
stremio-translations:
- specifier: github:Stremio/stremio-translations#7c0c337f32163aa13158bb90cd6133da43feafef
- version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef
+ specifier: github:Stremio/stremio-translations#1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe
+ version: https://codeload.github.com/Stremio/stremio-translations/tar.gz/1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe
url:
specifier: 0.11.4
version: 0.11.4
@@ -4133,8 +4133,8 @@ packages:
resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
engines: {node: '>= 0.4'}
- stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef:
- resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef}
+ stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe:
+ resolution: {tarball: https://codeload.github.com/Stremio/stremio-translations/tar.gz/1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe}
version: 1.45.0
string-length@4.0.2:
@@ -9378,7 +9378,7 @@ snapshots:
es-errors: 1.3.0
internal-slot: 1.1.0
- stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/7c0c337f32163aa13158bb90cd6133da43feafef: {}
+ stremio-translations@https://codeload.github.com/Stremio/stremio-translations/tar.gz/1bfcb6d2a4f37bb647959ba0bbbd1ade1415c2fe: {}
string-length@4.0.2:
dependencies:
From dbed391a86f1eb60c8450a678219311f2cb93e2d Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 16:44:08 +0200
Subject: [PATCH 12/25] chore: bump v5.0.0-beta.30
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 4f283d0ac..a9a78e7bc 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "stremio",
"displayName": "Stremio",
- "version": "5.0.0-beta.29",
+ "version": "5.0.0-beta.30",
"author": "Smart Code OOD",
"private": true,
"license": "gpl-2.0",
From 1c9813ebc96326d19e7dceafecc74cd43521add8 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Thu, 22 Jan 2026 21:04:57 +0200
Subject: [PATCH 13/25] fix(Image): update return renderFallback type
---
src/components/Image/Image.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/Image/Image.tsx b/src/components/Image/Image.tsx
index e64078fbc..5c7a93c00 100644
--- a/src/components/Image/Image.tsx
+++ b/src/components/Image/Image.tsx
@@ -7,7 +7,7 @@ type Props = {
src: string,
alt: string,
fallbackSrc: string,
- renderFallback: () => void,
+ renderFallback: () => React.ReactNode,
onError: (event: React.SyntheticEvent) => void,
};
From e29adde4bd80f365f2baef6ac1a807ff176f4f62 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Tue, 3 Feb 2026 16:44:25 +0800
Subject: [PATCH 14/25] fix: use translate prefix correction
---
src/common/useTranslate.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/common/useTranslate.js b/src/common/useTranslate.js
index 7214a4a1e..cf249b572 100644
--- a/src/common/useTranslate.js
+++ b/src/common/useTranslate.js
@@ -9,7 +9,7 @@ const useTranslate = () => {
const string = useCallback((key) => t(key), [t]);
const stringWithPrefix = useCallback((value, prefix, fallback = null) => {
- const key = `${prefix}${value}`;
+ const key = `${prefix}_${value}`;
const defaultValue = fallback ?? value.charAt(0).toUpperCase() + value.slice(1);
return t(key, {
From 0a1746dfe27fdb7d86d8dff55a606e12ffe0229f Mon Sep 17 00:00:00 2001
From: Botzy
Date: Tue, 3 Feb 2026 11:13:31 +0200
Subject: [PATCH 15/25] fix: links prefix and revert change to useTranslate
---
src/common/useTranslate.js | 2 +-
src/components/MetaPreview/MetaLinks/MetaLinks.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/common/useTranslate.js b/src/common/useTranslate.js
index cf249b572..7214a4a1e 100644
--- a/src/common/useTranslate.js
+++ b/src/common/useTranslate.js
@@ -9,7 +9,7 @@ const useTranslate = () => {
const string = useCallback((key) => t(key), [t]);
const stringWithPrefix = useCallback((value, prefix, fallback = null) => {
- const key = `${prefix}_${value}`;
+ const key = `${prefix}${value}`;
const defaultValue = fallback ?? value.charAt(0).toUpperCase() + value.slice(1);
return t(key, {
diff --git a/src/components/MetaPreview/MetaLinks/MetaLinks.js b/src/components/MetaPreview/MetaLinks/MetaLinks.js
index 3be090911..6e58a5d1e 100644
--- a/src/components/MetaPreview/MetaLinks/MetaLinks.js
+++ b/src/components/MetaPreview/MetaLinks/MetaLinks.js
@@ -14,7 +14,7 @@ const MetaLinks = ({ className, label, links }) => {
{
typeof label === 'string' && label.length > 0 ?
- { stringWithPrefix(label.toUpperCase(), 'LINKS') }
+ { stringWithPrefix(label.toUpperCase(), 'LINKS_') }
:
null
From fa135977487841c3f6085513b3b7533963227c97 Mon Sep 17 00:00:00 2001
From: Fawazorg
Date: Sat, 7 Feb 2026 20:29:13 +0300
Subject: [PATCH 16/25] fix: replace hardcoded strings with translation keys
---
src/routes/Intro/Intro.js | 2 +-
src/routes/Intro/PasswordResetModal/PasswordResetModal.js | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/routes/Intro/Intro.js b/src/routes/Intro/Intro.js
index 5a2f80aaa..1f041c004 100644
--- a/src/routes/Intro/Intro.js
+++ b/src/routes/Intro/Intro.js
@@ -183,7 +183,7 @@ const Intro = ({ queryParams }) => {
return;
}
if (!state.privacyPolicyAccepted) {
- dispatch({ type: 'error', error: 'You must accept the Privacy Policy' });
+ dispatch({ type: 'error', error: t('MUST_ACCEPT_PRIVACY_POLICY') });
return;
}
openLoaderModal();
diff --git a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
index 8c69c1489..e5c27a6b5 100644
--- a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
+++ b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
@@ -19,7 +19,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
emailRef.current.value.length > 0 && emailRef.current.validity.valid ?
platform.openExternal('https://www.strem.io/reset-password/' + emailRef.current.value, '_blank')
:
- setError('Invalid email');
+ setError(t('INVALID_EMAIL'));
}, []);
const passwordResetModalButtons = React.useMemo(() => {
return [
@@ -31,7 +31,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
}
},
{
- label: t('SEND'),
+ label: t('BUTTON_SEND'),
props: {
onClick: goToPasswordReset
}
@@ -52,7 +52,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
ref={emailRef}
className={styles['credentials-text-input']}
type={'email'}
- placeholder={'Email'}
+ placeholder={t('WEBSITE_PLACEHOLDER_EMAIL')}
defaultValue={typeof email === 'string' ? email : ''}
onChange={emailOnChange}
onSubmit={goToPasswordReset}
From 93ed428e8b1eb9bc26703fc1b23f851383134739 Mon Sep 17 00:00:00 2001
From: Fawazorg
Date: Sat, 7 Feb 2026 20:54:57 +0300
Subject: [PATCH 17/25] Fix Email key
---
src/routes/Intro/PasswordResetModal/PasswordResetModal.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
index e5c27a6b5..497568cc9 100644
--- a/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
+++ b/src/routes/Intro/PasswordResetModal/PasswordResetModal.js
@@ -52,7 +52,7 @@ const PasswordResetModal = ({ email, onCloseRequest }) => {
ref={emailRef}
className={styles['credentials-text-input']}
type={'email'}
- placeholder={t('WEBSITE_PLACEHOLDER_EMAIL')}
+ placeholder={t('EMAIL')}
defaultValue={typeof email === 'string' ? email : ''}
onChange={emailOnChange}
onSubmit={goToPasswordReset}
From ec6db02829dbd099b99dc90f4bfd97c2ae73da6e Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Mon, 23 Feb 2026 13:07:04 +0200
Subject: [PATCH 18/25] fix: host whitelist logic
---
src/common/Platform/Platform.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/common/Platform/Platform.tsx b/src/common/Platform/Platform.tsx
index 0da1881ef..86ca999c6 100644
--- a/src/common/Platform/Platform.tsx
+++ b/src/common/Platform/Platform.tsx
@@ -18,7 +18,9 @@ const PlatformProvider = ({ children }: Props) => {
const openExternal = (url: string) => {
try {
const { hostname } = new URL(url);
- const isWhitelisted = WHITELISTED_HOSTS.some((host: string) => hostname.endsWith(host));
+ const isWhitelisted = WHITELISTED_HOSTS.some((host: string) =>
+ hostname === host || hostname.endsWith('.' + host)
+ );
const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url;
window.open(finalUrl, '_blank');
From 3098f6417f75b4f6c270624b5b86f2813ce7248a Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Mon, 23 Feb 2026 14:50:17 +0200
Subject: [PATCH 19/25] fix: ios PWA status bar style
---
src/index.html | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/index.html b/src/index.html
index ff8166004..def81fd71 100644
--- a/src/index.html
+++ b/src/index.html
@@ -5,6 +5,7 @@
+
From df0d24a7d96c9e5eb8e930177ded968309528681 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Mon, 23 Feb 2026 15:08:24 +0200
Subject: [PATCH 20/25] fix: PWA safe area inset bg color
---
src/App/styles.less | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/App/styles.less b/src/App/styles.less
index e6fd7d747..7f8f97d5c 100644
--- a/src/App/styles.less
+++ b/src/App/styles.less
@@ -156,6 +156,7 @@ html {
overscroll-behavior: none;
user-select: none;
touch-action: manipulation;
+ background-color: var(--primary-background-color);
-webkit-tap-highlight-color: transparent;
@media (display-mode: standalone) {
From 8623627f4de30250d2ac22f71007025f11e71212 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Mon, 23 Feb 2026 15:23:37 +0200
Subject: [PATCH 21/25] refactor: use both vars for html background
---
src/App/styles.less | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/App/styles.less b/src/App/styles.less
index 7f8f97d5c..116b9b0be 100644
--- a/src/App/styles.less
+++ b/src/App/styles.less
@@ -156,7 +156,8 @@ html {
overscroll-behavior: none;
user-select: none;
touch-action: manipulation;
- background-color: var(--primary-background-color);
+ background-color: var(--secondary-background-color);
+ background: linear-gradient(41deg, var(--primary-background-color) 0%, var(--secondary-background-color) 100%);
-webkit-tap-highlight-color: transparent;
@media (display-mode: standalone) {
From c95f314a503cc4ad764416a6d9e4c85431c32a83 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Mon, 23 Feb 2026 15:35:55 +0200
Subject: [PATCH 22/25] fix: sideDrawer safe areas
---
src/App/styles.less | 1 -
src/routes/Player/SideDrawer/SideDrawer.less | 2 ++
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/App/styles.less b/src/App/styles.less
index 116b9b0be..5c50bd6bd 100644
--- a/src/App/styles.less
+++ b/src/App/styles.less
@@ -157,7 +157,6 @@ html {
user-select: none;
touch-action: manipulation;
background-color: var(--secondary-background-color);
- background: linear-gradient(41deg, var(--primary-background-color) 0%, var(--secondary-background-color) 100%);
-webkit-tap-highlight-color: transparent;
@media (display-mode: standalone) {
diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less
index 6623bc283..bacc3ca09 100644
--- a/src/routes/Player/SideDrawer/SideDrawer.less
+++ b/src/routes/Player/SideDrawer/SideDrawer.less
@@ -87,9 +87,11 @@
@media @phone-portrait {
.side-drawer {
max-width: 100dvw;
+ padding-top: calc(@padding + var(--safe-area-inset-top));
.close-button {
display: block;
+ top: calc(1.3rem + var(--safe-area-inset-top));
}
}
}
From fab318e647d1a449b982979c9af5d56a11c94201 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Mon, 23 Feb 2026 15:44:01 +0200
Subject: [PATCH 23/25] lower the bottom safe inset revert to primary bg
---
src/App/styles.less | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/App/styles.less b/src/App/styles.less
index 5c50bd6bd..5866f8af2 100644
--- a/src/App/styles.less
+++ b/src/App/styles.less
@@ -102,7 +102,7 @@
}
@media (display-mode: standalone) {
- --safe-area-inset-bottom: @calculated-bottom-safe-inset;
+ --safe-area-inset-bottom: 0rem;
}
}
@@ -156,7 +156,7 @@ html {
overscroll-behavior: none;
user-select: none;
touch-action: manipulation;
- background-color: var(--secondary-background-color);
+ background-color: var(--primary-background-color);
-webkit-tap-highlight-color: transparent;
@media (display-mode: standalone) {
From ac9ca71b5329b601d7e5c0c7b9ed8f8a27e2496e Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Mon, 23 Feb 2026 15:59:29 +0200
Subject: [PATCH 24/25] fix: ios pwa styles for the standalone display
---
src/App/styles.less | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/App/styles.less b/src/App/styles.less
index 5866f8af2..53ec4c1d5 100644
--- a/src/App/styles.less
+++ b/src/App/styles.less
@@ -23,8 +23,8 @@
// HTML sizes
@html-width: ~"calc(max(var(--small-viewport-width), var(--dynamic-viewport-width)))";
@html-height: ~"calc(max(var(--small-viewport-height), var(--dynamic-viewport-height)))";
-@html-standalone-width: ~"calc(max(100%, var(--small-viewport-width)))";
-@html-standalone-height: ~"calc(max(100%, var(--small-viewport-height)))";
+@html-standalone-width: ~"calc(max(100%, var(--large-viewport-width)))";
+@html-standalone-height: ~"calc(max(100%, var(--large-viewport-height)))";
// Safe area insets
@safe-area-inset-top: env(safe-area-inset-top, 0rem);
@@ -102,7 +102,7 @@
}
@media (display-mode: standalone) {
- --safe-area-inset-bottom: 0rem;
+ --safe-area-inset-bottom: @calculated-bottom-safe-inset;
}
}
From 3f5097f0a0ee7e99e28862af6bd7e85aeed7a301 Mon Sep 17 00:00:00 2001
From: "Timothy Z."
Date: Mon, 23 Feb 2026 16:07:41 +0200
Subject: [PATCH 25/25] fix(sidedrawer): always respect safe areas
---
src/routes/Player/SideDrawer/SideDrawer.less | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/routes/Player/SideDrawer/SideDrawer.less b/src/routes/Player/SideDrawer/SideDrawer.less
index bacc3ca09..e6831a71d 100644
--- a/src/routes/Player/SideDrawer/SideDrawer.less
+++ b/src/routes/Player/SideDrawer/SideDrawer.less
@@ -13,6 +13,7 @@
display: flex;
flex-direction: column;
padding: @padding;
+ padding-top: calc(@padding + var(--safe-area-inset-top));
height: 100dvh;
max-width: 35rem;
overflow-y: auto;
@@ -28,7 +29,7 @@
.close-button {
display: none;
position: absolute;
- top: 1.3rem;
+ top: calc(1.3rem + var(--safe-area-inset-top));
right: 1.3rem;
padding: 0.5rem;
background-color: transparent;
@@ -87,11 +88,9 @@
@media @phone-portrait {
.side-drawer {
max-width: 100dvw;
- padding-top: calc(@padding + var(--safe-area-inset-top));
.close-button {
display: block;
- top: calc(1.3rem + var(--safe-area-inset-top));
}
}
}