@amazon/vinyl-hls-parser 1.1.1 → 2.0.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.
Files changed (84) hide show
  1. package/dist/hlsTestAssets/types/current/tsconfig.tsbuildinfo +1 -1
  2. package/dist/index.cjs +140 -58
  3. package/dist/index.cjs.map +4 -4
  4. package/dist/index.js +139 -57
  5. package/dist/index.js.map +4 -4
  6. package/dist/test/benchmark.es5.js +79 -255
  7. package/dist/test/benchmark.js +584 -1224
  8. package/dist/test/benchmark.js.map +4 -4
  9. package/dist/types/current/index.d.ts +11 -10
  10. package/dist/types/current/index.d.ts.map +1 -1
  11. package/dist/types/current/index.js +11 -10
  12. package/dist/types/current/parse/parseMainPlaylist.d.ts +8 -3
  13. package/dist/types/current/parse/parseMainPlaylist.d.ts.map +1 -1
  14. package/dist/types/current/parse/parseMainPlaylist.js +43 -5
  15. package/dist/types/current/parse/parseMediaPlaylist.d.ts +11 -4
  16. package/dist/types/current/parse/parseMediaPlaylist.d.ts.map +1 -1
  17. package/dist/types/current/parse/parseMediaPlaylist.js +111 -7
  18. package/dist/types/current/tsconfig.tsbuildinfo +1 -1
  19. package/dist/types/current/types/AlternativeRendition.d.ts +13 -0
  20. package/dist/types/current/types/AlternativeRendition.d.ts.map +1 -1
  21. package/dist/types/current/types/HlsDateRange.d.ts +51 -0
  22. package/dist/types/current/types/HlsDateRange.d.ts.map +1 -0
  23. package/dist/types/current/types/HlsDateRange.js +11 -0
  24. package/dist/types/{ts4.7/types/MainPlaylist.d.ts → current/types/HlsMainPlaylist.d.ts} +4 -4
  25. package/dist/types/current/types/HlsMainPlaylist.d.ts.map +1 -0
  26. package/dist/types/{ts4.1/types/MediaPlaylist.d.ts → current/types/HlsMediaPlaylist.d.ts} +11 -4
  27. package/dist/types/current/types/HlsMediaPlaylist.d.ts.map +1 -0
  28. package/dist/types/ts3.7/index.d.ts +11 -10
  29. package/dist/types/ts3.7/parse/parseMainPlaylist.d.ts +8 -3
  30. package/dist/types/ts3.7/parse/parseMediaPlaylist.d.ts +11 -4
  31. package/dist/types/ts3.7/types/AlternativeRendition.d.ts +13 -0
  32. package/dist/types/ts3.7/types/HlsDateRange.d.ts +51 -0
  33. package/dist/types/ts3.7/types/{MainPlaylist.d.ts → HlsMainPlaylist.d.ts} +4 -4
  34. package/dist/types/ts3.7/types/{MediaPlaylist.d.ts → HlsMediaPlaylist.d.ts} +11 -4
  35. package/dist/types/ts4.1/index.d.ts +11 -10
  36. package/dist/types/ts4.1/parse/parseMainPlaylist.d.ts +8 -3
  37. package/dist/types/ts4.1/parse/parseMediaPlaylist.d.ts +11 -4
  38. package/dist/types/ts4.1/types/AlternativeRendition.d.ts +13 -0
  39. package/dist/types/ts4.1/types/HlsDateRange.d.ts +51 -0
  40. package/dist/types/{current/types/MainPlaylist.d.ts → ts4.1/types/HlsMainPlaylist.d.ts} +4 -4
  41. package/dist/types/{current/types/MediaPlaylist.d.ts → ts4.1/types/HlsMediaPlaylist.d.ts} +11 -4
  42. package/dist/types/ts4.7/index.d.ts +11 -10
  43. package/dist/types/ts4.7/parse/parseMainPlaylist.d.ts +8 -3
  44. package/dist/types/ts4.7/parse/parseMediaPlaylist.d.ts +11 -4
  45. package/dist/types/ts4.7/types/AlternativeRendition.d.ts +13 -0
  46. package/dist/types/ts4.7/types/HlsDateRange.d.ts +51 -0
  47. package/dist/types/{ts4.1/types/MainPlaylist.d.ts → ts4.7/types/HlsMainPlaylist.d.ts} +4 -4
  48. package/dist/types/ts4.7/types/{MediaPlaylist.d.ts → HlsMediaPlaylist.d.ts} +11 -4
  49. package/package.json +30 -19
  50. package/dist/test/benchmark.css +0 -71
  51. package/dist/test/benchmark.html +0 -67
  52. package/dist/test/favicon.png +0 -0
  53. package/dist/test/index.html +0 -61
  54. package/dist/test/integ.es5.js +0 -410
  55. package/dist/test/integ.js +0 -2127
  56. package/dist/test/integ.js.map +0 -7
  57. package/dist/test/jasmine.css +0 -304
  58. package/dist/test/jasmineHtml.es5.js +0 -670
  59. package/dist/test/jasmineHtml.js +0 -10851
  60. package/dist/test/jasmineHtml.js.map +0 -7
  61. package/dist/test/styles.css +0 -42
  62. package/dist/types/current/types/MainPlaylist.d.ts.map +0 -1
  63. package/dist/types/current/types/MediaPlaylist.d.ts.map +0 -1
  64. package/dist/unit/types/current/index.d.ts +0 -7
  65. package/dist/unit/types/current/index.d.ts.map +0 -1
  66. package/dist/unit/types/current/index.js +0 -6
  67. package/dist/unit/types/current/parse/parseAttributes.test.d.ts +0 -2
  68. package/dist/unit/types/current/parse/parseAttributes.test.d.ts.map +0 -1
  69. package/dist/unit/types/current/parse/parseAttributes.test.js +0 -70
  70. package/dist/unit/types/current/parse/parseMainPlaylist.test.d.ts +0 -2
  71. package/dist/unit/types/current/parse/parseMainPlaylist.test.d.ts.map +0 -1
  72. package/dist/unit/types/current/parse/parseMainPlaylist.test.js +0 -194
  73. package/dist/unit/types/current/parse/parseMediaPlaylist.test.d.ts +0 -2
  74. package/dist/unit/types/current/parse/parseMediaPlaylist.test.d.ts.map +0 -1
  75. package/dist/unit/types/current/parse/parseMediaPlaylist.test.js +0 -401
  76. package/dist/unit/types/current/parse/parseUtil.test.d.ts +0 -2
  77. package/dist/unit/types/current/parse/parseUtil.test.d.ts.map +0 -1
  78. package/dist/unit/types/current/parse/parseUtil.test.js +0 -67
  79. package/dist/unit/types/current/setup.d.ts +0 -2
  80. package/dist/unit/types/current/setup.d.ts.map +0 -1
  81. package/dist/unit/types/current/setup.js +0 -19
  82. package/dist/unit/types/current/tsconfig.tsbuildinfo +0 -1
  83. /package/dist/types/current/types/{MainPlaylist.js → HlsMainPlaylist.js} +0 -0
  84. /package/dist/types/current/types/{MediaPlaylist.js → HlsMediaPlaylist.js} +0 -0
