@videojs/spf 10.0.0-beta.17 → 10.0.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/default/dom/playback-engine/adapter.js +78 -73
- package/dist/default/dom/playback-engine/adapter.js.map +1 -1
- package/dist/default/dom/playback-engine.js +2 -2
- package/dist/default/dom.js +2 -2
- package/dist/dev/dom/playback-engine/adapter.d.ts +22 -23
- package/dist/dev/dom/playback-engine/adapter.d.ts.map +1 -1
- package/dist/dev/dom/playback-engine/adapter.js +78 -73
- package/dist/dev/dom/playback-engine/adapter.js.map +1 -1
- package/dist/dev/dom/playback-engine.d.ts +2 -2
- package/dist/dev/dom/playback-engine.js +2 -2
- package/dist/dev/dom.d.ts +2 -2
- package/dist/dev/dom.js +2 -2
- package/package.json +2 -2
|
@@ -2,7 +2,7 @@ import { update } from "../../core/signals/primitives.js";
|
|
|
2
2
|
import { createPlaybackEngine } from "./engine.js";
|
|
3
3
|
//#region src/dom/playback-engine/adapter.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Mixin that adds SPF playback engine behavior to any base class.
|
|
6
6
|
*
|
|
7
7
|
* Implements the src/play() contract per the WHATWG HTML spec so that SPF can
|
|
8
8
|
* be used anywhere a media element API is expected.
|
|
@@ -13,83 +13,88 @@ import { createPlaybackEngine } from "./engine.js";
|
|
|
13
13
|
* changes and re-applied to the new engine automatically.
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
|
-
*
|
|
16
|
+
* class SimpleHlsMedia extends SpfMediaMixin(HTMLVideoElementHost) {}
|
|
17
|
+
*
|
|
18
|
+
* const media = new SimpleHlsMedia();
|
|
17
19
|
* media.attach(document.querySelector('video'));
|
|
18
20
|
* media.src = 'https://stream.mux.com/abc123.m3u8';
|
|
19
|
-
*
|
|
20
|
-
* // Change source — old engine is destroyed, new one starts clean:
|
|
21
|
-
* media.src = 'https://stream.mux.com/xyz456.m3u8';
|
|
22
|
-
*
|
|
23
|
-
* // Explicit teardown:
|
|
24
|
-
* media.destroy();
|
|
25
21
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this
|
|
80
|
-
|
|
22
|
+
function SpfMediaMixin(BaseClass) {
|
|
23
|
+
class SpfMediaImpl extends BaseClass {
|
|
24
|
+
#engine;
|
|
25
|
+
#config;
|
|
26
|
+
#preload = "";
|
|
27
|
+
/** Pending loadstart listener from a deferred play() retry, if any. */
|
|
28
|
+
#loadstartListener = null;
|
|
29
|
+
constructor(...args) {
|
|
30
|
+
super(...args);
|
|
31
|
+
const { config } = args?.[0] ?? {};
|
|
32
|
+
this.#config = config;
|
|
33
|
+
this.#engine = createPlaybackEngine(config);
|
|
34
|
+
}
|
|
35
|
+
get engine() {
|
|
36
|
+
return this.#engine;
|
|
37
|
+
}
|
|
38
|
+
attach(mediaElement) {
|
|
39
|
+
super.attach?.(mediaElement);
|
|
40
|
+
update(this.#engine.owners, { mediaElement });
|
|
41
|
+
}
|
|
42
|
+
detach() {
|
|
43
|
+
this.#cancelPendingPlay();
|
|
44
|
+
update(this.#engine.owners, { mediaElement: void 0 });
|
|
45
|
+
super.detach?.();
|
|
46
|
+
}
|
|
47
|
+
destroy() {
|
|
48
|
+
this.#cancelPendingPlay();
|
|
49
|
+
this.#engine.destroy();
|
|
50
|
+
}
|
|
51
|
+
get preload() {
|
|
52
|
+
return this.#preload;
|
|
53
|
+
}
|
|
54
|
+
set preload(value) {
|
|
55
|
+
this.#preload = value;
|
|
56
|
+
if (value) update(this.#engine.state, { preload: value });
|
|
57
|
+
}
|
|
58
|
+
get src() {
|
|
59
|
+
return this.#engine.state.get().presentation?.url ?? "";
|
|
60
|
+
}
|
|
61
|
+
set src(value) {
|
|
62
|
+
const prevMediaElement = this.#engine.owners.get().mediaElement;
|
|
63
|
+
this.#cancelPendingPlay();
|
|
64
|
+
this.#engine.destroy();
|
|
65
|
+
this.#engine = createPlaybackEngine(this.#config);
|
|
66
|
+
if (this.#preload) update(this.#engine.state, { preload: this.#preload });
|
|
67
|
+
if (prevMediaElement) update(this.#engine.owners, { mediaElement: prevMediaElement });
|
|
68
|
+
if (value) update(this.#engine.state, { presentation: { url: value } });
|
|
69
|
+
}
|
|
70
|
+
play() {
|
|
71
|
+
const { mediaElement } = this.#engine.owners.get();
|
|
72
|
+
if (!mediaElement) return Promise.reject(/* @__PURE__ */ new Error("SpfMedia: no media element attached"));
|
|
73
|
+
update(this.#engine.state, { playbackInitiated: true });
|
|
74
|
+
return mediaElement.play().catch((err) => {
|
|
75
|
+
if (this.src) return new Promise((resolve, reject) => {
|
|
76
|
+
const listener = () => {
|
|
77
|
+
this.#loadstartListener = null;
|
|
78
|
+
mediaElement.play().then(resolve, reject);
|
|
79
|
+
};
|
|
80
|
+
this.#loadstartListener = listener;
|
|
81
|
+
mediaElement.addEventListener("loadstart", listener, { once: true });
|
|
82
|
+
});
|
|
83
|
+
throw err;
|
|
81
84
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.#loadstartListener = null;
|
|
85
|
+
}
|
|
86
|
+
#cancelPendingPlay() {
|
|
87
|
+
if (!this.#loadstartListener) return;
|
|
88
|
+
const { mediaElement } = this.#engine.owners.get();
|
|
89
|
+
mediaElement?.removeEventListener("loadstart", this.#loadstartListener);
|
|
90
|
+
this.#loadstartListener = null;
|
|
91
|
+
}
|
|
90
92
|
}
|
|
91
|
-
|
|
93
|
+
return SpfMediaImpl;
|
|
94
|
+
}
|
|
95
|
+
/** Standalone SPF media adapter with no base class. */
|
|
96
|
+
var SpfMedia = class extends SpfMediaMixin(class {}) {};
|
|
92
97
|
//#endregion
|
|
93
|
-
export { SpfMedia };
|
|
98
|
+
export { SpfMedia, SpfMediaMixin };
|
|
94
99
|
|
|
95
100
|
//# sourceMappingURL=adapter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","names":["#config","#engine","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"sourcesContent":["import { update } from '../../core/signals/primitives';\nimport type { PlaybackEngineConfig } from './engine';\nimport { createPlaybackEngine, type PlaybackEngine } from './engine';\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"adapter.js","names":["#config","#engine","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"sourcesContent":["import type { Constructor, MixinReturn } from '@videojs/utils/types';\nimport { update } from '../../core/signals/primitives';\nimport type { PlaybackEngineConfig } from './engine';\nimport { createPlaybackEngine, type PlaybackEngine } from './engine';\n\nexport interface SpfMediaAPI {\n readonly engine: PlaybackEngine;\n src: string;\n preload: '' | 'none' | 'metadata' | 'auto';\n attach(mediaElement: HTMLMediaElement): void;\n detach(): void;\n destroy(): void;\n play(): Promise<void>;\n}\n\n/**\n * Mixin that adds SPF playback engine behavior to any base class.\n *\n * Implements the src/play() contract per the WHATWG HTML spec so that SPF can\n * be used anywhere a media element API is expected.\n *\n * A new engine is created on every src assignment — this fully tears down all\n * state, SourceBuffers, and in-flight requests from the previous source before\n * the next one begins. The media element reference is preserved across src\n * changes and re-applied to the new engine automatically.\n *\n * @example\n * class SimpleHlsMedia extends SpfMediaMixin(HTMLVideoElementHost) {}\n *\n * const media = new SimpleHlsMedia();\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n */\nexport function SpfMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class SpfMediaImpl extends BaseClass {\n #engine: PlaybackEngine;\n #config: PlaybackEngineConfig;\n #preload: '' | 'none' | 'metadata' | 'auto' = '';\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { config } = args?.[0] ?? {};\n this.#config = config;\n this.#engine = createPlaybackEngine(config);\n }\n\n get engine(): PlaybackEngine {\n return this.#engine;\n }\n\n // -------------------------------------------------------------------------\n // Media element lifecycle\n // -------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n super.attach?.(mediaElement);\n update(this.#engine.owners, { mediaElement });\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n update(this.#engine.owners, { mediaElement: undefined });\n super.detach?.();\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // -------------------------------------------------------------------------\n // preload — synchronous IDL attribute (WHATWG §4.8.11.2)\n // -------------------------------------------------------------------------\n\n get preload(): '' | 'none' | 'metadata' | 'auto' {\n return this.#preload;\n }\n\n set preload(value: '' | 'none' | 'metadata' | 'auto') {\n this.#preload = value;\n if (value) {\n update(this.#engine.state, { preload: value });\n }\n // value = '' clears #preload (so the next engine recreation won't re-apply\n // an explicit value) but does not patch current state — the existing preload\n // stays in effect until the next src change creates a fresh engine.\n }\n\n // -------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // Each assignment destroys the current engine and starts a fresh one, exactly\n // as the browser's load algorithm resets all media element state on src change.\n // -------------------------------------------------------------------------\n\n get src(): string {\n return this.#engine.state.get().presentation?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#engine.owners.get().mediaElement;\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = createPlaybackEngine(this.#config);\n\n // Apply explicit preload before setting owners so syncPreloadAttribute skips\n // element inference and the explicit value is preserved across src changes.\n if (this.#preload) {\n update(this.#engine.state, { preload: this.#preload });\n }\n\n if (prevMediaElement) {\n update(this.#engine.owners, { mediaElement: prevMediaElement });\n }\n\n if (value) {\n update(this.#engine.state, { presentation: { url: value } });\n }\n }\n\n // -------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // -------------------------------------------------------------------------\n\n play(): Promise<void> {\n const { mediaElement } = this.#engine.owners.get();\n if (!mediaElement) {\n return Promise.reject(new Error('SpfMedia: no media element attached'));\n }\n\n // Signal play intent — enables loading even with preload=\"none\"\n update(this.#engine.state, { playbackInitiated: true });\n\n return mediaElement.play().catch((err: unknown) => {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned\n // by MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const { mediaElement } = this.#engine.owners.get();\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n }\n\n return SpfMediaImpl as unknown as MixinReturn<Base, SpfMediaAPI>;\n}\n\n/** Standalone SPF media adapter with no base class. */\nexport class SpfMedia extends SpfMediaMixin(class {}) {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,cAA6C,WAAiB;CAC5E,MAAM,qBAAqB,UAAU;EACnC;EACA;EACA,WAA8C;;EAG9C,qBAA0C;EAE1C,YAAY,GAAG,MAAa;AAC1B,SAAM,GAAG,KAAK;GAEd,MAAM,EAAE,WAAW,OAAO,MAAM,EAAE;AAClC,SAAA,SAAe;AACf,SAAA,SAAe,qBAAqB,OAAO;;EAG7C,IAAI,SAAyB;AAC3B,UAAO,MAAA;;EAOT,OAAO,cAAsC;AAC3C,SAAM,SAAS,aAAa;AAC5B,UAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,CAAC;;EAG/C,SAAe;AACb,SAAA,mBAAyB;AACzB,UAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,KAAA,GAAW,CAAC;AACxD,SAAM,UAAU;;EAGlB,UAAgB;AACd,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;;EAOxB,IAAI,UAA6C;AAC/C,UAAO,MAAA;;EAGT,IAAI,QAAQ,OAA0C;AACpD,SAAA,UAAgB;AAChB,OAAI,MACF,QAAO,MAAA,OAAa,OAAO,EAAE,SAAS,OAAO,CAAC;;EAalD,IAAI,MAAc;AAChB,UAAO,MAAA,OAAa,MAAM,KAAK,CAAC,cAAc,OAAO;;EAGvD,IAAI,IAAI,OAAe;GACrB,MAAM,mBAAmB,MAAA,OAAa,OAAO,KAAK,CAAC;AAEnD,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;AACtB,SAAA,SAAe,qBAAqB,MAAA,OAAa;AAIjD,OAAI,MAAA,QACF,QAAO,MAAA,OAAa,OAAO,EAAE,SAAS,MAAA,SAAe,CAAC;AAGxD,OAAI,iBACF,QAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,kBAAkB,CAAC;AAGjE,OAAI,MACF,QAAO,MAAA,OAAa,OAAO,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,CAAC;;EAShE,OAAsB;GACpB,MAAM,EAAE,iBAAiB,MAAA,OAAa,OAAO,KAAK;AAClD,OAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,sCAAsC,CAAC;AAIzE,UAAO,MAAA,OAAa,OAAO,EAAE,mBAAmB,MAAM,CAAC;AAEvD,UAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AAIjD,QAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;KAC5C,MAAM,iBAAiB;AACrB,YAAA,oBAA0B;AAC1B,mBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,WAAA,oBAA0B;AAC1B,kBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;MACpE;AAEJ,UAAM;KACN;;EAOJ,qBAA2B;AACzB,OAAI,CAAC,MAAA,kBAAyB;GAC9B,MAAM,EAAE,iBAAiB,MAAA,OAAa,OAAO,KAAK;AAClD,iBAAc,oBAAoB,aAAa,MAAA,kBAAwB;AACvE,SAAA,oBAA0B;;;AAI9B,QAAO;;;AAIT,IAAa,WAAb,cAA8B,cAAc,MAAM,GAAG,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { effect } from "../core/signals/effect.js";
|
|
2
2
|
import { createPlaybackEngine } from "./playback-engine/engine.js";
|
|
3
|
-
import { SpfMedia } from "./playback-engine/adapter.js";
|
|
4
|
-
export { SpfMedia, createPlaybackEngine, effect };
|
|
3
|
+
import { SpfMedia, SpfMediaMixin } from "./playback-engine/adapter.js";
|
|
4
|
+
export { SpfMedia, SpfMediaMixin, createPlaybackEngine, effect };
|
package/dist/default/dom.js
CHANGED
|
@@ -6,5 +6,5 @@ import { trackPlaybackInitiated } from "./dom/features/track-playback-initiated.
|
|
|
6
6
|
import { trackPlaybackRate } from "./dom/features/track-playback-rate.js";
|
|
7
7
|
import { appendSegment } from "./dom/media/append-segment.js";
|
|
8
8
|
import { flushBuffer } from "./dom/media/buffer-flusher.js";
|
|
9
|
-
import { SpfMedia } from "./dom/playback-engine/adapter.js";
|
|
10
|
-
export { SpfMedia, appendSegment, destroyVttParser, flushBuffer, loadSegments, loadTextTrackCues, parseVttSegment, trackCurrentTime, trackPlaybackInitiated, trackPlaybackRate };
|
|
9
|
+
import { SpfMedia, SpfMediaMixin } from "./dom/playback-engine/adapter.js";
|
|
10
|
+
export { SpfMedia, SpfMediaMixin, appendSegment, destroyVttParser, flushBuffer, loadSegments, loadTextTrackCues, parseVttSegment, trackCurrentTime, trackPlaybackInitiated, trackPlaybackRate };
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
-
import { PlaybackEngine
|
|
1
|
+
import { PlaybackEngine } from "./engine.js";
|
|
2
|
+
import { Constructor, MixinReturn } from "@videojs/utils/types";
|
|
2
3
|
|
|
3
4
|
//#region src/dom/playback-engine/adapter.d.ts
|
|
5
|
+
interface SpfMediaAPI {
|
|
6
|
+
readonly engine: PlaybackEngine;
|
|
7
|
+
src: string;
|
|
8
|
+
preload: '' | 'none' | 'metadata' | 'auto';
|
|
9
|
+
attach(mediaElement: HTMLMediaElement): void;
|
|
10
|
+
detach(): void;
|
|
11
|
+
destroy(): void;
|
|
12
|
+
play(): Promise<void>;
|
|
13
|
+
}
|
|
4
14
|
/**
|
|
5
|
-
*
|
|
15
|
+
* Mixin that adds SPF playback engine behavior to any base class.
|
|
6
16
|
*
|
|
7
17
|
* Implements the src/play() contract per the WHATWG HTML spec so that SPF can
|
|
8
18
|
* be used anywhere a media element API is expected.
|
|
@@ -13,29 +23,18 @@ import { PlaybackEngine, PlaybackEngineConfig } from "./engine.js";
|
|
|
13
23
|
* changes and re-applied to the new engine automatically.
|
|
14
24
|
*
|
|
15
25
|
* @example
|
|
16
|
-
*
|
|
26
|
+
* class SimpleHlsMedia extends SpfMediaMixin(HTMLVideoElementHost) {}
|
|
27
|
+
*
|
|
28
|
+
* const media = new SimpleHlsMedia();
|
|
17
29
|
* media.attach(document.querySelector('video'));
|
|
18
30
|
* media.src = 'https://stream.mux.com/abc123.m3u8';
|
|
19
|
-
*
|
|
20
|
-
* // Change source — old engine is destroyed, new one starts clean:
|
|
21
|
-
* media.src = 'https://stream.mux.com/xyz456.m3u8';
|
|
22
|
-
*
|
|
23
|
-
* // Explicit teardown:
|
|
24
|
-
* media.destroy();
|
|
25
31
|
*/
|
|
26
|
-
declare
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
destroy(): void;
|
|
33
|
-
get preload(): '' | 'none' | 'metadata' | 'auto';
|
|
34
|
-
set preload(value: '' | 'none' | 'metadata' | 'auto');
|
|
35
|
-
get src(): string;
|
|
36
|
-
set src(value: string);
|
|
37
|
-
play(): Promise<void>;
|
|
38
|
-
}
|
|
32
|
+
declare function SpfMediaMixin<Base extends Constructor<any>>(BaseClass: Base): MixinReturn<Base, SpfMediaAPI>;
|
|
33
|
+
declare const SpfMedia_base: MixinReturn<{
|
|
34
|
+
new (): {};
|
|
35
|
+
}, SpfMediaAPI>;
|
|
36
|
+
/** Standalone SPF media adapter with no base class. */
|
|
37
|
+
declare class SpfMedia extends SpfMedia_base {}
|
|
39
38
|
//#endregion
|
|
40
|
-
export { SpfMedia };
|
|
39
|
+
export { SpfMedia, SpfMediaAPI, SpfMediaMixin };
|
|
41
40
|
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.d.ts","names":[],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","names":[],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"mappings":";;;;UAKiB,WAAA;EAAA,SACN,MAAA,EAAQ,cAAA;EACjB,GAAA;EACA,OAAA;EACA,MAAA,CAAO,YAAA,EAAc,gBAAA;EACrB,MAAA;EACA,OAAA;EACA,IAAA,IAAQ,OAAA;AAAA;;;;;;;;;;;;;;;AAqBV;;;;iBAAgB,aAAA,cAA2B,WAAA,MAAA,CAAkB,SAAA,EAAW,IAAA,GAuIpC,WAAA,CAAY,IAAA,EAAM,WAAA;AAAA,cACrD,aAAA;;;;cAGY,QAAA,SAAiB,aAAA"}
|
|
@@ -2,7 +2,7 @@ import { update } from "../../core/signals/primitives.js";
|
|
|
2
2
|
import { createPlaybackEngine } from "./engine.js";
|
|
3
3
|
//#region src/dom/playback-engine/adapter.ts
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Mixin that adds SPF playback engine behavior to any base class.
|
|
6
6
|
*
|
|
7
7
|
* Implements the src/play() contract per the WHATWG HTML spec so that SPF can
|
|
8
8
|
* be used anywhere a media element API is expected.
|
|
@@ -13,83 +13,88 @@ import { createPlaybackEngine } from "./engine.js";
|
|
|
13
13
|
* changes and re-applied to the new engine automatically.
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
16
|
-
*
|
|
16
|
+
* class SimpleHlsMedia extends SpfMediaMixin(HTMLVideoElementHost) {}
|
|
17
|
+
*
|
|
18
|
+
* const media = new SimpleHlsMedia();
|
|
17
19
|
* media.attach(document.querySelector('video'));
|
|
18
20
|
* media.src = 'https://stream.mux.com/abc123.m3u8';
|
|
19
|
-
*
|
|
20
|
-
* // Change source — old engine is destroyed, new one starts clean:
|
|
21
|
-
* media.src = 'https://stream.mux.com/xyz456.m3u8';
|
|
22
|
-
*
|
|
23
|
-
* // Explicit teardown:
|
|
24
|
-
* media.destroy();
|
|
25
21
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
this
|
|
80
|
-
|
|
22
|
+
function SpfMediaMixin(BaseClass) {
|
|
23
|
+
class SpfMediaImpl extends BaseClass {
|
|
24
|
+
#engine;
|
|
25
|
+
#config;
|
|
26
|
+
#preload = "";
|
|
27
|
+
/** Pending loadstart listener from a deferred play() retry, if any. */
|
|
28
|
+
#loadstartListener = null;
|
|
29
|
+
constructor(...args) {
|
|
30
|
+
super(...args);
|
|
31
|
+
const { config } = args?.[0] ?? {};
|
|
32
|
+
this.#config = config;
|
|
33
|
+
this.#engine = createPlaybackEngine(config);
|
|
34
|
+
}
|
|
35
|
+
get engine() {
|
|
36
|
+
return this.#engine;
|
|
37
|
+
}
|
|
38
|
+
attach(mediaElement) {
|
|
39
|
+
super.attach?.(mediaElement);
|
|
40
|
+
update(this.#engine.owners, { mediaElement });
|
|
41
|
+
}
|
|
42
|
+
detach() {
|
|
43
|
+
this.#cancelPendingPlay();
|
|
44
|
+
update(this.#engine.owners, { mediaElement: void 0 });
|
|
45
|
+
super.detach?.();
|
|
46
|
+
}
|
|
47
|
+
destroy() {
|
|
48
|
+
this.#cancelPendingPlay();
|
|
49
|
+
this.#engine.destroy();
|
|
50
|
+
}
|
|
51
|
+
get preload() {
|
|
52
|
+
return this.#preload;
|
|
53
|
+
}
|
|
54
|
+
set preload(value) {
|
|
55
|
+
this.#preload = value;
|
|
56
|
+
if (value) update(this.#engine.state, { preload: value });
|
|
57
|
+
}
|
|
58
|
+
get src() {
|
|
59
|
+
return this.#engine.state.get().presentation?.url ?? "";
|
|
60
|
+
}
|
|
61
|
+
set src(value) {
|
|
62
|
+
const prevMediaElement = this.#engine.owners.get().mediaElement;
|
|
63
|
+
this.#cancelPendingPlay();
|
|
64
|
+
this.#engine.destroy();
|
|
65
|
+
this.#engine = createPlaybackEngine(this.#config);
|
|
66
|
+
if (this.#preload) update(this.#engine.state, { preload: this.#preload });
|
|
67
|
+
if (prevMediaElement) update(this.#engine.owners, { mediaElement: prevMediaElement });
|
|
68
|
+
if (value) update(this.#engine.state, { presentation: { url: value } });
|
|
69
|
+
}
|
|
70
|
+
play() {
|
|
71
|
+
const { mediaElement } = this.#engine.owners.get();
|
|
72
|
+
if (!mediaElement) return Promise.reject(/* @__PURE__ */ new Error("SpfMedia: no media element attached"));
|
|
73
|
+
update(this.#engine.state, { playbackInitiated: true });
|
|
74
|
+
return mediaElement.play().catch((err) => {
|
|
75
|
+
if (this.src) return new Promise((resolve, reject) => {
|
|
76
|
+
const listener = () => {
|
|
77
|
+
this.#loadstartListener = null;
|
|
78
|
+
mediaElement.play().then(resolve, reject);
|
|
79
|
+
};
|
|
80
|
+
this.#loadstartListener = listener;
|
|
81
|
+
mediaElement.addEventListener("loadstart", listener, { once: true });
|
|
82
|
+
});
|
|
83
|
+
throw err;
|
|
81
84
|
});
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
this.#loadstartListener = null;
|
|
85
|
+
}
|
|
86
|
+
#cancelPendingPlay() {
|
|
87
|
+
if (!this.#loadstartListener) return;
|
|
88
|
+
const { mediaElement } = this.#engine.owners.get();
|
|
89
|
+
mediaElement?.removeEventListener("loadstart", this.#loadstartListener);
|
|
90
|
+
this.#loadstartListener = null;
|
|
91
|
+
}
|
|
90
92
|
}
|
|
91
|
-
|
|
93
|
+
return SpfMediaImpl;
|
|
94
|
+
}
|
|
95
|
+
/** Standalone SPF media adapter with no base class. */
|
|
96
|
+
var SpfMedia = class extends SpfMediaMixin(class {}) {};
|
|
92
97
|
//#endregion
|
|
93
|
-
export { SpfMedia };
|
|
98
|
+
export { SpfMedia, SpfMediaMixin };
|
|
94
99
|
|
|
95
100
|
//# sourceMappingURL=adapter.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adapter.js","names":["#config","#engine","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"sourcesContent":["import { update } from '../../core/signals/primitives';\nimport type { PlaybackEngineConfig } from './engine';\nimport { createPlaybackEngine, type PlaybackEngine } from './engine';\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"adapter.js","names":["#config","#engine","#cancelPendingPlay","#preload","#loadstartListener"],"sources":["../../../../src/dom/playback-engine/adapter.ts"],"sourcesContent":["import type { Constructor, MixinReturn } from '@videojs/utils/types';\nimport { update } from '../../core/signals/primitives';\nimport type { PlaybackEngineConfig } from './engine';\nimport { createPlaybackEngine, type PlaybackEngine } from './engine';\n\nexport interface SpfMediaAPI {\n readonly engine: PlaybackEngine;\n src: string;\n preload: '' | 'none' | 'metadata' | 'auto';\n attach(mediaElement: HTMLMediaElement): void;\n detach(): void;\n destroy(): void;\n play(): Promise<void>;\n}\n\n/**\n * Mixin that adds SPF playback engine behavior to any base class.\n *\n * Implements the src/play() contract per the WHATWG HTML spec so that SPF can\n * be used anywhere a media element API is expected.\n *\n * A new engine is created on every src assignment — this fully tears down all\n * state, SourceBuffers, and in-flight requests from the previous source before\n * the next one begins. The media element reference is preserved across src\n * changes and re-applied to the new engine automatically.\n *\n * @example\n * class SimpleHlsMedia extends SpfMediaMixin(HTMLVideoElementHost) {}\n *\n * const media = new SimpleHlsMedia();\n * media.attach(document.querySelector('video'));\n * media.src = 'https://stream.mux.com/abc123.m3u8';\n */\nexport function SpfMediaMixin<Base extends Constructor<any>>(BaseClass: Base) {\n class SpfMediaImpl extends BaseClass {\n #engine: PlaybackEngine;\n #config: PlaybackEngineConfig;\n #preload: '' | 'none' | 'metadata' | 'auto' = '';\n\n /** Pending loadstart listener from a deferred play() retry, if any. */\n #loadstartListener: (() => void) | null = null;\n\n constructor(...args: any[]) {\n super(...args);\n\n const { config } = args?.[0] ?? {};\n this.#config = config;\n this.#engine = createPlaybackEngine(config);\n }\n\n get engine(): PlaybackEngine {\n return this.#engine;\n }\n\n // -------------------------------------------------------------------------\n // Media element lifecycle\n // -------------------------------------------------------------------------\n\n attach(mediaElement: HTMLMediaElement): void {\n super.attach?.(mediaElement);\n update(this.#engine.owners, { mediaElement });\n }\n\n detach(): void {\n this.#cancelPendingPlay();\n update(this.#engine.owners, { mediaElement: undefined });\n super.detach?.();\n }\n\n destroy(): void {\n this.#cancelPendingPlay();\n this.#engine.destroy();\n }\n\n // -------------------------------------------------------------------------\n // preload — synchronous IDL attribute (WHATWG §4.8.11.2)\n // -------------------------------------------------------------------------\n\n get preload(): '' | 'none' | 'metadata' | 'auto' {\n return this.#preload;\n }\n\n set preload(value: '' | 'none' | 'metadata' | 'auto') {\n this.#preload = value;\n if (value) {\n update(this.#engine.state, { preload: value });\n }\n // value = '' clears #preload (so the next engine recreation won't re-apply\n // an explicit value) but does not patch current state — the existing preload\n // stays in effect until the next src change creates a fresh engine.\n }\n\n // -------------------------------------------------------------------------\n // src — synchronous IDL attribute (WHATWG §4.8.11.2)\n // Each assignment destroys the current engine and starts a fresh one, exactly\n // as the browser's load algorithm resets all media element state on src change.\n // -------------------------------------------------------------------------\n\n get src(): string {\n return this.#engine.state.get().presentation?.url ?? '';\n }\n\n set src(value: string) {\n const prevMediaElement = this.#engine.owners.get().mediaElement;\n\n this.#cancelPendingPlay();\n this.#engine.destroy();\n this.#engine = createPlaybackEngine(this.#config);\n\n // Apply explicit preload before setting owners so syncPreloadAttribute skips\n // element inference and the explicit value is preserved across src changes.\n if (this.#preload) {\n update(this.#engine.state, { preload: this.#preload });\n }\n\n if (prevMediaElement) {\n update(this.#engine.owners, { mediaElement: prevMediaElement });\n }\n\n if (value) {\n update(this.#engine.state, { presentation: { url: value } });\n }\n }\n\n // -------------------------------------------------------------------------\n // play() — WHATWG §4.8.11.8\n // Delegates to the attached media element's native play().\n // -------------------------------------------------------------------------\n\n play(): Promise<void> {\n const { mediaElement } = this.#engine.owners.get();\n if (!mediaElement) {\n return Promise.reject(new Error('SpfMedia: no media element attached'));\n }\n\n // Signal play intent — enables loading even with preload=\"none\"\n update(this.#engine.state, { playbackInitiated: true });\n\n return mediaElement.play().catch((err: unknown) => {\n // If we have a pending HLS source, the rejection may be because MSE\n // hasn't attached a blob URL yet. Wait for loadstart (src assigned\n // by MSE setup) and retry once.\n if (this.src) {\n return new Promise<void>((resolve, reject) => {\n const listener = () => {\n this.#loadstartListener = null;\n mediaElement.play().then(resolve, reject);\n };\n this.#loadstartListener = listener;\n mediaElement.addEventListener('loadstart', listener, { once: true });\n });\n }\n throw err;\n });\n }\n\n // -------------------------------------------------------------------------\n // Private\n // -------------------------------------------------------------------------\n\n #cancelPendingPlay(): void {\n if (!this.#loadstartListener) return;\n const { mediaElement } = this.#engine.owners.get();\n mediaElement?.removeEventListener('loadstart', this.#loadstartListener);\n this.#loadstartListener = null;\n }\n }\n\n return SpfMediaImpl as unknown as MixinReturn<Base, SpfMediaAPI>;\n}\n\n/** Standalone SPF media adapter with no base class. */\nexport class SpfMedia extends SpfMediaMixin(class {}) {}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,cAA6C,WAAiB;CAC5E,MAAM,qBAAqB,UAAU;EACnC;EACA;EACA,WAA8C;;EAG9C,qBAA0C;EAE1C,YAAY,GAAG,MAAa;AAC1B,SAAM,GAAG,KAAK;GAEd,MAAM,EAAE,WAAW,OAAO,MAAM,EAAE;AAClC,SAAA,SAAe;AACf,SAAA,SAAe,qBAAqB,OAAO;;EAG7C,IAAI,SAAyB;AAC3B,UAAO,MAAA;;EAOT,OAAO,cAAsC;AAC3C,SAAM,SAAS,aAAa;AAC5B,UAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,CAAC;;EAG/C,SAAe;AACb,SAAA,mBAAyB;AACzB,UAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,KAAA,GAAW,CAAC;AACxD,SAAM,UAAU;;EAGlB,UAAgB;AACd,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;;EAOxB,IAAI,UAA6C;AAC/C,UAAO,MAAA;;EAGT,IAAI,QAAQ,OAA0C;AACpD,SAAA,UAAgB;AAChB,OAAI,MACF,QAAO,MAAA,OAAa,OAAO,EAAE,SAAS,OAAO,CAAC;;EAalD,IAAI,MAAc;AAChB,UAAO,MAAA,OAAa,MAAM,KAAK,CAAC,cAAc,OAAO;;EAGvD,IAAI,IAAI,OAAe;GACrB,MAAM,mBAAmB,MAAA,OAAa,OAAO,KAAK,CAAC;AAEnD,SAAA,mBAAyB;AACzB,SAAA,OAAa,SAAS;AACtB,SAAA,SAAe,qBAAqB,MAAA,OAAa;AAIjD,OAAI,MAAA,QACF,QAAO,MAAA,OAAa,OAAO,EAAE,SAAS,MAAA,SAAe,CAAC;AAGxD,OAAI,iBACF,QAAO,MAAA,OAAa,QAAQ,EAAE,cAAc,kBAAkB,CAAC;AAGjE,OAAI,MACF,QAAO,MAAA,OAAa,OAAO,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,CAAC;;EAShE,OAAsB;GACpB,MAAM,EAAE,iBAAiB,MAAA,OAAa,OAAO,KAAK;AAClD,OAAI,CAAC,aACH,QAAO,QAAQ,uBAAO,IAAI,MAAM,sCAAsC,CAAC;AAIzE,UAAO,MAAA,OAAa,OAAO,EAAE,mBAAmB,MAAM,CAAC;AAEvD,UAAO,aAAa,MAAM,CAAC,OAAO,QAAiB;AAIjD,QAAI,KAAK,IACP,QAAO,IAAI,SAAe,SAAS,WAAW;KAC5C,MAAM,iBAAiB;AACrB,YAAA,oBAA0B;AAC1B,mBAAa,MAAM,CAAC,KAAK,SAAS,OAAO;;AAE3C,WAAA,oBAA0B;AAC1B,kBAAa,iBAAiB,aAAa,UAAU,EAAE,MAAM,MAAM,CAAC;MACpE;AAEJ,UAAM;KACN;;EAOJ,qBAA2B;AACzB,OAAI,CAAC,MAAA,kBAAyB;GAC9B,MAAM,EAAE,iBAAiB,MAAA,OAAa,OAAO,KAAK;AAClD,iBAAc,oBAAoB,aAAa,MAAA,kBAAwB;AACvE,SAAA,oBAA0B;;;AAI9B,QAAO;;;AAIT,IAAa,WAAb,cAA8B,cAAc,MAAM,GAAG,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { PlaybackEngine, PlaybackEngineConfig, PlaybackEngineOwners, PlaybackEngineState, createPlaybackEngine } from "./playback-engine/engine.js";
|
|
2
|
-
import { SpfMedia } from "./playback-engine/adapter.js";
|
|
2
|
+
import { SpfMedia, SpfMediaAPI, SpfMediaMixin } from "./playback-engine/adapter.js";
|
|
3
3
|
import { effect } from "../core/signals/effect.js";
|
|
4
|
-
export { type PlaybackEngine, type PlaybackEngineConfig, type PlaybackEngineOwners, type PlaybackEngineState, SpfMedia, createPlaybackEngine, effect };
|
|
4
|
+
export { type PlaybackEngine, type PlaybackEngineConfig, type PlaybackEngineOwners, type PlaybackEngineState, SpfMedia, type SpfMediaAPI, SpfMediaMixin, createPlaybackEngine, effect };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { effect } from "../core/signals/effect.js";
|
|
2
2
|
import { createPlaybackEngine } from "./playback-engine/engine.js";
|
|
3
|
-
import { SpfMedia } from "./playback-engine/adapter.js";
|
|
4
|
-
export { SpfMedia, createPlaybackEngine, effect };
|
|
3
|
+
import { SpfMedia, SpfMediaMixin } from "./playback-engine/adapter.js";
|
|
4
|
+
export { SpfMedia, SpfMediaMixin, createPlaybackEngine, effect };
|
package/dist/dev/dom.d.ts
CHANGED
|
@@ -5,6 +5,6 @@ import { CurrentTimeOwners, CurrentTimeState, trackCurrentTime } from "./dom/fea
|
|
|
5
5
|
import { PlaybackInitiatedOwners, PlaybackInitiatedState, trackPlaybackInitiated } from "./dom/features/track-playback-initiated.js";
|
|
6
6
|
import { PlaybackRateOwners, PlaybackRateState, trackPlaybackRate } from "./dom/features/track-playback-rate.js";
|
|
7
7
|
import { flushBuffer } from "./dom/media/buffer-flusher.js";
|
|
8
|
-
import { SpfMedia } from "./dom/playback-engine/adapter.js";
|
|
8
|
+
import { SpfMedia, SpfMediaAPI, SpfMediaMixin } from "./dom/playback-engine/adapter.js";
|
|
9
9
|
import { destroyVttParser, parseVttSegment } from "./dom/text/parse-vtt-segment.js";
|
|
10
|
-
export { type CurrentTimeOwners, type CurrentTimeState, type PlaybackInitiatedOwners, type PlaybackInitiatedState, type PlaybackRateOwners, type PlaybackRateState, SpfMedia, appendSegment, destroyVttParser, flushBuffer, loadSegments, loadTextTrackCues, parseVttSegment, trackCurrentTime, trackPlaybackInitiated, trackPlaybackRate };
|
|
10
|
+
export { type CurrentTimeOwners, type CurrentTimeState, type PlaybackInitiatedOwners, type PlaybackInitiatedState, type PlaybackRateOwners, type PlaybackRateState, SpfMedia, type SpfMediaAPI, SpfMediaMixin, appendSegment, destroyVttParser, flushBuffer, loadSegments, loadTextTrackCues, parseVttSegment, trackCurrentTime, trackPlaybackInitiated, trackPlaybackRate };
|
package/dist/dev/dom.js
CHANGED
|
@@ -6,5 +6,5 @@ import { trackPlaybackInitiated } from "./dom/features/track-playback-initiated.
|
|
|
6
6
|
import { trackPlaybackRate } from "./dom/features/track-playback-rate.js";
|
|
7
7
|
import { appendSegment } from "./dom/media/append-segment.js";
|
|
8
8
|
import { flushBuffer } from "./dom/media/buffer-flusher.js";
|
|
9
|
-
import { SpfMedia } from "./dom/playback-engine/adapter.js";
|
|
10
|
-
export { SpfMedia, appendSegment, destroyVttParser, flushBuffer, loadSegments, loadTextTrackCues, parseVttSegment, trackCurrentTime, trackPlaybackInitiated, trackPlaybackRate };
|
|
9
|
+
import { SpfMedia, SpfMediaMixin } from "./dom/playback-engine/adapter.js";
|
|
10
|
+
export { SpfMedia, SpfMediaMixin, appendSegment, destroyVttParser, flushBuffer, loadSegments, loadTextTrackCues, parseVttSegment, trackCurrentTime, trackPlaybackInitiated, trackPlaybackRate };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@videojs/spf",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "10.0.0-beta.
|
|
4
|
+
"version": "10.0.0-beta.21",
|
|
5
5
|
"description": "Stream Processing Framework for Video.js 10",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
],
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"signal-polyfill": "^0.2.2",
|
|
45
|
-
"@videojs/utils": "10.0.0-beta.
|
|
45
|
+
"@videojs/utils": "10.0.0-beta.21"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@vitest/browser": "^4.1.0",
|