Add SRT/FLV/RTMP support and update documentation

Introduced Python scripts for SRT subtitle checking and fixing, and added comprehensive documentation covering advanced features such as protocol detection, subtitle/audio/quality management, keyboard shortcuts, and touch gestures. Updated local settings to allow new build and Python commands, added TypeScript definitions for FLV, and implemented RTMP/FLV protocol support in the player. Removed CHANGELOG.md and made various improvements to styles and example app.
This commit is contained in:
hibna
2025-11-03 02:35:56 +03:00
parent 42a12dfa8b
commit 36f83ff72c
26 changed files with 3833 additions and 1212 deletions
+58 -6
View File
@@ -3,7 +3,7 @@
* Handles loading hls.js from npm or CDN
*/
import type { AudioTrack, VideoQuality } from '../types'
import type { AudioTrack, VideoQuality, SubtitleTrack } from '../types'
import { getTranslations, detectBrowserLanguage } from '../i18n'
// Re-export control functions for backward compatibility
@@ -47,15 +47,20 @@ const loadHlsFromCDN = (): Promise<any> => {
*/
export const loadHls = async (): Promise<any> => {
try {
console.log('[HLS Loader] Attempting to load from npm package...')
// Try loading from npm package first
const hlsModule = await import('hls.js')
console.log('[HLS Loader] Successfully loaded from npm package')
return hlsModule.default
} catch {
} catch (npmError) {
console.warn('[HLS Loader] Failed to load from npm, trying CDN...', npmError)
try {
// Fallback to CDN
const Hls = await loadHlsFromCDN()
console.log('[HLS Loader] Successfully loaded from CDN')
return Hls
} catch {
} catch (cdnError) {
console.error('[HLS Loader] Failed to load from CDN:', cdnError)
throw new Error('Unable to load HLS.js library. HLS streaming is not available.')
}
}
@@ -82,14 +87,18 @@ export const hasNativeHlsSupport = (): boolean => {
export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
try {
if (!hls) {
console.warn('[HLS Loader] getHlsAudioTracks: No HLS instance provided')
return []
}
// Check if audioTracks property exists
if (!hls.audioTracks || !Array.isArray(hls.audioTracks)) {
console.warn('[HLS Loader] getHlsAudioTracks: No audioTracks array found on HLS instance')
return []
}
console.log('[HLS Loader] getHlsAudioTracks: Raw audioTracks from HLS:', hls.audioTracks)
const audioTracks: AudioTrack[] = hls.audioTracks.map((track: any, index: number) => {
const audioTrack = {
name: track.name || track.label || `Audio ${index + 1}`,
@@ -102,7 +111,38 @@ export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
return audioTrack
})
console.log('[HLS Loader] getHlsAudioTracks: Processed tracks:', audioTracks)
return audioTracks
} catch (error) {
console.error('[HLS Loader] getHlsAudioTracks: Error extracting audio tracks:', error)
return []
}
}
/**
* Extract subtitle tracks from HLS instance
*/
export const getHlsSubtitleTracks = (hls: any): SubtitleTrack[] => {
try {
if (!hls) {
return []
}
// Check if subtitleTracks property exists
if (!hls.subtitleTracks || !Array.isArray(hls.subtitleTracks)) {
return []
}
const subtitleTracks: SubtitleTrack[] = hls.subtitleTracks.map((track: any, index: number) => {
return {
label: track.name || track.label || `Subtitle ${index + 1}`,
lang: track.lang || track.language || 'unknown',
src: track.url || '',
default: track.default || false,
}
})
return subtitleTracks
} catch {
return []
}
@@ -113,10 +153,18 @@ export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
*/
export const getHlsQualities = (hls: any): VideoQuality[] => {
try {
if (!hls || !Array.isArray(hls.levels)) {
if (!hls) {
console.warn('[HLS Loader] getHlsQualities: No HLS instance provided')
return []
}
if (!Array.isArray(hls.levels)) {
console.warn('[HLS Loader] getHlsQualities: No levels array found on HLS instance')
return []
}
console.log('[HLS Loader] getHlsQualities: Raw levels from HLS:', hls.levels)
const qualities: VideoQuality[] = hls.levels.map((level: any, index: number) => {
const resolution = typeof level.attrs?.RESOLUTION === 'string' ? level.attrs.RESOLUTION : undefined
const [widthFromResolution, heightFromResolution] = resolution
@@ -149,14 +197,18 @@ export const getHlsQualities = (hls: any): VideoQuality[] => {
}
})
return qualities.sort((a, b) => {
const sortedQualities = qualities.sort((a, b) => {
const heightDifference = (b.height || 0) - (a.height || 0)
if (heightDifference !== 0) {
return heightDifference
}
return (b.bitrate || 0) - (a.bitrate || 0)
})
} catch {
console.log('[HLS Loader] getHlsQualities: Processed qualities:', sortedQualities)
return sortedQualities
} catch (error) {
console.error('[HLS Loader] getHlsQualities: Error extracting qualities:', error)
return []
}
}
+58 -4
View File
@@ -2,7 +2,7 @@
* HLS setup and configuration utilities
*/
import type { AudioTrack, VideoQuality } from '../types'
import type { AudioTrack, VideoQuality, SubtitleTrack } from '../types'
interface HlsSetupOptions {
video: HTMLVideoElement
@@ -10,6 +10,7 @@ interface HlsSetupOptions {
autoplay: boolean
onAudioTracksLoaded?: (tracks: AudioTrack[]) => void
onQualityLevelsLoaded?: (qualities: VideoQuality[]) => void
onSubtitleTracksLoaded?: (tracks: SubtitleTrack[]) => void
onError?: (error: Error) => void
}
@@ -19,47 +20,100 @@ export const setupHlsInstance = async ({
autoplay,
onAudioTracksLoaded,
onQualityLevelsLoaded,
onSubtitleTracksLoaded,
onError,
}: HlsSetupOptions): Promise<() => void> => {
const { loadHls, isHlsSupported, getHlsAudioTracks, getHlsQualities } = await import('./hlsLoader')
const { loadHls, isHlsSupported, getHlsAudioTracks, getHlsQualities, getHlsSubtitleTracks } = await import('./hlsLoader')
const Hls = await loadHls()
if (!isHlsSupported(Hls)) {
throw new Error('HLS.js is not supported in this browser')
}
console.log('[HLS Setup] Creating HLS instance for:', src)
const hls = new Hls({
enableWorker: true,
lowLatencyMode: false,
debug: false,
})
hls.loadSource(src)
hls.attachMedia(video)
let manifestParsedHandled = false
hls.on(Hls.Events.MANIFEST_PARSED, () => {
setTimeout(() => {
console.log('[HLS Setup] MANIFEST_PARSED event fired')
if (manifestParsedHandled) {
console.warn('[HLS Setup] MANIFEST_PARSED already handled, skipping')
return
}
manifestParsedHandled = true
// Wait for tracks to be fully populated
const loadTracks = () => {
const tracks = getHlsAudioTracks(hls)
const qualities = getHlsQualities(hls)
const subtitles = getHlsSubtitleTracks(hls)
console.log('[HLS Setup] Detected tracks:', {
audioTracks: tracks.length,
qualities: qualities.length,
subtitles: subtitles.length
})
if (tracks.length > 0) {
console.log('[HLS Setup] Loading audio tracks:', tracks)
onAudioTracksLoaded?.(tracks)
}
if (subtitles.length > 0) {
console.log('[HLS Setup] Loading subtitle tracks:', subtitles)
onSubtitleTracksLoaded?.(subtitles)
}
console.log('[HLS Setup] Loading quality levels:', qualities)
onQualityLevelsLoaded?.(qualities)
}, 100)
}
// Try immediately first
loadTracks()
// Also retry after a delay for Chrome compatibility
setTimeout(loadTracks, 200)
if (autoplay) {
void video.play().catch(() => undefined)
}
})
// Listen for LEVEL_LOADED to ensure qualities are populated
hls.on(Hls.Events.LEVEL_LOADED, () => {
const qualities = getHlsQualities(hls)
if (qualities.length > 0) {
console.log('[HLS Setup] LEVEL_LOADED - Qualities available:', qualities.length)
onQualityLevelsLoaded?.(qualities)
}
})
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, () => {
const tracks = getHlsAudioTracks(hls)
console.log('[HLS Setup] AUDIO_TRACKS_UPDATED event:', tracks.length, 'tracks')
if (tracks.length > 0) {
onAudioTracksLoaded?.(tracks)
}
})
hls.on(Hls.Events.SUBTITLE_TRACKS_UPDATED, () => {
const subtitles = getHlsSubtitleTracks(hls)
console.log('[HLS Setup] SUBTITLE_TRACKS_UPDATED event:', subtitles.length, 'tracks')
if (subtitles.length > 0) {
onSubtitleTracksLoaded?.(subtitles)
}
})
hls.on(Hls.Events.ERROR, (_event: any, data: any) => {
if (data.fatal) {
switch (data.type) {
+87 -1
View File
@@ -1,4 +1,4 @@
import type { AudioTrack } from '../types'
import type { AudioTrack, SubtitleTrack } from '../types'
/**
* Parses M3U8 manifest to extract audio tracks
@@ -73,6 +73,75 @@ const parseAudioMediaTag = (line: string): AudioTrack | null => {
}
}
/**
* Parses M3U8 manifest to extract subtitle tracks
*/
export const parseM3U8SubtitleTracks = (manifestContent: string): SubtitleTrack[] => {
const subtitleTracks: SubtitleTrack[] = []
const lines = manifestContent.split('\n')
for (const line of lines) {
if (line.startsWith('#EXT-X-MEDIA:TYPE=SUBTITLES')) {
const track = parseSubtitleMediaTag(line)
if (track) {
subtitleTracks.push(track)
}
}
}
return subtitleTracks
}
/**
* Parses a single #EXT-X-MEDIA:TYPE=SUBTITLES line
* Example: #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",URI="subtitles_en.m3u8"
*/
const parseSubtitleMediaTag = (line: string): SubtitleTrack | null => {
try {
const attributes: Record<string, string> = {}
// Extract all key-value pairs
const regex = /(\w+(?:-\w+)*)=("(?:[^"\\]|\\.)*"|[^,]+)/g
let match
while ((match = regex.exec(line)) !== null) {
const key = match[1]
let value = match[2]
// Remove quotes if present
if (value.startsWith('"') && value.endsWith('"')) {
value = value.slice(1, -1)
}
attributes[key] = value
}
// Only process if it's a SUBTITLES type
if (attributes['TYPE'] !== 'SUBTITLES') {
return null
}
// Extract required fields
const name = attributes['NAME']
const language = attributes['LANGUAGE'] || attributes['LANG'] || 'unknown'
const uri = attributes['URI']
const defaultTrack = attributes['DEFAULT'] === 'YES'
if (!name || !uri) {
return null
}
return {
label: name,
lang: language,
src: uri,
default: defaultTrack,
}
} catch {
return null
}
}
/**
* Fetches and parses M3U8 manifest from URL
*/
@@ -89,3 +158,20 @@ export const fetchAndParseM3U8 = async (url: string): Promise<AudioTrack[]> => {
return []
}
}
/**
* Fetches and parses M3U8 subtitle tracks from URL
*/
export const fetchAndParseM3U8Subtitles = async (url: string): Promise<SubtitleTrack[]> => {
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`Failed to fetch M3U8: ${response.statusText}`)
}
const manifestContent = await response.text()
return parseM3U8SubtitleTracks(manifestContent)
} catch {
return []
}
}
+168
View File
@@ -0,0 +1,168 @@
/**
* RTMP/FLV player dynamic loader
* Loads flv.js library with NPM fallback to CDN strategy
* Mirrors the HLS loader pattern for consistency
*/
const FLVJS_CDN_URL = 'https://cdn.jsdelivr.net/npm/flv.js@1.6.2/dist/flv.min.js'
/**
* Dynamically loads flv.js from CDN
* @returns Promise that resolves to the flv.js library
*/
const loadFlvjsFromCDN = async (): Promise<any> => {
return new Promise((resolve, reject) => {
if ((window as any).flvjs) {
resolve((window as any).flvjs)
return
}
const script = document.createElement('script')
script.src = FLVJS_CDN_URL
script.async = true
script.onload = () => {
if ((window as any).flvjs) {
resolve((window as any).flvjs)
} else {
reject(new Error('flv.js loaded but not available on window'))
}
}
script.onerror = () => {
reject(new Error(`Failed to load flv.js from CDN: ${FLVJS_CDN_URL}`))
}
document.head.appendChild(script)
})
}
/**
* Loads flv.js library
* Tries NPM package first, falls back to CDN if unavailable
* @returns Promise that resolves to the flv.js library
*/
export const loadFlvjs = async (): Promise<any> => {
try {
// Try loading from NPM package first
const flvModule = await import('flv.js')
return flvModule.default || flvModule
} catch (npmError) {
console.warn('flv.js NPM package not available, loading from CDN...', npmError)
try {
// Fallback to CDN
const flvjs = await loadFlvjsFromCDN()
return flvjs
} catch (cdnError) {
console.error('Failed to load flv.js from both NPM and CDN', cdnError)
throw new Error(
'Failed to load flv.js library. Please ensure flv.js is available or check your network connection.'
)
}
}
}
/**
* Checks if flv.js is supported in the current browser
* @param flvjs - The flv.js library instance
* @returns True if supported
*/
export const isFlvjsSupported = (flvjs: any): boolean => {
if (!flvjs) {
return false
}
// flv.js requires Media Source Extensions (MSE)
return flvjs.isSupported ? flvjs.isSupported() : false
}
/**
* Gets browser support information for flv.js
* @param flvjs - The flv.js library instance
* @returns Support information object
*/
export const getFlvjsSupportInfo = (flvjs: any): {
mseSupported: boolean
networkStreamIOSupported: boolean
httpsSupported: boolean
} => {
if (!flvjs || !flvjs.getFeatureList) {
return {
mseSupported: false,
networkStreamIOSupported: false,
httpsSupported: false,
}
}
const features = flvjs.getFeatureList()
return {
mseSupported: features.mseSupported || false,
networkStreamIOSupported: features.networkStreamIOSupported || false,
httpsSupported: features.httpsSupported || false,
}
}
/**
* Creates default flv.js configuration for RTMP/FLV streams
* @param isLive - Whether the stream is live
* @returns flv.js configuration object
*/
export const createDefaultFlvConfig = (isLive: boolean = true) => {
return {
enableWorker: true, // Enable worker for better performance
enableStashBuffer: !isLive, // Disable stash buffer for live streams (low latency)
stashInitialSize: isLive ? 128 : undefined, // Smaller initial size for live
isLive: isLive,
lazyLoad: false,
lazyLoadMaxDuration: 3 * 60, // 3 minutes for VOD
lazyLoadRecoverDuration: 30, // 30 seconds
deferLoadAfterSourceOpen: false,
autoCleanupSourceBuffer: true,
autoCleanupMaxBackwardDuration: 3 * 60, // 3 minutes
autoCleanupMinBackwardDuration: 2 * 60, // 2 minutes
fixAudioTimestampGap: true,
accurateSeek: !isLive, // Disable accurate seek for live streams
seekType: isLive ? 'range' : 'param',
seekParamStart: 'start',
rangeLoadZeroStart: false,
reuseRedirectedURL: true,
}
}
/**
* Extracts quality information from flv.js statistics (if available)
* Note: Unlike HLS, FLV/RTMP streams typically don't have multiple quality levels
* This is primarily for metadata display
* @param player - The flv.js player instance
* @returns Basic quality information
*/
export const extractFlvQualityInfo = (player: any): {
width?: number
height?: number
videoCodec?: string
audioCodec?: string
fps?: number
videoBitrate?: number
audioBitrate?: number
} | null => {
if (!player || !player.statisticsInfo) {
return null
}
try {
const stats = player.statisticsInfo
return {
width: stats.videoWidth,
height: stats.videoHeight,
videoCodec: stats.videoCodec,
audioCodec: stats.audioCodec,
fps: stats.fps,
videoBitrate: stats.videoBitrate,
audioBitrate: stats.audioBitrate,
}
} catch (error) {
console.warn('Failed to extract flv.js quality info:', error)
return null
}
}
+240
View File
@@ -0,0 +1,240 @@
/**
* 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'
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
console.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 as any).__rtmpInstance = player
// Event handlers
player.on(flvjs.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => {
console.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) {
console.error('Network error occurred:', errorDetail)
// Attempt recovery for recoverable network errors
if (
errorDetail === flvjs.ErrorDetails.NETWORK_EXCEPTION ||
errorDetail === flvjs.ErrorDetails.NETWORK_STATUS_CODE_INVALID
) {
console.log('Attempting to recover from network error...')
try {
player.unload()
player.load()
return
} catch (recoveryError) {
console.error('Failed to recover from network error:', recoveryError)
}
}
} else if (errorType === flvjs.ErrorTypes.MEDIA_ERROR) {
console.error('Media error occurred:', errorDetail)
// Some media errors are recoverable
if (errorDetail === flvjs.ErrorDetails.MEDIA_MSE_ERROR) {
console.log('Attempting to recover from media error...')
try {
player.unload()
player.load()
return
} catch (recoveryError) {
console.error('Failed to recover from media error:', recoveryError)
}
}
}
// Call error callback
if (onError) {
onError(error)
}
})
player.on(flvjs.Events.LOADING_COMPLETE, () => {
console.log('flv.js: Loading complete')
})
player.on(flvjs.Events.RECOVERED_EARLY_EOF, () => {
console.log('flv.js: Recovered from early EOF')
})
player.on(flvjs.Events.METADATA_ARRIVED, (metadata: any) => {
console.log('flv.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback
if (onLoadedMetadata) {
onLoadedMetadata()
}
})
player.on(flvjs.Events.STATISTICS_INFO, (stats: any) => {
// Statistics info for debugging/monitoring
// Can be used to display stream quality, bitrate, etc.
if (stats) {
;(video as any).__rtmpStats = stats
}
})
// Auto-play if requested
if (autoplay) {
try {
await video.play()
} catch (playError) {
console.warn('Autoplay failed:', playError)
// Autoplay might be blocked by browser, ignore error
}
}
// Return cleanup function
return () => {
try {
console.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 as any).__rtmpInstance
delete (video as any).__rtmpStats
} catch (cleanupError) {
console.error('Error during flv.js cleanup:', cleanupError)
}
}
} catch (error) {
console.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): any | null => {
if (!video) {
return null
}
return (video as any).__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): any | null => {
if (!video) {
return null
}
return (video as any).__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
}
+110
View File
@@ -0,0 +1,110 @@
/**
* Video protocol detection utility
* Detects the streaming protocol from a video URL
*/
export type VideoProtocol = 'native' | 'hls' | 'rtmp' | 'dash'
export interface ProtocolDetectionResult {
protocol: VideoProtocol
isLive: boolean
needsSpecialPlayer: boolean
}
/**
* Detects the video protocol from the source URL
* @param src - The video source URL
* @returns Protocol detection result
*/
export const detectVideoProtocol = (src: string): ProtocolDetectionResult => {
if (!src) {
return {
protocol: 'native',
isLive: false,
needsSpecialPlayer: false,
}
}
const lowerSrc = src.toLowerCase()
// RTMP protocol detection
// Supports: rtmp://, rtmps://, rtmpt://, rtmpe://
if (
lowerSrc.startsWith('rtmp://') ||
lowerSrc.startsWith('rtmps://') ||
lowerSrc.startsWith('rtmpt://') ||
lowerSrc.startsWith('rtmpe://')
) {
return {
protocol: 'rtmp',
isLive: true, // RTMP is typically used for live streaming
needsSpecialPlayer: true,
}
}
// HLS protocol detection
// Check for .m3u8 extension or HLS URL patterns
if (lowerSrc.includes('.m3u8')) {
return {
protocol: 'hls',
isLive: lowerSrc.includes('/live/') || lowerSrc.includes('live.m3u8'),
needsSpecialPlayer: true,
}
}
// DASH protocol detection
// Check for .mpd extension
if (lowerSrc.includes('.mpd')) {
return {
protocol: 'dash',
isLive: lowerSrc.includes('/live/') || lowerSrc.includes('live.mpd'),
needsSpecialPlayer: true,
}
}
// HTTP-FLV detection (alternative to RTMP)
if (lowerSrc.includes('.flv') || lowerSrc.includes('flv?')) {
return {
protocol: 'rtmp', // Use RTMP player for FLV files
isLive: lowerSrc.includes('/live/') || lowerSrc.includes('live.flv'),
needsSpecialPlayer: true,
}
}
// Native HTML5 video formats (MP4, WebM, OGG, etc.)
return {
protocol: 'native',
isLive: false,
needsSpecialPlayer: false,
}
}
/**
* Checks if the URL is an RTMP stream
* @param src - The video source URL
* @returns True if RTMP stream
*/
export const isRtmpStream = (src: string): boolean => {
const detection = detectVideoProtocol(src)
return detection.protocol === 'rtmp'
}
/**
* Checks if the URL is an HLS stream
* @param src - The video source URL
* @returns True if HLS stream
*/
export const isHlsStream = (src: string): boolean => {
const detection = detectVideoProtocol(src)
return detection.protocol === 'hls'
}
/**
* Checks if the stream is live
* @param src - The video source URL
* @returns True if live stream
*/
export const isLiveStream = (src: string): boolean => {
const detection = detectVideoProtocol(src)
return detection.isLive
}