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:
@@ -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
@@ -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"
|
||||
],
|
||||
|
||||
Generated
-3583
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// Mock for mpegts.js library used in tests
|
||||
export default {
|
||||
isSupported: () => true,
|
||||
getFeatureList: () => ({
|
||||
mseSupported: true,
|
||||
networkStreamIOSupported: true,
|
||||
httpsSupported: true,
|
||||
}),
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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,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)
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user