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 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", "vtt", "ttml", "dfxp", "ssa", "ass", "stpp", "wvtt"];
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$1 = VideoCodec$1 | AudioCodec$1 | SubtitleCodec;
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$1;
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 TrackBacking = Parameters<Input$1<Source>['_wrapBackingAsTrack']>[0];
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(".")[0]) {
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 "vtt": return "vtt";
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 backings = await input._getTrackBackings();
458
- if (!type) return backings;
459
- return backings.filter((backing) => getBackingType(backing) === type);
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",
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": ">=1.43.1"
55
+ "mediabunny": "^1.44.2"
56
56
  },
57
57
  "devDependencies": {
58
- "@types/node": "^25.6.0",
59
- "oxfmt": "^0.46.0",
60
- "oxlint": "^1.61.0",
61
- "tsdown": "^0.21.10",
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
  },