@jxstjh/jhvideo
Version:
HTML5 jhvideo base on MPEG2-TS Stream Player
925 lines (805 loc) • 34.1 kB
JavaScript
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;