Merge pull request #183 from Stremio/streaming-server-middleware

Streaming server middleware
This commit is contained in:
Nikola Hristov 2020-07-15 14:32:26 +03:00 committed by GitHub
commit c8157be70e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 645 additions and 2457 deletions

View file

@ -10,7 +10,9 @@
},
"globals": {
"YT": "readonly",
"FB": "readonly"
"FB": "readonly",
"cast": "readonly",
"chrome": "readonly"
},
"env": {
"node": true,

1
.npmrc
View file

@ -1 +0,0 @@
@stremio:registry=https://npm.pkg.github.com

View file

@ -15,11 +15,11 @@
},
"dependencies": {
"@sentry/browser": "5.11.1",
"@stremio/stremio-core-web": "0.12.0",
"@stremio/stremio-core-web": "0.16.0",
"@stremio/stremio-video": "0.0.4",
"a-color-picker": "1.2.1",
"classnames": "2.2.6",
"events": "1.1.1",
"hat": "0.0.3",
"lodash.debounce": "4.0.8",
"lodash.isequal": "4.5.0",
"lodash.throttle": "4.1.1",
@ -30,8 +30,7 @@
"react-focus-lock": "2.2.1",
"spatial-navigation-polyfill": "git+https://git@github.com/Stremio/spatial-navigation.git#40204ad9942fe786794c62f99ea5ab2b52b24096",
"stremio-colors": "git+https://git@github.com/Stremio/stremio-colors.git#v3.0.0",
"stremio-icons": "git+https://git@github.com/Stremio/stremio-icons.git#v2.0.2",
"vtt.js": "0.13.0"
"stremio-icons": "git+https://git@github.com/Stremio/stremio-icons.git#v2.0.2"
},
"devDependencies": {
"@babel/core": "7.8.7",
@ -68,4 +67,4 @@
"webpack-cli": "3.3.11",
"webpack-dev-server": "3.10.3"
}
}
}

View file

@ -3,9 +3,9 @@
require('spatial-navigation-polyfill');
const React = require('react');
const { Router } = require('stremio-router');
const { Core, KeyboardNavigation, ServicesProvider, Shell } = require('stremio/services');
const { Core, Shell, Chromecast, KeyboardShortcuts, ServicesProvider } = require('stremio/services');
const { NotFound } = require('stremio/routes');
const { ToastProvider } = require('stremio/common');
const { ToastProvider, CONSTANTS } = require('stremio/common');
const CoreEventsToaster = require('./CoreEventsToaster');
const routerViewsConfig = require('./routerViewsConfig');
const styles = require('./styles');
@ -15,47 +15,62 @@ const App = () => {
return NotFound;
}, []);
const services = React.useMemo(() => ({
keyboardNavigation: new KeyboardNavigation(),
core: new Core(),
shell: new Shell(),
core: new Core()
chromecast: new Chromecast(),
keyboardShortcuts: new KeyboardShortcuts()
}), []);
const [shellInitialized, setShellInitialized] = React.useState(false);
const [coreInitialized, setCoreInitialized] = React.useState(false);
const [shellInitialized, setShellInitialized] = React.useState(false);
React.useEffect(() => {
const onShellStateChanged = () => {
setShellInitialized(services.shell.active || services.shell.error instanceof Error);
};
const onCoreStateChanged = () => {
if (services.core.active) {
services.core.dispatch({
services.core.transport.dispatch({
action: 'Load',
args: {
model: 'Ctx'
}
});
}
setCoreInitialized(services.core.active);
};
services.shell.on('stateChanged', onShellStateChanged);
const onShellStateChanged = () => {
setShellInitialized(services.shell.active || services.shell.error instanceof Error);
};
const onChromecastStateChange = () => {
if (services.chromecast.active) {
services.chromecast.transport.setOptions({
receiverApplicationId: CONSTANTS.CHROMECAST_RECEIVER_APP_ID,
autoJoinPolicy: chrome.cast.AutoJoinPolicy.PAGE_SCOPED,
resumeSavedSession: false,
language: null
});
}
};
services.core.on('stateChanged', onCoreStateChanged);
services.keyboardNavigation.start();
services.shell.start();
services.shell.on('stateChanged', onShellStateChanged);
services.chromecast.on('stateChanged', onChromecastStateChange);
services.core.start();
window.shell = services.shell;
window.core = services.core;
services.shell.start();
services.chromecast.start();
services.keyboardShortcuts.start();
window.services = services;
return () => {
services.keyboardNavigation.stop();
services.shell.stop();
services.core.stop();
services.shell.off('stateChanged', onShellStateChanged);
services.shell.stop();
services.chromecast.stop();
services.keyboardShortcuts.stop();
services.core.off('stateChanged', onCoreStateChanged);
services.shell.off('stateChanged', onShellStateChanged);
services.chromecast.off('stateChanged', onChromecastStateChange);
};
}, []);
return (
<React.StrictMode>
<ServicesProvider services={services}>
{
shellInitialized && coreInitialized ?
coreInitialized && shellInitialized ?
<ToastProvider className={styles['toasts-container']}>
<CoreEventsToaster />
<Router

View file

@ -19,9 +19,9 @@ const CoreEventsToaster = () => {
});
}
};
core.on('Event', onEvent);
core.transport.on('Event', onEvent);
return () => {
core.off('Event', onEvent);
core.transport.off('Event', onEvent);
};
}, []);
return null;

View file

@ -26,7 +26,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
}
};
const installOnClick = (event) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'InstallAddon',
@ -42,7 +42,7 @@ const AddonDetailsModal = ({ transportUrl, onCloseRequest }) => {
}
};
const uninstallOnClick = (event) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UninstallAddon',

View file

