playsvideo 0.3.0 → 0.4.1

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,8 +1,9 @@
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
+ import { buildMkvKeyframeIndexFromSource } from './pipeline/mkv-keyframe-index.js';
6
7
  import { generateVodPlaylist } from './pipeline/playlist.js';
7
8
  import { buildSegmentPlan } from './pipeline/segment-plan.js';
8
9
  import { parseSubtitleFile, subtitleDataToWebVTT } from './pipeline/subtitle.js';
@@ -41,6 +42,13 @@ export class PlaysVideoEngine extends EventTarget {
41
42
  _passthrough = false;
42
43
  _blobUrl = null;
43
44
  _pendingFileType = null;
45
+ _codecPath = {
46
+ mode: 'pipeline',
47
+ sourceVideo: { short: null, full: null },
48
+ sourceAudio: { short: null, full: null },
49
+ outputVideo: { short: null, full: null },
50
+ outputAudio: { short: null, full: null },
51
+ };
44
52
  // Pre-built keyframe index (e.g. from MKV cues) to skip mediabunny scan
45
53
  _keyframeIndex = null;
46
54
  // Main-thread pipeline state (used by loadSource)
@@ -71,6 +79,15 @@ export class PlaysVideoEngine extends EventTarget {
71
79
  get passthrough() {
72
80
  return this._passthrough;
73
81
  }
82
+ get codecPath() {
83
+ return {
84
+ mode: this._codecPath.mode,
85
+ sourceVideo: { ...this._codecPath.sourceVideo },
86
+ sourceAudio: { ...this._codecPath.sourceAudio },
87
+ outputVideo: { ...this._codecPath.outputVideo },
88
+ outputAudio: { ...this._codecPath.outputAudio },
89
+ };
90
+ }
74
91
  get transcodeWorkerStates() {
75
92
  return this._transcodeWorkerStates.map((worker) => ({ ...worker }));
76
93
  }
@@ -182,6 +199,13 @@ export class PlaysVideoEngine extends EventTarget {
182
199
  this._passthrough = false;
183
200
  this._pendingFileType = null;
184
201
  this._keyframeIndex = null;
202
+ this._codecPath = {
203
+ mode: 'pipeline',
204
+ sourceVideo: { short: null, full: null },
205
+ sourceAudio: { short: null, full: null },
206
+ outputVideo: { short: null, full: null },
207
+ outputAudio: { short: null, full: null },
208
+ };
185
209
  // Source pipeline cleanup
186
210
  if (this._sourceSegmentAbort) {
187
211
  this._sourceSegmentAbort.abort();
@@ -364,17 +388,79 @@ export class PlaysVideoEngine extends EventTarget {
364
388
  message: msg.message,
365
389
  });
366
390
  }
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';
391
+ createPlaybackCapabilities() {
392
+ const capabilities = createBrowserPlaybackCapabilities(this.video);
393
+ if (FORCE_REMUX) {
394
+ return {
395
+ ...capabilities,
396
+ canPlayType: () => '',
397
+ };
398
+ }
399
+ return capabilities;
400
+ }
401
+ evaluateInitialPlayback(media) {
402
+ const options = this._blobUrl
403
+ ? [
404
+ { mode: 'direct-bytes', mimeType: this._pendingFileType, url: this._blobUrl },
405
+ { mode: 'hls' },
406
+ ]
407
+ : [{ mode: 'hls' }];
408
+ return evaluatePlaybackOptions({
409
+ options: [...options],
410
+ media,
411
+ capabilities: this.createPlaybackCapabilities(),
412
+ });
413
+ }
414
+ evaluateHlsPlayback(media) {
415
+ return evaluatePlaybackOptions({
416
+ options: [{ mode: 'hls' }],
417
+ media,
418
+ capabilities: this.createPlaybackCapabilities(),
419
+ }).evaluations[0];
420
+ }
421
+ logPlaybackDiagnostics(context, evaluation) {
422
+ for (const entry of evaluation.evaluations) {
423
+ for (const diagnostic of entry.diagnostics) {
424
+ mlog(`${context}: mode=${entry.option.mode} ${diagnostic.code} ${diagnostic.message}`);
425
+ }
426
+ }
427
+ if (!evaluation.recommended) {
428
+ mlog(`${context}: no-supported-option`);
429
+ }
430
+ }
431
+ throwPlaybackSelectionError(context, diagnostics) {
432
+ const detail = diagnostics.length > 0
433
+ ? diagnostics.map((diagnostic) => diagnostic.message).join(' ')
434
+ : 'No supported playback option.';
435
+ throw new Error(`${context}: ${detail}`);
436
+ }
437
+ failPlaybackSelection(context, diagnostics) {
438
+ const detail = diagnostics.length > 0
439
+ ? diagnostics.map((diagnostic) => diagnostic.message).join(' ')
440
+ : 'No supported playback option.';
441
+ this._phase = 'error';
442
+ this.dispatchEvent(new CustomEvent('error', { detail: { message: `${context}: ${detail}` } }));
443
+ }
444
+ makeCodecPathFromSource(media, mode, outputAudio = {
445
+ short: media.sourceAudioCodec,
446
+ full: media.audioCodec,
447
+ }) {
448
+ return {
449
+ mode,
450
+ sourceVideo: {
451
+ short: media.sourceVideoCodec,
452
+ full: media.videoCodec,
453
+ },
454
+ sourceAudio: {
455
+ short: media.sourceAudioCodec,
456
+ full: media.audioCodec,
457
+ },
458
+ outputVideo: {
459
+ short: media.sourceVideoCodec,
460
+ full: media.videoCodec,
461
+ },
462
+ outputAudio,
463
+ };
378
464
  }
379
465
  startPassthrough(src) {
380
466
  this._passthrough = true;
@@ -393,6 +479,7 @@ export class PlaysVideoEngine extends EventTarget {
393
479
  durationSec: this._durationSec,
394
480
  subtitleTracks: [],
395
481
  passthrough: true,
482
+ codecPath: this.codecPath,
396
483
  },
397
484
  }));
