mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-08-09 04:37:15 +00:00
Merge 7d0910d382 into 93defccd76
This commit is contained in:
commit
2dfd5311ac
7 changed files with 194 additions and 24 deletions
56
src/routes/Player/CastDevicesMenu/CastDevicesMenu.tsx
Normal file
56
src/routes/Player/CastDevicesMenu/CastDevicesMenu.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (C) 2017-2026 Smart code 203358507
|
||||
|
||||
import React, { forwardRef, memo, MouseEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import classNames from 'classnames';
|
||||
import Option from '../OptionsMenu/Option';
|
||||
import styles from './styles.less';
|
||||
|
||||
type CastDevice = {
|
||||
id: string,
|
||||
name: string,
|
||||
};
|
||||
|
||||
type Props = {
|
||||
className: string,
|
||||
devices: CastDevice[],
|
||||
loading: boolean,
|
||||
onDeviceSelected: (deviceId: string) => void,
|
||||
};
|
||||
|
||||
const CastDevicesMenu = memo(forwardRef<HTMLDivElement, Props>(({ className, devices, loading, onDeviceSelected }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onMouseDown = (event: MouseEvent) => {
|
||||
// @ts-expect-error: Property 'castDevicesMenuClosePrevented' does not exist on type 'MouseEvent'.
|
||||
event.nativeEvent.castDevicesMenuClosePrevented = true;
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={ref} className={classNames(className, styles['cast-devices-menu-container'])} onMouseDown={onMouseDown}>
|
||||
{
|
||||
devices.length > 0 ?
|
||||
devices.map(({ id, name }) => (
|
||||
<Option
|
||||
key={id}
|
||||
icon={'cast'}
|
||||
label={t('PLAYER_PLAY_IN', { device: name })}
|
||||
deviceId={id}
|
||||
onClick={onDeviceSelected}
|
||||
/>
|
||||
))
|
||||
:
|
||||
<div className={styles['message']}>
|
||||
{
|
||||
loading ?
|
||||
t('STREAM_LOADING')
|
||||
:
|
||||
t('PLAYER_NO_CAST_DEVICES_FOUND')
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}));
|
||||
|
||||
export default CastDevicesMenu;
|
||||
5
src/routes/Player/CastDevicesMenu/index.ts
Normal file
5
src/routes/Player/CastDevicesMenu/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2026 Smart code 203358507
|
||||
|
||||
import CastDevicesMenu from './CastDevicesMenu';
|
||||
|
||||
export default CastDevicesMenu;
|
||||
13
src/routes/Player/CastDevicesMenu/styles.less
Normal file
13
src/routes/Player/CastDevicesMenu/styles.less
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (C) 2017-2026 Smart code 203358507
|
||||
|
||||
.cast-devices-menu-container {
|
||||
width: 16rem;
|
||||
padding: 1rem;
|
||||
|
||||
.message {
|
||||
padding: 1rem;
|
||||
color: var(--primary-foreground-color);
|
||||
opacity: 0.7;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,6 +39,8 @@ const ControlBar = React.forwardRef(({
|
|||
onToggleSpeedMenu,
|
||||
onToggleSideDrawer,
|
||||
onToggleOptionsMenu,
|
||||
shellCastSupported,
|
||||
onToggleCastDevicesMenu,
|
||||
videoScale,
|
||||
videoScaleLabel,
|
||||
onVideoScaleChanged,
|
||||
|
|
@ -68,6 +70,9 @@ const ControlBar = React.forwardRef(({
|
|||
const onStatisticsButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.statisticsMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onCastDevicesButtonMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.castDevicesMenuClosePrevented = true;
|
||||
}, []);
|
||||
const onPlayPauseButtonClick = React.useCallback(() => {
|
||||
if (paused) {
|
||||
if (typeof onPlayRequested === 'function') {
|
||||
|
|
@ -95,9 +100,19 @@ const ControlBar = React.forwardRef(({
|
|||
}
|
||||
}
|
||||
}, [muted, onMuteRequested, onUnmuteRequested]);
|
||||
const castButtonDisabled = platform.shell.active ? !shellCastSupported : !chromecastServiceActive;
|
||||
const onChromecastButtonClick = React.useCallback(() => {
|
||||
if (platform.shell.active) {
|
||||
if (shellCastSupported && typeof onToggleCastDevicesMenu === 'function') {
|
||||
onToggleCastDevicesMenu();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (castButtonDisabled) {
|
||||
return;
|
||||
}
|
||||
chromecast.transport.requestSession();
|
||||
}, []);
|
||||
}, [castButtonDisabled, platform.shell.active, shellCastSupported, onToggleCastDevicesMenu]);
|
||||
React.useEffect(() => {
|
||||
const onStateChanged = () => {
|
||||
setChromecastServiceActive(chromecast.active);
|
||||
|
|
@ -163,7 +178,7 @@ const ControlBar = React.forwardRef(({
|
|||
<Button className={classnames(styles['control-bar-button'], { 'disabled': playbackSpeed === null })} tabIndex={-1} onMouseDown={onSpeedButtonMouseDown} onClick={onToggleSpeedMenu}>
|
||||
<Icon className={styles['icon']} name={'speed'} />
|
||||
</Button>
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': !chromecastServiceActive })} tabIndex={-1} onClick={onChromecastButtonClick}>
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': castButtonDisabled })} tabIndex={-1} onMouseDown={onCastDevicesButtonMouseDown} onClick={onChromecastButtonClick}>
|
||||
<Icon className={styles['icon']} name={'cast'} />
|
||||
</Button>
|
||||
<Button className={classnames(styles['control-bar-button'], { 'disabled': !Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0 })} tabIndex={-1} onMouseDown={onSubtitlesButtonMouseDown} onClick={onToggleSubtitlesMenu}>
|
||||
|
|
@ -222,6 +237,8 @@ ControlBar.propTypes = {
|
|||
onToggleSpeedMenu: PropTypes.func,
|
||||
onToggleSideDrawer: PropTypes.func,
|
||||
onToggleOptionsMenu: PropTypes.func,
|
||||
shellCastSupported: PropTypes.bool,
|
||||
onToggleCastDevicesMenu: PropTypes.func,
|
||||
onToggleStatisticsMenu: PropTypes.func,
|
||||
onMouseOver: PropTypes.func,
|
||||
onMouseMove: PropTypes.func,
|
||||
|
|
|
|||
|
|
@ -4,27 +4,26 @@ const React = require('react');
|
|||
const PropTypes = require('prop-types');
|
||||
const classnames = require('classnames');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const { useCore } = require('stremio/core');
|
||||
const { usePlatform, useToast } = require('stremio/common');
|
||||
const { default: usePlayOnDevice } = require('../usePlayOnDevice');
|
||||
const Option = require('./Option');
|
||||
const styles = require('./styles');
|
||||
|
||||
const OptionsMenu = React.memo(React.forwardRef(({ className, stream, playbackDevices, extraSubtitlesTracks, selectedExtraSubtitlesTrackId }, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const core = useCore();
|
||||
const platform = usePlatform();
|
||||
const toast = useToast();
|
||||
const [streamingUrl, downloadUrl, magnetUrl] = React.useMemo(() => {
|
||||
const { streamingUrl, playOnDevice } = usePlayOnDevice(stream);
|
||||
const [downloadUrl, magnetUrl] = React.useMemo(() => {
|
||||
return stream !== null ?
|
||||
stream.deepLinks &&
|
||||
stream.deepLinks.externalPlayer &&
|
||||
[
|
||||
stream.deepLinks.externalPlayer.streaming,
|
||||
stream.deepLinks.externalPlayer.download,
|
||||
stream.deepLinks.externalPlayer.magnet,
|
||||
]
|
||||
:
|
||||
[null, null, null];
|
||||
[null, null];
|
||||
}, [stream]);
|
||||
const externalDevices = React.useMemo(() => {
|
||||
return playbackDevices.filter(({ type }) => type === 'external');
|
||||
|
|
@ -89,20 +88,6 @@ const OptionsMenu = React.memo(React.forwardRef(({ className, stream, playbackDe
|
|||
subtitlesTrackUrl && platform.openExternal(subtitlesTrackUrl);
|
||||
}, [subtitlesTrackUrl]);
|
||||
|
||||
const onExternalDeviceRequested = React.useCallback((deviceId) => {
|
||||
if (streamingUrl) {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'PlayOnDevice',
|
||||
args: {
|
||||
device: deviceId,
|
||||
source: streamingUrl,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [streamingUrl]);
|
||||
const onMouseDown = React.useCallback((event) => {
|
||||
event.nativeEvent.optionsMenuClosePrevented = true;
|
||||
}, []);
|
||||
|
|
@ -161,7 +146,7 @@ const OptionsMenu = React.memo(React.forwardRef(({ className, stream, playbackDe
|
|||
label={t('PLAYER_PLAY_IN', { device: name })}
|
||||
deviceId={id}
|
||||
disabled={stream === null}
|
||||
onClick={onExternalDeviceRequested}
|
||||
onClick={playOnDevice}
|
||||
/>
|
||||
))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ const ControlBar = require('./ControlBar');
|
|||
const NextVideoPopup = require('./NextVideoPopup');
|
||||
const StatisticsMenu = require('./StatisticsMenu');
|
||||
const OptionsMenu = require('./OptionsMenu');
|
||||
const { default: CastDevicesMenu } = require('./CastDevicesMenu');
|
||||
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 { default: usePlayOnDevice } = require('./usePlayOnDevice');
|
||||
const useStatistics = require('./useStatistics');
|
||||
const useVideo = require('./useVideo');
|
||||
const { default: useSubtitles } = require('./useSubtitles');
|
||||
|
|
@ -40,6 +42,8 @@ const findTrackById = (tracks, id) => tracks.find((track) => track.id === id);
|
|||
|
||||
const GAMEPAD_HANDLER_ID = 'player';
|
||||
|
||||
const CAST_DEVICES_REFRESH_INTERVAL = 5000;
|
||||
|
||||
const Player = () => {
|
||||
const { stream, streamTransportUrl, metaTransportUrl, type, id, videoId } = useParams();
|
||||
const urlParams = React.useMemo(() => ({
|
||||
|
|
@ -97,12 +101,13 @@ const Player = () => {
|
|||
const [audioMenuOpen, , closeAudioMenu, toggleAudioMenu] = useBinaryState(false);
|
||||
const [speedMenuOpen, , closeSpeedMenu, toggleSpeedMenu] = useBinaryState(false);
|
||||
const [statisticsMenuOpen, , closeStatisticsMenu, toggleStatisticsMenu] = useBinaryState(false);
|
||||
const [castDevicesMenuOpen, , closeCastDevicesMenu, toggleCastDevicesMenu] = 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 || nextVideoPopupOpen;
|
||||
}, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, speedMenuOpen, statisticsMenuOpen, sideDrawerOpen, nextVideoPopupOpen]);
|
||||
return optionsMenuOpen || subtitlesMenuOpen || audioMenuOpen || speedMenuOpen || statisticsMenuOpen || castDevicesMenuOpen || sideDrawerOpen || nextVideoPopupOpen;
|
||||
}, [optionsMenuOpen, subtitlesMenuOpen, audioMenuOpen, speedMenuOpen, statisticsMenuOpen, castDevicesMenuOpen, sideDrawerOpen, nextVideoPopupOpen]);
|
||||
|
||||
const closeMenus = React.useCallback(() => {
|
||||
closeOptionsMenu();
|
||||
|
|
@ -110,9 +115,49 @@ const Player = () => {
|
|||
closeAudioMenu();
|
||||
closeSpeedMenu();
|
||||
closeStatisticsMenu();
|
||||
closeCastDevicesMenu();
|
||||
closeSideDrawer();
|
||||
}, []);
|
||||
|
||||
const castDevices = React.useMemo(() => {
|
||||
const seen = new Set();
|
||||
return playbackDevices
|
||||
.filter(({ type }) => type === 'chromecast' || type === 'tv')
|
||||
.sort((a, b) => a.type === b.type ? 0 : a.type === 'chromecast' ? -1 : 1)
|
||||
.filter(({ name }) => seen.has(name) ? false : seen.add(name));
|
||||
}, [playbackDevices]);
|
||||
const [castDevicesSearching, setCastDevicesSearching] = React.useState(false);
|
||||
const castDevicesLoading = platform.shell.active && (castDevicesSearching || (streamingServer.playbackDevices !== null && streamingServer.playbackDevices.type === 'Loading'));
|
||||
const { streamingUrl: castStreamingUrl, playOnDevice } = usePlayOnDevice(player.selected?.stream ?? null);
|
||||
const shellCastSupported = platform.shell.active && castStreamingUrl !== null;
|
||||
const refreshCastDevices = React.useCallback(() => {
|
||||
if (platform.shell.active) {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'RefreshPlaybackDevices',
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [platform.shell.active]);
|
||||
const onCastDeviceSelected = React.useCallback((deviceId) => {
|
||||
playOnDevice(deviceId);
|
||||
closeCastDevicesMenu();
|
||||
}, [playOnDevice]);
|
||||
React.useEffect(() => {
|
||||
if (castDevicesMenuOpen && platform.shell.active) {
|
||||
setCastDevicesSearching(true);
|
||||
refreshCastDevices();
|
||||
const interval = setInterval(refreshCastDevices, CAST_DEVICES_REFRESH_INTERVAL);
|
||||
const timeout = setTimeout(() => setCastDevicesSearching(false), CAST_DEVICES_REFRESH_INTERVAL);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
clearTimeout(timeout);
|
||||
setCastDevicesSearching(false);
|
||||
};
|
||||
}
|
||||
}, [castDevicesMenuOpen, refreshCastDevices]);
|
||||
|
||||
const {
|
||||
streamSubtitles,
|
||||
allSubtitleTracks,
|
||||
|
|
@ -297,6 +342,9 @@ const Player = () => {
|
|||
if (!event.nativeEvent.statisticsMenuClosePrevented) {
|
||||
closeStatisticsMenu();
|
||||
}
|
||||
if (!event.nativeEvent.castDevicesMenuClosePrevented) {
|
||||
closeCastDevicesMenu();
|
||||
}
|
||||
|
||||
closeSideDrawer();
|
||||
}, []);
|
||||
|
|
@ -898,6 +946,8 @@ const Player = () => {
|
|||
onVolumeChangeRequested={onVolumeChangeRequested}
|
||||
onSeekRequested={onSeekRequested}
|
||||
onToggleOptionsMenu={toggleOptionsMenu}
|
||||
shellCastSupported={shellCastSupported}
|
||||
onToggleCastDevicesMenu={toggleCastDevicesMenu}
|
||||
onToggleSubtitlesMenu={toggleSubtitlesMenu}
|
||||
onToggleAudioMenu={toggleAudioMenu}
|
||||
onToggleSpeedMenu={toggleSpeedMenu}
|
||||
|
|
@ -933,6 +983,14 @@ const Player = () => {
|
|||
{...statistics}
|
||||
/>
|
||||
</Transition>
|
||||
<Transition when={castDevicesMenuOpen} name={'fade'}>
|
||||
<CastDevicesMenu
|
||||
className={classnames(styles['layer'], styles['menu-layer'])}
|
||||
devices={castDevices}
|
||||
loading={castDevicesLoading}
|
||||
onDeviceSelected={onCastDeviceSelected}
|
||||
/>
|
||||
</Transition>
|
||||
<Transition when={sideDrawerOpen} name={'slide-left'}>
|
||||
<SideDrawer
|
||||
className={classnames(styles['layer'], styles['side-drawer-layer'])}
|
||||
|
|
|
|||
36
src/routes/Player/usePlayOnDevice.ts
Normal file
36
src/routes/Player/usePlayOnDevice.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright (C) 2017-2026 Smart code 203358507
|
||||
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useCore } from 'stremio/core';
|
||||
|
||||
type Stream = {
|
||||
deepLinks?: {
|
||||
externalPlayer?: {
|
||||
streaming?: string | null,
|
||||
},
|
||||
},
|
||||
} | null;
|
||||
|
||||
const usePlayOnDevice = (stream: Stream) => {
|
||||
const core = useCore();
|
||||
const streamingUrl = useMemo(() => {
|
||||
return stream?.deepLinks?.externalPlayer?.streaming ?? null;
|
||||
}, [stream]);
|
||||
const playOnDevice = useCallback((deviceId: string) => {
|
||||
if (streamingUrl) {
|
||||
core.transport.dispatch({
|
||||
action: 'StreamingServer',
|
||||
args: {
|
||||
action: 'PlayOnDevice',
|
||||
args: {
|
||||
device: deviceId,
|
||||
source: streamingUrl,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [streamingUrl]);
|
||||
return { streamingUrl, playOnDevice };
|
||||
};
|
||||
|
||||
export default usePlayOnDevice;
|
||||
Loading…
Reference in a new issue