release: v3.0.0

This commit is contained in:
hibna
2026-02-13 04:59:21 +03:00
parent 1f1b7d7de3
commit 9ab429b5c0
50 changed files with 6015 additions and 436 deletions
+22 -4
View File
@@ -3,11 +3,26 @@
* Separated to avoid circular dependencies and enable better tree-shaking
*/
const hasQualityControls = (
hls: HlsInstance | PlayerInstance | null | undefined
): hls is HlsInstance => {
return Boolean(hls && 'levels' in hls && Array.isArray(hls.levels) && 'currentLevel' in hls)
}
const hasAudioControls = (
hls: HlsInstance | PlayerInstance | null | undefined
): hls is HlsInstance => {
return Boolean(hls && 'audioTracks' in hls && Array.isArray(hls.audioTracks) && 'audioTrack' in hls)
}
/**
* Update active quality level in HLS instance. Passing null re-enables auto.
*/
export const setHlsQualityLevel = (hls: any, levelIndex: number | null | undefined): void => {
if (!hls || !Array.isArray(hls.levels)) {
export const setHlsQualityLevel = (
hls: HlsInstance | PlayerInstance | null | undefined,
levelIndex: number | null | undefined
): void => {
if (!hasQualityControls(hls)) {
return
}
@@ -30,8 +45,11 @@ export const setHlsQualityLevel = (hls: any, levelIndex: number | null | undefin
/**
* Set active audio track in HLS instance
*/
export const setHlsAudioTrack = (hls: any, audioTrackIndex: number): void => {
if (!hls || !hls.audioTracks) {
export const setHlsAudioTrack = (
hls: HlsInstance | PlayerInstance | null | undefined,
audioTrackIndex: number
): void => {
if (!hasAudioControls(hls)) {
return
}
+15 -17
View File
@@ -10,16 +10,16 @@ import { logger } from './logger'
// Re-export control functions for backward compatibility
export { setHlsQualityLevel, setHlsAudioTrack } from './hlsControl'
const HLS_CDN_URL = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.13/dist/hls.min.js'
const HLS_CDN_URL = 'https://cdn.jsdelivr.net/npm/hls.js@1.6.13/dist/hls.min.js'
/**
* Load hls.js from CDN as fallback
*/
const loadHlsFromCDN = (): Promise<any> => {
const loadHlsFromCDN = (): Promise<HlsConstructor> => {
return new Promise((resolve, reject) => {
// Check if already loaded globally
if (typeof (window as any).Hls !== 'undefined') {
resolve((window as any).Hls)
if (window.Hls) {
resolve(window.Hls)
return
}
@@ -28,8 +28,8 @@ const loadHlsFromCDN = (): Promise<any> => {
script.async = true
script.onload = () => {
if (typeof (window as any).Hls !== 'undefined') {
resolve((window as any).Hls)
if (window.Hls) {
resolve(window.Hls)
} else {
reject(new Error('HLS.js CDN loaded but Hls global not found'))
}
@@ -46,7 +46,7 @@ const loadHlsFromCDN = (): Promise<any> => {
/**
* Load hls.js with npm fallback to CDN
*/
export const loadHls = async (): Promise<any> => {
export const loadHls = async (): Promise<HlsConstructor> => {
try {
logger.log('[HLS Loader] Attempting to load from npm package...')
// Try loading from npm package first
@@ -70,7 +70,7 @@ export const loadHls = async (): Promise<any> => {
/**
* Check if HLS.js is supported in current browser
*/
export const isHlsSupported = (Hls: any): boolean => {
export const isHlsSupported = (Hls: HlsConstructor): boolean => {
return Hls && typeof Hls.isSupported === 'function' && Hls.isSupported()
}
@@ -85,7 +85,7 @@ export const hasNativeHlsSupport = (): boolean => {
/**
* Extract audio tracks from HLS instance
*/
export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
export const getHlsAudioTracks = (hls: HlsInstance): AudioTrack[] => {
try {
if (!hls) {
logger.warn('[HLS Loader] getHlsAudioTracks: No HLS instance provided')
@@ -100,7 +100,7 @@ export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
logger.log('[HLS Loader] getHlsAudioTracks: Raw audioTracks from HLS:', hls.audioTracks)
const audioTracks: AudioTrack[] = hls.audioTracks.map((track: any, index: number) => {
const audioTracks: AudioTrack[] = hls.audioTracks.map((track: HlsAudioTrack, index: number) => {
const audioTrack = {
name: track.name || track.label || `Audio ${index + 1}`,
language: track.lang || track.language || 'unknown',
@@ -123,7 +123,7 @@ export const getHlsAudioTracks = (hls: any): AudioTrack[] => {
/**
* Extract subtitle tracks from HLS instance
*/
export const getHlsSubtitleTracks = (hls: any): SubtitleTrack[] => {
export const getHlsSubtitleTracks = (hls: HlsInstance): SubtitleTrack[] => {
try {
if (!hls) {
return []
@@ -134,7 +134,7 @@ export const getHlsSubtitleTracks = (hls: any): SubtitleTrack[] => {
return []
}
const subtitleTracks: SubtitleTrack[] = hls.subtitleTracks.map((track: any, index: number) => {
const subtitleTracks: SubtitleTrack[] = hls.subtitleTracks.map((track: HlsSubtitleTrack, index: number) => {
return {
label: track.name || track.label || `Subtitle ${index + 1}`,
lang: track.lang || track.language || 'unknown',
@@ -152,7 +152,7 @@ export const getHlsSubtitleTracks = (hls: any): SubtitleTrack[] => {
/**
* Extract available quality levels from HLS instance
*/
export const getHlsQualities = (hls: any): VideoQuality[] => {
export const getHlsQualities = (hls: HlsInstance): VideoQuality[] => {
try {
if (!hls) {
logger.warn('[HLS Loader] getHlsQualities: No HLS instance provided')
@@ -166,7 +166,7 @@ export const getHlsQualities = (hls: any): VideoQuality[] => {
logger.log('[HLS Loader] getHlsQualities: Raw levels from HLS:', hls.levels)
const qualities: VideoQuality[] = hls.levels.map((level: any, index: number) => {
const qualities: VideoQuality[] = hls.levels.map((level: HlsLevel, index: number) => {
const resolution = typeof level.attrs?.RESOLUTION === 'string' ? level.attrs.RESOLUTION : undefined
const [widthFromResolution, heightFromResolution] = resolution
? resolution.split('x').map((value: string) => parseInt(value, 10))
@@ -175,7 +175,7 @@ export const getHlsQualities = (hls: any): VideoQuality[] => {
const translations = getTranslations(detectBrowserLanguage());
const width = level.width || widthFromResolution
const height = level.height || heightFromResolution
const bitrate = typeof level.bitrate === 'number' ? level.bitrate : level.attrs?.BANDWIDTH
const bitrate = typeof level.bitrate === 'number' ? level.bitrate : (level.attrs?.BANDWIDTH ? parseInt(level.attrs.BANDWIDTH, 10) : undefined)
let label: string
if (typeof level.name === 'string' && level.name.trim().length > 0) {
@@ -213,5 +213,3 @@ export const getHlsQualities = (hls: any): VideoQuality[] => {
return []
}
}
+3 -3
View File
@@ -115,7 +115,7 @@ export const setupHlsInstance = async ({
}
})
hls.on(Hls.Events.ERROR, (_event: any, data: any) => {
hls.on(Hls.Events.ERROR, (_event: unknown, data: { fatal: boolean; type: string }) => {
if (data.fatal) {
switch (data.type) {
case Hls.ErrorTypes.NETWORK_ERROR:
@@ -131,13 +131,13 @@ export const setupHlsInstance = async ({
}
})
;(video as any).__hlsInstance = hls
video.__hlsInstance = hls as PlayerInstance
return () => {
if (hls) {
hls.destroy()
}
delete (video as any).__hlsInstance
delete video.__hlsInstance
}
}
+53 -11
View File
@@ -1,9 +1,11 @@
/**
* MPEG-TS loader utility
* Dynamically loads mpegts.js library
* Dynamically loads mpegts.js library with CDN fallback
*/
import { logger } from './logger'
const MPEGTS_CDN_URL = 'https://cdn.jsdelivr.net/npm/mpegts.js@1.7.3/dist/mpegts.js'
export interface MpegtsConfig {
enableWorker?: boolean
enableStashBuffer?: boolean
@@ -20,14 +22,45 @@ export interface MpegtsConfig {
headers?: Record<string, string>
}
let mpegtsInstance: any = null
let loadingPromise: Promise<any> | null = null
let mpegtsInstance: MpegtsStatic | null = null
let loadingPromise: Promise<MpegtsStatic> | null = null
/**
* Load mpegts.js from CDN as fallback
*/
const loadMpegtsFromCDN = (): Promise<MpegtsStatic> => {
return new Promise((resolve, reject) => {
if (window.mpegts) {
resolve(window.mpegts)
return
}
const script = document.createElement('script')
script.src = MPEGTS_CDN_URL
script.async = true
script.onload = () => {
if (window.mpegts) {
resolve(window.mpegts)
} else {
reject(new Error('mpegts.js loaded but not available on window'))
}
}
script.onerror = () => {
reject(new Error(`Failed to load mpegts.js from CDN: ${MPEGTS_CDN_URL}`))
}
document.head.appendChild(script)
})
}
/**
* Load mpegts.js library dynamically
* Tries NPM package first, falls back to CDN if unavailable
* @returns Promise that resolves to mpegts.js module
*/
export const loadMpegts = async (): Promise<any> => {
export const loadMpegts = async (): Promise<MpegtsStatic> => {
// Return cached instance if available
if (mpegtsInstance) {
logger.log('[MPEG-TS Loader] Using cached mpegts.js instance')
@@ -47,10 +80,20 @@ export const loadMpegts = async (): Promise<any> => {
const module = await import('mpegts.js')
mpegtsInstance = module.default || module
logger.log('[MPEG-TS Loader] Successfully loaded from npm package')
return mpegtsInstance
} catch (error) {
logger.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')
return mpegtsInstance!
} catch (npmError) {
logger.warn('[MPEG-TS Loader] npm package not available, loading from CDN...', npmError)
try {
mpegtsInstance = await loadMpegtsFromCDN()
logger.log('[MPEG-TS Loader] Successfully loaded from CDN')
return mpegtsInstance!
} catch (cdnError) {
logger.error('[MPEG-TS Loader] Failed to load from both npm and CDN', cdnError)
throw new Error(
'Failed to load mpegts.js library. Please ensure mpegts.js is available or check your network connection.'
)
}
} finally {
loadingPromise = null
}
@@ -64,7 +107,7 @@ export const loadMpegts = async (): Promise<any> => {
* @param mpegts - The mpegts.js module
* @returns True if supported
*/
export const isMpegtsSupported = (mpegts: any): boolean => {
export const isMpegtsSupported = (mpegts: MpegtsStatic): boolean => {
return mpegts && mpegts.isSupported()
}
@@ -94,7 +137,7 @@ export const createDefaultMpegtsConfig = (isLive: boolean = false): MpegtsConfig
* Get the cached mpegts.js instance
* @returns The mpegts.js module or null
*/
export const getMpegtsInstance = (): any | null => {
export const getMpegtsInstance = (): MpegtsStatic | null => {
return mpegtsInstance
}
@@ -106,4 +149,3 @@ export const clearMpegtsCache = (): void => {
loadingPromise = null
logger.log('[MPEG-TS Loader] Cache cleared')
}
+11 -12
View File
@@ -69,10 +69,10 @@ export const setupMpegtsInstance = async ({
player.load()
// Store player instance on video element for later access
;(video as any).__mpegtsInstance = player
video.__mpegtsInstance = player
// Event handlers
player.on(mpegts.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => {
player.on(mpegts.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: unknown) => {
logger.error('mpegts.js error:', { errorType, errorDetail, errorInfo })
const error = new Error(`MPEG-TS Player Error: ${errorType} - ${errorDetail}`)
@@ -125,7 +125,7 @@ export const setupMpegtsInstance = async ({
logger.log('mpegts.js: Recovered from early EOF')
})
player.on(mpegts.Events.METADATA_ARRIVED, (metadata: any) => {
player.on(mpegts.Events.METADATA_ARRIVED, (metadata: Record<string, unknown>) => {
logger.log('mpegts.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback
@@ -134,10 +134,10 @@ export const setupMpegtsInstance = async ({
}
})
player.on(mpegts.Events.STATISTICS_INFO, (stats: any) => {
player.on(mpegts.Events.STATISTICS_INFO, (stats: Record<string, unknown>) => {
// Statistics info for debugging/monitoring
if (stats) {
;(video as any).__mpegtsStats = stats
video.__mpegtsStats = stats
}
})
@@ -174,8 +174,8 @@ export const setupMpegtsInstance = async ({
player.destroy()
// Clean up stored references
delete (video as any).__mpegtsInstance
delete (video as any).__mpegtsStats
delete video.__mpegtsInstance
delete video.__mpegtsStats
} catch (cleanupError) {
logger.error('Error during mpegts.js cleanup:', cleanupError)
}
@@ -199,11 +199,11 @@ export const setupMpegtsInstance = async ({
* @param video - The video element
* @returns The mpegts.js player instance or null
*/
export const getMpegtsInstance = (video: HTMLVideoElement | null): any | null => {
export const getMpegtsInstance = (video: HTMLVideoElement | null): MpegtsPlayer | PlayerInstance | null => {
if (!video) {
return null
}
return (video as any).__mpegtsInstance || null
return video.__mpegtsInstance ?? null
}
/**
@@ -211,11 +211,11 @@ export const getMpegtsInstance = (video: HTMLVideoElement | null): any | null =>
* @param video - The video element
* @returns The statistics object or null
*/
export const getMpegtsStats = (video: HTMLVideoElement | null): any | null => {
export const getMpegtsStats = (video: HTMLVideoElement | null): Record<string, unknown> | null => {
if (!video) {
return null
}
return (video as any).__mpegtsStats || null
return video.__mpegtsStats ?? null
}
/**
@@ -226,4 +226,3 @@ export const getMpegtsStats = (video: HTMLVideoElement | null): any | null => {
export const hasMpegtsInstance = (video: HTMLVideoElement | null): boolean => {
return getMpegtsInstance(video) !== null
}
+4 -16
View File
@@ -9,21 +9,15 @@
*/
export const setupFullscreenPolyfill = () => {
if (!document.exitFullscreen) {
// @ts-ignore - Legacy API
document.exitFullscreen = document.webkitExitFullscreen ||
// @ts-ignore
document.exitFullscreen = (document.webkitExitFullscreen ||
document.mozCancelFullScreen ||
// @ts-ignore
document.msExitFullscreen
document.msExitFullscreen) as typeof document.exitFullscreen
}
if (!Element.prototype.requestFullscreen) {
// @ts-ignore - Legacy API
Element.prototype.requestFullscreen = Element.prototype.webkitRequestFullscreen ||
// @ts-ignore
Element.prototype.requestFullscreen = (Element.prototype.webkitRequestFullscreen ||
Element.prototype.mozRequestFullScreen ||
// @ts-ignore
Element.prototype.msRequestFullscreen
Element.prototype.msRequestFullscreen) as typeof Element.prototype.requestFullscreen
}
// Fullscreen change event polyfill
@@ -41,11 +35,8 @@ export const setupFullscreenPolyfill = () => {
if (!Object.prototype.hasOwnProperty.call(document, 'fullscreenElement')) {
Object.defineProperty(document, 'fullscreenElement', {
get: function() {
// @ts-ignore
return this.webkitFullscreenElement ||
// @ts-ignore
this.mozFullScreenElement ||
// @ts-ignore
this.msFullscreenElement
}
})
@@ -157,11 +148,8 @@ export const features = {
if (!isBrowser) return false
return !!(
document.fullscreenEnabled ||
// @ts-ignore
document.webkitFullscreenEnabled ||
// @ts-ignore
document.mozFullScreenEnabled ||
// @ts-ignore
document.msFullscreenEnabled
)
},
+16 -17
View File
@@ -11,10 +11,10 @@ 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> => {
const loadFlvjsFromCDN = async (): Promise<FlvjsStatic> => {
return new Promise((resolve, reject) => {
if ((window as any).flvjs) {
resolve((window as any).flvjs)
if (window.flvjs) {
resolve(window.flvjs)
return
}
@@ -23,8 +23,8 @@ const loadFlvjsFromCDN = async (): Promise<any> => {
script.async = true
script.onload = () => {
if ((window as any).flvjs) {
resolve((window as any).flvjs)
if (window.flvjs) {
resolve(window.flvjs)
} else {
reject(new Error('flv.js loaded but not available on window'))
}
@@ -43,7 +43,7 @@ const loadFlvjsFromCDN = async (): Promise<any> => {
* 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> => {
export const loadFlvjs = async (): Promise<FlvjsStatic> => {
try {
// Try loading from NPM package first
const flvModule = await import('flv.js')
@@ -69,7 +69,7 @@ export const loadFlvjs = async (): Promise<any> => {
* @param flvjs - The flv.js library instance
* @returns True if supported
*/
export const isFlvjsSupported = (flvjs: any): boolean => {
export const isFlvjsSupported = (flvjs: FlvjsStatic): boolean => {
if (!flvjs) {
return false
}
@@ -83,7 +83,7 @@ export const isFlvjsSupported = (flvjs: any): boolean => {
* @param flvjs - The flv.js library instance
* @returns Support information object
*/
export const getFlvjsSupportInfo = (flvjs: any): {
export const getFlvjsSupportInfo = (flvjs: FlvjsStatic): {
mseSupported: boolean
networkStreamIOSupported: boolean
httpsSupported: boolean
@@ -138,7 +138,7 @@ export const createDefaultFlvConfig = (isLive: boolean = true) => {
* @param player - The flv.js player instance
* @returns Basic quality information
*/
export const extractFlvQualityInfo = (player: any): {
export const extractFlvQualityInfo = (player: FlvjsPlayer): {
width?: number
height?: number
videoCodec?: string
@@ -154,17 +154,16 @@ export const extractFlvQualityInfo = (player: any): {
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,
width: stats.videoWidth as number | undefined,
height: stats.videoHeight as number | undefined,
videoCodec: stats.videoCodec as string | undefined,
audioCodec: stats.audioCodec as string | undefined,
fps: stats.fps as number | undefined,
videoBitrate: stats.videoBitrate as number | undefined,
audioBitrate: stats.audioBitrate as number | undefined,
}
} catch (error) {
logger.warn('Failed to extract flv.js quality info:', error)
return null
}
}
+11 -12
View File
@@ -81,10 +81,10 @@ export const setupRtmpInstance = async ({
player.load()
// Store player instance on video element for later access
;(video as any).__rtmpInstance = player
video.__rtmpInstance = player
// Event handlers
player.on(flvjs.Events.ERROR, (errorType: string, errorDetail: string, errorInfo: any) => {
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}`)
@@ -137,7 +137,7 @@ export const setupRtmpInstance = async ({
logger.log('flv.js: Recovered from early EOF')
})
player.on(flvjs.Events.METADATA_ARRIVED, (metadata: any) => {
player.on(flvjs.Events.METADATA_ARRIVED, (metadata: Record<string, unknown>) => {
logger.log('flv.js: Metadata arrived', metadata)
// Trigger onLoadedMetadata callback
@@ -146,11 +146,11 @@ export const setupRtmpInstance = async ({
}
})
player.on(flvjs.Events.STATISTICS_INFO, (stats: any) => {
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 as any).__rtmpStats = stats
video.__rtmpStats = stats
}
})
@@ -187,8 +187,8 @@ export const setupRtmpInstance = async ({
player.destroy()
// Clean up stored references
delete (video as any).__rtmpInstance
delete (video as any).__rtmpStats
delete video.__rtmpInstance
delete video.__rtmpStats
} catch (cleanupError) {
logger.error('Error during flv.js cleanup:', cleanupError)
}
@@ -212,11 +212,11 @@ export const setupRtmpInstance = async ({
* @param video - The video element
* @returns The flv.js player instance or null
*/
export const getRtmpInstance = (video: HTMLVideoElement | null): any | null => {
export const getRtmpInstance = (video: HTMLVideoElement | null): FlvjsPlayer | PlayerInstance | null => {
if (!video) {
return null
}
return (video as any).__rtmpInstance || null
return video.__rtmpInstance ?? null
}
/**
@@ -224,11 +224,11 @@ export const getRtmpInstance = (video: HTMLVideoElement | null): any | null => {
* @param video - The video element
* @returns The statistics object or null
*/
export const getRtmpStats = (video: HTMLVideoElement | null): any | null => {
export const getRtmpStats = (video: HTMLVideoElement | null): Record<string, unknown> | null => {
if (!video) {
return null
}
return (video as any).__rtmpStats || null
return video.__rtmpStats ?? null
}
/**
@@ -239,4 +239,3 @@ export const getRtmpStats = (video: HTMLVideoElement | null): any | null => {
export const hasRtmpInstance = (video: HTMLVideoElement | null): boolean => {
return getRtmpInstance(video) !== null
}