shaka-player 4.1.0 → 4.1.3

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 (162) hide show
  1. package/.github/workflows/build-and-test.yaml +67 -15
  2. package/.github/workflows/compute-incremental-coverage.py +251 -0
  3. package/.github/workflows/report-incremental-coverage.yaml +39 -0
  4. package/.github/workflows/selenium-lab-tests.yaml +44 -1
  5. package/AUTHORS +2 -0
  6. package/CHANGELOG.md +42 -0
  7. package/CONTRIBUTORS +4 -0
  8. package/README.md +20 -6
  9. package/demo/common/assets.js +1 -1
  10. package/demo/index.html +1 -3
  11. package/demo/main.js +3 -3
  12. package/dist/controls.css +1 -1
  13. package/dist/demo.compiled.debug.js +3 -3
  14. package/dist/demo.compiled.debug.map +2 -2
  15. package/dist/demo.compiled.js +3 -3
  16. package/dist/demo.compiled.map +2 -2
  17. package/dist/deps.js +3 -3
  18. package/dist/locales.js +29 -29
  19. package/dist/receiver.compiled.debug.js +1 -1
  20. package/dist/receiver.compiled.debug.map +1 -1
  21. package/dist/receiver.compiled.js +1 -1
  22. package/dist/receiver.compiled.map +1 -1
  23. package/dist/shaka-player.compiled.d.ts +12 -1
  24. package/dist/shaka-player.compiled.debug.d.ts +12 -1
  25. package/dist/shaka-player.compiled.debug.externs.js +52 -40
  26. package/dist/shaka-player.compiled.debug.js +270 -257
  27. package/dist/shaka-player.compiled.debug.map +4 -4
  28. package/dist/shaka-player.compiled.externs.js +52 -40
  29. package/dist/shaka-player.compiled.js +754 -746
  30. package/dist/shaka-player.compiled.map +4 -4
  31. package/dist/shaka-player.ui.d.ts +12 -1
  32. package/dist/shaka-player.ui.debug.d.ts +12 -1
  33. package/dist/shaka-player.ui.debug.externs.js +52 -40
  34. package/dist/shaka-player.ui.debug.js +296 -283
  35. package/dist/shaka-player.ui.debug.map +4 -4
  36. package/dist/shaka-player.ui.externs.js +52 -40
  37. package/dist/shaka-player.ui.js +874 -867
  38. package/dist/shaka-player.ui.map +4 -4
  39. package/docs/tutorials/ui.md +6 -6
  40. package/docs/tutorials/upgrade.md +1 -0
  41. package/docs/tutorials/welcome.md +6 -3
  42. package/externs/shaka/player.js +4 -2
  43. package/karma.conf.js +3 -11
  44. package/lib/ads/client_side_ad_manager.js +1 -1
  45. package/lib/dash/dash_parser.js +5 -0
  46. package/lib/hls/hls_parser.js +183 -70
  47. package/lib/media/drm_engine.js +1 -1
  48. package/lib/media/media_source_engine.js +57 -24
  49. package/lib/media/presentation_timeline.js +24 -1
  50. package/lib/media/segment_index.js +26 -1
  51. package/lib/media/segment_reference.js +10 -3
  52. package/lib/media/streaming_engine.js +4 -4
  53. package/lib/media/time_ranges_utils.js +46 -0
  54. package/lib/player.js +1 -6
  55. package/lib/polyfill/media_capabilities.js +9 -0
  56. package/lib/polyfill/videoplaybackquality.js +3 -1
  57. package/lib/text/cue.js +0 -5
  58. package/lib/text/simple_text_displayer.js +21 -9
  59. package/lib/text/ui_text_displayer.js +83 -19
  60. package/lib/util/buffer_utils.js +3 -0
  61. package/lib/util/object_utils.js +1 -0
  62. package/lib/util/platform.js +19 -1
  63. package/lib/util/stream_utils.js +24 -3
  64. package/lib/util/string_utils.js +102 -13
  65. package/maintained-branches.md +15 -0
  66. package/package.json +3 -5
  67. package/test/hls/hls_parser_unit.js +102 -8
  68. package/test/media/drm_engine_integration.js +24 -8
  69. package/test/media/media_source_engine_integration.js +24 -10
  70. package/test/media/media_source_engine_unit.js +53 -38
  71. package/test/media/streaming_engine_unit.js +28 -27
  72. package/test/test/util/fake_media_source_engine.js +10 -9
  73. package/test/text/simple_text_displayer_unit.js +27 -0
  74. package/test/text/ui_text_displayer_unit.js +65 -0
  75. package/test/util/string_utils_unit.js +46 -8
  76. package/ui/text_selection.js +2 -2
  77. package/app-engine/README.md +0 -9
  78. package/app-engine/demo-version-index/README.md +0 -14
  79. package/app-engine/demo-version-index/app.yaml +0 -28
  80. package/app-engine/demo-version-index/generate.py +0 -164
  81. package/app-engine/demo-version-index/main.py +0 -11
  82. package/app-engine/demo-version-index/requirements.txt +0 -5
  83. package/app-engine/demo-version-index/templates/index.html +0 -114
  84. package/app-engine/shaka-player-demo/README.md +0 -18
  85. package/app-engine/shaka-player-demo/app.yaml +0 -61
  86. package/app-engine/shaka-player-demo/main.py +0 -95
  87. package/app-engine/shaka-player-demo/posters/10yo.png +0 -0
  88. package/app-engine/shaka-player-demo/posters/10yo.xcf +0 -0
  89. package/app-engine/shaka-player-demo/posters/1yo.jpg +0 -0
  90. package/app-engine/shaka-player-demo/posters/2yo.png +0 -0
  91. package/app-engine/shaka-player-demo/posters/2yo.xcf +0 -0
  92. package/app-engine/shaka-player-demo/posters/3yo.png +0 -0
  93. package/app-engine/shaka-player-demo/posters/3yo.xcf +0 -0
  94. package/app-engine/shaka-player-demo/posters/4yo.png +0 -0
  95. package/app-engine/shaka-player-demo/posters/4yo.xcf +0 -0
  96. package/app-engine/shaka-player-demo/posters/5yo.png +0 -0
  97. package/app-engine/shaka-player-demo/posters/5yo.xcf +0 -0
  98. package/app-engine/shaka-player-demo/posters/6yo.png +0 -0
  99. package/app-engine/shaka-player-demo/posters/6yo.xcf +0 -0
  100. package/app-engine/shaka-player-demo/posters/7yo.png +0 -0
  101. package/app-engine/shaka-player-demo/posters/7yo.xcf +0 -0
  102. package/app-engine/shaka-player-demo/posters/8yo.png +0 -0
  103. package/app-engine/shaka-player-demo/posters/8yo.xcf +0 -0
  104. package/app-engine/shaka-player-demo/posters/9yo.png +0 -0
  105. package/app-engine/shaka-player-demo/posters/9yo.xcf +0 -0
  106. package/app-engine/shaka-player-demo/posters/audioOnly.gif +0 -0
  107. package/app-engine/shaka-player-demo/posters/audioRickRoll.gif +0 -0
  108. package/app-engine/shaka-player-demo/posters/colorbars.gif +0 -0
  109. package/app-engine/shaka-player-demo/posters/fireworks.jpg +0 -0
  110. package/app-engine/shaka-player-demo/posters/party-hat.png +0 -0
  111. package/app-engine/shaka-player-demo/posters/sources.txt +0 -46
  112. package/app-engine/shaka-player-demo/posters/standard.png +0 -0
  113. package/app-engine/shaka-player-demo/posters/trek.jpg +0 -0
  114. package/app-engine/shaka-player-demo/posters/valentines.png +0 -0
  115. package/app-engine/shaka-player-demo/posters/zombie.jpg +0 -0
  116. package/app-engine/shaka-player-demo/posters.py +0 -110
  117. package/app-engine/shaka-player-demo/requirements.txt +0 -5
  118. package/app-engine/shaka-player-demo/time.txt +0 -1
  119. package/build/README.md +0 -127
  120. package/build/__init__.py +0 -0
  121. package/build/all.py +0 -150
  122. package/build/apps.py +0 -186
  123. package/build/build.py +0 -392
  124. package/build/check.py +0 -354
  125. package/build/checkversion.py +0 -84
  126. package/build/compiler.py +0 -513
  127. package/build/conformance.textproto +0 -335
  128. package/build/docker/Dockerfile +0 -20
  129. package/build/docker/docker-compose.yml +0 -10
  130. package/build/docs.py +0 -54
  131. package/build/eslint-plugin-shaka-rules/arg-comment-spacing.js +0 -72
  132. package/build/eslint-plugin-shaka-rules/index.js +0 -24
  133. package/build/eslint-plugin-shaka-rules/package.json +0 -7
  134. package/build/eslint-plugin-shaka-rules/private.js +0 -109
  135. package/build/gendeps.py +0 -71
  136. package/build/generateExterns.js +0 -867
  137. package/build/generateLocalizations.py +0 -262
  138. package/build/generateTsDefs.py +0 -137
  139. package/build/install-linux-prereqs.sh +0 -136
  140. package/build/license-header +0 -5
  141. package/build/misspellings.txt +0 -39
  142. package/build/pixelsChanged.js +0 -35
  143. package/build/shaka-lab.yaml +0 -161
  144. package/build/shakaBuildHelpers.py +0 -356
  145. package/build/stats.py +0 -816
  146. package/build/subprocessWindowsPatch.py +0 -103
  147. package/build/test.py +0 -541
  148. package/build/types/ads +0 -8
  149. package/build/types/cast +0 -6
  150. package/build/types/cea +0 -13
  151. package/build/types/complete +0 -9
  152. package/build/types/core +0 -108
  153. package/build/types/dash +0 -8
  154. package/build/types/hls +0 -9
  155. package/build/types/manifests +0 -5
  156. package/build/types/networking +0 -6
  157. package/build/types/offline +0 -22
  158. package/build/types/polyfill +0 -18
  159. package/build/types/text +0 -11
  160. package/build/types/ui +0 -39
  161. package/build/updateScreenshots.py +0 -85
  162. package/build/wrapper.template.js +0 -52
