import React, { forwardRef, useRef, useImperativeHandle, useState, useCallback, useEffect, } from 'react'; import { Animated, LayoutAnimation, Platform, UIManager } from 'react-native'; import BBPlayerView from './BBPlayerView'; import type { BBPlayerViewProps, BBPlayerViewMethods } from './BBPlayer.types'; // Enable LayoutAnimation on Android if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } /** * Animation type for collapse/expand transitions */ export type OutstreamAnimationType = 'timing' | 'spring' | 'layout' | 'none'; /** * Configuration for Outstream animations */ export interface OutstreamAnimationConfig { /** * Type of animation to use * - 'timing': Smooth linear animation (default) * - 'spring': Bouncy spring animation * - 'layout': Uses LayoutAnimation (simpler, may be smoother on some devices) * - 'none': Instant collapse/expand with no animation * @default 'timing' */ type?: OutstreamAnimationType; /** * Duration in milliseconds (for 'timing' and 'layout' types) * @default 300 */ duration?: number; /** * Spring damping (for 'spring' type only) * @default 15 */ damping?: number; /** * Spring stiffness (for 'spring' type only) * @default 100 */ stiffness?: number; } /** * Props for BBOutstreamView */ export interface BBOutstreamViewProps extends Omit { /** * Initial/expanded height of the player in pixels * @default 200 */ expandedHeight?: number; /** * Height when collapsed in pixels * @default 0 */ collapsedHeight?: number; /** * Animation configuration for collapse/expand */ animation?: OutstreamAnimationConfig; /** * Player options. `allowCollapseExpand` is automatically set to true. */ options?: Record; /** * Called when the player requests to collapse (after animation completes) */ onCollapsed?: () => void; /** * Called when the player requests to expand (after animation completes) */ onExpanded?: () => void; /** * Called when collapse/expand animation starts */ onAnimationStart?: (isCollapsing: boolean) => void; } /** * Methods available on BBOutstreamView ref */ export interface BBOutstreamViewMethods extends BBPlayerViewMethods { /** * Returns whether the player is currently collapsed */ isCollapsed: () => boolean; /** * Programmatically collapse the player with animation */ animateCollapse: () => void; /** * Programmatically expand the player with animation */ animateExpand: () => void; } /** * BBOutstreamView - Convenience wrapper for Outstream advertising * * This component wraps BBPlayerView with automatic collapse/expand animation * handling. It's designed for Outstream ad placements embedded in scrollable * content like articles or feeds. * * @example * ```tsx * * Article content... * * console.log('Player collapsed')} * onExpanded={() => console.log('Player expanded')} * /> * * More content... * * ``` */ const BBOutstreamView = forwardRef( ( { expandedHeight = 200, collapsedHeight = 0, animation = {}, options = {}, onCollapsed, onExpanded, onAnimationStart, onDidRequestCollapse, onDidRequestExpand, style, ...playerProps }, ref ) => { const playerRef = useRef(null); const heightAnim = useRef(new Animated.Value(expandedHeight)).current; const [isCollapsed, setIsCollapsed] = useState(false); // Default animation config const animConfig: Required = { type: animation.type ?? 'timing', duration: animation.duration ?? 300, damping: animation.damping ?? 15, stiffness: animation.stiffness ?? 100, }; // Update animated value when expandedHeight changes useEffect(() => { if (!isCollapsed) { heightAnim.setValue(expandedHeight); } }, [expandedHeight, isCollapsed, heightAnim]); const animateToHeight = useCallback( (toHeight: number, onComplete?: () => void) => { const isCollapsing = toHeight === collapsedHeight; onAnimationStart?.(isCollapsing); if (animConfig.type === 'none') { heightAnim.setValue(toHeight); onComplete?.(); return; } if (animConfig.type === 'layout') { LayoutAnimation.configureNext( LayoutAnimation.create( animConfig.duration, LayoutAnimation.Types.easeInEaseOut, LayoutAnimation.Properties.scaleY ), onComplete ); heightAnim.setValue(toHeight); return; } const animationFn = animConfig.type === 'spring' ? Animated.spring(heightAnim, { toValue: toHeight, damping: animConfig.damping, stiffness: animConfig.stiffness, useNativeDriver: false, }) : Animated.timing(heightAnim, { toValue: toHeight, duration: animConfig.duration, useNativeDriver: false, }); animationFn.start(({ finished }) => { if (finished) { onComplete?.(); } }); }, [heightAnim, collapsedHeight, animConfig, onAnimationStart] ); const handleRequestCollapse = useCallback(() => { setIsCollapsed(true); animateToHeight(collapsedHeight, onCollapsed); onDidRequestCollapse?.(); }, [animateToHeight, collapsedHeight, onCollapsed, onDidRequestCollapse]); const handleRequestExpand = useCallback(() => { setIsCollapsed(false); animateToHeight(expandedHeight, onExpanded); onDidRequestExpand?.(); }, [animateToHeight, expandedHeight, onExpanded, onDidRequestExpand]); // Programmatic collapse/expand const animateCollapse = useCallback(() => { if (!isCollapsed) { setIsCollapsed(true); animateToHeight(collapsedHeight, onCollapsed); playerRef.current?.collapse(); } }, [isCollapsed, animateToHeight, collapsedHeight, onCollapsed]); const animateExpand = useCallback(() => { if (isCollapsed) { setIsCollapsed(false); animateToHeight(expandedHeight, onExpanded); playerRef.current?.expand(); } }, [isCollapsed, animateToHeight, expandedHeight, onExpanded]); // Expose methods via ref useImperativeHandle( ref, () => ({ // Pass through all BBPlayerViewMethods play: () => playerRef.current?.play(), pause: () => playerRef.current?.pause(), seek: (position: number) => playerRef.current?.seek(position), seekRelative: (offset: number) => playerRef.current?.seekRelative(offset), setVolume: (volume: number) => playerRef.current?.setVolume(volume), setMuted: (muted: boolean) => playerRef.current?.setMuted(muted), setInView: (inView: boolean) => playerRef.current?.setInView(inView), collapse: () => playerRef.current?.collapse(), expand: () => playerRef.current?.expand(), enterFullscreen: () => playerRef.current?.enterFullscreen(), enterFullscreenLandscape: () => playerRef.current?.enterFullscreenLandscape(), exitFullscreen: () => playerRef.current?.exitFullscreen(), presentModal: () => playerRef.current?.presentModal(), closeModal: () => playerRef.current?.closeModal(), showCastPicker: () => playerRef.current?.showCastPicker(), destroy: () => playerRef.current?.destroy(), unload: () => playerRef.current?.unload(), autoPlayNextCancel: () => playerRef.current?.autoPlayNextCancel(), loadWithContentIdAndType: (...args: Parameters) => playerRef.current?.loadWithContentIdAndType(...args), /** @deprecated Use loadWithContentIdAndType() instead */ loadClip: (clipId: string, opts?: Parameters[1]) => playerRef.current?.loadClip(clipId, opts), loadWithClipId: (...args: Parameters) => playerRef.current?.loadWithClipId(...args), loadWithClipListId: (...args: Parameters) => playerRef.current?.loadWithClipListId(...args), loadWithProjectId: (...args: Parameters) => playerRef.current?.loadWithProjectId(...args), loadWithClipJson: (...args: Parameters) => playerRef.current?.loadWithClipJson(...args), loadWithClipListJson: (...args: Parameters) => playerRef.current?.loadWithClipListJson(...args), loadWithProjectJson: (...args: Parameters) => playerRef.current?.loadWithProjectJson(...args), loadWithJsonUrl: (...args: Parameters) => playerRef.current?.loadWithJsonUrl(...args), getDuration: () => playerRef.current?.getDuration() ?? Promise.resolve(null), getMuted: () => playerRef.current?.getMuted() ?? Promise.resolve(null), getVolume: () => playerRef.current?.getVolume() ?? Promise.resolve(null), getPhase: () => playerRef.current?.getPhase() ?? Promise.resolve(null), getState: () => playerRef.current?.getState() ?? Promise.resolve(null), getMode: () => playerRef.current?.getMode() ?? Promise.resolve(null), getClipData: () => playerRef.current?.getClipData() ?? Promise.resolve(null), getProjectData: () => playerRef.current?.getProjectData() ?? Promise.resolve(null), getPlayoutData: () => playerRef.current?.getPlayoutData() ?? Promise.resolve(null), getInView: () => playerRef.current?.getInView() ?? Promise.resolve(null), getAdMediaWidth: () => playerRef.current?.getAdMediaWidth() ?? Promise.resolve(null), getAdMediaHeight: () => playerRef.current?.getAdMediaHeight() ?? Promise.resolve(null), getIsCasting: () => playerRef.current?.getIsCasting() ?? Promise.resolve(null), getCastDeviceName: () => playerRef.current?.getCastDeviceName() ?? Promise.resolve(null), endCastSession: () => playerRef.current?.endCastSession(), getPlayerState: () => playerRef.current?.getPlayerState() ?? Promise.resolve(null), updatePlayout: (playoutJson: string) => playerRef.current?.updatePlayout(playoutJson), // Outstream-specific methods isCollapsed: () => isCollapsed, animateCollapse, animateExpand, }), [isCollapsed, animateCollapse, animateExpand] ); // Merge options with allowCollapseExpand: true const outstreamOptions = { ...options, allowCollapseExpand: true, }; return ( ); } ); BBOutstreamView.displayName = 'BBOutstreamPlayerView'; export default BBOutstreamView;