playsvideo 0.3.0 → 0.4.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/README.md CHANGED
@@ -18,7 +18,7 @@ Many video files won't play in a browser — not because the browser can't *deco
18
18
  |---|---|---|
19
19
  | **Containers** | MKV, MP4, AVI, TS, WebM | Demuxed and remuxed to fMP4 |
20
20
  | **Video** | H.264, H.265 (HEVC), VP9, AV1 | Passthrough — plays ~99% of files (~90% on Firefox; HEVC transcode planned) |
21
- | **Audio** | AAC, MP3, AC-3, E-AC-3, DTS, FLAC, Opus | Unsupported codecs transcoded to AAC on the fly |
21
+ | **Audio** | AAC, MP3, AC-3, E-AC-3, DTS, FLAC, Opus | Transcoded to AAC when the active playback path cannot use them safely |
22
22
  | **Subtitles** | SRT, ASS/SSA | Extracted and displayed as WebVTT |
23
23
 
24
24
  See [supported media](docs/supported-media.md) for the full codec matrix, browser compatibility, and transcode details.
@@ -30,13 +30,15 @@ Video file (MKV, MP4, AVI, …)
30
30
  → mediabunny demux (streaming, any file size)
31
31
  → keyframe-aligned segment plan
32
32
  → per segment:
33
- video passed through or transcoded if needed
34
- audio transcoded only if needed (AC-3/DTS/MP3/FLAC/Opus → AAC)
33
+ video remuxed / passed through
34
+ audio transcoded only if needed (AC-3/E-AC-3/DTS/MP3/FLAC/Opus → AAC)
35
35
  muxed to fMP4
36
36
  → hls.js plays segments on demand
37
37
  → subtitles extracted to WebVTT
