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 AdConfiguration from '../models/AdConfiguration'; import type ButtonInfo from '../models/ButtonInfo'; import type FWError from '../models/FWError'; import { FWEventName } from '../models/FWEventName'; import { FWNativeErrorAction } from '../models/FWNativeErrorAction'; import type VideoFeedConfiguration from '../models/VideoFeedConfiguration'; import type { VideoFeedSource } from '../models/VideoFeedSource'; 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 { AUTOSIZED_MAX_VIDEOS, isAutosizingSupported, isMaxVideosCapped, isMaxVideosSupported, } from '../utils/FWAutosizingUtil'; import { normalizeMaxVideos } from '../utils/FWMaxVideosUtil'; import FWVideoFeed from './FWVideoFeed'; import { FWComponentType } from '../models/FWComponentType'; export type VideoFeedMode = 'row' | 'column' | 'grid'; /** * The props of VideoFeed component. */ export interface IVideoFeedProps { /** * Standard React Native View Style. * When `autosizing` is enabled, the height is driven by the feed content and any * `height` set here is ignored. */ style?: StyleProp; /** * One of five available video feed sources.The playlistGroup is only supported on iOS. * Defaults to discover. */ source: VideoFeedSource; /** * Channel id of the feed. Required when the source is set as channel or playlist or dynamicContent. */ channel?: string; /** * Playlist id of the feed. Please note channel id is necessary. Required when the source is set as playlist. */ playlist?: string; /** * PlaylistGroup id of the feed. Required when the source is set as playlistGroup. Only supported on iOS. */ playlistGroup?: string; /** * The dynamic content parameters of the feed. Required when the source is set as dynamicContent. */ dynamicContentParameters?: { [key: string]: string[] }; /** * Hashtag filter expression is an s-expression used to provide feeds filtered by hashtags with specified criteria. * Queries are specified with boolean predicates on what hashtags are there on the video. * For instance, (and sport food) (or sport food) (and sport (or food comedy)) sport are all valid expressions. * Non-UTF-8 characters are not allowed. If using boolean predicates, the expression needs to be wrapped with parenthesis. * 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; /** * One of three available display modes. Defaults to row. */ mode?: VideoFeedMode; /** * Whether the feed sizes its own height to fit its content instead of requiring a * fixed height from the host app. Defaults to `false`. * * Intended for a feed inside a scrolling parent such as `ScrollView` or `FlatList`: the * feed expands to the full height of its content, does not scroll internally, and * autoplay is evaluated against the surrounding viewport, so the item that is actually * on screen plays as the parent scrolls. * * **For behavior identical on both platforms, use `grid` mode with `autosizing` enabled** * (`maxVideos` optional). That is the supported combination; the others differ: * iOS also autosizes `column`, and in `row` mode keeps a fixed row height and scrolls * horizontally as usual, while on Android the prop is ignored outside `grid` mode, with * a warning. * * When enabled: * - The height is driven by the native feed; any `height` in `style` is ignored. * Do not set `flex`, `minHeight` or `maxHeight` on the feed either. * - In `grid`/`column` mode the feed loads at most 20 videos (`maxVideos` is clamped * to 20) to keep the expanded height bounded. * - The value is fixed when the native feed is created. Changing it, or changing `mode`, * re-creates the native feed, which reloads its content from the first page. */ autosizing?: boolean; /** * Caps the total number of videos loaded into the feed. The feed paginates (20 videos * per page) until this total is reached and then stops loading more. Must be a * positive integer; other values are ignored (no cap) with a warning. * Defaults to `undefined` (no cap). * * When `autosizing` is enabled in `grid`/`column` mode, the effective cap is at * most 20. * * On iOS the cap applies to any feed. On Android it applies only to an autosized `grid` * feed and is ignored otherwise, with a warning: the Android SDK derives the * content-height grid from the cap itself, so capping a fixed-height feed would * silently make it autosize. * * The value is fixed when the native feed is created. Changing it re-creates the * native feed, which reloads its content from the first page. */ maxVideos?: number; /** * Specifies if Picture in Picture is enabled. */ enablePictureInPicture?: boolean; /** * Controls whether the system (native iOS) Picture-in-Picture is enabled. * When set to `false`, the system PiP window will not appear automatically, * while background audio and the in-app floating player continue to work. * Defaults to `true`. Only supported on iOS. */ enableSystemPictureInPicture?: boolean; /** * Configuration of the feed. */ videoFeedConfiguration?: VideoFeedConfiguration; /** * Configuration of the player. */ videoPlayerConfiguration?: VideoPlayerConfiguration; /** * Ad configuration of the feed. */ adConfiguration?: AdConfiguration; /** * The feed loading result callback. It means loading successfully when error equals to undefined. * This callback will not be triggered when the feed is empty; onVideoFeedEmpty will be triggered instead. */ onVideoFeedLoadFinished?: (error?: FWError) => void; /** * The callback is triggered when the feed loads successfully but there are no items in the video feed. * onVideoFeedLoadFinished will not be triggered in this case; the two callbacks are mutually exclusive. */ onVideoFeedEmpty?: (error?: FWError) => void; /** * Start Picture in Picture callback. Only supported on iOS. */ onVideoFeedDidStartPictureInPicture?: (error?: FWError) => void; /** * Stop Picture in Picture callback. Only supported on iOS. */ onVideoFeedDidStopPictureInPicture?: (error?: FWError) => void; /** * * The host app could use this callback to get feed id. * The feed id can be used for conversion tracking. */ onVideoFeedGetFeedId?: (feedId: string) => void; /** * The callback is triggered when videos are loaded in the video feed. * 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 video feed. */ onVideoFeedVideosLoaded?: (videos: VideoPlaybackDetails[]) => void; } interface IVideoFeedState { sdkInitSucceeded: boolean; /** * Height last reported by the native feed while `autosizing` is enabled. Kept across * native re-creations (key changes) so the feed does not collapse to the placeholder * height in between; the new native feed reports its height again right away. */ contentHeight?: number; } const NativeComponentName = 'FWVideoFeed'; /** * VideoFeed component. */ class VideoFeed extends React.Component { /** * @ignore */ static defaultProps = { mode: 'row', }; private _nativeComponentRef = React.createRef(); private _subscriptions: EmitterSubscription[] = []; private _loaded: boolean = false; constructor(props: IVideoFeedProps) { super(props); this.state = { sdkInitSucceeded: FWGlobalState.getInstance().sdkInitSucceeded, }; } public refresh = () => { FWLoggerUtil.log(`VideoFeed 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( `VideoFeed refresh commandId: ${commandId} nativeNodeHandle: ${nativeNodeHandle} reactTag: ${reactTag}` ); if (!nativeNodeHandle || !reactTag) { return; } UIManager.dispatchViewManagerCommand(reactTag, commandId, []); }; private _onVideoFeedLoadFinished = (event: NativeSyntheticEvent) => { const { onVideoFeedLoadFinished, onVideoFeedEmpty } = this.props; const { name, reason, action } = event.nativeEvent; if (onVideoFeedLoadFinished) { if (name) { let error: FWError = { name }; if (reason) { error.reason = reason; } if (Platform.OS === 'android') { if (!this._loaded) { onVideoFeedLoadFinished(error); } if ( action === FWNativeErrorAction.loadingFirstPage || !this._loaded ) { onVideoFeedEmpty?.(error); } } else { onVideoFeedLoadFinished(error); if (!this._loaded) { onVideoFeedEmpty?.(error); } } } else { if (Platform.OS === 'android') { if (!this._loaded) { onVideoFeedLoadFinished(); } } else { onVideoFeedLoadFinished(); } this._loaded = true; } } }; private _onVideoFeedEmpty = (event: NativeSyntheticEvent) => { FWLoggerUtil.log( `VideoFeed _onVideoFeedLoadFinished ${JSON.stringify(event.nativeEvent)}` ); const { onVideoFeedEmpty } = this.props; if (onVideoFeedEmpty) { onVideoFeedEmpty(); } }; private _onVideoFeedDidStartPictureInPicture = ( event: NativeSyntheticEvent ) => { FWLoggerUtil.log( `VideoFeed _onVideoFeedDidStartPictureInPicture ${JSON.stringify( event.nativeEvent )}` ); const { onVideoFeedDidStartPictureInPicture } = this.props; const { name, reason } = event.nativeEvent; if (onVideoFeedDidStartPictureInPicture) { if (name) { if (reason) { onVideoFeedDidStartPictureInPicture({ name, reason }); } else { onVideoFeedDidStartPictureInPicture({ name }); } } else { onVideoFeedDidStartPictureInPicture(); } } }; private _onVideoFeedDidStopPictureInPicture = ( event: NativeSyntheticEvent ) => { FWLoggerUtil.log( `VideoFeed _onVideoFeedDidStopPictureInPicture ${JSON.stringify( event.nativeEvent )}` ); const { onVideoFeedDidStopPictureInPicture } = this.props; const { name, reason } = event.nativeEvent; if (onVideoFeedDidStopPictureInPicture) { if (name) { if (reason) { onVideoFeedDidStopPictureInPicture({ name, reason }); } else { onVideoFeedDidStopPictureInPicture({ name }); } } else { onVideoFeedDidStopPictureInPicture(); } } }; private _onVideoFeedGetFeedId = (event: NativeSyntheticEvent) => { FWLoggerUtil.log( `VideoFeed onVideoFeedGetFeedId ${JSON.stringify(event.nativeEvent)}` ); const { onVideoFeedGetFeedId } = this.props; const { feedId } = event.nativeEvent; if (feedId) { FWGlobalState.getInstance().componentTypeByFeedIdMap.set( feedId, FWComponentType.videoFeed ); } if (onVideoFeedGetFeedId) { onVideoFeedGetFeedId(feedId ?? ''); } }; private _onVideoFeedContentSizeChange = ( event: NativeSyntheticEvent ) => { const { height } = event.nativeEvent; if ( !this._isAutosizingEnabled() || typeof height !== 'number' || !Number.isFinite(height) || height < 0 ) { return; } if (this.state.contentHeight === height) { return; } this.setState({ contentHeight: height }); }; private _onVideoFeedVideosLoaded = (event: NativeSyntheticEvent) => { const { onVideoFeedVideosLoaded } = this.props; const videos: VideoPlaybackDetails[] = event.nativeEvent?.videos ?? []; if (onVideoFeedVideosLoaded) { onVideoFeedVideosLoaded(videos); } }; /** * @ignore */ componentDidMount() { FWLoggerUtil.log( `VideoFeed 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 ); } /** * @ignore */ componentWillUnmount() { FWLoggerUtil.log( `VideoFeed componentWillUnmount ${JSON.stringify(this.props)}` ); this._subscriptions.forEach((value) => { value.remove(); }); this._subscriptions = []; } /** * @ignore */ render() { if (!this.state.sdkInitSucceeded) { return null; } 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 androidFontIsCustomOfAdBadge = adBadgeConfiguration?.androidFontInfo?.isCustom?.toString(); const androidFontTypefaceNameOfAdBadge = adBadgeConfiguration?.androidFontInfo?.typefaceName; const dataTrackingLevel = FireworkSDK.getInstance().dataTrackingLevel; const livestreamPlayerDesignVersion = FireworkSDK.getInstance().livestreamPlayerDesignVersion; const shortVideoPlayerDesignVersion = FireworkSDK.getInstance().shortVideoPlayerDesignVersion; const { source, channel, playlist, playlistGroup, hashtagFilterExpression, productIds, contentId, mode, enablePictureInPicture, adConfiguration, } = this.props; const dynamicContentParametersString = this._generateDynamicContentParametersString(); const autosizingEnabled = this._isAutosizingEnabled(); const maxVideos = this._getMaxVideos(); const videoFeedConfiguration = this._getVideoFeedConfiguration(); const cornerRadius = videoFeedConfiguration?.cornerRadius?.toFixed(5); const titleHidden = videoFeedConfiguration?.title?.hidden?.toString(); const titleTextColor = videoFeedConfiguration?.title?.textColor; const titleFontSize = videoFeedConfiguration?.title?.fontSize?.toFixed(5); const titleNumberOfLines = videoFeedConfiguration?.title?.numberOfLines; const titleAndroidFontInfo = videoFeedConfiguration?.title?.androidFontInfo; const titleGradientDrawable = videoFeedConfiguration?.title?.gradientDrawable; const titlePosition = videoFeedConfiguration?.titlePosition; const playIconHidden = videoFeedConfiguration?.playIcon?.hidden; const playIconWidth = videoFeedConfiguration?.playIcon?.iconWidth?.toFixed(5); const playIconImageName = videoFeedConfiguration?.playIcon?.imageName; const showAdBadge = videoFeedConfiguration?.showAdBadge?.toString(); const enableAutoplay = videoFeedConfiguration?.enableAutoplay; const gridColumns = videoFeedConfiguration?.gridColumns; const itemSpacing = videoFeedConfiguration?.itemSpacing?.toFixed(5); const contentPaddingTop = videoFeedConfiguration?.contentPadding?.top?.toFixed(5); const contentPaddingRight = videoFeedConfiguration?.contentPadding?.right?.toFixed(5); const contentPaddingBottom = videoFeedConfiguration?.contentPadding?.bottom?.toFixed(5); const contentPaddingLeft = videoFeedConfiguration?.contentPadding?.left?.toFixed(5); const videoPlayerConfiguration = this._getVideoPlayerConfiguration(); const playerStyle = videoPlayerConfiguration?.playerStyle; const videoCompleteAction = videoPlayerConfiguration?.videoCompleteAction; const showShareButton = videoPlayerConfiguration?.showShareButton; const showPlaybackButton = videoPlayerConfiguration?.showPlaybackButton; const showMuteButton = videoPlayerConfiguration?.showMuteButton; const showBranding = videoPlayerConfiguration?.showBranding; const ctaDelayType = videoPlayerConfiguration?.ctaDelay?.type; const ctaDelayValue = videoPlayerConfiguration?.ctaDelay?.value?.toFixed(5); const ctaHighlightDelayType = videoPlayerConfiguration?.ctaHighlightDelay?.type; const ctaHighlightDelayValue = videoPlayerConfiguration?.ctaHighlightDelay?.value?.toFixed(5); const shareBaseURL = videoPlayerConfiguration?.shareBaseURL; const ctaWidth = videoPlayerConfiguration?.ctaWidth; const buttonConfiguration = videoPlayerConfiguration?.buttonConfiguration; const videoPlayerLogoConfigurationJsonKey = gennerateJsonKey( videoPlayerConfiguration?.videoPlayerLogoConfiguration ); const countdownTimerConfigurationJsonKey = gennerateJsonKey( videoPlayerConfiguration?.countdownTimerConfiguration ); const showVideoDetailTitle = videoPlayerConfiguration?.showVideoDetailTitle; const requiresAds = adConfiguration?.requiresAds; const adsFetchTimeout = adConfiguration?.adsFetchTimeout; const vastAttributesString = this._generateVastAttributesString(); const replayBadgeVideoFeedConfigurationJsonKey = gennerateJsonKey( videoFeedConfiguration?.replayBadge ); const replayBadgeVideoPlayerConfigurationJsonKey = gennerateJsonKey( videoPlayerConfiguration?.replayBadgeConfiguration ); const shadowVideoFeedConfigurationJsonKey = gennerateJsonKey( videoFeedConfiguration?.shadow ); const feedCompleteAction = videoPlayerConfiguration?.feedCompleteAction; const shouldExtendMediaOutsideSafeArea = videoPlayerConfiguration?.shouldExtendMediaOutsideSafeArea; const statusBarHidden = videoPlayerConfiguration?.statusBarHidden; const statusBarStyle = videoPlayerConfiguration?.statusBarStyle; const pipPlacement = videoPlayerConfiguration?.pipPlacement; const scrollDirection = videoPlayerConfiguration?.scrollDirection; const isArrowButtonVisible = videoPlayerConfiguration?.isArrowButtonVisible; const ctaButtonStyle = videoPlayerConfiguration?.ctaButtonStyle; const showMoreButton = videoPlayerConfiguration?.showMoreButton; const shareButtonPosition = videoPlayerConfiguration?.shareButtonPosition; let key = `gShareBaseURL:${gShareBaseURL}`; key += `_appLanguage:${appLanguage}`; key += `_videoLaunchBehavior:${videoLaunchBehavior}`; key += `_adBadgeTextType:${adBadgeTextType}`; key += `_backgroundColorOfAdBadge:${backgroundColorOfAdBadge}`; key += `_textColorOfAdBadge:${textColorOfAdBadge}`; if (Platform.OS === 'android') { key += `_androidFontIsCustomOfAdBadge${androidFontIsCustomOfAdBadge}`; key += `_androidFontTypefaceNameOfAdBadge${androidFontTypefaceNameOfAdBadge}`; } 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 += `_mode:${mode}`; // Both are fixed when the native feed is created, so a change re-creates it. key += `_autosizing:${autosizingEnabled}`; key += `_maxVideos:${maxVideos}`; key += `_enablePictureInPicture:${enablePictureInPicture}`; key += `_titleHidden:${titleHidden}`; if (Platform.OS === 'android') { key += `_cornerRadius:${cornerRadius}`; key += `_titleTextColor:${titleTextColor}`; key += `_titleFontSize:${titleFontSize}`; key += `_titleNumberOfLines:${titleNumberOfLines}`; key += `_titleAndroidFontInfo.isCustom:${titleAndroidFontInfo?.isCustom}`; key += `_titleAndroidFontInfo.typefaceName:${titleAndroidFontInfo?.typefaceName}`; key += `_titleGradientDrawable.orientation:${titleGradientDrawable?.orientation}`; key += `_titleGradientDrawable.colors:${titleGradientDrawable?.colors?.join( ',' )}`; key += `_playIconHidden:${playIconHidden}`; key += `_playIconWidth:${playIconWidth}`; key += `_playIconImageName:${playIconImageName}`; key += `_showAdBadge:${showAdBadge}`; } key += `_titlePosition:${titlePosition}`; if (Platform.OS === 'android') { key += `_enableAutoplay:${enableAutoplay}`; } key += `_gridColumns:${gridColumns}`; key += `_itemSpacing:${itemSpacing}`; key += `_contentPaddingTop:${contentPaddingTop}`; key += `_contentPaddingRight:${contentPaddingRight}`; key += `_contentPaddingBottom:${contentPaddingBottom}`; key += `_contentPaddingLeft:${contentPaddingLeft}`; key += `_shareBaseURL:${shareBaseURL}`; if (Platform.OS === 'android') { key += `_playerStyle:${playerStyle}`; key += `_videoCompleteAction:${videoCompleteAction}`; key += `_showShareButton:${showShareButton}`; key += `_showPlaybackButton:${showPlaybackButton}`; key += `_showMuteButton:${showMuteButton}`; key += `_showBranding:${showBranding}`; key += `_ctaDelayType:${ctaDelayType}`; key += `_ctaDelayValue:${ctaDelayValue}`; key += `_ctaHighlightDelayType:${ctaHighlightDelayType}`; key += `_ctaHighlightDelayValue:${ctaHighlightDelayValue}`; key += `_ctaWidth:${ctaWidth}`; key += `_ctaButtonStyle.backgroundColor:${ctaButtonStyle?.backgroundColor}`; key += `_ctaButtonStyle.textColor:${ctaButtonStyle?.textColor}`; key += `_ctaButtonStyle.fontSize:${ctaButtonStyle?.fontSize}`; key += `_ctaButtonStyle.shape:${ctaButtonStyle?.shape}`; key += `_buttonConfiguration.videoDetailButton:${this._generateButtonInfoString( buttonConfiguration?.videoDetailButton )}`; key += `_buttonConfiguration.closeButton:${this._generateButtonInfoString( buttonConfiguration?.closeButton )}`; key += `_buttonConfiguration.muteButton:${this._generateButtonInfoString( buttonConfiguration?.muteButton )}`; key += `_buttonConfiguration.unmuteButton:${this._generateButtonInfoString( buttonConfiguration?.unmuteButton )}`; key += `_buttonConfiguration.playButton:${this._generateButtonInfoString( buttonConfiguration?.playButton )}`; key += `_buttonConfiguration.pauseButton:${this._generateButtonInfoString( buttonConfiguration?.pauseButton )}`; key += `_videoPlayerLogoConfiguration:${videoPlayerLogoConfigurationJsonKey}`; key += `_countdownTimerConfiguration:${countdownTimerConfigurationJsonKey}`; key += `_showVideoDetailTitle:${showVideoDetailTitle}`; } key += `_requiresAds:${requiresAds}`; key += `_adsFetchTimeout:${adsFetchTimeout}`; key += `_vastAttributes:${vastAttributesString}`; key += `_replayBadgeVideoFeedConfiguration:${replayBadgeVideoFeedConfigurationJsonKey}`; key += `_replayBadgeVideoPlayerConfiguration:${replayBadgeVideoPlayerConfigurationJsonKey}`; key += `_shadowVideoFeedConfiguration:${shadowVideoFeedConfigurationJsonKey}`; key += `_feedCompleteAction:${feedCompleteAction}`; key += `_shouldExtendMediaOutsideSafeArea:${shouldExtendMediaOutsideSafeArea}`; key += `_statusBarHidden:${statusBarHidden}`; key += `_statusBarStyle:${statusBarStyle}`; key += `_pipPlacement:${pipPlacement}`; key += `_scrollDirection:${scrollDirection}`; key += `_isArrowButtonVisible:${isArrowButtonVisible}`; key += `_showMoreButton:${showMoreButton}`; key += `_shareButtonPosition:${shareButtonPosition}`; return ( ); } private _isAutosizingEnabled(): boolean { const { autosizing, mode } = this.props; if (autosizing !== true) { return false; } const resolvedMode = mode ?? 'row'; if (isAutosizingSupported(Platform.OS, resolvedMode)) { return true; } if (Platform.OS === 'android') { FWLoggerUtil.warnOnce( `VideoFeed.autosizing.mode:${resolvedMode}`, `VideoFeed: autosizing is only supported in grid mode on Android, but mode is ${resolvedMode}; the prop is ignored.` ); } else { FWLoggerUtil.warnOnce( 'VideoFeed.autosizing', 'VideoFeed: autosizing is only supported on iOS and Android; the prop is ignored on this platform.' ); } return false; } private _getMaxVideos(): number | undefined { const { maxVideos, mode } = this.props; const result = normalizeMaxVideos(maxVideos); const autosizingEnabled = this._isAutosizingEnabled(); if (result === undefined && maxVideos !== undefined && maxVideos !== null) { FWLoggerUtil.warnOnce( `VideoFeed.maxVideos:${String(maxVideos)}`, `VideoFeed: maxVideos must be a positive integer, got ${String( maxVideos )}; the prop is ignored.` ); return undefined; } if ( result !== undefined && !isMaxVideosSupported(Platform.OS, autosizingEnabled) ) { FWLoggerUtil.warnOnce( 'VideoFeed.maxVideos.android', 'VideoFeed: on Android maxVideos only applies to an autosized grid feed; the prop is ignored.' ); return undefined; } // Reported rather than clamped here: the native feed stays the single source of truth for // the cap, but a host that asks for more videos than it can get should hear about it. if ( result !== undefined && isMaxVideosCapped(result, autosizingEnabled, mode) ) { FWLoggerUtil.warnOnce( `VideoFeed.maxVideos.capped:${result}`, `VideoFeed: an autosized feed loads at most ${AUTOSIZED_MAX_VIDEOS} videos, so maxVideos ${result} is reduced to ${AUTOSIZED_MAX_VIDEOS}.` ); } return result; } /** * Height style applied while `autosizing` is enabled. Appended after the host style, * so it overrides any `height` the host set. */ private _getAutosizingStyle(): ViewStyle { const { contentHeight } = this.state; return { height: contentHeight ?? this._getAutosizingPlaceholderHeight(), }; } /** * Height used until the native feed reports its first size. Mirrors the iOS pre-content * intrinsic height (`FWSVideoFeedView.intrinsicContentSize`): the 40pt loading footer * centered within the vertical content padding for `grid`/`column`, and the default * 220pt row height plus the vertical content padding for `row`. It guarantees the view * is laid out with a non-zero height while the first page loads; the native feed reports * its own value right after it is created and corrects any difference. * * Android reuses this approximation until its first positive native measurement. * Transient loading zeros are ignored; a confirmed empty feed reports zero. */ private _getAutosizingPlaceholderHeight(): number { const { mode, videoFeedConfiguration } = this.props; const contentPadding = videoFeedConfiguration?.contentPadding; const verticalPadding = (contentPadding?.top ?? 10) + (contentPadding?.bottom ?? 10); if (mode === 'row') { return verticalPadding + 220; } return 2 * verticalPadding + 40; } private _generateDynamicContentParametersString(): string { const { dynamicContentParameters } = this.props; if (!dynamicContentParameters) { return ''; } let resultString = ''; const sortedKeyList = Object.keys(dynamicContentParameters).sort(); for (const key of sortedKeyList) { const value = dynamicContentParameters[key] ?? []; const valueString = value.join(','); if (resultString.length > 0) { resultString += '_'; } resultString += `${key}:${valueString}`; } return resultString; } private _generateVastAttributesString(): string | undefined { const { adConfiguration } = this.props; const vastAttributes = adConfiguration?.vastAttributes; if (!vastAttributes) { return ''; } let resultString = ''; for (const attribute of vastAttributes) { if (resultString.length > 0) { resultString += '_'; } resultString += `${attribute.name}:${attribute.value}`; } return resultString; } private _generateButtonInfoString(buttonInfo?: ButtonInfo): string { return `imageName:${buttonInfo?.imageName}_systemImageName:${buttonInfo?.systemImageName}_tintColor:${buttonInfo?.tintColor}`; } private _getVideoFeedConfiguration(): VideoFeedConfiguration | undefined { const { videoFeedConfiguration } = this.props; if (!videoFeedConfiguration) { return videoFeedConfiguration; } let resultVideoFeedConfiguration = videoFeedConfiguration; if ( resultVideoFeedConfiguration.gridColumns && Math.floor(resultVideoFeedConfiguration.gridColumns) !== resultVideoFeedConfiguration.gridColumns ) { const gridColumns = Math.floor(resultVideoFeedConfiguration.gridColumns); resultVideoFeedConfiguration = { ...resultVideoFeedConfiguration, gridColumns, }; } if ( resultVideoFeedConfiguration.title && resultVideoFeedConfiguration.title.numberOfLines && Math.floor(resultVideoFeedConfiguration.title.numberOfLines) !== resultVideoFeedConfiguration.title.numberOfLines ) { const numberOfLines = Math.floor( resultVideoFeedConfiguration.title.numberOfLines ); resultVideoFeedConfiguration = { ...resultVideoFeedConfiguration, title: { ...resultVideoFeedConfiguration.title, numberOfLines, }, }; } return resultVideoFeedConfiguration; } private _getVideoPlayerConfiguration(): | VideoPlayerNativeConfiguration | undefined { const { videoPlayerConfiguration, enablePictureInPicture, enableSystemPictureInPicture, } = this.props; let resultVideoPlayerConfiguration: | VideoPlayerNativeConfiguration | undefined = videoPlayerConfiguration; if (typeof enablePictureInPicture === 'boolean') { if (!resultVideoPlayerConfiguration) { resultVideoPlayerConfiguration = {}; } resultVideoPlayerConfiguration = { ...resultVideoPlayerConfiguration, enablePictureInPicture, }; } if (typeof enableSystemPictureInPicture === 'boolean') { if (!resultVideoPlayerConfiguration) { resultVideoPlayerConfiguration = {}; } resultVideoPlayerConfiguration = { ...resultVideoPlayerConfiguration, enableSystemPictureInPicture, }; } return resultVideoPlayerConfiguration; } } export default VideoFeed;