import type { HybridObject } from 'react-native-nitro-modules'; import type { PlaybackState, MediaPosition, ItemSource, RepeatMode, RemoteCommand, RemoteCommandEvent, NowPlayingInfo, PlaybackMetricEvent, PreloadConfig, BufferConfig, SubtitleTrack, AudioTrack, AvailableTracks, VideoTrackSelection, SubtitleTrackSelection, AudioTrackSelection, DomainEvent, AviationInterruptionMode, BecomingNoisyBehavior, } from './types.nitro'; import type { MediaItem } from './MediaItem.nitro'; import type { AudioSession } from './AudioSession.nitro'; /** * PlaybackEngine is the unified playback interface. * * It replaces the previous separate Player, Queue, and NowPlayingController * specs with a single HybridObject that wraps the C++ PlaybackCoordinator. * * Architecture: * JS -> PlaybackEngine (Nitro HybridObject) * -> C++ PlaybackCoordinator (domain core, owns state machine + event bus) * -> CommandEvents -> Native decoder adapters (AVPlayer / ExoPlayer) * <- DecoderEvents <- Native decoder adapters * * Commands return Promise for dual sync/async use: * engine.play() // fire-and-forget (sync dispatch) * await engine.play() // wait for state transition (async) * * Supersession: when a pending transport command (load, loadAndPlay, play, * skipTo*) is replaced by a newer one, the pending promise RESOLVES once the * newer command takes over — replacement by the caller's own newer intent is * not a failure. A pending command interrupted by stop() or release() * rejects, since the operation genuinely did not complete. * * Properties are always synchronous get/set. */ export interface PlaybackEngine extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { /** Stable native identity for this playback engine instance. */ readonly engineId: string; // ─── Playback Commands (async — state transitions) ────────────── /** * Load a media item without playing it. * IDLE/STOPPED -> LOADING -> READY. * Rejects on ERROR. */ load(item: MediaItem): Promise; /** * Load a media item and begin playback. * IDLE/STOPPED -> LOADING -> READY -> PLAYING. * Rejects on ERROR. */ loadAndPlay(item: MediaItem): Promise; /** * Begin or resume playback. * READY/PAUSED -> PLAYING. Rejects if no item loaded. */ play(): Promise; /** Pause playback. PLAYING -> PAUSED. */ pause(): Promise; /** Stop playback and release item resources. any -> STOPPED. */ stop(): Promise; /** Seek to position in milliseconds. Resolves when seek completes. */ seekTo(positionMs: number): Promise; /** Tear down all native resources. Always sync. */ release(): void; // ─── Decoder Registration ────────────────────────────────────── /** * Activate the default platform decoder (AVPlayer on iOS, ExoPlayer on Android). * * Must be called before any playback operations. Core never creates a decoder * itself — plugins call this during setup to inject the platform adapter. * * No-op if a decoder is already registered. */ activateDefaultDecoder(): void; // ─── Playback State (sync reads) ──────────────────────────────── /** Current domain-level playback state. */ readonly state: PlaybackState; /** * Unified time model — position, duration, seekable range, live edge. * For on-demand: progress 0.0–1.0, finite durationMs. * For live: progress 0, durationMs Infinity, liveEdgeOffsetMs. */ readonly mediaPosition: MediaPosition; /** The currently loaded MediaItem, or undefined if none. */ readonly currentItem: MediaItem | undefined; /** How the current item was loaded (queue, direct, external). */ readonly currentItemSource: ItemSource; /** Whether the engine's media type is a live stream. */ readonly isLive: boolean; /** Convenience: state === 'playing'. */ readonly isPlaying: boolean; /** Convenience: state === 'paused'. */ readonly isPaused: boolean; /** Convenience: state is 'ready', 'playing', 'paused', or 'buffering'. */ readonly isLoaded: boolean; /** Available subtitle tracks discovered from the current media. Empty array if none. */ readonly availableSubtitleTracks: SubtitleTrack[]; /** Available audio tracks discovered from the current media. Empty array if none. */ readonly availableAudioTracks: AudioTrack[]; // ─── Player Properties (sync get/set) ─────────────────────────── /** Volume from 0.0 (silent) to 1.0 (full). Default 1.0. */ volume: number; /** Playback speed. 1.0 = normal, 2.0 = double speed. Default 1.0. */ rate: number; /** Whether audio output is muted. Default false. */ muted: boolean; /** Whether playback loops the current item. Default false. */ loop: boolean; // ─── Queue ────────────────────────────────────────────────────── /** * Replace the entire queue. Does not auto-play. * Use skipToIndex(0) after to begin playback. */ setQueue(items: MediaItem[]): void; /** Append an item to the end of the queue. */ addToQueue(item: MediaItem): void; /** Insert an item after the given index. */ insertInQueue(item: MediaItem, afterIndex: number): void; /** Remove the item at the given index. */ removeFromQueue(index: number): void; /** Move an item from one position to another. */ moveInQueue(fromIndex: number, toIndex: number): void; /** Clear all items from the queue. */ clearQueue(): void; /** Skip to next item. LOADING -> PLAYING. Resolves when next item is playing. */ skipToNext(): Promise; /** Skip to previous item. Resolves when previous item is playing. */ skipToPrevious(): Promise; /** * Skip to a specific index. * * When `autoPlay` is true the item begins playing once loaded; when false it * is positioned and loaded but left paused at its start (state settles on * `ready` without an audible start). Either way the promise resolves once the * item has loaded (the `loading -> ready` transition), so callers can await * positioning without waiting for playback. */ skipToIndex(index: number, autoPlay: boolean): Promise; /** Current queue contents (read-only snapshot). */ readonly queueItems: MediaItem[]; /** Current index in the queue. -1 if queue is empty or no current item. */ readonly queueIndex: number; /** Total number of items in the queue. */ readonly queueCount: number; /** Repeat mode: 'off', 'one' (repeat current), 'all' (repeat queue). */ repeatMode: RepeatMode; /** Whether shuffle is enabled. */ shuffleEnabled: boolean; // ─── Now Playing / Remote Commands ────────────────────────────── /** * Set which remote commands are enabled on the lock screen / notification. * Only enabled commands appear as controls. */ setEnabledCommands(commands: RemoteCommand[]): void; /** * Set which commands appear in the compact notification view (Android). * Must be a subset of enabledCommands. Max 3. * iOS: no-op (system decides compact layout). */ setCompactCommands(commands: RemoteCommand[]): void; /** Set the skip forward/backward interval in milliseconds. Default 15000. */ setSkipIntervalMs(intervalMs: number): void; /** * Manually override now playing metadata. * If not called, metadata is derived from the current MediaItem. */ updateNowPlaying(info: NowPlayingInfo): void; /** * Mark a remote command as overridden by JS. * * Native suppresses its default action for this command and only * dispatches the event to onRemoteCommand. JS is then responsible * for performing the action. */ overrideRemoteCommand(command: RemoteCommand): void; /** Remove a JS override, restoring native default handling. */ clearRemoteCommandOverride(command: RemoteCommand): void; // ─── Audio Session ────────────────────────────────────────────── /** Bind an AudioSession to this engine. */ setAudioSession(session: AudioSession): void; // ─── Events (callback registration) ───────────────────────────── // // Nitro pattern: single callback per event type per registration. // The Nitro bridge translates these into native listener patterns. // The owning AviationPlayer fans out to multiple JS subscribers. /** * Clear all registered native callbacks. Called internally by * wireNativeCallbacks() before re-registering to prevent accumulation. * Not intended for direct use by consumers. */ clearCallbacks(): void; /** Fires when the playback state changes. */ onStateChange(callback: (state: PlaybackState) => void): void; /** Fires on time/position updates (~250ms interval during playback). */ onMediaPositionChange(callback: (position: MediaPosition) => void): void; /** * Fires when the current media item changes, regardless of source. * Covers: queue navigation, direct loadAndPlay(), external transport. */ onCurrentItemChange( callback: (item: MediaItem | undefined, source: ItemSource) => void ): void; /** Fires when a remote command is received (lock screen, headphones, etc). */ onRemoteCommand(callback: (event: RemoteCommandEvent) => void): void; /** Fires on playback errors. */ onError(callback: (message: string, code: string) => void): void; /** Fires when the current item finishes playing naturally. */ onPlaybackEnded(callback: () => void): void; /** * Fires when the queue reaches its end (last item finished, * repeat mode is 'off'). */ onQueueEnd(callback: () => void): void; /** Fires when queue contents or queue configuration changes. */ onQueueChange(callback: () => void): void; /** Fires on playback metric events (cache, preload, TTFF). */ onPlaybackMetric(callback: (event: PlaybackMetricEvent) => void): void; /** * Fires when available tracks are discovered from the media. * Dispatched after decoder reports tracks (typically after readyToPlay on iOS, * onTracksChanged on Android). Participates in clearCallbacks() lifecycle. */ onTracksAvailable(callback: (tracks: AvailableTracks) => void): void; /** * Fires for every domain event the coordinator emits — state, queue, * errors, properties, cache/preload metrics, and plugin-raised ad events * alike. One callback per engine; lossless delivery. */ onDomainEvent(callback: (event: DomainEvent) => void): void; /** * Set the interruption policy. Default 'resume'. A switch takes effect for * the next interruption; in-flight interruptions are not re-run. */ setInterruptionMode(mode: AviationInterruptionMode): void; /** * Set the "headphones unplugged" behavior. Default 'pause'. On Android * this configures the decoder at build time; on iOS it gates route-driven * pausing in the engine. */ setBecomingNoisyBehavior(behavior: BecomingNoisyBehavior): void; // ─── Track Selection ────────────────────────────────────────────── /** Select video track/resolution. */ selectVideoTrack(selection: VideoTrackSelection): void; /** Select subtitle/caption track. */ selectSubtitleTrack(selection: SubtitleTrackSelection): void; /** Select audio track. */ selectAudioTrack(selection: AudioTrackSelection): void; // ─── Cache Management ───────────────────────────────────────────── /** Clear the entire disk cache. Resolves when complete. */ clearCache(): Promise; /** Get current cache size in bytes. */ getCacheSize(): number; /** Check if a specific URI is cached. */ isCached(uri: string): boolean; /** Enable or disable caching while the player is active. */ setCacheEnabled(enabled: boolean): void; // ─── Preload Configuration ────────────────────────────────────────── /** * Imperatively preload a list of items (L1 cache pre-download + L2 warm pool). * Items are preloaded in the background. DRM and live content are skipped. * Preloading must be enabled via setup() or setPreloadEnabled() first. */ preload(items: MediaItem[]): void; /** Configure preload behavior (item count, TTL). */ setPreloadConfig(config: PreloadConfig): void; // ─── Buffer & Cache Configuration ─────────────────────────────────── /** Configure buffer durations for playback. */ setBufferConfig(config: BufferConfig): void; /** Set maximum cache size in bytes. Enforced during LRU eviction. */ setCacheMaxSize(sizeBytes: number): void; /** Enable or disable the preload system. */ setPreloadEnabled(enabled: boolean): void; } /** * Factory for creating PlaybackEngine instances. * Autolinked — default-constructible. */ export interface PlaybackEngineFactory extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> { create(): PlaybackEngine; }