// 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, Button, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender } = require('stremio/common'); const { default: Icon } = require('@stremio/stremio-icons/react'); const BufferingLoader = require('./BufferingLoader'); 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, maxAudioChannels] = React.useMemo(() => { return [ queryParams.has('forceTranscoding'), queryParams.has('maxAudioChannels') ? parseInt(queryParams.get('maxAudioChannels'), 10) : null ]; }, [queryParams]); const [player, videoParamsChanged, timeChanged, pausedChanged, ended] = 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 [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false); const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false); 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) { 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.baseUrl !== null && streamingServer.baseUrl.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: typeof maxAudioChannels === 'number' ? maxAudioChannels : null, streamingServerURL: streamingServer.baseUrl.type === 'Ready' ? casting ? streamingServer.baseUrl.content : 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, maxAudioChannels, 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 (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && video.state.paused !== null) { if (video.state.paused) { onPlayRequested(); } else { onPauseRequested(); } } break; } case 'ArrowRight': { if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && video.state.time !== null) { const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration; onSeekRequested(video.state.time + seekDuration); } break; } case 'ArrowLeft': { if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && video.state.time !== null) { const seekDuration = event.shiftKey ? settings.seekShortTimeDuration : settings.seekTimeDuration; onSeekRequested(video.state.time - seekDuration); } break; } case 'ArrowUp': { if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && video.state.volume !== null) { onVolumeChangeRequested(video.state.volume + 5); } break; } case 'ArrowDown': { if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && video.state.volume !== null) { onVolumeChangeRequested(video.state.volume - 5); } break; } case 'KeyS': { closeOptionsMenu(); closeInfoMenu(); closeSpeedMenu(); closeVideosMenu(); closeStatisticsMenu(); 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(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { toggleInfoMenu(); } break; } case 'KeyR': { closeOptionsMenu(); closeInfoMenu(); closeSubtitlesMenu(); closeVideosMenu(); closeStatisticsMenu(); if (video.state.playbackSpeed !== null) { toggleSpeedMenu(); } break; } case 'KeyV': { closeOptionsMenu(); closeInfoMenu(); closeSubtitlesMenu(); closeSpeedMenu(); closeStatisticsMenu(); if (player.metaItem !== null && player.metaItem.type === 'Ready') { toggleVideosMenu(); } break; } case 'KeyD': { closeOptionsMenu(); closeInfoMenu(); closeSubtitlesMenu(); closeSpeedMenu(); closeVideosMenu(); 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': { closeOptionsMenu(); closeSubtitlesMenu(); closeInfoMenu(); closeSpeedMenu(); closeVideosMenu(); closeStatisticsMenu(); onDismissNextVideoPopup(); break; } } }; const onWheel = ({ deltaY }) => { if (deltaY > 0) { if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && video.state.volume !== null) { onVolumeChangeRequested(video.state.volume - 5); } } else { if (!subtitlesMenuOpen && !infoMenuOpen && !videosMenuOpen && !speedMenuOpen && !optionsMenuOpen && !statisticsMenuOpen && 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, subtitlesMenuOpen, infoMenuOpen, videosMenuOpen, speedMenuOpen, optionsMenuOpen, statisticsMenuOpen, 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 (