Some fixes

This commit is contained in:
hibna
2026-02-12 17:54:16 +03:00
parent f57ee77c56
commit 8a32c5c1b3
18 changed files with 997 additions and 135 deletions
+126
View File
@@ -0,0 +1,126 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { setupHlsInstance } from './hlsSetup'
class MockHlsInstance {
public static Events = {
MANIFEST_PARSED: 'manifestParsed',
LEVEL_LOADED: 'levelLoaded',
AUDIO_TRACKS_UPDATED: 'audioTracksUpdated',
SUBTITLE_TRACKS_UPDATED: 'subtitleTracksUpdated',
ERROR: 'error',
}
public static ErrorTypes = {
NETWORK_ERROR: 'networkError',
MEDIA_ERROR: 'mediaError',
OTHER_ERROR: 'otherError',
}
public loadSource = vi.fn()
public attachMedia = vi.fn()
public destroy = vi.fn()
public startLoad = vi.fn()
public recoverMediaError = vi.fn()
private handlers = new Map<string, Array<(...args: any[]) => void>>()
on(event: string, handler: (...args: any[]) => void) {
const existing = this.handlers.get(event) || []
existing.push(handler)
this.handlers.set(event, existing)
}
emit(event: string, ...args: any[]) {
for (const handler of this.handlers.get(event) || []) {
handler(...args)
}
}
}
const loadHls = vi.fn(async () => MockHlsInstance)
const isHlsSupported = vi.fn(() => true)
const getHlsAudioTracks = vi.fn(() => [{ name: 'English', language: 'en', groupId: 'audio', url: '' }])
const getHlsQualities = vi.fn(() => [{ label: '720p', height: 720, levelIndex: 1 }])
const getHlsSubtitleTracks = vi.fn(() => [{ label: 'English', lang: 'en', src: '/sub.vtt' }])
vi.mock('./hlsLoader', () => ({
loadHls,
isHlsSupported,
getHlsAudioTracks,
getHlsQualities,
getHlsSubtitleTracks,
}))
describe('setupHlsInstance', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
vi.clearAllMocks()
})
it('sets up hls, emits levels/tracks and cleans up', async () => {
const video = document.createElement('video')
const onAudioTracksLoaded = vi.fn()
const onQualityLevelsLoaded = vi.fn()
const onSubtitleTracksLoaded = vi.fn()
const cleanup = await setupHlsInstance({
video,
src: 'https://example.com/stream.m3u8',
autoplay: false,
onAudioTracksLoaded,
onQualityLevelsLoaded,
onSubtitleTracksLoaded,
})
const hls = (video as any).__hlsInstance as MockHlsInstance
expect(hls).toBeDefined()
expect(hls.loadSource).toHaveBeenCalledWith('https://example.com/stream.m3u8')
expect(hls.attachMedia).toHaveBeenCalledWith(video)
hls.emit(MockHlsInstance.Events.MANIFEST_PARSED)
expect(onAudioTracksLoaded).toHaveBeenCalled()
expect(onQualityLevelsLoaded).toHaveBeenCalled()
expect(onSubtitleTracksLoaded).toHaveBeenCalled()
vi.advanceTimersByTime(250)
expect(onQualityLevelsLoaded).toHaveBeenCalledTimes(2)
cleanup()
expect(hls.destroy).toHaveBeenCalledTimes(1)
expect((video as any).__hlsInstance).toBeUndefined()
})
it('attempts recovery on fatal network/media errors', async () => {
const video = document.createElement('video')
const onError = vi.fn()
await setupHlsInstance({
video,
src: 'https://example.com/stream.m3u8',
autoplay: false,
onError,
})
const hls = (video as any).__hlsInstance as MockHlsInstance
hls.emit(MockHlsInstance.Events.ERROR, null, {
fatal: true,
type: MockHlsInstance.ErrorTypes.NETWORK_ERROR,
})
hls.emit(MockHlsInstance.Events.ERROR, null, {
fatal: true,
type: MockHlsInstance.ErrorTypes.MEDIA_ERROR,
})
hls.emit(MockHlsInstance.Events.ERROR, null, {
fatal: true,
type: MockHlsInstance.ErrorTypes.OTHER_ERROR,
})
expect(hls.startLoad).toHaveBeenCalledTimes(1)
expect(hls.recoverMediaError).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(expect.any(Error))
})
})
+92
View File
@@ -0,0 +1,92 @@
import { describe, expect, it, vi } from 'vitest'
import { setupMpegtsInstance } from './mpegtsSetup'
const createMockPlayer = () => {
const handlers = new Map<string, (...args: any[]) => void>()
return {
attachMediaElement: vi.fn(),
load: vi.fn(),
unload: vi.fn(),
detachMediaElement: vi.fn(),
destroy: vi.fn(),
on: vi.fn((event: string, handler: (...args: any[]) => void) => {
handlers.set(event, handler)
}),
off: vi.fn(),
emit: (event: string, ...args: any[]) => {
handlers.get(event)?.(...args)
},
}
}
const mockPlayer = createMockPlayer()
const mockMpegts = {
Events: {
ERROR: 'error',
LOADING_COMPLETE: 'loadingComplete',
RECOVERED_EARLY_EOF: 'recoveredEOF',
METADATA_ARRIVED: 'metadataArrived',
STATISTICS_INFO: 'stats',
},
ErrorTypes: {
NETWORK_ERROR: 'networkError',
MEDIA_ERROR: 'mediaError',
},
ErrorDetails: {
NETWORK_EXCEPTION: 'networkException',
NETWORK_STATUS_CODE_INVALID: 'networkStatusCodeInvalid',
MEDIA_MSE_ERROR: 'mediaMSEError',
},
createPlayer: vi.fn(() => mockPlayer),
}
vi.mock('./mpegtsLoader', () => ({
loadMpegts: vi.fn(async () => mockMpegts),
isMpegtsSupported: vi.fn(() => true),
createDefaultMpegtsConfig: vi.fn(() => ({ enableWorker: false })),
}))
describe('setupMpegtsInstance', () => {
it('sets up and cleans mpegts player instance', async () => {
const video = document.createElement('video')
const cleanup = await setupMpegtsInstance({
video,
src: 'http://example.com/live/stream.ts',
autoplay: false,
})
expect(mockMpegts.createPlayer).toHaveBeenCalled()
expect(mockPlayer.attachMediaElement).toHaveBeenCalledWith(video)
expect(mockPlayer.load).toHaveBeenCalled()
expect((video as any).__mpegtsInstance).toBeDefined()
cleanup()
expect(mockPlayer.unload).toHaveBeenCalled()
expect(mockPlayer.detachMediaElement).toHaveBeenCalled()
expect(mockPlayer.destroy).toHaveBeenCalled()
expect((video as any).__mpegtsInstance).toBeUndefined()
})
it('triggers metadata callback and non-recoverable error callback', async () => {
const video = document.createElement('video')
const onLoadedMetadata = vi.fn()
const onError = vi.fn()
await setupMpegtsInstance({
video,
src: 'http://example.com/live/stream.ts',
autoplay: false,
onLoadedMetadata,
onError,
})
mockPlayer.emit(mockMpegts.Events.METADATA_ARRIVED, { duration: 123 })
mockPlayer.emit(mockMpegts.Events.ERROR, 'networkError', 'fatalError', {})
expect(onLoadedMetadata).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(expect.any(Error))
})
})
+92
View File
@@ -0,0 +1,92 @@
import { describe, expect, it, vi } from 'vitest'
import { setupRtmpInstance } from './rtmpSetup'
const createMockPlayer = () => {
const handlers = new Map<string, (...args: any[]) => void>()
return {
attachMediaElement: vi.fn(),
load: vi.fn(),
unload: vi.fn(),
detachMediaElement: vi.fn(),
destroy: vi.fn(),
on: vi.fn((event: string, handler: (...args: any[]) => void) => {
handlers.set(event, handler)
}),
off: vi.fn(),
emit: (event: string, ...args: any[]) => {
handlers.get(event)?.(...args)
},
}
}
const mockPlayer = createMockPlayer()
const mockFlvjs = {
Events: {
ERROR: 'error',
LOADING_COMPLETE: 'loadingComplete',
RECOVERED_EARLY_EOF: 'recoveredEOF',
METADATA_ARRIVED: 'metadataArrived',
STATISTICS_INFO: 'stats',
},
ErrorTypes: {
NETWORK_ERROR: 'networkError',
MEDIA_ERROR: 'mediaError',
},
ErrorDetails: {
NETWORK_EXCEPTION: 'networkException',
NETWORK_STATUS_CODE_INVALID: 'networkStatusCodeInvalid',
MEDIA_MSE_ERROR: 'mediaMSEError',
},
createPlayer: vi.fn(() => mockPlayer),
}
vi.mock('./rtmpLoader', () => ({
loadFlvjs: vi.fn(async () => mockFlvjs),
isFlvjsSupported: vi.fn(() => true),
createDefaultFlvConfig: vi.fn(() => ({ enableWorker: true })),
}))
describe('setupRtmpInstance', () => {
it('sets up and cleans flv player instance', async () => {
const video = document.createElement('video')
const cleanup = await setupRtmpInstance({
video,
src: 'http://example.com/live.flv',
autoplay: false,
})
expect(mockFlvjs.createPlayer).toHaveBeenCalled()
expect(mockPlayer.attachMediaElement).toHaveBeenCalledWith(video)
expect(mockPlayer.load).toHaveBeenCalled()
expect((video as any).__rtmpInstance).toBeDefined()
cleanup()
expect(mockPlayer.unload).toHaveBeenCalled()
expect(mockPlayer.detachMediaElement).toHaveBeenCalled()
expect(mockPlayer.destroy).toHaveBeenCalled()
expect((video as any).__rtmpInstance).toBeUndefined()
})
it('triggers metadata callback and non-recoverable error callback', async () => {
const video = document.createElement('video')
const onLoadedMetadata = vi.fn()
const onError = vi.fn()
await setupRtmpInstance({
video,
src: 'http://example.com/live.flv',
autoplay: false,
onLoadedMetadata,
onError,
})
mockPlayer.emit(mockFlvjs.Events.METADATA_ARRIVED, { duration: 123 })
mockPlayer.emit(mockFlvjs.Events.ERROR, 'networkError', 'fatalError', {})
expect(onLoadedMetadata).toHaveBeenCalledTimes(1)
expect(onError).toHaveBeenCalledWith(expect.any(Error))
})
})