@ngxsp/core 0.0.1-next.0

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/index.cjs ADDED
@@ -0,0 +1,1355 @@
1
+ 'use strict';
2
+
3
+ var rxjs = require('rxjs');
4
+
5
+ // src/facade/player-facade.ts
6
+
7
+ // src/engine/engine-factory.ts
8
+ function selectEngine(config, factories, caps) {
9
+ const candidates = (factories ?? []).filter((f) => {
10
+ try {
11
+ if (caps && typeof f.supportsWithCaps === "function") return !!f.supportsWithCaps(config, caps);
12
+ return !!f.supports(config);
13
+ } catch {
14
+ return false;
15
+ }
16
+ });
17
+ if (candidates.length === 0) return null;
18
+ candidates.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
19
+ return candidates[0];
20
+ }
21
+
22
+ // src/engine/native.engine.ts
23
+ function mimeFromUrl(url) {
24
+ try {
25
+ const clean = url.split("?")[0].split("#")[0];
26
+ const parts = clean.split(".");
27
+ const ext = (parts.length > 1 ? parts.pop() : "")?.toLowerCase() || "";
28
+ switch (ext) {
29
+ case "mp4":
30
+ return "video/mp4";
31
+ case "m4v":
32
+ return "video/x-m4v";
33
+ case "mov":
34
+ return "video/quicktime";
35
+ case "webm":
36
+ return "video/webm";
37
+ case "mkv":
38
+ return "video/x-matroska";
39
+ case "avi":
40
+ return "video/x-msvideo";
41
+ case "mpg":
42
+ case "mpeg":
43
+ return "video/mpeg";
44
+ case "ts":
45
+ return "video/mp2t";
46
+ case "ogv":
47
+ return "video/ogg";
48
+ default:
49
+ return "";
50
+ }
51
+ } catch {
52
+ return "";
53
+ }
54
+ }
55
+ var NativeEngine = class {
56
+ id = "native";
57
+ video;
58
+ listeners = [];
59
+ async attach(videoEl) {
60
+ this.video = videoEl;
61
+ const v = videoEl;
62
+ const emit = (e) => this.listeners.forEach((l) => l(e));
63
+ v.addEventListener("timeupdate", () => emit({ type: "timeupdate", currentTime: v.currentTime, duration: v.duration || 0 }));
64
+ v.addEventListener("playing", () => emit({ type: "state", state: "playing" }));
65
+ v.addEventListener("pause", () => emit({ type: "state", state: "paused" }));
66
+ v.addEventListener("waiting", () => emit({ type: "state", state: "buffering" }));
67
+ v.addEventListener("ended", () => emit({ type: "state", state: "ended" }));
68
+ v.addEventListener("error", () => {
69
+ const err = v.error;
70
+ emit({
71
+ type: "error",
72
+ error: { code: `HTML5_${err?.code ?? "UNKNOWN"}`, message: err?.message ?? "HTML5 video error", fatal: true, details: err }
73
+ });
74
+ });
75
+ }
76
+ async load(source, opts) {
77
+ if (!this.video) throw new Error("NativeEngine not attached");
78
+ const v = this.video;
79
+ if (source.type === "local-file") {
80
+ if (!source.file) throw new Error("source.file is required");
81
+ v.src = URL.createObjectURL(source.file);
82
+ } else {
83
+ if (!source.url) throw new Error("source.url is required");
84
+ const type = mimeFromUrl(source.url);
85
+ while (v.firstChild) v.removeChild(v.firstChild);
86
+ if (type) {
87
+ const s = document.createElement("source");
88
+ s.src = source.url;
89
+ s.type = type;
90
+ v.appendChild(s);
91
+ v.removeAttribute("src");
92
+ } else {
93
+ v.src = source.url;
94
+ }
95
+ }
96
+ await v.load();
97
+ if (opts?.startTimeSec != null && isFinite(opts.startTimeSec)) {
98
+ v.currentTime = Math.max(0, opts.startTimeSec);
99
+ }
100
+ }
101
+ async destroy() {
102
+ if (!this.video) return;
103
+ this.video.removeAttribute("src");
104
+ this.video.load();
105
+ this.video = void 0;
106
+ this.listeners = [];
107
+ }
108
+ async play() {
109
+ await this.video?.play();
110
+ }
111
+ async pause() {
112
+ this.video?.pause();
113
+ }
114
+ async seekTo(timeSec) {
115
+ if (this.video) this.video.currentTime = Math.max(0, timeSec);
116
+ }
117
+ async setPlaybackRate(rate) {
118
+ if (this.video) this.video.playbackRate = rate;
119
+ }
120
+ async setVolume(volume) {
121
+ if (this.video) this.video.volume = Math.min(1, Math.max(0, volume));
122
+ }
123
+ async setTextTrack(_trackId) {
124
+ }
125
+ async setAudioTrack(_trackId) {
126
+ }
127
+ async setVideoTrack(_trackId) {
128
+ }
129
+ async getTracks() {
130
+ return [];
131
+ }
132
+ onEvent(cb) {
133
+ this.listeners.push(cb);
134
+ return () => {
135
+ this.listeners = this.listeners.filter((x) => x !== cb);
136
+ };
137
+ }
138
+ };
139
+
140
+ // src/engine/native.factory.ts
141
+ function createNativeEngineFactory() {
142
+ return {
143
+ id: "native",
144
+ priority: 0,
145
+ supports: (cfg) => {
146
+ const drm = cfg.drm ?? cfg.source?.drm;
147
+ if (drm?.required) return false;
148
+ const t = cfg.source.type;
149
+ if (t === "url" || t === "local-file") return true;
150
+ return false;
151
+ },
152
+ create: () => new NativeEngine()
153
+ };
154
+ }
155
+
156
+ // src/engine/engine-profile.ts
157
+ function getEngineProfileHints(profile) {
158
+ switch (profile) {
159
+ case "tv":
160
+ return {
161
+ abr: {
162
+ // Prefer higher quality by default.
163
+ maxHeight: 2160
164
+ },
165
+ shakaStreaming: {
166
+ bufferingGoalSec: 30,
167
+ rebufferingGoalSec: 2
168
+ }
169
+ };
170
+ case "mobile":
171
+ return {
172
+ abr: {
173
+ // Reasonable default for mobile data/CPU.
174
+ maxHeight: 720
175
+ },
176
+ shakaStreaming: {
177
+ bufferingGoalSec: 15,
178
+ rebufferingGoalSec: 2
179
+ }
180
+ };
181
+ case "low-latency":
182
+ return {
183
+ abr: {
184
+ // Avoid very high res by default (keeps latency predictable).
185
+ maxHeight: 1080
186
+ },
187
+ shakaStreaming: {
188
+ lowLatencyMode: true,
189
+ bufferingGoalSec: 6,
190
+ rebufferingGoalSec: 1
191
+ }
192
+ };
193
+ case "default":
194
+ default:
195
+ return {
196
+ shakaStreaming: {
197
+ bufferingGoalSec: 20,
198
+ rebufferingGoalSec: 2
199
+ }
200
+ };
201
+ }
202
+ }
203
+ function applyEngineProfileToSource(source, profile) {
204
+ const p = profile ?? "default";
205
+ const hints = getEngineProfileHints(p);
206
+ const anySrc = source;
207
+ const abrIn = anySrc.abr ?? {};
208
+ const abrOut = { ...abrIn };
209
+ if (abrOut.maxHeight == null && hints.abr?.maxHeight != null) abrOut.maxHeight = hints.abr.maxHeight;
210
+ if (abrOut.maxBandwidth == null && hints.abr?.maxBandwidth != null) abrOut.maxBandwidth = hints.abr.maxBandwidth;
211
+ const out = {
212
+ ...anySrc,
213
+ // Expose profile to engines (optional).
214
+ profile: p
215
+ };
216
+ if (Object.keys(abrOut).length) out.abr = abrOut;
217
+ if (hints.shakaStreaming) {
218
+ out._shakaStreaming = {
219
+ ...anySrc._shakaStreaming ?? {},
220
+ ...hints.shakaStreaming
221
+ };
222
+ }
223
+ return out;
224
+ }
225
+
226
+ // src/engine/playback-capabilities.ts
227
+ function createDefaultCapabilities() {
228
+ return {
229
+ mse: false,
230
+ eme: false,
231
+ mediaCapabilities: false,
232
+ hls: false,
233
+ dash: false,
234
+ progressive: true,
235
+ codecs: { mp4_h264: false, mp4_hevc: false, webm_vp9: false, webm_av1: false },
236
+ drm: { widevine: false, playready: false, fairplay: false }
237
+ };
238
+ }
239
+ async function detectPlaybackCapabilities(videoEl) {
240
+ const caps = createDefaultCapabilities();
241
+ const w = globalThis;
242
+ caps.mse = !!w.MediaSource && typeof w.MediaSource.isTypeSupported === "function";
243
+ caps.eme = !!(w.navigator && w.navigator.requestMediaKeySystemAccess);
244
+ caps.mediaCapabilities = !!(w.navigator && w.navigator.mediaCapabilities);
245
+ const v = videoEl ?? (typeof document !== "undefined" ? document.createElement("video") : void 0);
246
+ const canPlay = (mime) => {
247
+ try {
248
+ if (caps.mse && w.MediaSource?.isTypeSupported) return !!w.MediaSource.isTypeSupported(mime);
249
+ if (v && typeof v.canPlayType === "function") return v.canPlayType(mime) !== "";
250
+ return false;
251
+ } catch {
252
+ return false;
253
+ }
254
+ };
255
+ caps.codecs.mp4_h264 = canPlay('video/mp4; codecs="avc1.42E01E"');
256
+ caps.codecs.mp4_hevc = canPlay('video/mp4; codecs="hvc1.1.6.L93.B0"');
257
+ caps.codecs.webm_vp9 = canPlay('video/webm; codecs="vp09.00.10.08"');
258
+ caps.codecs.webm_av1 = canPlay('video/webm; codecs="av01.0.05M.08"');
259
+ caps.dash = caps.mse;
260
+ caps.hls = canPlay("application/vnd.apple.mpegurl") || canPlay("application/x-mpegURL") || caps.mse;
261
+ caps.progressive = caps.codecs.mp4_h264 || caps.codecs.webm_vp9 || caps.codecs.webm_av1 || true;
262
+ if (caps.eme && w.navigator?.requestMediaKeySystemAccess) {
263
+ const probe = async (ks) => {
264
+ try {
265
+ const cfg = [{
266
+ initDataTypes: ["cenc"],
267
+ audioCapabilities: [{ contentType: 'audio/mp4; codecs="mp4a.40.2"' }],
268
+ videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.42E01E"' }]
269
+ }];
270
+ await w.navigator.requestMediaKeySystemAccess(ks, cfg);
271
+ return true;
272
+ } catch {
273
+ return false;
274
+ }
275
+ };
276
+ caps.drm.widevine = await probe("com.widevine.alpha");
277
+ caps.drm.playready = await probe("com.microsoft.playready");
278
+ caps.drm.fairplay = await probe("com.apple.fps.1_0");
279
+ }
280
+ return caps;
281
+ }
282
+
283
+ // src/engine/capability-report.ts
284
+ async function createMediaDecodingReport() {
285
+ const mc = globalThis.navigator?.mediaCapabilities;
286
+ if (!mc?.decodingInfo) return void 0;
287
+ const mk = (contentType, width, height, bitrate) => ({
288
+ type: "media-source",
289
+ video: { contentType, width, height, bitrate, framerate: 30 },
290
+ audio: { contentType: 'audio/mp4; codecs="mp4a.40.2"', channels: 2, bitrate: 128e3, samplerate: 48e3 }
291
+ });
292
+ const probes = [
293
+ ["h264_720p", mk('video/mp4; codecs="avc1.64001f"', 1280, 720, 25e5)],
294
+ ["h264_1080p", mk('video/mp4; codecs="avc1.640028"', 1920, 1080, 5e6)],
295
+ ["vp9_1080p", mk('video/webm; codecs="vp09.00.51.08"', 1920, 1080, 5e6)]
296
+ ];
297
+ const out = {};
298
+ for (const [key, cfg] of probes) {
299
+ try {
300
+ const info = await mc.decodingInfo(cfg);
301
+ out[key] = {
302
+ supported: !!info?.supported,
303
+ smooth: info?.smooth,
304
+ powerEfficient: info?.powerEfficient
305
+ };
306
+ } catch {
307
+ }
308
+ }
309
+ return Object.keys(out).length ? out : void 0;
310
+ }
311
+ function safeUa() {
312
+ try {
313
+ return globalThis.navigator?.userAgent;
314
+ } catch {
315
+ return void 0;
316
+ }
317
+ }
318
+ function canPlayType(videoEl, mime) {
319
+ try {
320
+ const v = videoEl ?? (typeof document !== "undefined" ? document.createElement("video") : void 0);
321
+ if (!v || typeof v.canPlayType !== "function") return false;
322
+ return v.canPlayType(mime) !== "";
323
+ } catch {
324
+ return false;
325
+ }
326
+ }
327
+ function createCapabilityReport(videoEl, cfg, mediaDecoding) {
328
+ const w = globalThis;
329
+ const notes = [];
330
+ const mse = !!w.MediaSource && typeof w.MediaSource.isTypeSupported === "function";
331
+ const eme = !!(w.navigator && w.navigator.requestMediaKeySystemAccess);
332
+ const mediaCapabilities = !!(w.navigator && w.navigator.mediaCapabilities);
333
+ if (!mse) notes.push("MediaSource (MSE) not available");
334
+ if (!eme) notes.push("Encrypted Media Extensions (EME) not available");
335
+ const canPlay = {
336
+ 'video/mp4; codecs="avc1.42E01E"': canPlayType(videoEl, 'video/mp4; codecs="avc1.42E01E"'),
337
+ 'video/mp4; codecs="hvc1.1.6.L93.B0"': canPlayType(videoEl, 'video/mp4; codecs="hvc1.1.6.L93.B0"'),
338
+ 'video/webm; codecs="vp09.00.10.08"': canPlayType(videoEl, 'video/webm; codecs="vp09.00.10.08"'),
339
+ 'video/webm; codecs="av01.0.05M.08"': canPlayType(videoEl, 'video/webm; codecs="av01.0.05M.08"'),
340
+ "application/vnd.apple.mpegurl": canPlayType(videoEl, "application/vnd.apple.mpegurl"),
341
+ "application/dash+xml": canPlayType(videoEl, "application/dash+xml")
342
+ };
343
+ const secure = (() => {
344
+ try {
345
+ return !!globalThis.isSecureContext;
346
+ } catch {
347
+ return false;
348
+ }
349
+ })();
350
+ if (!secure) notes.push("Not running in a secure context (HTTPS). DRM and some APIs may fail.");
351
+ const src = cfg?.source;
352
+ const drmCfg = cfg ? cfg.drm ?? src?.drm : void 0;
353
+ const drmFlag = !!drmCfg;
354
+ return {
355
+ timestamp: Date.now(),
356
+ userAgent: safeUa(),
357
+ source: src ? {
358
+ type: src.type,
359
+ live: src.live,
360
+ drm: drmFlag,
361
+ url: src.url,
362
+ contentId: src.contentId
363
+ } : void 0,
364
+ platform: { isSecureContext: secure },
365
+ features: { mse, eme, mediaCapabilities },
366
+ canPlay,
367
+ mediaDecoding,
368
+ notes
369
+ };
370
+ }
371
+
372
+ // src/remote-config/remote-config.ts
373
+ var DEFAULT_ALLOW = [
374
+ "profile",
375
+ "abr",
376
+ "resilience",
377
+ "drm",
378
+ "ads",
379
+ "ui",
380
+ "subtitles",
381
+ "metadata",
382
+ "progress",
383
+ "chapters",
384
+ "offline"
385
+ ];
386
+ function isPlainObject(v) {
387
+ return !!v && typeof v === "object" && !Array.isArray(v);
388
+ }
389
+ function deepMerge(base, patch) {
390
+ const out = { ...base };
391
+ for (const [k, v] of Object.entries(patch)) {
392
+ if (isPlainObject(v) && isPlainObject(out[k])) out[k] = deepMerge(out[k], v);
393
+ else out[k] = v;
394
+ }
395
+ return out;
396
+ }
397
+ function shallowMerge(base, patch) {
398
+ return { ...base, ...patch };
399
+ }
400
+ function hashKey(url) {
401
+ let h = 0;
402
+ for (let i = 0; i < url.length; i++) h = h * 31 + url.charCodeAt(i) >>> 0;
403
+ return h.toString(16);
404
+ }
405
+ async function readCache(storage, key) {
406
+ if (!storage?.get) return null;
407
+ const raw = await storage.get(key);
408
+ if (!raw) return null;
409
+ try {
410
+ const parsed = JSON.parse(raw);
411
+ if (!parsed || typeof parsed.ts !== "number" || !isPlainObject(parsed.data)) return null;
412
+ return parsed;
413
+ } catch {
414
+ return null;
415
+ }
416
+ }
417
+ async function writeCache(storage, key, entry) {
418
+ if (!storage?.set) return;
419
+ await storage.set(key, JSON.stringify(entry));
420
+ }
421
+ async function applyRemoteConfig(cfg, ports) {
422
+ const rc = cfg.remoteConfig;
423
+ if (!rc?.enabled) return { config: cfg, applied: false, source: "none" };
424
+ const url = rc.url;
425
+ const ttlMs = Math.max(1, rc.ttlSec ?? 300) * 1e3;
426
+ const mode = rc.mode ?? "deep";
427
+ const allow = rc.allow?.length ? rc.allow : DEFAULT_ALLOW;
428
+ const cacheKey = `remoteConfig:${hashKey(url)}`;
429
+ const now = Date.now();
430
+ try {
431
+ const cached = await readCache(ports.storage, cacheKey);
432
+ if (cached && now - cached.ts < ttlMs) {
433
+ const patch2 = filterAllowList(cached.data, allow);
434
+ const merged2 = mode === "shallow" ? shallowMerge(cfg, patch2) : deepMerge(cfg, patch2);
435
+ return { config: merged2, applied: true, payload: cached.data, source: "cache" };
436
+ }
437
+ const fetch2 = ports.http?.fetch;
438
+ if (!fetch2) throw new Error("Remote config enabled but no ports.http.fetch provided");
439
+ const res = await fetch2(url, { method: "GET", headers: { "accept": "application/json" } });
440
+ if (!res.ok) throw new Error(`Remote config HTTP ${res.status}`);
441
+ const json = await res.json();
442
+ if (!isPlainObject(json)) throw new Error("Remote config payload must be a JSON object");
443
+ const payload = json;
444
+ await writeCache(ports.storage, cacheKey, { ts: now, data: payload });
445
+ const patch = filterAllowList(payload, allow);
446
+ const merged = mode === "shallow" ? shallowMerge(cfg, patch) : deepMerge(cfg, patch);
447
+ return { config: merged, applied: true, payload, source: "network" };
448
+ } catch (err) {
449
+ ports.log?.warn?.("[ngxsp] remoteConfig failed", err);
450
+ return { config: cfg, applied: false, source: "none" };
451
+ }
452
+ }
453
+ function filterAllowList(payload, allow) {
454
+ const out = {};
455
+ for (const k of allow) {
456
+ if (Object.prototype.hasOwnProperty.call(payload, k)) out[k] = payload[k];
457
+ }
458
+ return out;
459
+ }
460
+
461
+ // src/ports/storage.port.ts
462
+ var MemoryStoragePort = class {
463
+ map = /* @__PURE__ */ new Map();
464
+ async get(key) {
465
+ return this.map.get(key) ?? null;
466
+ }
467
+ async set(key, value) {
468
+ this.map.set(key, value);
469
+ }
470
+ async remove(key) {
471
+ this.map.delete(key);
472
+ }
473
+ };
474
+
475
+ // src/ports/telemetry.port.ts
476
+ var NoopTelemetryPort = class {
477
+ add(_sample) {
478
+ }
479
+ };
480
+
481
+ // src/ports/log.port.ts
482
+ var ConsoleLogPort = class {
483
+ debug(...args) {
484
+ console.debug(...args);
485
+ }
486
+ info(...args) {
487
+ console.info(...args);
488
+ }
489
+ warn(...args) {
490
+ console.warn(...args);
491
+ }
492
+ error(...args) {
493
+ console.error(...args);
494
+ }
495
+ };
496
+
497
+ // src/ports/ports.ts
498
+ function createDefaultPorts() {
499
+ const http = { fetch: (input, init) => fetch(input, init) };
500
+ return {
501
+ http,
502
+ storage: new MemoryStoragePort(),
503
+ telemetry: new NoopTelemetryPort(),
504
+ log: new ConsoleLogPort()
505
+ };
506
+ }
507
+
508
+ // src/facade/player-facade.ts
509
+ var PlayerFacade = class {
510
+ constructor(chooseEngine = defaultEngineChooser, ports = createDefaultPorts()) {
511
+ this.chooseEngine = chooseEngine;
512
+ this.ports = ports;
513
+ }
514
+ engineFactories = [createNativeEngineFactory()];
515
+ engineId = "native";
516
+ lastNonFatal = null;
517
+ // Optional MediaCapabilities decoding probes (async, best-effort)
518
+ mediaDecodingSub = new rxjs.BehaviorSubject(null);
519
+ video;
520
+ engine;
521
+ plugins = [];
522
+ ctx;
523
+ stateSub = new rxjs.BehaviorSubject({ kind: "idle" });
524
+ timeSub = new rxjs.BehaviorSubject({ currentTime: 0, duration: 0 });
525
+ bufferSub = new rxjs.BehaviorSubject(0);
526
+ tracksSub = new rxjs.BehaviorSubject([]);
527
+ errorSub = new rxjs.Subject();
528
+ metadataSub = new rxjs.BehaviorSubject(null);
529
+ capsSub = new rxjs.BehaviorSubject(null);
530
+ capabilities$ = this.capsSub.asObservable();
531
+ state$ = this.stateSub.asObservable();
532
+ time$ = this.timeSub.asObservable();
533
+ bufferAheadSec$ = this.bufferSub.asObservable();
534
+ tracks$ = this.tracksSub.asObservable();
535
+ error$ = this.errorSub.asObservable();
536
+ metadata$ = this.metadataSub.asObservable();
537
+ seriesIndexSub = new rxjs.BehaviorSubject(null);
538
+ seriesIndex$ = this.seriesIndexSub.asObservable();
539
+ playlistIndexSub = new rxjs.BehaviorSubject(null);
540
+ playlistIndex$ = this.playlistIndexSub.asObservable();
541
+ playlist = [];
542
+ playlistKey = "";
543
+ externalVttUrl;
544
+ attach(videoEl, plugins = [], config, ports, engines) {
545
+ if (ports) {
546
+ this.ports = { ...this.ports, ...ports };
547
+ }
548
+ if (engines && engines.length) {
549
+ this.engineFactories = [createNativeEngineFactory(), ...engines];
550
+ } else {
551
+ this.engineFactories = [createNativeEngineFactory()];
552
+ }
553
+ this.video = videoEl;
554
+ this.plugins = plugins ?? [];
555
+ this.ctx = { config, facade: this, ports: this.ports, contentId: config.source.contentId };
556
+ void detectPlaybackCapabilities(videoEl).then((c) => this.capsSub.next(c)).catch(() => this.capsSub.next(null));
557
+ void createMediaDecodingReport().then((r) => this.mediaDecodingSub.next(r ?? null)).catch(() => this.mediaDecodingSub.next(null));
558
+ this.emitTelemetry("player:attach", "info");
559
+ this.emitTelemetry("player:source_set", "info", { type: config.source.type, live: config.source.live });
560
+ this.installVideoEventListeners();
561
+ for (const p of this.plugins) {
562
+ try {
563
+ void p.onInit?.(this.ctx);
564
+ } catch {
565
+ }
566
+ }
567
+ }
568
+ async load(config, opts) {
569
+ if (!this.video) throw new Error("PlayerFacade not attached");
570
+ if (!this.ctx) throw new Error("PlayerFacade context missing");
571
+ const rc = await applyRemoteConfig(config, this.ports);
572
+ if (rc.applied) {
573
+ this.ports.telemetry?.add?.({
574
+ event: "remote_config:applied",
575
+ level: "info",
576
+ ts: Date.now(),
577
+ data: { source: rc.source, url: config.remoteConfig?.url }
578
+ });
579
+ config = rc.config;
580
+ }
581
+ const list = config.playlist ?? [];
582
+ if (list.length > 0) {
583
+ const key = list.map((s) => s.contentId ?? s.url ?? s.filePath ?? "").join("|");
584
+ if (key !== this.playlistKey) {
585
+ this.playlistKey = key;
586
+ this.playlist = list;
587
+ const startIdx = config.playlistStartIndex ?? 0;
588
+ this.playlistIndexSub.next(Math.max(0, Math.min(startIdx, list.length - 1)));
589
+ }
590
+ if (opts?.playlistIndex != null) {
591
+ this.playlistIndexSub.next(Math.max(0, Math.min(opts.playlistIndex, list.length - 1)));
592
+ }
593
+ const idx = this.playlistIndexSub.value ?? 0;
594
+ const src = list[idx] ?? config.source;
595
+ config = { ...config, source: src, autoNext: config.autoNext ?? true };
596
+ this.emitTelemetry("player:playlist_index", "info", { index: idx, total: list.length });
597
+ }
598
+ this.ctx.config = config;
599
+ this.ctx.contentId = config.source.contentId;
600
+ this.emitTelemetry("player:load_start", "info");
601
+ try {
602
+ const now = globalThis.performance?.now?.() ?? Date.now();
603
+ this.qoe.loadStartMs = now;
604
+ this.qoe.firstFrameMs = 0;
605
+ this.qoe.startupMs = 0;
606
+ this.qoe.rebufferCount = 0;
607
+ this.qoe.rebufferMs = 0;
608
+ this.qoe.rebufferStartMs = 0;
609
+ this.qoe.firstFrameEmitted = false;
610
+ } catch {
611
+ }
612
+ for (const p of this.plugins) {
613
+ try {
614
+ await p.onLoadStart?.(this.ctx);
615
+ } catch {
616
+ }
617
+ }
618
+ if (this.engine) {
619
+ try {
620
+ await this.engine.destroy();
621
+ } catch {
622
+ }
623
+ this.engine = void 0;
624
+ }
625
+ const caps = this.capsSub.value ?? void 0;
626
+ const chosen = selectEngine(config, this.engineFactories, caps);
627
+ if (!chosen) {
628
+ const detail = { source: config.source, caps };
629
+ this.emitFatal(new Error(`No engine registered for this source: ${JSON.stringify(detail)}`), "ENGINE_MISSING");
630
+ throw new Error("ENGINE_MISSING");
631
+ }
632
+ const engine = chosen.create();
633
+ this.engine = engine;
634
+ this.engineId = engine.id ?? (engine.constructor?.name ?? "engine");
635
+ this.emitTelemetry("player:engine_selected", "info", { engineId: this.engineId });
636
+ await engine.attach(this.video);
637
+ const off = engine.onEvent((ev) => this.handleEngineEvent(ev));
638
+ this._offEngine = off;
639
+ const startTime = opts?.startTimeSec;
640
+ const sourceWithDrm = applyEngineProfileToSource(
641
+ {
642
+ ...config.source,
643
+ drm: config.drm ?? config.source.drm,
644
+ profile: config.profile ?? config.source.profile
645
+ },
646
+ config.profile ?? config.source.profile
647
+ );
648
+ try {
649
+ await engine.load(sourceWithDrm, { startTimeSec: startTime ?? void 0 });
650
+ this.stateSub.next({ kind: "paused" });
651
+ this.emitTelemetry("player:load_ok", "info");
652
+ } catch (err) {
653
+ this.emitTelemetry("player:load_fail", "error", {}, err?.message);
654
+ this.emitFatal(err, "UNKNOWN");
655
+ throw err;
656
+ }
657
+ for (const p of this.plugins) {
658
+ try {
659
+ await p.onLoaded?.(this.ctx);
660
+ } catch {
661
+ }
662
+ }
663
+ }
664
+ async reload(opts) {
665
+ if (!this.ctx) throw new Error("PlayerFacade context missing");
666
+ const snap = this.snapshot();
667
+ const start = opts?.startTimeSec ?? snap.currentTime ?? 0;
668
+ await this.load(this.ctx.config, { startTimeSec: start });
669
+ }
670
+ async next() {
671
+ if (!this.ctx) return;
672
+ const cfg = this.ctx.config;
673
+ const list = cfg.playlist ?? this.playlist;
674
+ if (!list || list.length === 0) return;
675
+ const idx = (this.playlistIndexSub.value ?? 0) + 1;
676
+ if (idx >= list.length) return;
677
+ await this.load(cfg, { playlistIndex: idx, startTimeSec: 0 });
678
+ }
679
+ async previous() {
680
+ if (!this.ctx) return;
681
+ const cfg = this.ctx.config;
682
+ const list = cfg.playlist ?? this.playlist;
683
+ if (!list || list.length === 0) return;
684
+ const idx = (this.playlistIndexSub.value ?? 0) - 1;
685
+ if (idx < 0) return;
686
+ await this.load(cfg, { playlistIndex: idx, startTimeSec: 0 });
687
+ }
688
+ async destroy() {
689
+ try {
690
+ for (const p of this.plugins) {
691
+ try {
692
+ await p.onDestroy?.(this.ctx);
693
+ } catch {
694
+ }
695
+ }
696
+ } finally {
697
+ try {
698
+ this._offEngine?.();
699
+ } catch {
700
+ }
701
+ try {
702
+ await this.engine?.destroy();
703
+ } catch {
704
+ }
705
+ this.engine = void 0;
706
+ this.video = void 0;
707
+ this.ctx = void 0;
708
+ this.plugins = [];
709
+ this.emitTelemetry("player:init", "debug", { destroyed: true });
710
+ }
711
+ }
712
+ /** Emit a non-fatal error (does not transition to fatal error state). */
713
+ emitNonFatal(err, code = "UNKNOWN") {
714
+ const pe = this.toPlayerError(err, false, code);
715
+ this.lastNonFatal = pe;
716
+ this.emitTelemetry("player:error_nonfatal", "warn", { code: pe.code }, pe.message);
717
+ this.errorSub.next(pe);
718
+ }
719
+ /** Emit a fatal error (transitions to error state). */
720
+ emitFatal(err, code = "UNKNOWN") {
721
+ const pe = this.toPlayerError(err, true, code);
722
+ this.emitTelemetry("player:error_fatal", "error", { code: pe.code }, pe.message);
723
+ this.stateSub.next({ kind: "error", error: pe });
724
+ this.errorSub.next(pe);
725
+ if (this.ctx) {
726
+ for (const p of this.plugins) {
727
+ try {
728
+ void p.onError?.(this.ctx, pe);
729
+ } catch {
730
+ }
731
+ }
732
+ }
733
+ }
734
+ async tryAutoNext() {
735
+ if (!this.ctx) return;
736
+ const cfg = this.ctx.config;
737
+ const list = cfg.playlist ?? this.playlist;
738
+ if (!list || list.length === 0) return;
739
+ const auto = cfg.autoNext ?? true;
740
+ if (!auto) return;
741
+ const idx = this.playlistIndexSub.value ?? 0;
742
+ if (idx >= list.length - 1) return;
743
+ const nextIdx = idx + 1;
744
+ this.emitTelemetry("player:auto_next", "info", { from: idx, to: nextIdx, total: list.length });
745
+ try {
746
+ await this.load(cfg, { playlistIndex: nextIdx, startTimeSec: 0 });
747
+ } catch {
748
+ }
749
+ }
750
+ installed = false;
751
+ // QoE metrics (v1.7)
752
+ qoe = {
753
+ loadStartMs: 0,
754
+ firstFrameMs: 0,
755
+ startupMs: 0,
756
+ rebufferCount: 0,
757
+ rebufferMs: 0,
758
+ rebufferStartMs: 0,
759
+ firstFrameEmitted: false
760
+ };
761
+ installVideoEventListeners() {
762
+ const v = this.video;
763
+ if (!v || this.installed) return;
764
+ this.installed = true;
765
+ const nowMs = () => globalThis.performance?.now?.() ?? Date.now();
766
+ const maybeFirstFrame = () => {
767
+ if (this.qoe.firstFrameEmitted) return;
768
+ const t = nowMs();
769
+ this.qoe.firstFrameMs = t;
770
+ if (this.qoe.loadStartMs) this.qoe.startupMs = Math.max(0, t - this.qoe.loadStartMs);
771
+ this.qoe.firstFrameEmitted = true;
772
+ this.emitTelemetry("qoe:first_frame", "info", { firstFrameMs: this.qoe.startupMs });
773
+ };
774
+ const maybeEndRebuffer = () => {
775
+ if (!this.qoe.rebufferStartMs) return;
776
+ const t = nowMs();
777
+ const delta = Math.max(0, t - this.qoe.rebufferStartMs);
778
+ this.qoe.rebufferMs += delta;
779
+ this.qoe.rebufferStartMs = 0;
780
+ this.emitTelemetry("qoe:rebuffer_end", "debug", { deltaMs: delta, totalMs: this.qoe.rebufferMs, count: this.qoe.rebufferCount });
781
+ };
782
+ v.addEventListener("timeupdate", () => {
783
+ this.emitTelemetry("player:timeupdate", "debug", { t: v.currentTime, d: v.duration });
784
+ });
785
+ v.addEventListener("loadeddata", () => {
786
+ maybeFirstFrame();
787
+ });
788
+ v.addEventListener("playing", () => {
789
+ maybeFirstFrame();
790
+ maybeEndRebuffer();
791
+ this.emitTelemetry("player:playing", "info");
792
+ });
793
+ v.addEventListener("waiting", () => {
794
+ if (!this.qoe.rebufferStartMs) {
795
+ this.qoe.rebufferCount += 1;
796
+ this.qoe.rebufferStartMs = nowMs();
797
+ this.emitTelemetry("qoe:rebuffer_start", "debug", { count: this.qoe.rebufferCount });
798
+ }
799
+ });
800
+ v.addEventListener("stalled", () => {
801
+ if (!this.qoe.rebufferStartMs) {
802
+ this.qoe.rebufferCount += 1;
803
+ this.qoe.rebufferStartMs = nowMs();
804
+ this.emitTelemetry("qoe:rebuffer_start", "debug", { count: this.qoe.rebufferCount, reason: "stalled" });
805
+ }
806
+ });
807
+ v.addEventListener("ended", () => {
808
+ maybeEndRebuffer();
809
+ this.emitTelemetry("player:ended", "info");
810
+ this.emitTelemetry("qoe:summary", "info", { startupMs: this.qoe.startupMs, rebufferCount: this.qoe.rebufferCount, rebufferMs: this.qoe.rebufferMs });
811
+ void this.tryAutoNext();
812
+ });
813
+ v.addEventListener("pause", () => {
814
+ this.emitTelemetry("player:pause", "info");
815
+ });
816
+ v.addEventListener("error", () => {
817
+ const me = v.error;
818
+ if (!me) return;
819
+ const codeMap = {
820
+ 1: "UNKNOWN",
821
+ 2: "NETWORK_HTTP",
822
+ 3: "DECODE_ERROR",
823
+ 4: "SRC_NOT_SUPPORTED"
824
+ };
825
+ const c = codeMap[me.code] ?? "UNKNOWN";
826
+ this.emitFatal(me, c);
827
+ });
828
+ }
829
+ emitTelemetry(event, level = "info", data, message) {
830
+ try {
831
+ const cfg = this.ctx?.config;
832
+ const contentId = cfg?.source?.contentId;
833
+ const sample = {
834
+ ts: Date.now(),
835
+ level,
836
+ event,
837
+ contentId,
838
+ engineId: this.engineId,
839
+ message,
840
+ data
841
+ };
842
+ this.ports.telemetry.add(sample);
843
+ } catch {
844
+ }
845
+ }
846
+ toPlayerError(err, fatal, code = "UNKNOWN") {
847
+ const e = err;
848
+ const msg = e?.message ? String(e.message) : String(err ?? "Unknown error");
849
+ return { code, message: msg, fatal, details: e };
850
+ }
851
+ async play() {
852
+ this.emitTelemetry("player:play_request", "info");
853
+ try {
854
+ await this.engine?.play();
855
+ this.emitTelemetry("player:play_ok", "info");
856
+ } catch (err) {
857
+ const name = err?.name;
858
+ if (name === "NotAllowedError") {
859
+ this.emitNonFatal(err, "AUTOPLAY_BLOCKED");
860
+ this.emitTelemetry("player:play_fail", "warn", { code: "AUTOPLAY_BLOCKED" }, err?.message);
861
+ } else {
862
+ this.emitFatal(err, "UNKNOWN");
863
+ this.emitTelemetry("player:play_fail", "error", { code: "UNKNOWN" }, err?.message);
864
+ }
865
+ throw err;
866
+ }
867
+ }
868
+ async pause() {
869
+ await this.engine?.pause();
870
+ }
871
+ async seekTo(timeSec) {
872
+ await this.engine?.seekTo(timeSec);
873
+ }
874
+ async setRate(rate) {
875
+ await this.engine?.setPlaybackRate(rate);
876
+ }
877
+ async setVolume(volume) {
878
+ await this.engine?.setVolume(volume);
879
+ }
880
+ async setTextTrack(trackId) {
881
+ if (trackId === "external") {
882
+ if (this.video && this.externalVttUrl) {
883
+ this.ensureExternalTrack(this.externalVttUrl);
884
+ }
885
+ return;
886
+ }
887
+ await this.engine?.setTextTrack(trackId);
888
+ }
889
+ async setAudioTrack(trackId) {
890
+ await this.engine?.setAudioTrack(trackId);
891
+ }
892
+ async setVideoTrack(trackId) {
893
+ await this.engine?.setVideoTrack(trackId);
894
+ }
895
+ // UI helpers
896
+ setAspectRatio(_mode) {
897
+ }
898
+ snapshot() {
899
+ const time = this.timeSub.value;
900
+ return {
901
+ currentTime: time.currentTime,
902
+ duration: time.duration,
903
+ state: this.stateSub.value,
904
+ bufferedAheadSec: this.bufferSub.value
905
+ };
906
+ }
907
+ /**
908
+ * Diagnostics helper for UI / support logs.
909
+ * Safe to call anytime after attach().
910
+ */
911
+ diagnostics() {
912
+ const snap = this.snapshot();
913
+ const bufferedEnd = (snap.currentTime ?? 0) + (snap.bufferedAheadSec ?? 0);
914
+ const time = {
915
+ currentTime: snap.currentTime,
916
+ duration: snap.duration,
917
+ bufferedEnd
918
+ };
919
+ const engine = {
920
+ id: this.engine?.id ?? "none",
921
+ selectedBy: "supports",
922
+ reasons: []
923
+ };
924
+ const source = this.ctx?.config?.source;
925
+ return {
926
+ ts: Date.now(),
927
+ engine,
928
+ capabilities: createCapabilityReport(
929
+ this.video ?? void 0,
930
+ this.ctx?.config,
931
+ this.mediaDecodingSub.value ?? void 0
932
+ ),
933
+ source,
934
+ state: snap.state,
935
+ time,
936
+ lastNonFatal: this.lastNonFatal,
937
+ // backward compat
938
+ selectedEngineId: engine.id,
939
+ snapshot: {
940
+ currentTime: snap.currentTime,
941
+ duration: snap.duration,
942
+ state: snap.state,
943
+ bufferedAheadSec: snap.bufferedAheadSec
944
+ }
945
+ };
946
+ }
947
+ // External tracks (for provider-based subtitles)
948
+ async setExternalVttTrack(lang, vtt) {
949
+ if (!this.video) return;
950
+ const blob = new Blob([vtt], { type: "text/vtt" });
951
+ this.externalVttUrl = URL.createObjectURL(blob);
952
+ this.ensureExternalTrack(this.externalVttUrl, lang);
953
+ }
954
+ setMetadata(meta) {
955
+ this.metadataSub.next(meta);
956
+ }
957
+ ensureExternalTrack(url, lang = "und") {
958
+ if (!this.video) return;
959
+ const prev = this.video.querySelector('track[data-external="true"]');
960
+ if (prev) prev.remove();
961
+ const track = document.createElement("track");
962
+ track.kind = "subtitles";
963
+ track.label = `External (${lang})`;
964
+ track.srclang = lang;
965
+ track.src = url;
966
+ track.default = true;
967
+ track.setAttribute("data-external", "true");
968
+ this.video.appendChild(track);
969
+ }
970
+ handleEngineEvent(ev) {
971
+ if (ev.type === "timeupdate") {
972
+ this.timeSub.next({ currentTime: ev.currentTime, duration: ev.duration });
973
+ return;
974
+ }
975
+ if (ev.type === "bufferupdate") {
976
+ this.bufferSub.next(ev.bufferedAheadSec);
977
+ return;
978
+ }
979
+ if (ev.type === "tracks") {
980
+ this.tracksSub.next(ev.tracks);
981
+ return;
982
+ }
983
+ if (ev.type === "state") {
984
+ const map = {
985
+ loading: { kind: "loading" },
986
+ buffering: { kind: "buffering" },
987
+ playing: { kind: "playing" },
988
+ paused: { kind: "paused" },
989
+ ended: { kind: "ended" }
990
+ };
991
+ this.stateSub.next(map[ev.state] ?? { kind: "idle" });
992
+ try {
993
+ this.ctx?.ports.telemetry.add({ t: Date.now(), kind: "state", state: ev.state });
994
+ } catch {
995
+ }
996
+ if (ev.state === "ended") void this.tryAutoNext();
997
+ return;
998
+ }
999
+ if (ev.type === "error") {
1000
+ const pe = { code: ev.error.code, message: ev.error.message, fatal: ev.error.fatal, details: ev.error.details };
1001
+ this.stateSub.next({ kind: "error", error: pe });
1002
+ this.errorSub.next(pe);
1003
+ try {
1004
+ this.ctx?.ports.telemetry.add({ t: Date.now(), kind: "error", code: pe.code, fatal: !!pe.fatal });
1005
+ } catch {
1006
+ }
1007
+ if (this.ctx) for (const p of this.plugins) void p.onError?.(this.ctx, pe);
1008
+ return;
1009
+ }
1010
+ }
1011
+ async tryRestoreStartTime(_config) {
1012
+ return null;
1013
+ }
1014
+ };
1015
+ function defaultEngineChooser(cfg) {
1016
+ const t = cfg.source.type;
1017
+ const url = t === "url" ? cfg.source.url : "";
1018
+ const looksLikeHls = typeof url === "string" && /\.m3u8($|\?)/i.test(url);
1019
+ const looksLikeDash = typeof url === "string" && /\.mpd($|\?)/i.test(url);
1020
+ const hasDrm = !!cfg.source.drm;
1021
+ if (t === "hls" || t === "dash" || looksLikeHls || looksLikeDash || hasDrm) return new NativeEngine();
1022
+ return new NativeEngine();
1023
+ }
1024
+
1025
+ // src/plugins/metadata/providers/local-metadata.provider.ts
1026
+ var LocalMetadataProvider = class {
1027
+ id = "local";
1028
+ async match(_hint) {
1029
+ return [];
1030
+ }
1031
+ async fetch(id) {
1032
+ const res = await fetch(id);
1033
+ if (!res.ok) throw new Error(`Metadata fetch failed: ${res.status}`);
1034
+ const ct = res.headers.get("content-type") ?? "";
1035
+ const text = await res.text();
1036
+ if (ct.includes("application/json") || looksLikeJson(text)) {
1037
+ try {
1038
+ return JSON.parse(text);
1039
+ } catch {
1040
+ }
1041
+ }
1042
+ return { description: text };
1043
+ }
1044
+ };
1045
+ function looksLikeJson(t) {
1046
+ const s = t.trim();
1047
+ return s.startsWith("{") && s.endsWith("}") || s.startsWith("[") && s.endsWith("]");
1048
+ }
1049
+
1050
+ // src/plugins/subtitles/providers/local-subtitle.provider.ts
1051
+ var LocalSubtitleProvider = class {
1052
+ id = "local";
1053
+ async search(query) {
1054
+ return [];
1055
+ }
1056
+ async fetch(candidate) {
1057
+ if (!candidate.url) throw new Error("LocalSubtitleProvider: candidate.url is required");
1058
+ const res = await fetch(candidate.url);
1059
+ if (!res.ok) throw new Error(`Subtitle fetch failed: ${res.status}`);
1060
+ return await res.text();
1061
+ }
1062
+ };
1063
+
1064
+ // src/utils/cache-store.ts
1065
+ var DefaultCacheStore = class {
1066
+ dbp = null;
1067
+ async get(key) {
1068
+ const db = await this.db();
1069
+ if (db) return idbGet(db, key);
1070
+ return lsGet(key);
1071
+ }
1072
+ async set(key, value, ttlMs) {
1073
+ const payload = { value, exp: Date.now() + ttlMs };
1074
+ const db = await this.db();
1075
+ if (db) return idbSet(db, key, payload);
1076
+ lsSet(key, payload);
1077
+ }
1078
+ async del(key) {
1079
+ const db = await this.db();
1080
+ if (db) return idbDel(db, key);
1081
+ localStorage.removeItem(key);
1082
+ }
1083
+ db() {
1084
+ if (typeof indexedDB === "undefined") return Promise.resolve(null);
1085
+ if (!this.dbp) this.dbp = openDb();
1086
+ return this.dbp;
1087
+ }
1088
+ };
1089
+ var DB_NAME = "ngx-smart-player-cache";
1090
+ var STORE = "kv";
1091
+ function openDb() {
1092
+ return new Promise((resolve) => {
1093
+ const req = indexedDB.open(DB_NAME, 1);
1094
+ req.onupgradeneeded = () => {
1095
+ const db = req.result;
1096
+ if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
1097
+ };
1098
+ req.onsuccess = () => resolve(req.result);
1099
+ req.onerror = () => resolve(null);
1100
+ });
1101
+ }
1102
+ async function idbGet(db, key) {
1103
+ return new Promise((resolve) => {
1104
+ const tx = db.transaction(STORE, "readonly");
1105
+ const st = tx.objectStore(STORE);
1106
+ const req = st.get(key);
1107
+ req.onsuccess = () => {
1108
+ const v = req.result;
1109
+ if (!v) return resolve(null);
1110
+ if (v.exp && Date.now() > v.exp) return resolve(null);
1111
+ resolve(v.value);
1112
+ };
1113
+ req.onerror = () => resolve(null);
1114
+ });
1115
+ }
1116
+ async function idbSet(db, key, val) {
1117
+ return new Promise((resolve) => {
1118
+ const tx = db.transaction(STORE, "readwrite");
1119
+ tx.objectStore(STORE).put(val, key);
1120
+ tx.oncomplete = () => resolve();
1121
+ tx.onerror = () => resolve();
1122
+ });
1123
+ }
1124
+ async function idbDel(db, key) {
1125
+ return new Promise((resolve) => {
1126
+ const tx = db.transaction(STORE, "readwrite");
1127
+ tx.objectStore(STORE).delete(key);
1128
+ tx.oncomplete = () => resolve();
1129
+ tx.onerror = () => resolve();
1130
+ });
1131
+ }
1132
+ function lsGet(key) {
1133
+ const raw = localStorage.getItem(key);
1134
+ if (!raw) return null;
1135
+ try {
1136
+ const v = JSON.parse(raw);
1137
+ if (v?.exp && Date.now() > v.exp) return null;
1138
+ return v?.value ?? null;
1139
+ } catch {
1140
+ return null;
1141
+ }
1142
+ }
1143
+ function lsSet(key, val) {
1144
+ try {
1145
+ localStorage.setItem(key, JSON.stringify(val));
1146
+ } catch {
1147
+ }
1148
+ }
1149
+
1150
+ // src/commands.ts
1151
+ function isCategorizedEventType(type) {
1152
+ return /^(playback|engine|network|drm|ads|subtitles|ui):/.test(type);
1153
+ }
1154
+
1155
+ // src/events/event-bus.ts
1156
+ var SimpleEventBus = class {
1157
+ handlers = /* @__PURE__ */ new Map();
1158
+ emit(event) {
1159
+ const set = this.handlers.get(event.type);
1160
+ if (!set) return;
1161
+ for (const h of Array.from(set)) h(event);
1162
+ }
1163
+ on(type, handler) {
1164
+ const set = this.handlers.get(type) ?? /* @__PURE__ */ new Set();
1165
+ set.add(handler);
1166
+ this.handlers.set(type, set);
1167
+ return () => set.delete(handler);
1168
+ }
1169
+ };
1170
+
1171
+ // src/plugins/progress/progress.plugin.ts
1172
+ function keyFor(contentId) {
1173
+ return `progress:${contentId}`;
1174
+ }
1175
+ var ProgressPlugin = class {
1176
+ id = "progress";
1177
+ timer = null;
1178
+ async onLoadStart(ctx) {
1179
+ this.stop();
1180
+ }
1181
+ async onLoaded(ctx) {
1182
+ const contentId = ctx.contentId ?? ctx.config.source.contentId;
1183
+ if (!contentId) return;
1184
+ try {
1185
+ if (ctx.config.progress.mode === "local" || ctx.config.progress.mode === "hybrid") {
1186
+ const raw = await ctx.ports.storage.get(keyFor(contentId));
1187
+ if (raw) {
1188
+ const rec = JSON.parse(raw);
1189
+ if (Number.isFinite(rec.positionSec) && rec.positionSec > 0) {
1190
+ await ctx.facade.seekTo(rec.positionSec);
1191
+ }
1192
+ }
1193
+ }
1194
+ } catch (e) {
1195
+ ctx.ports.log.warn("[ProgressPlugin] restore failed", e);
1196
+ }
1197
+ const intervalMs = Math.max(3, ctx.config.progress.intervalSec ?? 10) * 1e3;
1198
+ this.timer = setInterval(() => void this.save(ctx), intervalMs);
1199
+ }
1200
+ async onDestroy(ctx) {
1201
+ this.stop();
1202
+ await this.save(ctx);
1203
+ }
1204
+ stop() {
1205
+ if (this.timer) {
1206
+ clearInterval(this.timer);
1207
+ this.timer = null;
1208
+ }
1209
+ }
1210
+ async save(ctx) {
1211
+ const contentId = ctx.contentId ?? ctx.config.source.contentId;
1212
+ if (!contentId) return;
1213
+ const snap = ctx.facade.snapshot();
1214
+ const positionSec = Math.max(0, snap.currentTime);
1215
+ const rec = {
1216
+ contentId,
1217
+ positionSec,
1218
+ durationSec: snap.duration || void 0,
1219
+ updatedAt: Date.now()
1220
+ };
1221
+ try {
1222
+ if (ctx.config.progress.mode === "local" || ctx.config.progress.mode === "hybrid") {
1223
+ await ctx.ports.storage.set(keyFor(contentId), JSON.stringify(rec));
1224
+ }
1225
+ } catch (e) {
1226
+ ctx.ports.log.warn("[ProgressPlugin] local save failed", e);
1227
+ }
1228
+ try {
1229
+ if ((ctx.config.progress.mode === "server" || ctx.config.progress.mode === "hybrid") && ctx.config.progress.endpoint) {
1230
+ await ctx.ports.http.fetch(ctx.config.progress.endpoint, {
1231
+ method: "POST",
1232
+ headers: { "content-type": "application/json" },
1233
+ body: JSON.stringify(rec)
1234
+ });
1235
+ }
1236
+ } catch (e) {
1237
+ ctx.ports.log.warn("[ProgressPlugin] server save failed", e);
1238
+ }
1239
+ }
1240
+ };
1241
+
1242
+ // src/plugins/resilience/resilience.plugin.ts
1243
+ function sleep(ms) {
1244
+ return new Promise((r) => setTimeout(r, ms));
1245
+ }
1246
+ var ResiliencePlugin = class {
1247
+ id = "resilience";
1248
+ attempts = 0;
1249
+ recovering = false;
1250
+ async onLoadStart(ctx) {
1251
+ this.attempts = 0;
1252
+ this.recovering = false;
1253
+ if (ctx.config.resilience.healthEndpoint) {
1254
+ void this.health(ctx);
1255
+ }
1256
+ }
1257
+ async onError(ctx, err) {
1258
+ if (this.recovering) return;
1259
+ if (err.fatal) return;
1260
+ const max = Math.max(0, ctx.config.resilience.retries ?? 0);
1261
+ if (this.attempts >= max) return;
1262
+ this.attempts++;
1263
+ this.recovering = true;
1264
+ const min = ctx.config.resilience.backoffMs?.min ?? 500;
1265
+ const maxMs = ctx.config.resilience.backoffMs?.max ?? 8e3;
1266
+ const backoff = Math.min(maxMs, min * Math.pow(2, this.attempts - 1));
1267
+ ctx.ports.log.warn("[ResiliencePlugin] retry", { attempt: this.attempts, backoff, code: err.code });
1268
+ if (ctx.config.resilience.healthEndpoint) {
1269
+ await this.health(ctx);
1270
+ }
1271
+ await sleep(backoff);
1272
+ try {
1273
+ const snap = ctx.facade.snapshot();
1274
+ await ctx.facade.reload({ startTimeSec: snap.currentTime || void 0 });
1275
+ } catch (e) {
1276
+ ctx.ports.log.error("[ResiliencePlugin] reload failed", e);
1277
+ } finally {
1278
+ this.recovering = false;
1279
+ }
1280
+ }
1281
+ async health(ctx) {
1282
+ try {
1283
+ await ctx.ports.http.fetch(ctx.config.resilience.healthEndpoint, { method: "GET" });
1284
+ } catch (e) {
1285
+ ctx.ports.log.warn("[ResiliencePlugin] health check failed", e);
1286
+ }
1287
+ }
1288
+ };
1289
+
1290
+ // src/plugins/ads/ssai.plugin.ts
1291
+ function appendQuery(url, q) {
1292
+ try {
1293
+ const base = typeof window !== "undefined" ? window.location.href : "http://localhost";
1294
+ const u = new URL(url, base);
1295
+ for (const [k, v] of Object.entries(q)) {
1296
+ if (v === void 0 || v === null) continue;
1297
+ u.searchParams.set(k, String(v));
1298
+ }
1299
+ return u.toString();
1300
+ } catch {
1301
+ return url;
1302
+ }
1303
+ }
1304
+ function createSsaiManifestRewritePlugin() {
1305
+ return {
1306
+ id: "ads:ssai-manifest-rewrite",
1307
+ async onLoadStart(ctx) {
1308
+ const ads = ctx.config?.ads;
1309
+ if (!ads || ads.enabled !== true) return;
1310
+ if (ads.mode !== "ssai") return;
1311
+ const provider = ads.providerConfig ?? {};
1312
+ const src = ctx.config?.source;
1313
+ if (!src || typeof src.url !== "string" || !src.url) return;
1314
+ const before = src.url;
1315
+ let after = before;
1316
+ if (provider.stitchedUrl) after = provider.stitchedUrl;
1317
+ if (provider.appendQuery) after = appendQuery(after, provider.appendQuery);
1318
+ if (after !== before) {
1319
+ src.url = after;
1320
+ ctx.ports.telemetry?.add?.({
1321
+ event: "ssai:manifest_rewrite",
1322
+ level: "info",
1323
+ ts: Date.now(),
1324
+ payload: { before, after }
1325
+ });
1326
+ }
1327
+ }
1328
+ };
1329
+ }
1330
+
1331
+ exports.ConsoleLogPort = ConsoleLogPort;
1332
+ exports.DefaultCacheStore = DefaultCacheStore;
1333
+ exports.LocalMetadataProvider = LocalMetadataProvider;
1334
+ exports.LocalSubtitleProvider = LocalSubtitleProvider;
1335
+ exports.MemoryStoragePort = MemoryStoragePort;
1336
+ exports.NativeEngine = NativeEngine;
1337
+ exports.NoopTelemetryPort = NoopTelemetryPort;
1338
+ exports.PlayerFacade = PlayerFacade;
1339
+ exports.ProgressPlugin = ProgressPlugin;
1340
+ exports.ResiliencePlugin = ResiliencePlugin;
1341
+ exports.SimpleEventBus = SimpleEventBus;
1342
+ exports.applyEngineProfileToSource = applyEngineProfileToSource;
1343
+ exports.applyRemoteConfig = applyRemoteConfig;
1344
+ exports.createCapabilityReport = createCapabilityReport;
1345
+ exports.createDefaultCapabilities = createDefaultCapabilities;
1346
+ exports.createDefaultPorts = createDefaultPorts;
1347
+ exports.createMediaDecodingReport = createMediaDecodingReport;
1348
+ exports.createNativeEngineFactory = createNativeEngineFactory;
1349
+ exports.createSsaiManifestRewritePlugin = createSsaiManifestRewritePlugin;
1350
+ exports.detectPlaybackCapabilities = detectPlaybackCapabilities;
1351
+ exports.getEngineProfileHints = getEngineProfileHints;
1352
+ exports.isCategorizedEventType = isCategorizedEventType;
1353
+ exports.selectEngine = selectEngine;
1354
+ //# sourceMappingURL=index.cjs.map
1355
+ //# sourceMappingURL=index.cjs.map