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 +5 -3
- package/dist/engine.d.ts +25 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +211 -22
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/codec-probe.d.ts.map +1 -1
- package/dist/pipeline/codec-probe.js +8 -0
- package/dist/pipeline/codec-probe.js.map +1 -1
- package/dist/pipeline/segment-plan.d.ts.map +1 -1
- package/dist/pipeline/segment-plan.js +25 -11
- package/dist/pipeline/segment-plan.js.map +1 -1
- package/dist/playback-selection.d.ts +82 -0
- package/dist/playback-selection.d.ts.map +1 -0
- package/dist/playback-selection.js +194 -0
- package/dist/playback-selection.js.map +1 -0
- package/dist/worker.js +10 -0
- package/dist/worker.js.map +1 -1
- package/package.json +1 -1
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 |
|
|
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
|
|
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
|
|
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 {};
|
package/dist/engine.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;
|
|
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
|
-
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
370
|
-
return
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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(
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
demux.
|
|
567
|
-
|
|
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];
|