@@ -1,13 +1,14 @@
1
1
  /** Auto-generated from build scripts */
2
- export * from './types/VariantStream';
3
- export * from './types/SessionData';
4
- export * from './types/MediaPlaylist';
5
- export * from './types/MainPlaylist';
6
- export * from './types/HlsSegment';
7
- export * from './types/EncryptionKey';
8
- export * from './types/AlternativeRendition';
9
- export * from './parse/parseUtil';
10
- export * from './parse/parseMediaPlaylist';
11
- export * from './parse/parseMainPlaylist';
12
2
  export * from './parse/parseAttributes';
3
+ export * from './parse/parseMainPlaylist';
4
+ export * from './parse/parseMediaPlaylist';
5
+ export * from './parse/parseUtil';
6
+ export * from './types/AlternativeRendition';
7
+ export * from './types/EncryptionKey';
8
+ export * from './types/HlsDateRange';
9
+ export * from './types/HlsMainPlaylist';
10
+ export * from './types/HlsMediaPlaylist';
11
+ export * from './types/HlsSegment';
12
+ export * from './types/SessionData';
13
+ export * from './types/VariantStream';
13
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,cAAc,uBAAuB,CAAA;AACrC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,mBAAmB,CAAA;AACjC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,2BAA2B,CAAA;AACzC,cAAc,yBAAyB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AACxC,cAAc,yBAAyB,CAAA;AACvC,cAAc,2BAA2B,CAAA;AACzC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,mBAAmB,CAAA;AACjC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,uBAAuB,CAAA;AACrC,cAAc,sBAAsB,CAAA;AACpC,cAAc,yBAAyB,CAAA;AACvC,cAAc,0BAA0B,CAAA;AACxC,cAAc,oBAAoB,CAAA;AAClC,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA"}
@@ -1,12 +1,13 @@
1
1
  /** Auto-generated from build scripts */
