import { NativeModules, findNodeHandle } from "react-native"; import type { BBContentType, LoadClipOptions, LoadContext, BBPlayerState } from "./BBPlayer.types"; import type { State, Phase } from "./types"; // Try TurboModule first, fallback to legacy NativeModules let BBPlayerModule: typeof import("./specs/NativeBBPlayerModule").default | null = null; try { // Try to use TurboModule (New Architecture) BBPlayerModule = require("./specs/NativeBBPlayerModule").default; } catch { // Fallback to legacy NativeModules (Old Architecture) BBPlayerModule = NativeModules.BBPlayerModule; } // Final fallback if both fail if (!BBPlayerModule) { BBPlayerModule = NativeModules.BBPlayerModule; } /** * Get the view tag (node handle) for a ref */ function getViewTag(viewRef: React.RefObject): number | null { const node = viewRef.current; if (node == null) { console.warn("BBPlayerView: View ref is null"); return null; } const nodeHandle = findNodeHandle(node); if (nodeHandle == null) { console.warn("BBPlayerView: Could not find node handle"); return null; } return nodeHandle; } /** * Creates command functions for the BBPlayerView ref. * Uses NativeModule for reliable command dispatch on both Old and New Architecture. */ export function createCommands(viewRef: React.RefObject) { return { play: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.play(tag); }, pause: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.pause(tag); }, seek: (position: number) => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.seek(tag, position); }, seekRelative: (offsetSeconds: number) => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.seekRelative(tag, offsetSeconds); }, setMuted: (muted: boolean) => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.setMuted(tag, muted); }, setVolume: (volume: number) => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.setVolume(tag, volume); }, enterFullscreen: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.enterFullscreen(tag); }, enterFullscreenLandscape: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.enterFullscreenLandscape(tag); }, exitFullscreen: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.exitFullscreen(tag); }, presentModal: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.presentModal(tag); }, closeModal: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.closeModal(tag); }, collapse: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.collapse(tag); }, expand: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.expand(tag); }, autoPlayNextCancel: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.autoPlayNextCancel(tag); }, destroy: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.destroy(tag); }, unload: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.unload(tag); }, showCastPicker: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.showCastPicker(tag); }, updatePlayout: (playoutJson: string) => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.updatePlayout(tag, playoutJson); }, /** @deprecated Use loadWithContentIdAndType() instead */ loadClip: (clipId: string, options?: LoadClipOptions) => { const tag = getViewTag(viewRef); if (tag != null) { if (options?.playout !== undefined) { console.warn('BBPlayerView.loadClip: the `playout` option is ignored. Use reinitializeIfNeeded() to switch playout.'); } const contextJson = options?.context ? JSON.stringify(options.context) : null; // Fall through to loadWithClipId when seekTo/initiator are used to preserve backward compat if (options?.seekTo !== undefined || options?.initiator !== undefined) { BBPlayerModule?.loadWithClipId(tag, clipId, options.initiator ?? null, options?.autoPlay ?? false, options?.seekTo ?? 0, contextJson); } else { BBPlayerModule?.loadWithContentIdAndType(tag, clipId, 'mediaclip', options?.autoPlay ?? false, contextJson); } } }, // Legacy load methods (kept for backwards compatibility) loadWithClipId: ( clipId: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext ) => { const tag = getViewTag(viewRef); if (tag != null) { const contextJson = context ? JSON.stringify(context) : null; BBPlayerModule?.loadWithClipId( tag, clipId, initiator ?? null, autoPlay ?? false, seekTo ?? 0, contextJson ); } }, loadWithClipListId: ( clipListId: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext ) => { const tag = getViewTag(viewRef); if (tag != null) { const contextJson = context ? JSON.stringify(context) : null; BBPlayerModule?.loadWithClipListId( tag, clipListId, initiator ?? null, autoPlay ?? false, seekTo ?? 0, contextJson ); } }, loadWithProjectId: ( projectId: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext ) => { const tag = getViewTag(viewRef); if (tag != null) { const contextJson = context ? JSON.stringify(context) : null; BBPlayerModule?.loadWithProjectId( tag, projectId, initiator ?? null, autoPlay ?? false, seekTo ?? 0, contextJson ); } }, loadWithClipJson: ( clipJson: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext ) => { const tag = getViewTag(viewRef); if (tag != null) { const contextJson = context ? JSON.stringify(context) : null; BBPlayerModule?.loadWithClipJson( tag, clipJson, initiator ?? null, autoPlay ?? false, seekTo ?? 0, contextJson ); } }, loadWithClipListJson: ( clipListJson: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext ) => { const tag = getViewTag(viewRef); if (tag != null) { const contextJson = context ? JSON.stringify(context) : null; BBPlayerModule?.loadWithClipListJson( tag, clipListJson, initiator ?? null, autoPlay ?? false, seekTo ?? 0, contextJson ); } }, loadWithProjectJson: ( projectJson: string, initiator?: string, autoPlay?: boolean, seekTo?: number, context?: LoadContext ) => { const tag = getViewTag(viewRef); if (tag != null) { const contextJson = context ? JSON.stringify(context) : null; BBPlayerModule?.loadWithProjectJson( tag, projectJson, initiator ?? null, autoPlay ?? false, seekTo ?? 0, contextJson ); } }, loadWithJsonUrl: (jsonUrl: string, autoPlay?: boolean, context?: LoadContext) => { const tag = getViewTag(viewRef); if (tag != null) { const contextJson = context ? JSON.stringify(context) : null; BBPlayerModule?.loadWithJsonUrl(tag, jsonUrl, autoPlay ?? true, contextJson); } }, loadWithContentIdAndType: (contentId: string, contentType: BBContentType, autoPlay?: boolean, context?: LoadContext) => { const tag = getViewTag(viewRef); if (tag != null) { const contextJson = context ? JSON.stringify(context) : null; BBPlayerModule?.loadWithContentIdAndType(tag, contentId, contentType, autoPlay ?? true, contextJson); } }, // Note: For Shorts playback, use the BBShortsView component instead of BBPlayerView. // Viewability control setInView: (inView: boolean) => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.setInView(tag, inView); }, // Getter methods (async) getDuration: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getDuration) { return BBPlayerModule.getDuration(tag); } return null; }, getMuted: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getMuted) { return BBPlayerModule.getMuted(tag); } return null; }, getVolume: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getVolume) { return BBPlayerModule.getVolume(tag); } return null; }, getPhase: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getPhase) { return BBPlayerModule.getPhase(tag); } return null; }, getState: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getState) { return BBPlayerModule.getState(tag); } return null; }, getMode: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getMode) { return BBPlayerModule.getMode(tag); } return null; }, getClipData: async (): Promise<{ id?: string; title?: string; description?: string; length?: number; } | null> => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getClipData) { return BBPlayerModule.getClipData(tag); } return null; }, getProjectData: async (): Promise<{ id?: string; name?: string; } | null> => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getProjectData) { return BBPlayerModule.getProjectData(tag); } return null; }, getPlayoutData: async (): Promise<{ name?: string; } | null> => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getPlayoutData) { return BBPlayerModule.getPlayoutData(tag); } return null; }, getInView: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getInView) { return BBPlayerModule.getInView(tag); } return null; }, getAdMediaWidth: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getAdMediaWidth) { return BBPlayerModule.getAdMediaWidth(tag); } return null; }, getAdMediaHeight: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getAdMediaHeight) { return BBPlayerModule.getAdMediaHeight(tag); } return null; }, getIsCasting: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getIsCasting) { return BBPlayerModule.getIsCasting(tag); } return null; }, getCastDeviceName: async (): Promise => { const tag = getViewTag(viewRef); if (tag != null && BBPlayerModule?.getCastDeviceName) { return BBPlayerModule.getCastDeviceName(tag); } return null; }, endCastSession: () => { const tag = getViewTag(viewRef); if (tag != null) BBPlayerModule?.endCastSession(tag); }, /** * Get the complete player state as a structured object. * Aggregates all individual getter calls into a single BBPlayerState object. */ getPlayerState: async (): Promise => { const tag = getViewTag(viewRef); if (tag == null || !BBPlayerModule) { return null; } try { // Fetch all state in parallel for efficiency const [ state, phase, mode, duration, muted, volume, clipData, projectData, playoutData, ] = await Promise.all([ BBPlayerModule.getState?.(tag) ?? null, BBPlayerModule.getPhase?.(tag) ?? null, BBPlayerModule.getMode?.(tag) ?? null, BBPlayerModule.getDuration?.(tag) ?? 0, BBPlayerModule.getMuted?.(tag) ?? false, BBPlayerModule.getVolume?.(tag) ?? 1, BBPlayerModule.getClipData?.(tag) ?? null, BBPlayerModule.getProjectData?.(tag) ?? null, BBPlayerModule.getPlayoutData?.(tag) ?? null, ]); return { state: (state as State) ?? "IDLE", phase: (phase as Phase) ?? "INIT", mode: mode ?? null, duration: duration ?? 0, muted: muted ?? false, volume: volume ?? 1, clip: clipData, project: projectData, playout: playoutData, }; } catch (error) { console.warn("BBPlayerView: Failed to get player state", error); return null; } }, }; }