fix: isolate preload mixin for hls delegate (#1150)

This commit is contained in:
Wesley Luyten
2026-03-27 14:02:52 -05:00
committed by GitHub
parent 68a671368f
commit a96aad7558
2 changed files with 118 additions and 65 deletions
+18 -65
View File
@@ -2,13 +2,13 @@ import Hls from 'hls.js';
import { type Delegate, DelegateMixin } from '../../../core/media/delegate';
import { CustomVideoElement } from '../custom-media-element';
import { VideoProxy } from '../proxy';
import { HlsMediaPreloadMixin } from './preload';
import { HlsMediaTextTracksMixin } from './text-tracks';
export { Hls };
export type PlaybackType = (typeof PlaybackTypes)[keyof typeof PlaybackTypes];
export type SourceType = (typeof SourceTypes)[keyof typeof SourceTypes];
export type PreloadType = '' | 'none' | 'metadata' | 'auto';
export const PlaybackTypes = {
MSE: 'mse',
@@ -38,37 +38,34 @@ export class HlsMediaDelegateBase implements Delegate {
#debug: boolean = false;
#type: SourceType | undefined;
#preferPlayback: PlaybackType | undefined = 'mse';
#preloadOnPlayAbort?: AbortController;
#defaultMaxBufferLength = 0;
#defaultMaxBufferSize = 0;
constructor() {
this.#initialize();
this.initEngine();
}
#initialize(): void {
this.#preloadOnPlayAbort?.abort();
destroyEngine(): void {
this.#engine?.destroy();
this.#engine = null;
}
if (this.type !== SourceTypes.M3U8) return;
if (this.#preferPlayback === PlaybackTypes.NATIVE) return;
if (!Hls.isSupported()) return;
initEngine(): void {
if (this.#engine) this.destroyEngine();
if (!Hls.isSupported() || this.type !== SourceTypes.M3U8 || this.#preferPlayback === PlaybackTypes.NATIVE) {
if (this.#src) this.#requestLoad();
return;
}
this.#engine = new Hls({
...defaultConfig,
debug: this.#debug,
});
this.#defaultMaxBufferLength = this.#engine.config.maxBufferLength;
this.#defaultMaxBufferSize = this.#engine.config.maxBufferSize;
if (this.#target) {
this.#engine.attachMedia(this.#target as HTMLMediaElement);
}
if (this.#src) {
this.#requestLoad();
}
if (this.#src) this.#requestLoad();
}
/** The target element, or `null` when not attached. */
@@ -81,16 +78,6 @@ export class HlsMediaDelegateBase implements Delegate {
return this.#engine;
}
get preload(): PreloadType {
return this.#target?.preload || 'metadata';
}
set preload(value: PreloadType) {
if (!this.#target || this.#target.preload === value) return;
this.#target.preload = value;
this.#updatePreload();
}
/** Explicit source type. When unset, inferred from the source URL extension. */
get type(): SourceType | undefined {
return this.#type ?? inferSourceType(this.#src);
@@ -99,7 +86,7 @@ export class HlsMediaDelegateBase implements Delegate {
set type(value: SourceType | undefined) {
if (this.#type === value) return;
this.#type = value;
this.#initialize();
this.initEngine();
}
/** Enable hls.js debug logging. Re-initializes the engine when changed. */
@@ -110,12 +97,12 @@ export class HlsMediaDelegateBase implements Delegate {
set debug(value: boolean) {
if (this.#debug === value) return;
this.#debug = value;
this.#initialize();
this.initEngine();
}
/**
* Whether to prefer `'mse'` (hls.js) or `'native'` (browser-built-in) HLS
* playback. Changing this re-initializes the delegate.
* playback. Changing this re-initializes the engine.
*/
get preferPlayback(): PlaybackType | undefined {
return this.#preferPlayback;
@@ -124,7 +111,7 @@ export class HlsMediaDelegateBase implements Delegate {
set preferPlayback(value: PlaybackType | undefined) {
if (this.#preferPlayback === value) return;
this.#preferPlayback = value;
this.#initialize();
this.initEngine();
}
/** The HLS source URL to load. */
@@ -147,7 +134,6 @@ export class HlsMediaDelegateBase implements Delegate {
load(): void {
if (this.#engine) {
this.#engine.loadSource(this.#src);
this.#updatePreload();
} else if (this.#target) {
(this.#target as HTMLMediaElement).src = this.#src;
}
@@ -156,50 +142,17 @@ export class HlsMediaDelegateBase implements Delegate {
attach(target: HTMLMediaElement): void {
this.#target = target;
this.#engine?.attachMedia(target);
this.#updatePreload();
}
detach(): void {
this.#preloadOnPlayAbort?.abort();
this.#engine?.detachMedia();
this.#target = null;
}
destroy(): void {
this.#preloadOnPlayAbort?.abort();
this.#engine?.destroy();
this.#engine = null;
this.destroyEngine();
this.#target = null;
}
#updatePreload(): void {
this.#preloadOnPlayAbort?.abort();
if (!this.#target || !this.#engine) return;
const preload = (length?: number, size?: number) => {
if (!this.#engine) return;
this.#engine.config.maxBufferLength = length ?? this.#defaultMaxBufferLength;
this.#engine.config.maxBufferSize = size ?? this.#defaultMaxBufferSize;
this.#engine.startLoad();
};
if (this.preload === 'auto' || !this.#target.paused) {
preload();
return;
}
if (this.preload === 'metadata') {
preload(1, 1);
}
// preload === 'none' or preload === 'metadata' both defer full load until play.
this.#preloadOnPlayAbort = new AbortController();
this.#target.addEventListener('play', () => preload(), {
signal: this.#preloadOnPlayAbort.signal,
once: true,
});
}
}
function inferSourceType(src: string): SourceType {
@@ -208,7 +161,7 @@ function inferSourceType(src: string): SourceType {
return SourceTypes.M3U8;
}
export const HlsMediaDelegate = HlsMediaTextTracksMixin(HlsMediaDelegateBase);
export const HlsMediaDelegate = HlsMediaTextTracksMixin(HlsMediaPreloadMixin(HlsMediaDelegateBase));
// This is used by the web component because it needs to extend HTMLElement!
export class HlsCustomMedia extends DelegateMixin(CustomVideoElement, HlsMediaDelegate) {}
+100
View File
@@ -0,0 +1,100 @@
import type { Constructor } from '@videojs/utils/types';
import type Hls from 'hls.js';
export type PreloadType = '' | 'none' | 'metadata' | 'auto';
interface HlsPreloadHost {
readonly engine: Hls | null;
readonly target: EventTarget | null | undefined;
load?(): void;
attach?(target: EventTarget): void;
detach?(): void;
destroy?(): void;
destroyEngine?(): void;
}
/**
* Manages HLS preload behavior by mapping the media element's `preload`
* attribute to hls.js `startLoad` / buffer-limit configuration.
*
* - `'auto'` or already playing → full buffer limits, immediate start.
* - `'metadata'` → minimal buffer (1 byte / 1 second), deferred full load on play.
* - `'none'` / `''` → no start, deferred full load on play.
*/
export function HlsMediaPreloadMixin<Base extends Constructor<HlsPreloadHost>>(BaseClass: Base) {
class HlsMediaPreload extends (BaseClass as Constructor<HlsPreloadHost>) {
#preloadAbort?: AbortController;
#defaultMaxBufferLength: number | undefined;
#defaultMaxBufferSize: number | undefined;
get preload(): PreloadType {
return (this.target as HTMLMediaElement | null)?.preload || 'metadata';
}
set preload(value: PreloadType) {
const target = this.target as HTMLMediaElement | null;
if (!target || target.preload === value) return;
target.preload = value;
this.#updatePreload();
}
load(): void {
super.load?.();
this.#updatePreload();
}
attach(target: EventTarget): void {
super.attach?.(target);
this.#updatePreload();
}
destroyEngine(): void {
this.#preloadAbort?.abort();
super.destroyEngine?.();
}
detach(): void {
this.#preloadAbort?.abort();
super.detach?.();
}
#updatePreload(): void {
this.#preloadAbort?.abort();
const target = this.target as HTMLMediaElement | null;
const { engine } = this;
if (!target || !engine) return;
this.#defaultMaxBufferLength ??= engine.config.maxBufferLength;
this.#defaultMaxBufferSize ??= engine.config.maxBufferSize;
const defaultLength = this.#defaultMaxBufferLength;
const defaultSize = this.#defaultMaxBufferSize;
const startLoad = (length?: number, size?: number) => {
const { engine } = this;
if (!engine) return;
engine.config.maxBufferLength = length ?? defaultLength;
engine.config.maxBufferSize = size ?? defaultSize;
engine.startLoad();
};
if (this.preload === 'auto' || !target.paused) {
startLoad();
return;
}
if (this.preload === 'metadata') {
startLoad(1, 1);
}
this.#preloadAbort = new AbortController();
target.addEventListener('play', () => startLoad(), {
signal: this.#preloadAbort.signal,
once: true,
});
}
}
return HlsMediaPreload as unknown as Base;
}