import React from 'react'; import { type EmitterSubscription, findNodeHandle, type NativeSyntheticEvent, Platform, type StyleProp, UIManager, type ViewStyle, } from 'react-native'; import FireworkSDK from '../FireworkSDK'; import type FWError from '../models/FWError'; import { FWEventName } from '../models/FWEventName'; import type { PlayerDeckConfiguration } from '../models/PlayerDeckConfiguration'; import type { PlayerDeckSource } from '../models/PlayerDeckSource'; import type VideoPlaybackDetails from '../models/VideoPlaybackDetails'; import type VideoPlayerConfiguration from '../models/VideoPlayerConfiguration'; import type VideoPlayerNativeConfiguration from '../models/VideoPlayerNativeConfiguration'; import { FireworkSDKModuleEventEmitter } from '../modules/FireworkSDKModule'; import FWGlobalState from '../utils/FWGlobalState'; import gennerateJsonKey from '../utils/FWJsonUtil'; import FWLoggerUtil from '../utils/FWLoggerUtil'; import FWPlayerDeck from './FWPlayerDeck'; import { FWComponentType } from '../models/FWComponentType'; export interface IPlayerDeckProps { /** * Standard React Native View Style. */ style?: StyleProp; /** * The video feed source for the player deck. * Defaults to discover. */ source: PlayerDeckSource; /** * Channel id. Required when the source is set as channel, playlist, or dynamicContent. */ channel?: string; /** * Playlist id. Required when the source is set as playlist. */ playlist?: string; /** * PlaylistGroup id. Required when the source is set as playlistGroup. * Only supported on iOS. */ playlistGroup?: string; /** * Dynamic content parameters. Required when the source is set as dynamicContent. */ dynamicContentParameters?: { [key: string]: string[] }; /** * Hashtag filter expression. Required when the source is set as hashtagPlaylist. */ hashtagFilterExpression?: string; /** * Product ids used to generate the sku feed. */ productIds?: string[]; /** * The video or live stream id. Only supported on iOS. */ contentId?: string; /** * Specifies if Picture in Picture is enabled. */ enablePictureInPicture?: boolean; /** * Controls whether the system (native iOS) Picture-in-Picture is enabled. * Defaults to true. Only supported on iOS. */ enableSystemPictureInPicture?: boolean; /** * Configuration of the player deck. */ playerDeckConfiguration?: PlayerDeckConfiguration; /** * Configuration of the video player (for full-screen playback). */ videoPlayerConfiguration?: VideoPlayerConfiguration; /** * The feed loading result callback. * It means loading successfully when error equals to undefined. */ onPlayerDeckLoadFinished?: (error?: FWError) => void; /** * The callback is triggered when there are no items in the player deck. */ onPlayerDeckEmpty?: (error?: FWError) => void; /** * Start Picture in Picture callback. Only supported on iOS. */ onPlayerDeckDidStartPictureInPicture?: (error?: FWError) => void; /** * Stop Picture in Picture callback. Only supported on iOS. */ onPlayerDeckDidStopPictureInPicture?: (error?: FWError) => void; /** * The host app could use this callback to get feed id. */ onPlayerDeckGetFeedId?: (feedId: string) => void; /** * The callback is triggered when videos are loaded in the player deck. * It fires for the initial page and again for every subsequent page * (load more), and each call delivers only the newly loaded batch of videos * rather than the accumulated list. On Android it also fires for * pull-to-refresh and for the pages loaded by the full-screen player opened * from this player deck. */ onPlayerDeckVideosLoaded?: (videos: VideoPlaybackDetails[]) => void; } interface IPlayerDeckState { sdkInitSucceeded: boolean; } const NativeComponentName = 'FWPlayerDeck'; class PlayerDeck extends React.Component { private _nativeComponentRef = React.createRef(); private _subscriptions: EmitterSubscription[] = []; private _loaded: boolean = false; constructor(props: IPlayerDeckProps) { super(props); this.state = { sdkInitSucceeded: FWGlobalState.getInstance().sdkInitSucceeded, }; } public refresh = () => { FWLoggerUtil.log(`PlayerDeck refresh ${JSON.stringify(this.props)}`); const nativeNodeHandle = findNodeHandle(this._nativeComponentRef.current); let commandId: string | number = UIManager.getViewManagerConfig(NativeComponentName).Commands.refresh ?? ''; if (Platform.OS === 'android') { commandId = commandId.toString(); } let reactTag: number | null = findNodeHandle(nativeNodeHandle); FWLoggerUtil.log( `PlayerDeck refresh commandId: ${commandId} nativeNodeHandle: ${nativeNodeHandle} reactTag: ${reactTag}` ); if (!nativeNodeHandle || !reactTag) { return; } UIManager.dispatchViewManagerCommand(reactTag, commandId, []); }; private _onPlayerDeckLoadFinished = (event: NativeSyntheticEvent) => { const { onPlayerDeckLoadFinished, onPlayerDeckEmpty } = this.props; const { name, reason } = event.nativeEvent; if (onPlayerDeckLoadFinished) { if (name) { let error: FWError = { name }; if (reason) { error.reason = reason; } if (Platform.OS === 'android') { if (!this._loaded) { onPlayerDeckLoadFinished(error); } if (!this._loaded) { onPlayerDeckEmpty?.(error); } } else { onPlayerDeckLoadFinished(error); if (!this._loaded) { onPlayerDeckEmpty?.(error); } } } else { if (Platform.OS === 'android') { if (!this._loaded) { onPlayerDeckLoadFinished(); } } else { onPlayerDeckLoadFinished(); } this._loaded = true; } } }; private _onPlayerDeckEmpty = (_event: NativeSyntheticEvent) => { FWLoggerUtil.log(`PlayerDeck _onPlayerDeckEmpty`); const { onPlayerDeckEmpty } = this.props; if (onPlayerDeckEmpty) { onPlayerDeckEmpty(); } }; private _onPlayerDeckDidStartPictureInPicture = ( event: NativeSyntheticEvent ) => { FWLoggerUtil.log( `PlayerDeck _onPlayerDeckDidStartPictureInPicture ${JSON.stringify( event.nativeEvent )}` ); const { onPlayerDeckDidStartPictureInPicture } = this.props; const { name, reason } = event.nativeEvent; if (onPlayerDeckDidStartPictureInPicture) { if (name) { onPlayerDeckDidStartPictureInPicture( reason ? { name, reason } : { name } ); } else { onPlayerDeckDidStartPictureInPicture(); } } }; private _onPlayerDeckDidStopPictureInPicture = ( event: NativeSyntheticEvent ) => { FWLoggerUtil.log( `PlayerDeck _onPlayerDeckDidStopPictureInPicture ${JSON.stringify( event.nativeEvent )}` ); const { onPlayerDeckDidStopPictureInPicture } = this.props; const { name, reason } = event.nativeEvent; if (onPlayerDeckDidStopPictureInPicture) { if (name) { onPlayerDeckDidStopPictureInPicture( reason ? { name, reason } : { name } ); } else { onPlayerDeckDidStopPictureInPicture(); } } }; private _onPlayerDeckGetFeedId = (event: NativeSyntheticEvent) => { FWLoggerUtil.log( `PlayerDeck onPlayerDeckGetFeedId ${JSON.stringify(event.nativeEvent)}` ); const { onPlayerDeckGetFeedId } = this.props; const { feedId } = event.nativeEvent; if (feedId) { FWGlobalState.getInstance().componentTypeByFeedIdMap.set( feedId, FWComponentType.playerDeck ); } if (onPlayerDeckGetFeedId) { onPlayerDeckGetFeedId(feedId ?? ''); } }; private _onPlayerDeckVideosLoaded = (event: NativeSyntheticEvent) => { const { onPlayerDeckVideosLoaded } = this.props; const videos: VideoPlaybackDetails[] = event.nativeEvent?.videos ?? []; FWLoggerUtil.log( `PlayerDeck _onPlayerDeckVideosLoaded ${videos.length} videos: ${videos .map((video) => video.videoId) .join(',')}` ); if (onPlayerDeckVideosLoaded) { onPlayerDeckVideosLoaded(videos); } }; componentDidMount() { FWLoggerUtil.log( `PlayerDeck componentDidMount ${JSON.stringify(this.props)}` ); if (!this.state.sdkInitSucceeded) { FWGlobalState.getInstance().sdkInitSucceededPromise.then(() => { this.setState({ sdkInitSucceeded: true }); }); } const subscriptionOfShareBaseURLUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.ShareBaseURLUpdated, () => { FWLoggerUtil.log('Receive FWEventName.ShareBaseURLUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfShareBaseURLUpdated); const subscriptionOfAdBadgeConfigurationUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.AdBadgeConfigurationUpdated, () => { FWLoggerUtil.log('Receive FWEventName.AdBadgeConfigurationUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfAdBadgeConfigurationUpdated); const subscriptionOfVideoLaunchBehaviorUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.VideoLaunchBehaviorUpdated, () => { FWLoggerUtil.log('Receive FWEventName.VideoLaunchBehaviorUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfVideoLaunchBehaviorUpdated); const subscriptionOfAppLanguageUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.AppLanguageUpdated, () => { FWLoggerUtil.log('Receive FWEventName.AppLanguageUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfAppLanguageUpdated); const subscriptionOfDataTrackingLevelUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.DataTrackingLevelUpdated, () => { FWLoggerUtil.log('Receive FWEventName.DataTrackingLevelUpdated'); this.setState({}); } ); this._subscriptions.push(subscriptionOfDataTrackingLevelUpdated); const subscriptionOfLivestreamPlayerDesignVersionUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.LivestreamPlayerDesignVersionUpdated, () => { FWLoggerUtil.log( 'Receive FWEventName.LivestreamPlayerDesignVersionUpdated' ); this.setState({}); } ); this._subscriptions.push( subscriptionOfLivestreamPlayerDesignVersionUpdated ); const subscriptionOfShortVideoPlayerDesignVersionUpdated = FireworkSDKModuleEventEmitter.addListener( FWEventName.ShortVideoPlayerDesignVersionUpdated, () => { FWLoggerUtil.log( 'Receive FWEventName.ShortVideoPlayerDesignVersionUpdated' ); this.setState({}); } ); this._subscriptions.push( subscriptionOfShortVideoPlayerDesignVersionUpdated ); } componentWillUnmount() { FWLoggerUtil.log( `PlayerDeck componentWillUnmount ${JSON.stringify(this.props)}` ); this._subscriptions.forEach((value) => { value.remove(); }); this._subscriptions = []; } render() { if (!this.state.sdkInitSucceeded) { return null; } const videoPlayerConfiguration = this._getVideoPlayerConfiguration(); const key = this._generateKey(videoPlayerConfiguration); return ( ); } private _generateDynamicContentParametersString(): string { const { dynamicContentParameters } = this.props; if (!dynamicContentParameters) { return ''; } let resultString = ''; const sortedKeyList = Object.keys(dynamicContentParameters).sort(); for (const k of sortedKeyList) { const value = dynamicContentParameters[k] ?? []; const valueString = value.join(','); if (resultString.length > 0) { resultString += '_'; } resultString += `${k}:${valueString}`; } return resultString; } private _getVideoPlayerConfiguration(): | VideoPlayerNativeConfiguration | undefined { const { videoPlayerConfiguration, enablePictureInPicture, enableSystemPictureInPicture, } = this.props; let result: VideoPlayerNativeConfiguration | undefined = videoPlayerConfiguration; if (typeof enablePictureInPicture === 'boolean') { if (!result) { result = {}; } result = { ...result, enablePictureInPicture }; } if (typeof enableSystemPictureInPicture === 'boolean') { if (!result) { result = {}; } result = { ...result, enableSystemPictureInPicture }; } return result; } private _generateKey( videoPlayerConfiguration?: VideoPlayerNativeConfiguration ): string { const gShareBaseURL = FireworkSDK.getInstance().shareBaseURL; const appLanguage = FireworkSDK.getInstance().appLanguage; const videoLaunchBehavior = FireworkSDK.getInstance().videoLaunchBehavior; const adBadgeConfiguration = FireworkSDK.getInstance().adBadgeConfiguration; const adBadgeTextType = adBadgeConfiguration?.badgeTextType; const backgroundColorOfAdBadge = adBadgeConfiguration?.backgroundColor; const textColorOfAdBadge = adBadgeConfiguration?.textColor; const dataTrackingLevel = FireworkSDK.getInstance().dataTrackingLevel; const livestreamPlayerDesignVersion = FireworkSDK.getInstance().livestreamPlayerDesignVersion; const shortVideoPlayerDesignVersion = FireworkSDK.getInstance().shortVideoPlayerDesignVersion; const { source, channel, playlist, playlistGroup, hashtagFilterExpression, productIds, contentId, enablePictureInPicture, playerDeckConfiguration, } = this.props; const dynamicContentParametersString = this._generateDynamicContentParametersString(); let key = `gShareBaseURL:${gShareBaseURL}`; key += `_appLanguage:${appLanguage}`; key += `_videoLaunchBehavior:${videoLaunchBehavior}`; key += `_adBadgeTextType:${adBadgeTextType}`; key += `_backgroundColorOfAdBadge:${backgroundColorOfAdBadge}`; key += `_textColorOfAdBadge:${textColorOfAdBadge}`; key += `_dataTrackingLevel:${dataTrackingLevel}`; key += `_livestreamPlayerDesignVersion:${livestreamPlayerDesignVersion}`; key += `_shortVideoPlayerDesignVersion:${shortVideoPlayerDesignVersion}`; key += `_source:${source}`; key += `_channel:${channel}`; key += `_playlist:${playlist}`; key += `_playlistGroup:${playlistGroup}`; key += `_dynamicContentParameters:${dynamicContentParametersString}`; key += `_hashtagFilterExpression:${hashtagFilterExpression}`; key += `_productIds:${productIds?.join(',')}`; key += `_contentId:${contentId}`; key += `_enablePictureInPicture:${enablePictureInPicture}`; if (playerDeckConfiguration) { key += `_playerDeckConfiguration:${gennerateJsonKey(playerDeckConfiguration)}`; } if (videoPlayerConfiguration) { key += `_videoPlayerConfiguration:${gennerateJsonKey(videoPlayerConfiguration)}`; } return key; } } export default PlayerDeck;