diff --git a/src/routes/Player/ControlBar/ControlBar.js b/src/routes/Player/ControlBar/ControlBar.js index f97a19cea..b0631a3f3 100644 --- a/src/routes/Player/ControlBar/ControlBar.js +++ b/src/routes/Player/ControlBar/ControlBar.js @@ -90,36 +90,6 @@ const ControlBar = ({ } } }, [muted, onMuteRequested, onUnmuteRequested]); - const onSubtitlesButtonClick = React.useCallback(() => { - if (typeof onToggleSubtitlesMenu === 'function') { - onToggleSubtitlesMenu(); - } - }, [onToggleSubtitlesMenu]); - const onInfoButtonClick = React.useCallback(() => { - if (typeof onToggleInfoMenu === 'function') { - onToggleInfoMenu(); - } - }, [onToggleInfoMenu]); - const onSpeedButtonClick = React.useCallback(() => { - if (typeof onToggleSpeedMenu === 'function') { - onToggleSpeedMenu(); - } - }, [onToggleSpeedMenu]); - const onVideosButtonClick = React.useCallback(() => { - if (typeof onToggleVideosMenu === 'function') { - onToggleVideosMenu(); - } - }, [onToggleVideosMenu]); - const onOptionsButtonClick = React.useCallback(() => { - if (typeof onToggleOptionsMenu === 'function') { - onToggleOptionsMenu(); - } - }, [onToggleOptionsMenu]); - const onStatisticsButtonClick = React.useCallback(() => { - if (typeof onToggleStatisticsMenu === 'function') { - onToggleStatisticsMenu(); - } - }, [onToggleStatisticsMenu]); const onChromecastButtonClick = React.useCallback(() => { chromecast.transport.requestSession(); }, []); @@ -175,30 +145,30 @@ const ControlBar = ({
- - - - { metaItem?.content?.videos?.length > 0 ? - : null } -
diff --git a/src/routes/Player/Error/Error.js b/src/routes/Player/Error/Error.js new file mode 100644 index 000000000..6bf638a52 --- /dev/null +++ b/src/routes/Player/Error/Error.js @@ -0,0 +1,56 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +const React = require('react'); +const { useTranslation } = require('react-i18next'); +const PropTypes = require('prop-types'); +const classNames = require('classnames'); +const { default: Icon } = require('@stremio/stremio-icons/react'); +const Button = require('stremio/common/Button'); +const styles = require('./styles'); + +const Error = ({ className, code, message, stream }) => { + const { t } = useTranslation(); + + const [playlist, fileName] = React.useMemo(() => { + return [ + stream?.deepLinks?.externalPlayer?.playlist, + stream?.deepLinks?.externalPlayer?.fileName, + ]; + }, [stream]); + + return ( +
+
{message}
+ { + code === 2 ? +
{t('EXTERNAL_PLAYER_HINT')}
+ : + null + } + { + playlist && fileName ? + + : + null + } +
+ ); +}; + +Error.propTypes = { + className: PropTypes.string, + code: PropTypes.number, + message: PropTypes.string, + stream: PropTypes.object, +}; + +module.exports = Error; diff --git a/src/routes/Player/Error/index.js b/src/routes/Player/Error/index.js new file mode 100644 index 000000000..92e90c19d --- /dev/null +++ b/src/routes/Player/Error/index.js @@ -0,0 +1,5 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +const Error = require('./Error'); + +module.exports = Error; diff --git a/src/routes/Player/Error/styles.less b/src/routes/Player/Error/styles.less new file mode 100644 index 000000000..77ff064db --- /dev/null +++ b/src/routes/Player/Error/styles.less @@ -0,0 +1,64 @@ +// Copyright (C) 2017-2023 Smart code 203358507 + +@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less'; + +.error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + background-color: rgba(0, 0, 0, 1); + + .error-label { + flex: 0 1 auto; + padding: 0 8rem; + max-height: 4.8em; + font-size: 2rem; + color: var(--primary-foreground-color); + text-align: center; + } + + .error-sub { + flex: 0 1 auto; + padding: 0 2rem; + max-height: 4.8em; + font-size: 1.3rem; + margin-top: 0.8rem; + color: var(--primary-foreground-color); + text-align: center; + } + + .playlist-button { + flex: none; + display: flex; + flex-direction: row; + align-items: center; + height: 3.5rem; + border-radius: 3.5rem; + margin-top: 1.5rem; + padding: 0 2rem; + background-color: var(--secondary-accent-color); + + &:hover { + outline: var(--focus-outline-size) solid var(--secondary-accent-color); + background-color: transparent; + } + + .icon { + flex: none; + width: 1.5rem; + height: 1.5rem; + margin-right: 1rem; + color: var(--primary-foreground-color); + } + + .label { + flex: 1; + max-height: 2.4em; + font-size: 1.1rem; + font-weight: 500; + color: var(--primary-foreground-color); + text-align: center; + } + } +} \ No newline at end of file diff --git a/src/routes/Player/Player.js b/src/routes/Player/Player.js index 9a40b4cd7..9887156d2 100644 --- a/src/routes/Player/Player.js +++ b/src/routes/Player/Player.js @@ -8,9 +8,9 @@ const langs = require('langs'); const { useTranslation } = require('react-i18next'); const { useRouteFocused } = require('stremio-router'); const { useServices } = require('stremio/services'); -const { HorizontalNavBar, Button, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common'); -const { default: Icon } = require('@stremio/stremio-icons/react'); +const { HorizontalNavBar, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common'); const BufferingLoader = require('./BufferingLoader'); +const Error = require('./Error'); const ControlBar = require('./ControlBar'); const NextVideoPopup = require('./NextVideoPopup'); const StatisticsMenu = require('./StatisticsMenu'); @@ -19,10 +19,12 @@ const OptionsMenu = require('./OptionsMenu'); const VideosMenu = require('./VideosMenu'); const SubtitlesMenu = require('./SubtitlesMenu'); const SpeedMenu = require('./SpeedMenu'); -const Video = require('./Video'); 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(); @@ -30,86 +32,66 @@ const Player = ({ urlParams, queryParams }) => { 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 [, , , toggleFullscreen] = useFullscreen(); + 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 [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = 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 [videoState, setVideoState] = React.useReducer( - (videoState, nextVideoState) => ({ ...videoState, ...nextVideoState }), - { - manifest: null, - stream: null, - paused: null, - time: null, - duration: null, - buffering: null, - buffered: null, - volume: null, - muted: null, - playbackSpeed: null, - videoParams: null, - audioTracks: [], - selectedAudioTrackId: null, - subtitlesTracks: [], - selectedSubtitlesTrackId: null, - subtitlesOffset: null, - subtitlesSize: null, - subtitlesTextColor: null, - subtitlesBackgroundColor: null, - subtitlesOutlineColor: null, - extraSubtitlesTracks: [], - selectedExtraSubtitlesTrackId: null, - extraSubtitlesSize: null, - extraSubtitlesDelay: null, - extraSubtitlesOffset: null, - extraSubtitlesTextColor: null, - extraSubtitlesBackgroundColor: null, - extraSubtitlesOutlineColor: null - } - ); - const videoRef = React.useRef(null); - const dispatch = React.useCallback((action, options) => { - if (videoRef.current !== null) { - videoRef.current.dispatch(action, options); - } - }, []); - const onImplementationChanged = React.useCallback((manifest) => { - setVideoState({ manifest }); - manifest.props.forEach((propName) => { - dispatch({ type: 'observeProp', propName }); - }); - dispatch({ type: 'setProp', propName: 'subtitlesSize', propValue: settings.subtitlesSize }); - dispatch({ type: 'setProp', propName: 'subtitlesOffset', propValue: settings.subtitlesOffset }); - dispatch({ type: 'setProp', propName: 'subtitlesTextColor', propValue: settings.subtitlesTextColor }); - dispatch({ type: 'setProp', propName: 'subtitlesBackgroundColor', propValue: settings.subtitlesBackgroundColor }); - dispatch({ type: 'setProp', propName: 'subtitlesOutlineColor', propValue: settings.subtitlesOutlineColor }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesSize', propValue: settings.subtitlesSize }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesOffset', propValue: settings.subtitlesOffset }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesTextColor', propValue: settings.subtitlesTextColor }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesBackgroundColor', propValue: settings.subtitlesBackgroundColor }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesOutlineColor', propValue: settings.subtitlesOutlineColor }); + + 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 onPropChanged = React.useCallback((propName, propValue) => { - setVideoState({ [propName]: propValue }); - }, []); + const onEnded = React.useCallback(() => { ended(); if (player.nextVideo !== null) { @@ -118,6 +100,7 @@ const Player = ({ urlParams, queryParams }) => { window.history.back(); } }, [player.nextVideo, onNextVideoRequested]); + const onError = React.useCallback((error) => { console.error('Player', error); if (error.critical) { @@ -131,6 +114,7 @@ const Player = ({ urlParams, queryParams }) => { }); } }, []); + const onSubtitlesTrackLoaded = React.useCallback(() => { toast.show({ type: 'success', @@ -139,6 +123,7 @@ const Player = ({ urlParams, queryParams }) => { timeout: 3000 }); }, []); + const onExtraSubtitlesTrackLoaded = React.useCallback((track) => { toast.show({ type: 'success', @@ -147,53 +132,69 @@ const Player = ({ urlParams, queryParams }) => { timeout: 3000 }); }, []); + const onPlayRequested = React.useCallback(() => { - dispatch({ type: 'setProp', propName: 'paused', propValue: false }); + video.setProp('paused', false); }, []); + const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []); + const onPauseRequested = React.useCallback(() => { - dispatch({ type: 'setProp', propName: 'paused', propValue: true }); + video.setProp('paused', true); }, []); + const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []); const onMuteRequested = React.useCallback(() => { - dispatch({ type: 'setProp', propName: 'muted', propValue: true }); + video.setProp('muted', true); }, []); + const onUnmuteRequested = React.useCallback(() => { - dispatch({ type: 'setProp', propName: 'muted', propValue: false }); + video.setProp('muted', false); }, []); + const onVolumeChangeRequested = React.useCallback((volume) => { - dispatch({ type: 'setProp', propName: 'volume', propValue: volume }); + video.setProp('volume', volume); }, []); + const onSeekRequested = React.useCallback((time) => { - dispatch({ type: 'setProp', propName: 'time', propValue: time }); + video.setProp('time', time); }, []); + const onPlaybackSpeedChanged = React.useCallback((rate) => { - dispatch({ type: 'setProp', propName: 'playbackSpeed', propValue: rate }); + video.setProp('playbackSpeed', rate); }, []); + const onSubtitlesTrackSelected = React.useCallback((id) => { - dispatch({ type: 'setProp', propName: 'selectedSubtitlesTrackId', propValue: id }); - dispatch({ type: 'setProp', propName: 'selectedExtraSubtitlesTrackId', propValue: null }); + video.setProp('selectedSubtitlesTrackId', id); + video.setProp('selectedExtraSubtitlesTrackId', null); }, []); + const onExtraSubtitlesTrackSelected = React.useCallback((id) => { - dispatch({ type: 'setProp', propName: 'selectedSubtitlesTrackId', propValue: null }); - dispatch({ type: 'setProp', propName: 'selectedExtraSubtitlesTrackId', propValue: id }); + video.setProp('selectedSubtitlesTrackId', null); + video.setProp('selectedExtraSubtitlesTrackId', id); }, []); + const onAudioTrackSelected = React.useCallback((id) => { - dispatch({ type: 'setProp', propName: 'selectedAudioTrackId', propValue: id }); + video.setProp('selectedAudioTrackId', id); }, []); + const onExtraSubtitlesDelayChanged = React.useCallback((delay) => { - dispatch({ type: 'setProp', propName: 'extraSubtitlesDelay', propValue: 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(); @@ -207,20 +208,23 @@ const Player = ({ urlParams, queryParams }) => { } } }, [player.nextVideo]); + const onVideoClick = React.useCallback(() => { - if (videoState.paused !== null) { - if (videoState.paused) { + if (video.state.paused !== null) { + if (video.state.paused) { onPlayRequestedDebounced(); } else { onPauseRequestedDebounced(); } } - }, [videoState.paused]); + }, [video.state.paused]); + const onVideoDoubleClick = React.useCallback(() => { onPlayRequestedDebounced.cancel(); onPauseRequestedDebounced.cancel(); toggleFullscreen(); }, [toggleFullscreen]); + const onContainerMouseDown = React.useCallback((event) => { if (!event.nativeEvent.optionsMenuClosePrevented) { closeOptionsMenu(); @@ -241,6 +245,7 @@ const Player = ({ urlParams, queryParams }) => { closeStatisticsMenu(); } }, []); + const onContainerMouseMove = React.useCallback((event) => { setImmersed(false); if (!event.nativeEvent.immersePrevented) { @@ -249,51 +254,50 @@ const Player = ({ urlParams, queryParams }) => { 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) { - dispatch({ type: 'command', commandName: 'unload' }); - } else if ((player.selected.metaRequest === null || (player.metaItem !== null && player.metaItem.type !== 'Loading'))) { - dispatch({ - type: 'command', - commandName: 'load', - commandArgs: { - 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 + video.unload(); + } else if (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 + })) : - 0, - forceTranscoding: forceTranscoding || casting, - maxAudioChannels: settings.surroundSound ? 32 : 2, - streamingServerURL: streamingServer.baseUrl ? - casting ? - streamingServer.baseUrl - : - streamingServer.selected.transportUrl + [] + }, + 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 : - null, - seriesInfo: player.seriesInfo - } + streamingServer.selected.transportUrl + : + null, + seriesInfo: player.seriesInfo }, { chromecastTransport: chromecast.active ? chromecast.transport : null, shellTransport: shell.active ? shell.transport : null, @@ -301,89 +305,74 @@ const Player = ({ urlParams, queryParams }) => { } }, [streamingServer.baseUrl, player.selected, player.metaItem, forceTranscoding, casting]); React.useEffect(() => { - if (videoState.stream !== null) { - dispatch({ - type: 'command', - commandName: 'addExtraSubtitlesTracks', - commandArgs: { - tracks: player.subtitles.map((subtitles) => ({ - ...subtitles, - label: subtitles.url - })) - } - }); + if (video.state.stream !== null) { + const tracks = player.subtitles.map((subtitles) => ({ + ...subtitles, + label: subtitles.url + })); + video.addExtraSubtitlesTracks(tracks); } - }, [player.subtitles, videoState.stream]); + }, [player.subtitles, video.state.stream]); + React.useEffect(() => { - dispatch({ type: 'setProp', propName: 'subtitlesSize', propValue: settings.subtitlesSize }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesSize', propValue: settings.subtitlesSize }); + video.setProp('subtitlesSize', settings.subtitlesSize); + video.setProp('extraSubtitlesSize', settings.subtitlesSize); }, [settings.subtitlesSize]); + React.useEffect(() => { - dispatch({ type: 'setProp', propName: 'subtitlesOffset', propValue: settings.subtitlesOffset }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesOffset', propValue: settings.subtitlesOffset }); + video.setProp('subtitlesOffset', settings.subtitlesOffset); + video.setProp('extraSubtitlesOffset', settings.subtitlesOffset); }, [settings.subtitlesOffset]); + React.useEffect(() => { - dispatch({ type: 'setProp', propName: 'subtitlesTextColor', propValue: settings.subtitlesTextColor }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesTextColor', propValue: settings.subtitlesTextColor }); + video.setProp('subtitlesTextColor', settings.subtitlesTextColor); + video.setProp('extraSubtitlesTextColor', settings.subtitlesTextColor); }, [settings.subtitlesTextColor]); + React.useEffect(() => { - dispatch({ type: 'setProp', propName: 'subtitlesBackgroundColor', propValue: settings.subtitlesBackgroundColor }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesBackgroundColor', propValue: settings.subtitlesBackgroundColor }); + video.setProp('subtitlesBackgroundColor', settings.subtitlesBackgroundColor); + video.setProp('extraSubtitlesBackgroundColor', settings.subtitlesBackgroundColor); }, [settings.subtitlesBackgroundColor]); + React.useEffect(() => { - dispatch({ type: 'setProp', propName: 'subtitlesOutlineColor', propValue: settings.subtitlesOutlineColor }); - dispatch({ type: 'setProp', propName: 'extraSubtitlesOutlineColor', propValue: settings.subtitlesOutlineColor }); + video.setProp('subtitlesOutlineColor', settings.subtitlesOutlineColor); + video.setProp('extraSubtitlesOutlineColor', settings.subtitlesOutlineColor); }, [settings.subtitlesOutlineColor]); + React.useEffect(() => { - if (videoState.time !== null && !isNaN(videoState.time) && - videoState.duration !== null && !isNaN(videoState.duration) && - videoState.manifest !== null && typeof videoState.manifest.name === 'string') { - timeChanged(videoState.time, videoState.duration, videoState.manifest.name); + 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); } - }, [videoState.time, videoState.duration, videoState.manifest]); + }, [video.state.time, video.state.duration, video.state.manifest]); + React.useEffect(() => { - if (videoState.paused !== null) { - pausedChanged(videoState.paused); + if (video.state.paused !== null) { + pausedChanged(video.state.paused); } - }, [videoState.paused]); + }, [video.state.paused]); + React.useEffect(() => { - videoParamsChanged(videoState.videoParams); - }, [videoState.videoParams]); + videoParamsChanged(video.state.videoParams); + }, [video.state.videoParams]); + React.useEffect(() => { if (!!settings.bingeWatching && player.nextVideo !== null && !nextVideoPopupDismissed.current) { - if (videoState.time !== null && videoState.duration !== null && videoState.time < videoState.duration && (videoState.duration - videoState.time) <= settings.nextVideoNotificationDuration) { + 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, videoState.time, videoState.duration]); - React.useEffect(() => { - if (player.selected && player.selected.stream && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') { - const { infoHash, fileIdx } = player.selected.stream; - const getStatistics = () => { - core.transport.dispatch({ - action: 'StreamingServer', - args: { - action: 'GetStatistics', - args: { - infoHash, - fileIdx, - } - } - }); - }; - getStatistics(); - const statisticsInterval = setInterval(getStatistics, 5000); - return () => clearInterval(statisticsInterval); - } - }, [player.selected]); + }, [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(videoState.subtitlesTracks, settings.subtitlesLanguage); - const extraSubtitlesTrack = findTrackByLang(videoState.extraSubtitlesTracks, settings.subtitlesLanguage); + const subtitlesTrack = findTrackByLang(video.state.subtitlesTracks, settings.subtitlesLanguage); + const extraSubtitlesTrack = findTrackByLang(video.state.extraSubtitlesTracks, settings.subtitlesLanguage); if (subtitlesTrack && subtitlesTrack.id) { onSubtitlesTrackSelected(subtitlesTrack.id); @@ -393,41 +382,47 @@ const Player = ({ urlParams, queryParams }) => { defaultSubtitlesSelected.current = true; } } - }, [videoState.subtitlesTracks, videoState.extraSubtitlesTracks]); + }, [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(videoState.audioTracks, settings.audioLanguage); + const audioTrack = findTrackByLang(video.state.audioTracks, settings.audioLanguage); if (audioTrack && audioTrack.id) { onAudioTrackSelected(audioTrack.id); defaultAudioTrackSelected.current = true; } } - }, [videoState.audioTracks]); + }, [video.state.audioTracks]); + React.useEffect(() => { defaultSubtitlesSelected.current = false; defaultAudioTrackSelected.current = false; nextVideoPopupDismissed.current = false; - }, [videoState.stream]); + }, [video.state.stream]); + React.useEffect(() => { - if ((!Array.isArray(videoState.subtitlesTracks) || videoState.subtitlesTracks.length === 0) && - (!Array.isArray(videoState.extraSubtitlesTracks) || videoState.extraSubtitlesTracks.length === 0) && - (!Array.isArray(videoState.audioTracks) || videoState.audioTracks.length === 0)) { + 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(); } - }, [videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks]); + }, [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 (videoState.playbackSpeed === null) { + if (video.state.playbackSpeed === null) { closeSpeedMenu(); } - }, [videoState.playbackSpeed]); + }, [video.state.playbackSpeed]); + React.useEffect(() => { const toastFilter = (item) => item?.dataset?.type === 'CoreEvent'; toast.addFilter(toastFilter); @@ -463,12 +458,13 @@ const Player = ({ urlParams, queryParams }) => { } }; }, []); + React.useLayoutEffect(() => { const onKeyDown = (event) => { switch (event.code) { case 'Space': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.paused !== null) { - if (videoState.paused) { + if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) { + if (video.state.paused) { onPlayRequested(); } else { onPauseRequested(); @@ -478,55 +474,47 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'ArrowRight': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.time !== null) { + if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) { const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration; - onSeekRequested(videoState.time + seekDuration); + onSeekRequested(video.state.time + seekDuration); } break; } case 'ArrowLeft': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.time !== null) { + if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) { const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration; - onSeekRequested(videoState.time - seekDuration); + onSeekRequested(video.state.time - seekDuration); } break; } case 'ArrowUp': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.volume !== null) { - onVolumeChangeRequested(videoState.volume + 5); + if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) { + onVolumeChangeRequested(video.state.volume + 5); } break; } case 'ArrowDown': { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.volume !== null) { - onVolumeChangeRequested(videoState.volume - 5); + if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) { + onVolumeChangeRequested(video.state.volume - 5); } break; } case 'KeyS': { - closeOptionsMenu(); - closeInfoMenu(); - closeSpeedMenu(); - closeVideosMenu(); - closeStatisticsMenu(); - if ((Array.isArray(videoState.subtitlesTracks) && videoState.subtitlesTracks.length > 0) || - (Array.isArray(videoState.extraSubtitlesTracks) && videoState.extraSubtitlesTracks.length > 0) || - (Array.isArray(videoState.audioTracks) && videoState.audioTracks.length > 0)) { + 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 'KeyI': { - closeOptionsMenu(); - closeSubtitlesMenu(); - closeSpeedMenu(); - closeVideosMenu(); - closeStatisticsMenu(); + closeMenus(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { toggleInfoMenu(); } @@ -534,23 +522,15 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'KeyR': { - closeOptionsMenu(); - closeInfoMenu(); - closeSubtitlesMenu(); - closeVideosMenu(); - closeStatisticsMenu(); - if (videoState.playbackSpeed !== null) { + closeMenus(); + if (video.state.playbackSpeed !== null) { toggleSpeedMenu(); } break; } case 'KeyV': { - closeOptionsMenu(); - closeInfoMenu(); - closeSubtitlesMenu(); - closeSpeedMenu(); - closeStatisticsMenu(); + closeMenus(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { toggleVideosMenu(); } @@ -558,11 +538,7 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'KeyD': { - closeOptionsMenu(); - closeInfoMenu(); - closeSubtitlesMenu(); - closeSpeedMenu(); - closeVideosMenu(); + closeMenus(); if (streamingServer.statistics !== null && streamingServer.statistics.type !== 'Err' && player.selected && typeof player.selected.stream.infoHash === 'string' && typeof player.selected.stream.fileIdx === 'number') { toggleStatisticsMenu(); } @@ -570,25 +546,19 @@ const Player = ({ urlParams, queryParams }) => { break; } case 'Escape': { - closeOptionsMenu(); - closeSubtitlesMenu(); - closeInfoMenu(); - closeSpeedMenu(); - closeVideosMenu(); - closeStatisticsMenu(); - onDismissNextVideoPopup(); + closeMenus(); break; } } }; const onWheel = ({ deltaY }) => { if (deltaY > 0) { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.volume !== null) { - onVolumeChangeRequested(videoState.volume - 5); + if (!menusOpen && video.state.volume !== null) { + onVolumeChangeRequested(video.state.volume - 5); } } else { - if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && videoState.volume !== null) { - onVolumeChangeRequested(videoState.volume + 5); + if (!menusOpen && video.state.volume !== null) { + onVolumeChangeRequested(video.state.volume + 5); } } }; @@ -600,7 +570,24 @@ const Player = ({ urlParams, queryParams }) => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('wheel', onWheel); }; - }, [player.metaItem, player.selected, streamingServer.statistics, settings.seekTimeDuration, settings.seekShortTimeDuration, routeFocused, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, optionsMenuOpen, statisticsMenuOpen, videoState.paused, videoState.time, videoState.volume, videoState.audioTracks, videoState.subtitlesTracks, videoState.extraSubtitlesTracks, videoState.playbackSpeed, toggleSubtitlesMenu, toggleInfoMenu, toggleVideosMenu, toggleStatisticsMenu]); + }, [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(); @@ -608,65 +595,37 @@ const Player = ({ urlParams, queryParams }) => { onPauseRequestedDebounced.cancel(); }; }, []); + return ( -