Merge pull request #375 from Stremio/external-players

Allow Using External Players in Settings and Player
This commit is contained in:
Alexandru Branza 2023-05-31 12:05:37 +02:00 committed by GitHub
commit 49cf961e6a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 202 additions and 47 deletions

26
package-lock.json generated
View file

@ -12,7 +12,7 @@
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "5.0.1",
"@stremio/stremio-core-web": "0.44.14",
"@stremio/stremio-core-web": "0.44.17",
"@stremio/stremio-icons": "4.0.0",
"@stremio/stremio-video": "0.0.24",
"a-color-picker": "1.2.1",
@ -36,7 +36,7 @@
"react-i18next": "^12.1.1",
"react-is": "18.2.0",
"spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "git+https://git@github.com/Stremio/stremio-translations.git#f55fe0d7dfe33bd6be461b4430cba2319afd1d8e",
"stremio-translations": "git+https://git@github.com/Stremio/stremio-translations.git#eb2d0dbcf959d99fad448d4b41aca11c0ae29449",
"url": "0.11.0"
},
"devDependencies": {
@ -2702,9 +2702,9 @@
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
},
"node_modules/@stremio/stremio-core-web": {
"version": "0.44.14",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.14.tgz",
"integrity": "sha512-v2m6EoR49Qohbrv7PxwX1yS1XjNxCkTiEe4KOEAHsCydvjtryJOpBZCXFWDstSx2o57uYwfJq1DNJRvEUVpeCA==",
"version": "0.44.17",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.17.tgz",
"integrity": "sha512-F5mnx2zWTV5w1h9VX89qk/Wbv6DbKKM8KCIW2i/ZqlAqYGIjKHyjh1p7/g2yxGBV4dkm7DI1nQHwtao7lyCgww==",
"dependencies": {
"@babel/runtime": "7.16.0"
}
@ -12848,8 +12848,8 @@
},
"node_modules/stremio-translations": {
"version": "1.43.16",
"resolved": "git+https://git@github.com/Stremio/stremio-translations.git#f55fe0d7dfe33bd6be461b4430cba2319afd1d8e",
"integrity": "sha512-yCFQSjZ1R/xjbHiPb7UZnWUFNu5YQq6vsdzSQZ1DGizwGnERxUvRwyzh8a1tlNOEyjZoZyI4cqdbv/LHRGCqbA==",
"resolved": "git+https://git@github.com/Stremio/stremio-translations.git#eb2d0dbcf959d99fad448d4b41aca11c0ae29449",
"integrity": "sha512-SV3Foxdr1nYJZGhq0TX4rZaZiXtOTMX59dAUcOMT/EwrtDqCdg6fSbNuNAlO/adlaSyyFcpEZKC+U4kRRFANkA==",
"license": "MIT"
},
"node_modules/string_decoder": {
@ -16795,9 +16795,9 @@
"integrity": "sha512-Dt3PYmy1DZ473QNs99KYXVWQPHtpIl37VUY0+gCEvvuCqE1fRrZIJtZ9KbysUKonvO7WwdQDztgcW0iGoc1dEA=="
},
"@stremio/stremio-core-web": {
"version": "0.44.14",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.14.tgz",
"integrity": "sha512-v2m6EoR49Qohbrv7PxwX1yS1XjNxCkTiEe4KOEAHsCydvjtryJOpBZCXFWDstSx2o57uYwfJq1DNJRvEUVpeCA==",
"version": "0.44.17",
"resolved": "https://registry.npmjs.org/@stremio/stremio-core-web/-/stremio-core-web-0.44.17.tgz",
"integrity": "sha512-F5mnx2zWTV5w1h9VX89qk/Wbv6DbKKM8KCIW2i/ZqlAqYGIjKHyjh1p7/g2yxGBV4dkm7DI1nQHwtao7lyCgww==",
"requires": {
"@babel/runtime": "7.16.0"
}
@ -24508,9 +24508,9 @@
"dev": true
},
"stremio-translations": {
"version": "git+https://git@github.com/Stremio/stremio-translations.git#f55fe0d7dfe33bd6be461b4430cba2319afd1d8e",
"integrity": "sha512-yCFQSjZ1R/xjbHiPb7UZnWUFNu5YQq6vsdzSQZ1DGizwGnERxUvRwyzh8a1tlNOEyjZoZyI4cqdbv/LHRGCqbA==",
"from": "stremio-translations@git+https://git@github.com/Stremio/stremio-translations.git#f55fe0d7dfe33bd6be461b4430cba2319afd1d8e"
"version": "git+https://git@github.com/Stremio/stremio-translations.git#eb2d0dbcf959d99fad448d4b41aca11c0ae29449",
"integrity": "sha512-SV3Foxdr1nYJZGhq0TX4rZaZiXtOTMX59dAUcOMT/EwrtDqCdg6fSbNuNAlO/adlaSyyFcpEZKC+U4kRRFANkA==",
"from": "stremio-translations@git+https://git@github.com/Stremio/stremio-translations.git#eb2d0dbcf959d99fad448d4b41aca11c0ae29449"
},
"string_decoder": {
"version": "1.1.1",

View file

@ -15,7 +15,7 @@
"@babel/runtime": "7.16.0",
"@sentry/browser": "6.13.3",
"@stremio/stremio-colors": "5.0.1",
"@stremio/stremio-core-web": "0.44.14",
"@stremio/stremio-core-web": "0.44.17",
"@stremio/stremio-icons": "4.0.0",
"@stremio/stremio-video": "0.0.24",
"a-color-picker": "1.2.1",
@ -39,7 +39,7 @@
"react-i18next": "^12.1.1",
"react-is": "18.2.0",
"spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#64871b1422466f5f45d24ebc8bbd315b2ebab6a6",
"stremio-translations": "git+https://git@github.com/Stremio/stremio-translations.git#f55fe0d7dfe33bd6be461b4430cba2319afd1d8e",
"stremio-translations": "git+https://git@github.com/Stremio/stremio-translations.git#eb2d0dbcf959d99fad448d4b41aca11c0ae29449",
"url": "0.11.0"
},
"devDependencies": {

View file

@ -0,0 +1,30 @@
// Copyright (C) 2017-2022 Smart code 203358507
const platform = require('./platform');
let options = [{ label: 'EXTERNAL_PLAYER_DISABLED', value: 'internal' }];
if (platform.name === 'ios') {
options = options.concat([
{ label: 'VLC', value: 'vlc' },
{ label: 'Outplayer', value: 'outplayer' },
{ label: 'Infuse', value: 'infuse' }
]);
} else if (platform.name === 'android') {
options = options.concat([
{ label: 'EXTERNAL_PLAYER_ALLOW_CHOOSING', value: 'choose' },
{ label: 'VLC', value: 'vlc' },
{ label: 'Just Player', value: 'justplayer' },
{ label: 'MX Player', value: 'mxplayer' }
]);
} else if (['windows', 'macos', 'linux'].includes(platform.name)) {
options = options.concat([
{ label: 'VLC', value: 'vlc' }
]);
} else {
options = options.concat([
{ label: 'M3U Playlist', value: 'm3u' }
]);
}
module.exports = options;

View file

@ -40,6 +40,8 @@ const useOnScrollToBottom = require('./useOnScrollToBottom');
const useProfile = require('./useProfile');
const useStreamingServer = require('./useStreamingServer');
const useTorrent = require('./useTorrent');
const platform = require('./platform');
const externalPlayerOptions = require('./externalPlayerOptions');
module.exports = {
AddonDetailsModal,
@ -84,5 +86,7 @@ module.exports = {
useOnScrollToBottom,
useProfile,
useStreamingServer,
useTorrent
useTorrent,
platform,
externalPlayerOptions,
};

14
src/common/platform.js Normal file
View file

@ -0,0 +1,14 @@
// Copyright (C) 2017-2022 Smart code 203358507
const Bowser = require('bowser');
const browser = Bowser.parse(window.navigator?.userAgent || '');
const name = (browser?.os?.name || 'unknown').toLowerCase();
module.exports = {
name,
isMobile: () => {
return name === 'ios' || name === 'android';
}
};

View file

@ -4,25 +4,64 @@ const React = require('react');
const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('@stremio/stremio-icons/dom');
const { Button, Image, PlayIconCircleCentered } = require('stremio/common');
const { Button, Image, PlayIconCircleCentered, useProfile, platform, useStreamingServer, useToast } = require('stremio/common');
const { useServices } = require('stremio/services');
const StreamPlaceholder = require('./StreamPlaceholder');
const styles = require('./styles');
const Stream = ({ className, addonName, name, description, thumbnail, progress, deepLinks, ...props }) => {
const profile = useProfile();
const streamingServer = useStreamingServer();
const { core } = useServices();
const toast = useToast();
const href = React.useMemo(() => {
const haveStreamingServer = streamingServer.settings !== null && streamingServer.settings.type === 'Ready';
return deepLinks ?
typeof deepLinks.player === 'string' ?
deepLinks.player
profile.settings.playerType && profile.settings.playerType !== 'internal' ?
platform.isMobile() || !haveStreamingServer ?
(deepLinks.externalPlayer.openPlayer || {})[platform.name] || deepLinks.externalPlayer.href
: null
:
null
typeof deepLinks.player === 'string' ?
deepLinks.player
:
null
:
null;
}, [deepLinks]);
}, [deepLinks, profile, streamingServer]);
const onClick = React.useCallback((e) => {
if (href === null) {
// link does not lead to the player, it is expected to
// open with local video player through the streaming server
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'PlayOnDevice',
args: {
device: 'vlc',
source: deepLinks.externalPlayer.streaming
}
}
});
} else if (profile.settings.playerType === 'external') {
toast.show({
type: 'success',
title: 'Stream opened in external player',
timeout: 4000
});
}
props.onClick(e);
}, [href, deepLinks, props.onClick, profile, toast]);
const forceDownload = React.useMemo(() => {
// we only do this in one case to force the download
// of a M3U playlist generated in the browser
return href === deepLinks.externalPlayer.href ? deepLinks.externalPlayer.fileName : false;
}, [href]);
const renderThumbnailFallback = React.useCallback(() => (
<Icon className={styles['placeholder-icon']} icon={'ic_broken_link'} />
), []);
return (
<Button href={href} {...props} className={classnames(className, styles['stream-container'])} title={addonName}>
<Button href={href} download={forceDownload} {...props} onClick={onClick} className={classnames(className, styles['stream-container'])} title={addonName}>
{
typeof thumbnail === 'string' && thumbnail.length > 0 ?
<div className={styles['thumbnail-container']} title={name || addonName}>
@ -62,8 +101,58 @@ Stream.propTypes = {
thumbnail: PropTypes.string,
progress: PropTypes.number,
deepLinks: PropTypes.shape({
player: PropTypes.string
})
player: PropTypes.string,
externalPlayer: PropTypes.shape({
href: PropTypes.string,
fileName: PropTypes.string,
streaming: PropTypes.string,
openPlayer: PropTypes.shape({
choose: {
ios: PropTypes.string,
android: PropTypes.string,
windows: PropTypes.string,
macos: PropTypes.string,
linux: PropTypes.string
},
vlc: {
ios: PropTypes.string,
android: PropTypes.string,
windows: PropTypes.string,
macos: PropTypes.string,
linux: PropTypes.string
},
outplayer: {
ios: PropTypes.string,
android: PropTypes.string,
windows: PropTypes.string,
macos: PropTypes.string,
linux: PropTypes.string
},
infuse: {
ios: PropTypes.string,
android: PropTypes.string,
windows: PropTypes.string,
macos: PropTypes.string,
linux: PropTypes.string
},
justplayer: {
ios: PropTypes.string,
android: PropTypes.string,
windows: PropTypes.string,
macos: PropTypes.string,
linux: PropTypes.string
},
mxplayer: {
ios: PropTypes.string,
android: PropTypes.string,
windows: PropTypes.string,
macos: PropTypes.string,
linux: PropTypes.string
},
})
})
}),
onClick: PropTypes.func
};
module.exports = Stream;

