@transmitlive/m3u8-parser 4.7.2-beta.6 → 7.1.0-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +7 -2
  2. package/dist/m3u8-parser.cjs.js +446 -302
  3. package/dist/m3u8-parser.es.js +443 -297
  4. package/dist/m3u8-parser.js +462 -351
  5. package/dist/m3u8-parser.min.js +2 -2
  6. package/package.json +6 -11
  7. package/src/parse-stream.js +93 -18
  8. package/src/parser.js +120 -9
  9. package/test/fixtures/integration/absoluteUris.js +1 -0
  10. package/test/fixtures/integration/allowCache.js +1 -0
  11. package/test/fixtures/integration/allowCacheInvalid.js +1 -0
  12. package/test/fixtures/integration/alternateAudio.js +1 -0
  13. package/test/fixtures/integration/alternateVideo.js +1 -0
  14. package/test/fixtures/integration/brightcove.js +1 -0
  15. package/test/fixtures/integration/byteRange.js +1 -0
  16. package/test/fixtures/integration/dateTime.js +3 -0
  17. package/test/fixtures/integration/diff-init-key.js +1 -0
  18. package/test/fixtures/integration/disallowCache.js +1 -0
  19. package/test/fixtures/integration/disc-sequence.js +9 -4
  20. package/test/fixtures/integration/discontinuity.js +19 -9
  21. package/test/fixtures/integration/domainUris.js +1 -0
  22. package/test/fixtures/integration/empty.js +1 -0
  23. package/test/fixtures/integration/emptyAllowCache.js +1 -0
  24. package/test/fixtures/integration/emptyMediaSequence.js +9 -4
  25. package/test/fixtures/integration/emptyPlaylistType.js +1 -0
  26. package/test/fixtures/integration/emptyTargetDuration.js +1 -0
  27. package/test/fixtures/integration/encrypted.js +1 -0
  28. package/test/fixtures/integration/event.js +1 -0
  29. package/test/fixtures/integration/extXPlaylistTypeInvalidPlaylist.js +3 -1
  30. package/test/fixtures/integration/extinf.js +3 -1
  31. package/test/fixtures/integration/fmp4.js +3 -1
  32. package/test/fixtures/integration/headerOnly.js +1 -0
  33. package/test/fixtures/integration/invalidAllowCache.js +1 -0
  34. package/test/fixtures/integration/invalidMediaSequence.js +9 -4
  35. package/test/fixtures/integration/invalidPlaylistType.js +1 -0
  36. package/test/fixtures/integration/invalidTargetDuration.js +1 -0
  37. package/test/fixtures/integration/liveMissingSegmentDuration.js +3 -1
  38. package/test/fixtures/integration/liveStart30sBefore.js +19 -9
  39. package/test/fixtures/integration/llhls-byte-range.js +1 -0
  40. package/test/fixtures/integration/llhls-delta-byte-range.js +1 -0
  41. package/test/fixtures/integration/llhls.js +8 -0
  42. package/test/fixtures/integration/llhlsDelta.js +5 -0
  43. package/test/fixtures/integration/manifestExtTTargetdurationNegative.js +1 -0
  44. package/test/fixtures/integration/manifestExtXEndlistEarly.js +1 -0
  45. package/test/fixtures/integration/manifestNoExtM3u.js +1 -0
  46. package/test/fixtures/integration/master-fmp4.js +27 -25
  47. package/test/fixtures/integration/master.js +1 -0
  48. package/test/fixtures/integration/media.js +1 -0
  49. package/test/fixtures/integration/mediaSequence.js +9 -4
  50. package/test/fixtures/integration/missingEndlist.js +1 -0
  51. package/test/fixtures/integration/missingExtinf.js +1 -0
  52. package/test/fixtures/integration/missingMediaSequence.js +9 -4
  53. package/test/fixtures/integration/missingSegmentDuration.js +3 -1
  54. package/test/fixtures/integration/multipleAudioGroups.js +1 -0
  55. package/test/fixtures/integration/multipleAudioGroupsCombinedMain.js +1 -0
  56. package/test/fixtures/integration/multipleTargetDurations.js +1 -0
  57. package/test/fixtures/integration/multipleVideo.js +1 -0
  58. package/test/fixtures/integration/negativeMediaSequence.js +9 -4
  59. package/test/fixtures/integration/playlist.js +1 -0
  60. package/test/fixtures/integration/playlistMediaSequenceHigher.js +3 -1
  61. package/test/fixtures/integration/start.js +1 -0
  62. package/test/fixtures/integration/streamInfInvalid.js +1 -0
  63. package/test/fixtures/integration/twoMediaSequences.js +9 -4
  64. package/test/fixtures/integration/versionInvalid.js +1 -0
  65. package/test/fixtures/integration/whiteSpace.js +1 -0
  66. package/test/fixtures/integration/zeroDuration.js +1 -0
  67. package/test/parse-stream.test.js +112 -16
  68. package/test/parser.test.js +392 -17
