import React, { PureComponent } from 'react'; import { findNodeHandle, StyleSheet, requireNativeComponent, View, UIManager, Platform, NativeSyntheticEvent, HostComponent } from 'react-native'; import type { DurationChangeEvent, ErrorEvent, LoadedMetadataEvent, ReadyStateChangeEvent, THEOplayerViewComponent, THEOplayerViewProps, TimeUpdateEvent, ProgressEvent, PlayerError, SegmentNotFoundEvent, TextTrackListEvent, TextTrackEvent, AdEvent, AdsAPI, MediaTrackEvent, MediaTrackListEvent, CastAPI, CastEvent, } from '@wouterds/react-native-theoplayer'; import styles from './THEOplayerView.style'; import type { SourceDescription } from '@wouterds/react-native-theoplayer'; import { THEOplayerNativeAdsAPI } from './ads/THEOplayerNativeAdsAPI'; import { THEOplayerNativeCastAPI } from './cast/THEOplayerNativeCastApi'; import { decodeNanInf } from './utils/TypeUtils'; interface THEOplayerRCTViewProps extends THEOplayerViewProps { ref: React.RefObject; src: SourceDescription; seek?: number; onNativeSourceChange: () => void; onNativeLoadStart: () => void; onNativeLoadedData: () => void; onNativeLoadedMetadata: (event: NativeSyntheticEvent) => void; onNativeReadyStateChange?: (event: NativeSyntheticEvent) => void; onNativeError: (event: NativeSyntheticEvent) => void; onNativeProgress: (event: NativeSyntheticEvent) => void; onNativePlay: () => void; onNativePlaying: () => void; onNativePause: () => void; onNativeSeeking: () => void; onNativeSeeked: () => void; onNativeEnded: () => void; onNativeTimeUpdate: (event: NativeSyntheticEvent) => void; onNativeDurationChange: (event: NativeSyntheticEvent) => void; onNativeSegmentNotFound: (event: NativeSyntheticEvent) => void; onNativeTextTrackListEvent: (event: NativeSyntheticEvent) => void; onNativeTextTrackEvent: (event: NativeSyntheticEvent) => void; onNativeMediaTrackListEvent: (event: NativeSyntheticEvent) => void; onNativeMediaTrackEvent: (event: NativeSyntheticEvent) => void; onNativeAdEvent: (event: NativeSyntheticEvent) => void; onNativeCastEvent: (event: NativeSyntheticEvent) => void; onNativeFullscreenPlayerWillPresent?: () => void; onNativeFullscreenPlayerDidPresent?: () => void; onNativeFullscreenPlayerWillDismiss?: () => void; onNativeFullscreenPlayerDidDismiss?: () => void; } interface THEOplayerRCTViewState { isBuffering: boolean; error?: PlayerError; } interface THEOplayerViewNativeComponent extends THEOplayerViewComponent, HostComponent { setNativeProps: (props: Partial) => void; } export class THEOplayerView extends PureComponent implements THEOplayerViewComponent { private readonly _root: React.RefObject; private readonly _adsApi: THEOplayerNativeAdsAPI; private readonly _castApi: THEOplayerNativeCastAPI; private static initialState: THEOplayerRCTViewState = { isBuffering: false, error: undefined, }; constructor(props: THEOplayerRCTViewProps) { super(props); this._root = React.createRef(); this.state = THEOplayerView.initialState; this._adsApi = new THEOplayerNativeAdsAPI(this); this._castApi = new THEOplayerNativeCastAPI(this); } componentWillUnmount() { if (Platform.OS === 'ios') { // on iOS, we trigger an explicit 'destroy' to clean up the underlying THEOplayer this.destroyTheoPlayer(); } } private destroyTheoPlayer() { const node = findNodeHandle(this._root.current); const command = (UIManager as { [index: string]: any })['THEOplayerRCTView'].Commands.destroy; const params: any[] = []; UIManager.dispatchViewManagerCommand(node, command, params); } public seek(time: number): void { if (isNaN(time)) { throw new Error('Specified time is not a number'); } this.setNativeProps({ seek: time }); } public get nativeHandle(): number | null { return findNodeHandle(this._root.current); } public get ads(): AdsAPI { return this._adsApi; } public get cast(): CastAPI { return this._castApi; } private reset() { this.setState(THEOplayerView.initialState); } private setNativeProps(nativeProps: Partial) { if (this._root?.current) { this._root.current.setNativeProps(nativeProps); } } private maybeChangeBufferingState(isBuffering: boolean) { const { isBuffering: wasBuffering, error } = this.state; const { paused } = this.props; // do not change state to buffering in case of an error or if the player is paused const newIsBuffering = isBuffering && !error && !paused; this.setState({ isBuffering: newIsBuffering }); // notify change in buffering state if (newIsBuffering !== wasBuffering && this.props.onBufferingStateChange) { this.props.onBufferingStateChange(isBuffering); } } private _onSourceChange = () => { this.reset(); if (this.props.onSourceChange) { this.props.onSourceChange(); } }; private _onLoadStart = () => { // potentially notify change in buffering state this.maybeChangeBufferingState(true); if (this.props.onLoadStart) { this.props.onLoadStart(); } }; private _onLoadedData = () => { if (this.props.onLoadedData) { this.props.onLoadedData(); } }; private _onLoadedMetadata = (event: NativeSyntheticEvent) => { if (this.props.onLoadedMetadata) { this.props.onLoadedMetadata({ ...event.nativeEvent, duration: decodeNanInf(event.nativeEvent.duration), }); } }; private _onError = (event: NativeSyntheticEvent) => { const { error } = event.nativeEvent; this.setState({ error }); // potentially notify change in buffering state this.maybeChangeBufferingState(false); if (this.props.onError) { this.props.onError(event.nativeEvent); } }; private _onProgress = (event: NativeSyntheticEvent) => { if (this.props.onProgress) { this.props.onProgress(event.nativeEvent); } }; private _onPlay = () => { if (this.props.onPlay) { this.props.onPlay(); } }; private _onPlaying = () => { // potentially notify change in buffering state this.maybeChangeBufferingState(false); if (this.props.onPlaying) { this.props.onPlaying(); } }; private _onPause = () => { if (this.props.onPause) { this.props.onPause(); } }; private _onSeeking = () => { if (this.props.onSeeking) { this.props.onSeeking(); } }; private _onSeeked = () => { if (this.props.onSeeked) { this.props.onSeeked(); } }; private _onEnded = () => { if (this.props.onEnded) { this.props.onEnded(); } }; private _onReadStateChange = (event: NativeSyntheticEvent) => { // potentially notify change in buffering state this.maybeChangeBufferingState(event.nativeEvent.readyState < 3); if (this.props.onReadyStateChange) { this.props.onReadyStateChange(event.nativeEvent); } }; private _onTimeUpdate = (event: NativeSyntheticEvent) => { if (this.props.onTimeUpdate) { this.props.onTimeUpdate(event.nativeEvent); } }; private _onDurationChange = (event: NativeSyntheticEvent) => { if (this.props.onDurationChange) { this.props.onDurationChange({ duration: decodeNanInf(event.nativeEvent.duration), }); } }; private _onSegmentNotFound = (event: NativeSyntheticEvent) => { if (this.props.onSegmentNotFound) { this.props.onSegmentNotFound(event.nativeEvent); } }; private _onTextTrackListEvent = (event: NativeSyntheticEvent) => { if (this.props.onTextTrackListEvent) { this.props.onTextTrackListEvent(event.nativeEvent); } }; private _onTextTrackEvent = (event: NativeSyntheticEvent) => { if (this.props.onTextTrackEvent) { this.props.onTextTrackEvent(event.nativeEvent); } }; private _onMediaTrackListEvent = (event: NativeSyntheticEvent) => { if (this.props.onMediaTrackListEvent) { this.props.onMediaTrackListEvent(event.nativeEvent); } }; private _onMediaTrackEvent = (event: NativeSyntheticEvent) => { if (this.props.onMediaTrackEvent) { this.props.onMediaTrackEvent(event.nativeEvent); } }; private _onAdEvent = (event: NativeSyntheticEvent) => { if (this.props.onAdEvent) { this.props.onAdEvent(event.nativeEvent); } }; private _onCastEvent = (event: NativeSyntheticEvent) => { if (this.props.onCastEvent) { this.props.onCastEvent(event.nativeEvent); } }; private _onFullscreenPlayerWillPresent = () => { if (this.props.onFullscreenPlayerWillPresent) { this.props.onFullscreenPlayerWillPresent(); } }; private _onFullscreenPlayerDidPresent = () => { if (this.props.onFullscreenPlayerDidPresent) { this.props.onFullscreenPlayerDidPresent(); } }; private _onFullscreenPlayerWillDismiss = () => { if (this.props.onFullscreenPlayerWillDismiss) { this.props.onFullscreenPlayerWillDismiss(); } }; private _onFullscreenPlayerDidDismiss = () => { if (this.props.onFullscreenPlayerDidDismiss) { this.props.onFullscreenPlayerDidDismiss(); } }; private buildWrapperProps(): THEOplayerViewProps { const { targetVideoQuality } = this.props; return Object.assign( {}, { ...this.props, // Always pass an array for targetVideoQuality. targetVideoQuality: !targetVideoQuality ? [] : Array.isArray(targetVideoQuality) ? targetVideoQuality : [targetVideoQuality], }, ); } public render(): JSX.Element { const wrapperProps = this.buildWrapperProps(); return ( ); } } const LINKING_ERROR = `The package '@wouterds/react-native-theoplayer' doesn't seem to be linked. Make sure: \n\n` + Platform.select({ ios: "- You have run 'pod install'\n", default: '' }) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo managed workflow\n'; const ComponentName = 'THEOplayerRCTView'; const THEOplayerRCTView = UIManager.getViewManagerConfig(ComponentName) != null ? requireNativeComponent(ComponentName) : () => { throw new Error(LINKING_ERROR); };