398
485
  };
@@ -407,23 +494,44 @@ export class PlaysVideoEngine extends EventTarget {
407
494
  const msg = event.data;
408
495
  if (msg.type === 'probed') {
409
496
  // Worker finished demux — decide passthrough vs pipeline
410
- const canPlay = this.checkNativePlayback(msg.videoCodec, msg.audioCodec);
497
+ const media = {
498
+ sourceVideoCodec: msg.sourceVideoCodec ?? null,
499
+ sourceAudioCodec: msg.sourceAudioCodec ?? null,
500
+ videoCodec: msg.videoCodec ?? null,
501
+ audioCodec: msg.audioCodec ?? null,
502
+ };
503
+ const evaluation = this.evaluateInitialPlayback(media);
504
+ this.logPlaybackDiagnostics('playback selection', evaluation);
411
505
  this._subtitleTracks = msg.subtitleTracks ?? [];
412
- if (canPlay && this._blobUrl) {
413
- mlog(`passthrough: canPlayType accepted codecs=${msg.videoCodec}/${msg.audioCodec}`);
414
- this.startPassthrough(this._blobUrl);
506
+ const blobUrl = this._blobUrl;
507
+ const usePassthrough = evaluation.recommended?.option.mode === 'direct-bytes' && blobUrl !== null;
508
+ this._codecPath = this.makeCodecPathFromSource(media, usePassthrough ? 'passthrough' : 'pipeline');
509
+ if (usePassthrough && blobUrl) {
510
+ mlog(`passthrough: selected direct playback codecs=${msg.videoCodec}/${msg.audioCodec}`);
511
+ this.startPassthrough(blobUrl);
415
512
  this.worker.postMessage({ type: 'passthrough-pipeline' });
513
+ if (this._subtitleTracks.length > 0) {
514
+ this.dispatchSubtitleStatus(`Extracting ${this._subtitleTracks.length} subtitle track(s)...`);
515
+ }
516
+ else {
517
+ this.dispatchSubtitleStatus('No embedded subtitles');
518
+ }
416
519
  for (const track of this._subtitleTracks) {
417
520
  mlog(`requesting subtitle track=${track.index} lang=${track.language} codec=${track.codec}`);
418
521
  this.worker.postMessage({ type: 'subtitle', trackIndex: track.index });
419
522
  }
420
523
  }
421
524
  else {
525
+ const hlsEvaluation = evaluation.evaluations.find((entry) => entry.option.mode === 'hls');
526
+ if (evaluation.recommended?.option.mode !== 'hls') {
527
+ this.failPlaybackSelection('Playback selection failed', hlsEvaluation?.diagnostics ?? []);
528
+ return;
529
+ }
422
530
  if (this._blobUrl) {
423
531
  URL.revokeObjectURL(this._blobUrl);
424
532
  this._blobUrl = null;
425
533
  }
426
- mlog(`pipeline: canPlayType rejected, proceeding with remux pipeline`);
534
+ mlog('pipeline: selected remux/HLS playback');
427
535
  this.ensureTranscodeWorkers();
428
536
  const remuxMsg = { type: 'remux-pipeline' };
429
537
  if (this._keyframeIndex)
@@ -438,6 +546,25 @@ export class PlaysVideoEngine extends EventTarget {
438
546
  this._durationSec = msg.durationSec;
439
547
  this._subtitleTracks = msg.subtitleTracks ?? [];
440
548
  this._phase = 'ready';
549
+ this._codecPath = {
550
+ mode: 'pipeline',
551
+ sourceVideo: {
552
+ short: msg.sourceVideoCodec ?? this._codecPath.sourceVideo.short,
553
+ full: msg.sourceVideoCodecFull ?? this._codecPath.sourceVideo.full,
554
+ },
555
+ sourceAudio: {
556
+ short: msg.sourceAudioCodec ?? this._codecPath.sourceAudio.short,
557
+ full: msg.sourceAudioCodecFull ?? this._codecPath.sourceAudio.full,
558
+ },
559
+ outputVideo: {
560
+ short: msg.outputVideoCodec ?? this._codecPath.outputVideo.short,
561
+ full: msg.outputVideoCodecFull ?? this._codecPath.outputVideo.full,
562
+ },
563
+ outputAudio: {
564
+ short: msg.outputAudioCodec ?? this._codecPath.outputAudio.short,
565
+ full: msg.outputAudioCodecFull ?? this._codecPath.outputAudio.full,
566
+ },
567
+ };
441
568
  mlog(`ready segments=${msg.totalSegments} dur=${msg.durationSec.toFixed(1)}s`);
442
569
  // Resolve any pending requests
443
570
  if (this.pendingPlaylist) {
@@ -449,6 +576,12 @@ export class PlaysVideoEngine extends EventTarget {
449
576
  this.pendingInit = null;
450
577
  }
451
578
  // Request subtitle extraction for all embedded tracks
579
+ if (this._subtitleTracks.length > 0) {
580
+ this.dispatchSubtitleStatus(`Extracting ${this._subtitleTracks.length} subtitle track(s)...`);
581
+ }
582
+ else {
583
+ this.dispatchSubtitleStatus('No embedded subtitles');
584
+ }
452
585
  for (const track of this._subtitleTracks) {
453
586
  mlog(`requesting subtitle track=${track.index} lang=${track.language} codec=${track.codec}`);
454
587
  this.worker.postMessage({ type: 'subtitle', trackIndex: track.index });
@@ -458,17 +591,24 @@ export class PlaysVideoEngine extends EventTarget {
458
591
  totalSegments: this._totalSegments,
459
592
  durationSec: this._durationSec,
460
593
  subtitleTracks: this._subtitleTracks,
594
+ codecPath: this.codecPath,
461
595
  },
462
596
  }));
463
597
  this.startHls();
464
598
  }
465
599
  else if (msg.type === 'subtitle') {
466
600
  mlog(`subtitle arrived track=${msg.trackIndex} codec=${msg.codec} len=${msg.webvtt?.length}`);
601
+ const info = this._subtitleTracks.find((t) => t.index === msg.trackIndex);
602
+ const lang = info?.language ?? '?';
603
+ const cueMatch = msg.webvtt?.match(/\d\d:\d\d/g);
604
+ const cueCount = cueMatch ? Math.floor(cueMatch.length / 2) : 0;
605
+ this.dispatchSubtitleStatus(`Subtitle track ${msg.trackIndex}: ${lang} ${msg.codec} ${cueCount} cues, ${msg.webvtt?.length ?? 0} bytes`);
467
606
  this.addSubtitleTrack({
468
607
  webvtt: msg.webvtt,
469
608
  source: 'embedded',
470
609
  trackIndex: msg.trackIndex,
471
610
  defaultTrack: msg.trackIndex === 0,
611
+ selectTrack: msg.trackIndex === 0,
472
612
  });
473
613
  }
474
614
  else if (msg.type === 'segment-state') {
@@ -551,8 +691,15 @@ export class PlaysVideoEngine extends EventTarget {
551
691
  mlog(`source pipeline: pre-built keyframes=${index.keyframes.length}`);
552
692
  }
553
693
  else {
554
- index = await getKeyframeIndex(demux.videoSink, demux.duration);
555
- mlog(`source pipeline: keyframe-index keyframes=${index.keyframes.length}`);
694
+ const mkvIndex = await buildMkvKeyframeIndexFromSource(source);
695
+ if (mkvIndex) {
696
+ index = mkvIndex;
697
+ mlog(`source pipeline: mkv-cues keyframes=${index.keyframes.length}`);
698
+ }
699
+ else {
700
+ index = await getKeyframeIndex(demux.videoSink, demux.duration);
701
+ mlog(`source pipeline: keyframe-index keyframes=${index.keyframes.length}`);
702
+ }
556
703
  }
557
704
  // Build segment plan
558
705
  this._sourcePlan = buildSegmentPlan({
@@ -560,14 +707,36 @@ export class PlaysVideoEngine extends EventTarget {
560
707
  durationSec: index.duration,
561
708
  targetSegmentDurationSec: this._sourceTargetSegDuration,
562
709
  });
563
- // Check transcode
564
- const codecProber = createBrowserProber();
565
- this._sourceDoTranscode =
566
- demux.audioCodec !== null &&
567
- audioNeedsTranscode(codecProber, demux.audioCodec, demux.audioDecoderConfig?.codec);
710
+ const media = {
711
+ sourceVideoCodec: demux.videoCodec,
712
+ sourceAudioCodec: demux.audioCodec,
713
+ videoCodec: demux.videoDecoderConfig.codec,
714
+ audioCodec: demux.audioDecoderConfig?.codec ?? null,
715
+ };
716
+ const hlsEvaluation = this.evaluateHlsPlayback(media);
717
+ this.logPlaybackDiagnostics('source playback selection', {
718
+ recommended: hlsEvaluation.status === 'supported'
719
+ ? {
720
+ option: hlsEvaluation.option,
721
+ reason: hlsEvaluation.diagnostics[hlsEvaluation.diagnostics.length - 1] ?? {
722
+ code: 'selected-hls',
723
+ message: 'Recommended HLS playback.',
724
+ },
725
+ }
726
+ : null,
727
+ evaluations: [hlsEvaluation],
728
+ });
729
+ if (hlsEvaluation.status !== 'supported') {
730
+ this.throwPlaybackSelectionError('Source playback selection failed', hlsEvaluation.diagnostics);
731
+ }
732
+ this._sourceDoTranscode = hlsEvaluation.pipelineAudioRequiresTranscode === true;
568
733
  this._sourceAudioDecoderConfig = this._sourceDoTranscode
569
734
  ? makeAacDecoderConfig(demux.audioDecoderConfig)
570
735
  : demux.audioDecoderConfig;
736
+ this._codecPath = this.makeCodecPathFromSource(media, 'pipeline', {
737
+ short: this._sourceDoTranscode ? 'aac' : demux.audioCodec,
738
+ full: this._sourceAudioDecoderConfig?.codec ?? null,
739
+ });
571
740
  // Pre-process segment 0
572
741
  const seg0Result = await processSegmentWithAbort(this.makeSourceProcessorConfig(), 0);
573
742
  if (seg0Result.initSegment) {
@@ -596,6 +765,7 @@ export class PlaysVideoEngine extends EventTarget {
596
765
  totalSegments: this._totalSegments,
597
766
  durationSec: this._durationSec,
598
767
  subtitleTracks: this._subtitleTracks,
768
+ codecPath: this.codecPath,
599
769
  },
600
770
  }));
601
771
  this.startHls();
@@ -684,8 +854,12 @@ export class PlaysVideoEngine extends EventTarget {
684
854
  context = null;
685
855
  stats = makeStats();
686
856
  currentSegmentIndex = null;
857
+ callbacks = null;
858
+ aborted = false;
687
859
  load(context, _config, callbacks) {
688
860
  this.context = context;
861
+ this.callbacks = callbacks;
862
+ this.aborted = false;
689
863
  const url = context.url;
690
864
  if (url.includes('init.mp4')) {
691
865
  this.loadInit(context, callbacks);
@@ -729,22 +903,36 @@ export class PlaysVideoEngine extends EventTarget {
729
903
  : engine.requestSegment(index);
730
904
  segmentPromise
731
905
  .then((data) => {
906
+ if (this.aborted) {
907
+ return;
908
+ }
732
909
  this.currentSegmentIndex = null;
733
910
  this.stats.loaded = data.byteLength;
734
911
  this.stats.loading.end = performance.now();
735
912
  callbacks.onSuccess({ url: context.url, data }, this.stats, context, null);
736
913
  })
737
914
  .catch((err) => {
915
+ if (this.aborted) {
916
+ return;
917
+ }
738
918
  this.currentSegmentIndex = null;
739
919
  if (err instanceof DOMException && err.name === 'AbortError') {
740
920
  this.stats.aborted = true;
921
+ callbacks.onAbort?.(this.stats, context, null);
741
922
  return;
742
923
  }
743
924
  callbacks.onError({ code: 0, text: err.message }, context, null, this.stats);
744
925
  });
745
926
  }
746
927
  abort() {
928
+ if (this.aborted) {
929
+ return;
930
+ }
931
+ this.aborted = true;
932
+ this.stats.aborted = true;
933
+ let abortedActiveSegment = false;
747
934
  if (this.currentSegmentIndex !== null) {
935
+ abortedActiveSegment = true;
748
936
  if (engine._source) {
749
937
  // Source mode: abort the in-flight main-thread processing
750
938
  engine._sourceSegmentAbort?.abort();
@@ -755,9 +943,14 @@ export class PlaysVideoEngine extends EventTarget {
755
943
  }
756
944
  this.currentSegmentIndex = null;
757
945
  }
946
+ if (abortedActiveSegment && this.callbacks && this.context) {
947
+ this.callbacks.onAbort?.(this.stats, this.context, null);
948
+ }
758
949
  }
759
950
  destroy() {
760
951
  this.abort();
952
+ this.callbacks = null;
953
+ this.context = null;
761
954
  }
762
955
  }
763
956
  this.hls = new Hls({
@@ -839,6 +1032,10 @@ export class PlaysVideoEngine extends EventTarget {
839
1032
  }
840
1033
  track.track.mode = 'showing';
841
1034
  }
1035
+ dispatchSubtitleStatus(message) {
1036
+ mlog(`subtitle-status: ${message}`);
1037
+ this.dispatchEvent(new CustomEvent('subtitle-status', { detail: { message } }));
1038
+ }
842
1039
  restoreDefaultTextTrack() {
843
1040
  const preferred = this.attachedSubtitleTracks.find((attached) => attached.element.default) ??
844
1041
  this.attachedSubtitleTracks[0];