@@ -30,7 +30,7 @@ Set up controls with HTML data attributes:
30
30
  The data-shaka-player-cast-receiver-id tag allows you to provide a Cast Application ID that
31
31
  the cast button will cast to; the value provided here is the sample cast receiver. -->
32
32
  <div data-shaka-player-container style="max-width:40em"
33
- data-shaka-player-cast-receiver-id="BBED8D28">
33
+ data-shaka-player-cast-receiver-id="06EDA6EE">
34
34
  <!-- The data-shaka-player tag will make the UI library use this video element.
35
35
  If no video is provided, the UI will automatically make one inside the container div. -->
36
36
  <video autoplay data-shaka-player id="video" style="width:100%;height:100%"></video>
@@ -111,7 +111,7 @@ set up a listener for the 'caststatuschanged' events.
111
111
  <!-- Add a data-shaka-player-cast-receiver-id tag to provide a Cast Application ID that
112
112
  the cast button will cast to; the value provided here is the sample cast receiver. -->
113
113
  <div data-shaka-player-container style="max-width:40em"
114
- data-shaka-player-cast-receiver-id="BBED8D28">
114
+ data-shaka-player-cast-receiver-id="06EDA6EE">
115
115
  </div>
116
116
  ```
117
117
 
@@ -154,7 +154,7 @@ or a `<source>` tag inside it to enable auto loading of the specified content.
154
154
 
155
155
  ```html
156
156
  <div data-shaka-player-container style="max-width:40em"
157
- data-shaka-player-cast-receiver-id="BBED8D28">
157
+ data-shaka-player-cast-receiver-id="06EDA6EE">
158
158
  <!-- The manifest url in the src attribute will be automatically loaded -->
159
159
  <video autoplay data-shaka-player id="video" style="width:100%;height:100%"
160
160
  src="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd"></video>
@@ -165,7 +165,7 @@ or
165
165
 
166
166
  ```html
167
167
  <div data-shaka-player-container style="max-width:40em"
168
- data-shaka-player-cast-receiver-id="BBED8D28">
168
+ data-shaka-player-cast-receiver-id="06EDA6EE">
169
169
  <video autoplay data-shaka-player id="video" style="width:100%;height:100%">
170
170
  <!-- The manifest url in the src attribute will be auto loaded -->
171
171
  <source src="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd"/>
@@ -178,7 +178,7 @@ call to the first one fails.
178
178
 
179
179
  ```html
180
180
  <div data-shaka-player-container style="max-width:40em"
181
- data-shaka-player-cast-receiver-id="BBED8D28">
181
+ data-shaka-player-cast-receiver-id="06EDA6EE">
182
182
  <video autoplay data-shaka-player id="video" style="width:100%;height:100%">
183
183
  <!-- Try this first -->
184
184
  <source src="https://storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd"/>
@@ -222,7 +222,7 @@ const video = controls.getVideo();
222
222
  // Receiver Compatability.
223
223
  ui.configure({
224
224
  // Set the castReceiverAppId
225
- 'castReceiverAppId': 'BBED8D28',
225
+ 'castReceiverAppId': '06EDA6EE',
226
226
  // Enable casting to native Android Apps (e.g. Android TV Apps)
227
227
  'castAndroidReceiverCompatible': true,
228
228
  });
@@ -25,6 +25,7 @@ application:
25
25
  - TextDecoder/TextEncoder platform support or polyfill required (affects
26
26
  Xbox One, but not evergreen browsers); we suggest the polyfill
27
27
  [https://github.com/anonyco/FastestSmallestTextEncoderDecoder](fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js)
28
+ Fallback included by default in v4.2
28
29
 
29
30
  - Support removed:
30
31
  - IE11 support removed
@@ -98,11 +98,14 @@ You can find a full list of available browsers with `--browsers help`, and you
98
98
  can find a complete list of testing options with `--help`.
99
99
 
100
100
 
101
- #### Join the announcement list
101
+ #### Announcements
102
102
 
103
- If you want to receive release or survey announcements, you should join our
103
+ To subscribe to new releases on GitHub, you can follow
104
+ [instructions from this blog](https://www.jessesquires.com/blog/2020/07/30/github-tip-watching-releases/).
105
+
106
+ To receive infrequent announcements and surveys from us, you can join our
104
107
  [mailing list](https://groups.google.com/forum/#!forum/shaka-player-users).
105
- The list is very low volume.
108
+ The list is very low volume, and can only be written to by us.
106
109
 
107
110
 
108
111
  #### Continue the Tutorials
@@ -777,8 +777,10 @@ shaka.extern.DashManifestConfiguration;
777
777
  * <i>Defaults to <code>'avc1.42E01E'</code>.</i>
778
778
  * @property {boolean} ignoreManifestProgramDateTime
779
779
  * If <code>true</code>, the HLS parser will ignore the
780
- * <code>EXT-X-PROGRAM-DATE-TIME</code> tags in the manifest.
781
- * Meant for tags that are incorrect or malformed.
780
+ * <code>EXT-X-PROGRAM-DATE-TIME</code> tags in the manifest and use media
781
+ * sequence numbers instead.
782
+ * Meant for streams where <code>EXT-X-PROGRAM-DATE-TIME</code> is incorrect
783
+ * or malformed.
782
784
  * <i>Defaults to <code>false</code>.</i>
783
785
  * @property {string} mediaPlaylistFullMimeType
784
786
  * A string containing a full mime type, including both the basic mime type
package/karma.conf.js CHANGED
@@ -160,10 +160,6 @@ module.exports = (config) => {
160
160
  'node_modules/es6-promise-polyfill/promise.js',
161
161
  // Babel polyfill, required for async/await
162
162
  'node_modules/@babel/polyfill/dist/polyfill.js',
163
- // TextDecoder polyfill, required for TextDecoder/TextEncoder on IE and
164
- // legacy Edge
165
- // eslint-disable-next-line max-len
166
- 'node_modules/fastestsmallesttextencoderdecoder/EncoderDecoderTogether.min.js',
167
163
 
168
164
  // muxjs module next
169
165
  'node_modules/mux.js/dist/mux.min.js',
@@ -306,13 +302,6 @@ module.exports = (config) => {
306
302
  // Force failure when running empty test-suites.
307
303
  failOnEmptyTestSuite: true,
308
304
 
309
- coverageReporter: {
310
- includeAllSources: true,
311
- reporters: [
312
- {type: 'text'},
313
- ],
314
- },
315
-
316
305
  specReporter: {
317
306
  suppressSkipped: true,
318
307
  showBrowser: true,
@@ -405,9 +394,12 @@ module.exports = (config) => {
405
394
 
406
395
  config.set({
407
396
  coverageReporter: {
397
+ includeAllSources: true,
408
398
  reporters: [
409
399
  {type: 'html', dir: 'coverage'},
410
400
  {type: 'cobertura', dir: 'coverage', file: 'coverage.xml'},
401
+ {type: 'json-summary', dir: 'coverage', file: 'coverage.json'},
402
+ {type: 'json', dir: 'coverage', file: 'coverage-details.json'},
411
403
  ],
412
404
  },
413
405
  });
@@ -427,7 +427,7 @@ shaka.ads.ClientSideAdManager = class {
427
427
  onAdComplete_(e) {
428
428
  this.onEvent_(new shaka.util.FakeEvent(shaka.ads.AdManager.AD_STOPPED,
429
429
  (new Map()).set('originalEvent', e)));
430
- if (this.ad_.isLinear()) {
430
+ if (this.ad_ && this.ad_.isLinear()) {
431
431
  this.adContainer_.removeAttribute('ad-active');
432
432
  this.video_.play();
433
433
  }
@@ -513,6 +513,11 @@ shaka.dash.DashParser = class {
513
513
  }
514
514
  presentationTimeline.setClockOffset(offset);
515
515
  }
516
+
517
+ // This is the first point where we have a meaningful presentation start
518
+ // time, and we need to tell PresentationTimeline that so that it can
519
+ // maintain consistency from here on.
520
+ presentationTimeline.lockStartTime();
516
521
  } else {
517
522
  // Just update the variants and text streams, which may change as periods
518
523
  // are added or removed.
@@ -121,13 +121,19 @@ shaka.hls.HlsParser = class {
121
121
  this.updatePlaylistDelay_ = 0;
122
122
 
123
123
  /**
124
- * A time offset to apply to EXT-X-PROGRAM-DATE-TIME values to normalize
125
- * them so that they start at 0. This is necessary because these times will
126
- * be used to set presentation times for segments.
127
- * null means we don't have enough data yet.
124
+ * If true, we have already calculated offsets to synchronize streams.
125
+ * Offsets are computed in syncStreams*_().
126
+ * @private {boolean}
127
+ */
128
+ this.streamsSynced_ = false;
129
+
130
+ /**
131
+ * The minimum sequence number for generated segments, when ignoring
132
+ * EXT-X-PROGRAM-DATE-TIME.
133
+ *
128
134
  * @private {?number}
129
135
  */
130
- this.syncTimeOffset_ = null;
136
+ this.minSequenceNumber_ = null;
131
137
 
132
138
  /**
133
139
  * This timer is used to trigger the start of a manifest update. A manifest
@@ -361,39 +367,155 @@ shaka.hls.HlsParser = class {
361
367
  }
362
368
 
363
369
  /**
364
- * If necessary, makes sure that sync times will be normalized to 0, so that
365
- * a stream does not start buffering at 50 years in because sync times are
366
- * measured in time since 1970.
370
+ * Align all streams by sequence number by dropping early segments. Then
371
+ * offset all streams to begin at presentation time 0.
367
372
  * @private
368
373
  */
369
- calculateSyncTimeOffset_() {
370
- if (this.syncTimeOffset_ != null) {
371
- // The offset was already calculated.
374
+ syncStreamsWithSequenceNumber_() {
375
+ if (this.streamsSynced_) {
372
376
  return;
373
377
  }
374
378
 
375
- const segments = new Set();
376
- let lowestSyncTime = Infinity;
379
+ // Sync using media sequence number. Find the highest starting sequence
380
+ // number among all streams. Later, we will drop any references to
381
+ // earlier segments in other streams, then offset everything back to 0.
382
+ let highestStartingSequenceNumber = -1;
383
+ const firstSequenceNumberMap = new Map();
384
+
377
385
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
378
386
  const segmentIndex = streamInfo.stream.segmentIndex;
379
387
  if (segmentIndex) {
380
- segmentIndex.forEachTopLevelReference((segment) => {
381
- if (segment.syncTime != null) {
382
- lowestSyncTime = Math.min(lowestSyncTime, segment.syncTime);
383
- segments.add(segment);
388
+ const segment0 = segmentIndex.earliestReference();
389
+ if (segment0) {
390
+ // This looks inefficient, but iteration order is insertion order.
391
+ // So the very first entry should be the one we want.
392
+ // We assert that this holds true so that we are alerted by debug
393
+ // builds and tests if it changes. We still do a loop, though, so
394
+ // that the code functions correctly in production no matter what.
395
+ if (goog.DEBUG) {
396
+ const firstSequenceStartTime =
397
+ streamInfo.mediaSequenceToStartTime.values().next().value;
398
+ goog.asserts.assert(
399
+ firstSequenceStartTime == segment0.startTime,
400
+ 'Sequence number map is not ordered as expected!');
401
+ }
402
+ for (const [sequence, start] of streamInfo.mediaSequenceToStartTime) {
403
+ if (start == segment0.startTime) {
404
+ firstSequenceNumberMap.set(streamInfo, sequence);
405
+
406
+ highestStartingSequenceNumber = Math.max(
407
+ highestStartingSequenceNumber, sequence);
408
+ break;
409
+ }
384
410
  }
385
- });
411
+ }
412
+ }
413
+ }
414
+
415
+ if (highestStartingSequenceNumber < 0) {
416
+ // Nothing to sync.
417
+ return;
418
+ }
419
+
420
+ // From now on, updates will ignore any references before this number.
421
+ this.minSequenceNumber_ = highestStartingSequenceNumber;
422
+
423
+ shaka.log.debug('Syncing HLS streams against base sequence number:',
424
+ this.minSequenceNumber_);
425
+
426
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
427
+ const segmentIndex = streamInfo.stream.segmentIndex;
428
+ if (segmentIndex) {
429
+ // Drop any earlier references.
430
+ const numSegmentsToDrop = this.minSequenceNumber_ -
431
+ firstSequenceNumberMap.get(streamInfo);
432
+ segmentIndex.dropFirstReferences(numSegmentsToDrop);
433
+
434
+ // Now adjust timestamps back to begin at 0.
435
+ const segmentN = segmentIndex.earliestReference();
436
+ if (segmentN) {
437
+ this.offsetStream_(streamInfo, -segmentN.startTime);
438
+ }
439
+ }
440
+ }
441
+
442
+ this.streamsSynced_ = true;
443
+ }
444
+
445
+ /**
446
+ * Synchronize streams by the EXT-X-PROGRAM-DATE-TIME tags attached to their
447
+ * segments. Also normalizes segment times so that the earliest segment in
448
+ * any stream is at time 0.
449
+ * @private
450
+ */
451
+ syncStreamsWithProgramDateTime_() {
452
+ if (this.streamsSynced_) {
453
+ return;
454
+ }
455
+
456
+ let lowestSyncTime = Infinity;
457
+
458
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
459
+ const segmentIndex = streamInfo.stream.segmentIndex;
460
+ if (segmentIndex) {
461
+ const segment0 = segmentIndex.earliestReference();
462
+ if (segment0 != null && segment0.syncTime != null) {
463
+ lowestSyncTime = Math.min(lowestSyncTime, segment0.syncTime);
464
+ }
386
465
  }
387
466
  }
388
- if (segments.size > 0) {
389
- this.syncTimeOffset_ = -lowestSyncTime;
390
- for (const segment of segments) {
391
- segment.syncTime += this.syncTimeOffset_;
392
- for (const partial of segment.partialReferences) {
393
- partial.syncTime += this.syncTimeOffset_;
467
+
468
+ if (lowestSyncTime == Infinity) {
469
+ // Nothing to sync.
470
+ return;
471
+ }
472
+
473
+ shaka.log.debug('Syncing HLS streams against base time:', lowestSyncTime);
474
+
475
+ for (const streamInfo of this.uriToStreamInfosMap_.values()) {
476
+ const segmentIndex = streamInfo.stream.segmentIndex;
477
+ if (segmentIndex != null) {
478
+ const segment0 = segmentIndex.earliestReference();
479
+ if (segment0.syncTime == null) {
480
+ shaka.log.alwaysError('Missing EXT-X-PROGRAM-DATE-TIME for stream',
481
+ streamInfo.verbatimMediaPlaylistUri,
482
+ 'Expect AV sync issues!');
483
+ } else {
484
+ // The first segment's target startTime should be based entirely on
485
+ // its syncTime. The rest of the stream will be based on that
486
+ // starting point. The earliest segment sync time from any stream
487
+ // will become presentation time 0. If two streams start e.g. 6
488
+ // seconds apart in syncTime, then their first segments will also
489
+ // start 6 seconds apart in presentation time.
490
+ const segment0TargetTime = segment0.syncTime - lowestSyncTime;
491
+ const streamOffset = segment0TargetTime - segment0.startTime;
492
+
493
+ this.offsetStream_(streamInfo, streamOffset);
394
494
  }
395
495
  }
396
496
  }
497
+
498
+ this.streamsSynced_ = true;
499
+ }
500
+
501
+ /**
502
+ * @param {!shaka.hls.HlsParser.StreamInfo} streamInfo
503
+ * @param {number} offset
504
+ * @private
505
+ */
506
+ offsetStream_(streamInfo, offset) {
507
+ streamInfo.stream.segmentIndex.offset(offset);
508
+
509
+ streamInfo.maxTimestamp += offset;
510
+ goog.asserts.assert(streamInfo.maxTimestamp >= 0,
511
+ 'Negative maxTimestamp after adjustment!');
512
+
513
+ for (const [key, value] of streamInfo.mediaSequenceToStartTime) {
514
+ streamInfo.mediaSequenceToStartTime.set(key, value + offset);
515
+ }
516
+
517
+ shaka.log.debug('Offset', offset, 'applied to',
518
+ streamInfo.verbatimMediaPlaylistUri);
397
519
  }
398
520
 
399
521
  /**
@@ -525,7 +647,11 @@ shaka.hls.HlsParser = class {
525
647
 
526
648
  // Now that we have generated all streams, we can determine the offset to
527
649
  // apply to sync times.
528
- this.calculateSyncTimeOffset_();
650
+ if (this.config_.hls.ignoreManifestProgramDateTime) {
651
+ this.syncStreamsWithSequenceNumber_();
652
+ } else {
653
+ this.syncStreamsWithProgramDateTime_();
654
+ }
529
655
 
530
656
  if (this.aesEncrypted_ && variants.length == 0) {
531
657
  // We do not support AES-128 encryption with HLS yet. Variants is null
@@ -540,15 +666,12 @@ shaka.hls.HlsParser = class {
540
666
 
541
667
  // Find the min and max timestamp of the earliest segment in all streams.
542
668
  // Find the minimum duration of all streams as well.
543
- let minFirstTimestamp = Infinity;
544
669
  let minDuration = Infinity;
545
-
546
670
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
547
- minFirstTimestamp =
548
- Math.min(minFirstTimestamp, streamInfo.minTimestamp);
549
671
  if (streamInfo.stream.type != 'text') {
550
- minDuration = Math.min(minDuration,
551
- streamInfo.maxTimestamp - streamInfo.minTimestamp);
672
+ // Since everything is already offset to 0 (either by sync or by being
673
+ // VOD), only maxTimestamp is necessary to compute the duration.
674
+ minDuration = Math.min(minDuration, streamInfo.maxTimestamp);
552
675
  }
553
676
  }
554
677
 
@@ -592,18 +715,11 @@ shaka.hls.HlsParser = class {
592
715
  segmentAvailabilityDuration);
593
716
  }
594
717
  } else {
595
- // For VOD/EVENT content, offset everything back to 0.
596
- // Use the minimum timestamp as the offset for all streams.
597
718
  // Use the minimum duration as the presentation duration.
598
719
  this.presentationTimeline_.setDuration(minDuration);
599
- // Use a negative offset to adjust towards 0.
600
- this.presentationTimeline_.offset(-minFirstTimestamp);
601
720
 
602
721
  for (const streamInfo of this.uriToStreamInfosMap_.values()) {
603
- // The segments were created with actual media times, rather than
604
- // presentation-aligned times, so offset them all now.
605
- streamInfo.stream.segmentIndex.offset(-minFirstTimestamp);
606
- // Finally, fit the segments to the playlist duration.
722
+ // Fit the segments to the playlist duration.
607
723
  streamInfo.stream.segmentIndex.fit(/* periodStart= */ 0, minDuration);
608
724
  }
609
725
  }
@@ -624,8 +740,14 @@ shaka.hls.HlsParser = class {
624
740
  for (const stream of streamsToNotify) {
625
741
  this.segmentsToNotifyByStream_.push(stream.segmentIndex.references);
626
742
  }
743
+
627
744
  this.notifySegments_();
628
745
 
746
+ // This is the first point where we have a meaningful presentation start
747
+ // time, and we need to tell PresentationTimeline that so that it can
748
+ // maintain consistency from here on.
749
+ this.presentationTimeline_.lockStartTime();
750
+
629
751
  // This asserts that the live edge is being calculated from segment times.
630
752
  // For VOD and event streams, this check should still pass.
631
753
  goog.asserts.assert(
@@ -811,12 +933,7 @@ shaka.hls.HlsParser = class {
811
933
  });
812
934
 
813
935
  // Create stream info for each audio / video media tag.
814
- // Wait for the first stream info created, so that the start time is fetched
815
- // and can be reused.
816
- if (mediaTags.length) {
817
- await this.createStreamInfoFromMediaTag_(mediaTags[0]);
818
- }
819
- const promises = mediaTags.slice(1).map((tag) => {
936
+ const promises = mediaTags.map((tag) => {
820
937
  return this.createStreamInfoFromMediaTag_(tag);
821
938
  });
822
939
  await Promise.all(promises);
@@ -1584,7 +1701,6 @@ shaka.hls.HlsParser = class {
1584
1701
  throw error;
1585
1702
  }
1586
1703
 
1587
- const minTimestamp = segments[0].startTime;
1588
1704
  const lastEndTime = segments[segments.length - 1].endTime;
1589
1705
  /** @type {!shaka.media.SegmentIndex} */
1590
1706
  const segmentIndex = new shaka.media.SegmentIndex(segments);
@@ -1642,7 +1758,6 @@ shaka.hls.HlsParser = class {
1642
1758
  stream,
1643
1759
  verbatimMediaPlaylistUri,
1644
1760
  absoluteMediaPlaylistUri,
1645
- minTimestamp,
1646
1761
  maxTimestamp: lastEndTime,
1647
1762
  mediaSequenceToStartTime,
1648
1763
  canSkipSegments,
@@ -1750,8 +1865,6 @@ shaka.hls.HlsParser = class {
1750
1865
  /* presentationStartTime= */ null, /* delay= */ 0);
1751
1866
  this.presentationTimeline_.setStatic(true);
1752
1867
  }
1753
-
1754
- this.notifySegments_();
1755
1868
  }
1756
1869
 
1757
1870
  /**
@@ -1854,15 +1967,9 @@ shaka.hls.HlsParser = class {
1854
1967
  const dateTimeTag =
1855
1968
  shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
1856
1969
  if (dateTimeTag && dateTimeTag.value) {
1857
- const time = shaka.util.XmlUtils.parseDate(dateTimeTag.value);
1858
- goog.asserts.assert(time != null,
1970
+ syncTime = shaka.util.XmlUtils.parseDate(dateTimeTag.value);
1971
+ goog.asserts.assert(syncTime != null,
1859
1972
  'EXT-X-PROGRAM-DATE-TIME format not valid');
1860
- // Sync time offset is null on the first go-through. This indicates that
1861
- // we have not yet seen every stream, and thus do not yet have enough
1862
- // information to determine how to normalize the sync times.
1863
- // For that first go-through, the sync time will be applied after the
1864
- // references are all created. Until then, just offset by 0.
1865
- syncTime = time + (this.syncTimeOffset_ || 0);
1866
1973
  }
1867
1974
  }
1868
1975
 
@@ -2096,16 +2203,13 @@ shaka.hls.HlsParser = class {
2096
2203
  firstStartTime = mediaSequenceToStartTime.get(position);
2097
2204
  }
2098
2205
 
2099
- const firstSegmentUri = hlsSegments[0].absoluteUri;
2100
- shaka.log.debug('First segment', firstSegmentUri.split('/').pop(),
2101
- 'starts at', firstStartTime);
2102
-
2103
2206
  /** @type {!Array.<!shaka.media.SegmentReference>} */
2104
2207
  const references = [];
2105
2208
 
2209
+ let previousReference = null;
2210
+
2106
2211
  for (let i = 0; i < hlsSegments.length; i++) {
2107
2212
  const item = hlsSegments[i];
2108
- const previousReference = references[references.length - 1];
2109
2213
  const startTime =
2110
2214
  (i == 0) ? firstStartTime : previousReference.endTime;
2111
2215
  position = mediaSequenceNumber + skippedSegments + i;
@@ -2135,9 +2239,17 @@ shaka.hls.HlsParser = class {
2135
2239
  variables,
2136
2240
  playlist.absoluteUri,
2137
2241
  type);
2242
+ previousReference = reference;
2138
2243
 
2139
2244
  if (reference) {
2140
- references.push(reference);
2245
+ if (this.config_.hls.ignoreManifestProgramDateTime &&
2246
+ this.minSequenceNumber_ != null &&
2247
+ position < this.minSequenceNumber_) {
2248
+ // This segment is ignored as part of our fallback synchronization
2249
+ // method.
2250
+ } else {
2251
+ references.push(reference);
2252
+ }
2141
2253
  }
2142
2254
  }
2143
2255
 
@@ -2267,12 +2379,16 @@ shaka.hls.HlsParser = class {
2267
2379
  const ContentType = shaka.util.ManifestParserUtils.ContentType;
2268
2380
  const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
2269
2381
 
2382
+ // If you wait long enough, requesting the first segment can fail
2383
+ // because it has fallen off the left edge of DVR, so to be safer,
2384
+ // let's request the middle segment.
2270
2385
  goog.asserts.assert(playlist.segments.length,
2271
2386
  'Playlist should have segments!');
2272
- const firstSegmentUri = this.variableSubstitution_(
2273
- playlist.segments[0].absoluteUri, variables);
2387
+ const middleSegmentIdx = Math.trunc((playlist.segments.length - 1) / 2);
2388
+ const middleSegmentUri = this.variableSubstitution_(
2389
+ playlist.segments[middleSegmentIdx].absoluteUri, variables);
2274
2390
 
2275
- const parsedUri = new goog.Uri(firstSegmentUri);
2391
+ const parsedUri = new goog.Uri(middleSegmentUri);
2276
2392
  const extension = parsedUri.getPath().split('.').pop();
2277
2393
  const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
2278
2394
 
@@ -2302,7 +2418,7 @@ shaka.hls.HlsParser = class {
2302
2418
  // If unable to guess mime type, request a segment and try getting it
2303
2419
  // from the response.
2304
2420
  const headRequest = shaka.net.NetworkingEngine.makeRequest(
2305
- [firstSegmentUri], this.config_.retryParameters);
2421
+ [middleSegmentUri], this.config_.retryParameters);
2306
2422
  headRequest.method = 'HEAD';
2307
2423
 
2308
2424
  const response = await this.makeNetworkRequest_(
@@ -2589,7 +2705,6 @@ shaka.hls.HlsParser = class {
2589
2705
  * stream: !shaka.extern.Stream,
2590
2706
  * verbatimMediaPlaylistUri: string,
2591
2707
  * absoluteMediaPlaylistUri: string,
2592
- * minTimestamp: number,
2593
2708
  * maxTimestamp: number,
2594
2709
  * mediaSequenceToStartTime: !Map.<number, number>,
2595
2710
  * canSkipSegments: boolean
@@ -2608,8 +2723,6 @@ shaka.hls.HlsParser = class {
2608
2723
  * @property {string} absoluteMediaPlaylistUri
2609
2724
  * The absolute media playlist URI, resolved relative to the master playlist
2610
2725
  * and updated to reflect any redirects.
2611
- * @property {number} minTimestamp
2612
- * The minimum timestamp found in the stream.
2613
2726
  * @property {number} maxTimestamp
2614
2727
  * The maximum timestamp found in the stream.
2615
2728
  * @property {!Map.<number, number>} mediaSequenceToStartTime
@@ -1548,7 +1548,7 @@ shaka.media.DrmEngine = class {
1548
1548
  if (shaka.media.DrmEngine.isPlayReadyKeySystem(
1549
1549
  this.currentDrmInfo_.keySystem) &&
1550
1550
  keyId.byteLength == 16 &&
1551
- shaka.util.Platform.isEdge()) {
1551
+ (shaka.util.Platform.isEdge() || shaka.util.Platform.isPS4())) {
1552
1552
  // Read out some fields in little-endian:
1553
1553
  const dataView = shaka.util.BufferUtils.toDataView(keyId);
1554
1554
  const part0 = dataView.getUint32(0, /* LE= */ true);