dasha 4.0.3 → 4.1.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/dist/index.d.mts +96 -7
- package/dist/index.mjs +606 -15
- package/package.json +6 -6
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _$mediabunny from "mediabunny";
|
|
2
|
-
import { AudioCodec, DurationMetadataRequestOptions, EncodedPacket, FilePathSource, HLS_FORMATS, Input as Input$1, InputAudioTrack as InputAudioTrack$1, InputFormat, InputOptions, InputTrack as InputTrack$2, InputTrackQuery, InputTrackQuery as InputTrackQuery$1, InputVideoTrack as InputVideoTrack$1, MediaCodec, MetadataTags, PacketRetrievalOptions, Source, TrackDisposition, UrlSource, VideoCodec, asc, desc, prefer } from "mediabunny";
|
|
2
|
+
import { AudioCodec, DurationMetadataRequestOptions, EncodedPacket, FilePathSource, HLS_FORMATS, Input as Input$1, InputAudioTrack as InputAudioTrack$1, InputFormat, InputOptions, InputTrack as InputTrack$2, InputTrackQuery, InputTrackQuery as InputTrackQuery$1, InputVideoTrack as InputVideoTrack$1, MediaCodec as MediaCodec$1, MetadataTags, PacketRetrievalOptions, Source, TrackDisposition, UrlSource, VideoCodec, asc, desc, prefer } from "mediabunny";
|
|
3
3
|
import { Element } from "@xmldom/xmldom";
|
|
4
4
|
|
|
5
5
|
//#region src/mediabunny.d.ts
|
|
@@ -69,7 +69,7 @@ declare const AUDIO_CODECS: readonly ["aac", "opus", "mp3", "vorbis", "flac", "a
|
|
|
69
69
|
* @group Codecs
|
|
70
70
|
* @public
|
|
71
71
|
*/
|
|
72
|
-
declare const SUBTITLE_CODECS: readonly ["srt", "
|
|
72
|
+
declare const SUBTITLE_CODECS: readonly ["srt", "webvtt", "ttml", "dfxp", "ssa", "ass", "stpp"];
|
|
73
73
|
/**
|
|
74
74
|
* Union type of known video codecs.
|
|
75
75
|
* @group Codecs
|
|
@@ -99,7 +99,7 @@ type SubtitleCodec = (typeof SUBTITLE_CODECS)[number];
|
|
|
99
99
|
* @group Codecs
|
|
100
100
|
* @public
|
|
101
101
|
*/
|
|
102
|
-
type MediaCodec
|
|
102
|
+
type MediaCodec = VideoCodec$1 | AudioCodec$1 | SubtitleCodec;
|
|
103
103
|
//#endregion
|
|
104
104
|
//#region src/role-type.d.ts
|
|
105
105
|
declare const ROLE_TYPE: {
|
|
@@ -148,7 +148,7 @@ type DashParsedSegment = {
|
|
|
148
148
|
};
|
|
149
149
|
type DashTrackCommon = {
|
|
150
150
|
type: DashTrackType;
|
|
151
|
-
codec?: MediaCodec
|
|
151
|
+
codec?: MediaCodec;
|
|
152
152
|
codecString: string | null;
|
|
153
153
|
languageCode?: string;
|
|
154
154
|
peakBitrate: number | null;
|
|
@@ -385,7 +385,7 @@ declare abstract class DashTrackBackingBase {
|
|
|
385
385
|
delegate<T>(fn: (track: any) => T | Promise<T>): Promise<T>;
|
|
386
386
|
getId(): number;
|
|
387
387
|
getNumber(): number;
|
|
388
|
-
getCodec(): MediaCodec | null;
|
|
388
|
+
getCodec(): MediaCodec$1 | null;
|
|
389
389
|
getInternalCodecId(): null;
|
|
390
390
|
getName(): string | null;
|
|
391
391
|
getLanguageCode(): string;
|
|
@@ -446,6 +446,70 @@ declare class DashInputFormat extends InputFormat {
|
|
|
446
446
|
declare const DASH: DashInputFormat;
|
|
447
447
|
declare const DASH_FORMATS: InputFormat[];
|
|
448
448
|
//#endregion
|
|
449
|
+
//#region src/hls/hls-subtitles.d.ts
|
|
450
|
+
type SourceWithRootPath = {
|
|
451
|
+
rootPath: string;
|
|
452
|
+
_options?: {
|
|
453
|
+
requestInit?: RequestInit;
|
|
454
|
+
};
|
|
455
|
+
_url?: string | URL | Request;
|
|
456
|
+
};
|
|
457
|
+
type HlsSubtitleMediaTag = {
|
|
458
|
+
autoselect: boolean;
|
|
459
|
+
codec: SubtitleCodec;
|
|
460
|
+
codecString: string;
|
|
461
|
+
default: boolean;
|
|
462
|
+
forced: boolean;
|
|
463
|
+
groupId: string;
|
|
464
|
+
hearingImpaired: boolean;
|
|
465
|
+
languageCode: string;
|
|
466
|
+
name: string | null;
|
|
467
|
+
uri: string;
|
|
468
|
+
};
|
|
469
|
+
declare class HlsSubtitlePlaylist implements HlsSegmentedInput {
|
|
470
|
+
#private;
|
|
471
|
+
segments: HlsSegment[];
|
|
472
|
+
constructor(source: SourceWithRootPath, playlistPath: string);
|
|
473
|
+
runUpdateSegments(): Promise<void>;
|
|
474
|
+
getDurationFromMetadata(_options: DurationMetadataRequestOptions): Promise<number | null>;
|
|
475
|
+
getLiveRefreshInterval(): Promise<number | null>;
|
|
476
|
+
isRelativeToUnixEpoch(): Promise<boolean>;
|
|
477
|
+
}
|
|
478
|
+
declare class HlsSubtitleTrackBacking {
|
|
479
|
+
#private;
|
|
480
|
+
constructor(params: {
|
|
481
|
+
id: number;
|
|
482
|
+
number: number;
|
|
483
|
+
pairingMask: bigint;
|
|
484
|
+
source: SourceWithRootPath;
|
|
485
|
+
track: HlsSubtitleMediaTag;
|
|
486
|
+
});
|
|
487
|
+
getType(): "subtitle";
|
|
488
|
+
getId(): number;
|
|
489
|
+
getNumber(): number;
|
|
490
|
+
getCodec(): never;
|
|
491
|
+
getInternalCodecId(): null;
|
|
492
|
+
getName(): string | null;
|
|
493
|
+
getLanguageCode(): string;
|
|
494
|
+
getTimeResolution(): number;
|
|
495
|
+
isRelativeToUnixEpoch(): Promise<boolean>;
|
|
496
|
+
getDisposition(): TrackDisposition;
|
|
497
|
+
getPairingMask(): bigint;
|
|
498
|
+
getBitrate(): null;
|
|
499
|
+
getAverageBitrate(): null;
|
|
500
|
+
getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise<number | null>;
|
|
501
|
+
getLiveRefreshInterval(): Promise<number | null>;
|
|
502
|
+
getHasOnlyKeyPackets(): boolean;
|
|
503
|
+
getDecoderConfig(): Promise<null>;
|
|
504
|
+
getMetadataCodecParameterString(): string;
|
|
505
|
+
getFirstPacket(_options: unknown): Promise<EncodedPacket | null>;
|
|
506
|
+
getPacket(_timestamp: number, _options: unknown): Promise<EncodedPacket | null>;
|
|
507
|
+
getNextPacket(_packet: EncodedPacket, _options: unknown): Promise<EncodedPacket | null>;
|
|
508
|
+
getKeyPacket(_timestamp: number, _options: unknown): Promise<EncodedPacket | null>;
|
|
509
|
+
getNextKeyPacket(_packet: EncodedPacket, _options: unknown): Promise<EncodedPacket | null>;
|
|
510
|
+
getSegmentedInput(): HlsSubtitlePlaylist;
|
|
511
|
+
}
|
|
512
|
+
//#endregion
|
|
449
513
|
//#region src/mediabunny-input.d.ts
|
|
450
514
|
declare module 'mediabunny' {
|
|
451
515
|
interface Input<S extends Source = Source> {
|
|
@@ -460,11 +524,36 @@ type SegmentAccessMethods = {
|
|
|
460
524
|
type MediabunnyTrackWithSegments = InputTrack$2 & SegmentAccessMethods;
|
|
461
525
|
type MediabunnyVideoTrackWithSegments = InputVideoTrack$1 & SegmentAccessMethods;
|
|
462
526
|
type MediabunnyAudioTrackWithSegments = InputAudioTrack$1 & SegmentAccessMethods;
|
|
463
|
-
type
|
|
527
|
+
type NativeTrackBacking = Parameters<Input$1<Source>['_wrapBackingAsTrack']>[0];
|
|
528
|
+
type SegmentableBacking = {
|
|
529
|
+
getId(): number;
|
|
530
|
+
getNumber(): number;
|
|
531
|
+
getType(): string;
|
|
532
|
+
getCodec(): MediaCodec$1 | null | Promise<MediaCodec$1 | null>;
|
|
533
|
+
getInternalCodecId?(): string | number | Uint8Array | null | Promise<string | number | Uint8Array | null>;
|
|
534
|
+
getName?(): string | null | Promise<string | null>;
|
|
535
|
+
getLanguageCode?(): string | Promise<string>;
|
|
536
|
+
getTimeResolution?(): number | Promise<number>;
|
|
537
|
+
isRelativeToUnixEpoch?(): boolean | Promise<boolean>;
|
|
538
|
+
getDisposition?(): unknown | Promise<unknown>;
|
|
539
|
+
getPairingMask?(): bigint;
|
|
540
|
+
getBitrate?(): number | null | Promise<number | null>;
|
|
541
|
+
getAverageBitrate?(): number | null | Promise<number | null>;
|
|
542
|
+
getDurationFromMetadata?(options: unknown): Promise<number | null>;
|
|
543
|
+
getLiveRefreshInterval?(): Promise<number | null>;
|
|
544
|
+
getDecoderConfig?(): Promise<VideoDecoderConfig | AudioDecoderConfig | null>;
|
|
545
|
+
getMetadataCodecParameterString?(): string | null | Promise<string | null>;
|
|
546
|
+
getSegmentedInput?(): HlsSegmentedInput | DashSegmentedInput;
|
|
547
|
+
};
|
|
548
|
+
type TrackBacking = NativeTrackBacking | SegmentableBacking;
|
|
549
|
+
declare const BACKING_TYPE_SUBTITLE = "subtitle";
|
|
550
|
+
declare const BACKING_TYPE_AUDIO = "audio";
|
|
551
|
+
declare const BACKING_TYPE_VIDEO = "video";
|
|
464
552
|
declare const preserveSubtitleBackingsOnInput: (input: Input$1) => Input$1<Source>;
|
|
465
553
|
declare class SegmentedMediabunnyInput<S extends Source = Source> extends Input$1<S> {
|
|
466
554
|
#private;
|
|
467
555
|
_wrapBackingAsTrack(backing: TrackBacking): MediabunnyTrackWithSegments;
|
|
556
|
+
_getSyntheticTrackBackings(type?: typeof BACKING_TYPE_VIDEO | typeof BACKING_TYPE_AUDIO | typeof BACKING_TYPE_SUBTITLE): Promise<HlsSubtitleTrackBacking[]>;
|
|
468
557
|
getTracks(query?: InputTrackQuery$1<MediabunnyTrackWithSegments>): Promise<MediabunnyTrackWithSegments[]>;
|
|
469
558
|
getVideoTracks(query?: InputTrackQuery$1<MediabunnyVideoTrackWithSegments>): Promise<MediabunnyVideoTrackWithSegments[]>;
|
|
470
559
|
getAudioTracks(query?: InputTrackQuery$1<MediabunnyAudioTrackWithSegments>): Promise<MediabunnyAudioTrackWithSegments[]>;
|
|
@@ -488,4 +577,4 @@ declare const isInput: (value: unknown) => value is Input;
|
|
|
488
577
|
declare const getSegmentedInput: (track: InputTrack$1) => InputSegmentedInput;
|
|
489
578
|
declare const getSegments: (track: InputTrack$1) => Promise<InputSegment[]>;
|
|
490
579
|
//#endregion
|
|
491
|
-
export { DASH, DASH_FORMATS, type DashSegment, type DashSegmentedInput, FilePathSource, HLS_FORMATS, type HlsSegment, type HlsSegmentedInput, Input, InputAudioTrack, InputSegment, InputSegmentedInput, InputTrack$1 as InputTrack, type InputTrackQuery, type InputTrackWithBacking, InputVideoTrack, UrlSource, asc, desc, getSegmentedInput, getSegments, isInput, prefer, preserveSubtitleBackingsOnInput };
|
|
580
|
+
export { DASH, DASH_FORMATS, type DashSegment, type DashSegmentedInput, FilePathSource, HLS_FORMATS, type HlsSegment, type HlsSegmentedInput, Input, InputAudioTrack, InputSegment, InputSegmentedInput, InputTrack$1 as InputTrack, type InputTrackQuery, type InputTrackWithBacking, InputVideoTrack, type MediaCodec, type SubtitleCodec, UrlSource, asc, desc, getSegmentedInput, getSegments, isInput, prefer, preserveSubtitleBackingsOnInput };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { ADTS, CustomPathedSource, FilePathSource, HLS_FORMATS, Input as Input$1, InputFormat, InputTrack, MATROSKA, MP3, MP4, QTFF, UrlSource, WEBM, asc, desc, prefer } from "mediabunny";
|
|
1
|
+
import { ADTS, CustomPathedSource, FilePathSource, HLS, HLS_FORMATS, Input as Input$1, InputFormat, InputTrack, MATROSKA, MP3, MP4, QTFF, UrlSource, WEBM, asc, desc, prefer } from "mediabunny";
|
|
2
2
|
import { DOMParser } from "@xmldom/xmldom";
|
|
3
3
|
import { Temporal } from "temporal-polyfill";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
|
-
import { setTimeout } from "node:timers/promises";
|
|
5
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
6
7
|
//#region src/util.ts
|
|
7
8
|
const combineUrl = (baseUrl, relativeUrl) => {
|
|
8
9
|
if (!baseUrl.trim()) return relativeUrl;
|
|
@@ -90,15 +91,16 @@ const ROLE_TYPE = {
|
|
|
90
91
|
//#endregion
|
|
91
92
|
//#region src/subtitle.ts
|
|
92
93
|
const parseSubtitleCodecFromMime = (mime) => {
|
|
93
|
-
switch (mime.toLowerCase().trim().split(
|
|
94
|
+
switch (mime.toLowerCase().trim().split(/[.+;]/)[0]) {
|
|
94
95
|
case "srt":
|
|
95
96
|
case "x-subrip": return "srt";
|
|
96
97
|
case "ssa": return "ssa";
|
|
97
98
|
case "ass": return "ass";
|
|
98
99
|
case "ttml": return "ttml";
|
|
99
|
-
case "
|
|
100
|
+
case "webvtt":
|
|
101
|
+
case "vtt":
|
|
102
|
+
case "wvtt": return "webvtt";
|
|
100
103
|
case "stpp": return "stpp";
|
|
101
|
-
case "wvtt": return "wvtt";
|
|
102
104
|
default: throw new Error(`The MIME ${mime} is not supported as subtitle codec`);
|
|
103
105
|
}
|
|
104
106
|
};
|
|
@@ -250,15 +252,15 @@ const getDashTrackMatchKey = (track) => JSON.stringify({
|
|
|
250
252
|
const getSourcePath = (source) => {
|
|
251
253
|
if ("rootPath" in source && typeof source.rootPath === "string") return source.rootPath;
|
|
252
254
|
};
|
|
253
|
-
const normalizeHeaders = (headers) => {
|
|
255
|
+
const normalizeHeaders$1 = (headers) => {
|
|
254
256
|
if (!headers) return {};
|
|
255
257
|
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
|
|
256
258
|
if (Array.isArray(headers)) return Object.fromEntries(headers);
|
|
257
259
|
return { ...headers };
|
|
258
260
|
};
|
|
259
|
-
const getSourceHeaders = (source) => {
|
|
260
|
-
const requestHeaders = "_url" in source && source._url instanceof Request ? normalizeHeaders(source._url.headers) : {};
|
|
261
|
-
const optionHeaders = normalizeHeaders(("_options" in source && source._options && typeof source._options === "object" ? source._options : void 0)?.requestInit?.headers);
|
|
261
|
+
const getSourceHeaders$1 = (source) => {
|
|
262
|
+
const requestHeaders = "_url" in source && source._url instanceof Request ? normalizeHeaders$1(source._url.headers) : {};
|
|
263
|
+
const optionHeaders = normalizeHeaders$1(("_options" in source && source._options && typeof source._options === "object" ? source._options : void 0)?.requestInit?.headers);
|
|
262
264
|
return {
|
|
263
265
|
...requestHeaders,
|
|
264
266
|
...optionHeaders
|
|
@@ -269,7 +271,7 @@ const loadDashManifest = async (source) => {
|
|
|
269
271
|
const manifestPath = getSourcePath(source);
|
|
270
272
|
if (!manifestPath) throw new Error("DASH input currently requires a pathed source such as UrlSource.");
|
|
271
273
|
if (manifestPath.startsWith("http://") || manifestPath.startsWith("https://")) {
|
|
272
|
-
const response = await fetch(manifestPath, { headers: getSourceHeaders(source) });
|
|
274
|
+
const response = await fetch(manifestPath, { headers: getSourceHeaders$1(source) });
|
|
273
275
|
if (!response.ok) throw new Error(`Failed to fetch DASH manifest: ${response.status} ${response.statusText} (${response.url})`);
|
|
274
276
|
return {
|
|
275
277
|
text: await response.text(),
|
|
@@ -414,6 +416,581 @@ const parseDashRange = (range) => {
|
|
|
414
416
|
return [startRange, end - startRange + 1];
|
|
415
417
|
};
|
|
416
418
|
//#endregion
|
|
419
|
+
//#region src/hls/hls-subtitles.ts
|
|
420
|
+
const TAG_STREAM_INF = "#EXT-X-STREAM-INF:";
|
|
421
|
+
const TAG_MEDIA = "#EXT-X-MEDIA:";
|
|
422
|
+
const TAG_EXTINF = "#EXTINF:";
|
|
423
|
+
const TAG_MAP = "#EXT-X-MAP:";
|
|
424
|
+
const TAG_KEY = "#EXT-X-KEY:";
|
|
425
|
+
const TAG_MEDIA_SEQUENCE = "#EXT-X-MEDIA-SEQUENCE:";
|
|
426
|
+
const TAG_BYTERANGE = "#EXT-X-BYTERANGE:";
|
|
427
|
+
const TAG_PROGRAM_DATE_TIME = "#EXT-X-PROGRAM-DATE-TIME:";
|
|
428
|
+
const TAG_DISCONTINUITY = "#EXT-X-DISCONTINUITY";
|
|
429
|
+
const TAG_TARGETDURATION = "#EXT-X-TARGETDURATION:";
|
|
430
|
+
const TAG_ENDLIST = "#EXT-X-ENDLIST";
|
|
431
|
+
const TAG_PLAYLIST_TYPE = "#EXT-X-PLAYLIST-TYPE:";
|
|
432
|
+
const AES_128_BLOCK_SIZE = 16;
|
|
433
|
+
const IV_STRING_REGEX = /^0[xX][0-9a-fA-F]+$/;
|
|
434
|
+
const DEFAULT_TRACK_DISPOSITION$1 = {
|
|
435
|
+
commentary: false,
|
|
436
|
+
default: true,
|
|
437
|
+
forced: false,
|
|
438
|
+
hearingImpaired: false,
|
|
439
|
+
original: false,
|
|
440
|
+
primary: true,
|
|
441
|
+
visuallyImpaired: false
|
|
442
|
+
};
|
|
443
|
+
const subtitleBackingsCache = /* @__PURE__ */ new WeakMap();
|
|
444
|
+
const SRT_HEADER_REGEX = /(?:^|\n)\d+\s*\n(?:\d{2}:)?\d{2}:\d{2}[,.]\d{3}\s+-->\s+(?:\d{2}:)?\d{2}:\d{2}[,.]\d{3}/;
|
|
445
|
+
const TTML_MARKER_REGEX = /<tt(?:\s|>)/i;
|
|
446
|
+
const splitPlaylistLines = (text) => text.split(/\r?\n/g).map((line) => line.trim()).filter((line) => line.length > 0 && (!line.startsWith("#") || line.startsWith("#EXT")));
|
|
447
|
+
const normalizeHeaders = (headers) => {
|
|
448
|
+
if (!headers) return {};
|
|
449
|
+
if (headers instanceof Headers) return Object.fromEntries(headers.entries());
|
|
450
|
+
if (Array.isArray(headers)) return Object.fromEntries(headers);
|
|
451
|
+
return { ...headers };
|
|
452
|
+
};
|
|
453
|
+
const getSourceHeaders = (source) => {
|
|
454
|
+
const requestHeaders = source._url instanceof Request ? normalizeHeaders(source._url.headers) : {};
|
|
455
|
+
const optionHeaders = normalizeHeaders(source._options?.requestInit?.headers);
|
|
456
|
+
return {
|
|
457
|
+
...requestHeaders,
|
|
458
|
+
...optionHeaders
|
|
459
|
+
};
|
|
460
|
+
};
|
|
461
|
+
const joinHlsPath = (basePath, relativePath) => {
|
|
462
|
+
if (relativePath.includes("://")) return relativePath;
|
|
463
|
+
if (basePath.includes("://")) {
|
|
464
|
+
const queryIndex = basePath.indexOf("?");
|
|
465
|
+
if (queryIndex !== -1) basePath = basePath.slice(0, queryIndex);
|
|
466
|
+
}
|
|
467
|
+
let result;
|
|
468
|
+
if (relativePath.startsWith("/")) {
|
|
469
|
+
const protocolIndex = basePath.indexOf("://");
|
|
470
|
+
if (protocolIndex === -1) result = relativePath;
|
|
471
|
+
else {
|
|
472
|
+
const pathStart = basePath.indexOf("/", protocolIndex + 3);
|
|
473
|
+
result = pathStart === -1 ? basePath + relativePath : basePath.slice(0, pathStart) + relativePath;
|
|
474
|
+
}
|
|
475
|
+
} else {
|
|
476
|
+
const lastSlash = basePath.lastIndexOf("/");
|
|
477
|
+
result = lastSlash === -1 ? relativePath : basePath.slice(0, lastSlash + 1) + relativePath;
|
|
478
|
+
}
|
|
479
|
+
let prefix = "";
|
|
480
|
+
const protocolIndex = result.indexOf("://");
|
|
481
|
+
if (protocolIndex !== -1) {
|
|
482
|
+
const pathStart = result.indexOf("/", protocolIndex + 3);
|
|
483
|
+
if (pathStart !== -1) {
|
|
484
|
+
prefix = result.slice(0, pathStart);
|
|
485
|
+
result = result.slice(pathStart);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
const normalized = [];
|
|
489
|
+
for (const segment of result.split("/")) if (segment === "..") {
|
|
490
|
+
if (normalized.length === 0 || normalized.length === 1 && normalized[0] === "") throw new RangeError(`Invalid HLS path '${relativePath}': parent traversal exceeds root for base '${basePath}'.`);
|
|
491
|
+
normalized.pop();
|
|
492
|
+
} else if (segment !== ".") normalized.push(segment);
|
|
493
|
+
return prefix + normalized.join("/");
|
|
494
|
+
};
|
|
495
|
+
const toSegmentPath = (path) => path.includes("://") ? path : pathToFileURL(path).toString();
|
|
496
|
+
const parseAttributeBoolean = (value) => value?.toUpperCase() === "YES";
|
|
497
|
+
const stripQueryAndHash = (value) => value.split("#", 1)[0]?.split("?", 1)[0] ?? "";
|
|
498
|
+
const inferSubtitleCodecStringFromPath = (path) => {
|
|
499
|
+
const normalized = stripQueryAndHash(path).toLowerCase();
|
|
500
|
+
if (normalized.endsWith(".webvtt")) return "webvtt";
|
|
501
|
+
if (normalized.endsWith(".vtt")) return "vtt";
|
|
502
|
+
if (normalized.endsWith(".srt")) return "srt";
|
|
503
|
+
if (normalized.endsWith(".ttml")) return "ttml";
|
|
504
|
+
if (normalized.endsWith(".dfxp")) return "dfxp";
|
|
505
|
+
return null;
|
|
506
|
+
};
|
|
507
|
+
const parseDetectedSubtitleCodec = (codecString) => {
|
|
508
|
+
if (!codecString) return null;
|
|
509
|
+
const codec = tryParseSubtitleCodec(codecString);
|
|
510
|
+
if (!codec) return null;
|
|
511
|
+
return {
|
|
512
|
+
codec,
|
|
513
|
+
codecString
|
|
514
|
+
};
|
|
515
|
+
};
|
|
516
|
+
const sniffSubtitleCodecFromText = (text) => {
|
|
517
|
+
const normalized = text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").trimStart();
|
|
518
|
+
if (normalized.startsWith("WEBVTT")) return "webvtt";
|
|
519
|
+
if (TTML_MARKER_REGEX.test(normalized)) return "ttml";
|
|
520
|
+
if (SRT_HEADER_REGEX.test(normalized)) return "srt";
|
|
521
|
+
return null;
|
|
522
|
+
};
|
|
523
|
+
var AttributeList = class {
|
|
524
|
+
#attributes = {};
|
|
525
|
+
constructor(text) {
|
|
526
|
+
let key = "";
|
|
527
|
+
let value = "";
|
|
528
|
+
let inValue = false;
|
|
529
|
+
let inQuotes = false;
|
|
530
|
+
for (const char of text) {
|
|
531
|
+
if (char === "\"") {
|
|
532
|
+
inQuotes = !inQuotes;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (char === "=" && !inValue && !inQuotes) {
|
|
536
|
+
inValue = true;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (char === "," && !inQuotes) {
|
|
540
|
+
if (key) this.#attributes[key.trim().toLowerCase()] = value;
|
|
541
|
+
key = "";
|
|
542
|
+
value = "";
|
|
543
|
+
inValue = false;
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
if (inValue) value += char;
|
|
547
|
+
else key += char;
|
|
548
|
+
}
|
|
549
|
+
if (key) this.#attributes[key.trim().toLowerCase()] = value;
|
|
550
|
+
}
|
|
551
|
+
get(name) {
|
|
552
|
+
return this.#attributes[name.toLowerCase()] ?? null;
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
const loadPlaylistText = async (source, path) => {
|
|
556
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
557
|
+
const response = await fetch(path, { headers: getSourceHeaders(source) });
|
|
558
|
+
if (!response.ok) throw new Error(`Failed to fetch HLS playlist: ${response.status} ${response.statusText} (${response.url})`);
|
|
559
|
+
return {
|
|
560
|
+
path: response.url,
|
|
561
|
+
text: await response.text()
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (path.startsWith("file:")) return {
|
|
565
|
+
path,
|
|
566
|
+
text: await readFile(new URL(path), "utf8")
|
|
567
|
+
};
|
|
568
|
+
return {
|
|
569
|
+
path,
|
|
570
|
+
text: await readFile(path, "utf8")
|
|
571
|
+
};
|
|
572
|
+
};
|
|
573
|
+
const detectSubtitleCodecFromUri = async (source, uri) => {
|
|
574
|
+
const fromPath = parseDetectedSubtitleCodec(inferSubtitleCodecStringFromPath(uri));
|
|
575
|
+
if (fromPath) return fromPath;
|
|
576
|
+
const loaded = await loadPlaylistText(source, uri);
|
|
577
|
+
const fromText = parseDetectedSubtitleCodec(sniffSubtitleCodecFromText(loaded.text));
|
|
578
|
+
if (fromText) return fromText;
|
|
579
|
+
const lines = splitPlaylistLines(loaded.text);
|
|
580
|
+
if (lines[0] === "#EXTM3U") for (const line of lines.slice(1)) {
|
|
581
|
+
if (!line.startsWith("#")) {
|
|
582
|
+
const segmentUri = joinHlsPath(loaded.path, line);
|
|
583
|
+
const fromSegmentPath = parseDetectedSubtitleCodec(inferSubtitleCodecStringFromPath(segmentUri));
|
|
584
|
+
if (fromSegmentPath) return fromSegmentPath;
|
|
585
|
+
const fromSegmentText = parseDetectedSubtitleCodec(sniffSubtitleCodecFromText((await loadPlaylistText(source, segmentUri)).text));
|
|
586
|
+
if (fromSegmentText) return fromSegmentText;
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
if (!line.startsWith(TAG_MAP)) continue;
|
|
590
|
+
const mapUri = new AttributeList(line.slice(11)).get("uri");
|
|
591
|
+
if (!mapUri) continue;
|
|
592
|
+
const fromMapPath = parseDetectedSubtitleCodec(inferSubtitleCodecStringFromPath(joinHlsPath(loaded.path, mapUri)));
|
|
593
|
+
if (fromMapPath) return fromMapPath;
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
codec: "webvtt",
|
|
597
|
+
codecString: "webvtt"
|
|
598
|
+
};
|
|
599
|
+
};
|
|
600
|
+
const parseMediaRange = (value) => {
|
|
601
|
+
const separatorIndex = value.indexOf("@");
|
|
602
|
+
const length = Number(separatorIndex === -1 ? value : value.slice(0, separatorIndex));
|
|
603
|
+
if (!Number.isInteger(length) || length < 0) throw new Error(`Invalid #EXT-X-BYTERANGE length '${value}'.`);
|
|
604
|
+
const offsetValue = separatorIndex === -1 ? null : Number(value.slice(separatorIndex + 1));
|
|
605
|
+
if (offsetValue !== null && (!Number.isInteger(offsetValue) || offsetValue < 0)) throw new Error(`Invalid #EXT-X-BYTERANGE offset '${value}'.`);
|
|
606
|
+
return {
|
|
607
|
+
length,
|
|
608
|
+
offset: offsetValue
|
|
609
|
+
};
|
|
610
|
+
};
|
|
611
|
+
const parseHexIv = (value) => {
|
|
612
|
+
if (!IV_STRING_REGEX.test(value)) throw new Error(`Unsupported IV format '${value}'.`);
|
|
613
|
+
let hex = value.slice(2);
|
|
614
|
+
hex = hex.padStart(AES_128_BLOCK_SIZE * 2, "0");
|
|
615
|
+
const iv = new Uint8Array(AES_128_BLOCK_SIZE);
|
|
616
|
+
for (let index = 0; index < AES_128_BLOCK_SIZE; index++) {
|
|
617
|
+
const startIndex = index * 2;
|
|
618
|
+
iv[index] = Number.parseInt(hex.slice(startIndex, startIndex + 2), 16);
|
|
619
|
+
}
|
|
620
|
+
return iv;
|
|
621
|
+
};
|
|
622
|
+
const createSequenceIv = (sequenceNumber) => {
|
|
623
|
+
const iv = new Uint8Array(AES_128_BLOCK_SIZE);
|
|
624
|
+
const view = new DataView(iv.buffer, iv.byteOffset, iv.byteLength);
|
|
625
|
+
view.setUint32(8, Math.floor(sequenceNumber / 2 ** 32));
|
|
626
|
+
view.setUint32(12, sequenceNumber);
|
|
627
|
+
return iv;
|
|
628
|
+
};
|
|
629
|
+
var HlsSubtitlePlaylist = class {
|
|
630
|
+
segments = [];
|
|
631
|
+
#source;
|
|
632
|
+
#playlistPath;
|
|
633
|
+
#refreshIntervalSeconds = 5;
|
|
634
|
+
#streamHasEnded = false;
|
|
635
|
+
#currentUpdatePromise = null;
|
|
636
|
+
#lastUpdateTime = -Infinity;
|
|
637
|
+
constructor(source, playlistPath) {
|
|
638
|
+
this.#source = source;
|
|
639
|
+
this.#playlistPath = playlistPath;
|
|
640
|
+
}
|
|
641
|
+
runUpdateSegments() {
|
|
642
|
+
return this.#currentUpdatePromise ??= (async () => {
|
|
643
|
+
try {
|
|
644
|
+
await this.#maybeWaitForRefresh();
|
|
645
|
+
await this.#reloadSegments();
|
|
646
|
+
} finally {
|
|
647
|
+
this.#currentUpdatePromise = null;
|
|
648
|
+
}
|
|
649
|
+
})();
|
|
650
|
+
}
|
|
651
|
+
async getDurationFromMetadata(_options) {
|
|
652
|
+
await this.runUpdateSegments();
|
|
653
|
+
const lastSegment = this.segments.at(-1);
|
|
654
|
+
return lastSegment ? lastSegment.timestamp + lastSegment.duration : null;
|
|
655
|
+
}
|
|
656
|
+
async getLiveRefreshInterval() {
|
|
657
|
+
await this.runUpdateSegments();
|
|
658
|
+
return this.#streamHasEnded ? null : this.#refreshIntervalSeconds;
|
|
659
|
+
}
|
|
660
|
+
async isRelativeToUnixEpoch() {
|
|
661
|
+
await this.runUpdateSegments();
|
|
662
|
+
return this.segments.some((segment) => segment.relativeToUnixEpoch);
|
|
663
|
+
}
|
|
664
|
+
async #maybeWaitForRefresh() {
|
|
665
|
+
if (this.#streamHasEnded || this.#lastUpdateTime === -Infinity) return;
|
|
666
|
+
const elapsed = performance.now() - this.#lastUpdateTime;
|
|
667
|
+
const remaining = Math.max(0, this.#refreshIntervalSeconds * 1e3 - elapsed);
|
|
668
|
+
if (remaining > 50) await new Promise((resolve) => setTimeout(resolve, remaining));
|
|
669
|
+
}
|
|
670
|
+
async #reloadSegments() {
|
|
671
|
+
this.#lastUpdateTime = performance.now();
|
|
672
|
+
const loaded = await loadPlaylistText(this.#source, this.#playlistPath);
|
|
673
|
+
this.#playlistPath = loaded.path;
|
|
674
|
+
const lines = splitPlaylistLines(loaded.text);
|
|
675
|
+
if (lines[0] !== "#EXTM3U") throw new Error("Invalid M3U8 file; expected first line to be #EXTM3U.");
|
|
676
|
+
let accumulatedTime = 0;
|
|
677
|
+
let nextDuration = null;
|
|
678
|
+
let currentKey = null;
|
|
679
|
+
let nextSequenceNumber = 0;
|
|
680
|
+
let currentFirstSegment = null;
|
|
681
|
+
let currentInitSegment = null;
|
|
682
|
+
let nextByteRange = null;
|
|
683
|
+
let lastByteRangeEnd = null;
|
|
684
|
+
let lastProgramDateTimeSeconds = null;
|
|
685
|
+
let targetDuration = null;
|
|
686
|
+
let segmentSeen = false;
|
|
687
|
+
const segments = [];
|
|
688
|
+
let streamHasEnded = false;
|
|
689
|
+
for (const line of lines.slice(1)) {
|
|
690
|
+
if (!line.startsWith("#")) {
|
|
691
|
+
if (nextDuration === null) throw new Error("Invalid M3U8 file; a segment must be preceded by an #EXTINF tag.");
|
|
692
|
+
const location = {
|
|
693
|
+
path: toSegmentPath(joinHlsPath(this.#playlistPath, line)),
|
|
694
|
+
offset: nextByteRange?.offset ?? 0,
|
|
695
|
+
length: nextByteRange?.length ?? null
|
|
696
|
+
};
|
|
697
|
+
const encryption = currentKey?.method === "AES-128" && !currentKey.iv ? {
|
|
698
|
+
...currentKey,
|
|
699
|
+
iv: createSequenceIv(nextSequenceNumber)
|
|
700
|
+
} : currentKey;
|
|
701
|
+
const segment = {
|
|
702
|
+
timestamp: accumulatedTime,
|
|
703
|
+
duration: nextDuration,
|
|
704
|
+
relativeToUnixEpoch: lastProgramDateTimeSeconds !== null,
|
|
705
|
+
firstSegment: currentFirstSegment,
|
|
706
|
+
sequenceNumber: nextSequenceNumber,
|
|
707
|
+
location,
|
|
708
|
+
encryption,
|
|
709
|
+
initSegment: currentInitSegment,
|
|
710
|
+
lastProgramDateTimeSeconds
|
|
711
|
+
};
|
|
712
|
+
currentFirstSegment ??= segment;
|
|
713
|
+
accumulatedTime += nextDuration;
|
|
714
|
+
segments.push(segment);
|
|
715
|
+
nextDuration = null;
|
|
716
|
+
nextSequenceNumber += 1;
|
|
717
|
+
if (nextByteRange === null) lastByteRangeEnd = null;
|
|
718
|
+
else nextByteRange = null;
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (line.startsWith(TAG_EXTINF)) {
|
|
722
|
+
if (!segmentSeen) {
|
|
723
|
+
if (lastProgramDateTimeSeconds === null && nextSequenceNumber > 0 && targetDuration !== null) accumulatedTime = nextSequenceNumber * targetDuration;
|
|
724
|
+
segmentSeen = true;
|
|
725
|
+
}
|
|
726
|
+
const content = line.slice(8);
|
|
727
|
+
const commaIndex = content.indexOf(",");
|
|
728
|
+
const durationString = commaIndex === -1 ? content : content.slice(0, commaIndex);
|
|
729
|
+
const duration = Number(durationString);
|
|
730
|
+
if (!Number.isFinite(duration) || duration < 0) throw new Error(`Invalid #EXTINF tag duration '${durationString}'.`);
|
|
731
|
+
nextDuration = duration;
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
|
|
735
|
+
const value = Number(line.slice(22));
|
|
736
|
+
if (!Number.isInteger(value) || value < 0) throw new Error(`Invalid EXT-X-MEDIA-SEQUENCE value '${line.slice(22)}'.`);
|
|
737
|
+
nextSequenceNumber = value;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (line.startsWith(TAG_KEY)) {
|
|
741
|
+
const attributes = new AttributeList(line.slice(11));
|
|
742
|
+
const method = attributes.get("method");
|
|
743
|
+
if (method === "NONE") {
|
|
744
|
+
currentKey = null;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (method === "AES-128") {
|
|
748
|
+
const uri = attributes.get("uri");
|
|
749
|
+
if (!uri) throw new Error("Invalid #EXT-X-KEY: AES-128 requires a URI attribute.");
|
|
750
|
+
const iv = attributes.get("iv");
|
|
751
|
+
const keyFormat = attributes.get("keyformat") ?? "identity";
|
|
752
|
+
if (keyFormat !== "identity") throw new Error("For AES-128 encryption, only the 'identity' KEYFORMAT is currently supported.");
|
|
753
|
+
currentKey = {
|
|
754
|
+
method,
|
|
755
|
+
keyUri: joinHlsPath(this.#playlistPath, uri),
|
|
756
|
+
iv: iv ? parseHexIv(iv) : null,
|
|
757
|
+
keyFormat
|
|
758
|
+
};
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (method === "SAMPLE-AES" || method === "SAMPLE-AES-CTR") {
|
|
762
|
+
currentKey = { method };
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
throw new Error(`Unsupported encryption method '${method}'.`);
|
|
766
|
+
}
|
|
767
|
+
if (line.startsWith(TAG_BYTERANGE)) {
|
|
768
|
+
const range = parseMediaRange(line.slice(17));
|
|
769
|
+
const nextOffset = range.offset ?? lastByteRangeEnd ?? 0;
|
|
770
|
+
nextByteRange = {
|
|
771
|
+
offset: nextOffset,
|
|
772
|
+
length: range.length
|
|
773
|
+
};
|
|
774
|
+
lastByteRangeEnd = nextOffset + range.length;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (line.startsWith(TAG_MAP)) {
|
|
778
|
+
const attributes = new AttributeList(line.slice(11));
|
|
779
|
+
const uri = attributes.get("uri");
|
|
780
|
+
if (uri === null) throw new Error("Invalid M3U8 file; #EXT-X-MAP tag requires a URI attribute.");
|
|
781
|
+
const byterange = attributes.get("byterange");
|
|
782
|
+
const range = byterange ? parseMediaRange(byterange) : null;
|
|
783
|
+
currentInitSegment = {
|
|
784
|
+
timestamp: 0,
|
|
785
|
+
duration: 0,
|
|
786
|
+
relativeToUnixEpoch: false,
|
|
787
|
+
firstSegment: null,
|
|
788
|
+
sequenceNumber: -1,
|
|
789
|
+
location: {
|
|
790
|
+
path: toSegmentPath(joinHlsPath(this.#playlistPath, uri)),
|
|
791
|
+
offset: range?.offset ?? 0,
|
|
792
|
+
length: range?.length ?? null
|
|
793
|
+
},
|
|
794
|
+
encryption: null,
|
|
795
|
+
initSegment: null,
|
|
796
|
+
lastProgramDateTimeSeconds: null
|
|
797
|
+
};
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
|
|
801
|
+
const dateTimeString = line.slice(25);
|
|
802
|
+
const dateTimeSeconds = new Date(dateTimeString).getTime() / 1e3;
|
|
803
|
+
if (!Number.isFinite(dateTimeSeconds)) throw new Error(`Invalid EXT-X-PROGRAM-DATE-TIME value '${dateTimeString}'.`);
|
|
804
|
+
if (segments.length > 0 && lastProgramDateTimeSeconds === null) {
|
|
805
|
+
const lastSegment = segments.at(-1);
|
|
806
|
+
if (!lastSegment) throw new Error("Expected at least one prior HLS segment.");
|
|
807
|
+
const offset = dateTimeSeconds - (lastSegment.timestamp + lastSegment.duration);
|
|
808
|
+
for (const segment of segments) {
|
|
809
|
+
segment.timestamp += offset;
|
|
810
|
+
segment.relativeToUnixEpoch = true;
|
|
811
|
+
}
|
|
812
|
+
accumulatedTime += offset;
|
|
813
|
+
}
|
|
814
|
+
lastProgramDateTimeSeconds = dateTimeSeconds;
|
|
815
|
+
accumulatedTime = dateTimeSeconds;
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
if (line === TAG_DISCONTINUITY) {
|
|
819
|
+
currentFirstSegment = null;
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
if (line.startsWith(TAG_TARGETDURATION)) {
|
|
823
|
+
const duration = Number(line.slice(22));
|
|
824
|
+
if (!Number.isFinite(duration) || duration < 0) throw new Error(`Invalid EXT-X-TARGETDURATION value '${line.slice(22)}'.`);
|
|
825
|
+
this.#refreshIntervalSeconds = duration;
|
|
826
|
+
targetDuration = duration;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (line === TAG_ENDLIST) {
|
|
830
|
+
streamHasEnded = true;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
if (line.startsWith(TAG_PLAYLIST_TYPE)) {
|
|
834
|
+
if (line.slice(21).toLowerCase() === "vod") streamHasEnded = true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
this.segments = segments;
|
|
838
|
+
this.#streamHasEnded = streamHasEnded;
|
|
839
|
+
}
|
|
840
|
+
};
|
|
841
|
+
var HlsSubtitleTrackBacking = class {
|
|
842
|
+
#id;
|
|
843
|
+
#number;
|
|
844
|
+
#pairingMask;
|
|
845
|
+
#track;
|
|
846
|
+
#segmentedInput;
|
|
847
|
+
constructor(params) {
|
|
848
|
+
this.#id = params.id;
|
|
849
|
+
this.#number = params.number;
|
|
850
|
+
this.#pairingMask = params.pairingMask;
|
|
851
|
+
this.#track = params.track;
|
|
852
|
+
this.#segmentedInput = new HlsSubtitlePlaylist(params.source, params.track.uri);
|
|
853
|
+
}
|
|
854
|
+
getType() {
|
|
855
|
+
return "subtitle";
|
|
856
|
+
}
|
|
857
|
+
getId() {
|
|
858
|
+
return this.#id;
|
|
859
|
+
}
|
|
860
|
+
getNumber() {
|
|
861
|
+
return this.#number;
|
|
862
|
+
}
|
|
863
|
+
getCodec() {
|
|
864
|
+
return this.#track.codec;
|
|
865
|
+
}
|
|
866
|
+
getInternalCodecId() {
|
|
867
|
+
return null;
|
|
868
|
+
}
|
|
869
|
+
getName() {
|
|
870
|
+
return this.#track.name;
|
|
871
|
+
}
|
|
872
|
+
getLanguageCode() {
|
|
873
|
+
return this.#track.languageCode || "und";
|
|
874
|
+
}
|
|
875
|
+
getTimeResolution() {
|
|
876
|
+
return 1e3;
|
|
877
|
+
}
|
|
878
|
+
isRelativeToUnixEpoch() {
|
|
879
|
+
return this.#segmentedInput.isRelativeToUnixEpoch();
|
|
880
|
+
}
|
|
881
|
+
getDisposition() {
|
|
882
|
+
return {
|
|
883
|
+
...DEFAULT_TRACK_DISPOSITION$1,
|
|
884
|
+
default: this.#track.autoselect,
|
|
885
|
+
primary: this.#track.default,
|
|
886
|
+
forced: this.#track.forced,
|
|
887
|
+
hearingImpaired: this.#track.hearingImpaired
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
getPairingMask() {
|
|
891
|
+
return this.#pairingMask;
|
|
892
|
+
}
|
|
893
|
+
getBitrate() {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
getAverageBitrate() {
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
getDurationFromMetadata(options) {
|
|
900
|
+
return this.#segmentedInput.getDurationFromMetadata(options);
|
|
901
|
+
}
|
|
902
|
+
getLiveRefreshInterval() {
|
|
903
|
+
return this.#segmentedInput.getLiveRefreshInterval();
|
|
904
|
+
}
|
|
905
|
+
getHasOnlyKeyPackets() {
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
async getDecoderConfig() {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
getMetadataCodecParameterString() {
|
|
912
|
+
return this.#track.codecString;
|
|
913
|
+
}
|
|
914
|
+
async getFirstPacket(_options) {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
async getPacket(_timestamp, _options) {
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
async getNextPacket(_packet, _options) {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
async getKeyPacket(_timestamp, _options) {
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
async getNextKeyPacket(_packet, _options) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
getSegmentedInput() {
|
|
930
|
+
return this.#segmentedInput;
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
const parseMasterPlaylistSubtitles = async (input) => {
|
|
934
|
+
const source = input.source;
|
|
935
|
+
if (!("rootPath" in source) || typeof source.rootPath !== "string") return [];
|
|
936
|
+
const loaded = await loadPlaylistText(source, source.rootPath);
|
|
937
|
+
const lines = splitPlaylistLines(loaded.text);
|
|
938
|
+
if (lines[0] !== "#EXTM3U") return [];
|
|
939
|
+
const subtitleMediaTags = [];
|
|
940
|
+
const pairingMasks = /* @__PURE__ */ new Map();
|
|
941
|
+
let nextPairIndex = 0n;
|
|
942
|
+
for (let index = 1; index < lines.length; index++) {
|
|
943
|
+
const line = lines[index];
|
|
944
|
+
if (!line) continue;
|
|
945
|
+
if (line.startsWith(TAG_EXTINF)) return [];
|
|
946
|
+
if (line.startsWith(TAG_STREAM_INF)) {
|
|
947
|
+
const groupId = new AttributeList(line.slice(18)).get("subtitles");
|
|
948
|
+
if (groupId !== null) pairingMasks.set(groupId, (pairingMasks.get(groupId) ?? 0n) | 1n << nextPairIndex);
|
|
949
|
+
nextPairIndex += 1n;
|
|
950
|
+
index += 1;
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
if (!line.startsWith(TAG_MEDIA)) continue;
|
|
954
|
+
const attributes = new AttributeList(line.slice(13));
|
|
955
|
+
if (attributes.get("type")?.toLowerCase() !== "subtitles") continue;
|
|
956
|
+
const groupId = attributes.get("group-id");
|
|
957
|
+
const uri = attributes.get("uri");
|
|
958
|
+
if (!groupId || !uri) continue;
|
|
959
|
+
const characteristics = (attributes.get("characteristics") ?? "").split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
960
|
+
const name = attributes.get("name")?.trim() ?? null;
|
|
961
|
+
const hearingImpaired = characteristics.includes("public.accessibility.transcribes-spoken-dialog") || characteristics.includes("public.accessibility.describes-music-and-sound") || name?.toLowerCase().includes("sdh") === true;
|
|
962
|
+
subtitleMediaTags.push({
|
|
963
|
+
autoselect: parseAttributeBoolean(attributes.get("default")) || parseAttributeBoolean(attributes.get("autoselect")),
|
|
964
|
+
default: parseAttributeBoolean(attributes.get("default")),
|
|
965
|
+
forced: parseAttributeBoolean(attributes.get("forced")),
|
|
966
|
+
groupId,
|
|
967
|
+
hearingImpaired,
|
|
968
|
+
languageCode: attributes.get("language") ?? "und",
|
|
969
|
+
name,
|
|
970
|
+
uri: joinHlsPath(loaded.path, uri)
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
const detectedSubtitleMediaTags = await Promise.all(subtitleMediaTags.map(async (track) => ({
|
|
974
|
+
...track,
|
|
975
|
+
...await detectSubtitleCodecFromUri(source, track.uri)
|
|
976
|
+
})));
|
|
977
|
+
const nativeTrackCount = await input._getTrackBackings().then((backings) => backings.length);
|
|
978
|
+
return detectedSubtitleMediaTags.map((track, index) => new HlsSubtitleTrackBacking({
|
|
979
|
+
id: nativeTrackCount + index + 1,
|
|
980
|
+
number: index + 1,
|
|
981
|
+
pairingMask: pairingMasks.get(track.groupId) ?? 0n,
|
|
982
|
+
source,
|
|
983
|
+
track
|
|
984
|
+
}));
|
|
985
|
+
};
|
|
986
|
+
const getHlsSubtitleTrackBackings = (input) => {
|
|
987
|
+
const existing = subtitleBackingsCache.get(input);
|
|
988
|
+
if (existing) return existing;
|
|
989
|
+
const promise = parseMasterPlaylistSubtitles(input);
|
|
990
|
+
subtitleBackingsCache.set(input, promise);
|
|
991
|
+
return promise;
|
|
992
|
+
};
|
|
993
|
+
//#endregion
|
|
417
994
|
//#region src/mediabunny-input.ts
|
|
418
995
|
const requireSync = (value, getterName, asyncName) => {
|
|
419
996
|
if (value instanceof Promise) throw new Error(`'${getterName}' is not available synchronously for this track. Use '${asyncName}()' instead.`);
|
|
@@ -454,9 +1031,10 @@ const queryWrappedTracks = (input, backings, query) => {
|
|
|
454
1031
|
return queryTracks(backings.map((backing) => input._wrapBackingAsTrack(backing)), query);
|
|
455
1032
|
};
|
|
456
1033
|
const getTrackBackingsByType = async (input, type) => {
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
|
|
1034
|
+
const nativeBackings = await input._getTrackBackings();
|
|
1035
|
+
const syntheticBackings = await input._getSyntheticTrackBackings?.(type) ?? [];
|
|
1036
|
+
const backings = [...nativeBackings, ...syntheticBackings];
|
|
1037
|
+
return type ? backings.filter((backing) => getBackingType(backing) === type) : backings;
|
|
460
1038
|
};
|
|
461
1039
|
const patchBaseMediabunnyInput = () => {
|
|
462
1040
|
const prototype = Input$1.prototype;
|
|
@@ -525,6 +1103,7 @@ const preserveSubtitleBackingsOnInput = (input) => {
|
|
|
525
1103
|
var SegmentedMediabunnyInput = class extends Input$1 {
|
|
526
1104
|
#trackCache = /* @__PURE__ */ new WeakMap();
|
|
527
1105
|
#subtitleTrackCache = /* @__PURE__ */ new WeakMap();
|
|
1106
|
+
#hlsSubtitleBackingsPromise = null;
|
|
528
1107
|
async #queryTracks(query, type) {
|
|
529
1108
|
const backings = await getTrackBackingsByType(this, type);
|
|
530
1109
|
return queryWrappedTracks(this, backings, query);
|
|
@@ -537,6 +1116,18 @@ var SegmentedMediabunnyInput = class extends Input$1 {
|
|
|
537
1116
|
this.#trackCache.set(track, wrapped);
|
|
538
1117
|
return wrapped;
|
|
539
1118
|
}
|
|
1119
|
+
async _getSyntheticTrackBackings(type) {
|
|
1120
|
+
if (type && type !== BACKING_TYPE_SUBTITLE) return [];
|
|
1121
|
+
if (await this.getFormat() !== HLS) return [];
|
|
1122
|
+
if (!this.#hlsSubtitleBackingsPromise) {
|
|
1123
|
+
const promise = getHlsSubtitleTrackBackings(this).catch((error) => {
|
|
1124
|
+
if (this.#hlsSubtitleBackingsPromise === promise) this.#hlsSubtitleBackingsPromise = null;
|
|
1125
|
+
throw error;
|
|
1126
|
+
});
|
|
1127
|
+
this.#hlsSubtitleBackingsPromise = promise;
|
|
1128
|
+
}
|
|
1129
|
+
return this.#hlsSubtitleBackingsPromise;
|
|
1130
|
+
}
|
|
540
1131
|
#wrapSubtitleBacking(backing) {
|
|
541
1132
|
const existing = this.#subtitleTrackCache.get(backing);
|
|
542
1133
|
if (existing) return existing;
|
|
@@ -650,7 +1241,7 @@ var DashSegmentedInput = class {
|
|
|
650
1241
|
return this.currentUpdateSegmentsPromise ??= (async () => {
|
|
651
1242
|
try {
|
|
652
1243
|
const remainingWaitTimeMs = this.getRemainingWaitTimeMs();
|
|
653
|
-
if (remainingWaitTimeMs > 0) await setTimeout(remainingWaitTimeMs);
|
|
1244
|
+
if (remainingWaitTimeMs > 0) await setTimeout$1(remainingWaitTimeMs);
|
|
654
1245
|
this.lastSegmentUpdateTime = performance.now();
|
|
655
1246
|
await this.updateSegments();
|
|
656
1247
|
} finally {
|
|
@@ -1349,7 +1940,7 @@ var DashDemuxer = class {
|
|
|
1349
1940
|
baseUrl = "";
|
|
1350
1941
|
constructor(input) {
|
|
1351
1942
|
this.input = input;
|
|
1352
|
-
this.headers = getSourceHeaders(input.source);
|
|
1943
|
+
this.headers = getSourceHeaders$1(input.source);
|
|
1353
1944
|
}
|
|
1354
1945
|
readMetadata() {
|
|
1355
1946
|
return this.metadataPromise ??= (async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dasha",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Streaming manifest parser",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist"
|
|
@@ -52,13 +52,13 @@
|
|
|
52
52
|
"temporal-polyfill": "^0.3.2"
|
|
53
53
|
},
|
|
54
54
|
"peerDependencies": {
|
|
55
|
-
"mediabunny": "
|
|
55
|
+
"mediabunny": "^1.44.2"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@types/node": "^25.6.
|
|
59
|
-
"oxfmt": "^0.
|
|
60
|
-
"oxlint": "^1.
|
|
61
|
-
"tsdown": "^0.
|
|
58
|
+
"@types/node": "^25.6.2",
|
|
59
|
+
"oxfmt": "^0.48.0",
|
|
60
|
+
"oxlint": "^1.63.0",
|
|
61
|
+
"tsdown": "^0.22.0",
|
|
62
62
|
"typescript": "^6.0.3",
|
|
63
63
|
"vitest": "^4.1.5"
|
|
64
64
|
},
|