/** * RTMP/FLV player setup utility * Initializes and configures flv.js player for RTMP/FLV streams * Mirrors the HLS setup pattern for consistency */ import { loadFlvjs, isFlvjsSupported, createDefaultFlvConfig } from './rtmpLoader' import { isLiveStream } from './videoProtocol' import { logger } from './logger' export interface RtmpSetupOptions { video: HTMLVideoElement src: string autoplay?: boolean onError?: (error: Error) => void onLoadedMetadata?: () => void } /** * Sets up flv.js player instance for RTMP/FLV streaming * @param options - Setup options * @returns Cleanup function to destroy the player */ export const setupRtmpInstance = async ({ video, src, autoplay = false, onError, onLoadedMetadata, }: RtmpSetupOptions): Promise<() => void> => { try { // Load flv.js library const flvjs = await loadFlvjs() // Check if flv.js is supported if (!isFlvjsSupported(flvjs)) { const error = new Error( 'flv.js is not supported in this browser. Media Source Extensions (MSE) is required.' ) if (onError) { onError(error) } throw error } // Detect if stream is live const isLive = isLiveStream(src) // Determine media type let type = 'flv' if (src.startsWith('rtmp://') || src.startsWith('rtmps://')) { // For RTMP URLs, flv.js expects HTTP-FLV endpoint // This is a limitation - direct RTMP playback requires server-side conversion logger.warn( 'Direct RTMP playback requires an HTTP-FLV proxy. Please ensure your RTMP stream is available via HTTP-FLV.' ) type = 'flv' } else if (src.includes('.flv')) { type = 'flv' } // Create flv.js player configuration const config = createDefaultFlvConfig(isLive) // Create player instance const player = flvjs.createPlayer( { type: type, url: src, isLive: isLive, hasAudio: true, hasVideo: true, }, config ) // Attach to video element player.attachMediaElement(video) // Load the stream player.load() // Store player instance on video element for later access video.__rtmpInstance = player // Event handlers player.on(flvjs.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: unknown) => { logger.error('flv.js error:', { errorType, errorDetail, errorInfo }) const error = new Error(`FLV Player Error: ${errorType} - ${errorDetail}`) // Handle specific error types if (errorType === flvjs.ErrorTypes.NETWORK_ERROR) { logger.error('Network error occurred:', errorDetail) // Attempt recovery for recoverable network errors if ( errorDetail === flvjs.ErrorDetails.NETWORK_EXCEPTION || errorDetail === flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID ) { logger.log('Attempting to recover from network error...') try { player.unload() player.load() return } catch (recoveryError) { logger.error('Failed to recover from network error:', recoveryError) } } } else if (errorType === flvjs.ErrorTypes.MEDIA_ERROR) { logger.error('Media error occurred:', errorDetail) // Some media errors are recoverable if (errorDetail === flvjs.ErrorDetails.MEDIA_MSE_ERROR) { logger.log('Attempting to recover from media error...') try { player.unload() player.load() return } catch (recoveryError) { logger.error('Failed to recover from media error:', recoveryError) } } } // Call error callback if (onError) { onError(error) } }) player.on(flvjs.Events.LOADING_COMPLETE, () => { logger.log('flv.js: Loading complete') }) player.on(flvjs.Events.RECOVERED_EARLY_EOF, () => { logger.log('flv.js: Recovered from early EOF') }) player.on(flvjs.Events.METADATA_ARRIVED, (metadata: Record) => { logger.log('flv.js: Metadata arrived', metadata) // Trigger onLoadedMetadata callback if (onLoadedMetadata) { onLoadedMetadata() } }) player.on(flvjs.Events.STATISTICS_INFO, (stats: Record) => { // Statistics info for debugging/monitoring // Can be used to display stream quality, bitrate, etc. if (stats) { video.__rtmpStats = stats } }) // Auto-play if requested if (autoplay) { try { await video.play() } catch (playError) { logger.warn('Autoplay failed:', playError) // Autoplay might be blocked by browser, ignore error } } // Return cleanup function return () => { try { logger.log('Cleaning up flv.js player...') // Remove event listeners player.off(flvjs.Events.ERROR) player.off(flvjs.Events.LOADING_COMPLETE) player.off(flvjs.Events.RECOVERED_EARLY_EOF) player.off(flvjs.Events.METADATA_ARRIVED) player.off(flvjs.Events.STATISTICS_INFO) // Pause and unload video.pause() player.unload() // Detach from video element player.detachMediaElement() // Destroy player instance player.destroy() // Clean up stored references delete video.__rtmpInstance delete video.__rtmpStats } catch (cleanupError) { logger.error('Error during flv.js cleanup:', cleanupError) } } } catch (error) { logger.error('Failed to setup flv.js player:', error) const setupError = error instanceof Error ? error : new Error('Failed to setup RTMP/FLV player') if (onError) { onError(setupError) } throw setupError } } /** * Gets the current flv.js player instance from a video element * @param video - The video element * @returns The flv.js player instance or null */ export const getRtmpInstance = (video: HTMLVideoElement | null): FlvjsPlayer | PlayerInstance | null => { if (!video) { return null } return video.__rtmpInstance ?? null } /** * Gets the current flv.js statistics from a video element * @param video - The video element * @returns The statistics object or null */ export const getRtmpStats = (video: HTMLVideoElement | null): Record | null => { if (!video) { return null } return video.__rtmpStats ?? null } /** * Checks if a video element has an active RTMP player instance * @param video - The video element * @returns True if has active instance */ export const hasRtmpInstance = (video: HTMLVideoElement | null): boolean => { return getRtmpInstance(video) !== null }