diff --git a/packages/core/src/dom/media/hls/index.ts b/packages/core/src/dom/media/hls/index.ts
index ea24391d..057498b4 100644
--- a/packages/core/src/dom/media/hls/index.ts
+++ b/packages/core/src/dom/media/hls/index.ts
@@ -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) {}
diff --git a/packages/core/src/dom/media/hls/preload.ts b/packages/core/src/dom/media/hls/preload.ts
new file mode 100644
index 00000000..d8d959c9
--- /dev/null
+++ b/packages/core/src/dom/media/hls/preload.ts
@@ -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>(BaseClass: Base) {
+ class HlsMediaPreload extends (BaseClass as Constructor) {
+ #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;
+}