@@ -242,7 +242,7 @@ QUnit.test('parses #EXTM3U tags', function(assert) {
242
242
 
243
243
  // #EXTINF
244
244
  QUnit.test('parses minimal #EXTINF tags', function(assert) {
245
- const manifest = '#EXTINF\n';
245
+ const manifest = '#EXTINF:\n';
246
246
  let element;
247
247
 
248
248
  this.parseStream.on('data', function(elem) {
@@ -319,7 +319,7 @@ QUnit.test('parses #EXTINF tags with carriage returns', function(assert) {
319
319
 
320
320
  // #EXT-X-TARGETDURATION
321
321
  QUnit.test('parses minimal #EXT-X-TARGETDURATION tags', function(assert) {
322
- const manifest = '#EXT-X-TARGETDURATION\n';
322
+ const manifest = '#EXT-X-TARGETDURATION:\n';
323
323
  let element;
324
324
 
325
325
  this.parseStream.on('data', function(elem) {
@@ -379,7 +379,7 @@ QUnit.test('parses #EXT-X-VERSION with a version', function(assert) {
379
379
 
380
380
  // #EXT-X-MEDIA-SEQUENCE
381
381
  QUnit.test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function(assert) {
382
- const manifest = '#EXT-X-MEDIA-SEQUENCE\n';
382
+ const manifest = '#EXT-X-MEDIA-SEQUENCE:\n';
383
383
  let element;
384
384
 
385
385
  this.parseStream.on('data', function(elem) {
@@ -453,7 +453,7 @@ QUnit.test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function(assert)
453
453
 
454
454
  // #EXT-X-BYTERANGE
455
455
  QUnit.test('parses minimal #EXT-X-BYTERANGE tags', function(assert) {
456
- const manifest = '#EXT-X-BYTERANGE\n';
456
+ const manifest = '#EXT-X-BYTERANGE:\n';
457
457
  let element;
458
458
 
459
459
  this.parseStream.on('data', function(elem) {
@@ -589,7 +589,7 @@ QUnit.test('parses #EXT-X-MAP tags with arbitrary attributes', function(assert)
589
589
  });
590
590
  // #EXT-X-STREAM-INF
591
591
  QUnit.test('parses minimal #EXT-X-STREAM-INF tags', function(assert) {
592
- const manifest = '#EXT-X-STREAM-INF\n';
592
+ const manifest = '#EXT-X-STREAM-INF:\n';
593
593
  let element;
594
594
 
595
595
  this.parseStream.on('data', function(elem) {
@@ -604,7 +604,7 @@ QUnit.test('parses minimal #EXT-X-STREAM-INF tags', function(assert) {
604
604
  });
605
605
  // #EXT-X-PROGRAM-DATE-TIME
606
606
  QUnit.test('parses minimal EXT-X-PROGRAM-DATE-TIME tags', function(assert) {
607
- const manifest = '#EXT-X-PROGRAM-DATE-TIME\n';
607
+ const manifest = '#EXT-X-PROGRAM-DATE-TIME:\n';
608
608
  let element;
609
609
 
610
610
  this.parseStream.on('data', function(elem) {
@@ -698,6 +698,14 @@ QUnit.test('parses #EXT-X-STREAM-INF with common attributes', function(assert) {
698
698
  'avc1.4d400d, mp4a.40.2',
699
699
  'codecs are parsed'
700
700
  );
701
+
702
+ manifest = '#EXT-X-STREAM-INF:PATHWAY-ID="CDN-A"\n';
703
+ this.lineStream.push(manifest);
704
+
705
+ assert.ok(element, 'an event was triggered');
706
+ assert.strictEqual(element.type, 'tag', 'the line type is tag');
707
+ assert.strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
708
+ assert.strictEqual(element.attributes['PATHWAY-ID'], 'CDN-A', 'pathway-id is parsed');
701
709
  });
702
710
  QUnit.test('parses #EXT-X-STREAM-INF with arbitrary attributes', function(assert) {
703
711
  const manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n';
@@ -809,16 +817,6 @@ QUnit.test('parses lightly-broken #EXT-X-KEY tags', function(assert) {
809
817
  'parsed a single-quoted uri'
810
818
  );
811
819
 
812
- element = null;
813
- manifest = '#EXT-X-KEYURI="https://example.com/key",METHOD=AES-128\n';
814
- this.lineStream.push(manifest);
815
- assert.strictEqual(element.tagType, 'key', 'parsed the tag type');
816
- assert.strictEqual(
817
- element.attributes.URI,
818
- 'https://example.com/key',
819
- 'inferred a colon after the tag type'
820
- );
821
-
822
820
  element = null;
823
821
  manifest = '#EXT-X-KEY: URI = "https://example.com/key",METHOD=AES-128\n';
824
822
  this.lineStream.push(manifest);
@@ -889,6 +887,104 @@ QUnit.test('parses EXT-X-START PRECISE attribute', function(assert) {
889
887
  assert.strictEqual(element.attributes['TIME-OFFSET'], 1.4, 'parses time offset');
890
888
  assert.strictEqual(element.attributes.PRECISE, true, 'parses precise attribute');
891
889
  });
890
+ // #EXT-X-DATERANGE:
891
+ QUnit.test('parses minimal EXT-X-DATERANGE tag', function(assert) {
892
+ const manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z"\n';
893
+ let element;
894
+
895
+ this.parseStream.on('data', function(elem) {
896
+ element = elem;
897
+ });
898
+ this.lineStream.push(manifest);
899
+
900
+ assert.ok(element, 'an event was triggered');
901
+ assert.strictEqual(element.type, 'tag', 'the line type is tag');
902
+ assert.strictEqual(element.tagType, 'daterange', 'the tag type is daterange');
903
+ assert.strictEqual(element.attributes.ID, '12345');
904
+ assert.deepEqual(element.attributes['START-DATE'], new Date('2023-04-13T15:15:15.840000Z'));
905
+ });
906
+
907
+ QUnit.test('parses DURATION and PLANNED-DURATION attributes in EXT-X-DATERANGE tag', function(assert) {
908
+ const manifest = '#EXT-X-DATERANGE:ID="54545",START-DATE="2023-04-23T18:17:16.54000Z",PLANNED-DURATION="38.4",DURATION="15.5"\n';
909
+ let element;
910
+
911
+ this.parseStream.on('data', function(elem) {
912
+ element = elem;
913
+ });
914
+ this.lineStream.push(manifest);
915
+
916
+ assert.ok(element, 'an event was triggered');
917
+ assert.strictEqual(element.type, 'tag', 'the line type is tag');
918
+ assert.strictEqual(element.tagType, 'daterange', 'the tag type is daterange');
919
+ assert.strictEqual(element.attributes.ID, '54545');
920
+ assert.deepEqual(element.attributes['START-DATE'], new Date('2023-04-23T18:17:16.540000Z'));
921
+ assert.strictEqual(element.attributes.DURATION, 15.5);
922
+ assert.strictEqual(element.attributes['PLANNED-DURATION'], 38.4);
923
+ });
924
+
925
+ QUnit.test('parses SCTE35-CMD, SCTE35-OUT, SCTE35-IN EXT-X-DATERANGE tags', function(assert) {
926
+ let manifest = '#EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2023-04-13T15:15:15.840000Z",SCTE35-OUT="0xFC002F0000000000FF000014056FFFFFFF0"\n';
927
+ let element;
928
+
929
+ this.parseStream.on('data', function(elem) {
930
+ element = elem;
931
+ });
932
+ this.lineStream.push(manifest);
933
+
934
+ assert.ok(element, 'an event was triggered');
935
+ assert.strictEqual(element.attributes['SCTE35-OUT'], '0xFC002F0000000000FF000014056FFFFFFF0');
936
+
937
+ manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",SCTE35-IN="0xFC0000425100FFF0140500000300000000E77FEFFE0011FB9EFE0029004D1932E0000100101002A22"\n';
938
+ this.lineStream.push(manifest);
939
+
940
+ assert.ok(element, 'an event was triggered');
941
+ assert.strictEqual(element.attributes['SCTE35-IN'], '0xFC0000425100FFF0140500000300000000E77FEFFE0011FB9EFE0029004D1932E0000100101002A22');
942
+
943
+ manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",SCTE35-CMD="0xFC0000425100FFF014"\n';
944
+ this.lineStream.push(manifest);
945
+
946
+ assert.ok(element, 'an event was triggered');
947
+ assert.strictEqual(element.attributes['SCTE35-CMD'], '0xFC0000425100FFF014');
948
+ });
949
+
950
+ QUnit.test('custom attributes in EXT-X-DATERANGE are parsed', function(assert) {
951
+ let manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",X-CUSTOM-KEY="value"\n';
952
+ let element;
953
+
954
+ this.parseStream.on('data', function(elem) {
955
+ element = elem;
956
+ });
957
+ this.lineStream.push(manifest);
958
+
959
+ assert.ok(element, 'an event was triggered');
960
+ assert.strictEqual(element.attributes['X-CUSTOM-KEY'], 'value');
961
+
962
+ manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",X-CUSTOM-KEY="17.8"\n';
963
+ this.lineStream.push(manifest);
964
+
965
+ assert.ok(element, 'an event was triggered');
966
+ assert.strictEqual(element.attributes['X-CUSTOM-KEY'], 17.8);
967
+
968
+ manifest = '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",X-CUSTOM-KEY="0X12345abcde"\n';
969
+ this.lineStream.push(manifest);
970
+
971
+ assert.ok(element, 'an event was triggered');
972
+ assert.strictEqual(element.attributes['X-CUSTOM-KEY'], '0X12345abcde');
973
+ });
974
+
975
+ QUnit.test('parses EXT-X-INDEPENDENT-SEGMENTS', function(assert) {
976
+ const manifest = '#EXT-X-INDEPENDENT-SEGMENTS\n';
977
+ let element;
978
+
979
+ this.parseStream.on('data', function(elem) {
980
+ element = elem;
981
+ });
982
+ this.lineStream.push(manifest);
983
+
984
+ assert.ok(element, 'an event was triggered');
985
+ assert.strictEqual(element.type, 'tag', 'the line type is tag');
986
+ assert.strictEqual(element.tagType, 'independent-segments', 'the tag type is independent-segments');
987
+ });
892
988
 
893
989
  QUnit.test('ignores empty lines', function(assert) {
894
990
  const manifest = '\n';
@@ -1,6 +1,6 @@
1
1
  import QUnit from 'qunit';
2
- import testDataExpected from 'data-files!expecteds';
3
- import testDataManifests from 'data-files!manifests';
2
+ // import testDataExpected from 'data-files!expecteds';
3
+ // import testDataManifests from 'data-files!manifests';
4
4
  import {Parser} from '../src';
5
5
 
6
6
  QUnit.module('m3u8s', function(hooks) {
@@ -108,11 +108,10 @@ QUnit.module('m3u8s', function(hooks) {
108
108
  '#EXT-X-CUE-OUT:10',
109
109
  '#EXTINF:5,',
110
110
  'ex2.ts',
111
- '#EXT-X-CUE-OUT15',
112
111
  '#EXT-UKNOWN-TAG',
113
112
  '#EXTINF:5,',
114
113
  'ex3.ts',
115
- '#EXT-X-CUE-OUT',
114
+ '#EXT-X-CUE-OUT:',
116
115
  '#EXTINF:5,',
117
116
  'ex3.ts',
118
117
  '#EXT-X-ENDLIST'
@@ -122,7 +121,6 @@ QUnit.module('m3u8s', function(hooks) {
122
121
  this.parser.end();
123
122
 
124
123
  assert.equal(this.parser.manifest.segments[1].cueOut, '10', 'parser attached cue out tag');
125
- assert.equal(this.parser.manifest.segments[2].cueOut, '15', 'cue out without : seperator');
126
124
  assert.equal(this.parser.manifest.segments[3].cueOut, '', 'cue out without data');
127
125
  });
128
126
 
@@ -135,11 +133,10 @@ QUnit.module('m3u8s', function(hooks) {
135
133
  '#EXT-X-CUE-OUT-CONT:10/60',
136
134
  '#EXTINF:5,',
137
135
  'ex2.ts',
138
- '#EXT-X-CUE-OUT-CONT15/30',
139
136
  '#EXT-UKNOWN-TAG',
140
137
  '#EXTINF:5,',
141
138
  'ex3.ts',
142
- '#EXT-X-CUE-OUT-CONT',
139
+ '#EXT-X-CUE-OUT-CONT:',
143
140
  '#EXTINF:5,',
144
141
  'ex3.ts',
145
142
  '#EXT-X-ENDLIST'
@@ -152,10 +149,6 @@ QUnit.module('m3u8s', function(hooks) {
152
149
  this.parser.manifest.segments[1].cueOutCont, '10/60',
153
150
  'parser attached cue out cont tag'
154
151
  );
155
- assert.equal(
156
- this.parser.manifest.segments[2].cueOutCont, '15/30',
157
- 'cue out cont without : seperator'
158
- );
159
152
  assert.equal(this.parser.manifest.segments[3].cueOutCont, '', 'cue out cont without data');
160
153
  });
161
154
 
@@ -165,14 +158,13 @@ QUnit.module('m3u8s', function(hooks) {
165
158
  '#EXTINF:5,',
166
159
  '#COMMENT',
167
160
  'ex1.ts',
168
- '#EXT-X-CUE-IN',
161
+ '#EXT-X-CUE-IN:',
169
162
  '#EXTINF:5,',
170
163
  'ex2.ts',
171
164
  '#EXT-X-CUE-IN:15',
172
165
  '#EXT-UKNOWN-TAG',
173
166
  '#EXTINF:5,',
174
167
  'ex3.ts',
175
- '#EXT-X-CUE-IN=abc',
176
168
  '#EXTINF:5,',
177
169
  'ex3.ts',
178
170
  '#EXT-X-ENDLIST'
@@ -183,10 +175,6 @@ QUnit.module('m3u8s', function(hooks) {
183
175
 
184
176
  assert.equal(this.parser.manifest.segments[1].cueIn, '', 'parser attached cue in tag');
185
177
  assert.equal(this.parser.manifest.segments[2].cueIn, '15', 'cue in with data');
186
- assert.equal(
187
- this.parser.manifest.segments[3].cueIn, '=abc',
188
- 'cue in without colon seperator'
189
- );
190
178
  });
191
179
 
192
180
  QUnit.test('parses characteristics attribute', function(assert) {
@@ -857,8 +845,394 @@ QUnit.module('m3u8s', function(hooks) {
857
845
  );
858
846
  });
859
847
 
848
+ QUnit.test('PDT value is assigned to segments with explicit #EXT-X-PROGRAM-DATE-TIME tags', function(assert) {
849
+ this.parser.push([
850
+ '#EXTM3U',
851
+ '#EXT-X-VERSION:6',
852
+ '#EXT-X-TARGETDURATION:8',
853
+ '#EXT-X-MEDIA-SEQUENCE:0',
854
+ '#EXTINF:8.0',
855
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
856
+ 'https://example.com/playlist1.m3u8',
857
+ '#EXTINF:8.0,',
858
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T22:14:10.053+00:00',
859
+ 'https://example.com/playlist2.m3u8',
860
+ '#EXT-X-ENDLIST'
861
+ ].join('\n'));
862
+ this.parser.end();
863
+ assert.equal(this.parser.manifest.segments[0].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
864
+ assert.equal(this.parser.manifest.segments[1].programDateTime, new Date('2017-07-31T22:14:10.053+00:00').getTime());
865
+ });
866
+
867
+ QUnit.test('backfill PDT values when the first EXT-X-PROGRAM-DATE-TIME tag appears after one or more Media Segment URIs', function(assert) {
868
+ this.parser.push([
869
+ '#EXTM3U',
870
+ '#EXT-X-VERSION:6',
871
+ '#EXT-X-TARGETDURATION:8',
872
+ '#EXT-X-MEDIA-SEQUENCE:0',
873
+ '#EXTINF:8.0',
874
+ 'https://example.com/playlist1.m3u8',
875
+ '#EXTINF:8.0,',
876
+ 'https://example.com/playlist2.m3u8',
877
+ '#EXTINF:8.0',
878
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
879
+ 'https://example.com/playlist3.m3u8',
880
+ '#EXT-X-ENDLIST'
881
+ ].join('\n'));
882
+ this.parser.end();
883
+ const segments = this.parser.manifest.segments;
884
+
885
+ assert.equal(segments[2].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
886
+ assert.equal(segments[1].programDateTime, segments[2].programDateTime - (segments[1].duration * 1000));
887
+ assert.equal(segments[0].programDateTime, segments[1].programDateTime - (segments[0].duration * 1000));
888
+ });
889
+
890
+ QUnit.test('extrapolates forward when subsequent fragments do not have explicit PDT tags', function(assert) {
891
+ this.parser.push([
892
+ '#EXTM3U',
893
+ '#EXT-X-VERSION:6',
894
+ '#EXT-X-TARGETDURATION:8',
895
+ '#EXT-X-MEDIA-SEQUENCE:0',
896
+ '#EXTINF:8.0',
897
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
898
+ 'https://example.com/playlist1.m3u8',
899
+ '#EXTINF:8.0,',
900
+ 'https://example.com/playlist2.m3u8',
901
+ '#EXTINF:8.0',
902
+ 'https://example.com/playlist3.m3u8',
903
+ '#EXT-X-ENDLIST'
904
+ ].join('\n'));
905
+ this.parser.end();
906
+ const segments = this.parser.manifest.segments;
907
+
908
+ assert.equal(segments[0].programDateTime, new Date('2017-07-31T20:35:35.053+00:00').getTime());
909
+ assert.equal(segments[1].programDateTime, segments[0].programDateTime + segments[1].duration * 1000);
910
+ assert.equal(segments[2].programDateTime, segments[1].programDateTime + segments[2].duration * 1000);
911
+ });
912
+
913
+ QUnit.test('warns when #EXT-X-DATERANGE missing attribute', function(assert) {
914
+ this.parser.push([
915
+ '#EXT-X-VERSION:3',
916
+ '#EXT-X-MEDIA-SEQUENCE:0',
917
+ '#EXT-X-DISCONTINUITY-SEQUENCE:0',
918
+ '#EXTINF:10,',
919
+ 'media-00001.ts',
920
+ '#EXT-X-ENDLIST',
921
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
922
+ '#EXT-X-DATERANGE:ID="12345"'
923
+ ].join('\n'));
924
+ this.parser.end();
925
+
926
+ const warnings = [
927
+ '#EXT-X-DATERANGE #0 lacks required attribute(s): START-DATE'
928
+ ];
929
+
930
+ assert.deepEqual(
931
+ this.warnings,
932
+ warnings,
933
+ 'warnings as expected'
934
+ );
935
+ });
936
+
937
+ QUnit.test('warns when #EXT-X-DATERANGE end date attribute is less than start date', function(assert) {
938
+ this.parser.push([
939
+ '#EXT-X-VERSION:3',
940
+ '#EXT-X-MEDIA-SEQUENCE:0',
941
+ '#EXT-X-DISCONTINUITY-SEQUENCE:0',
942
+ '#EXTINF:10,',
943
+ 'media-00001.ts',
944
+ '#EXT-X-ENDLIST',
945
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
946
+ '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-DATE="2023-04-13T15:15:15.840000Z"'
947
+ ].join('\n'));
948
+ this.parser.end();
949
+
950
+ const warnings = [
951
+ 'EXT-X-DATERANGE END-DATE must be equal to or later than the value of the START-DATE'
952
+ ];
953
+
954
+ assert.deepEqual(
955
+ this.warnings,
956
+ warnings,
957
+ 'warnings as expected'
958
+ );
959
+ });
960
+
961
+ QUnit.test('warns when #EXT-X-DATERANGE duration or planned duration attribute is negative', function(assert) {
962
+ this.parser.push([
963
+ '#EXT-X-VERSION:3',
964
+ '#EXT-X-MEDIA-SEQUENCE:0',
965
+ '#EXT-X-DISCONTINUITY-SEQUENCE:0',
966
+ '#EXTINF:10,',
967
+ 'media-00001.ts',
968
+ '#EXT-X-ENDLIST',
969
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
970
+ '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",PLANNED-DURATION=-38.4,DURATION=-15.5'
971
+ ].join('\n'));
972
+ this.parser.end();
973
+
974
+ const warnings = [
975
+ 'EXT-X-DATERANGE DURATION must not be negative',
976
+ 'EXT-X-DATERANGE PLANNED-DURATION must not be negative'
977
+ ];
978
+
979
+ assert.deepEqual(
980
+ this.warnings,
981
+ warnings,
982
+ 'warnings as expected'
983
+ );
984
+ });
985
+
986
+ QUnit.test('warns when #EXT-X-DATERANGE has a END-ON-NEXT=YES attribute and a DURATION or END-DATE attribute', function(assert) {
987
+ this.parser.push([
988
+ '#EXT-X-VERSION:3',
989
+ '#EXT-X-MEDIA-SEQUENCE:0',
990
+ '#EXT-X-DISCONTINUITY-SEQUENCE:0',
991
+ '#EXTINF:10,',
992
+ 'media-00001.ts',
993
+ '#EXT-X-ENDLIST',
994
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
995
+ '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:15:15.840000Z",END-ON-NEXT=YES, END-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE"'
996
+ ].join('\n'));
997
+ this.parser.end();
998
+
999
+ const warnings = [
1000
+ 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must not contain DURATION or END-DATE attributes'
1001
+ ];
1002
+
1003
+ assert.deepEqual(
1004
+ this.warnings,
1005
+ warnings,
1006
+ 'warnings as expected'
1007
+ );
1008
+ });
1009
+
1010
+ QUnit.test('warns when #EXT-X-DATERANGE has a END-ON-NEXT=YES attribute but not a CLASS attribute', function(assert) {
1011
+ this.parser.push([
1012
+ '#EXT-X-VERSION:3',
1013
+ '#EXT-X-MEDIA-SEQUENCE:0',
1014
+ '#EXT-X-DISCONTINUITY-SEQUENCE:0',
1015
+ '#EXTINF:10,',
1016
+ 'media-00001.ts',
1017
+ '#EXT-X-ENDLIST',
1018
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
1019
+ '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES'
1020
+ ].join('\n'));
1021
+ this.parser.end();
1022
+
1023
+ const warnings = [
1024
+ 'EXT-X-DATERANGE with an END-ON-NEXT=YES attribute must have a CLASS attribute'
1025
+ ];
1026
+
1027
+ assert.deepEqual(
1028
+ this.warnings,
1029
+ warnings,
1030
+ 'warnings as expected'
1031
+ );
1032
+ });
1033
+
1034
+ QUnit.test('warns when playlist has multiple #EXT-X-DATERANGE tag same ID but different attribute values', function(assert) {
1035
+ this.parser.push([
1036
+ '#EXT-X-VERSION:3',
1037
+ '#EXT-X-MEDIA-SEQUENCE:0',
1038
+ '#EXT-X-DISCONTINUITY-SEQUENCE:0',
1039
+ '#EXTINF:10,',
1040
+ 'media-00001.ts',
1041
+ '#EXT-X-ENDLIST',
1042
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
1043
+ '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES,CLASS="CLASSATTRIBUTE"',
1044
+ '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE1"'
1045
+ ].join('\n'));
1046
+ this.parser.end();
1047
+
1048
+ const warnings = [
1049
+ 'EXT-X-DATERANGE tags with the same ID in a playlist must have the same attributes values'
1050
+ ];
1051
+
1052
+ assert.deepEqual(
1053
+ this.warnings,
1054
+ warnings,
1055
+ 'warnings as expected'
1056
+ );
1057
+ });
1058
+
1059
+ QUnit.test('when #EXT-X-DATERANGE has both DURATION and END-DATE attributes, value of the END-DATE attribute must be START-DATE + DURATION', function(assert) {
1060
+ this.parser.push([
1061
+ '#EXT-X-VERSION:3',
1062
+ '#EXT-X-MEDIA-SEQUENCE:0',
1063
+ '#EXT-X-DISCONTINUITY-SEQUENCE:0',
1064
+ '#EXTINF:10,',
1065
+ 'media-00001.ts',
1066
+ '#EXT-X-ENDLIST',
1067
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
1068
+ '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T15:16:15.840000Z",DURATION=14.0,END-DATE="2023-04-13T18:15:15.840000Z"'
1069
+ ].join('\n'));
1070
+ this.parser.end();
1071
+
1072
+ assert.deepEqual(this.parser.manifest.dateRanges[0].endDate, new Date('2023-04-13T15:16:29.840000Z'));
1073
+ });
1074
+
1075
+ QUnit.test('warns when playlist contains #EXT-X-DATERANGE tag but no #EXT-X-PROGRAM-DATE-TIME', function(assert) {
1076
+ this.parser.push([
1077
+ '#EXT-X-VERSION:3',
1078
+ '#EXT-X-MEDIA-SEQUENCE:0',
1079
+ '#EXT-X-DISCONTINUITY-SEQUENCE:0',
1080
+ '#EXTINF:10,',
1081
+ 'media-00001.ts',
1082
+ '#EXT-X-ENDLIST',
1083
+ '#EXT-X-DATERANGE:ID="12345",START-DATE="2023-04-13T18:16:15.840000Z",END-ON-NEXT=YES,CLASS="sampleClassAttrib"'
1084
+ ].join('\n'));
1085
+ this.parser.end();
1086
+
1087
+ const warnings = [
1088
+ 'A playlist with EXT-X-DATERANGE tag must contain atleast one EXT-X-PROGRAM-DATE-TIME tag'
1089
+ ];
1090
+
1091
+ assert.deepEqual(
1092
+ this.warnings,
1093
+ warnings,
1094
+ 'warnings as expected'
1095
+ );
1096
+ });
1097
+
1098
+ QUnit.test('playlist with multiple ext-x-daterange with same ID but no conflicting attributes', function(assert) {
1099
+ const expectedDateRange = {
1100
+ id: '12345',
1101
+ scte35In: '0xFC30200FFF2',
1102
+ scte35Out: '0xFC30200FFF2',
1103
+ startDate: new Date('2023-04-13T18:16:15.840000Z'),
1104
+ class: 'CLASSATTRIBUTE'
1105
+ };
1106
+
1107
+ this.parser.push([
1108
+ '#EXT-X-VERSION:3',
1109
+ '#EXT-X-MEDIA-SEQUENCE:0',
1110
+ '#EXT-X-DISCONTINUITY-SEQUENCE:0',
1111
+ '#EXTINF:10,',
1112
+ 'media-00001.ts',
1113
+ '#EXT-X-ENDLIST',
1114
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
1115
+ '#EXT-X-DATERANGE:ID="12345",SCTE35-IN=0xFC30200FFF2,START-DATE="2023-04-13T18:16:15.840000Z",CLASS="CLASSATTRIBUTE"',
1116
+ '#EXT-X-DATERANGE:ID="12345",SCTE35-OUT=0xFC30200FFF2,START-DATE="2023-04-13T18:16:15.840000Z"'
1117
+ ].join('\n'));
1118
+ this.parser.end();
1119
+ assert.equal(this.parser.manifest.dateRanges.length, 1, 'two dateranges with same ID are merged');
1120
+ assert.deepEqual(this.parser.manifest.dateRanges[0], expectedDateRange);
1121
+
1122
+ });
1123
+
1124
+ QUnit.test('playlist with multiple ext-x-daterange ', function(assert) {
1125
+ this.parser.push([
1126
+ ' #EXTM3U',
1127
+ '#EXT-X-VERSION:6',
1128
+ '#EXT-X-TARGETDURATION:8',
1129
+ '#EXT-X-MEDIA-SEQUENCE:0',
1130
+ '#EXT-X-PROGRAM-DATE-TIME:2017-07-31T20:35:35.053+00:00',
1131
+ '#EXT-X-DATERANGE:ID="event1",START-DATE="2023-04-20T10:00:00Z",DURATION=30.0,END-DATE="2023-04-20T10:00:30Z",X-CUSTOM-KEY="value"',
1132
+ '#EXTINF:8.0',
1133
+ 'https://example.com/playlist1.m3u8',
1134
+ '#EXT-SCTE35-IN:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
1135
+ '#EXT-X-DATERANGE:ID="event2",START-DATE="2023-04-20T11:00:00Z",DURATION=60.0,END-DATE="2023-04-20T11:01:00Z",X-CUSTOM-KEY="value"',
1136
+ '#EXTINF:8.0,',
1137
+ 'https://example.com/playlist2.m3u8',
1138
+ '#EXT-SCTE35-OUT:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
1139
+ '#EXT-X-DATERANGE:ID="event3",START-DATE="2023-04-20T12:00:00Z",DURATION=120.0,END-DATE="2023-04-20T12:02:00Z",X-CUSTOM-KEY="value"',
1140
+ '#EXTINF:8.0',
1141
+ 'https://example.com/playlist3.m3u8',
1142
+ '#EXT-SCTE35-IN:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
1143
+ '#EXT-SCTE35-OUT:0xFC002F0000000000FF000014056FFFFFFF065870697070657220506F6F7200',
1144
+ '#EXT-X-ENDLIST'
1145
+ ].join('\n'));
1146
+ this.parser.end();
1147
+ assert.equal(this.parser.manifest.dateRanges.length, 3);
1148
+ });
1149
+
1150
+ QUnit.test('parses #EXT-X-INDEPENDENT-SEGMENTS', function(assert) {
1151
+ this.parser.push([
1152
+ '#EXTM3U',
1153
+ '#EXT-X-VERSION:6',
1154
+ '#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=3.252,CAN-SKIP-UNTIL=42.0',
1155
+ '#EXT-X-INDEPENDENT-SEGMENTS'
1156
+ ].join('\n'));
1157
+ this.parser.end();
1158
+ assert.equal(this.parser.manifest.independentSegments, true);
1159
+ });
1160
+
1161
+ QUnit.test('parses #EXT-X-CONTENT-STEERING', function(assert) {
1162
+ const expectedContentSteeringObject = {
1163
+ serverUri: '/foo?bar=00012',
1164
+ pathwayId: 'CDN-A'
1165
+ };
1166
+
1167
+ this.parser.push('#EXT-X-CONTENT-STEERING:SERVER-URI="/foo?bar=00012",PATHWAY-ID="CDN-A"');
1168
+ this.parser.end();
1169
+ assert.deepEqual(this.parser.manifest.contentSteering, expectedContentSteeringObject);
1170
+ });
1171
+
1172
+ QUnit.test('parses #EXT-X-CONTENT-STEERING without PATHWAY-ID', function(assert) {
1173
+ const expectedContentSteeringObject = {
1174
+ serverUri: '/bar?foo=00012'
1175
+ };
1176
+
1177
+ this.parser.push('#EXT-X-CONTENT-STEERING:SERVER-URI="/bar?foo=00012"');
1178
+ this.parser.end();
1179
+ assert.deepEqual(this.parser.manifest.contentSteering, expectedContentSteeringObject);
1180
+ });
1181
+
1182
+ QUnit.test('warns on #EXT-X-CONTENT-STEERING missing SERVER-URI', function(assert) {
1183
+ const warning = ['#EXT-X-CONTENT-STEERING lacks required attribute(s): SERVER-URI'];
1184
+
1185
+ this.parser.push('#EXT-X-CONTENT-STEERING:PATHWAY-ID="CDN-A"');
1186
+ this.parser.end();
1187
+ assert.deepEqual(this.warnings, warning, 'warnings as expected');
1188
+ });
1189
+
1190
+ QUnit.test('playlist line numbers ', function(assert) {
1191
+ this.parser.push([
1192
+ '#EXTM3U',
1193
+ '#EXT-X-VERSION:7',
1194
+ '#EXT-X-TARGETDURATION:4',
1195
+ '#EXT-X-MEDIA-SEQUENCE:2421',
1196
+ '#EXT-X-MAP:URI="init.mp4"',
1197
+ '#EXTINF:3.989333,',
1198
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:19.871-0700',
1199
+ 'stream2421.m4s',
1200
+ '#EXTINF:4.010667,',
1201
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:23.860-0700',
1202
+ 'stream2422.m4s',
1203
+ '#EXTINF:3.989333,',
1204
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:27.871-0700',
1205
+ 'stream2423.m4s',
1206
+ '#EXTINF:4.010667,',
1207
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:31.860-0700',
1208
+ 'stream2424.m4s',
1209
+ '#EXTINF:3.989333,',
1210
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:35.871-0700',
1211
+ 'stream2425.m4s',
1212
+ '#EXTINF:4.010667,',
1213
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:39.860-0700',
1214
+ 'stream2426.m4s',
1215
+ '#EXTINF:3.989333,',
1216
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:43.871-0700',
1217
+ 'stream2427.m4s',
1218
+ '#EXTINF:4.010667,',
1219
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:47.860-0700',
1220
+ 'stream2428.m4s',
1221
+ '#EXTINF:3.989333,',
1222
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:51.871-0700',
1223
+ 'stream2429.m4s',
1224
+ '#EXTINF:4.010667,',
1225
+ '#EXT-X-PROGRAM-DATE-TIME:2024-04-17T09:43:55.860-0700',
1226
+ 'stream2430.m4s`'
1227
+ ].join('\n'));
1228
+ this.parser.end();
1229
+ assert.equal(this.parser.manifest.segments[2].lineNumberStart, 12);
1230
+ assert.equal(this.parser.manifest.segments[2].lineNumberEnd, 14);
1231
+ });
1232
+
860
1233
  QUnit.module('integration');
861
1234
 
1235
+ /*
862
1236
  for (const key in testDataExpected) {
863
1237
  if (!testDataManifests[key]) {
864
1238
  throw new Error(`${key}.js does not have an equivelent m3u8 manifest to test against`);
@@ -880,5 +1254,6 @@ QUnit.module('m3u8s', function(hooks) {
880
1254
  );
881
1255
  });
882
1256
  }
1257
+ */
883
1258
 
884
1259
  });