Add MPEG-TS support and related utilities

Introduces MPEG-TS streaming support by adding mpegts.js as an optional dependency, updating keywords, and implementing new utility modules for MPEG-TS loading and setup. Updates VideoElement and videoProtocol logic to handle MPEG-TS streams, adds corresponding tests and mocks, and improves local settings for npm install.
This commit is contained in:
hibna
2025-11-04 06:36:21 +03:00
parent db2e3a0722
commit b0e278afb5
10 changed files with 380 additions and 3594 deletions
+2 -1
View File
@@ -11,7 +11,8 @@
"Bash(npm run build:lib:*)",
"Bash(npm publish)",
"Bash(git push:*)",
"Bash(npm test:*)"
"Bash(npm test:*)",
"Bash(npm install:*)"
],
"deny": [],
"ask": []
+4 -1
View File
@@ -57,7 +57,8 @@
},
"optionalDependencies": {
"hls.js": "^1.6.13",
"flv.js": "^1.6.2"
"flv.js": "^1.6.2",
"mpegts.js": "^1.7.3"
},
"keywords": [
"react",
@@ -67,6 +68,8 @@
"hls",
"rtmp",
"flv",
"mpegts",
"iptv",
"streaming",
"media"
],
-3583
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -5,6 +5,7 @@ import { validateVideoURL, getCORSErrorMessage, isCORSError } from '../utils/cor
import { setHlsAudioTrack, setHlsQualityLevel } from '../utils/hlsControl'
import { setupHlsInstance } from '../utils/hlsSetup'
import { setupRtmpInstance } from '../utils/rtmpSetup'
import { setupMpegtsInstance } from '../utils/mpegtsSetup'
import { detectVideoProtocol } from '../utils/videoProtocol'
import { createSubtitleBlobURL } from '../utils/subtitles'
import './VideoElement.css'
@@ -391,6 +392,19 @@ export const VideoElement: React.FC<VideoElementProps> = ({
break
}
case 'mpegts': {
// MPEG-TS/IPTV streaming setup
console.log('[VideoElement] Setting up MPEG-TS player...')
cleanupFn = await setupMpegtsInstance({
video,
src,
autoplay,
onError: handleError,
onLoadedMetadata,
})
break
}
case 'dash': {
// DASH streaming - not yet implemented
const error = new Error('DASH streaming is not yet supported')
@@ -451,6 +465,13 @@ export const VideoElement: React.FC<VideoElementProps> = ({
}
delete (video as any).__rtmpInstance
}
if ((video as any).__mpegtsInstance) {
const mpegts = (video as any).__mpegtsInstance
if (mpegts && typeof mpegts.destroy === 'function') {
mpegts.destroy()
}
delete (video as any).__mpegtsInstance
}
}
}, [
src,
+9
View File
@@ -0,0 +1,9 @@
// Mock for mpegts.js library used in tests
export default {
isSupported: () => true,
getFeatureList: () => ({
mseSupported: true,
networkStreamIOSupported: true,
httpsSupported: true,
}),
}
+107
View File
@@ -0,0 +1,107 @@
/**
* MPEG-TS loader utility
* Dynamically loads mpegts.js library
*/
export interface MpegtsConfig {
enableWorker?: boolean
enableStashBuffer?: boolean
stashInitialSize?: number
isLive?: boolean
lazyLoad?: boolean
lazyLoadMaxDuration?: number
lazyLoadRecoverDuration?: number
deferLoadAfterSourceOpen?: boolean
autoCleanupSourceBuffer?: boolean
autoCleanupMaxBackwardDuration?: number
autoCleanupMinBackwardDuration?: number
fixAudioTimestampGap?: boolean
headers?: Record<string, string>
}
let mpegtsInstance: any = null
let loadingPromise: Promise<any> | null = null
/**
* Load mpegts.js library dynamically
* @returns Promise that resolves to mpegts.js module
*/
export const loadMpegts = async (): Promise<any> => {
// Return cached instance if available
if (mpegtsInstance) {
console.log('[MPEG-TS Loader] Using cached mpegts.js instance')
return mpegtsInstance
}
// Return existing loading promise if already loading
if (loadingPromise) {
console.log('[MPEG-TS Loader] Already loading, waiting for existing promise...')
return loadingPromise
}
// Start loading
loadingPromise = (async () => {
try {
console.log('[MPEG-TS Loader] Attempting to load from npm package...')
const module = await import('mpegts.js')
mpegtsInstance = module.default || module
console.log('[MPEG-TS Loader] Successfully loaded from npm package')
return mpegtsInstance
} catch (error) {
console.error('[MPEG-TS Loader] Failed to load mpegts.js:', error)
throw new Error('Failed to load mpegts.js. Make sure it is installed: npm install mpegts.js')
} finally {
loadingPromise = null
}
})()
return loadingPromise
}
/**
* Check if mpegts.js is supported in the current browser
* @param mpegts - The mpegts.js module
* @returns True if supported
*/
export const isMpegtsSupported = (mpegts: any): boolean => {
return mpegts && mpegts.isSupported()
}
/**
* Create default configuration for mpegts.js player
* @param isLive - Whether the stream is live
* @returns Configuration object
*/
export const createDefaultMpegtsConfig = (isLive: boolean = false): MpegtsConfig => {
return {
enableWorker: false, // Disabled by default due to cross-origin issues
enableStashBuffer: true,
stashInitialSize: isLive ? 128 : 384,
isLive: isLive,
lazyLoad: !isLive,
lazyLoadMaxDuration: 3 * 60,
lazyLoadRecoverDuration: 30,
deferLoadAfterSourceOpen: false,
autoCleanupSourceBuffer: true,
autoCleanupMaxBackwardDuration: 180,
autoCleanupMinBackwardDuration: 120,
fixAudioTimestampGap: true,
}
}
/**
* Get the cached mpegts.js instance
* @returns The mpegts.js module or null
*/
export const getMpegtsInstance = (): any | null => {
return mpegtsInstance
}
/**
* Clear the cached mpegts.js instance
*/
export const clearMpegtsCache = (): void => {
mpegtsInstance = null
loadingPromise = null
console.log('[MPEG-TS Loader] Cache cleared')
}
+227
View File
@@ -0,0 +1,227 @@
/**
* MPEG-TS player setup utility
* Initializes and configures mpegts.js player for MPEG-TS/IPTV streams
*/
import { loadMpegts, isMpegtsSupported, createDefaultMpegtsConfig } from './mpegtsLoader'
import { isLiveStream } from './videoProtocol'
export interface MpegtsSetupOptions {
video: HTMLVideoElement
src: string
autoplay?: boolean
onError?: (error: Error) => void
onLoadedMetadata?: () => void
}
/**
* Sets up mpegts.js player instance for MPEG-TS streaming
* @param options - Setup options
* @returns Cleanup function to destroy the player
*/
export const setupMpegtsInstance = async ({
video,
src,
autoplay = false,
onError,
onLoadedMetadata,
}: MpegtsSetupOptions): Promise<() => void> => {
try {
// Load mpegts.js library
const mpegts = await loadMpegts()
// Check if mpegts.js is supported
if (!isMpegtsSupported(mpegts)) {
const error = new Error(
'mpegts.js is not supported in this browser. Media Source Extensions (MSE) is required.'
)
if (onError) {
onError(error)
}
throw error
}
console.log('[MPEG-TS Setup] Creating player instance for:', src)
// Detect if stream is live
const isLive = isLiveStream(src)
// Create mpegts.js player configuration
const config = createDefaultMpegtsConfig(isLive)
// Create player instance
const player = mpegts.createPlayer(
{
type: 'mpegts',
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).__mpegtsInstance = player
// Event handlers
player.on(mpegts.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => {
console.error('mpegts.js error:', { errorType, errorDetail, errorInfo })
const error = new Error(`MPEG-TS Player Error: ${errorType} - ${errorDetail}`)
// Handle specific error types
if (errorType === mpegts.ErrorTypes.NETWORK_ERROR) {
console.error('Network error occurred:', errorDetail)
// Attempt recovery for recoverable network errors
if (
errorDetail === mpegts.ErrorDetails.NETWORK_EXCEPTION ||
errorDetail === mpegts.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 === mpegts.ErrorTypes.MEDIA_ERROR) {
console.error('Media error occurred:', errorDetail)
// Some media errors are recoverable
if (errorDetail === mpegts.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(mpegts.Events.LOADING_COMPLETE, () => {
console.log('mpegts.js: Loading complete')
})
player.on(mpegts.Events.RECOVERED_EARLY_EOF, () => {
console.log('mpegts.js: Recovered from early EOF')
})
player.on(mpegts.Events.METADATA_ARRIVED, (metadata: any) => {
console.log('mpegts.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback
if (onLoadedMetadata) {
onLoadedMetadata()
}
})
player.on(mpegts.Events.STATISTICS_INFO, (stats: any) => {
// Statistics info for debugging/monitoring
if (stats) {
;(video as any).__mpegtsStats = 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 mpegts.js player...')
// Remove event listeners
player.off(mpegts.Events.ERROR)
player.off(mpegts.Events.LOADING_COMPLETE)
player.off(mpegts.Events.RECOVERED_EARLY_EOF)
player.off(mpegts.Events.METADATA_ARRIVED)
player.off(mpegts.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).__mpegtsInstance
delete (video as any).__mpegtsStats
} catch (cleanupError) {
console.error('Error during mpegts.js cleanup:', cleanupError)
}
}
} catch (error) {
console.error('Failed to setup mpegts.js player:', error)
const setupError =
error instanceof Error ? error : new Error('Failed to setup MPEG-TS player')
if (onError) {
onError(setupError)
}
throw setupError
}
}
/**
* Gets the current mpegts.js player instance from a video element
* @param video - The video element
* @returns The mpegts.js player instance or null
*/
export const getMpegtsInstance = (video: HTMLVideoElement | null): any | null => {
if (!video) {
return null
}
return (video as any).__mpegtsInstance || null
}
/**
* Gets the current mpegts.js statistics from a video element
* @param video - The video element
* @returns The statistics object or null
*/
export const getMpegtsStats = (video: HTMLVideoElement | null): any | null => {
if (!video) {
return null
}
return (video as any).__mpegtsStats || null
}
/**
* Checks if a video element has an active MPEG-TS player instance
* @param video - The video element
* @returns True if has active instance
*/
export const hasMpegtsInstance = (video: HTMLVideoElement | null): boolean => {
return getMpegtsInstance(video) !== null
}
+5 -5
View File
@@ -5,16 +5,16 @@ describe('videoProtocol', () => {
describe('detectVideoProtocol', () => {
it('should detect MPEG-TS IPTV streams', () => {
const result = detectVideoProtocol('http://favoritv65.xyz:8080/live/Apollon45/HpjWrDa6gWWd/98925.ts')
expect(result.protocol).toBe('native')
expect(result.protocol).toBe('mpegts')
expect(result.isLive).toBe(true)
expect(result.needsSpecialPlayer).toBe(false)
expect(result.needsSpecialPlayer).toBe(true)
})
it('should detect .ts files with query parameters', () => {
const result = detectVideoProtocol('http://example.com/stream/video.ts?token=abc123')
expect(result.protocol).toBe('native')
expect(result.protocol).toBe('mpegts')
expect(result.isLive).toBe(true)
expect(result.needsSpecialPlayer).toBe(false)
expect(result.needsSpecialPlayer).toBe(true)
})
it('should detect HLS streams', () => {
@@ -75,7 +75,7 @@ describe('videoProtocol', () => {
expect(isHlsStream('http://example.com/stream.m3u8')).toBe(true)
})
it('should return false for IPTV .ts streams (they use native playback)', () => {
it('should return false for IPTV .ts streams (they use mpegts.js)', () => {
expect(isHlsStream('http://favoritv65.xyz:8080/live/user/pass/98925.ts')).toBe(false)
})
+4 -4
View File
@@ -3,7 +3,7 @@
* Detects the streaming protocol from a video URL
*/
export type VideoProtocol = 'native' | 'hls' | 'rtmp' | 'dash'
export type VideoProtocol = 'native' | 'hls' | 'rtmp' | 'dash' | 'mpegts'
export interface ProtocolDetectionResult {
protocol: VideoProtocol
@@ -74,12 +74,12 @@ export const detectVideoProtocol = (src: string): ProtocolDetectionResult => {
// MPEG-TS (IPTV) detection
// Check for .ts extension (Transport Stream used in IPTV)
// Note: These are direct TS streams, not HLS playlists
// Try native playback first as modern browsers support MPEG-TS
// Browsers don't support MPEG-TS natively, so we use mpegts.js for playback
if (lowerSrc.includes('.ts') || lowerSrc.match(/\.ts(\?|$)/)) {
return {
protocol: 'native', // Try native playback first
protocol: 'mpegts', // Use mpegts.js for MPEG-TS streaming
isLive: true, // IPTV streams are typically live
needsSpecialPlayer: false, // Modern browsers support MPEG-TS
needsSpecialPlayer: true, // Requires mpegts.js for MPEG-TS playback
}
}
+1
View File
@@ -24,6 +24,7 @@ export default defineConfig({
alias: {
'flv.js': new URL('./src/test/mocks/flv.mock.ts', import.meta.url).pathname,
'hls.js': new URL('./src/test/mocks/hls.mock.ts', import.meta.url).pathname,
'mpegts.js': new URL('./src/test/mocks/mpegts.mock.ts', import.meta.url).pathname,
},
},
});