@theoplayer/react-native-ui
Version:
A React Native UI for @theoplayer/react-native
383 lines (369 loc) • 12.5 kB
JavaScript
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { Animated, AppState, Platform, TouchableOpacity, View } from 'react-native';
import { PlayerContext } from '../util/PlayerContext';
import { useTVOSEventHandler } from '../util/TVUtils';
import { AdEventType, CastEventType, PlayerEventType, PresentationMode } from 'react-native-theoplayer';
import { ErrorDisplay } from '../message/ErrorDisplay';
import { defaultLocale } from '../util/Locale';
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
/**
* The default style for a fullscreen centered view.
*/
export const FULLSCREEN_CENTER_STYLE = {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
alignItems: 'center',
justifyContent: 'center'
};
/**
* The default style for the UI container.
*/
export const UI_CONTAINER_STYLE = {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
zIndex: 0,
justifyContent: 'center',
overflow: 'hidden'
};
/**
* The default style for the ad container.
*/
export const AD_CONTAINER_STYLE = {
position: 'absolute',
top: 100,
left: 0,
bottom: 100,
right: 0,
zIndex: 0,
justifyContent: 'center',
overflow: 'hidden'
};
/**
* The default style for the top container.
*/
export const TOP_UI_CONTAINER_STYLE = {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 1,
paddingTop: 10,
paddingLeft: 10,
paddingRight: 10
};
/**
* The default style for the center container.
*/
export const CENTER_UI_CONTAINER_STYLE = {
alignSelf: 'center',
justifyContent: 'center',
flexDirection: 'row',
width: '100%'
};
/**
* The default style for the bottom container.
*/
export const BOTTOM_UI_CONTAINER_STYLE = {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
paddingBottom: 10,
paddingLeft: 10,
paddingRight: 10
};
/**
* The default style for the ad container.
*/
export const AD_UI_TOP_CONTAINER_STYLE = TOP_UI_CONTAINER_STYLE;
export const AD_UI_CENTER_CONTAINER_STYLE = CENTER_UI_CONTAINER_STYLE;
export const AD_UI_BOTTOM_CONTAINER_STYLE = BOTTOM_UI_CONTAINER_STYLE;
/**
* 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.
*/
export const UiContainer = props => {
const _currentFadeOutTimeout = useRef(undefined);
const fadeAnimation = useRef(new Animated.Value(1)).current;
const [currentMenu, setCurrentMenu] = useState(undefined);
const [isPassingPointerEvents, setIsPassingPointerEvents] = useState(true);
const [buttonsEnabled_, setButtonsEnabled] = useState(true);
const [error, setError] = useState(undefined);
const [didPlay, setDidPlay] = useState(false);
const [paused, setPaused] = useState(true);
const [casting, setCasting] = useState(false);
const [pip, setPip] = useState(false);
const [adInProgress, setAdInProgress] = useState(false);
const [adTapped, setAdTapped] = useState(false);
const appStateSubscription = useRef(null);
const _menus = useRef([]).current;
const {
player,
locale
} = props;
const combinedLocale = {
...defaultLocale,
...locale
};
// Animation control
const fadeOutBlocked = !didPlay || currentMenu !== undefined || casting || pip || paused;
const doFadeOut_ = useCallback(() => {
if (fadeOutBlocked) {
return;
}
clearTimeout(_currentFadeOutTimeout.current);
setButtonsEnabled(false);
Animated.timing(fadeAnimation, {
useNativeDriver: true,
toValue: 0,
duration: 200
}).start(() => {
setIsPassingPointerEvents(false);
});
}, [fadeOutBlocked, fadeAnimation]);
const resumeUIFadeOut_ = useCallback(() => {
clearTimeout(_currentFadeOutTimeout.current);
// @ts-ignore
_currentFadeOutTimeout.current = setTimeout(doFadeOut_, props.theme.fadeAnimationTimoutMs);
}, [doFadeOut_, props.theme.fadeAnimationTimoutMs]);
const fadeInUI_ = useCallback((fadeOutEnabled = true) => {
clearTimeout(_currentFadeOutTimeout.current);
_currentFadeOutTimeout.current = undefined;
if (!isPassingPointerEvents) {
setIsPassingPointerEvents(true);
Animated.timing(fadeAnimation, {
useNativeDriver: true,
toValue: 1,
duration: 200
}).start(() => {
setButtonsEnabled(true);
});
}
if (fadeOutEnabled) {
resumeUIFadeOut_();
}
}, [fadeAnimation, isPassingPointerEvents, resumeUIFadeOut_]);
// TVOS events hook
useTVOSEventHandler(_event => {
fadeInUI_();
});
// Ad listeners hook
useEffect(() => {
const handleAdEvent = event => {
if (event.subType === AdEventType.AD_BREAK_BEGIN) {
setAdInProgress(true);
setAdTapped(false);
} else if (event.subType === AdEventType.AD_BREAK_END) {
setAdInProgress(false);
setAdTapped(false);
} else if (event.subType === AdEventType.AD_CLICKED || event.subType === AdEventType.AD_TAPPED) {
setAdTapped(true);
}
};
player.addEventListener(PlayerEventType.AD_EVENT, handleAdEvent);
appStateSubscription.current = AppState.addEventListener('change', state => {
if (state === 'active' && adInProgress && adTapped) {
player.play();
setAdTapped(false);
}
});
return () => {
player.removeEventListener(PlayerEventType.AD_EVENT, handleAdEvent);
appStateSubscription.current?.remove();
};
}, [player, adInProgress, adTapped]);
// Player listeners hook
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 === CastEventType.CHROMECAST_STATE_CHANGE || event.subType === CastEventType.AIRPLAY_STATE_CHANGE) {
setCasting(event.state === 'connecting' || event.state === 'connected');
}
};
const handleEnded = () => {
fadeInUI_(false);
};
const handlePresentationModeChange = event => {
setPip(event.presentationMode === PresentationMode.pip);
if (event.presentationMode !== PresentationMode.pip) {
fadeInUI_();
}
};
player.addEventListener(PlayerEventType.LOAD_START, handleLoadStart);
player.addEventListener(PlayerEventType.ERROR, handleError);
player.addEventListener(PlayerEventType.CAST_EVENT, handleCastEvent);
player.addEventListener(PlayerEventType.PLAY, handlePlay);
player.addEventListener(PlayerEventType.PLAYING, handlePlay);
player.addEventListener(PlayerEventType.PAUSE, handlePause);
player.addEventListener(PlayerEventType.SOURCE_CHANGE, handleSourceChange);
player.addEventListener(PlayerEventType.ENDED, handleEnded);
player.addEventListener(PlayerEventType.PRESENTATIONMODE_CHANGE, handlePresentationModeChange);
setPip(player.presentationMode === 'picture-in-picture');
return () => {
player.removeEventListener(PlayerEventType.LOAD_START, handleLoadStart);
player.removeEventListener(PlayerEventType.ERROR, handleError);
player.removeEventListener(PlayerEventType.CAST_EVENT, handleCastEvent);
player.removeEventListener(PlayerEventType.PLAY, handlePlay);
player.removeEventListener(PlayerEventType.PLAYING, handlePlay);
player.removeEventListener(PlayerEventType.PAUSE, handlePause);
player.removeEventListener(PlayerEventType.SOURCE_CHANGE, handleSourceChange);
player.removeEventListener(PlayerEventType.ENDED, handleEnded);
player.removeEventListener(PlayerEventType.PRESENTATIONMODE_CHANGE, handlePresentationModeChange);
};
}, [player, paused, resumeUIFadeOut_, fadeInUI_, currentMenu]);
// State reflecting hook
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 playPause_ = () => {
if (paused) {
player.play();
} else {
player.pause();
}
};
const enterPip_ = () => {
// Make sure the UI is disabled first before entering PIP
clearTimeout(_currentFadeOutTimeout.current);
setButtonsEnabled(false);
Animated.timing(fadeAnimation, {
useNativeDriver: true,
toValue: 0,
duration: 0
}).start(() => {
setIsPassingPointerEvents(false);
player.presentationMode = PresentationMode.pip;
});
};
/**
* Request to show the UI due to user input.
*/
const onUserAction_ = useCallback(() => {
if (!didPlay) {
return;
}
fadeInUI_();
}, [fadeInUI_, didPlay]);
const combinedUiContainerStyle = [UI_CONTAINER_STYLE, props.style];
const combinedAdContainerStyle = [AD_CONTAINER_STYLE, props.style];
const showMobileAdLayout = adInProgress && Platform.OS != 'web';
if (error !== undefined) {
return /*#__PURE__*/_jsx(ErrorDisplay, {
error: error
});
}
if (Platform.OS !== 'web' && pip) {
return /*#__PURE__*/_jsx(_Fragment, {});
}
const ui = {
buttonsEnabled_,
onUserAction_,
openMenu_,
closeCurrentMenu_,
enterPip_
};
return /*#__PURE__*/_jsxs(PlayerContext.Provider, {
value: {
player,
style: props.theme,
ui,
adInProgress,
locale: combinedLocale
},
children: [/*#__PURE__*/_jsx(View, {
style: FULLSCREEN_CENTER_STYLE,
pointerEvents: 'none',
children: props.behind
}), !showMobileAdLayout && /*#__PURE__*/_jsx(Animated.View, {
style: [combinedUiContainerStyle, {
opacity: fadeAnimation
}],
onTouchStart: onUserAction_,
onTouchMove: onUserAction_,
pointerEvents: isPassingPointerEvents ? 'auto' : 'box-only',
...(Platform.OS === 'web' ? {
onMouseMove: onUserAction_,
onMouseLeave: doFadeOut_
} : {}),
children: /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(View, {
style: [combinedUiContainerStyle, {
backgroundColor: props.theme.colors.uiBackground
}],
onTouchStart: doFadeOut_
}), currentMenu !== undefined && /*#__PURE__*/_jsx(View, {
style: [combinedUiContainerStyle],
children: currentMenu
}), currentMenu === undefined && !adInProgress && /*#__PURE__*/_jsxs(_Fragment, {
children: [didPlay && /*#__PURE__*/_jsx(View, {
style: [TOP_UI_CONTAINER_STYLE, props.topStyle],
children: props.top
}), /*#__PURE__*/_jsx(View, {
style: [CENTER_UI_CONTAINER_STYLE, props.centerStyle],
children: props.center
}), didPlay && /*#__PURE__*/_jsx(View, {
style: [BOTTOM_UI_CONTAINER_STYLE, props.bottomStyle],
children: props.bottom
}), props.children]
}), currentMenu === undefined && adInProgress && /*#__PURE__*/_jsxs(_Fragment, {
children: [/*#__PURE__*/_jsx(View, {
style: [AD_UI_TOP_CONTAINER_STYLE, props.adTopStyle],
children: props.adTop
}), /*#__PURE__*/_jsx(View, {
style: [AD_UI_CENTER_CONTAINER_STYLE, props.adCenterStyle],
children: props.adCenter
}), /*#__PURE__*/_jsx(View, {
style: [AD_UI_BOTTOM_CONTAINER_STYLE, props.adBottomStyle],
children: props.adBottom
})]
})]
})
}), showMobileAdLayout && /*#__PURE__*/_jsx(View, {
style: [combinedAdContainerStyle],
children: /*#__PURE__*/_jsx(TouchableOpacity, {
style: [FULLSCREEN_CENTER_STYLE],
onPress: playPause_
})
})]
});
};
//# sourceMappingURL=UiContainer.js.map