242 lines
6.6 KiB
TypeScript
242 lines
6.6 KiB
TypeScript
/**
|
|
* 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<string, unknown>) => {
|
|
logger.log('flv.js: Metadata arrived', metadata)
|
|
|
|
// Trigger onLoadedMetadata callback
|
|
if (onLoadedMetadata) {
|
|
onLoadedMetadata()
|
|
}
|
|
})
|
|
|
|
player.on(flvjs.Events.STATISTICS_INFO, (stats: Record<string, unknown>) => {
|
|
// 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<string, unknown> | 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
|
|
}
|