View file

@ -588,6 +588,12 @@ const Player = ({ urlParams, queryParams }) => {
error !== null ?
<div className={classnames(styles['layer'], styles['error-layer'])}>
<div className={styles['error-label']} title={error.message}>{error.message}</div>
{
error.code === 2 ?
<div className={styles['error-sub']} title={t('EXTERNAL_PLAYER_HINT')}>{t('EXTERNAL_PLAYER_HINT')}</div>
:
null
}
{
player.selected !== null ?
<Button className={styles['playlist-button']} title={t('PLAYER_OPEN_IN_EXTERNAL')} href={player.selected.stream.deepLinks.externalPlayer.href} download={player.selected.stream.deepLinks.externalPlayer.fileName} target={'_blank'}>

View file

@ -54,6 +54,16 @@ html:not(.active-slider-within) {
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: @color-surface-light5-90;
text-align: center;
}
.playlist-button {
flex: none;
display: flex;

View file

@ -35,10 +35,10 @@ const Settings = () => {
subtitlesOutlineColorInput,
audioLanguageSelect,
seekTimeDurationSelect,
playInExternalPlayerSelect,
nextVideoPopupDurationSelect,
bingeWatchingCheckbox,
playInBackgroundCheckbox,
playInExternalPlayerCheckbox,
hardwareDecodingCheckbox,
streamingServerUrlInput
} = useProfileSettingsInputs(profile);
@ -399,11 +399,9 @@ const Settings = () => {
<div className={styles['option-name-container']}>
<div className={styles['label']}>{ t('SETTINGS_PLAY_IN_EXTERNAL_PLAYER') }</div>
</div>
<Checkbox
className={classnames(styles['option-input-container'], styles['checkbox-container'])}
disabled={true}
tabIndex={-1}
{...playInExternalPlayerCheckbox}
<Multiselect
className={classnames(styles['option-input-container'], styles['multiselect-container'])}
{...playInExternalPlayerSelect}
/>
</div>
<div className={styles['option-container']}>

View file

@ -3,7 +3,7 @@
const React = require('react');
const { useTranslation } = require('react-i18next');
const { useServices } = require('stremio/services');
const { CONSTANTS, interfaceLanguages, languageNames } = require('stremio/common');
const { CONSTANTS, interfaceLanguages, languageNames, externalPlayerOptions } = require('stremio/common');
const useProfileSettingsInputs = (profile) => {
const { t } = useTranslation();
@ -157,6 +157,25 @@ const useProfileSettingsInputs = (profile) => {
});
}
}), [profile.settings]);
const playInExternalPlayerSelect = React.useMemo(() => ({
options: externalPlayerOptions.map((opt) => {
opt.label = t(opt.label);
return opt;
}),
selected: [`${profile.settings.playerType || 'internal'}`],
onSelect: (event) => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
playerType: event.value
}
}
});
}
}), [profile.settings]);
const nextVideoPopupDurationSelect = React.useMemo(() => ({
options: CONSTANTS.NEXT_VIDEO_POPUP_DURATIONS.map((duration) => ({
value: `${duration}`,
@ -212,21 +231,6 @@ const useProfileSettingsInputs = (profile) => {
});
}
}), [profile.settings]);
const playInExternalPlayerCheckbox = React.useMemo(() => ({
checked: profile.settings.playInExternalPlayer,
onClick: () => {
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
args: {
...profile.settings,
playInExternalPlayer: !profile.settings.playInExternalPlayer
}
}
});
}
}), [profile.settings]);
const hardwareDecodingCheckbox = React.useMemo(() => ({
checked: profile.settings.hardwareDecoding,
onClick: () => {
@ -266,10 +270,10 @@ const useProfileSettingsInputs = (profile) => {
subtitlesOutlineColorInput,
audioLanguageSelect,
seekTimeDurationSelect,
playInExternalPlayerSelect,
nextVideoPopupDurationSelect,
bingeWatchingCheckbox,
playInBackgroundCheckbox,
playInExternalPlayerCheckbox,
hardwareDecodingCheckbox,
streamingServerUrlInput
};