@ -1,5 +1,6 @@
// Copyright (C) 2017-2020 Smart code 203358507
const CHROMECAST_RECEIVER_APP_ID = '1634F54B';
const SUBTITLES_SIZES = [75, 100, 125, 150, 175, 200, 250];
const SUBTITLES_FONTS = ['Roboto', 'Arial', 'Halvetica', 'Times New Roman', 'Verdana', 'Courier', 'Lucida Console', 'sans-serif', 'serif', 'monospace'];
const CATALOG_PREVIEW_SIZE = 10;
@ -24,6 +25,7 @@ const TYPE_PRIORITIES = {
};
module.exports = {
CHROMECAST_RECEIVER_APP_ID,
SUBTITLES_SIZES,
SUBTITLES_FONTS,
CATALOG_PREVIEW_SIZE,

View file

@ -52,7 +52,7 @@ const LibItem = ({ id, ...props }) => {
}
case 'dismiss': {
if (typeof id === 'string') {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'RewindLibraryItem',

View file

@ -26,7 +26,7 @@ const NavMenu = (props) => {
event.nativeEvent.togglePopupPrevented = true;
}, []);
const logoutButtonOnClick = React.useCallback(() => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'Logout'

View file

@ -8,18 +8,18 @@ const useNotifications = () => {
const { core } = useServices();
React.useEffect(() => {
const onNewState = () => {
const state = core.getState();
const state = core.transport.getState();
setNotifications(state.notifications.groups);
};
core.on('NewModel', onNewState);
core.dispatch({
core.transport.on('NewModel', onNewState);
core.transport.dispatch({
action: 'Load',
args: {
load: 'Notifications'
}
});
return () => {
core.off('NewModel', onNewState);
core.transport.off('NewModel', onNewState);
};
}, []);
return notifications;

View file

@ -36,6 +36,7 @@ const useInLibrary = require('./useInLibrary');
const useLiveRef = require('./useLiveRef');
const useModelState = require('./useModelState');
const useProfile = require('./useProfile');
const useStreamingServer = require('./useStreamingServer');
module.exports = {
AddonDetailsModal,
@ -76,4 +77,5 @@ module.exports = {
useLiveRef,
useModelState,
useProfile,
useStreamingServer,
};

View file

@ -7,14 +7,14 @@ const useModelState = require('stremio/common/useModelState');
const useInLibrary = (metaItem) => {
const { core } = useServices();
const initLibraryItemsState = React.useCallback(() => {
return core.getState('library_items');
return core.transport.getState('library_items');
}, []);
const libraryItems = useModelState({
model: 'library_items',
init: initLibraryItemsState
});
const addToLibrary = React.useCallback((metaItem) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'AddToLibrary',
@ -23,7 +23,7 @@ const useInLibrary = (metaItem) => {
});
}, []);
const removeFromLibrary = React.useCallback((metaItem) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'RemoveFromLibrary',

View file

@ -13,26 +13,26 @@ const useModelState = ({ model, init, action, timeout, onNewState, map, mapWithC
const routeFocused = useRouteFocused();
const [state, setState] = useDeepEqualState(init);
React.useLayoutEffect(() => {
core.dispatch(action, modelRef.current);
core.transport.dispatch(action, modelRef.current);
}, [action]);
React.useLayoutEffect(() => {
return () => {
core.dispatch({ action: 'Unload' }, modelRef.current);
core.transport.dispatch({ action: 'Unload' }, modelRef.current);
};
}, []);
React.useLayoutEffect(() => {
const onNewStateThrottled = throttle(() => {
const state = core.getState(modelRef.current);
const state = core.transport.getState(modelRef.current);
if (typeof onNewState === 'function') {
const action = onNewState(state);
const handled = core.dispatch(action, modelRef.current);
const handled = core.transport.dispatch(action, modelRef.current);
if (handled) {
return;
}
}
if (typeof mapWithCtx === 'function') {
const ctx = core.getState('ctx');
const ctx = core.transport.getState('ctx');
setState(mapWithCtx(state, ctx));
} else if (typeof map === 'function') {
setState(map(state));
@ -41,14 +41,14 @@ const useModelState = ({ model, init, action, timeout, onNewState, map, mapWithC
}
}, timeout);
if (routeFocused) {
core.on('NewState', onNewStateThrottled);
core.transport.on('NewState', onNewStateThrottled);
if (mountedRef.current) {
onNewStateThrottled.call();
}
}
return () => {
onNewStateThrottled.cancel();
core.off('NewState', onNewStateThrottled);
core.transport.off('NewState', onNewStateThrottled);
};
}, [routeFocused, timeout, onNewState, map, mapWithCtx]);
React.useLayoutEffect(() => {

View file

@ -11,7 +11,7 @@ const mapProfileState = (ctx) => {
const useProfile = () => {
const { core } = useServices();
const initProfileState = React.useCallback(() => {
const ctx = core.getState('ctx');
const ctx = core.transport.getState('ctx');
return mapProfileState(ctx);
}, []);
const profile = useModelState({

View file

@ -2,15 +2,15 @@
const React = require('react');
const { useServices } = require('stremio/services');
const { useModelState } = require('stremio/common');
const useModelState = require('stremio/common/useModelState');
const useStreamingServer = () => {
const { core } = useServices();
const initStreamingServer = React.useCallback(() => {
return core.getState('streaming_server');
return core.transport.getState('streaming_server');
}, []);
const loadStreamingServerAction = React.useMemo(() => {
const streamingServer = core.getState('streaming_server');
const streamingServer = core.transport.getState('streaming_server');
if (streamingServer.selected === null) {
return {
action: 'StreamingServer',

View file

@ -16,6 +16,7 @@
<script type="text/javascript">
<%= compilation.assets['main.js'].source() %>
</script>
<script src="//www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1"></script>
</body>
</html>

View file

@ -126,7 +126,7 @@ const useAddons = (urlParams) => {
}
};
} else {
const addons = core.getState('addons');
const addons = core.transport.getState('addons');
if (addons.selectable.catalogs.length > 0) {
return {
action: 'Load',

View file

@ -24,7 +24,7 @@ const mapContinueWatchingPreviewState = (continue_watching_preview) => {
const useContinueWatchingPreview = () => {
const { core } = useServices();
const initContinueWatchingPreviewState = React.useMemo(() => {
return mapContinueWatchingPreviewState(core.getState('continue_watching_preview'));
return mapContinueWatchingPreviewState(core.transport.getState('continue_watching_preview'));
}, []);
return useModelState({
model: 'continue_watching_preview',

View file

@ -90,7 +90,7 @@ const useDiscover = (urlParams, queryParams) => {
}
};
} else {
const discover = core.getState('discover');
const discover = core.transport.getState('discover');
if (discover.selectable.types.length > 0) {
return {
action: 'Load',

View file

@ -92,7 +92,7 @@ const Intro = ({ queryParams }) => {
if (!user || typeof user.fbLoginToken !== 'string' || typeof user.email !== 'string') {
throw new Error('Login failed at getting token from Stremio');
}
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'Authenticate',
@ -123,7 +123,7 @@ const Intro = ({ queryParams }) => {
return;
}
openLoaderModal();
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'Authenticate',
@ -140,7 +140,7 @@ const Intro = ({ queryParams }) => {
dispatch({ type: 'error', error: 'You must accept the Terms of Service' });
return;
}
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'Logout'
@ -170,7 +170,7 @@ const Intro = ({ queryParams }) => {
return;
}
openLoaderModal();
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'Authenticate',
@ -270,10 +270,10 @@ const Intro = ({ queryParams }) => {
}
};
if (routeFocused) {
core.on('Event', onEvent);
core.transport.on('Event', onEvent);
}
return () => {
core.off('Event', onEvent);
core.transport.off('Event', onEvent);
};
}, [routeFocused]);
React.useEffect(() => {

View file

@ -5,6 +5,7 @@ const PropTypes = require('prop-types');
const classnames = require('classnames');
const Icon = require('stremio-icons/dom');
const { Button } = require('stremio/common');
const { useServices } = require('stremio/services');
const SeekBar = require('./SeekBar');
const VolumeSlider = require('./VolumeSlider');
const styles = require('./styles');
@ -28,6 +29,8 @@ const ControlBar = ({
onToggleInfoMenu,
...props
}) => {
const { chromecast } = useServices();
const [chromecastServiceActive, setChromecastServiceActive] = React.useState(() => chromecast.active);
const onSubtitlesButtonMouseDown = React.useCallback((event) => {
event.nativeEvent.subtitlesMenuClosePrevented = true;
}, []);
@ -66,6 +69,18 @@ const ControlBar = ({
onToggleInfoMenu();
}
}, [onToggleInfoMenu]);
const onChromecastButtonClick = React.useCallback(() => {
chromecast.transport.requestSession();
}, []);
React.useEffect(() => {
const onStateChanged = () => {
setChromecastServiceActive(chromecast.active);
};
chromecast.on('stateChanged', onStateChanged);
return () => {
chromecast.off('stateChanged', onStateChanged);
};
}, []);
return (
<div {...props} className={classnames(className, styles['control-bar-container'])}>
<SeekBar
@ -102,7 +117,7 @@ const ControlBar = ({
<Button className={classnames(styles['control-bar-button'], { 'disabled': typeof info !== 'object' || info === null })} tabIndex={-1} onMouseDown={onInfoButtonMouseDown} onClick={onInfoButtonClick}>
<Icon className={styles['icon']} icon={'ic_info'} />
</Button>
<Button className={classnames(styles['control-bar-button'], 'disabled')} tabIndex={-1}>
<Button className={classnames(styles['control-bar-button'], { 'disabled': !chromecastServiceActive })} tabIndex={-1} onClick={onChromecastButtonClick}>
<Icon className={styles['icon']} icon={'ic_cast'} />
</Button>
<Button className={classnames(styles['control-bar-button'], { 'disabled': !Array.isArray(subtitlesTracks) || subtitlesTracks.length === 0 })} tabIndex={-1} onMouseDown={onSubtitlesButtonMouseDown} onClick={onSubtitlesButtonClick}>

View file

@ -6,7 +6,7 @@ const classnames = require('classnames');
const debounce = require('lodash.debounce');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { HorizontalNavBar, useDeepEqualEffect, useFullscreen, useBinaryState, useToast, useProfile } = require('stremio/common');
const { HorizontalNavBar, useDeepEqualEffect, useFullscreen, useBinaryState, useToast, useProfile, useStreamingServer } = require('stremio/common');
const BufferingLoader = require('./BufferingLoader');
const ControlBar = require('./ControlBar');
const InfoMenu = require('./InfoMenu');
@ -18,14 +18,18 @@ const useSettings = require('./useSettings');
const styles = require('./styles');
const Player = ({ urlParams }) => {
const { core } = useServices();
const { core, chromecast } = useServices();
const profile = useProfile();
const [player, updateLibraryItemState, pushToLibrary] = usePlayer(urlParams);
const [settings, updateSettings] = useSettings(profile);
const streamingServer = useStreamingServer();
const info = useInfo(player, profile);
const routeFocused = useRouteFocused();
const toast = useToast();
const [, , , toggleFullscreen] = useFullscreen();
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 [subtitlesMenuOpen, , closeSubtitlesMenu, toggleSubtitlesMenu] = useBinaryState(false);
@ -58,21 +62,21 @@ const Player = ({ urlParams }) => {
}, []);
const onImplementationChanged = React.useCallback((manifest) => {
manifest.props.forEach((propName) => {
dispatch({ observedPropName: propName });
dispatch({ type: 'observeProp', propName });
});
dispatch({ propName: 'subtitlesSize', propValue: settings.subtitles_size });
dispatch({ propName: 'subtitlesTextColor', propValue: settings.subtitles_text_color });
dispatch({ propName: 'subtitlesBackgroundColor', propValue: settings.subtitles_background_color });
dispatch({ propName: 'subtitlesOutlineColor', propValue: settings.subtitles_outline_color });
dispatch({ propName: 'subtitlesOffset', propValue: settings.subtitles_offset });
dispatch({ type: 'setProp', propName: 'subtitlesSize', propValue: settings.subtitles_size });
dispatch({ type: 'setProp', propName: 'subtitlesTextColor', propValue: settings.subtitles_text_color });
dispatch({ type: 'setProp', propName: 'subtitlesBackgroundColor', propValue: settings.subtitles_background_color });
dispatch({ type: 'setProp', propName: 'subtitlesOutlineColor', propValue: settings.subtitles_outline_color });
dispatch({ type: 'setProp', propName: 'subtitlesOffset', propValue: settings.subtitles_offset });
}, [settings.subtitles_size, settings.subtitles_text_color, settings.subtitles_background_color, settings.subtitles_outline_color, settings.subtitles_offset]);
const onPropChanged = React.useCallback((propName, propValue) => {
setVideoState({ [propName]: propValue });
}, []);
const onEnded = React.useCallback(() => {
core.dispatch({ action: 'Unload' }, 'player');
core.transport.dispatch({ action: 'Unload' }, 'player');
if (player.lib_item !== null) {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'RewindLibraryItem',
@ -106,30 +110,30 @@ const Player = ({ urlParams }) => {
});
}, []);
const onPlayRequested = React.useCallback(() => {
dispatch({ propName: 'paused', propValue: false });
dispatch({ type: 'setProp', propName: 'paused', propValue: false });
}, []);
const onPlayRequestedDebounced = React.useCallback(debounce(onPlayRequested, 200), []);
const onPauseRequested = React.useCallback(() => {
dispatch({ propName: 'paused', propValue: true });
dispatch({ type: 'setProp', propName: 'paused', propValue: true });
}, []);
const onPauseRequestedDebounced = React.useCallback(debounce(onPauseRequested, 200), []);
const onMuteRequested = React.useCallback(() => {
dispatch({ propName: 'muted', propValue: true });
dispatch({ type: 'setProp', propName: 'muted', propValue: true });
}, []);
const onUnmuteRequested = React.useCallback(() => {
dispatch({ propName: 'muted', propValue: false });
dispatch({ type: 'setProp', propName: 'muted', propValue: false });
}, []);
const onVolumeChangeRequested = React.useCallback((volume) => {
dispatch({ propName: 'volume', propValue: volume });
dispatch({ type: 'setProp', propName: 'volume', propValue: volume });
}, []);
const onSeekRequested = React.useCallback((time) => {
dispatch({ propName: 'time', propValue: time });
dispatch({ type: 'setProp', propName: 'time', propValue: time });
}, []);
const onSubtitlesTrackSelected = React.useCallback((trackId) => {
dispatch({ propName: 'selectedSubtitlesTrackId', propValue: trackId });
dispatch({ type: 'setProp', propName: 'selectedSubtitlesTrackId', propValue: trackId });
}, []);
const onSubtitlesDelayChanged = React.useCallback((delay) => {
dispatch({ propName: 'subtitlesDelay', propValue: delay });
dispatch({ type: 'setProp', propName: 'subtitlesDelay', propValue: delay });
}, []);
const onSubtitlesSizeChanged = React.useCallback((size) => {
updateSettings({ subtitles_size: size });
@ -177,22 +181,26 @@ const Player = ({ urlParams }) => {
useDeepEqualEffect(() => {
setError(null);
if (player.selected === null) {
dispatch({ commandName: 'stop' });
} else {
dispatch({ type: 'command', commandName: 'unload' });
} else if (streamingServer.base_url !== null && streamingServer.base_url.type !== 'Loading') {
dispatch({
type: 'command',
commandName: 'load',
commandArgs: {
stream: player.selected.stream,
streamingServerUrl: settings.streaming_server_url,
autoplay: true,
time: player.lib_item !== null && player.selected.video_id !== null && player.lib_item.state.video_id === player.selected.video_id ?
player.lib_item.state.timeOffset
:
0
0,
transcode: casting,
streamingServerURL: typeof streamingServer.base_url.content === 'string' ? streamingServer.base_url.content : null,
chromecastTransport: chromecast.transport
}
});
if (Array.isArray(player.selected.stream.subtitles)) {
dispatch({
type: 'command',
commandName: 'addSubtitlesTracks',
commandArgs: {
tracks: player.selected.stream.subtitles.map(({ url, lang }) => ({
@ -204,9 +212,10 @@ const Player = ({ urlParams }) => {
});
}
}
}, [player.selected]);
}, [streamingServer.base_url, player.selected, casting]);
useDeepEqualEffect(() => {
dispatch({
type: 'command',
commandName: 'addSubtitlesTracks',
commandArgs: {
tracks: player.subtitles_resources
@ -220,21 +229,21 @@ const Player = ({ urlParams }) => {
}, [])
}
});
}, [player.selected, player.subtitles_resources]);
}, [streamingServer.base_url, player.subtitles_resources, player.selected, casting]);
React.useEffect(() => {
dispatch({ propName: 'subtitlesSize', propValue: settings.subtitles_size });
dispatch({ type: 'setProp', propName: 'subtitlesSize', propValue: settings.subtitles_size });
}, [settings.subtitles_size]);
React.useEffect(() => {
dispatch({ propName: 'subtitlesTextColor', propValue: settings.subtitles_text_color });
dispatch({ type: 'setProp', propName: 'subtitlesTextColor', propValue: settings.subtitles_text_color });
}, [settings.subtitles_text_color]);
React.useEffect(() => {
dispatch({ propName: 'subtitlesBackgroundColor', propValue: settings.subtitles_background_color });
dispatch({ type: 'setProp', propName: 'subtitlesBackgroundColor', propValue: settings.subtitles_background_color });
}, [settings.subtitles_background_color]);
React.useEffect(() => {
dispatch({ propName: 'subtitlesOutlineColor', propValue: settings.subtitles_outline_color });
dispatch({ type: 'setProp', propName: 'subtitlesOutlineColor', propValue: settings.subtitles_outline_color });
}, [settings.subtitles_outline_color]);
React.useEffect(() => {
dispatch({ propName: 'subtitlesOffset', propValue: settings.subtitles_offset });
dispatch({ type: 'setProp', propName: 'subtitlesOffset', propValue: settings.subtitles_offset });
}, [settings.subtitles_offset]);
React.useEffect(() => {
if (videoState.time !== null && !isNaN(videoState.time) && videoState.duration !== null && !isNaN(videoState.duration)) {
@ -257,6 +266,31 @@ const Player = ({ urlParams }) => {
clearInterval(intervalId);
};
}, []);
React.useEffect(() => {
const onCastStateChange = () => {
setCasting(chromecast.active && chromecast.transport.getCastState() === cast.framework.CastState.CONNECTED);
};
const onChromecastStateChange = () => {
if (chromecast.active) {
chromecast.transport.on(
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
onCastStateChange
);
onCastStateChange();
}
};
chromecast.on('stateChanged', onChromecastStateChange);
onChromecastStateChange();
return () => {
chromecast.off('stateChanged', onChromecastStateChange);
if (chromecast.active) {
chromecast.transport.off(
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
onCastStateChange
);
}
};
}, []);
React.useLayoutEffect(() => {
const onKeyDown = (event) => {
switch (event.code) {
@ -337,7 +371,7 @@ const Player = ({ urlParams }) => {
};
}, []);
return (
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen })}
<div className={classnames(styles['player-container'], { [styles['immersed']]: immersed && !casting && videoState.paused !== null && !videoState.paused && !subtitlesMenuOpen && !infoMenuOpen })}
onMouseDown={onContainerMouseDown}
onMouseMove={onContainerMouseMove}
onMouseOver={onContainerMouseMove}

View file

@ -2,9 +2,10 @@
const React = require('react');
const PropTypes = require('prop-types');
const hat = require('hat');
const classnames = require('classnames');
const { useLiveRef } = require('stremio/common');
const selectVideoImplementation = require('./selectVideoImplementation');
const styles = require('./styles');
const Video = React.forwardRef(({ className, ...props }, ref) => {
const onEndedRef = useLiveRef(props.onEnded);
@ -13,20 +14,16 @@ const Video = React.forwardRef(({ className, ...props }, ref) => {
const onPropChangedRef = useLiveRef(props.onPropChanged);
const onSubtitlesTrackLoadedRef = useLiveRef(props.onSubtitlesTrackLoaded);
const onImplementationChangedRef = useLiveRef(props.onImplementationChanged);
const containerElementRef = React.useRef(null);
const videoElementRef = React.useRef(null);
const videoRef = React.useRef(null);
const id = React.useMemo(() => `video-${hat()}`, []);
const dispatch = React.useCallback((args) => {
if (args && args.commandName === 'load' && args.commandArgs) {
const Video = selectVideoImplementation(args.commandArgs.shell, args.commandArgs.stream);
if (typeof Video !== 'function') {
videoRef.current = null;
} else if (videoRef.current === null || videoRef.current.constructor !== Video) {
dispatch({ commandName: 'destroy' });
const dispatch = React.useCallback((action) => {
if (action && action.type === 'command' && action.commandName === 'load' && action.commandArgs) {
const Video = selectVideoImplementation(action.commandArgs);
if (videoRef.current === null || videoRef.current.constructor !== Video) {
dispatch({ type: 'command', commandName: 'destroy' });
videoRef.current = new Video({
id: id,
containerElement: containerElementRef.current,
shell: args.commandArgs.shell
...action.commandArgs,
containerElement: videoElementRef.current
});
videoRef.current.on('ended', () => {
if (typeof onEndedRef.current === 'function') {
@ -61,21 +58,23 @@ const Video = React.forwardRef(({ className, ...props }, ref) => {
if (videoRef.current !== null) {
try {
videoRef.current.dispatch(args);
} catch (e) {
videoRef.current.dispatch(action);
} catch (error) {
// eslint-disable-next-line no-console
console.error(videoRef.current.constructor.manifest.name, e);
console.error(videoRef.current.constructor.manifest.name, error);
}
}
}, []);
React.useImperativeHandle(ref, () => ({ dispatch }), []);
React.useEffect(() => {
return () => {
dispatch({ commandName: 'destroy' });
dispatch({ type: 'command', commandName: 'destroy' });
};
}, []);
return (
<div ref={containerElementRef} id={id} className={className} />
<div className={classnames(className, styles['video-container'])}>
<div ref={videoElementRef} className={styles['video']} />
</div>
);
});

View file

@ -1,23 +1,25 @@
// Copyright (C) 2017-2020 Smart code 203358507
const { HTMLVideo, YouTubeVideo, MPVVideo, withStreamingServer } = require('stremio-video');
const { ChromecastVideo, HTMLVideo, YouTubeVideo, withStreamingServer, withHTMLSubtitles } = require('@stremio/stremio-video');
const selectVideoImplementation = (shell, stream) => {
if (shell) {
return MPVVideo;
const selectVideoImplementation = (args) => {
// TODO handle stream.behaviorHints
// TODO handle IFrameVideo
// TODO handle MPVVideo
if (args.chromecastTransport && args.chromecastTransport.getCastState() === cast.framework.CastState.CONNECTED) {
return ChromecastVideo;
}
if (stream) {
if (typeof stream.url === 'string') {
return HTMLVideo;
} else if (typeof stream.ytId === 'string') {
return YouTubeVideo;
} else if (typeof stream.infoHash === 'string') {
return withStreamingServer(HTMLVideo);
}
if (args.stream && typeof args.stream.ytId === 'string') {
return withHTMLSubtitles(YouTubeVideo);
}
return null;
if (typeof args.streamingServerURL === 'string') {
return withHTMLSubtitles(withStreamingServer(HTMLVideo));
}
return withHTMLSubtitles(HTMLVideo);
};
module.exports = selectVideoImplementation;

View file

@ -0,0 +1,10 @@
.video-container {
.video {
width: 100%;
height: 100%;
* {
font-size: inherit;
}
}
}

View file

@ -43,6 +43,7 @@ html:not(.active-slider-within) {
flex-direction: row;
align-items: center;
justify-content: center;
background-color: @color-background-dark5;
.error-label {
flex: 0 1 auto;

View file

@ -135,7 +135,7 @@ const usePlayer = (urlParams) => {
}
}, [urlParams]);
const updateLibraryItemState = React.useCallback((time, duration) => {
core.dispatch({
core.transport.dispatch({
action: 'Player',
args: {
action: 'UpdateLibraryItemState',
@ -144,7 +144,7 @@ const usePlayer = (urlParams) => {
}, 'player');
}, []);
const pushToLibrary = React.useCallback(() => {
core.dispatch({
core.transport.dispatch({
action: 'Player',
args: {
action: 'PushToLibrary'

View file

@ -6,7 +6,7 @@ const { useServices } = require('stremio/services');
const useSettings = (profile) => {
const { core } = useServices();
const updateSettings = React.useCallback((settings) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',

View file

@ -6,8 +6,7 @@ const throttle = require('lodash.throttle');
const Icon = require('stremio-icons/dom');
const { useRouteFocused } = require('stremio-router');
const { useServices } = require('stremio/services');
const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, useProfile } = require('stremio/common');
const useStreamingServer = require('./useStreamingServer');
const { Button, Checkbox, MainNavBars, Multiselect, ColorInput, useProfile, useStreamingServer } = require('stremio/common');
const useProfileSettingsInputs = require('./useProfileSettingsInputs');
const useStreamingServerSettingsInputs = require('./useStreamingServerSettingsInputs');
const styles = require('./styles');
@ -38,7 +37,7 @@ const Settings = () => {
torrentProfileSelect
} = useStreamingServerSettingsInputs(streamingServer);
const logoutButtonOnClick = React.useCallback(() => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'Logout'
@ -58,7 +57,7 @@ const Settings = () => {
// TODO
}, []);
const reloadStreamingServer = React.useCallback(() => {
core.dispatch({
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'Reload'

View file

@ -18,7 +18,7 @@ const useProfileSettingsInputs = (profile) => {
profile.settings.interface_language;
},
onSelect: (event) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
@ -43,7 +43,7 @@ const useProfileSettingsInputs = (profile) => {
profile.settings.subtitles_language;
},
onSelect: (event) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
@ -65,7 +65,7 @@ const useProfileSettingsInputs = (profile) => {
return `${profile.settings.subtitles_size}%`;
},
onSelect: (event) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
@ -80,7 +80,7 @@ const useProfileSettingsInputs = (profile) => {
const subtitlesTextColorInput = useDeepEqualMemo(() => ({
value: profile.settings.subtitles_text_color,
onChange: (event) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
@ -95,7 +95,7 @@ const useProfileSettingsInputs = (profile) => {
const subtitlesBackgroundColorInput = useDeepEqualMemo(() => ({
value: profile.settings.subtitles_background_color,
onChange: (event) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
@ -110,7 +110,7 @@ const useProfileSettingsInputs = (profile) => {
const subtitlesOutlineColorInput = useDeepEqualMemo(() => ({
value: profile.settings.subtitles_outline_color,
onChange: (event) => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
@ -125,7 +125,7 @@ const useProfileSettingsInputs = (profile) => {
const bingeWatchingCheckbox = useDeepEqualMemo(() => ({
checked: profile.settings.binge_watching,
onClick: () => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
@ -140,7 +140,7 @@ const useProfileSettingsInputs = (profile) => {
const playInBackgroundCheckbox = useDeepEqualMemo(() => ({
checked: profile.settings.play_in_background,
onClick: () => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
@ -155,7 +155,7 @@ const useProfileSettingsInputs = (profile) => {
const playInExternalPlayerCheckbox = useDeepEqualMemo(() => ({
checked: profile.settings.play_in_external_player,
onClick: () => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',
@ -170,7 +170,7 @@ const useProfileSettingsInputs = (profile) => {
const hardwareDecodingCheckbox = useDeepEqualMemo(() => ({
checked: profile.settings.hardware_decoding,
onClick: () => {
core.dispatch({
core.transport.dispatch({
action: 'Ctx',
args: {
action: 'UpdateSettings',

View file

@ -60,7 +60,7 @@ const useStreaminServerSettingsInputs = (streaminServer) => {
return cacheSizeToString(streaminServer.settings.content.cacheSize);
},
onSelect: (event) => {
core.dispatch({
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'UpdateSettings',
@ -115,7 +115,7 @@ const useStreaminServerSettingsInputs = (streaminServer) => {
}, 'custom');
},
onSelect: (event) => {
core.dispatch({
core.transport.dispatch({
action: 'StreamingServer',
args: {
action: 'UpdateSettings',

View file

@ -0,0 +1,102 @@
// Copyright (C) 2017-2020 Smart code 203358507
const EventEmitter = require('events');
const ChromecastTransport = require('./ChromecastTransport');
let castAPIAvailable = null;
const castAPIEvents = new EventEmitter();
window['__onGCastApiAvailable'] = function(available) {
delete window['__onGCastApiAvailable'];
castAPIAvailable = available;
castAPIEvents.emit('availabilityChanged');
};
function Chromecast() {
let active = false;
let error = null;
let starting = false;
let transport = null;
const events = new EventEmitter();
events.on('error', () => { });
function onCastAPIAvailabilityChanged() {
if (castAPIAvailable) {
active = true;
error = null;
starting = false;
transport = new ChromecastTransport();
} else {
active = false;
error = new Error('Google Cast API not available');
starting = false;
transport = null;
}
onStateChanged();
}
function onStateChanged() {
events.emit('stateChanged');
}
Object.defineProperties(this, {
active: {
configurable: false,
enumerable: true,
get: function() {
return active;
}
},
error: {
configurable: false,
enumerable: true,
get: function() {
return error;
}
},
starting: {
configurable: false,
enumerable: true,
get: function() {
return starting;
}
},
transport: {
configurable: false,
enumerable: true,
get: function() {
return transport;
}
}
});
this.start = function() {
if (active || error instanceof Error || starting) {
return;
}
starting = true;
if (castAPIAvailable !== null) {
onCastAPIAvailabilityChanged();
} else {
castAPIEvents.on('availabilityChanged', onCastAPIAvailabilityChanged);
onStateChanged();
}
};
this.stop = function() {
castAPIEvents.off('availabilityChanged', onCastAPIAvailabilityChanged);
active = false;
error = null;
starting = false;
transport = null;
onStateChanged();
};
this.on = function(name, listener) {
events.on(name, listener);
};
this.off = function(name, listener) {
events.off(name, listener);
};
}
module.exports = Chromecast;

View file

@ -0,0 +1,104 @@
// Copyright (C) 2017-2020 Smart code 203358507
const EventEmitter = require('events');
const MESSAGE_NAMESPACE = 'urn:x-cast:com.stremio';
function ChromecastTransport() {
const events = new EventEmitter();
events.on('error', () => { });
cast.framework.CastContext.getInstance().addEventListener(
cast.framework.CastContextEventType.CAST_STATE_CHANGED,
onCastStateChanged
);
cast.framework.CastContext.getInstance().addEventListener(
cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
onSesstionStateChanged
);
function onMessage(_, message) {
events.emit('message', message);
}
function onApplicationStatusChanged(event) {
events.emit(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, event);
}
function onApplicationMetadataChanged(event) {
events.emit(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, event);
}
function onActiveInputStateChanged(event) {
events.emit(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, event);
}
function onVolumeChanged(event) {
events.emit(cast.framework.CastSession.VOLUME_CHANGED, event);
}
function onMediaSessionChanged(event) {
events.emit(cast.framework.CastSession.MEDIA_SESSION, event);
}
function onCastStateChanged(event) {
events.emit(cast.framework.CastContextEventType.CAST_STATE_CHANGED, event);
}
function onSesstionStateChanged(event) {
events.emit(cast.framework.CastContextEventType.SESSION_STATE_CHANGED, event);
switch (event.sessionState) {
case cast.framework.SessionState.SESSION_STARTED: {
event.session.addMessageListener(MESSAGE_NAMESPACE, onMessage);
event.session.addEventListener(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, onApplicationStatusChanged);
event.session.addEventListener(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, onApplicationMetadataChanged);
event.session.addEventListener(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, onActiveInputStateChanged);
event.session.addEventListener(cast.framework.CastSession.VOLUME_CHANGED, onVolumeChanged);
event.session.addEventListener(cast.framework.CastSession.MEDIA_SESSION, onMediaSessionChanged);
break;
}
case cast.framework.SessionState.SESSION_ENDED: {
event.session.removeMessageListener(MESSAGE_NAMESPACE, onMessage);
event.session.removeEventListener(cast.framework.CastSession.APPLICATION_STATUS_CHANGED, onApplicationStatusChanged);
event.session.removeEventListener(cast.framework.CastSession.APPLICATION_METADATA_CHANGED, onApplicationMetadataChanged);
event.session.removeEventListener(cast.framework.CastSession.ACTIVE_INPUT_STATE_CHANGED, onActiveInputStateChanged);
event.session.removeEventListener(cast.framework.CastSession.VOLUME_CHANGED, onVolumeChanged);
event.session.removeEventListener(cast.framework.CastSession.MEDIA_SESSION, onMediaSessionChanged);
break;
}
}
}
this.on = function(name, listener) {
events.on(name, listener);
};
this.off = function(name, listener) {
events.off(name, listener);
};
this.getCastState = function() {
return cast.framework.CastContext.getInstance().getCastState();
};
this.getSessionState = function() {
return cast.framework.CastContext.getInstance().getSessionState();
};
this.getCastDevice = function() {
const session = cast.framework.CastContext.getInstance().getCurrentSession();
if (session !== null) {
return session.getCastDevice();
}
return null;
};
this.setOptions = function(options) {
cast.framework.CastContext.getInstance().setOptions(options);
};
this.requestSession = function() {
return cast.framework.CastContext.getInstance().requestSession();
};
this.endCurrentSession = function(stopCasting) {
cast.framework.CastContext.getInstance().endCurrentSession(stopCasting);
};
this.sendMessage = function(message) {
const castSession = cast.framework.CastContext.getInstance().getCurrentSession();
if (castSession !== null) {
return castSession.sendMessage(MESSAGE_NAMESPACE, message);
} else {
return Promise.reject(new Error('Session not started'));
}
};
}
module.exports = ChromecastTransport;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2020 Smart code 203358507
const Chromecast = require('./Chromecast');
module.exports = Chromecast;

View file

@ -1,77 +1,47 @@
// Copyright (C) 2017-2020 Smart code 203358507
const EventEmitter = require('events');
const { default: init, StremioCoreWeb } = require('@stremio/stremio-core-web');
const { default: initializeCoreAPI } = require('@stremio/stremio-core-web');
const CoreTransport = require('./CoreTransport');
let coreAPIAvailable = null;
const coreAPIEvents = new EventEmitter();
initializeCoreAPI()
.then(() => {
coreAPIAvailable = true;
coreAPIEvents.emit('availabilityChanged');
})
.catch(() => {
coreAPIAvailable = false;
coreAPIEvents.emit('availabilityChanged');
});
function Core() {
let active = false;
let error = null;
let starting = false;
let stremio_core = null;
let transport = null;
const events = new EventEmitter();
events.on('error', () => { });
function onStateChanged() {
events.emit('stateChanged');
}
function start() {
if (active || error instanceof Error || starting) {
return;
function onCoreAPIAvailabilityChanged() {
if (coreAPIAvailable) {
active = true;
error = null;
starting = false;
transport = new CoreTransport();
} else {
active = false;
error = new Error('Stremio Core API not available');
starting = false;
transport = null;
}
starting = true;
init()
.then(() => {
if (starting) {
stremio_core = new StremioCoreWeb(({ name, args } = {}) => {
if (active) {
try {
events.emit(name, args);
} catch (e) {
/* eslint-disable-next-line no-console */
console.error(e);
}
}
});
active = true;
onStateChanged();
}
})
.catch((e) => {
error = new Error('Unable to init stremio-core-web');
error.error = e;
onStateChanged();
})
.then(() => {
starting = false;
});
}
function stop() {
active = false;
error = null;
starting = false;
stremio_core = null;
onStateChanged();
}
function on(name, listener) {
events.on(name, listener);
}
function off(name, listener) {
events.off(name, listener);
}
function dispatch(action, model) {
if (!active || typeof action === 'undefined') {
return false;
}
return stremio_core.dispatch(action, model);
}
function getState(model) {
if (!active) {
return null;
}
return stremio_core.get_state(model);
function onStateChanged() {
events.emit('stateChanged');
}
Object.defineProperties(this, {
@ -88,17 +58,54 @@ function Core() {
get: function() {
return error;
}
},
starting: {
configurable: false,
enumerable: true,
get: function() {
return starting;
}
},
transport: {
configurable: false,
enumerable: true,
get: function() {
return transport;
}
}
});
this.start = start;
this.stop = stop;
this.on = on;
this.off = off;
this.dispatch = dispatch;
this.getState = getState;
this.start = function() {
if (active || error instanceof Error || starting) {
return;
}
Object.freeze(this);
starting = true;
if (coreAPIAvailable !== null) {
onCoreAPIAvailabilityChanged();
} else {
coreAPIEvents.on('availabilityChanged', onCoreAPIAvailabilityChanged);
onStateChanged();
}
};
this.stop = function() {
coreAPIEvents.off('availabilityChanged', onCoreAPIAvailabilityChanged);
active = false;
error = null;
starting = false;
if (transport !== null) {
transport.free();
transport = null;
}
onStateChanged();
};
this.on = function(name, listener) {
events.on(name, listener);
};
this.off = function(name, listener) {
events.off(name, listener);
};
}
module.exports = Core;

View file

@ -0,0 +1,36 @@
// Copyright (C) 2017-2020 Smart code 203358507
const EventEmitter = require('events');
const { StremioCoreWeb } = require('@stremio/stremio-core-web');
function CoreTransport() {
const events = new EventEmitter();
events.on('error', () => { });
const core = new StremioCoreWeb(({ name, args }) => {
try {
events.emit(name, args);
} catch (error) {
/* eslint-disable-next-line no-console */
console.error(error);
}
});
this.on = function(name, listener) {
events.on(name, listener);
};
this.off = function(name, listener) {
events.off(name, listener);
};
this.getState = function(model) {
return core.get_state(model);
};
this.dispatch = function(action, model) {
return core.dispatch(action, model);
};
this.free = function() {
core.free();
};
}
module.exports = CoreTransport;

View file

@ -1,5 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
const KeyboardNavigation = require('./KeyboardNavigation');
module.exports = KeyboardNavigation;

View file

@ -1,10 +1,15 @@
// Copyright (C) 2017-2020 Smart code 203358507
function KeyboardNavigation() {
const EventEmitter = require('events');
function KeyboardShortcuts() {
let active = false;
const events = new EventEmitter();
events.on('error', () => { });
function onKeyDown(event) {
if (event.keyboardNavigationPrevented || event.target.tagName === 'INPUT') {
if (event.keyboardShortcutPrevented || event.target.tagName === 'INPUT') {
return;
}
@ -61,17 +66,8 @@ function KeyboardNavigation() {
}
}
}
function start() {
if (active) {
return;
}
window.addEventListener('keydown', onKeyDown);
active = true;
}
function stop() {
window.removeEventListener('keydown', onKeyDown);
active = false;
function onStateChanged() {
events.emit('stateChanged');
}
Object.defineProperties(this, {
@ -84,10 +80,20 @@ function KeyboardNavigation() {
}
});
this.start = start;
this.stop = stop;
this.start = function() {
if (active) {
return;
}
Object.freeze(this);
window.addEventListener('keydown', onKeyDown);
active = true;
onStateChanged();
};
this.stop = function() {
window.removeEventListener('keydown', onKeyDown);
active = false;
onStateChanged();
};
}
module.exports = KeyboardNavigation;
module.exports = KeyboardShortcuts;

View file

@ -0,0 +1,5 @@
// Copyright (C) 2017-2020 Smart code 203358507
const KeyboardShortcuts = require('./KeyboardShortcuts');
module.exports = KeyboardShortcuts;

View file

@ -6,43 +6,13 @@ function Shell() {
let active = false;
let error = null;
let starting = false;
const events = new EventEmitter();
events.on('error', () => { });
function onStateChanged() {
events.emit('stateChanged');
}
function start() {
if (active || error instanceof Error || starting) {
return;
}
starting = true;
setTimeout(() => {
error = new Error('Unable to init stremio shell');
starting = false;
onStateChanged();
});
}
function stop() {
active = false;
error = null;
starting = false;
onStateChanged();
}
function on(name, listener) {
events.on(name, listener);
}
function off(name, listener) {
events.off(name, listener);
}
function dispatch() {
if (!active) {
return;
}
// TODO
}
Object.defineProperties(this, {
active: {
@ -58,16 +28,38 @@ function Shell() {
get: function() {
return error;
}
},
starting: {
configurable: false,
enumerable: true,
get: function() {
return starting;
}
}
});
this.start = start;
this.stop = stop;
this.on = on;
this.off = off;
this.dispatch = dispatch;
this.start = function() {
if (active || error instanceof Error || starting) {
return;
}
Object.freeze(this);
active = false;
error = new Error('Stremio Shell API not available');
starting = false;
onStateChanged();
};
this.stop = function() {
active = false;
error = null;
starting = false;
onStateChanged();
};
this.on = function(name, listener) {
events.on(name, listener);
};
this.off = function(name, listener) {
events.off(name, listener);
};
}
module.exports = Shell;

View file

@ -1,13 +1,15 @@
// Copyright (C) 2017-2020 Smart code 203358507
const Chromecast = require('./Chromecast');
const Core = require('./Core');
const KeyboardNavigation = require('./KeyboardNavigation');
const KeyboardShortcuts = require('./KeyboardShortcuts');
const { ServicesProvider, useServices } = require('./ServicesContext');
const Shell = require('./Shell');
module.exports = {
Chromecast,
Core,
KeyboardNavigation,
KeyboardShortcuts,
ServicesProvider,
useServices,
Shell

View file

@ -1,314 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
var EventEmitter = require('events');
var subtitlesParser = require('./subtitlesParser');
var subtitlesRenderer = require('./subtitlesRenderer');
var colorConverter = require('./colorConverter');
var COLOR_REGEX = /^#[A-Fa-f0-9]{8}$/;
var ERROR_CODE = Object.freeze({
FETCH_FAILED: 70,
PARSE_FAILED: 71
});
var SIZE_COEF = 25;
function HTMLSubtitles(options) {
var containerElement = options && options.containerElement;
if (!(containerElement instanceof HTMLElement) || !containerElement.hasAttribute('id')) {
throw new Error('Instance of HTMLElement with id attribute required');
}
if (!document.body.contains(containerElement)) {
throw new Error('Container element not attached to body');
}
var destroyed = false;
var events = new EventEmitter();
var cuesByTime = null;
var tracks = Object.freeze([]);
var selectedTrackId = null;
var delay = null;
var stylesElement = document.createElement('style');
var subtitlesElement = document.createElement('div');
events.on('error', function() { });
containerElement.appendChild(stylesElement);
var subtitlesContainerStylesIndex = stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles { position: absolute; right: 0; bottom: 0; left: 0; z-index: 0; text-align: center; }', stylesElement.sheet.cssRules.length);
var subtitlesCueStylesIndex = stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles .cue { display: inline-block; padding: 0.2em; text-shadow: 0 0 0.03em #222222ff, 0 0 0.03em #222222ff, 0 0 0.03em #222222ff, 0 0 0.03em #222222ff, 0 0 0.03em #222222ff; background-color: #00000000; color: #ffffffff; font-size: 4vmin; }', stylesElement.sheet.cssRules.length);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .subtitles .cue * { font-size: inherit; }', stylesElement.sheet.cssRules.length);
containerElement.appendChild(subtitlesElement);
subtitlesElement.classList.add('subtitles');
function on(eventName, listener) {
if (destroyed) {
return;
}
events.on(eventName, listener);
}
function addTracks(extraTracks) {
if (destroyed || !Array.isArray(extraTracks)) {
return;
}
tracks = extraTracks
.filter(function(track) {
return track &&
typeof track.url === 'string' &&
track.url.length > 0 &&
typeof track.origin === 'string' &&
track.origin.length > 0 &&
track.origin !== 'EMBEDDED IN VIDEO';
})
.map(function(track) {
return Object.freeze(Object.assign({}, track, {
id: track.url
}));
})
.concat(tracks)
.filter(function(track, index, tracks) {
for (var i = 0; i < tracks.length; i++) {
if (tracks[i].id === track.id) {
return i === index;
}
}
return false;
});
Object.freeze(tracks);
events.emit('propChanged', 'tracks');
}
function updateText(mediaTime) {
while (subtitlesElement.hasChildNodes()) {
subtitlesElement.removeChild(subtitlesElement.lastChild);
}
if (cuesByTime === null || isNaN(mediaTime) || mediaTime === null) {
return;
}
var time = mediaTime + delay;
subtitlesRenderer.render(cuesByTime, time)
.forEach(function(cueNode) {
cueNode.classList.add('cue');
subtitlesElement.append(cueNode, document.createElement('br'));
});
}
function clearTracks() {
updateText(NaN);
cuesByTime = null;
tracks = Object.freeze([]);
selectedTrackId = null;
delay = null;
events.emit('propChanged', 'tracks');
events.emit('propChanged', 'selectedTrackId');
events.emit('propChanged', 'delay');
}
function destroy() {
destroyed = true;
clearTracks();
events.emit('propChanged', 'size');
events.emit('propChanged', 'textColor');
events.emit('propChanged', 'backgroundColor');
events.emit('propChanged', 'outlineColor');
events.emit('propChanged', 'offset');
events.removeAllListeners();
events.on('error', function() { });
containerElement.removeChild(stylesElement);
containerElement.removeChild(subtitlesElement);
}
Object.defineProperties(this, {
tracks: {
configurable: false,
enumerable: true,
get: function() {
return Object.freeze(tracks.slice());
}
},
selectedTrackId: {
configurable: false,
enumerable: true,
get: function() {
return selectedTrackId;
},
set: function(value) {
if (destroyed) {
return;
}
cuesByTime = null;
selectedTrackId = null;
delay = null;
updateText(NaN);
var selecterdTrack = tracks.find(function(track) {
return track.id === value;
});
if (selecterdTrack) {
selectedTrackId = selecterdTrack.id;
delay = 0;
fetch(selecterdTrack.url)
.then(function(resp) {
return resp.text();
})
.catch(function(error) {
events.emit('error', Object.freeze({
code: ERROR_CODE.FETCH_FAILED,
message: 'Failed to fetch subtitles from ' + selecterdTrack.origin,
track: selecterdTrack,
error: error
}));
})
.then(function(text) {
if (typeof text === 'string' && selectedTrackId === selecterdTrack.id) {
cuesByTime = subtitlesParser.parse(text);
if (cuesByTime.times.length === 0) {
throw new Error('parse failed');
}
events.emit('trackLoaded', selecterdTrack);
}
})
.catch(function(error) {
events.emit('error', Object.freeze({
code: ERROR_CODE.PARSE_FAILED,
message: 'Failed to parse subtitles from ' + selecterdTrack.origin,
track: selecterdTrack,
error: error
}));
});
}
events.emit('propChanged', 'selectedTrackId');
events.emit('propChanged', 'delay');
}
},
delay: {
configurable: false,
enumerable: true,
get: function() {
return delay;
},
set: function(value) {
if (destroyed || isNaN(value) || value === null || selectedTrackId === null) {
return;
}
delay = parseInt(value);
updateText(NaN);
events.emit('propChanged', 'delay');
}
},
size: {
configurable: false,
enumerable: true,
get: function() {
if (destroyed) {
return null;
}
return parseInt(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.fontSize) * SIZE_COEF
},
set: function(value) {
if (destroyed || isNaN(value) || value === null) {
return;
}
stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.fontSize = Math.floor(value / SIZE_COEF) + 'vmin';
events.emit('propChanged', 'size');
}
},
offset: {
configurable: false,
enumerable: true,
get: function() {
if (destroyed) {
return null;
}
return parseInt(stylesElement.sheet.cssRules[subtitlesContainerStylesIndex].style.bottom);
},
set: function(value) {
if (destroyed || isNaN(value) || value === null) {
return;
}
stylesElement.sheet.cssRules[subtitlesContainerStylesIndex].style.bottom = Math.max(0, Math.min(100, parseInt(value))) + '%';
events.emit('propChanged', 'offset');
}
},
textColor: {
configurable: false,
enumerable: true,
get: function() {
if (destroyed) {
return null;
}
return colorConverter.rgbaToHex(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.color);
},
set: function(value) {
if (destroyed || typeof value !== 'string' || value.length !== 9 || !value.match(COLOR_REGEX)) {
return;
}
stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.color = value;
events.emit('propChanged', 'textColor');
}
},
backgroundColor: {
configurable: false,
enumerable: true,
get: function() {
if (destroyed) {
return null;
}
return colorConverter.rgbaToHex(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.backgroundColor);
},
set: function(value) {
if (destroyed || typeof value !== 'string' || value.length !== 9 || !value.match(COLOR_REGEX)) {
return;
}
stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.backgroundColor = value;
events.emit('propChanged', 'backgroundColor');
}
},
outlineColor: {
configurable: false,
enumerable: false,
get: function() {
if (destroyed) {
return null;
}
return colorConverter.rgbaToHex(stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.textShadow);
},
set: function(value) {
if (destroyed || typeof value !== 'string' || value.length !== 9 || !value.match(COLOR_REGEX)) {
return;
}
stylesElement.sheet.cssRules[subtitlesCueStylesIndex].style.textShadow =
value + ' 0 0 0.03em,' +
value + ' 0 0 0.03em,' +
value + ' 0 0 0.03em,' +
value + ' 0 0 0.03em,' +
value + ' 0 0 0.03em';
events.emit('propChanged', 'outlineColor');
}
}
});
this.on = on;
this.addTracks = addTracks;
this.updateText = updateText;
this.clearTracks = clearTracks;
this.destroy = destroy;
Object.freeze(this);
};
Object.freeze(HTMLSubtitles);
module.exports = HTMLSubtitles;

View file

@ -1,460 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
var EventEmitter = require('events');
var HTMLSubtitles = require('./HTMLSubtitles');
function HTMLVideo(options) {
var containerElement = options && options.containerElement;
if (!(containerElement instanceof HTMLElement) || !containerElement.hasAttribute('id')) {
throw new Error('Instance of HTMLElement with id attribute required');
}
if (!document.body.contains(containerElement)) {
throw new Error('Container element not attached to body');
}
var destroyed = false;
var loaded = false;
var events = new EventEmitter();
var observedProps = {};
var subtitles = new HTMLSubtitles({ containerElement: containerElement });
var stylesElement = document.createElement('style');
var videoElement = document.createElement('video');
events.on('error', function() { });
subtitles.on('propChanged', onSubtitlesPropChanged);
subtitles.on('trackLoaded', onSubtitlesTrackLoaded);
subtitles.on('error', onSubtitlesError);
containerElement.appendChild(stylesElement);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .video { position: absolute; width: 100%; height: 100%; z-index: -1; background-color: black; }', stylesElement.sheet.cssRules.length);
containerElement.appendChild(videoElement);
videoElement.classList.add('video');
videoElement.crossOrigin = 'anonymous';
videoElement.controls = false;
videoElement.onpause = function() {
onVideoPropChanged('paused');
};
videoElement.onplay = function() {
onVideoPropChanged('paused');
};
videoElement.ontimeupdate = function() {
onVideoPropChanged('currentTime');
};
videoElement.ondurationchange = function() {
onVideoPropChanged('duration');
};
videoElement.onwaiting = function() {
onVideoPropChanged('readyState');
};
videoElement.onplaying = function() {
onVideoPropChanged('readyState');
};
videoElement.onloadeddata = function() {
onVideoPropChanged('readyState');
};
videoElement.onvolumechange = function() {
onVideoPropChanged('volume');
onVideoPropChanged('muted');
};
videoElement.onended = function() {
onVideoEnded();
};
videoElement.onerror = function() {
onVideoError();
};
function onSubtitlesPropChanged(propName) {
switch (propName) {
case 'tracks': {
if (observedProps['subtitlesTracks']) {
events.emit('propChanged', 'subtitlesTracks', getProp('subtitlesTracks'));
}
break;
}
case 'selectedTrackId': {
if (observedProps['selectedSubtitlesTrackId']) {
events.emit('propChanged', 'selectedSubtitlesTrackId', getProp('selectedSubtitlesTrackId'));
}
break;
}
case 'delay': {
subtitles.updateText(getProp('time'));
if (observedProps['subtitlesDelay']) {
events.emit('propChanged', 'subtitlesDelay', getProp('subtitlesDelay'));
}
break;
}
case 'size': {
if (observedProps['subtitlesSize']) {
events.emit('propChanged', 'subtitlesSize', getProp('subtitlesSize'));
}
break;
}
case 'offset': {
if (observedProps['subtitlesOffset']) {
events.emit('propChanged', 'subtitlesOffset', getProp('subtitlesOffset'));
}
break;
}
case 'textColor': {
if (observedProps['subtitlesTextColor']) {
events.emit('propChanged', 'subtitlesTextColor', getProp('subtitlesTextColor'));
}
break;
}
case 'backgroundColor': {
if (observedProps['subtitlesBackgroundColor']) {
events.emit('propChanged', 'subtitlesBackgroundColor', getProp('subtitlesBackgroundColor'));
}
break;
}
case 'outlineColor': {
if (observedProps['subtitlesOutlineColor']) {
events.emit('propChanged', 'subtitlesOutlineColor', getProp('subtitlesOutlineColor'));
}
break;
}
}
}
function onSubtitlesTrackLoaded(track) {
subtitles.updateText(getProp('time'));
events.emit('subtitlesTrackLoaded', track);
}
function onSubtitlesError(error) {
onError(Object.assign({}, error, {
critical: false
}));
}
function onVideoPropChanged(propName) {
switch (propName) {
case 'paused': {
if (observedProps['paused']) {
events.emit('propChanged', 'paused', getProp('paused'));
}
break;
}
case 'currentTime': {
subtitles.updateText(getProp('time'));
if (observedProps['time']) {
events.emit('propChanged', 'time', getProp('time'));
}
break;
}
case 'duration': {
if (observedProps['duration']) {
events.emit('propChanged', 'duration', getProp('duration'));
}
break;
}
case 'readyState': {
if (observedProps['buffering']) {
events.emit('propChanged', 'buffering', getProp('buffering'));
}
break;
}
case 'volume': {
if (observedProps['volume']) {
events.emit('propChanged', 'volume', getProp('volume'));
}
break;
}
case 'muted': {
if (observedProps['muted']) {
events.emit('propChanged', 'muted', getProp('muted'));
}
break;
}
}
}
function onVideoEnded() {
events.emit('ended');
}
function onVideoError() {
onError({
code: videoElement.error.code,
message: videoElement.error.message,
critical: true
});
}
function onError(error) {
if (!error) {
return;
}
Object.freeze(error);
events.emit('error', error);
if (error.critical) {
command('stop');
}
}
function getProp(propName) {
switch (propName) {
case 'paused': {
if (!loaded) {
return null;
}
return !!videoElement.paused;
}
case 'time': {
if (!loaded || isNaN(videoElement.currentTime) || videoElement.currentTime === null) {
return null;
}
return Math.floor(videoElement.currentTime * 1000);
}
case 'duration': {
if (!loaded || isNaN(videoElement.duration) || videoElement.duration === null) {
return null;
}
return Math.floor(videoElement.duration * 1000);
}
case 'buffering': {
if (!loaded) {
return null;
}
return videoElement.readyState < videoElement.HAVE_FUTURE_DATA;
}
case 'volume': {
if (destroyed || isNaN(videoElement.volume) || videoElement.volume === null) {
return null;
}
return Math.floor(videoElement.volume * 100);
}
case 'muted': {
if (destroyed) {
return null;
}
return !!videoElement.muted;
}
case 'subtitlesTracks': {
return subtitles.tracks;
}
case 'selectedSubtitlesTrackId': {
return subtitles.selectedTrackId;
}
case 'subtitlesDelay': {
return subtitles.delay;
}
case 'subtitlesSize': {
return subtitles.size;
}
case 'subtitlesOffset': {
return subtitles.offset;
}
case 'subtitlesTextColor': {
return subtitles.textColor;
}
case 'subtitlesBackgroundColor': {
return subtitles.backgroundColor;
}
case 'subtitlesOutlineColor': {
return subtitles.outlineColor;
}
default: {
throw new Error('getProp not supported: ' + propName);
}
}
}
function observeProp(propName) {
if (HTMLVideo.manifest.props.indexOf(propName) === -1) {
throw new Error('observeProp not supported: ' + propName);
}
events.emit('propValue', propName, getProp(propName));
observedProps[propName] = true;
}
function setProp(propName, propValue) {
switch (propName) {
case 'paused': {
if (loaded) {
if (!!propValue) {
videoElement.pause();
} else {
videoElement.play();
}
}
break;
}
case 'time': {
if (loaded && !isNaN(propValue) && propValue !== null) {
videoElement.currentTime = parseInt(propValue) / 1000;
}
break;
}
case 'volume': {
if (!isNaN(propValue) && propValue !== null) {
videoElement.muted = false;
videoElement.volume = Math.max(0, Math.min(100, parseInt(propValue))) / 100;
}
break;
}
case 'muted': {
videoElement.muted = !!propValue;
break;
}
case 'selectedSubtitlesTrackId': {
if (loaded) {
subtitles.selectedTrackId = propValue;
}
break;
}
case 'subtitlesDelay': {
if (loaded) {
subtitles.delay = propValue;
}
break;
}
case 'subtitlesSize': {
subtitles.size = propValue;
break;
}
case 'subtitlesOffset': {
subtitles.offset = propValue;
break;
}
case 'subtitlesTextColor': {
subtitles.textColor = propValue;
break;
}
case 'subtitlesBackgroundColor': {
subtitles.backgroundColor = propValue;
break;
}
case 'subtitlesOutlineColor': {
subtitles.outlineColor = propValue;
break;
}
default: {
throw new Error('setProp not supported: ' + propName);
}
}
}
function command(commandName, commandArgs) {
switch (commandName) {
case 'addSubtitlesTracks': {
if (loaded && commandArgs) {
subtitles.addTracks(commandArgs.tracks);
}
break;
}
case 'stop': {
loaded = false;
videoElement.removeAttribute('src');
videoElement.load();
videoElement.currentTime = 0;
onVideoPropChanged('paused');
onVideoPropChanged('currentTime');
onVideoPropChanged('duration');
onVideoPropChanged('readyState');
subtitles.clearTracks();
break;
}
case 'load': {
if (commandArgs && commandArgs.stream && typeof commandArgs.stream.url === 'string') {
command('stop');
videoElement.autoplay = typeof commandArgs.autoplay === 'boolean' ? commandArgs.autoplay : true;
videoElement.currentTime = !isNaN(commandArgs.time) && commandArgs.time !== null ? parseInt(commandArgs.time) / 1000 : 0;
videoElement.src = commandArgs.stream.url;
loaded = true;
onVideoPropChanged('paused');
onVideoPropChanged('currentTime');
onVideoPropChanged('duration');
onVideoPropChanged('readyState');
}
break;
}
case 'destroy': {
command('stop');
destroyed = true;
onVideoPropChanged('volume');
onVideoPropChanged('muted');
subtitles.destroy();
events.removeAllListeners();
events.on('error', function() { });
videoElement.onpause = null;
videoElement.onplay = null;
videoElement.ontimeupdate = null;
videoElement.ondurationchange = null;
videoElement.onwaiting = null;
videoElement.onplaying = null;
videoElement.onloadeddata = null;
videoElement.onvolumechange = null;
videoElement.onended = null;
videoElement.onerror = null;
containerElement.removeChild(videoElement);
containerElement.removeChild(stylesElement);
break;
}
default: {
throw new Error('command not supported: ' + commandName);
}
}
}
function on(eventName, listener) {
if (destroyed) {
throw new Error('Video is destroyed');
}
events.on(eventName, listener);
}
function dispatch(args) {
if (destroyed) {
throw new Error('Video is destroyed');
}
if (args) {
if (typeof args.commandName === 'string') {
command(args.commandName, args.commandArgs);
return;
} else if (typeof args.propName === 'string') {
setProp(args.propName, args.propValue);
return;
} else if (typeof args.observedPropName === 'string') {
observeProp(args.observedPropName);
return;
}
}
throw new Error('Invalid dispatch call: ' + JSON.stringify(args));
}
this.on = on;
this.dispatch = dispatch;
Object.freeze(this);
};
HTMLVideo.manifest = Object.freeze({
name: 'HTMLVideo',
embedded: true,
props: Object.freeze(['paused', 'time', 'duration', 'buffering', 'volume', 'muted', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesSize', 'subtitlesDelay', 'subtitlesOffset', 'subtitlesTextColor', 'subtitlesBackgroundColor', 'subtitlesOutlineColor'])
});
Object.freeze(HTMLVideo);
module.exports = HTMLVideo;

View file

@ -1,438 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
var EventEmitter = require('events');
var MPV_CRITICAL_ERROR_CODES = [];
function MPVVideo(options) {
var ipc = options && options.ipc;
var id = options && options.id;
if (!ipc) {
throw new Error('ipc parameter is required');
}
if (typeof id !== 'string') {
throw new Error('id parameter is required');
}
var ready = false;
var loaded = false;
var destroyed = false;
var events = new EventEmitter();
var observedProps = {};
var dispatchArgsReadyQueue = [];
events.on('error', function() { });
ipc.dispatch('mpv', 'createChannel', id)
.then(function() {
if (destroyed) {
return;
}
ready = true;
Promise.all([getProp('volume'), getProp('mute')]).then(function(values) {
if (destroyed) {
return;
}
onPropChanged('volume', values[0]);
onPropChanged('mute', values[1]);
});
ipc.on('mpvEvent', onMpvEvent);
flushDispatchArgsQueue(dispatchArgsReadyQueue);
})
.catch(function(error) {
onChannelError(error);
});
function mapPausedValue(pause) {
if (!loaded || typeof pause !== 'boolean') {
return null;
}
return pause;
}
function mapTimeValue(timePos) {
if (!loaded || isNaN(timePos) || timePos === null) {
return null;
}
return Math.round(timePos * 1000);
}
function mapDurationValue(duration) {
if (!loaded || isNaN(duration) || duration === null) {
return null;
}
return Math.round(duration * 1000);
}
function mapBufferingValue(seeking, pausedForCache) {
if (!loaded || (seeking === null && pausedForCache === null)) {
return null;
}
return !!seeking || !!pausedForCache;
}
function mapVolumeValue(volume) {
if (!ready || destroyed || isNaN(volume) || volume === null) {
return null;
}
return volume;
}
function mapMutedValue(mute) {
if (!ready || destroyed || typeof mute !== 'boolean') {
return null;
}
return mute;
}
function onError(error) {
if (destroyed || !error) {
return;
}
Object.freeze(error);
events.emit('error', error);
if (error.critical) {
dispatch('command', 'stop');
}
}
function onEnded() {
if (destroyed) {
return;
}
events.emit('ended');
}
function onPropChanged(propName, propValue) {
switch (propName) {
case 'pause': {
if (observedProps['paused']) {
events.emit('propChanged', 'paused', mapPausedValue(propValue));
}
break;
}
case 'time-pos': {
if (observedProps['time']) {
events.emit('propChanged', 'time', mapTimeValue(propValue));
}
break;
}
case 'duration': {
if (observedProps['duration']) {
events.emit('propChanged', 'duration', mapDurationValue(propValue));
}
break;
}
case 'seeking': {
if (observedProps['buffering']) {
events.emit('propChanged', 'buffering', mapBufferingValue(propValue, null));
}
break;
}
case 'paused-for-cache': {
if (observedProps['buffering']) {
events.emit('propChanged', 'buffering', mapBufferingValue(null, propValue));
}
break;
}
case 'volume': {
if (observedProps['volume']) {
events.emit('propChanged', 'volume', mapVolumeValue(propValue));
}
break;
}
case 'mute': {
if (observedProps['muted']) {
events.emit('propChanged', 'muted', mapMutedValue(propValue));
}
break;
}
}
}
function onMpvEvent(data) {
if (destroyed) {
return;
}
if (!data || data.channelId !== id) {
onChannelError(ipc.errors.mpv_channel_id_expired);
return;
}
switch (data.eventName) {
case 'error': {
onError(Object.assign({}, data, {
critical: MPV_CRITICAL_ERROR_CODES.indexOf(data.code) !== -1
}));
break;
}
case 'ended': {
onEnded();
break;
}
case 'propChanged': {
onPropChanged(data.propName, data.propValue);
break;
}
}
}
function onChannelError(error) {
if (destroyed) {
return;
}
onError(Object.assign({}, error, {
critical: true
}));
dispatch('command', 'destroy');
}
function observeProp(propName) {
ipc.dispatch('mpv', 'observeProp', id, propName)
.catch(function(error) {
onChannelError(error);
});
}
function getProp(propName) {
return ipc.dispatch('mpv', 'getProp', id, propName)
.catch(function(error) {
onChannelError(error);
return null;
});
}
function setProp(propName, propValue) {
ipc.dispatch('mpv', 'setProp', id, propName, propValue)
.catch(function(error) {
onChannelError(error);
});
}
function command() {
ipc.dispatch.apply(null, ['mpv', 'command', id].concat(Array.from(arguments)))
.catch(function(error) {
onChannelError(error);
});
}
function flushDispatchArgsQueue(dispatchArgsQueue) {
if (destroyed) {
return;
}
while (dispatchArgsQueue.length > 0) {
var args = dispatchArgsQueue.shift();
dispatch.apply(null, args);
}
}
function on(eventName, listener) {
if (destroyed) {
throw new Error('Video is destroyed');
}
events.on(eventName, listener);
}
function dispatch() {
if (destroyed) {
throw new Error('Video is destroyed');
}
switch (arguments[0]) {
case 'observeProp': {
switch (arguments[1]) {
case 'paused': {
observeProp('pause');
getProp('pause').then(function(pause) {
if (destroyed) {
return;
}
observedProps['paused'] = true;
events.emit('propValue', 'paused', mapPausedValue(pause));
});
return;
}
case 'time': {
observeProp('time-pos');
getProp('time-pos').then(function(timePos) {
if (destroyed) {
return;
}
observedProps['time'] = true;
events.emit('propValue', 'time', mapTimeValue(timePos));
});
return;
}
case 'duration': {
observeProp('duration');
getProp('duration').then(function(duration) {
if (destroyed) {
return;
}
observedProps['duration'] = true;
events.emit('propValue', 'duration', mapDurationValue(duration));
});
return;
}
case 'buffering': {
observeProp('seeking');
observeProp('paused-for-cache');
Promise.all([getProp('seeking'), getProp('paused-for-cache')]).then(function(values) {
if (destroyed) {
return;
}
observedProps['buffering'] = true;
events.emit('propValue', 'buffering', mapBufferingValue(values[0], values[1]));
});
return;
}
case 'volume': {
observeProp('volume');
getProp('volume').then(function(volume) {
if (destroyed) {
return;
}
observedProps['volume'] = true;
events.emit('propValue', 'volume', mapVolumeValue(volume));
});
return;
}
case 'muted': {
observeProp('mute');
getProp('mute').then(function(mute) {
if (destroyed) {
return;
}
observedProps['muted'] = true;
events.emit('propValue', 'muted', mapMutedValue(mute));
});
return;
}
}
}
case 'setProp': {
switch (arguments[1]) {
case 'paused': {
if (loaded) {
setProp('pause', !!arguments[2]);
}
return;
}
case 'time': {
if (loaded && !isNaN(arguments[2]) && arguments[2] !== null) {
setProp('time-pos', arguments[2] / 1000);
}
return;
}
case 'volume': {
if (ready) {
if (!isNaN(arguments[2]) && arguments[2] !== null) {
setProp('mute', false);
setProp('volume', Math.max(0, Math.min(100, arguments[2])));
}
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
case 'muted': {
if (ready) {
setProp('mute', !!arguments[2]);
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
}
}
case 'command': {
switch (arguments[1]) {
case 'stop': {
loaded = false;
if (ready) {
command('stop');
}
onPropChanged('pause', null);
onPropChanged('time-pos', null);
onPropChanged('duration', null);
onPropChanged('seeking', null);
onPropChanged('paused-for-cache', null);
return;
}
case 'load': {
if (ready) {
dispatch('command', 'stop');
loaded = true;
var startTime = !isNaN(arguments[3].time) && arguments[3].time !== null ? Math.round(arguments[3].time / 1000) : 0;
command('loadfile', arguments[2].url, 'replace', 'time-pos=' + startTime);
setProp('pause', arguments[3].autoplay === false);
Promise.all([
getProp('pause'),
getProp('time-pos'),
getProp('duration'),
getProp('seeking'),
getProp('paused-for-cache')
]).then(function(values) {
if (destroyed) {
return;
}
onPropChanged('pause', values[0]);
onPropChanged('time-pos', values[1]);
onPropChanged('duration', values[2]);
onPropChanged('seeking', values[3]);
onPropChanged('paused-for-cache', values[4]);
});
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
case 'destroy': {
dispatch('command', 'stop');
destroyed = true;
onPropChanged('volume', null);
onPropChanged('mute', null);
events.removeAllListeners();
events.on('error', function() { });
ipc.off('mpvEvent', onMpvEvent);
dispatchArgsReadyQueue = [];
return;
}
}
}
}
throw new Error('Invalid dispatch call: ' + Array.from(arguments).map(String));
}
this.on = on;
this.dispatch = dispatch;
Object.freeze(this);
}
MPVVideo.manifest = Object.freeze({
name: 'MPVVideo',
embedded: true,
props: Object.freeze(['paused', 'time', 'duration', 'volume', 'muted', 'buffering'])
});
Object.freeze(MPVVideo);
module.exports = MPVVideo;

View file

@ -1,3 +0,0 @@
# stremio-video
### TODO move this folder in a separate repo

View file

@ -1,685 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
var EventEmitter = require('events');
var HTMLSubtitles = require('./HTMLSubtitles');
function YouTubeVideo(options) {
var containerElement = options && options.containerElement;
if (!(containerElement instanceof HTMLElement) || !containerElement.hasAttribute('id')) {
throw new Error('Instance of HTMLElement with id attribute required');
}
var self = this;
var ready = false;
var loaded = false;
var destroyed = false;
var events = new EventEmitter();
var dispatchArgsReadyQueue = [];
var dispatchArgsLoadedQueue = [];
var pausedObserved = false;
var timeObserved = false;
var durationObserved = false;
var bufferingObserved = false;
var volumeObserved = false;
var propChangedIntervalId = window.setInterval(onPropChangedInterval, 100);
var embeddedSubtitlesSelectedTrackId = null;
var subtitles = new HTMLSubtitles(containerElement);
var video = null;
var scriptElement = document.createElement('script');
var stylesElement = document.createElement('style');
var videoContainer = document.createElement('div');
events.on('error', function() { });
subtitles.on('error', onSubtitlesError);
subtitles.on('load', updateSubtitleText);
scriptElement.type = 'text/javascript';
scriptElement.src = 'https://www.youtube.com/iframe_api';
scriptElement.onload = onYouTubePlayerApiLoaded;
scriptElement.onerror = onYouTubePlayerApiError;
containerElement.appendChild(scriptElement);
containerElement.appendChild(stylesElement);
stylesElement.sheet.insertRule('#' + containerElement.id + ' .video { position: absolute; width: 100%; height: 100%; z-index: -1; }', stylesElement.sheet.cssRules.length);
containerElement.appendChild(videoContainer);
videoContainer.classList.add('video');
function getPaused() {
if (!loaded) {
return null;
}
return video.getPlayerState() !== YT.PlayerState.PLAYING;
}
function getTime() {
if (!loaded || isNaN(video.getCurrentTime()) || video.getCurrentTime() === null) {
return null;
}
return Math.floor(video.getCurrentTime() * 1000);
}
function getDuration() {
if (!loaded || isNaN(video.getDuration()) || video.getDuration() === null) {
return null;
}
return Math.floor(video.getDuration() * 1000);
}
function getBuffering() {
if (!loaded) {
return null;
}
return video.getPlayerState() === YT.PlayerState.BUFFERING;
}
function getVolume() {
if (!ready || destroyed || isNaN(video.getVolume()) || video.getVolume() === null) {
return null;
}
return video.isMuted() ? 0 : video.getVolume();
}
function getSubtitlesTracks() {
if (!loaded) {
return Object.freeze([]);
}
var embeddedTracks = (video.getOption('captions', 'tracklist') || [])
.map(function(track) {
return Object.freeze({
id: track.languageCode,
origin: 'EMBEDDED IN VIDEO',
label: track.languageName
});
});
var extraTracks = subtitles.dispatch('getProp', 'tracks');
var allTracks = embeddedTracks.concat(extraTracks)
.filter(function(track, index, tracks) {
for (var i = 0; i < tracks.length; i++) {
if (tracks[i].id === track.id) {
return i === index;
}
}
return false;
});
return Object.freeze(allTracks);
}
function getSelectedSubtitlesTrackId() {
if (!loaded) {
return null;
}
return embeddedSubtitlesSelectedTrackId !== null ?
embeddedSubtitlesSelectedTrackId
:
subtitles.dispatch('getProp', 'selectedTrackId');
}
function getSubtitlesDelay() {
if (!loaded) {
return null;
}
return embeddedSubtitlesSelectedTrackId !== null ?
null
:
subtitles.dispatch('getProp', 'delay');
}
function getsubtitlesSize() {
if (!ready || destroyed) {
return null;
}
return subtitles.dispatch('getProp', 'size');
}
function getSubtitlesDarkBackground() {
if (!ready || destroyed) {
return null;
}
return embeddedSubtitlesSelectedTrackId !== null ?
null
:
subtitles.dispatch('getProp', 'darkBackground');
}
function getSubtitleOffset() {
if (!ready || destroyed) {
return null;
}
return embeddedSubtitlesSelectedTrackId !== null ?
null
:
subtitles.dispatch('getProp', 'offset');
}
function onEnded() {
events.emit('ended');
}
function onError(error) {
Object.freeze(error);
events.emit('error', error);
if (error.critical) {
self.dispatch('command', 'stop');
}
}
function onPausedChanged() {
events.emit('propChanged', 'paused', getPaused());
}
function onTimeChanged() {
events.emit('propChanged', 'time', getTime());
}
function onDurationChanged() {
events.emit('propChanged', 'duration', getDuration());
}
function onBufferingChanged() {
events.emit('propChanged', 'buffering', getBuffering());
}
function onVolumeChanged() {
events.emit('propChanged', 'volume', getVolume());
}
function onSubtitlesTracksChanged() {
events.emit('propChanged', 'subtitlesTracks', getSubtitlesTracks());
}
function onSelectedSubtitlesTrackIdChanged() {
events.emit('propChanged', 'selectedSubtitlesTrackId', getSelectedSubtitlesTrackId());
}
function onSubtitlesDelayChanged() {
events.emit('propChanged', 'subtitlesDelay', getSubtitlesDelay());
}
function onsubtitlesSizeChanged() {
events.emit('propChanged', 'subtitlesSize', getsubtitlesSize());
}
function onSubtitlesDarkBackgroundChanged() {
events.emit('propChanged', 'subtitlesDarkBackground', getSubtitlesDarkBackground());
}
function onSubtitleOffsetChanged() {
events.emit('propChanged', 'subtitleOffset', getSubtitleOffset());
}
function onSubtitlesError(error) {
var code;
var message;
switch (error.code) {
case HTMLSubtitles.ERROR.SUBTITLES_FETCH_FAILED: {
code = HTMLSubtitles.ERROR.SUBTITLES_FETCH_FAILED;
message = 'Failed to fetch subtitles from ' + error.track.origin;
break;
}
case HTMLSubtitles.ERROR.SUBTITLES_PARSE_FAILED: {
code = HTMLSubtitles.ERROR.SUBTITLES_PARSE_FAILED;
message = 'Failed to parse subtitles from ' + error.track.origin;
break;
}
default: {
code = -1;
message = 'Unknown subtitles error';
}
}
onError({
code: code,
message: message,
critical: false
});
}
function onYouTubePlayerApiError() {
onError({
code: YouTubeVideo.ERROR.API_LOAD_FAILED,
message: 'YouTube player API failed to load',
critical: true
});
}
function onYouTubePlayerApiLoaded() {
if (destroyed) {
return;
}
if (!YT) {
onYouTubePlayerApiError();
return;
}
YT.ready(function() {
if (destroyed) {
return;
}
video = new YT.Player(videoContainer, {
height: '100%',
width: '100%',
playerVars: {
autoplay: 1,
cc_load_policy: 3,
controls: 0,
disablekb: 1,
enablejsapi: 1,
fs: 0,
iv_load_policy: 3,
loop: 0,
modestbranding: 1,
playsinline: 1,
rel: 0
},
events: {
onError: onVideoError,
onReady: onVideoReady,
onStateChange: onVideoStateChange,
onApiChange: onVideoApiChange
}
});
});
}
function onVideoError(error) {
var code;
var message;
switch (error.data) {
case YouTubeVideo.ERROR.INVALID_REQUEST: {
code = YouTubeVideo.ERROR.INVALID_REQUEST;
message = 'Invalid request';
break;
}
case YouTubeVideo.ERROR.CONTENT_CANNOT_BE_PLAYED: {
code = YouTubeVideo.ERROR.CONTENT_CANNOT_BE_PLAYED;
message = 'The requested content cannot be played';
break;
}
case YouTubeVideo.ERROR.REMOVED_VIDEO: {
code = YouTubeVideo.ERROR.REMOVED_VIDEO;
message = 'The video has been removed or marked as private';
break;
}
case YouTubeVideo.ERROR.CONTENT_CANNOT_BE_EMBEDDED1:
case YouTubeVideo.ERROR.CONTENT_CANNOT_BE_EMBEDDED2: {
code = YouTubeVideo.ERROR.CONTENT_CANNOT_BE_EMBEDDED1;
message = 'The video cannot be played in embedded players';
break;
}
default: {
code = -1;
message = 'Unknown video error';
}
}
onError({
code: code,
message: message,
critical: true
});
}
function onVideoReady() {
ready = true;
onVolumeChanged();
onsubtitlesSizeChanged();
onSubtitlesDarkBackgroundChanged();
onSubtitleOffsetChanged();
flushDispatchArgsQueue(dispatchArgsReadyQueue);
}
function onVideoStateChange(state) {
if (bufferingObserved) {
onBufferingChanged();
}
switch (state.data) {
case YT.PlayerState.ENDED: {
onEnded();
break;
}
case YT.PlayerState.PAUSED:
case YT.PlayerState.PLAYING: {
if (pausedObserved) {
onPausedChanged();
}
if (timeObserved) {
onTimeChanged();
}
if (durationObserved) {
onDurationChanged();
}
break;
}
case YT.PlayerState.UNSTARTED: {
if (pausedObserved) {
onPausedChanged();
}
break;
}
}
}
function onVideoApiChange() {
video.loadModule('captions');
onSubtitlesTracksChanged();
}
function onPropChangedInterval() {
if (timeObserved) {
onTimeChanged();
}
if (durationObserved) {
onDurationChanged();
}
if (volumeObserved) {
onVolumeChanged();
}
updateSubtitleText();
}
function updateSubtitleText() {
subtitles.dispatch('command', 'updateText', getTime());
}
function flushDispatchArgsQueue(dispatchArgsQueue) {
while (dispatchArgsQueue.length > 0) {
var args = dispatchArgsQueue.shift();
self.dispatch.apply(self, args);
}
}
this.on = function(eventName, listener) {
if (destroyed) {
throw new Error('Unable to add ' + eventName + ' listener');
}
events.on(eventName, listener);
};
this.dispatch = function() {
if (destroyed) {
throw new Error('Unable to dispatch ' + arguments[0]);
}
switch (arguments[0]) {
case 'observeProp': {
switch (arguments[1]) {
case 'paused': {
events.emit('propValue', 'paused', getPaused());
pausedObserved = true;
return;
}
case 'time': {
events.emit('propValue', 'time', getTime());
timeObserved = true;
return;
}
case 'duration': {
events.emit('propValue', 'duration', getDuration());
durationObserved = true;
return;
}
case 'buffering': {
events.emit('propValue', 'buffering', getBuffering());
bufferingObserved = true;
return;
}
case 'volume': {
events.emit('propValue', 'volume', getVolume());
volumeObserved = true;
return;
}
case 'subtitlesTracks': {
events.emit('propValue', 'subtitlesTracks', getSubtitlesTracks());
return;
}
case 'selectedSubtitlesTrackId': {
events.emit('propValue', 'selectedSubtitlesTrackId', getSelectedSubtitlesTrackId());
return;
}
case 'subtitlesDelay': {
events.emit('propValue', 'subtitlesDelay', getSubtitlesDelay());
return;
}
case 'subtitlesSize': {
events.emit('propValue', 'subtitlesSize', getsubtitlesSize());
return;
}
case 'subtitlesDarkBackground': {
events.emit('propValue', 'subtitlesDarkBackground', getSubtitlesDarkBackground());
return;
}
case 'subtitleOffset': {
events.emit('propValue', 'subtitleOffset', getSubtitleOffset());
return;
}
default: {
throw new Error('observeProp not supported: ' + arguments[1]);
}
}
}
case 'setProp': {
switch (arguments[1]) {
case 'paused': {
if (loaded) {
arguments[2] ? video.pauseVideo() : video.playVideo();
} else {
dispatchArgsLoadedQueue.push(Array.from(arguments));
}
return;
}
case 'time': {
if (loaded) {
if (!isNaN(arguments[2]) && arguments[2] !== null) {
video.seekTo(arguments[2] / 1000);
}
} else {
dispatchArgsLoadedQueue.push(Array.from(arguments));
}
return;
}
case 'volume': {
if (ready) {
if (!isNaN(arguments[2]) && arguments[2] !== null) {
video.unMute();
video.setVolume(Math.max(0, Math.min(100, arguments[2])));
}
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
case 'selectedSubtitlesTrackId': {
if (loaded) {
embeddedSubtitlesSelectedTrackId = null;
var tracks = getSubtitlesTracks();
for (var i = 0; i < tracks.length; i++) {
if (tracks[i].id === arguments[2] && tracks[i].origin === 'EMBEDDED IN VIDEO') {
embeddedSubtitlesSelectedTrackId = tracks[i].id;
break;
}
}
video.setOption('captions', 'track', { languageCode: arguments[2] });
subtitles.dispatch('setProp', 'selectedTrackId', arguments[2]);
onSubtitlesDelayChanged();
onSubtitlesDarkBackgroundChanged();
onSelectedSubtitlesTrackIdChanged();
updateSubtitleText();
} else {
dispatchArgsLoadedQueue.push(Array.from(arguments));
}
return;
}
case 'subtitlesDelay': {
if (loaded) {
subtitles.dispatch('setProp', 'delay', arguments[2]);
onSubtitlesDelayChanged();
updateSubtitleText();
} else {
dispatchArgsLoadedQueue.push(Array.from(arguments));
}
return;
}
case 'subtitlesSize': {
if (ready) {
subtitles.dispatch('setProp', 'size', arguments[2]);
video.setOption('captions', 'fontSize', Math.max(1, Math.min(5, Math.floor(arguments[2]))) - 2);
onsubtitlesSizeChanged();
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
case 'subtitlesDarkBackground': {
if (ready) {
subtitles.dispatch('setProp', 'darkBackground', arguments[2]);
onSubtitlesDarkBackgroundChanged();
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
case 'subtitleOffset': {
if (ready) {
subtitles.dispatch('setProp', 'offset', arguments[2]);
onSubtitleOffsetChanged();
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
default: {
throw new Error('setProp not supported: ' + arguments[1]);
}
}
}
case 'command': {
switch (arguments[1]) {
case 'addSubtitlesTracks': {
if (loaded) {
subtitles.dispatch('command', 'addTracks', arguments[2]);
onSubtitlesTracksChanged();
} else {
dispatchArgsLoadedQueue.push(Array.from(arguments));
}
return;
}
case 'mute': {
if (ready) {
video.mute();
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
case 'unmute': {
if (ready) {
video.unMute();
if (video.getVolume() === 0) {
video.setVolume(50);
}
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
case 'stop': {
loaded = false;
dispatchArgsLoadedQueue = [];
subtitles.dispatch('command', 'clearTracks');
if (ready) {
video.stopVideo();
}
onPausedChanged();
onTimeChanged();
onDurationChanged();
onBufferingChanged();
onSubtitlesTracksChanged();
onSelectedSubtitlesTrackIdChanged();
onSubtitlesDelayChanged();
updateSubtitleText();
return;
}
case 'load': {
if (ready) {
var dispatchArgsLoadedQueueCopy = dispatchArgsLoadedQueue.slice();
self.dispatch('command', 'stop');
dispatchArgsLoadedQueue = dispatchArgsLoadedQueueCopy;
var autoplay = typeof arguments[3].autoplay === 'boolean' ? arguments[3].autoplay : true;
var time = !isNaN(arguments[3].time) && arguments[3].time !== null ? arguments[3].time / 1000 : 0;
if (autoplay) {
video.loadVideoById({
videoId: arguments[2].ytId,
startSeconds: time
});
} else {
video.cueVideoById({
videoId: arguments[2].ytId,
startSeconds: time
});
}
loaded = true;
onPausedChanged();
onTimeChanged();
onDurationChanged();
onBufferingChanged();
onSubtitlesTracksChanged();
onSelectedSubtitlesTrackIdChanged();
onSubtitlesDelayChanged();
updateSubtitleText();
flushDispatchArgsQueue(dispatchArgsLoadedQueue);
} else {
dispatchArgsReadyQueue.push(Array.from(arguments));
}
return;
}
case 'destroy': {
self.dispatch('command', 'stop');
destroyed = true;
onVolumeChanged();
onsubtitlesSizeChanged();
onSubtitlesDarkBackgroundChanged();
onSubtitleOffsetChanged();
events.removeAllListeners();
clearInterval(propChangedIntervalId);
if (ready) {
video.destroy();
}
containerElement.removeChild(scriptElement);
containerElement.removeChild(videoContainer);
containerElement.removeChild(stylesElement);
subtitles.dispatch('command', 'destroy');
return;
}
default: {
throw new Error('command not supported: ' + arguments[1]);
}
}
}
default: {
throw new Error('Invalid dispatch call: ' + Array.from(arguments).map(String));
}
}
};
Object.freeze(this);
};
YouTubeVideo.ERROR = Object.freeze({
API_LOAD_FAILED: 12,
INVALID_REQUEST: 2,
CONTENT_CANNOT_BE_PLAYED: 5,
REMOVED_VIDEO: 100,
CONTENT_CANNOT_BE_EMBEDDED1: 101,
CONTENT_CANNOT_BE_EMBEDDED2: 150
});
YouTubeVideo.manifest = Object.freeze({
name: 'YouTubeVideo',
embedded: true,
props: Object.freeze(['paused', 'time', 'duration', 'volume', 'buffering', 'subtitlesTracks', 'selectedSubtitlesTrackId', 'subtitlesSize', 'subtitlesDelay', 'subtitlesDarkBackground', 'subtitleOffset'])
});
Object.freeze(YouTubeVideo);
module.exports = YouTubeVideo;

View file

@ -1,26 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
function binarySearchUpperBound(array, value) {
if (value < array[0] || array[array.length - 1] < value) {
return -1;
}
var left = 0;
var right = array.length - 1;
var index = -1;
while (left <= right) {
var middle = Math.floor((left + right) / 2);
if (array[middle] > value) {
right = middle - 1;
} else if (array[middle] < value) {
left = middle + 1;
} else {
index = middle;
left = middle + 1;
}
}
return index !== -1 ? index : right;
}
module.exports = binarySearchUpperBound;

View file

@ -1,18 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
function padWithZero(str) {
return ('0' + str).slice(-2);
}
function rgbaToHex(rgbaString) {
var values = rgbaString.split('(')[1].split(')')[0].split(',');
var red = parseInt(values[0]).toString(16);
var green = parseInt(values[1]).toString(16);
var blue = parseInt(values[2]).toString(16);
var alpha = Math.round((values[3] || 1) * 255).toString(16);
return '#' + padWithZero(red) + padWithZero(green) + padWithZero(blue) + padWithZero(alpha);
}
module.exports = {
rgbaToHex
};

View file

@ -1,13 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
var HTMLVideo = require('./HTMLVideo');
var MPVVideo = require('./MPVVideo');
var YouTubeVideo = require('./YouTubeVideo');
var withStreamingServer = require('./withStreamingServer');
module.exports = {
HTMLVideo,
MPVVideo,
YouTubeVideo,
withStreamingServer
};

View file

@ -1,61 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
var VTTJS = require('vtt.js');
var binarySearchUpperBound = require('./binarySearchUpperBound');
function parse(text) {
var nativeVTTCue = window.VTTCue;
window.VTTCue = VTTJS.VTTCue;
var parser = new VTTJS.WebVTT.Parser(window, VTTJS.WebVTT.StringDecoder());
var cues = [];
var cuesByTime = {};
parser.oncue = function(c) {
var cue = Object.freeze({
startTime: (c.startTime * 1000) | 0,
endTime: (c.endTime * 1000) | 0,
text: c.text
});
cues.push(cue);
cuesByTime[cue.startTime] = cuesByTime[cue.startTime] || [];
cuesByTime[cue.endTime] = cuesByTime[cue.endTime] || [];
};
parser.parse(text);
parser.flush();
window.VTTCue = nativeVTTCue;
cuesByTime.times = Object.keys(cuesByTime)
.map(function(time) {
return parseInt(time);
})
.sort(function(t1, t2) {
return t1 - t2;
});
Object.freeze(cues);
Object.freeze(cuesByTime);
Object.freeze(cuesByTime.times);
for (var i = 0; i < cues.length; i++) {
cuesByTime[cues[i].startTime].push(cues[i]);
var startTimeIndex = binarySearchUpperBound(cuesByTime.times, cues[i].startTime);
for (var j = startTimeIndex + 1; j < cuesByTime.times.length; j++) {
if (cues[i].endTime <= cuesByTime.times[j]) {
break;
}
cuesByTime[cuesByTime.times[j]].push(cues[i]);
}
}
for (var i = 0; i < cuesByTime.times.length; i++) {
cuesByTime[cuesByTime.times[i]].sort(function(c1, c2) {
return c1.startTime - c2.startTime ||
c1.endTime - c2.endTime;
});
Object.freeze(cuesByTime[cuesByTime.times[i]]);
}
return cuesByTime;
}
module.exports = Object.freeze({
parse: parse
});

View file

@ -1,22 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
var VTTJS = require('vtt.js');
var binarySearchUpperBound = require('./binarySearchUpperBound');
function render(cuesByTime, time) {
var nodes = [];
var timeIndex = binarySearchUpperBound(cuesByTime.times, time);
if (timeIndex !== -1) {
var cuesForTime = cuesByTime[cuesByTime.times[timeIndex]];
for (var i = 0; i < cuesForTime.length; i++) {
var node = VTTJS.WebVTT.convertCueToDOMTree(window, cuesForTime[i].text);
nodes.push(node);
}
}
return Object.freeze(nodes);
}
module.exports = Object.freeze({
render: render
});

View file

@ -1,136 +0,0 @@
// Copyright (C) 2017-2020 Smart code 203358507
var UrlUtils = require('url');
var EventEmitter = require('events');
function withStreamingServer(Video) {
function StreamingServerVideo(options) {
var video = new Video(options);
var events = new EventEmitter();
var destroyed = false;
var stream = null;
events.on('error', function() { });
function onError(error) {
if (!error) {
return;
}
Object.freeze(error);
events.emit('error', error);
if (error.critical) {
video.dispatch({ commandName: 'stop' });
}
}
function on(eventName, listener) {
if (!destroyed) {
events.on(eventName, listener);
}
video.on(eventName, listener);
}
function dispatch(args) {
if (!destroyed && args && args.commandName === 'load') {
stream = null;
video.dispatch({ commandName: 'stop' });
if (args.commandArgs && args.commandArgs.stream && typeof args.commandArgs.stream.infoHash === 'string' && typeof args.commandArgs.streamingServerUrl === 'string') {
stream = args.commandArgs.stream;
if (stream.fileIdx !== null && !isNaN(stream.fileIdx)) {
video.dispatch({
commandName: 'load',
commandArgs: {
autoplay: args.commandArgs.autoplay,
time: args.commandArgs.time,
stream: {
url: UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/' + String(stream.fileIdx))
}
}
});
} else {
fetch(UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/create'), {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
torrent: {
infoHash: stream.infoHash
}
})
}).then(function(resp) {
return resp.json();
}).then(function(resp) {
if (stream !== args.commandArgs.stream) {
return;
}
if (!Array.isArray(resp.files) || resp.files.length === 0) {
onError({
message: 'Unable to get files from torrent',
critical: true
});
return;
}
var fileIdx = resp.files.reduce((fileIdx, _, index, files) => {
if (files[index].length > files[fileIdx].length) {
return index;
}
return fileIdx;
}, 0);
video.dispatch({
commandName: 'load',
commandArgs: {
autoplay: args.commandArgs.autoplay,
time: args.commandArgs.time,
stream: {
url: UrlUtils.resolve(args.commandArgs.streamingServerUrl, stream.infoHash + '/' + String(fileIdx))
}
}
});
}).catch(function(error) {
if (stream !== args.commandArgs.stream) {
return;
}
onError({
message: 'Unable to get files from torrent',
critical: true,
error: error
});
});
}
}
} else {
if (args && args.commandName === 'destroy') {
destroyed = true;
stream = null;
events.removeAllListeners();
events.on('error', function() { });
}
video.dispatch(args);
}
}
this.on = on;
this.dispatch = dispatch;
Object.freeze(this);
}
StreamingServerVideo.manifest = Object.freeze({
name: Video.manifest.name + 'WithStreamingServer',
embedded: true,
props: Object.freeze(Video.manifest.props)
});
Object.freeze(StreamingServerVideo);
return StreamingServerVideo;
}
module.exports = withStreamingServer;

View file

@ -106,8 +106,7 @@ module.exports = (env, argv) => ({
extensions: ['.js', '.json', '.less', '.wasm'],
alias: {
'stremio': path.resolve(__dirname, 'src'),
'stremio-router': path.resolve(__dirname, 'src/router'),
'stremio-video': path.resolve(__dirname, 'src/video')
'stremio-router': path.resolve(__dirname, 'src/router')
}
},
devServer: {
@ -142,7 +141,7 @@ module.exports = (env, argv) => ({
}),
new webpack.ProgressPlugin(),
new CopyWebpackPlugin([
{ from: 'node_modules/@stremio/stremio-core-web/static', to: '' },
{ from: 'node_modules/@stremio/stremio-core-web/stremio_core_web_bg.wasm', to: '' },
{ from: 'images', to: 'images' },
{ from: 'fonts', to: 'fonts' }
]),
@ -153,7 +152,7 @@ module.exports = (env, argv) => ({
new MiniCssExtractPlugin(),
new CleanWebpackPlugin({
verbose: true,
cleanOnceBeforeBuildPatterns: [],
cleanOnceBeforeBuildPatterns: ['*'],
cleanAfterEveryBuildPatterns: ['./main.js', './main.css']
})
]

View file

@ -998,6 +998,13 @@
core-js-pure "^3.0.0"
regenerator-runtime "^0.13.4"
"@babel/runtime@7.10.0":
version "7.10.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.0.tgz#2cdcd6d7a391c24f7154235134c830cfb58ac0b1"
integrity sha512-tgYb3zVApHbLHYOPWtVwg25sBqHhfBXRKeKoTIyoheIxln1nA7oBl7SfHfiTG2GhDPI8EUBkOD/0wJCP/3HN4Q==
dependencies:
regenerator-runtime "^0.13.4"
"@babel/runtime@7.8.7":
version "7.8.7"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.8.7.tgz#8fefce9802db54881ba59f90bb28719b4996324d"
@ -1782,12 +1789,23 @@
telejson "^3.0.2"
util-deprecate "^1.0.2"
"@stremio/stremio-core-web@0.12.0":
version "0.12.0"
resolved "https://npm.pkg.github.com/download/@stremio/stremio-core-web/0.12.0/e498bcdf44176c46602f06a97cfb76c2e05ac15bd9f42058f10fab17fe8399da#fa8051f5d34115fba6e63635480da6542b890eb3"
integrity sha512-7cgrZs59mCmiEbCAlbXdGUDp0zp2ErpPzA8wVk1surWElipShpc0eZqMeeaD2o4bBUl5P9UwHv5HWEVm9+03jA==
"@stremio/stremio-core-web@0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@stremio/stremio-core-web/-/stremio-core-web-0.16.0.tgz#ae60ea017a48616db9b91ca9057d6df594fbe2e5"
integrity sha512-1DkiHMI47xolPta+SzN60vgvqBxF+73JU7H4yUoKV50H+opQ6lF5/Xb0E7+KmG/ZHERgaZX4YMafzCuf/6g3WQ==
dependencies:
"@babel/runtime" "7.8.7"
"@babel/runtime" "7.10.0"
"@stremio/stremio-video@0.0.4":
version "0.0.4"
resolved "https://registry.yarnpkg.com/@stremio/stremio-video/-/stremio-video-0.0.4.tgz#338fcc0e39152e69ea8b0ecfd8beefab8b486624"
integrity sha512-ffRSs3KiDDoHmCWTqcP//dqPDiqN9AMOaQEp0Rkw3O6L6wzISNfdi/ajwHQaHO1B4Jc6QXtejcLUNiZ+cI6tmg==
dependencies:
events "1.1.1"
magnet-uri "5.2.4"
url "0.11.0"
video-name-parser "1.4.6"
vtt.js "0.13.0"
"@svgr/babel-plugin-add-jsx-attribute@^4.2.0":
version "4.2.0"
@ -5940,11 +5958,6 @@ hastscript@^5.0.0:
property-information "^5.0.0"
space-separated-tokens "^1.0.0"
hat@0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/hat/-/hat-0.0.3.tgz#bb014a9e64b3788aed8005917413d4ff3d502d8a"
integrity sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=
he@1.2.x, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@ -7648,6 +7661,14 @@ lru-cache@^5.1.1:
dependencies:
yallist "^3.0.2"
magnet-uri@5.2.4:
version "5.2.4"
resolved "https://registry.yarnpkg.com/magnet-uri/-/magnet-uri-5.2.4.tgz#7afe5b736af04445aff744c93a890a3710077688"
integrity sha512-VYaJMxhr8B9BrCiNINUsuhaEe40YnG+AQBwcqUKO66lSVaI9I3A1iH/6EmEwRI8OYUg5Gt+4lLE7achg676lrg==
dependencies:
thirty-two "^1.0.1"
uniq "^1.0.1"
make-dir@^2.0.0, make-dir@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5"
@ -11061,6 +11082,11 @@ text-table@0.2.0, text-table@^0.2.0:
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
thirty-two@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/thirty-two/-/thirty-two-1.0.2.tgz#4ca2fffc02a51290d2744b9e3f557693ca6b627a"
integrity sha1-TKL//AKlEpDSdEueP1V2k8prYno=
throat@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"
@ -11387,7 +11413,7 @@ url-parse@^1.4.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
url@^0.11.0:
url@0.11.0, url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
integrity sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=
@ -11502,6 +11528,11 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
video-name-parser@1.4.6:
version "1.4.6"
resolved "https://registry.yarnpkg.com/video-name-parser/-/video-name-parser-1.4.6.tgz#8e7926ab2ba9253fed290b399e453d3b0702c687"
integrity sha512-ZdeYjh8X4ms1EzjY/UoiTZ6JWbi8SYyOPGY0jESSLq2BAmdc5sZHi+F8J19Qz0y7H1WSpaltojsCkO1p2dH4YA==
vm-browserify@^1.0.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0"