mirror of
https://github.com/Stremio/stremio-web.git
synced 2026-03-11 17:15:48 +00:00
Merge pull request #183 from Stremio/streaming-server-middleware
Streaming server middleware
This commit is contained in:
commit
c8157be70e
54 changed files with 645 additions and 2457 deletions
|
|
@ -10,7 +10,9 @@
|
|||
},
|
||||
"globals": {
|
||||
"YT": "readonly",
|
||||
"FB": "readonly"
|
||||
"FB": "readonly",
|
||||
"cast": "readonly",
|
||||
"chrome": "readonly"
|
||||
},
|
||||
"env": {
|
||||
"node": true,
|
||||
|
|
|
|||
1
.npmrc
1
.npmrc
|
|
@ -1 +0,0 @@
|
|||
@stremio:registry=https://npm.pkg.github.com
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const LibItem = ({ id, ...props }) => {
|
|||
}
|
||||
case 'dismiss': {
|
||||
if (typeof id === 'string') {
|
||||
core.dispatch({
|
||||
core.transport.dispatch({
|
||||
action: 'Ctx',
|
||||
args: {
|
||||
action: 'RewindLibraryItem',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
@ -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>
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
10
src/routes/Player/Video/styles.less
Normal file
10
src/routes/Player/Video/styles.less
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.video-container {
|
||||
.video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
* {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
102
src/services/Chromecast/Chromecast.js
Normal file
102
src/services/Chromecast/Chromecast.js
Normal 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;
|
||||
104
src/services/Chromecast/ChromecastTransport.js
Normal file
104
src/services/Chromecast/ChromecastTransport.js
Normal 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;
|
||||
5
src/services/Chromecast/index.js
Normal file
5
src/services/Chromecast/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2020 Smart code 203358507
|
||||
|
||||
const Chromecast = require('./Chromecast');
|
||||
|
||||
module.exports = Chromecast;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
36
src/services/Core/CoreTransport.js
Normal file
36
src/services/Core/CoreTransport.js
Normal 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;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
// Copyright (C) 2017-2020 Smart code 203358507
|
||||
|
||||
const KeyboardNavigation = require('./KeyboardNavigation');
|
||||
|
||||
module.exports = KeyboardNavigation;
|
||||
|
|
@ -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;
|
||||
5
src/services/KeyboardShortcuts/index.js
Normal file
5
src/services/KeyboardShortcuts/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (C) 2017-2020 Smart code 203358507
|
||||
|
||||
const KeyboardShortcuts = require('./KeyboardShortcuts');
|
||||
|
||||
module.exports = KeyboardShortcuts;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# stremio-video
|
||||
|
||||
### TODO move this folder in a separate repo
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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
|
||||
});
|
||||
|
|
@ -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;
|
||||
|
|
@ -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']
|
||||
})
|
||||
]
|
||||
|
|
|
|||
53
yarn.lock
53
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue