@theoplayer/react-native-ui
Version:
A React Native UI for @theoplayer/react-native
372 lines (357 loc) • 15.3 kB
JavaScript
;
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