2
- export * from './types/VariantStream';
3
- export * from './types/SessionData';
4
- export * from './types/MediaPlaylist';
5
- export * from './types/MainPlaylist';
6
- export * from './types/HlsSegment';
7
- export * from './types/EncryptionKey';
8
- export * from './types/AlternativeRendition';
9
- export * from './parse/parseUtil';
10
- export * from './parse/parseMediaPlaylist';
11
- export * from './parse/parseMainPlaylist';
12
2
  export * from './parse/parseAttributes';
3
+ export * from './parse/parseMainPlaylist';
4
+ export * from './parse/parseMediaPlaylist';
5
+ export * from './parse/parseUtil';
6
+ export * from './types/AlternativeRendition';
7
+ export * from './types/EncryptionKey';
8
+ export * from './types/HlsDateRange';
9
+ export * from './types/HlsMainPlaylist';
10
+ export * from './types/HlsMediaPlaylist';
11
+ export * from './types/HlsSegment';
12
+ export * from './types/SessionData';
13
+ export * from './types/VariantStream';
@@ -1,10 +1,15 @@
1
- import type { MainPlaylist } from '../types/MainPlaylist';
1
+ import type { HlsMainPlaylist } from '../types/HlsMainPlaylist';
2
2
  /**
3
3
  * Parses an HLS master playlist from M3U8 text.
4
4
  *
5
5
  * @param text The raw M3U8 manifest string.
6
- * @returns A readonly MainPlaylist structure.
6
+ * @param queryParams Optional query parameters from the playlist's own URL,
7
+ * used to resolve `#EXT-X-DEFINE:QUERYPARAM="name"` entries (RFC 8216
8
+ * §4.4.5.1). Resolved values are exposed via {@link HlsMainPlaylist.defines} so
9
+ * child media playlists can `IMPORT` them. Interstitial ad manifests (e.g.
10
+ * AWS MediaTailor) carry session tokens this way.
11
+ * @returns A readonly HlsMainPlaylist structure.
7
12
  * @throws StringParseError if the manifest is malformed.
8
13
  */
9
- export declare function parseMainPlaylist(text: string): MainPlaylist;
14
+ export declare function parseMainPlaylist(text: string, queryParams?: Readonly<Record<string, string>>): HlsMainPlaylist;
10
15
  //# sourceMappingURL=parseMainPlaylist.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parseMainPlaylist.d.ts","sourceRoot":"","sources":["../../../../src/parse/parseMainPlaylist.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAYxD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAkH5D"}
