UNPKG

@jxstjh/jhvideo

Version:

HTML5 jhvideo base on MPEG2-TS Stream Player

925 lines (805 loc) 34.1 kB
import EventEmitter from 'events'; import PlayerEvents from './player-events.js'; import { ErrorTypes, ErrorDetails } from './player-errors.js'; import { createDefaultConfig } from '../config.js'; import { InvalidArgumentException, IllegalStateException } from '../utils/exception.js'; import Hls from 'hls.js'; import { Player as WasmPlayer, EventInfo } from './wasm_player.js'; import { parseHevcInitSegment, parseHevcMediaSegment } from '../utils/fmp4-hevc.js'; function resolveUrl(url, baseUrl) { return new URL(url, baseUrl).toString(); } function parsePlaylist(text, baseUrl) { const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean); const targetDurationLine = lines.find((line) => line.startsWith('#EXT-X-TARGETDURATION:')); const mediaSequenceLine = lines.find((line) => line.startsWith('#EXT-X-MEDIA-SEQUENCE:')); const mapLine = lines.find((line) => line.startsWith('#EXT-X-MAP:')); const variants = []; const segments = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith('#EXT-X-STREAM-INF:')) { const nextLine = lines[i + 1]; if (nextLine && !nextLine.startsWith('#')) { variants.push(resolveUrl(nextLine, baseUrl)); } } if (line.startsWith('#EXTINF:')) { const nextLine = lines[i + 1]; if (nextLine && !nextLine.startsWith('#')) { segments.push(nextLine); } } } let mapUri = null; if (mapLine) { const matched = mapLine.match(/URI="([^"]+)"/); if (matched) { mapUri = resolveUrl(matched[1], baseUrl); } } const mediaSequence = mediaSequenceLine ? parseInt(mediaSequenceLine.split(':')[1], 10) : 0; const targetDuration = targetDurationLine ? parseFloat(targetDurationLine.split(':')[1]) : 1; return { isMaster: variants.length > 0, variants, targetDuration, mediaSequence, mapUri, segments: segments.map((segment, index) => ({ sequence: mediaSequence + index, url: resolveUrl(segment, baseUrl) })) }; } function summarizeHevcUnits(units) { return units.map((unit) => unit && unit.byteLength ? ((unit[0] >> 1) & 0x3f) : -1); } function cloneUnitWithStartCode(unit) { const startCode = new Uint8Array([0, 0, 0, 1]); const packet = new Uint8Array(startCode.byteLength + unit.byteLength); packet.set(startCode, 0); packet.set(unit, startCode.byteLength); return packet; } function concatUint8Arrays(arrays) { const totalLength = arrays.reduce((sum, item) => sum + item.byteLength, 0); const result = new Uint8Array(totalLength); let offset = 0; arrays.forEach((item) => { result.set(item, offset); offset += item.byteLength; }); return result; } class HlsPlayer { constructor(mediaDataSource, config) { this.TAG = 'HlsPlayer'; this._type = 'HlsPlayer'; this._emitter = new EventEmitter(); this._config = createDefaultConfig(); if (typeof config === 'object') { Object.assign(this._config, config); } let typeLowerCase = mediaDataSource.type.toLowerCase(); if (typeLowerCase !== 'hls') { throw new InvalidArgumentException('HlsPlayer requires an hls MediaDataSource input!'); } this.e = { onvLoadedMetadata: this._onvLoadedMetadata.bind(this), onHlsManifestParsed: this._onHlsManifestParsed.bind(this), onHlsError: this._onHlsError.bind(this), onHlsFragLoaded: this._onHlsFragLoaded.bind(this), onvCanPlay: this._onvCanPlay.bind(this), onvPlaying: this._onvPlaying.bind(this), onvError: this._onvError.bind(this) }; this._pendingSeekTime = null; this._statisticsReporter = null; this._networkDead = false; this._mediaDataSource = mediaDataSource; this._mediaElement = null; this._canvasElement = null; this._hls = null; this._wasmPlayer = null; this._useWasmPlayback = false; this._firstWasmFrameRendered = false; this._wasmPlaylistTimer = null; this._playlistPromise = Promise.resolve(); this._processedSegments = new Set(); this._playlistUrl = mediaDataSource.url; this._initSegmentInfo = null; this._bootstrapAbortController = null; this._fatalWasmError = false; this._wasmPollGeneration = 0; this._hevcBootstrap = null; this._pendingHevcFallback = false; this._standardPlaybackConfirmed = false; this._destroyed = false; this._loadGeneration = 0; } destroy() { this._destroyed = true; this._loadGeneration += 1; if (this._mediaElement) { this.unload(); this.detachMediaElement(); } this.e = null; this._mediaDataSource = null; this._emitter.removeAllListeners(); this._emitter = null; } on(event, listener) { if (event === PlayerEvents.MEDIA_INFO) { if (this._mediaElement != null && this._mediaElement.readyState !== 0) { // HAVE_NOTHING Promise.resolve().then(() => { this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo); }); } } else if (event === PlayerEvents.STATISTICS_INFO) { if (this._mediaElement != null && this._mediaElement.readyState !== 0) { Promise.resolve().then(() => { this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo); }); } } this._emitter.addListener(event, listener); } off(event, listener) { this._emitter.removeListener(event, listener); } attachMediaElement(mediaElement, canvasElement) { this._mediaElement = mediaElement; this._canvasElement = canvasElement; mediaElement.addEventListener('loadedmetadata', this.e.onvLoadedMetadata); mediaElement.addEventListener('canplay', this.e.onvCanPlay); mediaElement.addEventListener('playing', this.e.onvPlaying); mediaElement.addEventListener('error', this.e.onvError); if (this._pendingSeekTime != null) { try { mediaElement.currentTime = this._pendingSeekTime; this._pendingSeekTime = null; } catch (e) { // IE11 may throw InvalidStateError if readyState === 0 // Defer set currentTime operation after loadedmetadata } } } detachMediaElement() { this._loadGeneration += 1; this._stopWasmPlaylistTimer(); const listeners = this.e; if (this._bootstrapAbortController) { this._bootstrapAbortController.abort(); this._bootstrapAbortController = null; } if (this._hls && listeners) { this._hls.off(Hls.Events.MANIFEST_PARSED, listeners.onHlsManifestParsed); this._hls.off(Hls.Events.FRAG_LOADED, listeners.onHlsFragLoaded); this._hls.off(Hls.Events.ERROR, listeners.onHlsError); } if (this._hls) { this._hls.detachMedia(); this._hls.destroy(); this._hls = null; } if (this._wasmPlayer) { this._wasmPlayer.destroy(); this._wasmPlayer = null; } if (this._mediaElement) { this._mediaElement.src = ''; this._mediaElement.removeAttribute('src'); if (listeners) { this._mediaElement.removeEventListener('loadedmetadata', listeners.onvLoadedMetadata); this._mediaElement.removeEventListener('canplay', listeners.onvCanPlay); this._mediaElement.removeEventListener('playing', listeners.onvPlaying); this._mediaElement.removeEventListener('error', listeners.onvError); } this._mediaElement = null; } this._canvasElement = null; this._useWasmPlayback = false; this._firstWasmFrameRendered = false; this._processedSegments.clear(); this._initSegmentInfo = null; this._fatalWasmError = false; this._wasmPollGeneration += 1; this._hevcBootstrap = null; this._pendingHevcFallback = false; this._standardPlaybackConfirmed = false; if (this._statisticsReporter != null) { window.clearInterval(this._statisticsReporter); this._statisticsReporter = null; } } load() { if (!this._mediaElement) { throw new IllegalStateException('HTMLMediaElement must be attached before load()!'); } this._loadGeneration += 1; this._networkDead = false; this._statisticsReporter = window.setInterval( this._reportStatisticsInfo.bind(this), this._config.statisticsInfoReportInterval ); this._bootstrapLoadMode(this._loadGeneration); } unload() { this._loadGeneration += 1; this._networkDead = false; this._stopWasmPlaylistTimer(); if (this._bootstrapAbortController) { this._bootstrapAbortController.abort(); this._bootstrapAbortController = null; } if (this._wasmPlayer) { this._wasmPlayer.destroy(); this._wasmPlayer = null; } if (this._hls) { this._hls.stopLoad(); } if (this._mediaElement) { this._mediaElement.src = ''; this._mediaElement.removeAttribute('src'); } this._useWasmPlayback = false; this._firstWasmFrameRendered = false; this._processedSegments.clear(); this._initSegmentInfo = null; this._fatalWasmError = false; this._wasmPollGeneration += 1; this._hevcBootstrap = null; this._pendingHevcFallback = false; this._standardPlaybackConfirmed = false; if (this._statisticsReporter != null) { window.clearInterval(this._statisticsReporter); this._statisticsReporter = null; } } play() { if (this._wasmPlayer) { return this._wasmPlayer.play(); } return this._mediaElement.play(); } pause() { if (this._wasmPlayer) { this._wasmPlayer.pause(); return; } this._mediaElement.pause(); } get isNetworkDead() { return this._networkDead === true; } get type() { return this._type; } get buffered() { if (this._wasmPlayer) { return this._mediaElement ? this._mediaElement.buffered : null; } return this._mediaElement.buffered; } get duration() { if (this._wasmPlayer) { return this._wasmPlayer.duration; } return this._mediaElement.duration; } get volume() { return this._mediaElement.volume; } set volume(value) { this._mediaElement.volume = value; } get muted() { return this._mediaElement.muted; } set muted(muted) { this._mediaElement.muted = muted; } get currentTime() { if (this._wasmPlayer) { return this._wasmPlayer.currentTime || 0; } if (this._mediaElement) { return this._mediaElement.currentTime; } return 0; } set currentTime(seconds) { if (this._wasmPlayer && typeof this._wasmPlayer.setDidirectPlayTime === 'function') { this._wasmPlayer.setDidirectPlayTime(seconds); } else if (this._mediaElement) { this._mediaElement.currentTime = seconds; } else { this._pendingSeekTime = seconds; } } get mediaInfo() { if (this._wasmPlayer) { return Object.assign({ mimeType: 'video/mp4; codecs="hvc1"' }, this._wasmPlayer.mediaInfo || {}); } let mediaPrefix = (this._mediaElement instanceof HTMLAudioElement) ? 'audio/' : 'video/'; let info = { mimeType: mediaPrefix + 'mp2t' // HLS usually relies on mpeg-ts }; if (this._mediaElement) { info.duration = Math.floor(this._mediaElement.duration * 1000); if (this._mediaElement instanceof HTMLVideoElement) { info.width = this._mediaElement.videoWidth; info.height = this._mediaElement.videoHeight; } } return info; } get statisticsInfo() { if (this._wasmPlayer) { return { playerType: this._type, url: this._mediaDataSource.url, decodedFrames: this._wasmPlayer.decodedFrames }; } let info = { playerType: this._type, url: this._mediaDataSource.url }; if (!(this._mediaElement instanceof HTMLVideoElement)) { return info; } let hasQualityInfo = true; let decoded = 0; let dropped = 0; if (this._mediaElement.getVideoPlaybackQuality) { let quality = this._mediaElement.getVideoPlaybackQuality(); decoded = quality.totalVideoFrames; dropped = quality.droppedVideoFrames; } else if (this._mediaElement.webkitDecodedFrameCount != undefined) { decoded = this._mediaElement.webkitDecodedFrameCount; dropped = this._mediaElement.webkitDroppedFrameCount; } else { hasQualityInfo = false; } if (hasQualityInfo) { info.decodedFrames = decoded; info.droppedFrames = dropped; } return info; } _onvLoadedMetadata(e) { if (this._wasmPlayer) { return; } this._networkDead = false; this._standardPlaybackConfirmed = true; if (this._pendingSeekTime != null) { this._mediaElement.currentTime = this._pendingSeekTime; this._pendingSeekTime = null; } this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo); } _onHlsManifestParsed(event, data) { this._networkDead = false; // Trigger LOADING_COMPLETE or custom logic if needed. // HLS auto-plays if not prevented, loadedmetadata handles MEDIA_INFO. } _onHlsFragLoaded() { this._networkDead = false; } _onHlsError(event, data) { if (this._shouldFallbackToWasm(data)) { this._fallbackToWasmPlayback(data); return; } if (data.fatal) { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: this._networkDead = true; /*console.log('11 - 触发了网络错误!'); console.error('具体的网络错误类型:', data.details); console.error('错误发生的 URL:', data.url); console.error('完整的错误数据:', data);*/ // try to recover network error console.warn('Fatal network error encountered, try to recover'); this._hls.startLoad(); this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.NETWORK_ERROR, ErrorDetails.NETWORK_EXCEPTION, data); break; case Hls.ErrorTypes.MEDIA_ERROR: this._networkDead = false; console.warn('Fatal media error encountered, try to recover'); this._hls.recoverMediaError(); this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.MEDIA_ERROR, ErrorDetails.MEDIA_FORMAT_ERROR, data); break; default: this._networkDead = false; // cannot recover this.destroy(); this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.OTHER_ERROR, ErrorDetails.MEDIA_MSE_ERROR, data); break; } } } _onvCanPlay() { this._networkDead = false; this._standardPlaybackConfirmed = true; } _onvPlaying() { this._networkDead = false; this._standardPlaybackConfirmed = true; } _onvError() { // Native media errors are not necessarily network disconnects. this._networkDead = false; if (this._shouldFallbackToWasm()) { this._fallbackToWasmPlayback(this._mediaElement && this._mediaElement.error ? this._mediaElement.error : new Error('Native HEVC playback failed before first frame')); } } _reportStatisticsInfo() { this._emitter.emit(PlayerEvents.STATISTICS_INFO, this.statisticsInfo); } _isDisposed() { return this._destroyed || !this.e || !this._emitter; } _isStaleLoad(loadGeneration) { return this._isDisposed() || loadGeneration !== this._loadGeneration || !this._mediaElement || !this._mediaDataSource; } _bootstrapLoadMode(loadGeneration) { let abortController = null; this._playlistPromise = this._playlistPromise.then(async () => { if (this._isStaleLoad(loadGeneration)) { return; } abortController = new AbortController(); this._bootstrapAbortController = abortController; this._fatalWasmError = false; this._wasmPollGeneration += 1; try { const bootstrap = await this._fetchBootstrapInfo(this._mediaDataSource.url, abortController.signal); if (abortController.signal.aborted || this._isStaleLoad(loadGeneration)) { return; } if (bootstrap && bootstrap.initInfo && bootstrap.initInfo.codecId === 12) { // HEVC(H265) 先尝试走浏览器/HLS.js 直播放。 // 只有当前环境明确不支持,或者直播放在首帧前失败,才回退到 Wasm 解码。 if (this._shouldPreferStandardHevcPlayback(bootstrap)) { console.log('[HLS HEVC] direct playback selected', { codecType: bootstrap.initInfo.codecType, playlistUrl: bootstrap.playlistUrl, preferMse: this._canUseMseHevcPlayback(bootstrap.initInfo), preferNative: this._canUseNativeHevcPlayback() }); this._hevcBootstrap = bootstrap; this._pendingHevcFallback = true; this._standardPlaybackConfirmed = false; this._startStandardHlsPlayback(loadGeneration); } else { console.log('[HLS HEVC] wasm playback selected', { codecType: bootstrap.initInfo.codecType, playlistUrl: bootstrap.playlistUrl }); this._startWasmPlayback(bootstrap); } return; } } catch (error) { if (error.name !== 'AbortError') { console.warn('HLS HEVC bootstrap failed, fallback to default HLS playback.', error); } } if (abortController.signal.aborted || this._isStaleLoad(loadGeneration)) { return; } this._hevcBootstrap = null; this._pendingHevcFallback = false; this._standardPlaybackConfirmed = false; this._startStandardHlsPlayback(loadGeneration); }).finally(() => { if (this._bootstrapAbortController === abortController) { this._bootstrapAbortController = null; } }); } async _fetchBootstrapInfo(url, signal) { const response = await fetch(url, { signal }); const text = await response.text(); let playlist = parsePlaylist(text, url); if (playlist.isMaster) { if (!playlist.variants.length) { return null; } // 当前只取第一路 variant,后续如果要做自适应码率,再从这里扩展。 return this._fetchBootstrapInfo(playlist.variants[0], signal); } if (!playlist.mapUri) { return null; } const initResponse = await fetch(playlist.mapUri, { signal }); const initSegment = await initResponse.arrayBuffer(); const initInfo = parseHevcInitSegment(initSegment); return { playlistUrl: url, playlist, initInfo }; } _shouldPreferStandardHevcPlayback(bootstrap) { if (!bootstrap || !bootstrap.initInfo || bootstrap.initInfo.codecId !== 12) { return false; } return this._canUseMseHevcPlayback(bootstrap.initInfo) || this._canUseNativeHevcPlayback(); } _canUseMseHevcPlayback(initInfo) { const mediaSource = window.MediaSource || window.ManagedMediaSource; if (!mediaSource || typeof mediaSource.isTypeSupported !== 'function') { return false; } // 先尝试 init 段里实际解析出的 sample entry,再用常见 HEVC codec string 兜底。 const codecType = initInfo && initInfo.codecType ? initInfo.codecType : 'hvc1'; const codecCandidates = [ `video/mp4; codecs="${codecType}"`, 'video/mp4; codecs="hvc1.1.6.L93.B0"', 'video/mp4; codecs="hev1.1.6.L93.B0"' ]; return codecCandidates.some((mimeType) => { try { return mediaSource.isTypeSupported(mimeType); } catch (error) { return false; } }); } _canUseNativeHevcPlayback() { return !!(this._mediaElement && this._mediaElement.canPlayType && this._mediaElement.canPlayType('application/vnd.apple.mpegurl')); } _shouldFallbackToWasm(data) { // 只有“已选择 HEVC 直播放,但首帧还没确认成功”的窗口期里,才允许自动回退。 return !!( this._pendingHevcFallback && this._hevcBootstrap && !this._standardPlaybackConfirmed && (!data || data.fatal && data.type === Hls.ErrorTypes.MEDIA_ERROR) ); } _fallbackToWasmPlayback(reason) { if (!this._pendingHevcFallback || !this._hevcBootstrap) { return; } const bootstrap = this._hevcBootstrap; const listeners = this.e; this._pendingHevcFallback = false; this._standardPlaybackConfirmed = false; if (this._hls && listeners) { this._hls.off(Hls.Events.MANIFEST_PARSED, listeners.onHlsManifestParsed); this._hls.off(Hls.Events.FRAG_LOADED, listeners.onHlsFragLoaded); this._hls.off(Hls.Events.ERROR, listeners.onHlsError); } if (this._hls) { this._hls.destroy(); this._hls = null; } if (this._mediaElement) { this._mediaElement.pause(); this._mediaElement.removeAttribute('src'); this._mediaElement.load(); } console.warn('HLS HEVC direct playback failed, fallback to wasm playback.', reason); console.log('[HLS HEVC] direct playback failed, fallback to wasm', { codecType: bootstrap.initInfo ? bootstrap.initInfo.codecType : null, playlistUrl: bootstrap.playlistUrl }); this._startWasmPlayback(bootstrap); } _startStandardHlsPlayback(loadGeneration = this._loadGeneration) { if (this._isStaleLoad(loadGeneration)) { return; } this._useWasmPlayback = false; this._type = 'HlsPlayer'; const listeners = this.e; if (!listeners) { return; } if (Hls.isSupported()) { let hlsConfig = this._config.hlsConfig || {}; this._hls = new Hls(hlsConfig); this._hls.on(Hls.Events.MANIFEST_PARSED, listeners.onHlsManifestParsed); this._hls.on(Hls.Events.FRAG_LOADED, listeners.onHlsFragLoaded); this._hls.on(Hls.Events.ERROR, listeners.onHlsError); this._hls.attachMedia(this._mediaElement); this._hls.loadSource(this._mediaDataSource.url); } else if (this._mediaElement.canPlayType('application/vnd.apple.mpegurl')) { this._mediaElement.src = this._mediaDataSource.url; this._mediaElement.load(); } else { throw new IllegalStateException('HLS is not supported in this browser!'); } if (this._mediaElement.readyState > 0) { this._mediaElement.currentTime = 0; } } _startWasmPlayback(bootstrap) { this._useWasmPlayback = true; this._type = 'HlsWasmPlayer'; this._playlistUrl = bootstrap.playlistUrl; this._initSegmentInfo = bootstrap.initInfo; this._firstWasmFrameRendered = false; this._processedSegments.clear(); this._fatalWasmError = false; this._wasmPollGeneration += 1; const pollGeneration = this._wasmPollGeneration; console.log('[HLS HEVC] init info', { codecType: bootstrap.initInfo.codecType, codecId: bootstrap.initInfo.codecId, trackId: bootstrap.initInfo.trackId, timescale: bootstrap.initInfo.timescale, duration: bootstrap.initInfo.duration, width: bootstrap.initInfo.width, height: bootstrap.initInfo.height, lengthSize: bootstrap.initInfo.lengthSize, parameterSets: bootstrap.initInfo.parameterSets ? bootstrap.initInfo.parameterSets.length : 0 }); if (this._hls) { this._hls.destroy(); this._hls = null; } if (this._mediaElement) { this._mediaElement.pause(); this._mediaElement.removeAttribute('src'); this._mediaElement.load(); } if (this._wasmPlayer) { this._wasmPlayer.destroy(); } if (this._canvasElement && bootstrap.initInfo.width && bootstrap.initInfo.height) { this._canvasElement.width = bootstrap.initInfo.width; this._canvasElement.height = bootstrap.initInfo.height; } const duration = bootstrap.initInfo.duration ? Math.floor((bootstrap.initInfo.duration / bootstrap.initInfo.timescale) * 1000) : 0; this._wasmPlayer = new WasmPlayer(this._canvasElement, this._config.isLive, bootstrap.initInfo.codecId, duration, this._config.workerPath); this._wasmPlayer.on(EventInfo.ERROR, (error) => { this._fatalWasmError = true; this._stopWasmPlaylistTimer(); this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.MEDIA_ERROR, ErrorDetails.MEDIA_FORMAT_ERROR, error); }); this._wasmPlayer.on(EventInfo.FIRST_CANPLAY, () => { const mediaInfo = this._wasmPlayer.mediaInfo || {}; if (this._canvasElement && mediaInfo.width && mediaInfo.height) { this._canvasElement.width = mediaInfo.width; this._canvasElement.height = mediaInfo.height; } this._networkDead = false; this._emitter.emit(PlayerEvents.MEDIA_INFO, this.mediaInfo); if (!this._firstWasmFrameRendered) { this._firstWasmFrameRendered = true; this._emitter.emit('onVCanPlay'); } }); this._wasmPlayer.initDecodeWorker(); this._wasmPlayer.play(); this._consumePlaylist(bootstrap.playlist).catch((error) => { this._networkDead = true; this._fatalWasmError = true; this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.NETWORK_ERROR, ErrorDetails.NETWORK_EXCEPTION, error); }).finally(() => { this._scheduleNextPlaylistPoll(bootstrap.playlist.targetDuration, pollGeneration); }); } _scheduleNextPlaylistPoll(targetDuration, pollGeneration = this._wasmPollGeneration) { if (!this._useWasmPlayback || this._fatalWasmError) { return; } this._stopWasmPlaylistTimer(); const delay = Math.max(1000, Math.floor((targetDuration || 1) * 500)); this._wasmPlaylistTimer = window.setTimeout(() => { if (pollGeneration !== this._wasmPollGeneration) { return; } this._refreshWasmPlaylist(pollGeneration); }, delay); } _stopWasmPlaylistTimer() { if (this._wasmPlaylistTimer != null) { window.clearTimeout(this._wasmPlaylistTimer); this._wasmPlaylistTimer = null; } } async _refreshWasmPlaylist(pollGeneration = this._wasmPollGeneration) { if (!this._useWasmPlayback || this._fatalWasmError || pollGeneration !== this._wasmPollGeneration) { return; } try { const response = await fetch(this._playlistUrl); const text = await response.text(); const playlist = parsePlaylist(text, this._playlistUrl); await this._consumePlaylist(playlist); this._networkDead = false; this._scheduleNextPlaylistPoll(playlist.targetDuration, pollGeneration); } catch (error) { this._networkDead = true; this._fatalWasmError = true; this._stopWasmPlaylistTimer(); console.error('[HLS HEVC] playlist refresh failed', error); this._emitter.emit(PlayerEvents.ERROR, ErrorTypes.NETWORK_ERROR, ErrorDetails.NETWORK_EXCEPTION, error); } } async _consumePlaylist(playlist) { for (const segment of playlist.segments) { if (this._processedSegments.has(segment.sequence)) { continue; } const response = await fetch(segment.url); const arrayBuffer = await response.arrayBuffer(); let samples; try { samples = parseHevcMediaSegment(arrayBuffer, this._initSegmentInfo); } catch (error) { /*console.error('[HLS HEVC] segment parse failed', { url: segment.url, sequence: segment.sequence, error });*/ throw error; } /*console.log('[HLS HEVC] segment parsed', { url: segment.url, sequence: segment.sequence, sampleCount: samples.length });*/ for (let sampleIndex = 0; sampleIndex < samples.length; sampleIndex++) { const sample = samples[sampleIndex]; const accessUnitPackets = []; // 一个 sample 对应一个完整访问单元。 // 这里统一拼成 Annex B 后一次性送进解码器,避免把同一帧拆成多次 decode 请求造成时序抖动。 if (sample.keyframe && this._initSegmentInfo.parameterSets && this._initSegmentInfo.parameterSets.length) { this._initSegmentInfo.parameterSets.forEach((parameterSet) => { if (parameterSet && parameterSet.byteLength) { accessUnitPackets.push(cloneUnitWithStartCode(parameterSet)); } }); } sample.units.forEach((unit) => { if (unit && unit.byteLength) { accessUnitPackets.push(cloneUnitWithStartCode(unit)); } }); if (!accessUnitPackets.length) { continue; } const accessUnitData = concatUint8Arrays(accessUnitPackets); if (sampleIndex < 3) { /*console.log('[HLS HEVC] sample info', { sequence: segment.sequence, sampleIndex, ptsMs: sample.ptsMs, frameType: sample.frameType, keyframe: sample.keyframe, unitCount: sample.units.length, unitTypes: summarizeHevcUnits(sample.units), inputUnitCount: accessUnitPackets.length, inputBytes: accessUnitData.byteLength });*/ } this._wasmPlayer.inputData('video', { codecId: this._initSegmentInfo.codecId, pts: sample.ptsMs, frameType: sample.frameType, data: accessUnitData }); } this._processedSegments.add(segment.sequence); } } } export default HlsPlayer;