mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-05-24 08:32:10 +00:00
feat(ios): add mobile player picker for external playback
On iOS mobile, stream taps open a bottom sheet to choose Stremio, VLC, Infuse, or Outplayer instead of the broken in-app player. Adds iOS URL builders, safe custom-scheme opening, and Platform bypass for app links.
This commit is contained in:
parent
87ccc591df
commit
6ba73a48f2
10 changed files with 366 additions and 22 deletions
|
|
@ -54,6 +54,14 @@ const SUPPORTED_LOCAL_SUBTITLES = [
|
|||
'text/vtt',
|
||||
];
|
||||
|
||||
/** Per-tap player picker on iOS */
|
||||
const IOS_MOBILE_PICKER_PLAYERS = [
|
||||
{ id: 'stremio', label: 'Stremio', icon: 'play', requiresStreaming: false },
|
||||
{ id: 'vlc', label: 'VLC', icon: 'vlc', requiresStreaming: true },
|
||||
{ id: 'infuse', label: 'Infuse', icon: 'play', requiresStreaming: true },
|
||||
{ id: 'outplayer', label: 'Outplayer', icon: 'play', requiresStreaming: true },
|
||||
];
|
||||
|
||||
const EXTERNAL_PLAYERS = [
|
||||
{
|
||||
label: 'EXTERNAL_PLAYER_DISABLED',
|
||||
|
|
@ -143,6 +151,7 @@ module.exports = {
|
|||
MIME_SIGNATURES,
|
||||
SUPPORTED_LOCAL_SUBTITLES,
|
||||
EXTERNAL_PLAYERS,
|
||||
IOS_MOBILE_PICKER_PLAYERS,
|
||||
WHITELISTED_HOSTS,
|
||||
PROTOCOL,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { createContext, useContext } from 'react';
|
||||
import { WHITELISTED_HOSTS } from 'stremio/common/CONSTANTS';
|
||||
import openAppDeepLink, { isAppDeepLink } from 'stremio/common/openAppDeepLink';
|
||||
import { name, isMobile } from './device';
|
||||
import useShell from './shell/useShell';
|
||||
|
||||
|
|
@ -21,9 +22,14 @@ const PlatformProvider = ({ children }: Props) => {
|
|||
|
||||
const openExternal = (url: string) => {
|
||||
try {
|
||||
const { hostname } = new URL(url);
|
||||
if (isAppDeepLink(url)) {
|
||||
openAppDeepLink(url);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = new URL(url);
|
||||
const isWhitelisted = WHITELISTED_HOSTS.some((host: string) =>
|
||||
hostname === host || hostname.endsWith('.' + host)
|
||||
parsed.hostname === host || parsed.hostname.endsWith('.' + host)
|
||||
);
|
||||
const finalUrl = !isWhitelisted ? `https://www.stremio.com/warning#${encodeURIComponent(url)}` : url;
|
||||
|
||||
|
|
|
|||
40
src/common/buildIosPlayerUrl.js
Normal file
40
src/common/buildIosPlayerUrl.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
// URL schemes aligned with stremio-core deep_links/mod.rs
|
||||
|
||||
const STREMIO_PLAYER_SUCCESS = 'stremio%3A%2F%2F%2Fplayer%3FexternalPlayerSuccess%3D1';
|
||||
const STREMIO_PLAYER_ERROR = 'stremio%3A%2F%2F%2Fplayer%3FexternalPlayerSuccess%3D0';
|
||||
|
||||
/**
|
||||
* @param {string | null | undefined} streamingUrl HTTP stream from streaming server
|
||||
* @param {string} playerId
|
||||
* @param {{ playlist?: string | null }} [options]
|
||||
* @returns {string | null}
|
||||
*/
|
||||
const buildIosPlayerUrl = (streamingUrl, playerId, options = {}) => {
|
||||
if (playerId === 'stremio') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!streamingUrl || typeof streamingUrl !== 'string') {
|
||||
return playerId === 'm3u' && options.playlist ? options.playlist : null;
|
||||
}
|
||||
|
||||
const urlEncoded = encodeURIComponent(streamingUrl);
|
||||
|
||||
switch (playerId) {
|
||||
case 'vlc':
|
||||
return `vlc-x-callback://x-callback-url/stream?url=${urlEncoded}`;
|
||||
case 'outplayer':
|
||||
return streamingUrl.replace(/^https?:\/\//i, 'outplayer://');
|
||||
case 'infuse':
|
||||
return `infuse://x-callback-url/play?x-success=${STREMIO_PLAYER_SUCCESS}&x-error=${STREMIO_PLAYER_ERROR}&url=${urlEncoded}`;
|
||||
case 'vidhub':
|
||||
return `open-vidhub://x-callback-url/open?on-success=${STREMIO_PLAYER_SUCCESS}&on-failed=${STREMIO_PLAYER_ERROR}&url=${urlEncoded}`;
|
||||
case 'm3u':
|
||||
return options.playlist ?? null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = buildIosPlayerUrl;
|
||||
|
|
@ -7,6 +7,9 @@ const { ToastProvider, useToast } = require('./Toast');
|
|||
const { TooltipProvider, Tooltip } = require('./Tooltips');
|
||||
const { ShortcutsProvider, useShortcuts, onShortcut } = require('./Shortcuts');
|
||||
const CONSTANTS = require('./CONSTANTS');
|
||||
const buildIosPlayerUrl = require('./buildIosPlayerUrl');
|
||||
const openAppDeepLink = require('./openAppDeepLink');
|
||||
const usePWA = require('./usePWA');
|
||||
const { withCoreSuspender, useCoreSuspender } = require('./CoreSuspender');
|
||||
const getVisibleChildrenRange = require('./getVisibleChildrenRange');
|
||||
const interfaceLanguages = require('./interfaceLanguages.json');
|
||||
|
|
@ -45,6 +48,9 @@ module.exports = {
|
|||
TooltipProvider,
|
||||
Tooltip,
|
||||
CONSTANTS,
|
||||
buildIosPlayerUrl,
|
||||
openAppDeepLink,
|
||||
usePWA,
|
||||
withCoreSuspender,
|
||||
useCoreSuspender,
|
||||
getVisibleChildrenRange,
|
||||
|
|
|
|||
29
src/common/openAppDeepLink.js
Normal file
29
src/common/openAppDeepLink.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
/**
|
||||
* Opens a custom URL scheme (vlc-x-callback:, outplayer:, …) on iOS.
|
||||
* window.location.assign / <a href> in PWA often rewrite schemes to https://host//path.
|
||||
*/
|
||||
const openAppDeepLink = (url) => {
|
||||
if (typeof url !== 'string' || url.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = url;
|
||||
anchor.rel = 'noopener noreferrer';
|
||||
anchor.style.display = 'none';
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
};
|
||||
|
||||
const isAppDeepLink = (url) => {
|
||||
if (typeof url !== 'string' || url.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return /^[a-z][a-z0-9+.-]*:/i.test(url) && !/^https?:\/\//i.test(url);
|
||||
};
|
||||
|
||||
module.exports = openAppDeepLink;
|
||||
module.exports.isAppDeepLink = isAppDeepLink;
|
||||
|
|
@ -2,9 +2,18 @@
|
|||
|
||||
const React = require('react');
|
||||
|
||||
const isDisplayModeInstalled = () => (
|
||||
window.matchMedia('(display-mode: standalone)').matches ||
|
||||
window.matchMedia('(display-mode: fullscreen)').matches ||
|
||||
window.matchMedia('(display-mode: minimal-ui)').matches
|
||||
);
|
||||
|
||||
const usePWA = () => {
|
||||
const isPWA = React.useMemo(() => {
|
||||
const isIOSPWA = window.navigator.standalone;
|
||||
const isIOSPWA = Boolean(
|
||||
window.navigator.standalone === true ||
|
||||
isDisplayModeInstalled()
|
||||
);
|
||||
const isAndroidPWA = window.matchMedia('(display-mode: standalone)').matches;
|
||||
return [isIOSPWA, isAndroidPWA];
|
||||
}, []);
|
||||
|
|
@ -12,3 +21,4 @@ const usePWA = () => {
|
|||
};
|
||||
|
||||
module.exports = usePWA;
|
||||
module.exports.isDisplayModeInstalled = isDisplayModeInstalled;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const React = require('react');
|
||||
const PropTypes = require('prop-types');
|
||||
const { useTranslation } = require('react-i18next');
|
||||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { BottomSheet, Button } = require('stremio/components');
|
||||
const { CONSTANTS } = require('stremio/common');
|
||||
const styles = require('./styles');
|
||||
|
||||
const MobilePlayerPicker = ({ show, streamingUrl, onClose, onSelectPlayer }) => {
|
||||
const { t } = useTranslation();
|
||||
const hasStreaming = typeof streamingUrl === 'string' && streamingUrl.length > 0;
|
||||
|
||||
const onPlayerClick = React.useCallback((player) => {
|
||||
if (player.requiresStreaming && !hasStreaming) {
|
||||
return;
|
||||
}
|
||||
onSelectPlayer(player.id);
|
||||
}, [hasStreaming, onSelectPlayer]);
|
||||
|
||||
return (
|
||||
<BottomSheet
|
||||
title={t('MOBILE_PLAYER_PICKER_TITLE', { defaultValue: 'Play with' })}
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
>
|
||||
<div className={styles['picker-content']}>
|
||||
{
|
||||
!hasStreaming ?
|
||||
<div className={styles['hint']}>
|
||||
{t('MOBILE_PLAYER_HTTP_ONLY_HINT', { defaultValue: 'External players need an HTTP stream (e.g. Real-Debrid). Torrent-only sources may not work.' })}
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
<div className={styles['players-grid']}>
|
||||
{CONSTANTS.IOS_MOBILE_PICKER_PLAYERS.map((player) => {
|
||||
const disabled = player.requiresStreaming && !hasStreaming;
|
||||
const label = t(player.label, { defaultValue: player.label, keySeparator: false });
|
||||
return (
|
||||
<Button
|
||||
key={player.id}
|
||||
className={styles['player-option']}
|
||||
title={label}
|
||||
disabled={disabled}
|
||||
onClick={() => onPlayerClick(player)}
|
||||
>
|
||||
<div className={styles['player-icon-wrap']}>
|
||||
<Icon className={styles['player-icon']} name={player.icon} />
|
||||
</div>
|
||||
<div className={styles['player-label']}>{label}</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</BottomSheet>
|
||||
);
|
||||
};
|
||||
|
||||
MobilePlayerPicker.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
streamingUrl: PropTypes.string,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onSelectPlayer: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
module.exports = MobilePlayerPicker;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2023 Smart code 203358507
|
||||
|
||||
const MobilePlayerPicker = require('./MobilePlayerPicker');
|
||||
|
||||
module.exports = MobilePlayerPicker;
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
@import (reference) '~@stremio/stremio-colors/less/stremio-colors.less';
|
||||
|
||||
.picker-content {
|
||||
padding: 0 1rem 1.5rem;
|
||||
|
||||
.hint {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-placeholder);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.players-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.player-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 0.5rem;
|
||||
border-radius: var(--border-radius);
|
||||
min-height: 5.5rem;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--overlay-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.player-icon-wrap {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background-color: var(--secondary-accent-color);
|
||||
|
||||
.player-icon {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
|
||||
.player-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: var(--primary-foreground-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,10 +6,12 @@ const classnames = require('classnames');
|
|||
const { default: Icon } = require('@stremio/stremio-icons/react');
|
||||
const { t } = require('i18next');
|
||||
const { useCore } = require('stremio/core');
|
||||
const { useProfile, usePlatform, useToast, useBinaryState } = require('stremio/common');
|
||||
const { useProfile, usePlatform, useToast, useBinaryState, buildIosPlayerUrl, openAppDeepLink } = require('stremio/common');
|
||||
const { isAppDeepLink } = require('stremio/common/openAppDeepLink');
|
||||
const { Button, Image, Popup } = require('stremio/components');
|
||||
const { useRouteFocused } = require('stremio-router');
|
||||
const StreamPlaceholder = require('./StreamPlaceholder');
|
||||
const MobilePlayerPicker = require('./MobilePlayerPicker');
|
||||
const styles = require('./styles');
|
||||
|
||||
const Stream = ({ className, videoId, videoReleased, addonName, name, description, thumbnail, progress, deepLinks, ...props }) => {
|
||||
|
|
@ -19,7 +21,10 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
const core = useCore();
|
||||
const routeFocused = useRouteFocused();
|
||||
|
||||
const useMobilePlayerPicker = platform.name === 'ios' && platform.isMobile;
|
||||
|
||||
const [menuOpen, , closeMenu, toggleMenu] = useBinaryState(false);
|
||||
const [playerPickerOpen, openPlayerPicker, closePlayerPicker] = useBinaryState(false);
|
||||
|
||||
const popupLabelOnMouseUp = React.useCallback((event) => {
|
||||
if (!event.nativeEvent.togglePopupPrevented) {
|
||||
|
|
@ -97,6 +102,12 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
return deepLinks?.externalPlayer?.magnet;
|
||||
}, [deepLinks]);
|
||||
|
||||
const appDeepLinkHref = React.useMemo(() => {
|
||||
return platform.name === 'ios' && isAppDeepLink(href) ? href : null;
|
||||
}, [platform.name, href]);
|
||||
|
||||
const streamButtonHref = useMobilePlayerPicker || appDeepLinkHref ? null : href;
|
||||
|
||||
const markVideoAsWatched = React.useCallback(() => {
|
||||
if (typeof videoId === 'string') {
|
||||
core.transport.dispatch({
|
||||
|
|
@ -109,11 +120,93 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
}
|
||||
}, [videoId, videoReleased]);
|
||||
|
||||
const onStreamAnalytics = React.useCallback(() => {
|
||||
if (typeof props.onClick === 'function') {
|
||||
props.onClick({ nativeEvent: {} });
|
||||
}
|
||||
}, [props.onClick]);
|
||||
|
||||
const openNativePlayer = React.useCallback(() => {
|
||||
if (typeof deepLinks?.player === 'string') {
|
||||
window.location = deepLinks.player;
|
||||
}
|
||||
}, [deepLinks]);
|
||||
|
||||
const onMobilePlayerSelect = React.useCallback((playerId) => {
|
||||
if (playerId === 'stremio') {
|
||||
openNativePlayer();
|
||||
} else {
|
||||
const url = buildIosPlayerUrl(streamLink, playerId, {
|
||||
playlist: deepLinks?.externalPlayer?.playlist,
|
||||
});
|
||||
|
||||
if (url) {
|
||||
openAppDeepLink(url);
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: t('MOBILE_PLAYER_OPENED_EXTERNAL', { defaultValue: 'Stream opened in external player' }),
|
||||
timeout: 4000,
|
||||
});
|
||||
} else {
|
||||
toast.show({
|
||||
type: 'error',
|
||||
title: t('MOBILE_PLAYER_NO_HTTP_STREAM', { defaultValue: 'This stream has no HTTP URL for external players.' }),
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
closePlayerPicker();
|
||||
closeMenu();
|
||||
markVideoAsWatched();
|
||||
onStreamAnalytics();
|
||||
}, [streamLink, deepLinks, markVideoAsWatched, onStreamAnalytics, openNativePlayer, closePlayerPicker, closeMenu]);
|
||||
|
||||
const onPlayButtonClick = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
closeMenu();
|
||||
openPlayerPicker();
|
||||
}, [closeMenu, openPlayerPicker]);
|
||||
|
||||
const onAppDeepLinkPlayClick = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
closeMenu();
|
||||
if (appDeepLinkHref) {
|
||||
openAppDeepLink(appDeepLinkHref);
|
||||
markVideoAsWatched();
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: 'Stream opened in external player',
|
||||
timeout: 4000
|
||||
});
|
||||
onStreamAnalytics();
|
||||
}
|
||||
}, [appDeepLinkHref, closeMenu, markVideoAsWatched, onStreamAnalytics]);
|
||||
|
||||
const onClick = React.useCallback((event) => {
|
||||
if (event.nativeEvent.togglePopupPrevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (useMobilePlayerPicker) {
|
||||
event.preventDefault();
|
||||
openPlayerPicker();
|
||||
return;
|
||||
}
|
||||
|
||||
if (appDeepLinkHref) {
|
||||
event.preventDefault();
|
||||
openAppDeepLink(appDeepLinkHref);
|
||||
markVideoAsWatched();
|
||||
toast.show({
|
||||
type: 'success',
|
||||
title: 'Stream opened in external player',
|
||||
timeout: 4000
|
||||
});
|
||||
onStreamAnalytics();
|
||||
return;
|
||||
}
|
||||
|
||||
if (profile.settings.playerType !== null) {
|
||||
markVideoAsWatched();
|
||||
toast.show({
|
||||
|
|
@ -123,10 +216,8 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
});
|
||||
}
|
||||
|
||||
if (typeof props.onClick === 'function') {
|
||||
props.onClick(event);
|
||||
}
|
||||
}, [props.onClick, profile.settings, markVideoAsWatched]);
|
||||
onStreamAnalytics();
|
||||
}, [useMobilePlayerPicker, appDeepLinkHref, openPlayerPicker, profile.settings, markVideoAsWatched, onStreamAnalytics]);
|
||||
|
||||
const copyMagnetLink = React.useCallback((event) => {
|
||||
event.preventDefault();
|
||||
|
|
@ -200,7 +291,7 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
|
||||
const renderLabel = React.useMemo(() => function renderLabel({ className, children, ...props }) {
|
||||
return (
|
||||
<Button className={classnames(className, styles['stream-container'])} title={addonName} href={href} target={target} download={download} onClick={onClick} {...props}>
|
||||
<Button className={classnames(className, styles['stream-container'])} title={addonName} href={streamButtonHref} target={target} download={download} onClick={onClick} {...props}>
|
||||
<div className={styles['info-container']}>
|
||||
{
|
||||
typeof thumbnail === 'string' && thumbnail.length > 0 ?
|
||||
|
|
@ -232,7 +323,7 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
{children}
|
||||
</Button>
|
||||
);
|
||||
}, [thumbnail, progress, addonName, name, description, href, target, download, onClick]);
|
||||
}, [thumbnail, progress, addonName, name, description, streamButtonHref, target, download, onClick]);
|
||||
|
||||
const renderMenu = React.useMemo(() => function renderMenu() {
|
||||
return (
|
||||
|
|
@ -240,7 +331,12 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
<div className={styles['context-menu-title']}>
|
||||
{description}
|
||||
</div>
|
||||
<Button className={styles['context-menu-option-container']} title={t('CTX_PLAY')}>
|
||||
<Button
|
||||
className={styles['context-menu-option-container']}
|
||||
title={t('CTX_PLAY')}
|
||||
onClick={useMobilePlayerPicker ? onPlayButtonClick : appDeepLinkHref ? onAppDeepLinkPlayClick : undefined}
|
||||
href={useMobilePlayerPicker || appDeepLinkHref ? null : href}
|
||||
>
|
||||
<Icon className={styles['menu-icon']} name={'play'} />
|
||||
<div className={styles['context-menu-option-label']}>{t('CTX_PLAY')}</div>
|
||||
</Button>
|
||||
|
|
@ -267,25 +363,39 @@ const Stream = ({ className, videoId, videoReleased, addonName, name, descriptio
|
|||
}
|
||||
</div>
|
||||
);
|
||||
}, [copyStreamLink, onClick]);
|
||||
}, [copyStreamLink, copyMagnetLink, copyDownloadLink, description, streamLink, magnetLink, downloadLink, useMobilePlayerPicker, onPlayButtonClick, onAppDeepLinkPlayClick, appDeepLinkHref, href]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!routeFocused) {
|
||||
closeMenu();
|
||||
closePlayerPicker();
|
||||
}
|
||||
}, [routeFocused]);
|
||||
|
||||
return (
|
||||
<Popup
|
||||
className={className}
|
||||
onMouseUp={popupLabelOnMouseUp}
|
||||
onLongPress={popupLabelOnLongPress}
|
||||
onContextMenu={popupLabelOnContextMenu}
|
||||
open={menuOpen}
|
||||
onCloseRequest={closeMenu}
|
||||
renderLabel={renderLabel}
|
||||
renderMenu={renderMenu}
|
||||
/>
|
||||
<React.Fragment>
|
||||
<Popup
|
||||
className={className}
|
||||
onMouseUp={popupLabelOnMouseUp}
|
||||
onLongPress={popupLabelOnLongPress}
|
||||
onContextMenu={popupLabelOnContextMenu}
|
||||
open={menuOpen}
|
||||
onCloseRequest={closeMenu}
|
||||
renderLabel={renderLabel}
|
||||
renderMenu={renderMenu}
|
||||
/>
|
||||
{
|
||||
useMobilePlayerPicker ?
|
||||
<MobilePlayerPicker
|
||||
show={playerPickerOpen}
|
||||
streamingUrl={streamLink}
|
||||
onClose={closePlayerPicker}
|
||||
onSelectPlayer={onMobilePlayerSelect}
|
||||
/>
|
||||
:
|
||||
null
|
||||
}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue