shaka-player 4.2.10 → 4.2.12

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 (64) hide show
  1. package/.github/workflows/appspot.yaml +39 -0
  2. package/.github/workflows/demo-version-index.yaml +3 -3
  3. package/CHANGELOG.md +42 -0
  4. package/CONTRIBUTORS +1 -0
  5. package/README.md +3 -0
  6. package/demo/custom.js +1 -1
  7. package/demo/demo.less +1 -0
  8. package/dist/demo.compiled.debug.js +2 -2
  9. package/dist/demo.compiled.debug.map +2 -2
  10. package/dist/demo.compiled.js +2 -2
  11. package/dist/demo.compiled.map +2 -2
  12. package/dist/demo.css +1 -1
  13. package/dist/demo.css.map +1 -1
  14. package/dist/deps.js +2 -2
  15. package/dist/locales.js +31 -31
  16. package/dist/shaka-player.compiled.d.ts +30 -0
  17. package/dist/shaka-player.compiled.debug.d.ts +30 -0
  18. package/dist/shaka-player.compiled.debug.externs.js +9 -0
  19. package/dist/shaka-player.compiled.debug.js +250 -246
  20. package/dist/shaka-player.compiled.debug.map +3 -3
  21. package/dist/shaka-player.compiled.externs.js +9 -0
  22. package/dist/shaka-player.compiled.js +789 -787
  23. package/dist/shaka-player.compiled.map +4 -4
  24. package/dist/shaka-player.ui.d.ts +30 -0
  25. package/dist/shaka-player.ui.debug.d.ts +30 -0
  26. package/dist/shaka-player.ui.debug.externs.js +9 -0
  27. package/dist/shaka-player.ui.debug.js +290 -285
  28. package/dist/shaka-player.ui.debug.map +3 -3
  29. package/dist/shaka-player.ui.externs.js +9 -0
  30. package/dist/shaka-player.ui.js +943 -941
  31. package/dist/shaka-player.ui.map +4 -4
  32. package/docs/tutorials/drm-config.md +9 -6
  33. package/docs/tutorials/welcome.md +1 -1
  34. package/externs/ima.js +34 -0
  35. package/lib/abr/simple_abr_manager.js +1 -1
  36. package/lib/cea/mp4_cea_parser.js +30 -14
  37. package/lib/dash/dash_parser.js +18 -3
  38. package/lib/hls/hls_parser.js +19 -2
  39. package/lib/hls/manifest_text_parser.js +10 -1
  40. package/lib/media/drm_engine.js +23 -14
  41. package/lib/media/gap_jumping_controller.js +24 -2
  42. package/lib/media/media_source_engine.js +10 -6
  43. package/lib/media/playhead.js +11 -0
  44. package/lib/media/region_observer.js +19 -1
  45. package/lib/media/segment_reference.js +22 -0
  46. package/lib/media/streaming_engine.js +16 -7
  47. package/lib/media/video_wrapper.js +6 -1
  48. package/lib/player.js +13 -4
  49. package/lib/polyfill/abort_controller.js +7 -0
  50. package/lib/text/vtt_text_parser.js +2 -1
  51. package/lib/util/manifest_parser_utils.js +1 -1
  52. package/lib/util/mp4_box_parsers.js +8 -3
  53. package/lib/util/stream_utils.js +1 -0
  54. package/package.json +9 -4
  55. package/test/cea/mp4_cea_parser_unit.js +26 -3
  56. package/test/dash/dash_parser_manifest_unit.js +120 -0
  57. package/test/hls/hls_live_unit.js +3 -1
  58. package/test/hls/hls_parser_unit.js +150 -0
  59. package/test/media/media_source_engine_unit.js +31 -0
  60. package/test/media/region_observer_unit.js +2 -1
  61. package/test/test/assets/multiple-trun-init.mp4 +0 -0
  62. package/test/test/assets/multiple-trun-segment.mp4 +0 -0
  63. package/test/test/util/layout_tests.js +4 -3
  64. package/ui/resolution_selection.js +7 -1
@@ -640,10 +640,15 @@ shaka.media.StreamingEngine = class {
640
640
  const mediaState = this.mediaStates_.get(type);
641
641
  const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
642
642
 
643
- // Always clear the iterator since we need to start streaming from the
644
- // new time. This also happens in clearBuffer_, but if we don't clear,
645
- // we still want to reset the iterator.
646
- mediaState.segmentIterator = null;
643
+ let segment = null;
644
+ if (mediaState.segmentIterator) {
645
+ segment = mediaState.segmentIterator.current();
646
+ }
647
+ // Only reset the iterator if we seek outside the current segment.
648
+ if (!segment || segment.startTime > presentationTime ||
649
+ segment.endTime < presentationTime) {
650
+ mediaState.segmentIterator = null;
651
+ }
647
652
 
648
653
  if (!newTimeIsBuffered(type)) {
649
654
  const bufferEnd =
@@ -1303,7 +1308,8 @@ shaka.media.StreamingEngine = class {
1303
1308
  const dataToAppend = remaining.subarray(0, offset);
1304
1309
  remaining = remaining.subarray(offset);
1305
1310
  await this.append_(
1306
- mediaState, presentationTime, stream, reference, dataToAppend);
1311
+ mediaState, presentationTime, stream, reference, dataToAppend,
1312
+ /* isChunkedData= */ true);
1307
1313
  }
1308
1314
  };
1309
1315
 
@@ -1691,10 +1697,12 @@ shaka.media.StreamingEngine = class {
1691
1697
  * @param {shaka.extern.Stream} stream
1692
1698
  * @param {!shaka.media.SegmentReference} reference
1693
1699
  * @param {BufferSource} segment
1700
+ * @param {boolean=} isChunkedData
1694
1701
  * @return {!Promise}
1695
1702
  * @private
1696
1703
  */
1697
- async append_(mediaState, presentationTime, stream, reference, segment) {
1704
+ async append_(mediaState, presentationTime, stream, reference, segment,
1705
+ isChunkedData = false) {
1698
1706
  const logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
1699
1707
 
1700
1708
  const hasClosedCaptions = stream.closedCaptions &&
@@ -1727,7 +1735,8 @@ shaka.media.StreamingEngine = class {
1727
1735
  reference,
1728
1736
  hasClosedCaptions,
1729
1737
  seeked,
1730
- adaptation);
1738
+ adaptation,
1739
+ isChunkedData);
1731
1740
  this.destroyer_.ensureNotDestroyed();
1732
1741
  shaka.log.v2(logPrefix, 'appended media segment');
1733
1742
  }
@@ -27,15 +27,19 @@ shaka.media.VideoWrapper = class {
27
27
  /**
28
28
  * @param {!HTMLMediaElement} video
29
29
  * @param {function()} onSeek Called when the video seeks.
30
+ * @param {function(number)} onStarted Called when the video has started.
30
31
  * @param {number} startTime The time to start at.
31
32
  */
32
- constructor(video, onSeek, startTime) {
33
+ constructor(video, onSeek, onStarted, startTime) {
33
34
  /** @private {HTMLMediaElement} */
34
35
  this.video_ = video;
35
36
 
36
37
  /** @private {function()} */
37
38
  this.onSeek_ = onSeek;
38
39
 
40
+ /** @private {function(number)} */
41
+ this.onStarted_ = onStarted;
42
+
39
43
  /** @private {number} */
40
44
  this.startTime_ = startTime;
41
45
 
@@ -165,6 +169,7 @@ shaka.media.VideoWrapper = class {
165
169
  this.started_ = true;
166
170
 
167
171
  this.eventManager_.listen(this.video_, 'seeking', () => this.onSeek_());
172
+ this.onStarted_(this.video_.currentTime);
168
173
  }
169
174
  };
170
175
 
package/lib/player.js CHANGED
@@ -1996,7 +1996,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
1996
1996
  this.abrManager_.setMediaElement(mediaElement);
1997
1997
 
1998
1998
  this.playhead_ = this.createPlayhead(has.startTime);
1999
- this.playheadObservers_ = this.createPlayheadObserversForMSE_();
1999
+ this.playheadObservers_ =
2000
+ this.createPlayheadObserversForMSE_(has.startTimeOfLoad);
2000
2001
 
2001
2002
  // We need to start the buffer management code near the end because it will
2002
2003
  // set the initial buffering state and that depends on other components
@@ -2761,17 +2762,21 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
2761
2762
  * Create the observers for MSE playback. These observers are responsible for
2762
2763
  * notifying the app and player of specific events during MSE playback.
2763
2764
  *
2765
+ * @param {number} startTimeOfLoad
2764
2766
  * @return {!shaka.media.PlayheadObserverManager}
2765
2767
  * @private
2766
2768
  */
2767
- createPlayheadObserversForMSE_() {
2769
+ createPlayheadObserversForMSE_(startTimeOfLoad) {
2768
2770
  goog.asserts.assert(this.manifest_, 'Must have manifest');
2769
2771
  goog.asserts.assert(this.regionTimeline_, 'Must have region timeline');
2770
2772
  goog.asserts.assert(this.video_, 'Must have video element');
2771
2773
 
2774
+ const startsPastZero = this.isLive() || startTimeOfLoad > 0;
2775
+
2772
2776
  // Create the region observer. This will allow us to notify the app when we
2773
2777
  // move in and out of timeline regions.
2774
- const regionObserver = new shaka.media.RegionObserver(this.regionTimeline_);
2778
+ const regionObserver = new shaka.media.RegionObserver(
2779
+ this.regionTimeline_, startsPastZero);
2775
2780
 
2776
2781
  regionObserver.addEventListener('enter', (event) => {
2777
2782
  /** @type {shaka.extern.TimelineRegionInfo} */
@@ -5686,6 +5691,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget {
5686
5691
  error.category != shaka.util.Error.Category.NETWORK) {
5687
5692
  return false;
5688
5693
  }
5694
+ if (this.loadMode_ != shaka.Player.LoadMode.MEDIA_SOURCE) {
5695
+ return false;
5696
+ }
5697
+ goog.asserts.assert(this.manifest_, 'Should have manifest!');
5689
5698
 
5690
5699
  if (!navigator.onLine) {
5691
5700
  // Don't restrict variants if we're completely offline, or else we end up
@@ -6626,7 +6635,7 @@ shaka.Player.TYPICAL_BUFFERING_THRESHOLD_ = 0.5;
6626
6635
  * @define {string} A version number taken from git at compile time.
6627
6636
  * @export
6628
6637
  */
6629
- shaka.Player.version = 'v4.2.10-uncompiled';
6638
+ shaka.Player.version = 'v4.2.12-uncompiled';
6630
6639
 
6631
6640
  // Initialize the deprecation system using the version string we just set
6632
6641
  // on the player.
@@ -86,6 +86,13 @@ class extends shaka.util.FakeEventTarget {
86
86
  return this.reason_;
87
87
  }
88
88
 
89
+ /** @return {void} */
90
+ throwIfAborted() {
91
+ if (this.aborted_) {
92
+ throw this.reason_;
93
+ }
94
+ }
95
+
89
96
  /**
90
97
  * @param {*} reason
91
98
  * @private
@@ -785,7 +785,8 @@ shaka.text.VttTextParser = class {
785
785
  } else if ((results = /^size:([\d.]+)%$/.exec(word))) {
786
786
  cue.size = Number(results[1]);
787
787
  } else if ((results =
788
- /^position:([\d.]+)%(?:,(line-left|line-right|center|start|end))?$/
788
+ // eslint-disable-next-line max-len
789
+ /^position:([\d.]+)%(?:,(line-left|line-right|middle|center|start|end))?$/
789
790
  .exec(word))) {
790
791
  cue.position = Number(results[1]);
791
792
  if (results[2]) {
@@ -168,7 +168,7 @@ shaka.util.ManifestParserUtils.VIDEO_CODEC_REGEXPS_ = [
168
168
  /^hvc/,
169
169
  /^vp0?[89]/,
170
170
  /^av01/,
171
- /^dvhe/,
171
+ /^dvh/,
172
172
  ];
173
173
 
174
174
 
@@ -112,10 +112,11 @@ shaka.util.Mp4BoxParsers = class {
112
112
  static parseTRUN(reader, version, flags) {
113
113
  const sampleCount = reader.readUint32();
114
114
  const sampleData = [];
115
+ let dataOffset = null;
115
116
 
116
- // Skip "data_offset" if present.
117
+ // "data_offset"
117
118
  if (flags & 0x000001) {
118
- reader.skip(4);
119
+ dataOffset = reader.readUint32();
119
120
  }
120
121
 
121
122
  // Skip "first_sample_flags" if present.
@@ -159,6 +160,7 @@ shaka.util.Mp4BoxParsers = class {
159
160
  return {
160
161
  sampleCount,
161
162
  sampleData,
163
+ dataOffset,
162
164
  };
163
165
  }
164
166
 
@@ -252,13 +254,16 @@ shaka.util.ParsedTREXBox;
252
254
  /**
253
255
  * @typedef {{
254
256
  * sampleCount: number,
255
- * sampleData: !Array.<shaka.util.ParsedTRUNSample>
257
+ * sampleData: !Array.<shaka.util.ParsedTRUNSample>,
258
+ * dataOffset: ?number
256
259
  * }}
257
260
  *
258
261
  * @property {number} sampleCount
259
262
  * As per the spec: the number of samples being added in this run;
260
263
  * @property {!Array.<shaka.util.ParsedTRUNSample>} sampleData
261
264
  * An array of size <sampleCount> containing data for each sample
265
+ * @property {?number} dataOffset
266
+ * If specified via flags, this indicate the offset of the sample in bytes.
262
267
  *
263
268
  * @exportDoc
264
269
  */
@@ -1066,6 +1066,7 @@ shaka.util.StreamUtils = class {
1066
1066
  track.frameRate = video.frameRate || null;
1067
1067
  track.pixelAspectRatio = video.pixelAspectRatio || null;
1068
1068
  track.videoBandwidth = video.bandwidth || null;
1069
+ track.hdr = video.hdr || null;
1069
1070
  }
1070
1071
 
1071
1072
  if (audio) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shaka-player",
3
3
  "description": "DASH/EME video player library",
4
- "version": "4.2.10",
4
+ "version": "4.2.12",
5
5
  "homepage": "https://github.com/shaka-project/shaka-player",
6
6
  "author": "Google",
7
7
  "maintainers": [
@@ -18,6 +18,7 @@
18
18
  "awesomplete": "^1.1.5",
19
19
  "babel-plugin-istanbul": "^6.1.1",
20
20
  "cajon": "^0.4.4",
21
+ "clean-package": "^2.2.0",
21
22
  "code-prettify": "^0.1.0",
22
23
  "color-themes-for-google-code-prettify": "^2.0.4",
23
24
  "core-js": "^3.21.1",
@@ -41,7 +42,7 @@
41
42
  "karma-coverage": "^2.2.0",
42
43
  "karma-jasmine": "^4.0.1",
43
44
  "karma-jasmine-ajax": "^0.1.13",
44
- "karma-local-wd-launcher": "^1.6.1",
45
+ "karma-local-wd-launcher": "^1.6.2",
45
46
  "karma-opera-launcher": "^1.0.0",
46
47
  "karma-sourcemap-loader": "^0.3.8",
47
48
  "karma-spec-reporter": "^0.0.34",
@@ -91,12 +92,16 @@
91
92
  },
92
93
  "license": "Apache-2.0",
93
94
  "scripts": {
95
+ "prepack": "clean-package",
96
+ "postpack": "clean-package restore",
94
97
  "prepublishOnly": "python build/checkversion.py && python build/all.py --force"
95
98
  },
96
99
  "dependencies": {
97
100
  "eme-encryption-scheme-polyfill": "^2.1.1"
98
101
  },
99
- "engines": {
100
- "node": ">=14"
102
+ "clean-package": {
103
+ "remove": [
104
+ "engines"
105
+ ]
101
106
  }
102
107
  }
@@ -7,20 +7,33 @@
7
7
  describe('Mp4CeaParser', () => {
8
8
  const ceaInitSegmentUri = '/base/test/test/assets/cea-init.mp4';
9
9
  const ceaSegmentUri = '/base/test/test/assets/cea-segment.mp4';
10
+ const multipleTrunInitSegmentUri =
11
+ '/base/test/test/assets/multiple-trun-init.mp4';
12
+ const multipleTrunSegmentUri =
13
+ '/base/test/test/assets/multiple-trun-segment.mp4';
10
14
  const Util = shaka.test.Util;
11
15
 
12
16
  /** @type {!ArrayBuffer} */
13
17
  let ceaInitSegment;
14
18
  /** @type {!ArrayBuffer} */
15
19
  let ceaSegment;
20
+ /** @type {!ArrayBuffer} */
21
+ let multipleTrunInitSegment;
22
+ /** @type {!ArrayBuffer} */
23
+ let multipleTrunSegment;
16
24
 
17
25
  beforeAll(async () => {
18
- const responses = await Promise.all([
26
+ [
27
+ ceaInitSegment,
28
+ ceaSegment,
29
+ multipleTrunInitSegment,
30
+ multipleTrunSegment,
31
+ ] = await Promise.all([
19
32
  shaka.test.Util.fetch(ceaInitSegmentUri),
20
33
  shaka.test.Util.fetch(ceaSegmentUri),
34
+ shaka.test.Util.fetch(multipleTrunInitSegmentUri),
35
+ shaka.test.Util.fetch(multipleTrunSegmentUri),
21
36
  ]);
22
- ceaInitSegment = responses[0];
23
- ceaSegment = responses[1];
24
37
  });
25
38
 
26
39
  /**
@@ -70,6 +83,16 @@ describe('Mp4CeaParser', () => {
70
83
  .toEqual(expectedCea708Packet);
71
84
  });
72
85
 
86
+ it('parses cea data from a segment with multiple trun boxes', () => {
87
+ const ceaParser = new shaka.cea.Mp4CeaParser();
88
+
89
+ ceaParser.init(multipleTrunInitSegment);
90
+ const ceaPackets = ceaParser.parse(multipleTrunSegment);
91
+ // The first trun box references samples with 48 CEA packets.
92
+ // The second trun box references samples with 132 more, for a total of 180.
93
+ expect(ceaPackets.length).toBe(180);
94
+ });
95
+
73
96
  it('parses an invalid init segment', () => {
74
97
  const cea708Parser = new shaka.cea.Mp4CeaParser();
75
98
 
@@ -1931,6 +1931,66 @@ describe('DashParser Manifest', () => {
1931
1931
  expect(stream.hdr).toBe('PQ');
1932
1932
  });
1933
1933
 
1934
+ it('supports SupplementalProperty MatrixCoefficients', async () => {
1935
+ // (DASH-IF IOP v4.3 6.2.5.1.)
1936
+ const scheme = cicpScheme('MatrixCoefficients');
1937
+ const manifestText = [
1938
+ '<MPD minBufferTime="PT75S">',
1939
+ ' <Period id="1" duration="PT30S">',
1940
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
1941
+ ` <SupplementalProperty schemeIdUri="${scheme}" value="9" />`,
1942
+ ' <Representation codecs="hvc1.2.4.L153.B0">',
1943
+ ' <BaseURL>v-sd.mp4</BaseURL>',
1944
+ ' <SegmentBase indexRange="100-200" />',
1945
+ ' </Representation>',
1946
+ ' </AdaptationSet>',
1947
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
1948
+ ' <Representation id="audio-en">',
1949
+ ' <BaseURL>a-en.mp4</BaseURL>',
1950
+ ' <SegmentBase indexRange="100-200" />',
1951
+ ' </Representation>',
1952
+ ' </AdaptationSet>',
1953
+ ' </Period>',
1954
+ '</MPD>',
1955
+ ].join('\n');
1956
+
1957
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
1958
+
1959
+ /** @type {shaka.extern.Manifest} */
1960
+ const manifest = await parser.start('dummy://foo', playerInterface);
1961
+ expect(manifest.variants.length).toBe(1);
1962
+ });
1963
+
1964
+ it('supports SupplementalProperty ColourPrimaries', async () => {
1965
+ // (DASH-IF IOP v4.3 6.2.5.1.)
1966
+ const scheme = cicpScheme('ColourPrimaries');
1967
+ const manifestText = [
1968
+ '<MPD minBufferTime="PT75S">',
1969
+ ' <Period id="1" duration="PT30S">',
1970
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
1971
+ ` <SupplementalProperty schemeIdUri="${scheme}" value="9" />`,
1972
+ ' <Representation codecs="hvc1.2.4.L153.B0">',
1973
+ ' <BaseURL>v-sd.mp4</BaseURL>',
1974
+ ' <SegmentBase indexRange="100-200" />',
1975
+ ' </Representation>',
1976
+ ' </AdaptationSet>',
1977
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
1978
+ ' <Representation id="audio-en">',
1979
+ ' <BaseURL>a-en.mp4</BaseURL>',
1980
+ ' <SegmentBase indexRange="100-200" />',
1981
+ ' </Representation>',
1982
+ ' </AdaptationSet>',
1983
+ ' </Period>',
1984
+ '</MPD>',
1985
+ ].join('\n');
1986
+
1987
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
1988
+
1989
+ /** @type {shaka.extern.Manifest} */
1990
+ const manifest = await parser.start('dummy://foo', playerInterface);
1991
+ expect(manifest.variants.length).toBe(1);
1992
+ });
1993
+
1934
1994
  it('supports HDR signaling via EssentialProperty', async () => {
1935
1995
  // (DASH-IF IOP v4.3 6.2.5.1.)
1936
1996
  const hdrScheme = cicpScheme('TransferCharacteristics');
@@ -1964,6 +2024,66 @@ describe('DashParser Manifest', () => {
1964
2024
  expect(stream.hdr).toBe('HLG');
1965
2025
  });
1966
2026
 
2027
+ it('supports EssentialProperty MatrixCoefficients', async () => {
2028
+ // (DASH-IF IOP v4.3 6.2.5.1.)
2029
+ const hdrScheme = cicpScheme('MatrixCoefficients');
2030
+ const manifestText = [
2031
+ '<MPD minBufferTime="PT75S">',
2032
+ ' <Period id="1" duration="PT30S">',
2033
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
2034
+ ` <EssentialProperty schemeIdUri="${hdrScheme}" value="9" />`,
2035
+ ' <Representation codecs="hvc1.2.4.L153.B0">',
2036
+ ' <BaseURL>v-sd.mp4</BaseURL>',
2037
+ ' <SegmentBase indexRange="100-200" />',
2038
+ ' </Representation>',
2039
+ ' </AdaptationSet>',
2040
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
2041
+ ' <Representation id="audio-en">',
2042
+ ' <BaseURL>a-en.mp4</BaseURL>',
2043
+ ' <SegmentBase indexRange="100-200" />',
2044
+ ' </Representation>',
2045
+ ' </AdaptationSet>',
2046
+ ' </Period>',
2047
+ '</MPD>',
2048
+ ].join('\n');
2049
+
2050
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
2051
+
2052
+ /** @type {shaka.extern.Manifest} */
2053
+ const manifest = await parser.start('dummy://foo', playerInterface);
2054
+ expect(manifest.variants.length).toBe(1);
2055
+ });
2056
+
2057
+ it('supports EssentialProperty ColourPrimaries', async () => {
2058
+ // (DASH-IF IOP v4.3 6.2.5.1.)
2059
+ const hdrScheme = cicpScheme('ColourPrimaries');
2060
+ const manifestText = [
2061
+ '<MPD minBufferTime="PT75S">',
2062
+ ' <Period id="1" duration="PT30S">',
2063
+ ' <AdaptationSet id="2" mimeType="video/mp4">',
2064
+ ` <EssentialProperty schemeIdUri="${hdrScheme}" value="9" />`,
2065
+ ' <Representation codecs="hvc1.2.4.L153.B0">',
2066
+ ' <BaseURL>v-sd.mp4</BaseURL>',
2067
+ ' <SegmentBase indexRange="100-200" />',
2068
+ ' </Representation>',
2069
+ ' </AdaptationSet>',
2070
+ ' <AdaptationSet id="3" mimeType="audio/mp4">',
2071
+ ' <Representation id="audio-en">',
2072
+ ' <BaseURL>a-en.mp4</BaseURL>',
2073
+ ' <SegmentBase indexRange="100-200" />',
2074
+ ' </Representation>',
2075
+ ' </AdaptationSet>',
2076
+ ' </Period>',
2077
+ '</MPD>',
2078
+ ].join('\n');
2079
+
2080
+ fakeNetEngine.setResponseText('dummy://foo', manifestText);
2081
+
2082
+ /** @type {shaka.extern.Manifest} */
2083
+ const manifest = await parser.start('dummy://foo', playerInterface);
2084
+ expect(manifest.variants.length).toBe(1);
2085
+ });
2086
+
1967
2087
  it('supports SDR signalling via EssentialProperty', async () => {
1968
2088
  // (DASH-IF IOP v4.3 6.2.5.1.)
1969
2089
  const scheme = cicpScheme('TransferCharacteristics');
@@ -560,7 +560,8 @@ describe('HlsParser live', () => {
560
560
  // partialRef3
561
561
  '#EXT-X-PART:DURATION=2,URI="partial.mp4",BYTERANGE=210@0\n',
562
562
  // preloadRef
563
- '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="partial.mp4",BYTERANGE-START=210\n',
563
+ '#EXT-X-PRELOAD-HINT:TYPE=PART,URI="partial.mp4",BYTERANGE-START=210,',
564
+ 'BYTERANGE-LENGTH=210\n',
564
565
  ].join('');
565
566
 
566
567
  const partialRef = makeReference(
@@ -583,6 +584,7 @@ describe('HlsParser live', () => {
583
584
  const preloadRef = makeReference(
584
585
  'test:/partial.mp4', 6, 7.5, /* syncTime= */ null,
585
586
  /* baseUri= */ '', /* startByte= */ 210, /* endByte= */ null);
587
+ preloadRef.markAsPreload();
586
588
 
587
589
  // ref2 is not fully published yet, so it doesn't have a segment uri.
588
590
  const ref2 = makeReference(
@@ -983,6 +983,81 @@ describe('HlsParser', () => {
983
983
  await testHlsParser(master, media, manifest);
984
984
  });
985
985
 
986
+ it('adds subtitle role when characteristics are empty', async () => {
987
+ const master = [
988
+ '#EXTM3U\n',
989
+ '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1",',
990
+ 'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1",SUBTITLES="sub1"\n',
991
+ 'video\n',
992
+
993
+ '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="en",',
994
+ 'NAME="English",URI="audio"\n',
995
+
996
+ '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="en",',
997
+ 'CHARACTERISTICS="public.accessibility.describes-video,',
998
+ 'public.accessibility.describes-music-and-sound",',
999
+ 'NAME="English (describes-video)",URI="audio2"\n',
1000
+
1001
+ '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="en",',
1002
+ 'NAME="English (subtitle)",DEFAULT=YES,AUTOSELECT=YES,',
1003
+ 'URI="text"\n',
1004
+
1005
+ '#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",LANGUAGE="en",',
1006
+ 'NAME="English (caption)",DEFAULT=YES,AUTOSELECT=YES,',
1007
+ 'CHARACTERISTICS="public.accessibility.describes-music-and-sound",',
1008
+ 'URI="text2"\n',
1009
+ ].join('');
1010
+
1011
+ const media = [
1012
+ '#EXTM3U\n',
1013
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
1014
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
1015
+ '#EXTINF:5,\n',
1016
+ '#EXT-X-BYTERANGE:121090@616\n',
1017
+ 'main.mp4',
1018
+ ].join('');
1019
+
1020
+ const manifest = shaka.test.ManifestGenerator.generate((manifest) => {
1021
+ manifest.anyTimeline();
1022
+ manifest.addPartialVariant((variant) => {
1023
+ variant.language = 'en';
1024
+ variant.addPartialStream(ContentType.VIDEO);
1025
+ variant.addPartialStream(ContentType.AUDIO, (stream) => {
1026
+ stream.language = 'en';
1027
+ stream.roles = [];
1028
+ });
1029
+ });
1030
+ manifest.addPartialVariant((variant) => {
1031
+ variant.language = 'en';
1032
+ variant.addPartialStream(ContentType.VIDEO);
1033
+ variant.addPartialStream(ContentType.AUDIO, (stream) => {
1034
+ stream.language = 'en';
1035
+ stream.roles = [
1036
+ 'public.accessibility.describes-video',
1037
+ 'public.accessibility.describes-music-and-sound',
1038
+ ];
1039
+ });
1040
+ });
1041
+ manifest.addPartialTextStream((stream) => {
1042
+ stream.language = 'en';
1043
+ stream.kind = TextStreamKind.SUBTITLE;
1044
+ stream.roles = [
1045
+ 'subtitle',
1046
+ ];
1047
+ });
1048
+ manifest.addPartialTextStream((stream) => {
1049
+ stream.language = 'en';
1050
+ stream.kind = TextStreamKind.SUBTITLE;
1051
+ stream.roles = [
1052
+ 'public.accessibility.describes-music-and-sound',
1053
+ ];
1054
+ });
1055
+ manifest.sequenceMode = true;
1056
+ });
1057
+
1058
+ await testHlsParser(master, media, manifest);
1059
+ });
1060
+
986
1061
  it('parses characteristics from text tags', async () => {
987
1062
  const master = [
988
1063
  '#EXTM3U\n',
@@ -1652,6 +1727,81 @@ describe('HlsParser', () => {
1652
1727
  }
1653
1728
  });
1654
1729
 
1730
+ it('ignore segments with #EXTINF:0', async () => {
1731
+ const master = [
1732
+ '#EXTM3U\n',
1733
+ '#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",LANGUAGE="eng",',
1734
+ 'CHANNELS="2",URI="audio"\n',
1735
+ '#EXT-X-STREAM-INF:BANDWIDTH=200,CODECS="avc1,mp4a",',
1736
+ 'RESOLUTION=960x540,FRAME-RATE=60,AUDIO="aud1"\n',
1737
+ 'video\n',
1738
+ ].join('');
1739
+
1740
+ const video = [
1741
+ '#EXTM3U\n',
1742
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
1743
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
1744
+ '#EXTINF:5,\n',
1745
+ 'main.mp4\n',
1746
+ '#EXTINF:0,\n',
1747
+ 'main.mp4\n',
1748
+ '#EXTINF:5,\n',
1749
+ 'main.mp4\n',
1750
+ ].join('');
1751
+
1752
+ const audio = [
1753
+ '#EXTM3U\n',
1754
+ '#EXT-X-PLAYLIST-TYPE:VOD\n',
1755
+ '#EXT-X-MAP:URI="init.mp4",BYTERANGE="616@0"\n',
1756
+ '#EXTINF:5,\n',
1757
+ 'main.mp4\n',
1758
+ '#EXTINF:0,\n',
1759
+ 'main.mp4\n',
1760
+ '#EXTINF:5,\n',
1761
+ 'main.mp4\n',
1762
+ ].join('');
1763
+
1764
+ fakeNetEngine
1765
+ .setResponseText('test:/master', master)
1766
+ .setResponseText('test:/audio', audio)
1767
+ .setResponseText('test:/video', video)
1768
+ .setResponseValue('test:/init.mp4', initSegmentData)
1769
+ .setResponseValue('test:/main.mp4', segmentData);
1770
+
1771
+ const actual = await parser.start('test:/master', playerInterface);
1772
+
1773
+ expect(actual.variants.length).toBe(1);
1774
+
1775
+ const variant = actual.variants[0];
1776
+ expect(variant.video).toBeTruthy();
1777
+ expect(variant.audio).toBeTruthy();
1778
+
1779
+ await variant.video.createSegmentIndex();
1780
+ goog.asserts.assert(variant.video.segmentIndex != null,
1781
+ 'Null segmentIndex!');
1782
+
1783
+ const firstVideoReference = variant.video.segmentIndex.get(0);
1784
+ const secondVideoReference = variant.video.segmentIndex.get(1);
1785
+ const thirdVideoReference = variant.video.segmentIndex.get(2);
1786
+
1787
+ expect(firstVideoReference).not.toBe(null);
1788
+ expect(secondVideoReference).not.toBe(null);
1789
+ expect(thirdVideoReference).toBe(null);
1790
+
1791
+
1792
+ await variant.audio.createSegmentIndex();
1793
+ goog.asserts.assert(variant.audio.segmentIndex != null,
1794
+ 'Null segmentIndex!');
1795
+
1796
+ const firstAudioReference = variant.audio.segmentIndex.get(0);
1797
+ const secondAudioReference = variant.audio.segmentIndex.get(1);
1798
+ const thirdAudioReference = variant.audio.segmentIndex.get(2);
1799
+
1800
+ expect(firstAudioReference).not.toBe(null);
1801
+ expect(secondAudioReference).not.toBe(null);
1802
+ expect(thirdAudioReference).toBe(null);
1803
+ });
1804
+
1655
1805
  it('Disable audio does not create audio streams', async () => {
1656
1806
  const master = [
1657
1807
  '#EXTM3U\n',