UNPKG

@theoplayer/react-native-ui

Version:

A React Native UI for @theoplayer/react-native

372 lines (357 loc) 15.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.UiContainer = exports.UI_CONTAINER_STYLE = exports.TOP_UI_CONTAINER_STYLE = exports.FULLSCREEN_CENTER_STYLE = exports.CENTER_UI_CONTAINER_STYLE = exports.BOTTOM_UI_CONTAINER_STYLE = exports.AD_UI_TOP_CONTAINER_STYLE = exports.AD_UI_CENTER_CONTAINER_STYLE = exports.AD_UI_BOTTOM_CONTAINER_STYLE = void 0; var _react = _interopRequireWildcard(require("react")); var _reactNative = require("react-native"); var _PlayerContext = require("../util/PlayerContext"); var _TVUtils = require("../util/TVUtils"); var _reactNativeTheoplayer = require("react-native-theoplayer"); var _ErrorDisplay = require("../message/ErrorDisplay"); var _Locale = require("../util/Locale"); var _usePointerMove = require("../../hooks/usePointerMove"); var _useThrottledCallback = require("../../hooks/useThrottledCallback"); var _jsxRuntime = require("react/jsx-runtime"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); } /** * The default style for a fullscreen centered view. */ const FULLSCREEN_CENTER_STYLE = exports.FULLSCREEN_CENTER_STYLE = { position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, alignItems: 'center', justifyContent: 'center' }; /** * The default style for the UI container. */ const UI_CONTAINER_STYLE = exports.UI_CONTAINER_STYLE = { position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, zIndex: 0, justifyContent: 'center', overflow: 'hidden' }; /** * The default style for the top container. */ const TOP_UI_CONTAINER_STYLE = exports.TOP_UI_CONTAINER_STYLE = { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 1, paddingTop: 10, paddingLeft: 10, paddingRight: 10, pointerEvents: 'box-none' }; /** * The default style for the center container. */ const CENTER_UI_CONTAINER_STYLE = exports.CENTER_UI_CONTAINER_STYLE = { alignSelf: 'center', justifyContent: 'center', flexDirection: 'row', width: '100%', pointerEvents: 'box-none' }; /** * The default style for the bottom container. */ const BOTTOM_UI_CONTAINER_STYLE = exports.BOTTOM_UI_CONTAINER_STYLE = { position: 'absolute', left: 0, right: 0, bottom: 0, zIndex: 1, paddingBottom: 10, paddingLeft: 10, paddingRight: 10, pointerEvents: 'box-none' }; /** * The default style for the ad container. */ const AD_UI_TOP_CONTAINER_STYLE = exports.AD_UI_TOP_CONTAINER_STYLE = TOP_UI_CONTAINER_STYLE; const AD_UI_CENTER_CONTAINER_STYLE = exports.AD_UI_CENTER_CONTAINER_STYLE = CENTER_UI_CONTAINER_STYLE; const AD_UI_BOTTOM_CONTAINER_STYLE = exports.AD_UI_BOTTOM_CONTAINER_STYLE = BOTTOM_UI_CONTAINER_STYLE; const WEB_POINTER_MOVE_THROTTLE = 500; /** * A component that does all the coordination between UI components. * - It provides all UI components with the PlayerContext, so they can access the styling and player. * - It provides slots for UI components to be places in the top/center/bottom positions. * - It uses animations to fade the UI in and out when applicable. */ const UiContainer = props => { const _currentFadeOutTimeout = (0, _react.useRef)(undefined); const fadeAnimation = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current; const [currentMenu, setCurrentMenu] = (0, _react.useState)(undefined); const [uiVisible_, setUiVisible] = (0, _react.useState)(true); const [error, setError] = (0, _react.useState)(undefined); const [didPlay, setDidPlay] = (0, _react.useState)(false); const [paused, setPaused] = (0, _react.useState)(true); const [casting, setCasting] = (0, _react.useState)(false); const [pip, setPip] = (0, _react.useState)(false); const [adInProgress, setAdInProgress] = (0, _react.useState)(false); const [adTapped, setAdTapped] = (0, _react.useState)(false); const appStateSubscription = (0, _react.useRef)(null); const _menus = (0, _react.useRef)([]).current; const { player, locale } = props; const combinedLocale = { ..._Locale.defaultLocale, ...locale }; // Animation control const fadeOutBlocked = !didPlay || currentMenu !== undefined || casting || pip || paused; const doFadeOut_ = (0, _react.useCallback)(() => { if (fadeOutBlocked) { return; } clearTimeout(_currentFadeOutTimeout.current); _reactNative.Animated.timing(fadeAnimation, { useNativeDriver: true, toValue: 0, duration: 200 }).start(() => { setUiVisible(false); }); }, [fadeOutBlocked, fadeAnimation]); const resumeUIFadeOut_ = (0, _react.useCallback)(() => { clearTimeout(_currentFadeOutTimeout.current); // @ts-ignore _currentFadeOutTimeout.current = setTimeout(doFadeOut_, props.theme.fadeAnimationTimoutMs); }, [doFadeOut_, props.theme.fadeAnimationTimoutMs]); const fadeInUI_ = (0, _react.useCallback)((fadeOutEnabled = true) => { clearTimeout(_currentFadeOutTimeout.current); _currentFadeOutTimeout.current = undefined; setUiVisible(true); _reactNative.Animated.timing(fadeAnimation, { useNativeDriver: true, toValue: 1, duration: 200 }).start(); if (fadeOutEnabled) { resumeUIFadeOut_(); } }, [fadeAnimation, resumeUIFadeOut_]); // TVOS events hook (0, _TVUtils.useTVOSEventHandler)(_event => { fadeInUI_(); }); // Ad listeners hook (0, _react.useEffect)(() => { const handleAdEvent = event => { if (event.subType === _reactNativeTheoplayer.AdEventType.AD_BREAK_BEGIN) { setAdInProgress(true); setAdTapped(false); } else if (event.subType === _reactNativeTheoplayer.AdEventType.AD_BREAK_END) { setAdInProgress(false); setAdTapped(false); } else if (event.subType === _reactNativeTheoplayer.AdEventType.AD_CLICKED || event.subType === _reactNativeTheoplayer.AdEventType.AD_TAPPED) { setAdTapped(true); } }; player.addEventListener(_reactNativeTheoplayer.PlayerEventType.AD_EVENT, handleAdEvent); appStateSubscription.current = _reactNative.AppState.addEventListener('change', state => { if (state === 'active' && adInProgress && adTapped) { player.play(); setAdTapped(false); } }); return () => { player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.AD_EVENT, handleAdEvent); appStateSubscription.current?.remove(); }; }, [player, adInProgress, adTapped]); // Player listeners hook (0, _react.useEffect)(() => { const handlePlay = () => { setDidPlay(true); setPaused(false); }; const handlePause = () => { setDidPlay(true); setPaused(true); }; const handleSourceChange = () => { setPaused(player.paused); }; const handleLoadStart = () => { setError(undefined); }; const handleError = event => { setError(event.error); }; const handleCastEvent = event => { if (event.subType === _reactNativeTheoplayer.CastEventType.CHROMECAST_STATE_CHANGE || event.subType === _reactNativeTheoplayer.CastEventType.AIRPLAY_STATE_CHANGE) { setCasting(event.state === 'connecting' || event.state === 'connected'); } }; const handleEnded = () => { fadeInUI_(false); }; const handlePresentationModeChange = event => { setPip(event.presentationMode === _reactNativeTheoplayer.PresentationMode.pip); if (event.presentationMode !== _reactNativeTheoplayer.PresentationMode.pip) { fadeInUI_(); } }; player.addEventListener(_reactNativeTheoplayer.PlayerEventType.LOAD_START, handleLoadStart); player.addEventListener(_reactNativeTheoplayer.PlayerEventType.ERROR, handleError); player.addEventListener(_reactNativeTheoplayer.PlayerEventType.CAST_EVENT, handleCastEvent); player.addEventListener(_reactNativeTheoplayer.PlayerEventType.PLAY, handlePlay); player.addEventListener(_reactNativeTheoplayer.PlayerEventType.PLAYING, handlePlay); player.addEventListener(_reactNativeTheoplayer.PlayerEventType.PAUSE, handlePause); player.addEventListener(_reactNativeTheoplayer.PlayerEventType.SOURCE_CHANGE, handleSourceChange); player.addEventListener(_reactNativeTheoplayer.PlayerEventType.ENDED, handleEnded); player.addEventListener(_reactNativeTheoplayer.PlayerEventType.PRESENTATIONMODE_CHANGE, handlePresentationModeChange); setPip(player.presentationMode === 'picture-in-picture'); return () => { player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.LOAD_START, handleLoadStart); player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.ERROR, handleError); player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.CAST_EVENT, handleCastEvent); player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.PLAY, handlePlay); player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.PLAYING, handlePlay); player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.PAUSE, handlePause); player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.SOURCE_CHANGE, handleSourceChange); player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.ENDED, handleEnded); player.removeEventListener(_reactNativeTheoplayer.PlayerEventType.PRESENTATIONMODE_CHANGE, handlePresentationModeChange); }; }, [player, paused, resumeUIFadeOut_, fadeInUI_, currentMenu]); // State reflecting hook (0, _react.useEffect)(() => { if (currentMenu === undefined && !paused && didPlay) { resumeUIFadeOut_(); } else { fadeInUI_(false); } }, [currentMenu, paused, didPlay, fadeInUI_, resumeUIFadeOut_]); // Interactions const openMenu_ = menuConstructor => { _menus.push(menuConstructor); setCurrentMenu(menuConstructor()); }; const closeCurrentMenu_ = () => { _menus.pop(); const nextMenu = _menus.length > 0 ? _menus[_menus.length - 1] : undefined; setCurrentMenu(nextMenu?.()); }; const enterPip_ = () => { // Make sure the UI is disabled first before entering PIP clearTimeout(_currentFadeOutTimeout.current); fadeAnimation.setValue(0); setUiVisible(false); player.presentationMode = _reactNativeTheoplayer.PresentationMode.pip; }; /** * Request to show the UI due to user input. */ const onUserAction_ = (0, _react.useCallback)(() => { if (!didPlay) { return; } fadeInUI_(); }, [fadeInUI_, didPlay]); /** * On Web platform, use (throttled) pointer moves on the root container to enable showing/hiding instead of the UI container. * If an ad is playing, the UI should pass through all pointer events ("box-none") in order for ad clickThrough to work. * Throttle the callback avoids hammering the fade-in animation. */ (0, _usePointerMove.usePointerMove)('#theoplayer-root-container', (0, _useThrottledCallback.useThrottledCallback)(onUserAction_, WEB_POINTER_MOVE_THROTTLE), doFadeOut_); const combinedUiContainerStyle = [UI_CONTAINER_STYLE, props.style]; if (error !== undefined) { return /*#__PURE__*/(0, _jsxRuntime.jsx)(_ErrorDisplay.ErrorDisplay, { error: error }); } if (_reactNative.Platform.OS !== 'web' && pip) { return /*#__PURE__*/(0, _jsxRuntime.jsx)(_jsxRuntime.Fragment, {}); } const ui = { buttonsEnabled_: uiVisible_, onUserAction_, openMenu_, closeCurrentMenu_, enterPip_ }; const hasUIComponents = props.top || props.center || props.bottom; const hasAdUIComponents = props.adTop || props.adCenter || props.adBottom; const showUIBackground = !adInProgress && hasUIComponents || adInProgress && hasAdUIComponents; return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_PlayerContext.PlayerContext.Provider, { value: { player, style: props.theme, ui, adInProgress, locale: combinedLocale }, children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: FULLSCREEN_CENTER_STYLE, pointerEvents: 'none', children: props.behind }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, { style: [combinedUiContainerStyle, { opacity: fadeAnimation }], onTouchMove: onUserAction_, onStartShouldSetResponder: () => true, onResponderRelease: onUserAction_, pointerEvents: adInProgress ? 'box-none' : uiVisible_ ? 'auto' : 'box-only', children: uiVisible_ && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [showUIBackground && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [combinedUiContainerStyle, { backgroundColor: props.theme.colors.uiBackground }] // become responder on the start of a touch or mouse click , onStartShouldSetResponder: () => true // at the end of the touch or mouse click, fade-out UI , onResponderRelease: doFadeOut_, pointerEvents: adInProgress ? 'box-none' : uiVisible_ ? 'auto' : 'box-only' }), currentMenu !== undefined && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [combinedUiContainerStyle], children: currentMenu }), currentMenu === undefined && !adInProgress && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [didPlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [TOP_UI_CONTAINER_STYLE, props.topStyle], pointerEvents: 'box-none', children: props.top }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [CENTER_UI_CONTAINER_STYLE, props.centerStyle], pointerEvents: 'box-none', children: props.center }), didPlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [BOTTOM_UI_CONTAINER_STYLE, props.bottomStyle], pointerEvents: 'box-none', children: props.bottom }), props.children] }), currentMenu === undefined && adInProgress && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [AD_UI_TOP_CONTAINER_STYLE, props.adTopStyle], children: props.adTop }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [AD_UI_CENTER_CONTAINER_STYLE, props.adCenterStyle], children: props.adCenter }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, { style: [AD_UI_BOTTOM_CONTAINER_STYLE, props.adBottomStyle], children: props.adBottom })] })] }) })] }); }; exports.UiContainer = UiContainer; //# sourceMappingURL=UiContainer.js.map