1
+ {"version":3,"file":"parseMainPlaylist.d.ts","sourceRoot":"","sources":["../../../../src/parse/parseMainPlaylist.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAa/D;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC7B,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC/C,eAAe,CAwIjB"}
@@ -2,8 +2,9 @@
2
2
  * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3
3
  * SPDX-License-Identifier: Apache-2.0
4
4
  */
5
- import { StringReader, ValidationError } from '@amazon/vinyl-util';
5
+ import { StringReader, substitute, ValidationError } from '@amazon/vinyl-util';
6
6
  import { renditionTypeValidator } from '../types/AlternativeRendition';
7
+ import { HLS_VARIABLE_PATTERN } from './parseMediaPlaylist';
7
8
  import { parseAttributes } from './parseAttributes';
8
9
  import { readLine, skipWhitespaceLine, addIfPresent } from './parseUtil';
9
10
  const EXTM3U = '#EXTM3U';
@@ -15,10 +16,15 @@ const EXT_X_DEFINE = '#EXT-X-DEFINE:';
15
16
  * Parses an HLS master playlist from M3U8 text.
16
17
  *
17
18
  * @param text The raw M3U8 manifest string.
18
- * @returns A readonly MainPlaylist structure.
19
+ * @param queryParams Optional query parameters from the playlist's own URL,
20
+ * used to resolve `#EXT-X-DEFINE:QUERYPARAM="name"` entries (RFC 8216
21
+ * §4.4.5.1). Resolved values are exposed via {@link HlsMainPlaylist.defines} so
22
+ * child media playlists can `IMPORT` them. Interstitial ad manifests (e.g.
23
+ * AWS MediaTailor) carry session tokens this way.
24
+ * @returns A readonly HlsMainPlaylist structure.
19
25
  * @throws StringParseError if the manifest is malformed.
20
26
  */
21
- export function parseMainPlaylist(text) {
27
+ export function parseMainPlaylist(text, queryParams) {
22
28
  const reader = new StringReader(text);
23
29
  reader.white();
24
30
  if (!reader.stringIf(EXTM3U)) {
@@ -51,7 +57,7 @@ export function parseMainPlaylist(text) {
51
57
  }
52
58
  variants.push(addIfPresent({
53
59
  bandwidth: Number(attrs['BANDWIDTH']),
54
- uri,
60
+ uri: substituteHlsVariables(uri, defines),
55
61
  ...(attrs['FRAME-RATE'] && {
56
62
  frameRate: Number(attrs['FRAME-RATE']),
57
63
  }),
@@ -71,6 +77,9 @@ export function parseMainPlaylist(text) {
71
77
  const attrs = parseAttributes(attrReader);
72
78
  const type = attrs['TYPE'];
73
79
  renditionTypeValidator.assert(type);
80
+ if (attrs['URI']) {
81
+ attrs['URI'] = substituteHlsVariables(attrs['URI'], defines);
82
+ }
74
83
  alternativeRenditions.push(addIfPresent({
75
84
  type,
76
85
  groupId: attrs['GROUP-ID'],
@@ -81,6 +90,15 @@ export function parseMainPlaylist(text) {
81
90
  ...(attrs['AUTOSELECT'] && {
82
91
  autoSelect: attrs['AUTOSELECT'] === 'YES',
83
92
  }),
93
+ ...(attrs['FORCED'] && {
94
+ forced: attrs['FORCED'] === 'YES',
95
+ }),
96
+ ...(attrs['CHARACTERISTICS'] && {
97
+ characteristics: attrs['CHARACTERISTICS']
98
+ .split(',')
99
+ .map((c) => c.trim())
100
+ .filter((c) => c.length > 0),
101
+ }),
84
102
  }, attrs, {
85
103
  URI: 'uri',
86
104
  LANGUAGE: 'language',
@@ -102,7 +120,17 @@ export function parseMainPlaylist(text) {
102
120
  const attrStr = trimmed.substring(EXT_X_DEFINE.length);
103
121
  const attrReader = new StringReader(attrStr);
104
122
  const attrs = parseAttributes(attrReader);
105
- if (attrs['NAME'] && attrs['VALUE']) {
123
+ const queryParamName = attrs['QUERYPARAM'];
124
+ if (queryParamName) {
125
+ // Per RFC 8216 §4.4.5.1: QUERYPARAM copies the value from the
126
+ // named query parameter of the playlist's own URL. Ignore
127
+ // silently if the parameter is absent.
128
+ const queryValue = queryParams?.[queryParamName];
129
+ if (queryValue !== undefined) {
130
+ defines[queryParamName] = queryValue;
131
+ }
132
+ }
133
+ else if (attrs['NAME'] && attrs['VALUE']) {
106
134
  defines[attrs['NAME']] = attrs['VALUE'];
107
135
  }
108
136
  }
@@ -110,6 +138,16 @@ export function parseMainPlaylist(text) {
110
138
  }
111
139
  return { variants, alternativeRenditions, sessionData, defines };
112
140
  }
141
+ /**
142
+ * Applies EXT-X-DEFINE variable substitution (per RFC 8216 §4.2) to a manifest
143
+ * value, replacing `{$NAME}` tokens with the values declared earlier in the
144
+ * playlist. Returns the input unchanged when no variables have been declared.
145
+ */
146
+ function substituteHlsVariables(value, variables) {
147
+ if (!value || Object.keys(variables).length === 0)
148
+ return value;
149
+ return substitute(value, variables, HLS_VARIABLE_PATTERN);
150
+ }
113
151
  /**
114
152
  * Parses a resolution string (e.g., "1920x1080") into width and height numbers.
115
153
  * Throws ValidationError for invalid formats per HLS spec.
@@ -1,12 +1,19 @@
1
- import type { MediaPlaylist } from '../types/MediaPlaylist';
1
+ import type { HlsMediaPlaylist } from '../types/HlsMediaPlaylist';
2
2
  export declare const HLS_VARIABLE_PATTERN: RegExp;
3
3
  /**
4
4
  * Parses an HLS media playlist from M3U8 text.
5
5
  *
6
6
  * @param text The raw M3U8 manifest string.
7
- * @param variables Optional variable definitions from the master playlist's #EXT-X-DEFINE tags.
8
- * @returns A readonly MediaPlaylist structure.
7
+ * @param variables Optional variable definitions from the parent multivariant
8
+ * playlist's #EXT-X-DEFINE tags. Media-playlist `#EXT-X-DEFINE:IMPORT="name"`
9
+ * entries resolve against this map. Local `#EXT-X-DEFINE:NAME=,VALUE=`
10
+ * entries are also supported and take precedence.
11
+ * @param queryParams Optional query parameters from the playlist's own URL,
12
+ * used to resolve `#EXT-X-DEFINE:QUERYPARAM="name"` entries (RFC 8216
13
+ * §4.4.5.1). Interstitial ad manifests (e.g. AWS MediaTailor) commonly carry
14
+ * session tokens this way and reference them as `{$name}` in child URIs.
15
+ * @returns A readonly HlsMediaPlaylist structure.
9
16
  * @throws StringParseError if the manifest is malformed.
10
17
  */
11
- export declare function parseMediaPlaylist(text: string, variables?: Readonly<Record<string, string>>): MediaPlaylist;
18
+ export declare function parseMediaPlaylist(text: string, variables?: Readonly<Record<string, string>>, queryParams?: Readonly<Record<string, string>>): HlsMediaPlaylist;
12
19
  //# sourceMappingURL=parseMediaPlaylist.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"parseMediaPlaylist.d.ts","sourceRoot":"","sources":["../../../../src/parse/parseMediaPlaylist.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAI1D,eAAO,MAAM,oBAAoB,QAAkB,CAAA;AAenD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC7C,aAAa,CA+Jf"}
1
+ {"version":3,"file":"parseMediaPlaylist.d.ts","sourceRoot":"","sources":["../../../../src/parse/parseMediaPlaylist.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA;AAQjE,eAAO,MAAM,oBAAoB,QAAmB,CAAA;AAiBpD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,EAC5C,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC/C,gBAAgB,CAiMlB"}
@@ -6,7 +6,11 @@ import { StringReader, substitute } from '@amazon/vinyl-util';
6
6
  import { encryptionMethodValidator } from '../types/EncryptionKey';
7
7
  import { parseAttributes } from './parseAttributes';
8
8
  import { readLine, skipWhitespaceLine, addIfPresent } from './parseUtil';
9
- export const HLS_VARIABLE_PATTERN = /\{\$([^}]+)}/g;
9
+ // The variable-name class excludes `{` (and `}`) so a match attempt can't
10
+ // scan across token boundaries. With `[^}]` a crafted value such as
11
+ // `{$|{$|{$|…` makes replace() do polynomial work (ReDoS); `[^{}]` keeps it
12
+ // linear, and HLS variable names never contain `{`/`}`.
13
+ export const HLS_VARIABLE_PATTERN = /\{\$([^{}]+)}/g;
10
14
  const EXTM3U = '#EXTM3U';
11
15
  const EXTINF = '#EXTINF:';
12
16
  const EXT_X_TARGETDURATION = '#EXT-X-TARGETDURATION:';
@@ -19,15 +23,28 @@ const EXT_X_BYTERANGE = '#EXT-X-BYTERANGE:';
19
23
  const EXT_X_DISCONTINUITY = '#EXT-X-DISCONTINUITY';
20
24
  const EXT_X_PROGRAM_DATE_TIME = '#EXT-X-PROGRAM-DATE-TIME:';
21
25
  const EXT_X_MAP = '#EXT-X-MAP:';
26
+ const EXT_X_DEFINE = '#EXT-X-DEFINE:';
27
+ const EXT_X_DATERANGE = '#EXT-X-DATERANGE:';
22
28
  /**
23
29
  * Parses an HLS media playlist from M3U8 text.
24
30
  *
25
31
  * @param text The raw M3U8 manifest string.
26
- * @param variables Optional variable definitions from the master playlist's #EXT-X-DEFINE tags.
27
- * @returns A readonly MediaPlaylist structure.
32
+ * @param variables Optional variable definitions from the parent multivariant
33
+ * playlist's #EXT-X-DEFINE tags. Media-playlist `#EXT-X-DEFINE:IMPORT="name"`
34
+ * entries resolve against this map. Local `#EXT-X-DEFINE:NAME=,VALUE=`
35
+ * entries are also supported and take precedence.
36
+ * @param queryParams Optional query parameters from the playlist's own URL,
37
+ * used to resolve `#EXT-X-DEFINE:QUERYPARAM="name"` entries (RFC 8216
38
+ * §4.4.5.1). Interstitial ad manifests (e.g. AWS MediaTailor) commonly carry
39
+ * session tokens this way and reference them as `{$name}` in child URIs.
40
+ * @returns A readonly HlsMediaPlaylist structure.
28
41
  * @throws StringParseError if the manifest is malformed.
29
42
  */
30
- export function parseMediaPlaylist(text, variables) {
43
+ export function parseMediaPlaylist(text, variables, queryParams) {
44
+ const defines = { ...(variables ?? {}) };
45
+ const substituteVars = (v) => Object.keys(defines).length === 0
46
+ ? v
47
+ : substitute(v, defines, HLS_VARIABLE_PATTERN);
31
48
  const reader = new StringReader(text);
32
49
  reader.white();
33
50
  if (!reader.stringIf(EXTM3U)) {
@@ -45,6 +62,7 @@ export function parseMediaPlaylist(text, variables) {
45
62
  let pendingDiscontinuity = false;
46
63
  let pendingDateTime;
47
64
  const segments = [];
65
+ const dateRanges = [];
48
66
  let sequenceCounter = 0;
49
67
  while (reader.hasNext()) {
50
68
  if (skipWhitespaceLine(reader))
@@ -63,9 +81,7 @@ export function parseMediaPlaylist(text, variables) {
63
81
  continue;
64
82
  const nextLine = readLine(reader).trim();
65
83
  if (!nextLine.startsWith('#')) {
66
- uri = variables
67
- ? substitute(nextLine, variables, HLS_VARIABLE_PATTERN)
68
- : nextLine;
84
+ uri = substituteVars(nextLine);
69
85
  break;
70
86
  }
71
87
  // Process tags that come between EXTINF and URI
@@ -168,6 +184,37 @@ export function parseMediaPlaylist(text, variables) {
168
184
  else if (trimmed.startsWith(EXT_X_PROGRAM_DATE_TIME)) {
169
185
  pendingDateTime = trimmed.substring(EXT_X_PROGRAM_DATE_TIME.length);
170
186
  }
187
+ else if (trimmed.startsWith(EXT_X_DEFINE)) {
188
+ const attrStr = trimmed.substring(EXT_X_DEFINE.length);
189
+ const attrReader = new StringReader(attrStr);
190
+ const attrs = parseAttributes(attrReader);
191
+ const importName = attrs['IMPORT'];
192
+ const queryParamName = attrs['QUERYPARAM'];
193
+ if (importName) {
194
+ // Per RFC 8216 §4.4.2.3: IMPORT copies the value from the
195
+ // parent multivariant's DEFINE map. Ignore silently if unknown.
196
+ const imported = variables?.[importName];
197
+ if (imported !== undefined)
198
+ defines[importName] = imported;
199
+ }
200
+ else if (queryParamName) {
201
+ // Per RFC 8216 §4.4.5.1: QUERYPARAM copies the value from the
202
+ // named query parameter of the playlist's own URL. Ignore
203
+ // silently if the parameter is absent.
204
+ const queryValue = queryParams?.[queryParamName];
205
+ if (queryValue !== undefined) {
206
+ defines[queryParamName] = queryValue;
207
+ }
208
+ }
209
+ else if (attrs['NAME'] && attrs['VALUE']) {
210
+ defines[attrs['NAME']] = attrs['VALUE'];
211
+ }
212
+ }
213
+ else if (trimmed.startsWith(EXT_X_DATERANGE)) {
214
+ const dateRange = parseHlsDateRange(trimmed.substring(EXT_X_DATERANGE.length), substituteVars);
215
+ if (dateRange)
216
+ dateRanges.push(dateRange);
217
+ }
171
218
  // Unrecognized tags and comments are silently skipped
172
219
  }
173
220
  return {
@@ -177,6 +224,63 @@ export function parseMediaPlaylist(text, variables) {
177
224
  playlistType,
178
225
  ended,
179
226
  segments,
227
+ dateRanges,
228
+ };
229
+ }
230
+ /**
231
+ * Attribute names that map to typed {@link HlsDateRange} fields and are therefore
232
+ * excluded from the free-form {@link HlsDateRange.clientAttributes} bag.
233
+ */
234
+ const DATERANGE_RESERVED_ATTRS = new Set([
235
+ 'ID',
236
+ 'CLASS',
237
+ 'START-DATE',
238
+ 'END-DATE',
239
+ 'DURATION',
240
+ 'PLANNED-DURATION',
241
+ 'END-ON-NEXT',
242
+ ]);
243
+ /**
244
+ * Parses the attribute list of an EXT-X-DATERANGE tag. Returns undefined when
245
+ * the tag is missing the required ID or START-DATE attributes (per RFC 8216
246
+ * §4.3.2.7 a Date Range without both is not usable), so malformed tags are
247
+ * skipped rather than throwing.
248
+ *
249
+ * The `X-` client-defined attributes are collected verbatim into
250
+ * {@link HlsDateRange.clientAttributes}. Interstitial attributes such as
251
+ * `X-ASSET-URI` frequently reference EXT-X-DEFINE variables, so string values
252
+ * are passed through the same substitution applied to segment URIs.
253
+ */
254
+ function parseHlsDateRange(attrStr, substituteVars) {
255
+ const attrs = parseAttributes(new StringReader(attrStr));
256
+ const id = attrs['ID'];
257
+ // START-DATE may be absent (the Record index type hides this), so gate on
258
+ // key presence rather than a nullish fallback.
259
+ const startDate = 'START-DATE' in attrs ? attrs['START-DATE'] : '';
260
+ // END-ON-NEXT ranges anchor to the following range and may legitimately
261
+ // omit START-DATE; RFC requires ID and CLASS in that case.
262
+ const endOnNext = attrs['END-ON-NEXT'] === 'YES';
263
+ if (!id)
264
+ return undefined;
265
+ if (!startDate && !endOnNext)
266
+ return undefined;
267
+ const clientAttributes = {};
268
+ for (const [key, value] of Object.entries(attrs)) {
269
+ if (DATERANGE_RESERVED_ATTRS.has(key))
270
+ continue;
271
+ clientAttributes[key] = substituteVars(value);
272
+ }
273
+ return {
274
+ id,
275
+ startDate,
276
+ ...(attrs['CLASS'] && { classId: attrs['CLASS'] }),
277
+ ...(attrs['END-DATE'] && { endDate: attrs['END-DATE'] }),
278
+ ...(attrs['DURATION'] && { duration: Number(attrs['DURATION']) }),
279
+ ...(attrs['PLANNED-DURATION'] && {
280
+ plannedDuration: Number(attrs['PLANNED-DURATION']),
281
+ }),
282
+ ...(endOnNext && { endOnNext: true }),
283
+ clientAttributes,
180
284
  };
181
285
  }
182
286
  /**