mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-01-11 22:40:31 +00:00
967 lines
40 KiB
JavaScript
967 lines
40 KiB
JavaScript
// 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 { onFileDrop, useSettings, useProfile, useFullscreen, useBinaryState, useToast, useStreamingServer, withCoreSuspender, CONSTANTS, useShell, usePlatform, onShortcut } = require('stremio/common');
|
|
const { HorizontalNavBar, Transition, ContextMenu } = require('stremio/components');
|
|
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 OptionsMenu = require('./OptionsMenu');
|
|
const SubtitlesMenu = require('./SubtitlesMenu');
|
|
const { default: AudioMenu } = require('./AudioMenu');
|
|
const SpeedMenu = require('./SpeedMenu');
|
|
const { default: SideDrawerButton } = require('./SideDrawerButton');
|
|
const { default: SideDrawer } = require('./SideDrawer');
|
|
const usePlayer = require('./usePlayer');
|
|
const useStatistics = require('./useStatistics');
|
|
const useVideo = require('./useVideo');
|
|
const styles = require('./styles');
|
|
const Video = require('./Video');
|
|
const { default: Indicator } = require('./Indicator/Indicator');
|
|
|
|
const Player = ({ urlParams, queryParams }) => {
|
|
const { t } = useTranslation();
|
|
const services = useServices();
|
|
const shell = useShell();
|
|
const forceTranscoding = React.useMemo(() => {
|
|
return queryParams.has('forceTranscoding');
|
|
}, [queryParams]);
|
|
const profile = useProfile();
|
|
const [player, videoParamsChanged, timeChanged, seek, pausedChanged, ended, nextVideo] = usePlayer(urlParams);
|
|
const [settings, updateSettings] = useSettings();
|
|
const streamingServer = useStreamingServer();
|
|
const statistics = useStatistics(player, streamingServer);
|
|
const video = useVideo();
|
|
const routeFocused = useRouteFocused();
|
|
const platform = usePlatform();
|
|
const toast = useToast();
|
|
|
|
const [seeking, setSeeking] = React.useState(false);
|
|
|
|
const [casting, setCasting] = React.useState(() => {
|
|
return services.chromecast.active && services.chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED;
|
|
});
|
|
const playbackDevices = React.useMemo(() => streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Ready' ? streamingServer.playbackDevices.content : [], [streamingServer]);
|
|
|
|
const bufferingRef = React.useRef();
|
|
const errorRef = React.useRef();
|
|
|
|
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 [audioMenuOpen, , closeAudioMenu, toggleAudioMenu] = useBinaryState(false);
|
|
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
|
|
const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
|
|
const [nextVideoPopupOpen, openNextVideoPopup, closeNextVideoPopup] = useBinaryState(false);
|
|
const [sideDrawerOpen, , closeSideDrawer, toggleSideDrawer] = useBinaryState(false);
|
|
|
|
const menusOpen = React.useMemo(() => {
|
|
return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen || speedMenuOpen || statisticsMenuOpen || sideDrawerOpen;
|
|
}, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, speedMenuOpen, statisticsMenuOpen, sideDrawerOpen]);
|
|
|
|
const closeMenus = React.useCallback(() => {
|
|
closeOptionsMenu();
|
|
closeSubtitlesMenu();
|
|
closeAudioMenu();
|
|
closeSpeedMenu();
|
|
closeStatisticsMenu();
|
|
closeSideDrawer();
|
|
}, []);
|
|
|
|
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 isNavigating = React.useRef(false);
|
|
|
|
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 handleNextVideoNavigation = React.useCallback((deepLinks, bingeWatching, ended) => {
|
|
if (ended) {
|
|
if (bingeWatching) {
|
|
if (deepLinks.player) {
|
|
isNavigating.current = true;
|
|
window.location.replace(deepLinks.player);
|
|
} else if (deepLinks.metaDetailsStreams) {
|
|
isNavigating.current = true;
|
|
window.location.replace(deepLinks.metaDetailsStreams);
|
|
}
|
|
} else {
|
|
window.history.back();
|
|
}
|
|
} else {
|
|
if (deepLinks.player) {
|
|
isNavigating.current = true;
|
|
window.location.replace(deepLinks.player);
|
|
} else if (deepLinks.metaDetailsStreams) {
|
|
isNavigating.current = true;
|
|
window.location.replace(deepLinks.metaDetailsStreams);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
const onEnded = React.useCallback(() => {
|
|
// here we need to explicitly check for isNavigating.current
|
|
// the ended event can be called multiple times by MPV inside Shell
|
|
if (isNavigating.current) {
|
|
return;
|
|
}
|
|
|
|
ended();
|
|
if (window.playerNextVideo !== null) {
|
|
nextVideo();
|
|
|
|
const deepLinks = window.playerNextVideo.deepLinks;
|
|
handleNextVideoNavigation(deepLinks, profile.settings.bingeWatching, true);
|
|
|
|
} else {
|
|
window.history.back();
|
|
}
|
|
}, []);
|
|
|
|
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') :
|
|
track.local ? t('PLAYER_SUBTITLES_LOADED_LOCAL') :
|
|
t('PLAYER_SUBTITLES_LOADED_ORIGIN', { origin: track.origin }),
|
|
timeout: 3000
|
|
});
|
|
}, []);
|
|
|
|
const onExtraSubtitlesTrackAdded = React.useCallback((track) => {
|
|
if (track.local) {
|
|
video.setExtraSubtitlesTrack(track.id);
|
|
}
|
|
}, []);
|
|
|
|
const onPlayRequested = React.useCallback(() => {
|
|
video.setProp('paused', false);
|
|
setSeeking(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);
|
|
seek(time, video.state.duration, video.state.manifest?.name);
|
|
}, [video.state.duration, video.state.manifest]);
|
|
|
|
const onPlaybackSpeedChanged = React.useCallback((rate) => {
|
|
video.setProp('playbackSpeed', rate);
|
|
}, []);
|
|
|
|
const onSubtitlesTrackSelected = React.useCallback((id) => {
|
|
video.setSubtitlesTrack(id);
|
|
}, []);
|
|
|
|
const onExtraSubtitlesTrackSelected = React.useCallback((id) => {
|
|
video.setExtraSubtitlesTrack(id);
|
|
}, []);
|
|
|
|
const onAudioTrackSelected = React.useCallback((id) => {
|
|
video.setProp('selectedAudioTrackId', id);
|
|
}, []);
|
|
|
|
const onExtraSubtitlesDelayChanged = React.useCallback((delay) => {
|
|
video.setProp('extraSubtitlesDelay', delay);
|
|
}, []);
|
|
|
|
const onIncreaseSubtitlesDelay = React.useCallback(() => {
|
|
const delay = video.state.extraSubtitlesDelay + 250;
|
|
onExtraSubtitlesDelayChanged(delay);
|
|
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
|
|
|
|
const onDecreaseSubtitlesDelay = React.useCallback(() => {
|
|
const delay = video.state.extraSubtitlesDelay - 250;
|
|
onExtraSubtitlesDelayChanged(delay);
|
|
}, [video.state.extraSubtitlesDelay, onExtraSubtitlesDelayChanged]);
|
|
|
|
const onSubtitlesSizeChanged = React.useCallback((size) => {
|
|
updateSettings({ subtitlesSize: size });
|
|
}, [updateSettings]);
|
|
|
|
const onUpdateSubtitlesSize = React.useCallback((delta) => {
|
|
const sizeIndex = CONSTANTS.SUBTITLES_SIZES.indexOf(video.state.subtitlesSize);
|
|
const size = CONSTANTS.SUBTITLES_SIZES[Math.max(0, Math.min(CONSTANTS.SUBTITLES_SIZES.length - 1, sizeIndex + delta))];
|
|
onSubtitlesSizeChanged(size);
|
|
}, [video.state.subtitlesSize, onSubtitlesSizeChanged]);
|
|
|
|
const onSubtitlesOffsetChanged = React.useCallback((offset) => {
|
|
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;
|
|
handleNextVideoNavigation(deepLinks, profile.settings.bingeWatching, false);
|
|
}
|
|
}, [player.nextVideo, handleNextVideoNavigation, profile.settings]);
|
|
|
|
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.audioMenuClosePrevented) {
|
|
closeAudioMenu();
|
|
}
|
|
if (!event.nativeEvent.speedMenuClosePrevented) {
|
|
closeSpeedMenu();
|
|
}
|
|
if (!event.nativeEvent.statisticsMenuClosePrevented) {
|
|
closeStatisticsMenu();
|
|
}
|
|
|
|
closeSideDrawer();
|
|
}, []);
|
|
|
|
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;
|
|
}, []);
|
|
|
|
onFileDrop(CONSTANTS.SUPPORTED_LOCAL_SUBTITLES, async (filename, buffer) => {
|
|
video.addLocalSubtitles(filename, buffer);
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
setError(null);
|
|
video.unload();
|
|
|
|
if (player.selected && player.stream?.type === 'Ready' && streamingServer.settings?.type !== 'Loading') {
|
|
video.load({
|
|
stream: {
|
|
...player.stream.content,
|
|
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,
|
|
hardwareDecoding: settings.hardwareDecoding,
|
|
videoMode: settings.videoMode,
|
|
platform: platform.name,
|
|
streamingServerURL: streamingServer.baseUrl ?
|
|
casting ?
|
|
streamingServer.baseUrl
|
|
:
|
|
streamingServer.selected.transportUrl
|
|
:
|
|
null,
|
|
seriesInfo: player.seriesInfo,
|
|
}, {
|
|
chromecastTransport: services.chromecast.active ? services.chromecast.transport : null,
|
|
shellTransport: services.shell.active ? services.shell.transport : null,
|
|
});
|
|
}
|
|
}, [streamingServer.baseUrl, player.selected, player.stream, forceTranscoding, casting]);
|
|
React.useEffect(() => {
|
|
if (video.state.stream !== null) {
|
|
const tracks = player.subtitles.map((subtitles) => ({
|
|
...subtitles,
|
|
label: subtitles.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(() => {
|
|
!seeking && timeChanged(video.state.time, video.state.duration, video.state.manifest?.name);
|
|
}, [video.state.time, video.state.duration, video.state.manifest, seeking]);
|
|
|
|
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 (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();
|
|
}
|
|
}
|
|
if (player.nextVideo) {
|
|
// This is a workaround for the fact that when we call onEnded nextVideo from the player is already set to null since core unloads the stream
|
|
// we explicitly set it to a global variable so we can access it in the onEnded function
|
|
// this is not a good solution but it works for now
|
|
window.playerNextVideo = player.nextVideo;
|
|
} else {
|
|
window.playerNextVideo = null;
|
|
}
|
|
}, [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);
|
|
|
|
if (settings.subtitlesLanguage === null) {
|
|
onSubtitlesTrackSelected(null);
|
|
onExtraSubtitlesTrackSelected(null);
|
|
defaultSubtitlesSelected.current = true;
|
|
return;
|
|
}
|
|
|
|
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;
|
|
// we need a timeout here to make sure that previous page unloads and the new one loads
|
|
// avoiding race conditions and flickering
|
|
setTimeout(() => isNavigating.current = false, 1000);
|
|
}, [video.state.stream]);
|
|
|
|
React.useEffect(() => {
|
|
if ((!Array.isArray(video.state.subtitlesTracks) || video.state.subtitlesTracks.length === 0) &&
|
|
(!Array.isArray(video.state.extraSubtitlesTracks) || video.state.extraSubtitlesTracks.length === 0)) {
|
|
closeSubtitlesMenu();
|
|
}
|
|
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks]);
|
|
|
|
React.useEffect(() => {
|
|
if (!Array.isArray(video.state.audioTracks) || video.state.audioTracks.length === 0) {
|
|
closeAudioMenu();
|
|
}
|
|
}, [video.state.audioTracks]);
|
|
|
|
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(services.chromecast.active && services.chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED);
|
|
};
|
|
const onChromecastServiceStateChange = () => {
|
|
onCastStateChange();
|
|
if (services.chromecast.active) {
|
|
services.chromecast.transport.on(
|
|
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
|
onCastStateChange
|
|
);
|
|
}
|
|
};
|
|
const onCoreEvent = ({ event }) => {
|
|
if (event === 'PlayingOnDevice') {
|
|
onPauseRequested();
|
|
}
|
|
};
|
|
services.chromecast.on('stateChanged', onChromecastServiceStateChange);
|
|
services.core.transport.on('CoreEvent', onCoreEvent);
|
|
onChromecastServiceStateChange();
|
|
return () => {
|
|
toast.removeFilter(toastFilter);
|
|
services.chromecast.off('stateChanged', onChromecastServiceStateChange);
|
|
services.core.transport.off('CoreEvent', onCoreEvent);
|
|
if (services.chromecast.active) {
|
|
services.chromecast.transport.off(
|
|
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
|
|
onCastStateChange
|
|
);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
React.useEffect(() => {
|
|
if (settings.pauseOnMinimize && (shell.windowClosed || shell.windowHidden)) {
|
|
onPauseRequested();
|
|
}
|
|
}, [settings.pauseOnMinimize, shell.windowClosed, shell.windowHidden]);
|
|
|
|
// Media Session PlaybackState
|
|
React.useEffect(() => {
|
|
if (!navigator.mediaSession) return;
|
|
|
|
const playbackState = !video.state.paused ? 'playing' : 'paused';
|
|
navigator.mediaSession.playbackState = playbackState;
|
|
|
|
return () => navigator.mediaSession.playbackState = 'none';
|
|
}, [video.state.paused]);
|
|
|
|
// Media Session Metadata
|
|
React.useEffect(() => {
|
|
if (!navigator.mediaSession) return;
|
|
|
|
const metaItem = player.metaItem && player.metaItem?.type === 'Ready' ? player.metaItem.content : null;
|
|
const videoId = player.selected ? player.selected?.streamRequest?.path?.id : null;
|
|
const video = metaItem ? metaItem.videos.find(({ id }) => id === videoId) : null;
|
|
|
|
const videoInfo = video && video.season && video.episode ? ` (${video.season}x${video.episode})`: null;
|
|
const videoTitle = video ? `${video.title}${videoInfo}` : null;
|
|
const metaTitle = metaItem ? metaItem.name : null;
|
|
const imageUrl = metaItem ? metaItem.logo : null;
|
|
|
|
const title = videoTitle ?? metaTitle;
|
|
const artist = videoTitle ? metaTitle : undefined;
|
|
const artwork = imageUrl ? [{ src: imageUrl }] : undefined;
|
|
|
|
if (title) {
|
|
navigator.mediaSession.metadata = new MediaMetadata({
|
|
title,
|
|
artist,
|
|
artwork,
|
|
});
|
|
}
|
|
}, [player.metaItem, player.selected]);
|
|
|
|
// Media Session Actions
|
|
React.useEffect(() => {
|
|
if (!navigator.mediaSession) return;
|
|
|
|
navigator.mediaSession.setActionHandler('play', onPlayRequested);
|
|
navigator.mediaSession.setActionHandler('pause', onPauseRequested);
|
|
|
|
const nexVideoCallback = player.nextVideo ? onNextVideoRequested : null;
|
|
navigator.mediaSession.setActionHandler('nexttrack', nexVideoCallback);
|
|
}, [player.nextVideo, onPlayRequested, onPauseRequested, onNextVideoRequested]);
|
|
|
|
onShortcut('playPause', () => {
|
|
if (!menusOpen && !nextVideoPopupOpen && video.state.paused !== null) {
|
|
if (video.state.paused) {
|
|
onPlayRequested();
|
|
setSeeking(false);
|
|
} else {
|
|
onPauseRequested();
|
|
}
|
|
}
|
|
}, [menusOpen, nextVideoPopupOpen, video.state.paused, onPlayRequested, onPauseRequested]);
|
|
|
|
onShortcut('seekForward', (combo) => {
|
|
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
|
|
const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
|
setSeeking(true);
|
|
onSeekRequested(video.state.time + seekDuration);
|
|
}
|
|
}, [menusOpen, nextVideoPopupOpen, video.state.time, onSeekRequested]);
|
|
|
|
onShortcut('seekBackward', (combo) => {
|
|
if (!menusOpen && !nextVideoPopupOpen && video.state.time !== null) {
|
|
const seekDuration = combo === 1 ? settings.seekShortTimeDuration : settings.seekTimeDuration;
|
|
setSeeking(true);
|
|
onSeekRequested(video.state.time - seekDuration);
|
|
}
|
|
}, [menusOpen, nextVideoPopupOpen, video.state.time, onSeekRequested]);
|
|
|
|
onShortcut('volumeUp', () => {
|
|
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
|
onVolumeChangeRequested(Math.min(video.state.volume + 5, 200));
|
|
}
|
|
}, [menusOpen, nextVideoPopupOpen, video.state.volume]);
|
|
|
|
onShortcut('volumeDown', () => {
|
|
if (!menusOpen && !nextVideoPopupOpen && video.state.volume !== null) {
|
|
onVolumeChangeRequested(Math.min(video.state.volume - 5, 200));
|
|
}
|
|
}, [menusOpen, nextVideoPopupOpen, video.state.volume]);
|
|
|
|
onShortcut('subtitlesDelay', (combo) => {
|
|
combo === 1 ? onIncreaseSubtitlesDelay() : onDecreaseSubtitlesDelay();
|
|
}, [onIncreaseSubtitlesDelay, onDecreaseSubtitlesDelay]);
|
|
|
|
onShortcut('subtitlesSize', (combo) => {
|
|
combo === 1 ? onUpdateSubtitlesSize(-1) : onUpdateSubtitlesSize(1);
|
|
}, [onUpdateSubtitlesSize, onUpdateSubtitlesSize]);
|
|
|
|
onShortcut('subtitlesMenu', () => {
|
|
closeMenus();
|
|
if (video.state?.subtitlesTracks?.length > 0 || video.state?.extraSubtitlesTracks?.length > 0) {
|
|
toggleSubtitlesMenu();
|
|
}
|
|
}, [video.state.subtitlesTracks, video.state.extraSubtitlesTracks, toggleSubtitlesMenu]);
|
|
|
|
onShortcut('audioMenu', () => {
|
|
closeMenus();
|
|
if (video.state?.audioTracks?.length > 0) {
|
|
toggleAudioMenu();
|
|
}
|
|
}, [video.state.audioTracks, toggleAudioMenu]);
|
|
|
|
onShortcut('infoMenu', () => {
|
|
closeMenus();
|
|
if (player.metaItem?.type === 'Ready') {
|
|
toggleSideDrawer();
|
|
}
|
|
}, [player.metaItem, toggleSideDrawer]);
|
|
|
|
onShortcut('speedMenu', () => {
|
|
closeMenus();
|
|
if (video.state.playbackSpeed !== null) {
|
|
toggleSpeedMenu();
|
|
}
|
|
}, [video.state.playbackSpeed, toggleSpeedMenu]);
|
|
|
|
onShortcut('statisticsMenu', () => {
|
|
closeMenus();
|
|
const stream = player.selected?.stream;
|
|
if (streamingServer?.statistics?.type !== 'Err' && typeof stream === 'string' && typeof stream === 'number') {
|
|
toggleStatisticsMenu();
|
|
}
|
|
}, [player.selected, streamingServer.statistics, toggleStatisticsMenu]);
|
|
|
|
onShortcut('exit', () => {
|
|
closeMenus();
|
|
!settings.escExitFullscreen && window.history.back();
|
|
}, [settings.escExitFullscreen]);
|
|
|
|
React.useLayoutEffect(() => {
|
|
const onKeyUp = (event) => {
|
|
if (event.code === 'ArrowRight' || event.code === 'ArrowLeft') {
|
|
setSeeking(false);
|
|
}
|
|
};
|
|
const onWheel = ({ deltaY }) => {
|
|
if (menusOpen || video.state.volume === null) return;
|
|
|
|
if (deltaY > 0) {
|
|
onVolumeChangeRequested(Math.max(video.state.volume - 5, 0));
|
|
} else {
|
|
if (video.state.volume < 100) {
|
|
onVolumeChangeRequested(Math.min(video.state.volume + 5, 100));
|
|
}
|
|
}
|
|
};
|
|
if (routeFocused) {
|
|
window.addEventListener('keyup', onKeyUp);
|
|
window.addEventListener('wheel', onWheel);
|
|
}
|
|
return () => {
|
|
window.removeEventListener('keyup', onKeyUp);
|
|
window.removeEventListener('wheel', onWheel);
|
|
};
|
|
}, [routeFocused, menusOpen, video.state.volume]);
|
|
|
|
React.useEffect(() => {
|
|
video.events.on('error', onError);
|
|
video.events.on('ended', onEnded);
|
|
video.events.on('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
|
video.events.on('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
|
video.events.on('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
|
|
video.events.on('implementationChanged', onImplementationChanged);
|
|
|
|
return () => {
|
|
video.events.off('error', onError);
|
|
video.events.off('ended', onEnded);
|
|
video.events.off('subtitlesTrackLoaded', onSubtitlesTrackLoaded);
|
|
video.events.off('extraSubtitlesTrackLoaded', onExtraSubtitlesTrackLoaded);
|
|
video.events.off('extraSubtitlesTrackAdded', onExtraSubtitlesTrackAdded);
|
|
video.events.off('implementationChanged', onImplementationChanged);
|
|
};
|
|
}, []);
|
|
|
|
React.useLayoutEffect(() => {
|
|
return () => {
|
|
setImmersedDebounced.cancel();
|
|
onPlayRequestedDebounced.cancel();
|
|
onPauseRequestedDebounced.cancel();
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<div className={classnames(styles['player-container'], { [styles['overlayHidden']]: overlayHidden })}
|
|
onMouseDown={onContainerMouseDown}
|
|
onMouseMove={onContainerMouseMove}
|
|
onMouseOver={onContainerMouseMove}
|
|
onMouseLeave={onContainerMouseLeave}>
|
|
<Video
|
|
ref={video.containerRef}
|
|
className={styles['layer']}
|
|
onClick={onVideoClick}
|
|
onDoubleClick={onVideoDoubleClick}
|
|
/>
|
|
{
|
|
!video.state.loaded ?
|
|
<div className={classnames(styles['layer'], styles['background-layer'])}>
|
|
<img className={styles['image']} src={player?.metaItem?.content?.background} />
|
|
</div>
|
|
:
|
|
null
|
|
}
|
|
{
|
|
(video.state.buffering || !video.state.loaded) && !error ?
|
|
<BufferingLoader
|
|
ref={bufferingRef}
|
|
className={classnames(styles['layer'], styles['buffering-layer'])}
|
|
logo={player?.metaItem?.content?.logo}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
{
|
|
error !== null ?
|
|
<Error
|
|
ref={errorRef}
|
|
className={classnames(styles['layer'], styles['error-layer'])}
|
|
stream={video.state.stream}
|
|
{...error}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
{
|
|
menusOpen ?
|
|
<div className={styles['layer']} />
|
|
:
|
|
null
|
|
}
|
|
{
|
|
video.state.volume !== null && overlayHidden ?
|
|
<VolumeChangeIndicator
|
|
muted={video.state.muted}
|
|
volume={video.state.volume}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
<ContextMenu on={[video.containerRef, bufferingRef, errorRef]} autoClose>
|
|
<OptionsMenu
|
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
|
stream={player?.selected?.stream}
|
|
playbackDevices={playbackDevices}
|
|
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
|
|
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
|
|
/>
|
|
</ContextMenu>
|
|
<HorizontalNavBar
|
|
className={classnames(styles['layer'], styles['nav-bar-layer'])}
|
|
title={player.title !== null ? player.title : ''}
|
|
backButton={true}
|
|
fullscreenButton={true}
|
|
onMouseMove={onBarMouseMove}
|
|
onMouseOver={onBarMouseMove}
|
|
/>
|
|
{
|
|
player.metaItem?.type === 'Ready' ?
|
|
<SideDrawerButton
|
|
className={classnames(styles['layer'], styles['side-drawer-button-layer'])}
|
|
onClick={toggleSideDrawer}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
<ControlBar
|
|
className={classnames(styles['layer'], styles['control-bar-layer'])}
|
|
paused={video.state.paused}
|
|
time={video.state.time}
|
|
duration={video.state.duration}
|
|
buffered={video.state.buffered}
|
|
volume={video.state.volume}
|
|
muted={video.state.muted}
|
|
playbackSpeed={video.state.playbackSpeed}
|
|
subtitlesTracks={video.state.subtitlesTracks.concat(video.state.extraSubtitlesTracks)}
|
|
audioTracks={video.state.audioTracks}
|
|
metaItem={player.metaItem}
|
|
nextVideo={player.nextVideo}
|
|
stream={player.selected !== null ? player.selected.stream : null}
|
|
statistics={statistics}
|
|
onPlayRequested={onPlayRequested}
|
|
onPauseRequested={onPauseRequested}
|
|
onNextVideoRequested={onNextVideoRequested}
|
|
onMuteRequested={onMuteRequested}
|
|
onUnmuteRequested={onUnmuteRequested}
|
|
onVolumeChangeRequested={onVolumeChangeRequested}
|
|
onSeekRequested={onSeekRequested}
|
|
onToggleOptionsMenu={toggleOptionsMenu}
|
|
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
|
onToggleAudioMenu={toggleAudioMenu}
|
|
onToggleSpeedMenu={toggleSpeedMenu}
|
|
onToggleStatisticsMenu={toggleStatisticsMenu}
|
|
onToggleSideDrawer={toggleSideDrawer}
|
|
onMouseMove={onBarMouseMove}
|
|
onMouseOver={onBarMouseMove}
|
|
onTouchEnd={onContainerMouseLeave}
|
|
/>
|
|
<Indicator
|
|
className={classnames(styles['layer'], styles['indicator-layer'])}
|
|
videoState={video.state}
|
|
disabled={subtitlesMenuOpen}
|
|
/>
|
|
{
|
|
nextVideoPopupOpen ?
|
|
<NextVideoPopup
|
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
|
metaItem={player.metaItem !== null && player.metaItem.type === 'Ready' ? player.metaItem.content : null}
|
|
nextVideo={player.nextVideo}
|
|
onDismiss={onDismissNextVideoPopup}
|
|
onNextVideoRequested={onNextVideoRequested}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
{
|
|
statisticsMenuOpen ?
|
|
<StatisticsMenu
|
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
|
{...statistics}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
<Transition when={sideDrawerOpen} name={'slide-left'}>
|
|
<SideDrawer
|
|
className={classnames(styles['layer'], styles['side-drawer-layer'])}
|
|
metaItem={player.metaItem?.content}
|
|
seriesInfo={player.seriesInfo}
|
|
closeSideDrawer={closeSideDrawer}
|
|
selected={player.selected?.streamRequest?.path.id}
|
|
/>
|
|
</Transition>
|
|
{
|
|
subtitlesMenuOpen ?
|
|
<SubtitlesMenu
|
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
|
subtitlesTracks={video.state.subtitlesTracks}
|
|
selectedSubtitlesTrackId={video.state.selectedSubtitlesTrackId}
|
|
subtitlesOffset={video.state.subtitlesOffset}
|
|
subtitlesSize={video.state.subtitlesSize}
|
|
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
|
|
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
|
|
extraSubtitlesOffset={video.state.extraSubtitlesOffset}
|
|
extraSubtitlesDelay={video.state.extraSubtitlesDelay}
|
|
extraSubtitlesSize={video.state.extraSubtitlesSize}
|
|
onSubtitlesTrackSelected={onSubtitlesTrackSelected}
|
|
onExtraSubtitlesTrackSelected={onExtraSubtitlesTrackSelected}
|
|
onSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
|
|
onSubtitlesSizeChanged={onSubtitlesSizeChanged}
|
|
onExtraSubtitlesOffsetChanged={onSubtitlesOffsetChanged}
|
|
onExtraSubtitlesDelayChanged={onExtraSubtitlesDelayChanged}
|
|
onExtraSubtitlesSizeChanged={onSubtitlesSizeChanged}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
{
|
|
audioMenuOpen ?
|
|
<AudioMenu
|
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
|
audioTracks={video.state.audioTracks}
|
|
selectedAudioTrackId={video.state.selectedAudioTrackId}
|
|
onAudioTrackSelected={onAudioTrackSelected}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
{
|
|
speedMenuOpen ?
|
|
<SpeedMenu
|
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
|
playbackSpeed={video.state.playbackSpeed}
|
|
onPlaybackSpeedChanged={onPlaybackSpeedChanged}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
{
|
|
optionsMenuOpen ?
|
|
<OptionsMenu
|
|
className={classnames(styles['layer'], styles['menu-layer'])}
|
|
stream={player.selected.stream}
|
|
playbackDevices={playbackDevices}
|
|
extraSubtitlesTracks={video.state.extraSubtitlesTracks}
|
|
selectedExtraSubtitlesTrackId={video.state.selectedExtraSubtitlesTrackId}
|
|
/>
|
|
:
|
|
null
|
|
}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
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)
|
|
};
|
|
|
|
const PlayerFallback = () => (
|
|
<div className={classnames(styles['player-container'])} />
|
|
);
|
|
|
|
module.exports = withCoreSuspender(Player, PlayerFallback);
|