38
38
  ```
39
39
 
40
+ Note: passthrough/native playback and remuxed HLS/MSE playback are evaluated separately. A source file can play fine via direct passthrough in both Chrome and Safari on macOS, yet still be unsafe once routed through the remuxed HLS/fMP4 pipeline. That is what we observed with AC-3: the original file played directly, but Safari produced audible stalls after remuxing to HLS/fMP4, while Chrome continued to play correctly. The remux pipeline therefore treats AC-3/E-AC-3 as unsafe there and transcodes them to AAC, without affecting native passthrough decisions.
41
+
40
42
  Video transcode is almost never needed — browsers natively decode the vast majority of video codecs. When audio transcode is needed, a lightweight 1.8 MB ffmpeg.wasm build is lazy-loaded entirely in-browser. No SharedArrayBuffer required — works on any host without special CORS headers.
41
43
 
42
44
  ### Under the hood
package/dist/engine.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface ReadyDetail {
7
7
  durationSec: number;
8
8
  subtitleTracks: SubtitleTrackInfo[];
9
9
  passthrough?: boolean;
10
+ codecPath: CodecPath;
10
11
  }
11
12
  export interface ErrorDetail {
12
13
  message: string;
@@ -15,6 +16,9 @@ export interface LoadingDetail {
15
16
  file?: File;
16
17
  url?: string;
17
18
  }
19
+ export interface SubtitleStatusDetail {
20
+ message: string;
21
+ }
18
22
  export interface WasmWorkerState extends TranscodeWorkerSnapshot {
19
23
  id: number;
20
24
  }
@@ -53,6 +57,17 @@ export interface ExternalSubtitleOptions {
53
57
  language?: string;
54
58
  kind?: 'subtitles' | 'captions';
55
59
  }
60
+ export interface CodecDescriptor {
61
+ short: string | null;
62
+ full: string | null;
63
+ }
64
+ export interface CodecPath {
65
+ mode: 'passthrough' | 'pipeline';
66
+ sourceVideo: CodecDescriptor;
67
+ sourceAudio: CodecDescriptor;
68
+ outputVideo: CodecDescriptor;
69
+ outputAudio: CodecDescriptor;
70
+ }
56
71
  interface EngineEventMap {
57
72
  ready: CustomEvent<ReadyDetail>;
58
73
  error: CustomEvent<ErrorDetail>;
@@ -82,6 +97,7 @@ export declare class PlaysVideoEngine extends EventTarget {
82
97
  private _passthrough;
83
98
  private _blobUrl;
84
99
  private _pendingFileType;
100
+ private _codecPath;
85
101
  private _keyframeIndex;
86
102
  private _source;
87
103
  private _sourceDemux;
@@ -98,6 +114,7 @@ export declare class PlaysVideoEngine extends EventTarget {
98
114
  get durationSec(): number;
99
115
  get subtitleTracks(): SubtitleTrackInfo[];
100
116
  get passthrough(): boolean;
117
+ get codecPath(): CodecPath;
101
118
  get transcodeWorkerStates(): WasmWorkerState[];
102
119
  get segmentStates(): SegmentState[];
103
120
  constructor(video: HTMLVideoElement, options?: EngineOptions);
@@ -137,7 +154,13 @@ export declare class PlaysVideoEngine extends EventTarget {
137
154
  private updateTranscodeWorkerState;
138
155
  private noteSegmentState;
139
156
  private handleWorkerSegmentState;
140
- private checkNativePlayback;
157
+ private createPlaybackCapabilities;
158
+ private evaluateInitialPlayback;
159
+ private evaluateHlsPlayback;
160
+ private logPlaybackDiagnostics;
161
+ private throwPlaybackSelectionError;
162
+ private failPlaybackSelection;
163
+ private makeCodecPathFromSource;
141
164
  private startPassthrough;
142
165
  private handleWorkerMessage;
143
166
  private requestSegment;
@@ -149,6 +172,7 @@ export declare class PlaysVideoEngine extends EventTarget {
149
172
  private addSubtitleTrack;
150
173
  private removeSubtitleTracks;
151
174
  private showTextTrack;
175
+ private dispatchSubtitleStatus;
152
176
  private restoreDefaultTextTrack;
153
177
  }
154
178
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
package/dist/engine.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import Hls from 'hls.js';
2
2
  import { WasmFfmpegRunner } from './adapters/wasm-ffmpeg.js';
3
+ import { createBrowserPlaybackCapabilities, evaluatePlaybackOptions, } from './playback-selection.js';
3
4
  import { createLocalAudioTranscoder, makeAacDecoderConfig } from './pipeline/audio-transcode.js';
4
- import { audioNeedsTranscode, createBrowserProber } from './pipeline/codec-probe.js';
5
5
  import { demuxSource, getKeyframeIndex } from './pipeline/demux.js';
6
6
  import { generateVodPlaylist } from './pipeline/playlist.js';
7
7
  import { buildSegmentPlan } from './pipeline/segment-plan.js';
@@ -41,6 +41,13 @@ export class PlaysVideoEngine extends EventTarget {
41
41
  _passthrough = false;
42
42
  _blobUrl = null;
43
43
  _pendingFileType = null;
44
+ _codecPath = {
45
+ mode: 'pipeline',
46
+ sourceVideo: { short: null, full: null },
47
+ sourceAudio: { short: null, full: null },
48
+ outputVideo: { short: null, full: null },
49
+ outputAudio: { short: null, full: null },
50
+ };
44
51
  // Pre-built keyframe index (e.g. from MKV cues) to skip mediabunny scan
45
52
  _keyframeIndex = null;
46
53
  // Main-thread pipeline state (used by loadSource)
@@ -71,6 +78,15 @@ export class PlaysVideoEngine extends EventTarget {
71
78
  get passthrough() {
72
79
  return this._passthrough;
73
80
  }
81
+ get codecPath() {
82
+ return {
83
+ mode: this._codecPath.mode,
84
+ sourceVideo: { ...this._codecPath.sourceVideo },
85
+ sourceAudio: { ...this._codecPath.sourceAudio },
86
+ outputVideo: { ...this._codecPath.outputVideo },
87
+ outputAudio: { ...this._codecPath.outputAudio },
88
+ };
89
+ }
74
90
  get transcodeWorkerStates() {
75
91
  return this._transcodeWorkerStates.map((worker) => ({ ...worker }));
76
92
  }
@@ -182,6 +198,13 @@ export class PlaysVideoEngine extends EventTarget {
182
198
  this._passthrough = false;
183
199
  this._pendingFileType = null;
184
200
  this._keyframeIndex = null;
201
+ this._codecPath = {
202
+ mode: 'pipeline',
203
+ sourceVideo: { short: null, full: null },
204
+ sourceAudio: { short: null, full: null },
205
+ outputVideo: { short: null, full: null },
206
+ outputAudio: { short: null, full: null },
207
+ };
185
208
  // Source pipeline cleanup
186
209
  if (this._sourceSegmentAbort) {
187
210
  this._sourceSegmentAbort.abort();
@@ -364,17 +387,79 @@ export class PlaysVideoEngine extends EventTarget {
364
387
  message: msg.message,
365
388
  });
366
389
  }
367
- checkNativePlayback(videoCodec, audioCodec) {
368
- const mime = this._pendingFileType;
369
- if (!mime)
370
- return false;
371
- const codecs = audioCodec ? `${videoCodec}, ${audioCodec}` : videoCodec;
372
- const fullMime = `${mime}; codecs="${codecs}"`;
373
- const result = this.video.canPlayType(fullMime);
374
- mlog(`canPlayType("${fullMime}") = "${result}"`);
375
- if (FORCE_REMUX)
376
- return false;
377
- return result === 'probably' || result === 'maybe';
390
+ createPlaybackCapabilities() {
391
+ const capabilities = createBrowserPlaybackCapabilities(this.video);
392
+ if (FORCE_REMUX) {
393
+ return {
394
+ ...capabilities,
395
+ canPlayType: () => '',
396
+ };
397
+ }
398
+ return capabilities;
399
+ }
400
+ evaluateInitialPlayback(media) {
401
+ const options = this._blobUrl
402
+ ? [
403
+ { mode: 'direct-bytes', mimeType: this._pendingFileType, url: this._blobUrl },
404
+ { mode: 'hls' },
405
+ ]
406
+ : [{ mode: 'hls' }];
407
+ return evaluatePlaybackOptions({
408
+ options: [...options],
409
+ media,
410
+ capabilities: this.createPlaybackCapabilities(),
411
+ });
412
+ }
413
+ evaluateHlsPlayback(media) {
414
+ return evaluatePlaybackOptions({
415
+ options: [{ mode: 'hls' }],
416
+ media,
417
+ capabilities: this.createPlaybackCapabilities(),
418
+ }).evaluations[0];
419
+ }
420
+ logPlaybackDiagnostics(context, evaluation) {
421
+ for (const entry of evaluation.evaluations) {
422
+ for (const diagnostic of entry.diagnostics) {
423
+ mlog(`${context}: mode=${entry.option.mode} ${diagnostic.code} ${diagnostic.message}`);
424
+ }
425
+ }
426
+ if (!evaluation.recommended) {
427
+ mlog(`${context}: no-supported-option`);
428
+ }
429
+ }
430
+ throwPlaybackSelectionError(context, diagnostics) {
431
+ const detail = diagnostics.length > 0
432
+ ? diagnostics.map((diagnostic) => diagnostic.message).join(' ')
433
+ : 'No supported playback option.';
434
+ throw new Error(`${context}: ${detail}`);
435
+ }
436
+ failPlaybackSelection(context, diagnostics) {
437
+ const detail = diagnostics.length > 0
438
+ ? diagnostics.map((diagnostic) => diagnostic.message).join(' ')
439
+ : 'No supported playback option.';
440
+ this._phase = 'error';
441
+ this.dispatchEvent(new CustomEvent('error', { detail: { message: `${context}: ${detail}` } }));
442
+ }
443
+ makeCodecPathFromSource(media, mode, outputAudio = {
444
+ short: media.sourceAudioCodec,
445
+ full: media.audioCodec,
446
+ }) {
447
+ return {
448
+ mode,
449
+ sourceVideo: {
450
+ short: media.sourceVideoCodec,
451
+ full: media.videoCodec,
452
+ },
453
+ sourceAudio: {
454
+ short: media.sourceAudioCodec,
455
+ full: media.audioCodec,
456
+ },
457
+ outputVideo: {
458
+ short: media.sourceVideoCodec,
459
+ full: media.videoCodec,
460
+ },
461
+ outputAudio,
462
+ };
378
463
  }
379
464
  startPassthrough(src) {
380
465
  this._passthrough = true;
@@ -393,6 +478,7 @@ export class PlaysVideoEngine extends EventTarget {
393
478
  durationSec: this._durationSec,
394
479
  subtitleTracks: [],
395
480
  passthrough: true,
481
+ codecPath: this.codecPath,
396
482
  },
397
483
  }));
398
484
  };
@@ -407,23 +493,44 @@ export class PlaysVideoEngine extends EventTarget {
407
493
  const msg = event.data;
408
494
  if (msg.type === 'probed') {
409
495
  // Worker finished demux — decide passthrough vs pipeline
410
- const canPlay = this.checkNativePlayback(msg.videoCodec, msg.audioCodec);
496
+ const media = {
497
+ sourceVideoCodec: msg.sourceVideoCodec ?? null,
498
+ sourceAudioCodec: msg.sourceAudioCodec ?? null,
499
+ videoCodec: msg.videoCodec ?? null,
500
+ audioCodec: msg.audioCodec ?? null,
501
+ };
502
+ const evaluation = this.evaluateInitialPlayback(media);
503
+ this.logPlaybackDiagnostics('playback selection', evaluation);
411
504
  this._subtitleTracks = msg.subtitleTracks ?? [];
412
- if (canPlay && this._blobUrl) {
413
- mlog(`passthrough: canPlayType accepted codecs=${msg.videoCodec}/${msg.audioCodec}`);
414
- this.startPassthrough(this._blobUrl);
505
+ const blobUrl = this._blobUrl;
506
+ const usePassthrough = evaluation.recommended?.option.mode === 'direct-bytes' && blobUrl !== null;
507
+ this._codecPath = this.makeCodecPathFromSource(media, usePassthrough ? 'passthrough' : 'pipeline');
508
+ if (usePassthrough && blobUrl) {
509
+ mlog(`passthrough: selected direct playback codecs=${msg.videoCodec}/${msg.audioCodec}`);
510
+ this.startPassthrough(blobUrl);
415
511
  this.worker.postMessage({ type: 'passthrough-pipeline' });
512
+ if (this._subtitleTracks.length > 0) {
513
+ this.dispatchSubtitleStatus(`Extracting ${this._subtitleTracks.length} subtitle track(s)...`);
514
+ }
515
+ else {
516
+ this.dispatchSubtitleStatus('No embedded subtitles');
517
+ }
416
518
  for (const track of this._subtitleTracks) {
417
519
  mlog(`requesting subtitle track=${track.index} lang=${track.language} codec=${track.codec}`);
418
520
  this.worker.postMessage({ type: 'subtitle', trackIndex: track.index });
419
521
  }
420
522
  }
421
523
  else {
524
+ const hlsEvaluation = evaluation.evaluations.find((entry) => entry.option.mode === 'hls');
525
+ if (evaluation.recommended?.option.mode !== 'hls') {
526
+ this.failPlaybackSelection('Playback selection failed', hlsEvaluation?.diagnostics ?? []);
527
+ return;
528
+ }
422
529
  if (this._blobUrl) {
423
530
  URL.revokeObjectURL(this._blobUrl);
424
531
  this._blobUrl = null;
425
532
  }
426
- mlog(`pipeline: canPlayType rejected, proceeding with remux pipeline`);
533
+ mlog('pipeline: selected remux/HLS playback');
427
534
  this.ensureTranscodeWorkers();
428
535
  const remuxMsg = { type: 'remux-pipeline' };
429
536
  if (this._keyframeIndex)
@@ -438,6 +545,25 @@ export class PlaysVideoEngine extends EventTarget {
438
545
  this._durationSec = msg.durationSec;
439
546
  this._subtitleTracks = msg.subtitleTracks ?? [];
440
547
  this._phase = 'ready';
548
+ this._codecPath = {
549
+ mode: 'pipeline',
550
+ sourceVideo: {
551
+ short: msg.sourceVideoCodec ?? this._codecPath.sourceVideo.short,
552
+ full: msg.sourceVideoCodecFull ?? this._codecPath.sourceVideo.full,
553
+ },
554
+ sourceAudio: {
555
+ short: msg.sourceAudioCodec ?? this._codecPath.sourceAudio.short,
556
+ full: msg.sourceAudioCodecFull ?? this._codecPath.sourceAudio.full,
557
+ },
558
+ outputVideo: {
559
+ short: msg.outputVideoCodec ?? this._codecPath.outputVideo.short,
560
+ full: msg.outputVideoCodecFull ?? this._codecPath.outputVideo.full,
561
+ },
562
+ outputAudio: {
563
+ short: msg.outputAudioCodec ?? this._codecPath.outputAudio.short,
564
+ full: msg.outputAudioCodecFull ?? this._codecPath.outputAudio.full,
565
+ },
566
+ };
441
567
  mlog(`ready segments=${msg.totalSegments} dur=${msg.durationSec.toFixed(1)}s`);
442
568
  // Resolve any pending requests
443
569
  if (this.pendingPlaylist) {
@@ -449,6 +575,12 @@ export class PlaysVideoEngine extends EventTarget {
449
575
  this.pendingInit = null;
450
576
  }
451
577
  // Request subtitle extraction for all embedded tracks
578
+ if (this._subtitleTracks.length > 0) {
579
+ this.dispatchSubtitleStatus(`Extracting ${this._subtitleTracks.length} subtitle track(s)...`);
580
+ }
581
+ else {
582
+ this.dispatchSubtitleStatus('No embedded subtitles');
583
+ }
452
584
  for (const track of this._subtitleTracks) {
453
585
  mlog(`requesting subtitle track=${track.index} lang=${track.language} codec=${track.codec}`);
454
586
  this.worker.postMessage({ type: 'subtitle', trackIndex: track.index });
@@ -458,17 +590,24 @@ export class PlaysVideoEngine extends EventTarget {
458
590
  totalSegments: this._totalSegments,
459
591
  durationSec: this._durationSec,
460
592
  subtitleTracks: this._subtitleTracks,
593
+ codecPath: this.codecPath,
461
594
  },
462
595
  }));
463
596
  this.startHls();
464
597
  }
465
598
  else if (msg.type === 'subtitle') {
466
599
  mlog(`subtitle arrived track=${msg.trackIndex} codec=${msg.codec} len=${msg.webvtt?.length}`);
600
+ const info = this._subtitleTracks.find((t) => t.index === msg.trackIndex);
601
+ const lang = info?.language ?? '?';
602
+ const cueMatch = msg.webvtt?.match(/\d\d:\d\d/g);
603
+ const cueCount = cueMatch ? Math.floor(cueMatch.length / 2) : 0;
604
+ this.dispatchSubtitleStatus(`Subtitle track ${msg.trackIndex}: ${lang} ${msg.codec} ${cueCount} cues, ${msg.webvtt?.length ?? 0} bytes`);
467
605
  this.addSubtitleTrack({
468
606
  webvtt: msg.webvtt,
469
607
  source: 'embedded',
470
608
  trackIndex: msg.trackIndex,
471
609
  defaultTrack: msg.trackIndex === 0,
610
+ selectTrack: msg.trackIndex === 0,
472
611
  });
473
612
  }
474
613
  else if (msg.type === 'segment-state') {
@@ -560,14 +699,36 @@ export class PlaysVideoEngine extends EventTarget {
560
699
  durationSec: index.duration,
561
700
  targetSegmentDurationSec: this._sourceTargetSegDuration,
562
701
  });
563
- // Check transcode
564
- const codecProber = createBrowserProber();
565
- this._sourceDoTranscode =
566
- demux.audioCodec !== null &&
567
- audioNeedsTranscode(codecProber, demux.audioCodec, demux.audioDecoderConfig?.codec);
702
+ const media = {
703
+ sourceVideoCodec: demux.videoCodec,
704
+ sourceAudioCodec: demux.audioCodec,
705
+ videoCodec: demux.videoDecoderConfig.codec,
706
+ audioCodec: demux.audioDecoderConfig?.codec ?? null,
707
+ };
708
+ const hlsEvaluation = this.evaluateHlsPlayback(media);
709
+ this.logPlaybackDiagnostics('source playback selection', {
710
+ recommended: hlsEvaluation.status === 'supported'
711
+ ? {
712
+ option: hlsEvaluation.option,
713
+ reason: hlsEvaluation.diagnostics[hlsEvaluation.diagnostics.length - 1] ?? {
714
+ code: 'selected-hls',
715
+ message: 'Recommended HLS playback.',
716
+ },
717
+ }
718
+ : null,
719
+ evaluations: [hlsEvaluation],
720
+ });
721
+ if (hlsEvaluation.status !== 'supported') {
722
+ this.throwPlaybackSelectionError('Source playback selection failed', hlsEvaluation.diagnostics);
723
+ }
724
+ this._sourceDoTranscode = hlsEvaluation.pipelineAudioRequiresTranscode === true;
568
725
  this._sourceAudioDecoderConfig = this._sourceDoTranscode
569
726
  ? makeAacDecoderConfig(demux.audioDecoderConfig)
570
727
  : demux.audioDecoderConfig;
728
+ this._codecPath = this.makeCodecPathFromSource(media, 'pipeline', {
729
+ short: this._sourceDoTranscode ? 'aac' : demux.audioCodec,
730
+ full: this._sourceAudioDecoderConfig?.codec ?? null,
731
+ });
571
732
  // Pre-process segment 0
572
733
  const seg0Result = await processSegmentWithAbort(this.makeSourceProcessorConfig(), 0);
573
734
  if (seg0Result.initSegment) {
@@ -596,6 +757,7 @@ export class PlaysVideoEngine extends EventTarget {
596
757
  totalSegments: this._totalSegments,
597
758
  durationSec: this._durationSec,
598
759
  subtitleTracks: this._subtitleTracks,
760
+ codecPath: this.codecPath,
599
761
  },
600
762
  }));
601
763
  this.startHls();
@@ -684,8 +846,12 @@ export class PlaysVideoEngine extends EventTarget {
684
846
  context = null;
685
847
  stats = makeStats();
686
848
  currentSegmentIndex = null;
849
+ callbacks = null;
850
+ aborted = false;
687
851
  load(context, _config, callbacks) {
688
852
  this.context = context;
853
+ this.callbacks = callbacks;
854
+ this.aborted = false;
689
855
  const url = context.url;
690
856
  if (url.includes('init.mp4')) {
691
857
  this.loadInit(context, callbacks);
@@ -729,22 +895,36 @@ export class PlaysVideoEngine extends EventTarget {
729
895
  : engine.requestSegment(index);
730
896
  segmentPromise
731
897
  .then((data) => {
898
+ if (this.aborted) {
899
+ return;
900
+ }
732
901
  this.currentSegmentIndex = null;
733
902
  this.stats.loaded = data.byteLength;
734
903
  this.stats.loading.end = performance.now();
735
904
  callbacks.onSuccess({ url: context.url, data }, this.stats, context, null);
736
905
  })
737
906
  .catch((err) => {
907
+ if (this.aborted) {
908
+ return;
909
+ }
738
910
  this.currentSegmentIndex = null;
739
911
  if (err instanceof DOMException && err.name === 'AbortError') {
740
912
  this.stats.aborted = true;
913
+ callbacks.onAbort?.(this.stats, context, null);
741
914
  return;
742
915
  }
743
916
  callbacks.onError({ code: 0, text: err.message }, context, null, this.stats);
744
917
  });
745
918
  }
746
919
  abort() {
920
+ if (this.aborted) {
921
+ return;
922
+ }
923
+ this.aborted = true;
924
+ this.stats.aborted = true;
925
+ let abortedActiveSegment = false;
747
926
  if (this.currentSegmentIndex !== null) {
927
+ abortedActiveSegment = true;
748
928
  if (engine._source) {
749
929
  // Source mode: abort the in-flight main-thread processing
750
930
  engine._sourceSegmentAbort?.abort();
@@ -755,9 +935,14 @@ export class PlaysVideoEngine extends EventTarget {
755
935
  }
756
936
  this.currentSegmentIndex = null;
757
937
  }
938
+ if (abortedActiveSegment && this.callbacks && this.context) {
939
+ this.callbacks.onAbort?.(this.stats, this.context, null);
940
+ }
758
941
  }
759
942
  destroy() {
760
943
  this.abort();
944
+ this.callbacks = null;
945
+ this.context = null;
761
946
  }
762
947
  }
763
948
  this.hls = new Hls({
@@ -839,6 +1024,10 @@ export class PlaysVideoEngine extends EventTarget {
839
1024
  }
840
1025
  track.track.mode = 'showing';
841
1026
  }
1027
+ dispatchSubtitleStatus(message) {
1028
+ mlog(`subtitle-status: ${message}`);
1029
+ this.dispatchEvent(new CustomEvent('subtitle-status', { detail: { message } }));
1030
+ }
842
1031
  restoreDefaultTextTrack() {
843
1032
  const preferred = this.attachedSubtitleTracks.find((attached) => attached.element.default) ??
844
1033
  this.attachedSubtitleTracks[0];