diff --git a/api-extractor/report/hls.js.api.md b/api-extractor/report/hls.js.api.md index d2cd6c4af40..d393fa6beda 100644 --- a/api-extractor/report/hls.js.api.md +++ b/api-extractor/report/hls.js.api.md @@ -1578,12 +1578,14 @@ class Hls implements HlsEventEmitter { on(event: E, listener: HlsListeners[E], context?: Context): void; // (undocumented) once(event: E, listener: HlsListeners[E], context?: Context): void; + pauseBuffering(): void; get playingDate(): Date | null; recoverMediaError(): void; // (undocumented) removeAllListeners(event?: E | undefined): void; // (undocumented) removeLevel(levelIndex: any, urlId?: number): void; + resumeBuffering(): void; get startLevel(): number; // Warning: (ae-setter-with-docs) The doc comment for the property "startLevel" must appear on the getter, not the setter. set startLevel(newLevel: number); @@ -2586,6 +2588,8 @@ export interface ManifestParsedData { export interface MediaAttachedData { // (undocumented) media: HTMLMediaElement; + // (undocumented) + mediaSource?: MediaSource; } // Warning: (ae-missing-release-tag) "MediaAttachingData" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/src/controller/buffer-controller.ts b/src/controller/buffer-controller.ts index e9906328537..366cc7cc1f3 100644 --- a/src/controller/buffer-controller.ts +++ b/src/controller/buffer-controller.ts @@ -13,6 +13,7 @@ import { SourceBufferName, SourceBufferListeners, } from '../types/buffer'; +import CapLevelController from './cap-level-controller'; import type { LevelUpdatedData, BufferAppendingData, @@ -29,7 +30,6 @@ import type { ChunkMetadata } from '../types/transmuxer'; import type Hls from '../hls'; import type { LevelDetails } from '../loader/level-details'; -const MediaSource = getMediaSource(); const VIDEO_CODEC_PROFILE_REPLACE = /(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/; @@ -157,12 +157,15 @@ export default class BufferController implements ComponentAPI { data: MediaAttachingData ) { const media = (this.media = data.media); + const MediaSource = getMediaSource(); if (media && MediaSource) { const ms = (this.mediaSource = new MediaSource()); // MediaSource listeners are arrow functions with a lexical scope, and do not need to be bound ms.addEventListener('sourceopen', this._onMediaSourceOpen); ms.addEventListener('sourceended', this._onMediaSourceEnded); ms.addEventListener('sourceclose', this._onMediaSourceClose); + ms.addEventListener('startstreaming', this._onStartStreaming); + ms.addEventListener('endstreaming', this._onEndStreaming); // link video and media Source media.src = self.URL.createObjectURL(ms); // cache the locally generated object url @@ -170,6 +173,34 @@ export default class BufferController implements ComponentAPI { media.addEventListener('emptied', this._onMediaEmptied); } } + private _onEndStreaming = (event) => { + this.hls.pauseBuffering(); + }; + private _onStartStreaming = (event) => { + const { hls, mediaSource } = this; + if (!hls || !mediaSource) { + return; + } + if ('quality' in mediaSource) { + if (mediaSource.quality === 'low') { + hls.autoLevelCapping = CapLevelController.getMaxLevelByMediaSize( + hls.levels, + 1280, + 720 + ); + } else if (mediaSource.quality === 'medium') { + hls.autoLevelCapping = CapLevelController.getMaxLevelByMediaSize( + hls.levels, + 1920, + 1080 + ); + } else { + // do not cap max quality + hls.autoLevelCapping = -1; + } + } + hls.resumeBuffering(); + }; protected onMediaDetaching() { const { media, mediaSource, _objectUrl } = this; @@ -193,6 +224,8 @@ export default class BufferController implements ComponentAPI { mediaSource.removeEventListener('sourceopen', this._onMediaSourceOpen); mediaSource.removeEventListener('sourceended', this._onMediaSourceEnded); mediaSource.removeEventListener('sourceclose', this._onMediaSourceClose); + mediaSource.removeEventListener('startstreaming', this._onStartStreaming); + mediaSource.removeEventListener('endstreaming', this._onEndStreaming); // Detach properly the MediaSource from the HTMLMediaElement as // suggested in https://github.com/w3c/media-source/issues/53. @@ -777,6 +810,13 @@ export default class BufferController implements ComponentAPI { this.addBufferListener(sbName, 'updatestart', this._onSBUpdateStart); this.addBufferListener(sbName, 'updateend', this._onSBUpdateEnd); this.addBufferListener(sbName, 'error', this._onSBUpdateError); + // ManagedSourceBuffer bufferedchange event + this.addBufferListener(sbName, 'bufferedchange', (event) => { + this.hls.trigger(Events.BUFFER_FLUSHED, { + type: trackName as SourceBufferName, + }); + }); + this.tracks[trackName] = { buffer: sb, codec: codec, @@ -808,7 +848,10 @@ export default class BufferController implements ComponentAPI { if (media) { media.removeEventListener('emptied', this._onMediaEmptied); this.updateMediaElementDuration(); - this.hls.trigger(Events.MEDIA_ATTACHED, { media }); + this.hls.trigger(Events.MEDIA_ATTACHED, { + media, + mediaSource: mediaSource as MediaSource, + }); } if (mediaSource) { diff --git a/src/demux/transmuxer-interface.ts b/src/demux/transmuxer-interface.ts index e8e25d16987..3f19f0718a7 100644 --- a/src/demux/transmuxer-interface.ts +++ b/src/demux/transmuxer-interface.ts @@ -22,8 +22,6 @@ import type { PlaylistLevelType } from '../types/loader'; import type { TypeSupported } from './tsdemuxer'; import type { RationalTimestamp } from '../utils/timescale-conversion'; -const MediaSource = getMediaSource() || { isTypeSupported: () => false }; - export default class TransmuxerInterface { public error: Error | null = null; private hls: Hls; @@ -66,6 +64,7 @@ export default class TransmuxerInterface { this.observer.on(Events.FRAG_DECRYPTED, forwardMessage); this.observer.on(Events.ERROR, forwardMessage); + const MediaSource = getMediaSource() || { isTypeSupported: () => false }; const typeSupported: TypeSupported = { mp4: MediaSource.isTypeSupported('video/mp4'), mpeg: MediaSource.isTypeSupported('audio/mpeg'), diff --git a/src/hls.ts b/src/hls.ts index b8d08d28ed3..e6f6709fec3 100644 --- a/src/hls.ts +++ b/src/hls.ts @@ -52,6 +52,7 @@ export default class Hls implements HlsEventEmitter { private coreComponents: ComponentAPI[]; private networkControllers: NetworkComponentAPI[]; + private started: boolean = false; private _emitter: HlsEventEmitter = new EventEmitter(); private _autoLevelCapping: number; private _maxHdcpLevel: HdcpLevel = null; @@ -398,6 +399,7 @@ export default class Hls implements HlsEventEmitter { */ startLoad(startPosition: number = -1) { logger.log(`startLoad(${startPosition})`); + this.started = true; this.networkControllers.forEach((controller) => { controller.startLoad(startPosition); }); @@ -408,11 +410,37 @@ export default class Hls implements HlsEventEmitter { */ stopLoad() { logger.log('stopLoad'); + this.started = false; this.networkControllers.forEach((controller) => { controller.stopLoad(); }); } + /** + * Resumes stream controller segment loading if previously started. + */ + resumeBuffering() { + if (this.started) { + this.networkControllers.forEach((controller) => { + if ('fragmentLoader' in controller) { + controller.startLoad(-1); + } + }); + } + } + + /** + * Stops stream controller segment loading without changing 'started' state like stopLoad(). + * This allows for media buffering to be paused without interupting playlist loading. + */ + pauseBuffering() { + this.networkControllers.forEach((controller) => { + if ('fragmentLoader' in controller) { + controller.stopLoad(); + } + }); + } + /** * Swap through possible audio codecs in the stream (for example to switch from stereo to 5.1) */ diff --git a/src/types/events.ts b/src/types/events.ts index 53ede3da7f8..0287a3bb829 100644 --- a/src/types/events.ts +++ b/src/types/events.ts @@ -37,6 +37,7 @@ export interface MediaAttachingData { export interface MediaAttachedData { media: HTMLMediaElement; + mediaSource?: MediaSource; } export interface BufferCodecsData { diff --git a/src/utils/codecs.ts b/src/utils/codecs.ts index 3f1e4ea20fd..f76d437bae6 100644 --- a/src/utils/codecs.ts +++ b/src/utils/codecs.ts @@ -76,8 +76,6 @@ const sampleEntryCodesISO = { }, }; -const MediaSource = getMediaSource(); - export type CodecType = 'audio' | 'video'; export function isCodecType(codec: string, type: CodecType): boolean { @@ -95,6 +93,7 @@ export function areCodecsMediaSourceSupported( } function isCodecMediaSourceSupported(codec: string, type: CodecType): boolean { + const MediaSource = getMediaSource(); return ( MediaSource?.isTypeSupported(`${type || 'video'}/mp4;codecs="${codec}"`) ?? false diff --git a/src/utils/mediasource-helper.ts b/src/utils/mediasource-helper.ts index e43f7bf9f7e..c80834b8d92 100644 --- a/src/utils/mediasource-helper.ts +++ b/src/utils/mediasource-helper.ts @@ -4,5 +4,9 @@ export function getMediaSource(): typeof MediaSource | undefined { if (typeof self === 'undefined') return undefined; - return self.MediaSource || ((self as any).WebKitMediaSource as MediaSource); + return ( + ((self as any).ManagedMediaSource as typeof MediaSource) || + self.MediaSource || + ((self as any).WebKitMediaSource as typeof MediaSource) + ); } diff --git a/tests/unit/controller/content-steering-controller.ts b/tests/unit/controller/content-steering-controller.ts index f901db81f24..696bd91ca3f 100644 --- a/tests/unit/controller/content-steering-controller.ts +++ b/tests/unit/controller/content-steering-controller.ts @@ -30,8 +30,6 @@ import { getMediaSource } from '../../../src/utils/mediasource-helper'; chai.use(sinonChai); const expect = chai.expect; -const MediaSource = getMediaSource(); - type ConentSteeringControllerTestable = Omit< ContentSteeringController, | 'enabled' @@ -67,6 +65,7 @@ describe('ContentSteeringController', function () { let contentSteeringController: ConentSteeringControllerTestable; beforeEach(function () { + const MediaSource = getMediaSource(); hls = new HlsMock({ loader: MockXhr, }); diff --git a/tests/unit/controller/level-controller.ts b/tests/unit/controller/level-controller.ts index 1217aa13c5a..c727781d521 100644 --- a/tests/unit/controller/level-controller.ts +++ b/tests/unit/controller/level-controller.ts @@ -32,8 +32,6 @@ import { getMediaSource } from '../../../src/utils/mediasource-helper'; chai.use(sinonChai); const expect = chai.expect; -const MediaSource = getMediaSource(); - type LevelControllerTestable = Omit & { onManifestLoaded: (event: string, data: Partial) => void; onAudioTrackSwitched: (event: string, data: { id: number }) => void; @@ -86,6 +84,7 @@ describe('LevelController', function () { let levelController: LevelControllerTestable; beforeEach(function () { + const MediaSource = getMediaSource(); hls = new HlsMock({}); levelController = new LevelController( hls as any,