From 7fd0fe3c1194969456264b8d9618a5bd2050f92b Mon Sep 17 00:00:00 2001 From: Rex Feng Date: Fri, 31 May 2024 10:53:24 -0700 Subject: [PATCH 1/4] when `this.stats` is called, add optional chaining or check for existence (#6459) (cherry picked from commit f379442b014e9fea7543070b9c5221fd3edbbf2b) --- src/utils/xhr-loader.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/xhr-loader.ts b/src/utils/xhr-loader.ts index 7160a23013a..d6155cc622a 100644 --- a/src/utils/xhr-loader.ts +++ b/src/utils/xhr-loader.ts @@ -40,8 +40,6 @@ class XhrLoader implements Loader { this.config = null; this.context = null; this.xhrSetup = null; - // @ts-ignore - this.stats = null; } abortInternal() { @@ -100,15 +98,16 @@ class XhrLoader implements Loader { if (xhrSetup) { Promise.resolve() .then(() => { - if (this.stats.aborted) return; + if (this.loader !== xhr || this.stats.aborted) return; return xhrSetup(xhr, context.url); }) .catch((error: Error) => { + if (this.loader !== xhr || this.stats.aborted) return; xhr.open('GET', context.url, true); return xhrSetup(xhr, context.url); }) .then(() => { - if (this.stats.aborted) return; + if (this.loader !== xhr || this.stats.aborted) return; this.openAndSendXhr(xhr, context, config); }) .catch((error: Error) => { @@ -263,7 +262,8 @@ class XhrLoader implements Loader { } loadtimeout() { - const retryConfig = this.config?.loadPolicy.timeoutRetry; + if (!this.config) return; + const retryConfig = this.config.loadPolicy.timeoutRetry; const retryCount = this.stats.retry; if (shouldRetry(retryConfig, retryCount, true)) { this.retry(retryConfig); From adf87aac73d13d7e89909740727a7dda51993bd4 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 31 May 2024 14:54:26 -0700 Subject: [PATCH 2/4] Fix TSDemuxer parsing error handling in sync path (#6469) * Add type safety to worker message handler and replace deprecated vendor field with empty string Related to #6445 (cherry picked from commit 1e4e605f3af970c97ce2f13d6e2ea18bceb277b3) * Fix TSDemuxer parsing error handling Fixes #6445 * Warn with error message when available --- src/controller/base-stream-controller.ts | 2 +- src/demux/transmuxer-interface.ts | 42 ++++++++++++-------- src/demux/transmuxer-worker.ts | 2 +- src/demux/tsdemuxer.ts | 49 ++++++++++++++---------- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index fec01ccabd2..fec1a33ffa2 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -397,7 +397,7 @@ export default class BaseStreamController if (this.state === State.STOPPED || this.state === State.ERROR) { return; } - this.warn(reason); + this.warn(`Frag error: ${reason?.message || reason}`); this.resetFragmentLoading(frag); }); } diff --git a/src/demux/transmuxer-interface.ts b/src/demux/transmuxer-interface.ts index 52df5527635..72db05fe4bc 100644 --- a/src/demux/transmuxer-interface.ts +++ b/src/demux/transmuxer-interface.ts @@ -17,7 +17,7 @@ import { EventEmitter } from 'eventemitter3'; import { Fragment, Part } from '../loader/fragment'; import type { ChunkMetadata, TransmuxerResult } from '../types/transmuxer'; import type Hls from '../hls'; -import type { HlsEventEmitter } from '../events'; +import type { HlsEventEmitter, HlsListeners } from '../events'; import type { PlaylistLevelType } from '../types/loader'; import type { TypeSupported } from './tsdemuxer'; import type { RationalTimestamp } from '../utils/timescale-conversion'; @@ -31,7 +31,9 @@ export default class TransmuxerInterface { private part: Part | null = null; private useWorker: boolean; private workerContext: WorkerContext | null = null; - private onwmsg?: Function; + private onwmsg?: ( + event: MessageEvent<{ event: string; data?: any } | null>, + ) => void; private transmuxer: Transmuxer | null = null; private onTransmuxComplete: (transmuxResult: TransmuxerResult) => void; private onFlush: (chunkMeta: ChunkMetadata) => void; @@ -75,9 +77,6 @@ export default class TransmuxerInterface { : false, }; - // navigator.vendor is not always available in Web Worker - // refer to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/navigator - const vendor = navigator.vendor; if (this.useWorker && typeof Worker !== 'undefined') { const canCreateWorker = config.workerPath || hasUMDWorker(); if (canCreateWorker) { @@ -89,9 +88,9 @@ export default class TransmuxerInterface { logger.log(`injecting Web Worker for "${id}"`); this.workerContext = injectWorker(); } - this.onwmsg = (ev: any) => this.onWorkerMessage(ev); + this.onwmsg = (event) => this.onWorkerMessage(event); const { worker } = this.workerContext; - worker.addEventListener('message', this.onwmsg as any); + worker.addEventListener('message', this.onwmsg); worker.onerror = (event) => { const error = new Error( `${event.message} (${event.filename}:${event.lineno})`, @@ -109,7 +108,7 @@ export default class TransmuxerInterface { worker.postMessage({ cmd: 'init', typeSupported: m2tsTypeSupported, - vendor: vendor, + vendor: '', id: id, config: JSON.stringify(config), }); @@ -124,7 +123,7 @@ export default class TransmuxerInterface { this.observer, m2tsTypeSupported, config, - vendor, + '', id, ); } @@ -136,12 +135,12 @@ export default class TransmuxerInterface { this.observer, m2tsTypeSupported, config, - vendor, + '', id, ); } - resetWorker(): void { + resetWorker() { if (this.workerContext) { const { worker, objectURL } = this.workerContext; if (objectURL) { @@ -155,7 +154,7 @@ export default class TransmuxerInterface { } } - destroy(): void { + destroy() { if (this.workerContext) { this.resetWorker(); this.onwmsg = undefined; @@ -188,7 +187,7 @@ export default class TransmuxerInterface { accurateTimeOffset: boolean, chunkMeta: ChunkMetadata, defaultInitPTS?: RationalTimestamp, - ): void { + ) { chunkMeta.transmuxing.start = self.performance.now(); const { transmuxer } = this; const timeOffset = part ? part.start : frag.start; @@ -355,9 +354,20 @@ export default class TransmuxerInterface { this.onFlush(chunkMeta); } - private onWorkerMessage(ev: any): void { - const data = ev.data; + private onWorkerMessage( + event: MessageEvent<{ event: string; data?: any } | null>, + ) { + const data = event.data; + if (!data?.event) { + logger.warn( + `worker message received with no ${data ? 'event name' : 'data'}`, + ); + return; + } const hls = this.hls; + if (!this.hls) { + return; + } switch (data.event) { case 'init': { const objectURL = this.workerContext?.objectURL; @@ -389,7 +399,7 @@ export default class TransmuxerInterface { data.data = data.data || {}; data.data.frag = this.frag; data.data.id = this.id; - hls.trigger(data.event, data.data); + hls.trigger(data.event as keyof HlsListeners, data.data); break; } } diff --git a/src/demux/transmuxer-worker.ts b/src/demux/transmuxer-worker.ts index f150c48395b..b10f69f46c9 100644 --- a/src/demux/transmuxer-worker.ts +++ b/src/demux/transmuxer-worker.ts @@ -43,7 +43,7 @@ function startWorker(self) { observer, data.typeSupported, config, - data.vendor, + '', data.id, ); enableLogs(config.debug, data.id); diff --git a/src/demux/tsdemuxer.ts b/src/demux/tsdemuxer.ts index 02dc71afd82..7a973407494 100644 --- a/src/demux/tsdemuxer.ts +++ b/src/demux/tsdemuxer.ts @@ -363,6 +363,7 @@ class TSDemuxer implements Demuxer { offset, this.typeSupported, isSampleAes, + this.observer, ); // only update track id if track PID found while parsing PMT @@ -411,16 +412,12 @@ class TSDemuxer implements Demuxer { } if (tsPacketErrors > 0) { - const error = new Error( - `Found ${tsPacketErrors} TS packet/s that do not start with 0x47`, + emitParsingError( + this.observer, + new Error( + `Found ${tsPacketErrors} TS packet/s that do not start with 0x47`, + ), ); - this.observer.emit(Events.ERROR, Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_PARSING_ERROR, - fatal: false, - error, - reason: error.message, - }); } videoTrack.pesData = videoData; @@ -603,16 +600,7 @@ class TSDemuxer implements Demuxer { } else { reason = 'No ADTS header found in AAC PES'; } - const error = new Error(reason); - logger.warn(`parsing error: ${reason}`); - this.observer.emit(Events.ERROR, Events.ERROR, { - type: ErrorTypes.MEDIA_ERROR, - details: ErrorDetails.FRAG_PARSING_ERROR, - fatal: false, - levelRetry: recoverable, - error, - reason, - }); + emitParsingError(this.observer, new Error(reason), recoverable); if (!recoverable) { return; } @@ -743,6 +731,7 @@ function parsePMT( offset: number, typeSupported: TypeSupported, isSampleAes: boolean, + observer: HlsEventEmitter, ) { const result = { audioPid: -1, @@ -872,10 +861,12 @@ function parsePMT( case 0xc2: // SAMPLE-AES EC3 /* falls through */ case 0x87: - throw new Error('Unsupported EC-3 in M2TS found'); + emitParsingError(observer, new Error('Unsupported EC-3 in M2TS found')); + return result; case 0x24: - throw new Error('Unsupported HEVC in M2TS found'); + emitParsingError(observer, new Error('Unsupported HEVC in M2TS found')); + return result; default: // logger.log('unknown stream type:' + data[offset]); @@ -888,6 +879,22 @@ function parsePMT( return result; } +function emitParsingError( + observer: HlsEventEmitter, + error: Error, + levelRetry?: boolean, +) { + logger.warn(`parsing error: ${error.message}`); + observer.emit(Events.ERROR, Events.ERROR, { + type: ErrorTypes.MEDIA_ERROR, + details: ErrorDetails.FRAG_PARSING_ERROR, + fatal: false, + levelRetry, + error, + reason: error.message, + }); +} + function logEncryptedSamplesFoundInUnencryptedStream(type: string) { logger.log(`${type} with AES-128-CBC encryption found in unencrypted stream`); } From 0729c3445fa3e7ea383971e13873fde77595e527 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 31 May 2024 17:18:55 -0700 Subject: [PATCH 3/4] Fix exception reading metadata.channelCount with HE-AAC when changeType is not supported (#6472) Fixes #6470 --- src/controller/stream-controller.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/controller/stream-controller.ts b/src/controller/stream-controller.ts index b1a2a1fadce..acbf5a1442c 100644 --- a/src/controller/stream-controller.ts +++ b/src/controller/stream-controller.ts @@ -1272,7 +1272,13 @@ export default class StreamController // In the case that AAC and HE-AAC audio codecs are signalled in manifest, // force HE-AAC, as it seems that most browsers prefers it. // don't force HE-AAC if mono stream, or in Firefox - if (audio.metadata.channelCount !== 1 && ua.indexOf('firefox') === -1) { + const audioMetadata = audio.metadata; + if ( + audioMetadata && + 'channelCount' in audioMetadata && + (audioMetadata.channelCount || 1) !== 1 && + ua.indexOf('firefox') === -1 + ) { audioCodec = 'mp4a.40.5'; } } From e1c2904da78e76c882b529f9ae9444b4fa0bb4e8 Mon Sep 17 00:00:00 2001 From: Rob Walch Date: Fri, 31 May 2024 17:31:26 -0700 Subject: [PATCH 4/4] Don't append over first fragment when next fragment aligns with playlist within 1/200s tolerance (#6471) Fixes edge-case starting in v1.5 that causes #6441 --- src/controller/base-stream-controller.ts | 5 ++-- src/controller/fragment-finders.ts | 37 ++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/controller/base-stream-controller.ts b/src/controller/base-stream-controller.ts index fec1a33ffa2..6a3674d9874 100644 --- a/src/controller/base-stream-controller.ts +++ b/src/controller/base-stream-controller.ts @@ -1278,7 +1278,7 @@ export default class BaseStreamController let { fragPrevious } = this; let { fragments, endSN } = levelDetails; const { fragmentHint } = levelDetails; - const tolerance = config.maxFragLookUpTolerance; + const { maxFragLookUpTolerance } = config; const partList = levelDetails.partList; const loadingParts = !!( @@ -1294,7 +1294,8 @@ export default class BaseStreamController let frag; if (bufferEnd < end) { - const lookupTolerance = bufferEnd > end - tolerance ? 0 : tolerance; + const lookupTolerance = + bufferEnd > end - maxFragLookUpTolerance ? 0 : maxFragLookUpTolerance; // Remove the tolerance if it would put the bufferEnd past the actual end of stream // Uses buffer and sequence number to calculate switch segment (required if using EXT-X-DISCONTINUITY-SEQUENCE) frag = findFragmentByPTS( diff --git a/src/controller/fragment-finders.ts b/src/controller/fragment-finders.ts index 47f19ae2c30..c289e9dab60 100644 --- a/src/controller/fragment-finders.ts +++ b/src/controller/fragment-finders.ts @@ -58,6 +58,7 @@ export function findFragmentByPTS( fragments: Array, bufferEnd: number = 0, maxFragLookUpTolerance: number = 0, + nextFragLookupTolerance: number = 0.005, ): Fragment | null { let fragNext: Fragment | null = null; if (fragPrevious) { @@ -76,9 +77,17 @@ export function findFragmentByPTS( // Prefer the next fragment if it's within tolerance if ( fragNext && - (!fragPrevious || fragPrevious.level === fragNext.level) && - fragmentWithinToleranceTest(bufferEnd, maxFragLookUpTolerance, fragNext) === - 0 + (((!fragPrevious || fragPrevious.level === fragNext.level) && + fragmentWithinToleranceTest( + bufferEnd, + maxFragLookUpTolerance, + fragNext, + ) === 0) || + fragmentWithinFastStartSwitch( + fragNext, + fragPrevious, + Math.min(nextFragLookupTolerance, maxFragLookUpTolerance), + )) ) { return fragNext; } @@ -94,6 +103,28 @@ export function findFragmentByPTS( return fragNext; } +function fragmentWithinFastStartSwitch( + fragNext: Fragment, + fragPrevious: Fragment | null, + nextFragLookupTolerance: number, +): boolean { + if ( + fragPrevious && + fragPrevious.start === 0 && + fragPrevious.level < fragNext.level && + (fragPrevious.endPTS || 0) > 0 + ) { + const firstDuration = fragPrevious.tagList.reduce((duration, tag) => { + if (tag[0] === 'INF') { + duration += parseFloat(tag[1]); + } + return duration; + }, nextFragLookupTolerance); + return fragNext.start <= firstDuration; + } + return false; +} + /** * The test function used by the findFragmentBySn's BinarySearch to look for the best match to the current buffer conditions. * @param candidate - The fragment to test