shaka-player 3.3.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build-and-test.yaml +0 -1
- package/.github/workflows/demo-version-index.yaml +8 -0
- package/.github/workflows/release-please.yaml +23 -2
- package/.github/workflows/selenium-lab-tests.yaml +0 -1
- package/.github/workflows/sync-labels.yaml +33 -0
- package/.github/workflows/update-issues.yaml +29 -0
- package/.github/workflows/validate-pr-title.yaml +35 -0
- package/AUTHORS +1 -0
- package/CHANGELOG.md +198 -32
- package/CONTRIBUTORS +1 -0
- package/README.md +10 -7
- package/app-engine/demo-version-index/README.md +3 -3
- package/app-engine/demo-version-index/app.yaml +27 -1
- package/app-engine/demo-version-index/generate.py +164 -0
- package/app-engine/demo-version-index/main.py +4 -155
- package/app-engine/demo-version-index/requirements.txt +0 -1
- package/build/README.md +2 -2
- package/build/compiler.py +1 -1
- package/build/generateTsDefs.py +0 -2
- package/build/shaka-lab.yaml +0 -2
- package/build/test.py +7 -1
- package/build/types/core +0 -1
- package/build/types/polyfill +0 -3
- package/demo/common/asset.js +17 -1
- package/demo/common/assets.js +29 -0
- package/demo/common/message_ids.js +6 -3
- package/demo/config.js +10 -11
- package/demo/custom.js +39 -0
- package/demo/demo_utils.js +0 -1
- package/demo/locales/en.json +6 -3
- package/demo/locales/source.json +22 -10
- package/demo/main.js +9 -6
- package/demo/search.js +2 -1
- package/dist/controls.css +1 -1
- package/dist/demo.compiled.debug.js +71 -64
- package/dist/demo.compiled.debug.map +3 -3
- package/dist/demo.compiled.js +71 -64
- package/dist/demo.compiled.map +3 -3
- package/dist/demo.css +1 -1
- package/dist/demo.css.map +1 -1
- package/dist/deps.js +16 -20
- package/dist/locales.js +31 -29
- package/dist/receiver.compiled.debug.js +32 -28
- package/dist/receiver.compiled.debug.map +3 -3
- package/dist/receiver.compiled.js +32 -28
- package/dist/receiver.compiled.map +3 -3
- package/dist/shaka-player.compiled.d.ts +78 -83
- package/dist/shaka-player.compiled.debug.d.ts +78 -83
- package/dist/shaka-player.compiled.debug.externs.js +126 -126
- package/dist/shaka-player.compiled.debug.js +426 -483
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +126 -126
- package/dist/shaka-player.compiled.js +838 -856
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +80 -83
- package/dist/shaka-player.ui.debug.d.ts +80 -83
- package/dist/shaka-player.ui.debug.externs.js +134 -126
- package/dist/shaka-player.ui.debug.js +488 -543
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +134 -126
- package/dist/shaka-player.ui.js +980 -994
- package/dist/shaka-player.ui.map +4 -4
- package/docs/tutorials/config.md +0 -2
- package/docs/tutorials/drm-config.md +5 -0
- package/docs/tutorials/fairplay.md +7 -22
- package/docs/tutorials/faq.md +0 -11
- package/docs/tutorials/index.json +2 -8
- package/docs/tutorials/plugins.md +1 -3
- package/docs/tutorials/upgrade.md +83 -11
- package/externs/polyfill.js +15 -0
- package/externs/shaka/manifest.js +5 -1
- package/externs/shaka/net.js +12 -0
- package/externs/shaka/offline.js +5 -1
- package/externs/shaka/player.js +39 -32
- package/externs/shaka/text.js +12 -12
- package/externs/sourcebuffer.js +15 -0
- package/lib/ads/client_side_ad.js +13 -2
- package/lib/ads/client_side_ad_manager.js +22 -4
- package/lib/cast/cast_utils.js +0 -1
- package/lib/dash/content_protection.js +85 -5
- package/lib/dash/dash_parser.js +1 -1
- package/lib/hls/hls_parser.js +378 -743
- package/lib/hls/manifest_text_parser.js +2 -3
- package/lib/media/drm_engine.js +102 -53
- package/lib/media/gap_jumping_controller.js +10 -45
- package/lib/media/media_source_engine.js +87 -15
- package/lib/media/playhead.js +2 -5
- package/lib/media/presentation_timeline.js +1 -2
- package/lib/media/segment_index.js +13 -46
- package/lib/media/segment_reference.js +47 -1
- package/lib/media/streaming_engine.js +29 -11
- package/lib/media/time_ranges_utils.js +4 -9
- package/lib/net/networking_engine.js +3 -0
- package/lib/offline/indexeddb/v1_storage_cell.js +1 -0
- package/lib/offline/indexeddb/v2_storage_cell.js +1 -0
- package/lib/offline/manifest_converter.js +1 -0
- package/lib/offline/storage.js +100 -132
- package/lib/player.js +17 -201
- package/lib/polyfill/media_capabilities.js +12 -6
- package/lib/polyfill/mediasource.js +0 -28
- package/lib/polyfill/patchedmediakeys_nop.js +2 -0
- package/lib/polyfill/patchedmediakeys_webkit.js +2 -0
- package/lib/text/cue.js +0 -6
- package/lib/text/lrc_text_parser.js +8 -0
- package/lib/text/mp4_ttml_parser.js +8 -0
- package/lib/text/mp4_vtt_parser.js +8 -0
- package/lib/text/sbv_text_parser.js +8 -0
- package/lib/text/simple_text_displayer.js +1 -7
- package/lib/text/srt_text_parser.js +8 -0
- package/lib/text/ssa_text_parser.js +8 -0
- package/lib/text/text_engine.js +19 -3
- package/lib/text/ttml_text_parser.js +8 -0
- package/lib/text/ui_text_displayer.js +1 -7
- package/lib/text/vtt_text_parser.js +24 -2
- package/lib/text/web_vtt_generator.js +1 -7
- package/lib/util/error.js +9 -13
- package/lib/util/fairplay_utils.js +109 -0
- package/lib/util/functional.js +0 -42
- package/lib/util/mime_utils.js +2 -0
- package/lib/util/platform.js +45 -0
- package/lib/util/player_configuration.js +12 -6
- package/lib/util/pssh.js +23 -4
- package/lib/util/stream_utils.js +92 -22
- package/lib/util/uint8array_utils.js +0 -16
- package/lib/util/xml_utils.js +1 -1
- package/package.json +1 -1
- package/roadmap.md +19 -19
- package/shaka-player.uncompiled.js +0 -3
- package/test/cast/cast_utils_unit.js +1 -0
- package/test/dash/dash_parser_content_protection_unit.js +44 -0
- package/test/dash/dash_parser_manifest_unit.js +0 -24
- package/test/dash/dash_parser_segment_template_unit.js +3 -4
- package/test/demo/demo_unit.js +2 -1
- package/test/hls/hls_live_unit.js +99 -220
- package/test/hls/hls_parser_unit.js +515 -432
- package/test/media/drm_engine_unit.js +86 -120
- package/test/media/media_source_engine_integration.js +26 -3
- package/test/media/media_source_engine_unit.js +7 -6
- package/test/media/playhead_unit.js +27 -191
- package/test/media/segment_index_unit.js +15 -25
- package/test/media/streaming_engine_integration.js +4 -39
- package/test/media/streaming_engine_unit.js +3 -2
- package/test/media/time_ranges_utils_unit.js +0 -5
- package/test/offline/manifest_convert_unit.js +34 -4
- package/test/offline/offline_manifest_parser_unit.js +1 -0
- package/test/offline/storage_integration.js +5 -4
- package/test/player_external.js +0 -6
- package/test/player_integration.js +16 -0
- package/test/player_unit.js +26 -0
- package/test/test/util/fake_media_source_engine.js +11 -4
- package/test/test/util/manifest_generator.js +3 -1
- package/test/test/util/manifest_parser_util.js +4 -1
- package/test/test/util/offline_utils.js +1 -0
- package/test/test/util/streaming_engine_util.js +1 -0
- package/test/test/util/test_scheme.js +24 -1
- package/test/test/util/util.js +4 -4
- package/test/text/cue_integration.js +2 -2
- package/test/text/lrc_text_parser_unit.js +6 -7
- package/test/text/mp4_ttml_parser_unit.js +6 -4
- package/test/text/mp4_vtt_parser_unit.js +6 -5
- package/test/text/sbv_text_parser_unit.js +5 -6
- package/test/text/srt_text_parser_unit.js +5 -6
- package/test/text/ssa_text_parser_unit.js +17 -29
- package/test/text/text_engine_unit.js +45 -5
- package/test/text/ttml_text_parser_unit.js +77 -77
- package/test/text/vtt_text_parser_unit.js +55 -57
- package/test/ui/ui_integration.js +0 -16
- package/test/ui/ui_unit.js +27 -0
- package/test/util/cmcd_manager_unit.js +3 -0
- package/test/util/stream_utils_unit.js +23 -7
- package/ui/controls.js +65 -40
- package/ui/fullscreen_button.js +26 -6
- package/ui/less/containers.less +3 -3
- package/ui/locales/en.json +1 -0
- package/ui/locales/source.json +5 -0
- package/ui/resolution_selection.js +31 -33
- package/ui/ui.js +0 -17
- package/docs/tutorials/upgrade-v2-5.md +0 -441
- package/docs/tutorials/upgrade-v3.md +0 -38
- package/lib/polyfill/mathround.js +0 -49
- package/lib/polyfill/patchedmediakeys_apple.js +0 -744
- package/lib/polyfill/patchedmediakeys_ms.js +0 -698
- package/lib/util/mutex.js +0 -52
- package/test/util/functional_unit.js +0 -82
package/lib/hls/hls_parser.js
CHANGED
|
@@ -18,6 +18,7 @@ goog.require('shaka.log');
|
|
|
18
18
|
goog.require('shaka.media.DrmEngine');
|
|
19
19
|
goog.require('shaka.media.InitSegmentReference');
|
|
20
20
|
goog.require('shaka.media.ManifestParser');
|
|
21
|
+
goog.require('shaka.media.MediaSourceEngine');
|
|
21
22
|
goog.require('shaka.media.PresentationTimeline');
|
|
22
23
|
goog.require('shaka.media.SegmentIndex');
|
|
23
24
|
goog.require('shaka.media.SegmentReference');
|
|
@@ -26,19 +27,17 @@ goog.require('shaka.net.NetworkingEngine');
|
|
|
26
27
|
goog.require('shaka.util.ArrayUtils');
|
|
27
28
|
goog.require('shaka.util.BufferUtils');
|
|
28
29
|
goog.require('shaka.util.CmcdManager');
|
|
29
|
-
goog.require('shaka.util.DataViewReader');
|
|
30
30
|
goog.require('shaka.util.Error');
|
|
31
31
|
goog.require('shaka.util.FakeEvent');
|
|
32
32
|
goog.require('shaka.util.Functional');
|
|
33
33
|
goog.require('shaka.util.LanguageUtils');
|
|
34
34
|
goog.require('shaka.util.ManifestParserUtils');
|
|
35
35
|
goog.require('shaka.util.MimeUtils');
|
|
36
|
-
goog.require('shaka.util.Mp4Parser');
|
|
37
|
-
goog.require('shaka.util.Mp4BoxParsers');
|
|
38
|
-
goog.require('shaka.util.Networking');
|
|
39
36
|
goog.require('shaka.util.OperationManager');
|
|
40
37
|
goog.require('shaka.util.Pssh');
|
|
41
38
|
goog.require('shaka.util.Timer');
|
|
39
|
+
goog.require('shaka.util.Platform');
|
|
40
|
+
goog.require('shaka.util.XmlUtils');
|
|
42
41
|
goog.requireType('shaka.hls.Segment');
|
|
43
42
|
|
|
44
43
|
|
|
@@ -95,9 +94,6 @@ shaka.hls.HlsParser = class {
|
|
|
95
94
|
* createStreamInfoFromMediaTag_, createStreamInfoFromImageTag_ and
|
|
96
95
|
* createStreamInfoFromVariantTag_.
|
|
97
96
|
*
|
|
98
|
-
* During parsing of updates, used by getStartTime_ to determine the start
|
|
99
|
-
* time of the first segment from existing segment references.
|
|
100
|
-
*
|
|
101
97
|
* @private {!Map.<string, shaka.hls.HlsParser.StreamInfo>}
|
|
102
98
|
*/
|
|
103
99
|
this.uriToStreamInfosMap_ = new Map();
|
|
@@ -124,6 +120,15 @@ shaka.hls.HlsParser = class {
|
|
|
124
120
|
*/
|
|
125
121
|
this.updatePlaylistDelay_ = 0;
|
|
126
122
|
|
|
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.
|
|
128
|
+
* @private {?number}
|
|
129
|
+
*/
|
|
130
|
+
this.syncTimeOffset_ = null;
|
|
131
|
+
|
|
127
132
|
/**
|
|
128
133
|
* This timer is used to trigger the start of a manifest update. A manifest
|
|
129
134
|
* update is async. Once the update is finished, the timer will be restarted
|
|
@@ -175,9 +180,6 @@ shaka.hls.HlsParser = class {
|
|
|
175
180
|
/** @private {Map.<string, string>} */
|
|
176
181
|
this.groupIdToCodecsMap_ = new Map();
|
|
177
182
|
|
|
178
|
-
/** @private {?number} */
|
|
179
|
-
this.playlistStartTime_ = null;
|
|
180
|
-
|
|
181
183
|
/** A cache mapping EXT-X-MAP tag info to the InitSegmentReference created
|
|
182
184
|
* from the tag.
|
|
183
185
|
* The key is a string combining the EXT-X-MAP tag's absolute uri, and
|
|
@@ -185,15 +187,6 @@ shaka.hls.HlsParser = class {
|
|
|
185
187
|
* {!Map.<string, !shaka.media.InitSegmentReference>} */
|
|
186
188
|
this.mapTagToInitSegmentRefMap_ = new Map();
|
|
187
189
|
|
|
188
|
-
/**
|
|
189
|
-
* A cache mapping a discontinuity sequence number of a segment with
|
|
190
|
-
* EXT-X-DISCONTINUITY tag into its timestamp offset.
|
|
191
|
-
* Key: the discontinuity sequence number of a segment
|
|
192
|
-
* Value: the segment reference's timestamp offset.
|
|
193
|
-
* {!Map.<number, number>}
|
|
194
|
-
*/
|
|
195
|
-
this.discontinuityToTso_ = new Map();
|
|
196
|
-
|
|
197
190
|
/** @private {boolean} */
|
|
198
191
|
this.lowLatencyMode_ = false;
|
|
199
192
|
}
|
|
@@ -222,7 +215,7 @@ shaka.hls.HlsParser = class {
|
|
|
222
215
|
this.masterPlaylistUri_ = response.uri;
|
|
223
216
|
|
|
224
217
|
goog.asserts.assert(response.data, 'Response data should be non-null!');
|
|
225
|
-
await this.parseManifest_(response.data);
|
|
218
|
+
await this.parseManifest_(response.data, uri);
|
|
226
219
|
|
|
227
220
|
// Start the update timer if we want updates.
|
|
228
221
|
const delay = this.updatePlaylistDelay_;
|
|
@@ -277,8 +270,6 @@ shaka.hls.HlsParser = class {
|
|
|
277
270
|
|
|
278
271
|
/** @type {!Array.<!Promise>} */
|
|
279
272
|
const updates = [];
|
|
280
|
-
// Reset the start time for the new media playlist.
|
|
281
|
-
this.playlistStartTime_ = null;
|
|
282
273
|
const streamInfos = Array.from(this.uriToStreamInfosMap_.values());
|
|
283
274
|
// Wait for the first stream info created, so that the start time is fetched
|
|
284
275
|
// and can be reused.
|
|
@@ -329,11 +320,10 @@ shaka.hls.HlsParser = class {
|
|
|
329
320
|
|
|
330
321
|
const stream = streamInfo.stream;
|
|
331
322
|
|
|
332
|
-
const segments =
|
|
323
|
+
const segments = this.createSegments_(
|
|
333
324
|
streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
|
|
334
325
|
stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables,
|
|
335
|
-
|
|
336
|
-
stream.bandwidth);
|
|
326
|
+
stream.codecs);
|
|
337
327
|
|
|
338
328
|
stream.segmentIndex.mergeAndEvict(
|
|
339
329
|
segments, this.presentationTimeline_.getSegmentAvailabilityStart());
|
|
@@ -370,14 +360,51 @@ shaka.hls.HlsParser = class {
|
|
|
370
360
|
// No-op
|
|
371
361
|
}
|
|
372
362
|
|
|
363
|
+
/**
|
|
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.
|
|
367
|
+
* @private
|
|
368
|
+
*/
|
|
369
|
+
calculateSyncTimeOffset_() {
|
|
370
|
+
if (this.syncTimeOffset_ != null) {
|
|
371
|
+
// The offset was already calculated.
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const segments = new Set();
|
|
376
|
+
let lowestSyncTime = Infinity;
|
|
377
|
+
for (const streamInfo of this.uriToStreamInfosMap_.values()) {
|
|
378
|
+
const segmentIndex = streamInfo.stream.segmentIndex;
|
|
379
|
+
if (segmentIndex) {
|
|
380
|
+
segmentIndex.forEachTopLevelReference((segment) => {
|
|
381
|
+
if (segment.syncTime != null) {
|
|
382
|
+
lowestSyncTime = Math.min(lowestSyncTime, segment.syncTime);
|
|
383
|
+
segments.add(segment);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
}
|
|
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_;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
373
399
|
/**
|
|
374
400
|
* Parses the manifest.
|
|
375
401
|
*
|
|
376
402
|
* @param {BufferSource} data
|
|
403
|
+
* @param {string} uri
|
|
377
404
|
* @return {!Promise}
|
|
378
405
|
* @private
|
|
379
406
|
*/
|
|
380
|
-
async parseManifest_(data) {
|
|
407
|
+
async parseManifest_(data, uri) {
|
|
381
408
|
const Utils = shaka.hls.Utils;
|
|
382
409
|
|
|
383
410
|
goog.asserts.assert(this.masterPlaylistUri_,
|
|
@@ -386,63 +413,106 @@ shaka.hls.HlsParser = class {
|
|
|
386
413
|
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
387
414
|
data, this.masterPlaylistUri_);
|
|
388
415
|
|
|
389
|
-
// We don't support directly providing a Media Playlist.
|
|
390
|
-
// See the error code for details.
|
|
391
|
-
if (playlist.type != shaka.hls.PlaylistType.MASTER) {
|
|
392
|
-
throw new shaka.util.Error(
|
|
393
|
-
shaka.util.Error.Severity.CRITICAL,
|
|
394
|
-
shaka.util.Error.Category.MANIFEST,
|
|
395
|
-
shaka.util.Error.Code.HLS_MASTER_PLAYLIST_NOT_PROVIDED);
|
|
396
|
-
}
|
|
397
|
-
|
|
398
416
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
399
417
|
const variablesTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-DEFINE');
|
|
400
418
|
|
|
401
419
|
this.parseMasterVariables_(variablesTags);
|
|
402
420
|
|
|
403
|
-
/** @type {!Array.<!shaka.
|
|
404
|
-
|
|
405
|
-
/** @type {!Array.<!shaka.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
421
|
+
/** @type {!Array.<!shaka.extern.Variant>} */
|
|
422
|
+
let variants = [];
|
|
423
|
+
/** @type {!Array.<!shaka.extern.Stream>} */
|
|
424
|
+
let textStreams = [];
|
|
425
|
+
/** @type {!Array.<!shaka.extern.Stream>} */
|
|
426
|
+
let imageStreams = [];
|
|
427
|
+
|
|
428
|
+
// Parsing a media playlist results in a single-variant stream.
|
|
429
|
+
if (playlist.type == shaka.hls.PlaylistType.MEDIA) {
|
|
430
|
+
// Get necessary info for this stream, from the config. These are things
|
|
431
|
+
// we would normally find from the master playlist (e.g. from values on
|
|
432
|
+
// EXT-X-MEDIA tags).
|
|
433
|
+
const fullMimeType = this.config_.hls.mediaPlaylistFullMimeType;
|
|
434
|
+
const mimeType = shaka.util.MimeUtils.getBasicType(fullMimeType);
|
|
435
|
+
const type = mimeType.split('/')[0];
|
|
436
|
+
const codecs = shaka.util.MimeUtils.getCodecs(fullMimeType);
|
|
437
|
+
|
|
438
|
+
// Some values we cannot figure out, and aren't important enough to ask
|
|
439
|
+
// the user to provide through config values. A lot of these are only
|
|
440
|
+
// relevant to ABR, which isn't necessary if there's only one variant.
|
|
441
|
+
// So these unknowns should be set to false or null, largely.
|
|
442
|
+
const language = '';
|
|
443
|
+
const channelsCount = null;
|
|
444
|
+
const spatialAudio = false;
|
|
445
|
+
const characteristics = null;
|
|
446
|
+
const closedCaptions = new Map();
|
|
447
|
+
const forced = false; // Only relevant for text.
|
|
448
|
+
const primary = true; // This is the only stream!
|
|
449
|
+
const name = 'Media Playlist';
|
|
450
|
+
|
|
451
|
+
// Make the stream info, with those values.
|
|
452
|
+
const streamInfo = await this.convertParsedPlaylistIntoStreamInfo_(
|
|
453
|
+
playlist, uri, uri, codecs, type, language, primary, name,
|
|
454
|
+
channelsCount, closedCaptions, characteristics, forced, spatialAudio,
|
|
455
|
+
mimeType);
|
|
456
|
+
goog.asserts.assert(streamInfo != null, 'StreamInfo is null!');
|
|
457
|
+
this.uriToStreamInfosMap_.set(uri, streamInfo);
|
|
458
|
+
|
|
459
|
+
// Wrap the stream from that stream info with a variant.
|
|
460
|
+
variants.push({
|
|
461
|
+
id: 0,
|
|
462
|
+
language: 'und',
|
|
463
|
+
primary: true,
|
|
464
|
+
audio: type == 'audio' ? streamInfo.stream : null,
|
|
465
|
+
video: type == 'video' ? streamInfo.stream : null,
|
|
466
|
+
bandwidth: 0,
|
|
467
|
+
allowedByApplication: true,
|
|
468
|
+
allowedByKeySystem: true,
|
|
469
|
+
decodingInfos: [],
|
|
470
|
+
});
|
|
471
|
+
} else {
|
|
472
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
473
|
+
const mediaTags = Utils.filterTagsByName(playlist.tags, 'EXT-X-MEDIA');
|
|
474
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
475
|
+
const variantTags = Utils.filterTagsByName(
|
|
476
|
+
playlist.tags, 'EXT-X-STREAM-INF');
|
|
477
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
478
|
+
const imageTags = Utils.filterTagsByName(
|
|
479
|
+
playlist.tags, 'EXT-X-IMAGE-STREAM-INF');
|
|
480
|
+
|
|
481
|
+
this.parseCodecs_(variantTags);
|
|
482
|
+
|
|
483
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
484
|
+
const sesionDataTags =
|
|
485
|
+
Utils.filterTagsByName(playlist.tags, 'EXT-X-SESSION-DATA');
|
|
486
|
+
for (const tag of sesionDataTags) {
|
|
487
|
+
const id = tag.getAttributeValue('DATA-ID');
|
|
488
|
+
const uri = tag.getAttributeValue('URI');
|
|
489
|
+
const language = tag.getAttributeValue('LANGUAGE');
|
|
490
|
+
const value = tag.getAttributeValue('VALUE');
|
|
491
|
+
const data = (new Map()).set('id', id);
|
|
492
|
+
if (uri) {
|
|
493
|
+
data.set('uri', shaka.hls.Utils.constructAbsoluteUri(
|
|
494
|
+
this.masterPlaylistUri_, uri));
|
|
495
|
+
}
|
|
496
|
+
if (language) {
|
|
497
|
+
data.set('language', language);
|
|
498
|
+
}
|
|
499
|
+
if (value) {
|
|
500
|
+
data.set('value', value);
|
|
501
|
+
}
|
|
502
|
+
const event = new shaka.util.FakeEvent('sessiondata', data);
|
|
503
|
+
if (this.playerInterface_) {
|
|
504
|
+
this.playerInterface_.onEvent(event);
|
|
505
|
+
}
|
|
436
506
|
}
|
|
437
|
-
}
|
|
438
507
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
508
|
+
// Parse audio and video media tags first, so that we can extract segment
|
|
509
|
+
// start time from audio/video streams and reuse for text streams.
|
|
510
|
+
await this.createStreamInfosFromMediaTags_(mediaTags);
|
|
511
|
+
this.parseClosedCaptions_(mediaTags);
|
|
512
|
+
variants = await this.createVariantsForTags_(variantTags);
|
|
513
|
+
textStreams = await this.parseTexts_(mediaTags);
|
|
514
|
+
imageStreams = await this.parseImages_(imageTags);
|
|
515
|
+
}
|
|
446
516
|
|
|
447
517
|
// Make sure that the parser has not been destroyed.
|
|
448
518
|
if (!this.playerInterface_) {
|
|
@@ -452,6 +522,10 @@ shaka.hls.HlsParser = class {
|
|
|
452
522
|
shaka.util.Error.Code.OPERATION_ABORTED);
|
|
453
523
|
}
|
|
454
524
|
|
|
525
|
+
// Now that we have generated all streams, we can determine the offset to
|
|
526
|
+
// apply to sync times.
|
|
527
|
+
this.calculateSyncTimeOffset_();
|
|
528
|
+
|
|
455
529
|
if (this.aesEncrypted_ && variants.length == 0) {
|
|
456
530
|
// We do not support AES-128 encryption with HLS yet. Variants is null
|
|
457
531
|
// when the playlist is encrypted with AES-128.
|
|
@@ -564,6 +638,7 @@ shaka.hls.HlsParser = class {
|
|
|
564
638
|
imageStreams,
|
|
565
639
|
offlineSessionIds: [],
|
|
566
640
|
minBufferTime: 0,
|
|
641
|
+
sequenceMode: true,
|
|
567
642
|
};
|
|
568
643
|
this.playerInterface_.makeTextStreamsForClosedCaptions(this.manifest_);
|
|
569
644
|
}
|
|
@@ -764,7 +839,7 @@ shaka.hls.HlsParser = class {
|
|
|
764
839
|
const videoRange = tag.getAttributeValue('VIDEO-RANGE');
|
|
765
840
|
|
|
766
841
|
const streamInfos = await this.createStreamInfosForVariantTag_(tag,
|
|
767
|
-
resolution, frameRate
|
|
842
|
+
resolution, frameRate);
|
|
768
843
|
|
|
769
844
|
if (streamInfos) {
|
|
770
845
|
goog.asserts.assert(streamInfos.audio.length ||
|
|
@@ -798,11 +873,10 @@ shaka.hls.HlsParser = class {
|
|
|
798
873
|
* @param {!shaka.hls.Tag} tag
|
|
799
874
|
* @param {?string} resolution
|
|
800
875
|
* @param {?string} frameRate
|
|
801
|
-
* @param {number} bandwidth
|
|
802
876
|
* @return {!Promise.<?shaka.hls.HlsParser.StreamInfos>}
|
|
803
877
|
* @private
|
|
804
878
|
*/
|
|
805
|
-
async createStreamInfosForVariantTag_(tag, resolution, frameRate
|
|
879
|
+
async createStreamInfosForVariantTag_(tag, resolution, frameRate) {
|
|
806
880
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
807
881
|
/** @type {!Array.<string>} */
|
|
808
882
|
let allCodecs = this.getCodecsForVariantTag_(tag);
|
|
@@ -872,8 +946,7 @@ shaka.hls.HlsParser = class {
|
|
|
872
946
|
let streamInfo;
|
|
873
947
|
if (!ignoreStream) {
|
|
874
948
|
streamInfo =
|
|
875
|
-
await this.createStreamInfoFromVariantTag_(tag, allCodecs, type
|
|
876
|
-
bandwidth);
|
|
949
|
+
await this.createStreamInfoFromVariantTag_(tag, allCodecs, type);
|
|
877
950
|
}
|
|
878
951
|
if (streamInfo) {
|
|
879
952
|
res[streamInfo.stream.type] = [streamInfo];
|
|
@@ -1314,11 +1387,10 @@ shaka.hls.HlsParser = class {
|
|
|
1314
1387
|
* @param {!shaka.hls.Tag} tag
|
|
1315
1388
|
* @param {!Array.<string>} allCodecs
|
|
1316
1389
|
* @param {string} type
|
|
1317
|
-
* @param {number} bandwidth
|
|
1318
1390
|
* @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
|
|
1319
1391
|
* @private
|
|
1320
1392
|
*/
|
|
1321
|
-
async createStreamInfoFromVariantTag_(tag, allCodecs, type
|
|
1393
|
+
async createStreamInfoFromVariantTag_(tag, allCodecs, type) {
|
|
1322
1394
|
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
|
|
1323
1395
|
'Should only be called on variant tags!');
|
|
1324
1396
|
const verbatimMediaPlaylistUri = this.variableSubstitution_(
|
|
@@ -1334,7 +1406,7 @@ shaka.hls.HlsParser = class {
|
|
|
1334
1406
|
codecs, type, /* language= */ 'und', /* primary= */ false,
|
|
1335
1407
|
/* name= */ null, /* channelcount= */ null, closedCaptions,
|
|
1336
1408
|
/* characteristics= */ null, /* forced= */ false,
|
|
1337
|
-
/* spatialAudio= */ false
|
|
1409
|
+
/* spatialAudio= */ false);
|
|
1338
1410
|
if (streamInfo == null) {
|
|
1339
1411
|
return null;
|
|
1340
1412
|
}
|
|
@@ -1361,13 +1433,12 @@ shaka.hls.HlsParser = class {
|
|
|
1361
1433
|
* @param {?string} characteristics
|
|
1362
1434
|
* @param {boolean} forced
|
|
1363
1435
|
* @param {boolean} spatialAudio
|
|
1364
|
-
* @param {(number|undefined)} bandwidth
|
|
1365
1436
|
* @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
|
|
1366
1437
|
* @private
|
|
1367
1438
|
*/
|
|
1368
1439
|
async createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
|
|
1369
1440
|
primary, name, channelsCount, closedCaptions, characteristics, forced,
|
|
1370
|
-
spatialAudio
|
|
1441
|
+
spatialAudio) {
|
|
1371
1442
|
// TODO: Refactor, too many parameters
|
|
1372
1443
|
let absoluteMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
1373
1444
|
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
|
|
@@ -1381,6 +1452,34 @@ shaka.hls.HlsParser = class {
|
|
|
1381
1452
|
const playlist = this.manifestTextParser_.parsePlaylist(
|
|
1382
1453
|
response.data, absoluteMediaPlaylistUri);
|
|
1383
1454
|
|
|
1455
|
+
return this.convertParsedPlaylistIntoStreamInfo_(playlist,
|
|
1456
|
+
verbatimMediaPlaylistUri, absoluteMediaPlaylistUri, codecs, type,
|
|
1457
|
+
language, primary, name, channelsCount, closedCaptions, characteristics,
|
|
1458
|
+
forced, spatialAudio);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* @param {!shaka.hls.Playlist} playlist
|
|
1463
|
+
* @param {string} verbatimMediaPlaylistUri
|
|
1464
|
+
* @param {string} absoluteMediaPlaylistUri
|
|
1465
|
+
* @param {string} codecs
|
|
1466
|
+
* @param {string} type
|
|
1467
|
+
* @param {string} language
|
|
1468
|
+
* @param {boolean} primary
|
|
1469
|
+
* @param {?string} name
|
|
1470
|
+
* @param {?number} channelsCount
|
|
1471
|
+
* @param {Map.<string, string>} closedCaptions
|
|
1472
|
+
* @param {?string} characteristics
|
|
1473
|
+
* @param {boolean} forced
|
|
1474
|
+
* @param {boolean} spatialAudio
|
|
1475
|
+
* @param {(string|undefined)} mimeType
|
|
1476
|
+
* @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
|
|
1477
|
+
* @private
|
|
1478
|
+
*/
|
|
1479
|
+
async convertParsedPlaylistIntoStreamInfo_(playlist, verbatimMediaPlaylistUri,
|
|
1480
|
+
absoluteMediaPlaylistUri, codecs, type, language, primary, name,
|
|
1481
|
+
channelsCount, closedCaptions, characteristics, forced, spatialAudio,
|
|
1482
|
+
mimeType = undefined) {
|
|
1384
1483
|
if (playlist.type != shaka.hls.PlaylistType.MEDIA) {
|
|
1385
1484
|
// EXT-X-MEDIA and EXT-X-IMAGE-STREAM-INF tags should point to media
|
|
1386
1485
|
// playlists.
|
|
@@ -1390,6 +1489,22 @@ shaka.hls.HlsParser = class {
|
|
|
1390
1489
|
shaka.util.Error.Code.HLS_INVALID_PLAYLIST_HIERARCHY);
|
|
1391
1490
|
}
|
|
1392
1491
|
|
|
1492
|
+
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
1493
|
+
const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
|
|
1494
|
+
'EXT-X-DEFINE');
|
|
1495
|
+
|
|
1496
|
+
const mediaVariables = this.parseMediaVariables_(variablesTags);
|
|
1497
|
+
|
|
1498
|
+
goog.asserts.assert(playlist.segments != null,
|
|
1499
|
+
'Media playlist should have segments!');
|
|
1500
|
+
|
|
1501
|
+
this.determinePresentationType_(playlist);
|
|
1502
|
+
|
|
1503
|
+
if (!mimeType) {
|
|
1504
|
+
mimeType = await this.guessMimeType_(type, codecs, playlist,
|
|
1505
|
+
mediaVariables);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1393
1508
|
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
1394
1509
|
const drmTags = [];
|
|
1395
1510
|
if (playlist.segments) {
|
|
@@ -1424,7 +1539,7 @@ shaka.hls.HlsParser = class {
|
|
|
1424
1539
|
const drmParser =
|
|
1425
1540
|
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_[keyFormat];
|
|
1426
1541
|
|
|
1427
|
-
const drmInfo = drmParser ? drmParser(drmTag) : null;
|
|
1542
|
+
const drmInfo = drmParser ? drmParser(drmTag, mimeType) : null;
|
|
1428
1543
|
if (drmInfo) {
|
|
1429
1544
|
if (drmInfo.keyIds) {
|
|
1430
1545
|
for (const keyId of drmInfo.keyIds) {
|
|
@@ -1445,47 +1560,19 @@ shaka.hls.HlsParser = class {
|
|
|
1445
1560
|
shaka.util.Error.Code.HLS_KEYFORMATS_NOT_SUPPORTED);
|
|
1446
1561
|
}
|
|
1447
1562
|
|
|
1448
|
-
/** @type {!Array.<!shaka.hls.Tag>} */
|
|
1449
|
-
const variablesTags = shaka.hls.Utils.filterTagsByName(playlist.tags,
|
|
1450
|
-
'EXT-X-DEFINE');
|
|
1451
|
-
|
|
1452
|
-
const mediaVariables = this.parseMediaVariables_(variablesTags);
|
|
1453
|
-
|
|
1454
|
-
goog.asserts.assert(playlist.segments != null,
|
|
1455
|
-
'Media playlist should have segments!');
|
|
1456
|
-
|
|
1457
|
-
this.determinePresentationType_(playlist);
|
|
1458
|
-
|
|
1459
|
-
/** @type {string} */
|
|
1460
|
-
const mimeType = await this.guessMimeType_(type, codecs, playlist,
|
|
1461
|
-
mediaVariables);
|
|
1462
|
-
|
|
1463
1563
|
// MediaSource expects no codec strings combined with raw formats.
|
|
1464
|
-
|
|
1465
|
-
if (shaka.hls.HlsParser.RAW_FORMATS_.includes(mimeType)) {
|
|
1564
|
+
if (shaka.media.MediaSourceEngine.RAW_FORMATS.includes(mimeType)) {
|
|
1466
1565
|
codecs = '';
|
|
1467
1566
|
}
|
|
1468
1567
|
|
|
1469
1568
|
/** @type {!Map.<number, number>} */
|
|
1470
1569
|
const mediaSequenceToStartTime = new Map();
|
|
1471
1570
|
|
|
1472
|
-
/**
|
|
1473
|
-
* A map of a discontinuity sequence number, to the first segment's media
|
|
1474
|
-
* sequence number with the discontinuity sequence number.
|
|
1475
|
-
* Key: the discontinuity sequence number of a few segments
|
|
1476
|
-
* Value: the first segment's media sequence number of the segments with
|
|
1477
|
-
* this discontinuity sequence number.
|
|
1478
|
-
* Used to get the discontinuity sequence number with playlist delta
|
|
1479
|
-
* updates with lowLatencyMode enabled.
|
|
1480
|
-
* {!Map.<number, number>}
|
|
1481
|
-
*/
|
|
1482
|
-
const discontinuityToMediaSequence = new Map();
|
|
1483
|
-
|
|
1484
1571
|
let segments;
|
|
1485
1572
|
try {
|
|
1486
|
-
segments =
|
|
1573
|
+
segments = this.createSegments_(verbatimMediaPlaylistUri,
|
|
1487
1574
|
playlist, type, mimeType, mediaSequenceToStartTime, mediaVariables,
|
|
1488
|
-
|
|
1575
|
+
codecs);
|
|
1489
1576
|
} catch (error) {
|
|
1490
1577
|
if (error.code == shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM) {
|
|
1491
1578
|
shaka.log.alwaysWarn('Skipping unsupported HLS stream',
|
|
@@ -1557,7 +1644,6 @@ shaka.hls.HlsParser = class {
|
|
|
1557
1644
|
minTimestamp,
|
|
1558
1645
|
maxTimestamp: lastEndTime,
|
|
1559
1646
|
mediaSequenceToStartTime,
|
|
1560
|
-
discontinuityToMediaSequence,
|
|
1561
1647
|
canSkipSegments,
|
|
1562
1648
|
};
|
|
1563
1649
|
}
|
|
@@ -1736,16 +1822,15 @@ shaka.hls.HlsParser = class {
|
|
|
1736
1822
|
* @param {shaka.media.SegmentReference} previousReference
|
|
1737
1823
|
* @param {!shaka.hls.Segment} hlsSegment
|
|
1738
1824
|
* @param {number} startTime
|
|
1739
|
-
* @param {number} timestampOffset
|
|
1740
1825
|
* @param {!Map.<string, string>} variables
|
|
1741
1826
|
* @param {string} absoluteMediaPlaylistUri
|
|
1742
|
-
* @return {!shaka.media.SegmentReference}
|
|
1743
1827
|
* @param {string} type
|
|
1828
|
+
* @return {!shaka.media.SegmentReference}
|
|
1744
1829
|
* @private
|
|
1745
1830
|
*/
|
|
1746
1831
|
createSegmentReference_(
|
|
1747
1832
|
initSegmentReference, previousReference, hlsSegment, startTime,
|
|
1748
|
-
|
|
1833
|
+
variables, absoluteMediaPlaylistUri, type) {
|
|
1749
1834
|
const tags = hlsSegment.tags;
|
|
1750
1835
|
const absoluteSegmentUri = this.variableSubstitution_(
|
|
1751
1836
|
hlsSegment.absoluteUri, variables);
|
|
@@ -1756,6 +1841,35 @@ shaka.hls.HlsParser = class {
|
|
|
1756
1841
|
let startByte = 0;
|
|
1757
1842
|
let endByte = null;
|
|
1758
1843
|
|
|
1844
|
+
if (hlsSegment.partialSegments.length && !this.lowLatencyMode_) {
|
|
1845
|
+
shaka.log.alwaysWarn('Low-latency HLS live stream detected, but ' +
|
|
1846
|
+
'low-latency streaming mode is not enabled in Shaka ' +
|
|
1847
|
+
'Player. Set streaming.lowLatencyMode configuration to ' +
|
|
1848
|
+
'true, and see https://bit.ly/3clctcj for details.');
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
let syncTime = null;
|
|
1852
|
+
if (!this.config_.hls.ignoreManifestProgramDateTime) {
|
|
1853
|
+
const dateTimeTag =
|
|
1854
|
+
shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-PROGRAM-DATE-TIME');
|
|
1855
|
+
if (dateTimeTag && dateTimeTag.value) {
|
|
1856
|
+
const time = shaka.util.XmlUtils.parseDate(dateTimeTag.value);
|
|
1857
|
+
goog.asserts.assert(time != null,
|
|
1858
|
+
'EXT-X-PROGRAM-DATE-TIME format not valid');
|
|
1859
|
+
// Sync time offset is null on the first go-through. This indicates that
|
|
1860
|
+
// we have not yet seen every stream, and thus do not yet have enough
|
|
1861
|
+
// information to determine how to normalize the sync times.
|
|
1862
|
+
// For that first go-through, the sync time will be applied after the
|
|
1863
|
+
// references are all created. Until then, just offset by 0.
|
|
1864
|
+
syncTime = time + (this.syncTimeOffset_ || 0);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
let status = shaka.media.SegmentReference.Status.AVAILABLE;
|
|
1869
|
+
if (shaka.hls.Utils.getFirstTagWithName(tags, 'EXT-X-GAP')) {
|
|
1870
|
+
status = shaka.media.SegmentReference.Status.MISSING;
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1759
1873
|
// Create SegmentReferences for the partial segments.
|
|
1760
1874
|
const partialSegmentRefs = [];
|
|
1761
1875
|
if (this.lowLatencyMode_ && hlsSegment.partialSegments.length) {
|
|
@@ -1794,7 +1908,7 @@ shaka.hls.HlsParser = class {
|
|
|
1794
1908
|
pStartByte,
|
|
1795
1909
|
pEndByte,
|
|
1796
1910
|
initSegmentReference,
|
|
1797
|
-
timestampOffset,
|
|
1911
|
+
/* timestampOffset= */ 0, // This value is ignored in sequence mode.
|
|
1798
1912
|
/* appendWindowStart= */ 0,
|
|
1799
1913
|
/* appendWindowEnd= */ Infinity);
|
|
1800
1914
|
partialSegmentRefs.push(partial);
|
|
@@ -1862,12 +1976,14 @@ shaka.hls.HlsParser = class {
|
|
|
1862
1976
|
startByte,
|
|
1863
1977
|
endByte,
|
|
1864
1978
|
initSegmentReference,
|
|
1865
|
-
timestampOffset,
|
|
1979
|
+
/* timestampOffset= */ 0, // This value is ignored in sequence mode.
|
|
1866
1980
|
/* appendWindowStart= */ 0,
|
|
1867
1981
|
/* appendWindowEnd= */ Infinity,
|
|
1868
1982
|
partialSegmentRefs,
|
|
1869
1983
|
tilesLayout,
|
|
1870
1984
|
tileDuration,
|
|
1985
|
+
syncTime,
|
|
1986
|
+
status,
|
|
1871
1987
|
);
|
|
1872
1988
|
}
|
|
1873
1989
|
|
|
@@ -1921,15 +2037,12 @@ shaka.hls.HlsParser = class {
|
|
|
1921
2037
|
* @param {string} mimeType
|
|
1922
2038
|
* @param {!Map.<number, number>} mediaSequenceToStartTime
|
|
1923
2039
|
* @param {!Map.<string, string>} variables
|
|
1924
|
-
* @param {!Map.<number, number>} discontinuityToMediaSequence
|
|
1925
2040
|
* @param {string} codecs
|
|
1926
|
-
* @
|
|
1927
|
-
* @return {!Promise<!Array.<!shaka.media.SegmentReference>>}
|
|
2041
|
+
* @return {!Array.<!shaka.media.SegmentReference>}
|
|
1928
2042
|
* @private
|
|
1929
2043
|
*/
|
|
1930
|
-
|
|
1931
|
-
mediaSequenceToStartTime, variables,
|
|
1932
|
-
codecs, bandwidth) {
|
|
2044
|
+
createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
|
|
2045
|
+
mediaSequenceToStartTime, variables, codecs) {
|
|
1933
2046
|
/** @type {Array.<!shaka.hls.Segment>} */
|
|
1934
2047
|
const hlsSegments = playlist.segments;
|
|
1935
2048
|
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
|
|
@@ -1946,69 +2059,26 @@ shaka.hls.HlsParser = class {
|
|
|
1946
2059
|
const skippedSegments =
|
|
1947
2060
|
skipTag ? Number(skipTag.getAttributeValue('SKIPPED-SEGMENTS')) : 0;
|
|
1948
2061
|
let position = mediaSequenceNumber + skippedSegments;
|
|
1949
|
-
let firstStartTime;
|
|
2062
|
+
let firstStartTime = 0;
|
|
2063
|
+
|
|
1950
2064
|
// For live stream, use the cached value in the mediaSequenceToStartTime
|
|
1951
2065
|
// map if available.
|
|
1952
|
-
// Since createSegments_() is asynchronous and we are updating the streams
|
|
1953
|
-
// in parallel, the global playlistStartTime_ may get updated by other
|
|
1954
|
-
// playlist updates rather than the current one.
|
|
1955
|
-
|
|
1956
2066
|
if (this.isLive_() && mediaSequenceToStartTime.has(position)) {
|
|
1957
2067
|
firstStartTime = mediaSequenceToStartTime.get(position);
|
|
1958
|
-
} else {
|
|
1959
|
-
if (this.playlistStartTime_ == null) {
|
|
1960
|
-
// For VOD and EVENT playlists, all variants must start at the same
|
|
1961
|
-
// time, so we can fetch the start time once and reuse for the others.
|
|
1962
|
-
// This is not guaranteed when updating a LIVE stream. We assume the
|
|
1963
|
-
// first segment in each live playlist is no more than one segment out
|
|
1964
|
-
// of sync with the other playlists, so we can fetch the start time for
|
|
1965
|
-
// once.
|
|
1966
|
-
initSegmentRef = this.getInitSegmentReference_(
|
|
1967
|
-
playlist.absoluteUri, hlsSegments[0].tags, variables);
|
|
1968
|
-
goog.asserts.assert(
|
|
1969
|
-
type != shaka.util.ManifestParserUtils.ContentType.TEXT &&
|
|
1970
|
-
type != shaka.util.ManifestParserUtils.ContentType.IMAGE,
|
|
1971
|
-
'Should only get start time from audio or video streams');
|
|
1972
|
-
this.playlistStartTime_ = await this.getStartTime_(
|
|
1973
|
-
verbatimMediaPlaylistUri, initSegmentRef, mimeType,
|
|
1974
|
-
position, /* isDiscontinuity= */ false,
|
|
1975
|
-
hlsSegments[0], variables, type, codecs, bandwidth);
|
|
1976
|
-
}
|
|
1977
|
-
firstStartTime = this.playlistStartTime_;
|
|
1978
2068
|
}
|
|
1979
2069
|
|
|
1980
2070
|
const firstSegmentUri = hlsSegments[0].absoluteUri;
|
|
1981
2071
|
shaka.log.debug('First segment', firstSegmentUri.split('/').pop(),
|
|
1982
2072
|
'starts at', firstStartTime);
|
|
1983
2073
|
|
|
1984
|
-
let discontintuitySequenceNum = shaka.hls.Utils.getFirstTagWithNameAsNumber(
|
|
1985
|
-
playlist.tags, 'EXT-X-DISCONTINUITY-SEQUENCE');
|
|
1986
|
-
if (this.lowLatencyMode_) {
|
|
1987
|
-
if (!discontinuityToMediaSequence.has(discontintuitySequenceNum)) {
|
|
1988
|
-
discontinuityToMediaSequence.set(discontintuitySequenceNum, position);
|
|
1989
|
-
}
|
|
1990
|
-
if (skippedSegments) {
|
|
1991
|
-
// With delta updates, the DISCONTINUITY may be skipped. Check if
|
|
1992
|
-
// the discontintuity Sequence Number based on the media sequence
|
|
1993
|
-
// number.
|
|
1994
|
-
const disconMap = discontinuityToMediaSequence;
|
|
1995
|
-
while (disconMap.has(discontintuitySequenceNum + 1) &&
|
|
1996
|
-
disconMap.get(discontintuitySequenceNum + 1) < position) {
|
|
1997
|
-
discontintuitySequenceNum++;
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
let timestampOffset =
|
|
2002
|
-
this.discontinuityToTso_.get(discontintuitySequenceNum) || 0;
|
|
2003
|
-
|
|
2004
2074
|
/** @type {!Array.<!shaka.media.SegmentReference>} */
|
|
2005
2075
|
const references = [];
|
|
2006
2076
|
|
|
2007
2077
|
for (let i = 0; i < hlsSegments.length; i++) {
|
|
2008
2078
|
const item = hlsSegments[i];
|
|
2009
2079
|
const previousReference = references[references.length - 1];
|
|
2010
|
-
const startTime =
|
|
2011
|
-
|
|
2080
|
+
const startTime =
|
|
2081
|
+
(i == 0) ? firstStartTime : previousReference.endTime;
|
|
2012
2082
|
position = mediaSequenceNumber + skippedSegments + i;
|
|
2013
2083
|
|
|
2014
2084
|
mediaSequenceToStartTime.set(position, startTime);
|
|
@@ -2016,19 +2086,6 @@ shaka.hls.HlsParser = class {
|
|
|
2016
2086
|
initSegmentRef = this.getInitSegmentReference_(playlist.absoluteUri,
|
|
2017
2087
|
item.tags, variables);
|
|
2018
2088
|
|
|
2019
|
-
const discontintuityTag = shaka.hls.Utils.getFirstTagWithName(item.tags,
|
|
2020
|
-
'EXT-X-DISCONTINUITY');
|
|
2021
|
-
if (discontintuityTag) {
|
|
2022
|
-
discontintuitySequenceNum++;
|
|
2023
|
-
discontinuityToMediaSequence.set(discontintuitySequenceNum, position);
|
|
2024
|
-
|
|
2025
|
-
// eslint-disable-next-line no-await-in-loop
|
|
2026
|
-
timestampOffset = await this.getTimestampOffset_(
|
|
2027
|
-
discontintuitySequenceNum, verbatimMediaPlaylistUri, initSegmentRef,
|
|
2028
|
-
mimeType, position, item, variables, startTime, type, codecs,
|
|
2029
|
-
bandwidth);
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
2089
|
// If the stream is low latency and the user has not configured the
|
|
2033
2090
|
// lowLatencyMode, but if it has been configured to activate the
|
|
2034
2091
|
// lowLatencyMode if a stream of this type is detected, we automatically
|
|
@@ -2041,514 +2098,94 @@ shaka.hls.HlsParser = class {
|
|
|
2041
2098
|
}
|
|
2042
2099
|
}
|
|
2043
2100
|
|
|
2044
|
-
const
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
return references;
|
|
2068
|
-
}
|
|
2069
|
-
|
|
2070
|
-
/**
|
|
2071
|
-
* Gets the start time of the first segment of the playlist from existing
|
|
2072
|
-
* value (if possible) or by downloading it and parsing it otherwise.
|
|
2073
|
-
*
|
|
2074
|
-
* @param {number} discontintuitySequenceNum
|
|
2075
|
-
* @param {string} verbatimMediaPlaylistUri
|
|
2076
|
-
* @param {shaka.media.InitSegmentReference} initSegmentRef
|
|
2077
|
-
* @param {string} mimeType
|
|
2078
|
-
* @param {number} mediaSequenceNumber
|
|
2079
|
-
* @param {!shaka.hls.Segment} segment
|
|
2080
|
-
* @param {!Map.<string, string>} variables
|
|
2081
|
-
* @param {number} startTime
|
|
2082
|
-
* @param {string} type
|
|
2083
|
-
* @param {string} codecs
|
|
2084
|
-
* @param {number|undefined} bandwidth
|
|
2085
|
-
* @return {!Promise.<number>}
|
|
2086
|
-
* @throws {shaka.util.Error}
|
|
2087
|
-
* @private
|
|
2088
|
-
*/
|
|
2089
|
-
async getTimestampOffset_(discontintuitySequenceNum,
|
|
2090
|
-
verbatimMediaPlaylistUri, initSegmentRef,
|
|
2091
|
-
mimeType, mediaSequenceNumber, segment, variables, startTime, type,
|
|
2092
|
-
codecs, bandwidth) {
|
|
2093
|
-
let timestampOffset = 0;
|
|
2094
|
-
if (this.discontinuityToTso_.has(discontintuitySequenceNum)) {
|
|
2095
|
-
timestampOffset =
|
|
2096
|
-
this.discontinuityToTso_.get(discontintuitySequenceNum);
|
|
2097
|
-
} else {
|
|
2098
|
-
const mediaStartTime = await this.getStartTime_(
|
|
2099
|
-
verbatimMediaPlaylistUri, initSegmentRef, mimeType,
|
|
2100
|
-
mediaSequenceNumber, /* isDiscontinuity= */ true, segment,
|
|
2101
|
-
variables, type, codecs, bandwidth);
|
|
2102
|
-
timestampOffset = startTime - mediaStartTime;
|
|
2103
|
-
shaka.log.v1('Segment timestampOffset =', timestampOffset);
|
|
2104
|
-
this.discontinuityToTso_.set(
|
|
2105
|
-
discontintuitySequenceNum, timestampOffset);
|
|
2106
|
-
}
|
|
2107
|
-
return timestampOffset;
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
/**
|
|
2111
|
-
* Try to fetch the starting part of a segment, and fall back to a full
|
|
2112
|
-
* segment if we have to.
|
|
2113
|
-
*
|
|
2114
|
-
* @param {!shaka.media.AnySegmentReference} reference
|
|
2115
|
-
* @param {string} type
|
|
2116
|
-
* @param {string} mimeType
|
|
2117
|
-
* @param {string} codecs
|
|
2118
|
-
* @param {number|undefined} bandwidth
|
|
2119
|
-
* @return {!Promise.<shaka.extern.Response>}
|
|
2120
|
-
* @private
|
|
2121
|
-
*/
|
|
2122
|
-
async fetchStartOfSegment_(reference, type, mimeType, codecs, bandwidth) {
|
|
2123
|
-
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
2124
|
-
|
|
2125
|
-
// Create two requests:
|
|
2126
|
-
// 1. A partial request meant to fetch the smallest part of the segment
|
|
2127
|
-
// required to get the time stamp.
|
|
2128
|
-
// 2. A full request meant as a fallback for when the server does not
|
|
2129
|
-
// support partial requests.
|
|
2130
|
-
const fullRequest = shaka.util.Networking.createSegmentRequest(
|
|
2131
|
-
reference.getUris(),
|
|
2132
|
-
reference.startByte,
|
|
2133
|
-
reference.endByte,
|
|
2134
|
-
this.config_.retryParameters);
|
|
2135
|
-
|
|
2136
|
-
// We can only add partial CMCD data here because the stream
|
|
2137
|
-
// and manifest objects are still being created
|
|
2138
|
-
this.playerInterface_.modifySegmentRequest(
|
|
2139
|
-
fullRequest,
|
|
2140
|
-
{
|
|
2141
|
-
type: type,
|
|
2142
|
-
init: reference instanceof shaka.media.InitSegmentReference,
|
|
2143
|
-
duration: reference.endTime - reference.startTime,
|
|
2144
|
-
mimeType: mimeType,
|
|
2145
|
-
codecs: codecs,
|
|
2146
|
-
bandwidth: bandwidth,
|
|
2147
|
-
},
|
|
2148
|
-
);
|
|
2149
|
-
|
|
2150
|
-
if (this.config_.hls.useFullSegmentsForStartTime) {
|
|
2151
|
-
return this.makeNetworkRequest_(fullRequest, requestType);
|
|
2152
|
-
}
|
|
2153
|
-
|
|
2154
|
-
const partialRequest = shaka.util.Networking.createSegmentRequest(
|
|
2155
|
-
reference.getUris(),
|
|
2156
|
-
reference.startByte,
|
|
2157
|
-
reference.startByte + shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_ - 1,
|
|
2158
|
-
this.config_.retryParameters);
|
|
2159
|
-
|
|
2160
|
-
this.playerInterface_.modifySegmentRequest(
|
|
2161
|
-
partialRequest,
|
|
2162
|
-
{
|
|
2163
|
-
type: type,
|
|
2164
|
-
init: reference instanceof shaka.media.InitSegmentReference,
|
|
2165
|
-
duration: reference.endTime - reference.startTime,
|
|
2166
|
-
mimeType: mimeType,
|
|
2167
|
-
codecs: codecs,
|
|
2168
|
-
bandwidth: bandwidth,
|
|
2169
|
-
},
|
|
2170
|
-
);
|
|
2171
|
-
|
|
2172
|
-
// TODO(vaage): The need to do fall back requests is not likely to be unique
|
|
2173
|
-
// to here. It would be nice if the fallback(s) could be included into
|
|
2174
|
-
// the same abortable operation as the original request.
|
|
2175
|
-
//
|
|
2176
|
-
// What would need to change with networking engine to support requests
|
|
2177
|
-
// with fallback(s)?
|
|
2178
|
-
try {
|
|
2179
|
-
const response = await this.makeNetworkRequest_(
|
|
2180
|
-
partialRequest, requestType);
|
|
2181
|
-
|
|
2182
|
-
return response;
|
|
2183
|
-
} catch (e) {
|
|
2184
|
-
// If the networking operation was aborted, we don't want to treat it as
|
|
2185
|
-
// a request failure. We surface the error so that the OPERATION_ABORTED
|
|
2186
|
-
// error will be handled correctly.
|
|
2187
|
-
if (e.code == shaka.util.Error.Code.OPERATION_ABORTED) {
|
|
2188
|
-
throw e;
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
// The partial request may fail for a number of reasons.
|
|
2192
|
-
// Some servers do not support Range requests, and others do not support
|
|
2193
|
-
// the OPTIONS request which must be made before any cross-origin Range
|
|
2194
|
-
// request. Since this fallback is expensive, warn the app developer.
|
|
2195
|
-
shaka.log.alwaysWarn('Unable to fetch the starting part of HLS ' +
|
|
2196
|
-
'segment! Falling back to a full segment request, ' +
|
|
2197
|
-
'which is expensive! Your server should ' +
|
|
2198
|
-
'support Range requests and CORS preflights.',
|
|
2199
|
-
partialRequest.uris[0]);
|
|
2200
|
-
|
|
2201
|
-
const response = await this.makeNetworkRequest_(fullRequest, requestType);
|
|
2202
|
-
|
|
2203
|
-
return response;
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
/**
|
|
2208
|
-
* Gets the start time of a segment from the existing manifest (if possible)
|
|
2209
|
-
* or by downloading it and parsing it otherwise.
|
|
2210
|
-
*
|
|
2211
|
-
* @param {string} verbatimMediaPlaylistUri
|
|
2212
|
-
* @param {shaka.media.InitSegmentReference} initSegmentRef
|
|
2213
|
-
* @param {string} mimeType
|
|
2214
|
-
* @param {number} mediaSequenceNumber
|
|
2215
|
-
* @param {boolean} isDiscontinuity
|
|
2216
|
-
* @param {!shaka.hls.Segment} segment
|
|
2217
|
-
* @param {!Map.<string, string>} variables
|
|
2218
|
-
* @param {string} type
|
|
2219
|
-
* @param {string} codecs
|
|
2220
|
-
* @param {number|undefined} bandwidth
|
|
2221
|
-
* @return {!Promise.<number>}
|
|
2222
|
-
* @private
|
|
2223
|
-
*/
|
|
2224
|
-
async getStartTime_(
|
|
2225
|
-
verbatimMediaPlaylistUri, initSegmentRef, mimeType, mediaSequenceNumber,
|
|
2226
|
-
isDiscontinuity, segment, variables, type, codecs, bandwidth) {
|
|
2227
|
-
const segmentRef = this.createSegmentReference_(
|
|
2228
|
-
initSegmentRef,
|
|
2229
|
-
/* previousReference= */ null,
|
|
2230
|
-
segment,
|
|
2231
|
-
/* startTime= */ 0,
|
|
2232
|
-
/* timestampOffset= */ 0,
|
|
2233
|
-
variables,
|
|
2234
|
-
/* absoluteMediaPlaylistUri= */ '',
|
|
2235
|
-
type);
|
|
2236
|
-
// If we are updating the manifest, we can usually skip fetching the segment
|
|
2237
|
-
// by examining the references we already have. This won't be possible if
|
|
2238
|
-
// there was some kind of lag or delay updating the manifest on the server,
|
|
2239
|
-
// in which extreme case we would fall back to fetching a segment. This
|
|
2240
|
-
// allows us to both avoid fetching segments when possible, and recover from
|
|
2241
|
-
// certain server-side issues gracefully.
|
|
2242
|
-
// Do not use cached start time for the segments with discontinuity tags.
|
|
2243
|
-
if (this.manifest_ && !isDiscontinuity) {
|
|
2244
|
-
const streamInfo =
|
|
2245
|
-
this.uriToStreamInfosMap_.get(verbatimMediaPlaylistUri);
|
|
2246
|
-
const startTime = streamInfo.mediaSequenceToStartTime.get(
|
|
2247
|
-
mediaSequenceNumber);
|
|
2248
|
-
if (startTime != undefined) {
|
|
2249
|
-
// We found it! Avoid fetching and parsing the segment.
|
|
2250
|
-
shaka.log.v1('Found segment start time in previous manifest',
|
|
2251
|
-
startTime);
|
|
2252
|
-
return startTime;
|
|
2253
|
-
}
|
|
2254
|
-
|
|
2255
|
-
shaka.log.debug(
|
|
2256
|
-
'Unable to find segment start time in previous manifest!');
|
|
2257
|
-
}
|
|
2258
|
-
|
|
2259
|
-
// TODO: Introduce a new tag to extend HLS and provide the first segment's
|
|
2260
|
-
// start time. This will avoid the need for these fetches in content
|
|
2261
|
-
// packaged with Shaka Packager. This web-friendly extension to HLS can
|
|
2262
|
-
// then be proposed to Apple for inclusion in a future version of HLS.
|
|
2263
|
-
// See https://github.com/google/shaka-packager/issues/294
|
|
2264
|
-
|
|
2265
|
-
shaka.log.v1('Fetching segment to find start time');
|
|
2266
|
-
mimeType = mimeType.toLowerCase();
|
|
2267
|
-
|
|
2268
|
-
if (shaka.hls.HlsParser.RAW_FORMATS_.includes(mimeType)) {
|
|
2269
|
-
// Raw formats contain no timestamps. Even if there is an ID3 tag with a
|
|
2270
|
-
// timestamp, that's not going to be honored by MediaSource, which will
|
|
2271
|
-
// use sequence mode for these segments. We don't yet support sequence
|
|
2272
|
-
// mode, so we must reject these streams.
|
|
2273
|
-
// TODO(#2337): Support sequence mode and align raw format timestamps to
|
|
2274
|
-
// other streams.
|
|
2275
|
-
shaka.log.alwaysWarn(
|
|
2276
|
-
'Raw formats are not yet supported. Skipping ' + mimeType);
|
|
2277
|
-
throw new shaka.util.Error(
|
|
2278
|
-
shaka.util.Error.Severity.RECOVERABLE,
|
|
2279
|
-
shaka.util.Error.Category.MANIFEST,
|
|
2280
|
-
shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM);
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
if (mimeType == 'video/webm') {
|
|
2284
|
-
shaka.log.alwaysWarn('WebM in HLS is not yet supported. Skipping.');
|
|
2285
|
-
throw new shaka.util.Error(
|
|
2286
|
-
shaka.util.Error.Severity.RECOVERABLE,
|
|
2287
|
-
shaka.util.Error.Category.MANIFEST,
|
|
2288
|
-
shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM);
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') {
|
|
2292
|
-
// We also need the init segment to get the correct timescale. But if the
|
|
2293
|
-
// stream is self-initializing, use the same response for both.
|
|
2294
|
-
const fetches = [this.fetchStartOfSegment_(
|
|
2295
|
-
segmentRef,
|
|
2296
|
-
type,
|
|
2297
|
-
mimeType,
|
|
2298
|
-
codecs,
|
|
2299
|
-
bandwidth,
|
|
2300
|
-
)];
|
|
2301
|
-
|
|
2302
|
-
if (initSegmentRef) {
|
|
2303
|
-
fetches.push(this.fetchStartOfSegment_(
|
|
2304
|
-
initSegmentRef,
|
|
2305
|
-
type,
|
|
2306
|
-
mimeType,
|
|
2307
|
-
codecs,
|
|
2308
|
-
bandwidth,
|
|
2309
|
-
));
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
const responses = await Promise.all(fetches);
|
|
2313
|
-
|
|
2314
|
-
// If the stream is self-initializing, use the main segment in-place of
|
|
2315
|
-
// the init segment.
|
|
2316
|
-
const segmentResponse = responses[0];
|
|
2317
|
-
const initSegmentResponse = responses[1] || responses[0];
|
|
2318
|
-
|
|
2319
|
-
return this.getStartTimeFromMp4Segment_(
|
|
2320
|
-
verbatimMediaPlaylistUri, segmentResponse.uri,
|
|
2321
|
-
segmentResponse.data, initSegmentResponse.data);
|
|
2322
|
-
}
|
|
2323
|
-
|
|
2324
|
-
if (mimeType == 'video/mp2t') {
|
|
2325
|
-
const response = await this.fetchStartOfSegment_(
|
|
2326
|
-
segmentRef,
|
|
2327
|
-
type,
|
|
2328
|
-
mimeType,
|
|
2329
|
-
codecs,
|
|
2330
|
-
bandwidth,
|
|
2331
|
-
);
|
|
2332
|
-
goog.asserts.assert(response.data, 'Should have a response body!');
|
|
2333
|
-
return this.getStartTimeFromTsSegment_(
|
|
2334
|
-
verbatimMediaPlaylistUri, response.uri, response.data);
|
|
2335
|
-
}
|
|
2336
|
-
|
|
2337
|
-
throw new shaka.util.Error(
|
|
2338
|
-
shaka.util.Error.Severity.CRITICAL,
|
|
2339
|
-
shaka.util.Error.Category.MANIFEST,
|
|
2340
|
-
shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME,
|
|
2341
|
-
verbatimMediaPlaylistUri);
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
/**
|
|
2345
|
-
* Parses an mp4 segment to get its start time.
|
|
2346
|
-
*
|
|
2347
|
-
* @param {string} playlistUri
|
|
2348
|
-
* @param {string} segmentUri
|
|
2349
|
-
* @param {BufferSource} mediaData
|
|
2350
|
-
* @param {BufferSource} initData
|
|
2351
|
-
* @return {number}
|
|
2352
|
-
* @private
|
|
2353
|
-
*/
|
|
2354
|
-
getStartTimeFromMp4Segment_(playlistUri, segmentUri, mediaData, initData) {
|
|
2355
|
-
const Mp4Parser = shaka.util.Mp4Parser;
|
|
2356
|
-
|
|
2357
|
-
let timescale = 0;
|
|
2358
|
-
new Mp4Parser()
|
|
2359
|
-
.box('moov', Mp4Parser.children)
|
|
2360
|
-
.box('trak', Mp4Parser.children)
|
|
2361
|
-
.box('mdia', Mp4Parser.children)
|
|
2362
|
-
.fullBox('mdhd', (box) => {
|
|
2363
|
-
goog.asserts.assert(
|
|
2364
|
-
box.version == 0 || box.version == 1,
|
|
2365
|
-
'MDHD version can only be 0 or 1');
|
|
2366
|
-
|
|
2367
|
-
const parsedMDHDBox = shaka.util.Mp4BoxParsers.parseMDHD(
|
|
2368
|
-
box.reader, box.version);
|
|
2369
|
-
|
|
2370
|
-
timescale = parsedMDHDBox.timescale;
|
|
2371
|
-
box.parser.stop();
|
|
2372
|
-
}).parse(initData, /* partialOkay= */ true);
|
|
2373
|
-
|
|
2374
|
-
if (!timescale) {
|
|
2375
|
-
shaka.log.error('Unable to find timescale in init segment!');
|
|
2376
|
-
throw new shaka.util.Error(
|
|
2377
|
-
shaka.util.Error.Severity.CRITICAL,
|
|
2378
|
-
shaka.util.Error.Category.MANIFEST,
|
|
2379
|
-
shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME,
|
|
2380
|
-
playlistUri, segmentUri);
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
let startTime = 0;
|
|
2384
|
-
let parsedMedia = false;
|
|
2385
|
-
new Mp4Parser()
|
|
2386
|
-
.box('moof', Mp4Parser.children)
|
|
2387
|
-
.box('traf', Mp4Parser.children)
|
|
2388
|
-
.fullBox('tfdt', (box) => {
|
|
2389
|
-
goog.asserts.assert(
|
|
2390
|
-
box.version == 0 || box.version == 1,
|
|
2391
|
-
'TFDT version can only be 0 or 1');
|
|
2392
|
-
|
|
2393
|
-
const parsedTFDTBox = shaka.util.Mp4BoxParsers.parseTFDT(
|
|
2394
|
-
box.reader, box.version);
|
|
2395
|
-
const baseTime = parsedTFDTBox.baseMediaDecodeTime;
|
|
2396
|
-
startTime = baseTime / timescale;
|
|
2397
|
-
parsedMedia = true;
|
|
2398
|
-
box.parser.stop();
|
|
2399
|
-
}).parse(mediaData, /* partialOkay= */ true);
|
|
2400
|
-
|
|
2401
|
-
if (!parsedMedia) {
|
|
2402
|
-
throw new shaka.util.Error(
|
|
2403
|
-
shaka.util.Error.Severity.CRITICAL,
|
|
2404
|
-
shaka.util.Error.Category.MANIFEST,
|
|
2405
|
-
shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME,
|
|
2406
|
-
playlistUri, segmentUri);
|
|
2407
|
-
}
|
|
2408
|
-
return startTime;
|
|
2409
|
-
}
|
|
2410
|
-
|
|
2411
|
-
/**
|
|
2412
|
-
* Parses a TS segment to get its start time.
|
|
2413
|
-
*
|
|
2414
|
-
* @param {string} playlistUri
|
|
2415
|
-
* @param {string} segmentUri
|
|
2416
|
-
* @param {BufferSource} data
|
|
2417
|
-
* @return {number}
|
|
2418
|
-
* @private
|
|
2419
|
-
*/
|
|
2420
|
-
getStartTimeFromTsSegment_(playlistUri, segmentUri, data) {
|
|
2421
|
-
const reader = new shaka.util.DataViewReader(
|
|
2422
|
-
data, shaka.util.DataViewReader.Endianness.BIG_ENDIAN);
|
|
2423
|
-
|
|
2424
|
-
const fail = () => {
|
|
2425
|
-
throw new shaka.util.Error(
|
|
2426
|
-
shaka.util.Error.Severity.CRITICAL,
|
|
2427
|
-
shaka.util.Error.Category.MANIFEST,
|
|
2428
|
-
shaka.util.Error.Code.HLS_COULD_NOT_PARSE_SEGMENT_START_TIME,
|
|
2429
|
-
playlistUri, segmentUri);
|
|
2430
|
-
};
|
|
2431
|
-
|
|
2432
|
-
let packetStart = 0;
|
|
2433
|
-
let syncByte = 0;
|
|
2434
|
-
|
|
2435
|
-
const skipPacket = () => {
|
|
2436
|
-
// 188-byte packets are standard, so assume that.
|
|
2437
|
-
reader.seek(packetStart + 188);
|
|
2438
|
-
syncByte = reader.readUint8();
|
|
2439
|
-
if (syncByte != 0x47) {
|
|
2440
|
-
// We haven't found the sync byte, so try it as a 192-byte packet.
|
|
2441
|
-
reader.seek(packetStart + 192);
|
|
2442
|
-
syncByte = reader.readUint8();
|
|
2443
|
-
}
|
|
2444
|
-
if (syncByte != 0x47) {
|
|
2445
|
-
// We still haven't found the sync byte, so try as a 204-byte packet.
|
|
2446
|
-
reader.seek(packetStart + 204);
|
|
2447
|
-
syncByte = reader.readUint8();
|
|
2448
|
-
}
|
|
2449
|
-
if (syncByte != 0x47) {
|
|
2450
|
-
// We still haven't found the sync byte, so the packet was of a
|
|
2451
|
-
// non-standard size.
|
|
2452
|
-
fail();
|
|
2453
|
-
}
|
|
2454
|
-
// Put the sync byte back so we can read it in the next loop.
|
|
2455
|
-
reader.rewind(1);
|
|
2456
|
-
};
|
|
2457
|
-
|
|
2458
|
-
// We will look a few packet-lengths forward to find the first sync byte.
|
|
2459
|
-
// Note that we are using this method on what is already a subset of the
|
|
2460
|
-
// file (the first |shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_| bytes), so
|
|
2461
|
-
// we can't look too far ahead to begin with.
|
|
2462
|
-
let syncByteScanLength = Math.min(reader.getLength() - 188, 5 * 188);
|
|
2463
|
-
|
|
2464
|
-
// TODO: refactor this while loop for better readability.
|
|
2465
|
-
// eslint-disable-next-line no-constant-condition
|
|
2466
|
-
while (true) {
|
|
2467
|
-
// Format reference: https://bit.ly/TsPacket
|
|
2468
|
-
packetStart = reader.getPosition();
|
|
2469
|
-
|
|
2470
|
-
syncByte = reader.readUint8();
|
|
2471
|
-
if (syncByte != 0x47) {
|
|
2472
|
-
if (syncByteScanLength > 0) {
|
|
2473
|
-
// This file could have started with a cut-off TS packet. Scan forward
|
|
2474
|
-
// until we find a sync byte.
|
|
2475
|
-
syncByteScanLength -= 1;
|
|
2101
|
+
const reference = this.createSegmentReference_(
|
|
2102
|
+
initSegmentRef,
|
|
2103
|
+
previousReference,
|
|
2104
|
+
item,
|
|
2105
|
+
startTime,
|
|
2106
|
+
variables,
|
|
2107
|
+
playlist.absoluteUri,
|
|
2108
|
+
type);
|
|
2109
|
+
|
|
2110
|
+
references.push(reference);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
// If some segments have sync times, but not all, extrapolate the sync
|
|
2114
|
+
// times of the ones with none.
|
|
2115
|
+
const someSyncTime = references.some((ref) => ref.syncTime != null);
|
|
2116
|
+
if (someSyncTime) {
|
|
2117
|
+
for (let i = 0; i < references.length; i++) {
|
|
2118
|
+
const reference = references[i];
|
|
2119
|
+
if (reference.syncTime != null) {
|
|
2120
|
+
// No need to extrapolate.
|
|
2476
2121
|
continue;
|
|
2477
2122
|
}
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
const pesHeaderLengthRemaining = reader.readUint8();
|
|
2531
|
-
if (pesHeaderLengthRemaining == 0) {
|
|
2532
|
-
fail();
|
|
2123
|
+
// Find the nearest segment with syncTime, in either direction.
|
|
2124
|
+
// This looks forward and backward simultaneously, keeping track of what
|
|
2125
|
+
// to offset the syncTime it finds by as it goes.
|
|
2126
|
+
let forwardAdd = 0;
|
|
2127
|
+
let forwardI = i;
|
|
2128
|
+
/**
|
|
2129
|
+
* Look forwards one reference at a time, summing all durations as we
|
|
2130
|
+
* go, until we find a reference with a syncTime to use as a basis.
|
|
2131
|
+
* This DOES count the original reference, but DOESN'T count the first
|
|
2132
|
+
* reference with a syncTime (as we approach it from behind).
|
|
2133
|
+
* @return {?number}
|
|
2134
|
+
*/
|
|
2135
|
+
const lookForward = () => {
|
|
2136
|
+
const other = references[forwardI];
|
|
2137
|
+
if (other) {
|
|
2138
|
+
if (other.syncTime != null) {
|
|
2139
|
+
return other.syncTime + forwardAdd;
|
|
2140
|
+
}
|
|
2141
|
+
forwardAdd -= other.endTime - other.startTime;
|
|
2142
|
+
forwardI += 1;
|
|
2143
|
+
}
|
|
2144
|
+
return null;
|
|
2145
|
+
};
|
|
2146
|
+
let backwardAdd = 0;
|
|
2147
|
+
let backwardI = i;
|
|
2148
|
+
/**
|
|
2149
|
+
* Look backwards one reference at a time, summing all durations as we
|
|
2150
|
+
* go, until we find a reference with a syncTime to use as a basis.
|
|
2151
|
+
* This DOESN'T count the original reference, but DOES count the first
|
|
2152
|
+
* reference with a syncTime (as we approach it from ahead).
|
|
2153
|
+
* @return {?number}
|
|
2154
|
+
*/
|
|
2155
|
+
const lookBackward = () => {
|
|
2156
|
+
const other = references[backwardI];
|
|
2157
|
+
if (other) {
|
|
2158
|
+
if (other != reference) {
|
|
2159
|
+
backwardAdd += other.endTime - other.startTime;
|
|
2160
|
+
}
|
|
2161
|
+
if (other.syncTime != null) {
|
|
2162
|
+
return other.syncTime + backwardAdd;
|
|
2163
|
+
}
|
|
2164
|
+
backwardI -= 1;
|
|
2165
|
+
}
|
|
2166
|
+
return null;
|
|
2167
|
+
};
|
|
2168
|
+
while (reference.syncTime == null) {
|
|
2169
|
+
reference.syncTime = lookBackward();
|
|
2170
|
+
if (reference.syncTime == null) {
|
|
2171
|
+
reference.syncTime = lookForward();
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2533
2174
|
}
|
|
2175
|
+
}
|
|
2534
2176
|
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2177
|
+
// Split the sync times properly among partial segments.
|
|
2178
|
+
if (someSyncTime) {
|
|
2179
|
+
for (const reference of references) {
|
|
2180
|
+
let syncTime = reference.syncTime;
|
|
2181
|
+
for (const partial of reference.partialReferences) {
|
|
2182
|
+
partial.syncTime = syncTime;
|
|
2183
|
+
syncTime += partial.endTime - partial.startTime;
|
|
2184
|
+
}
|
|
2539
2185
|
}
|
|
2540
|
-
|
|
2541
|
-
const pts0 = reader.readUint8();
|
|
2542
|
-
const pts1 = reader.readUint16();
|
|
2543
|
-
const pts2 = reader.readUint16();
|
|
2544
|
-
// Reconstruct 33-bit PTS from the 5-byte, padded structure.
|
|
2545
|
-
const ptsHigh3 = (pts0 & 0x0e) >> 1;
|
|
2546
|
-
const ptsLow30 = ((pts1 & 0xfffe) << 14) | ((pts2 & 0xfffe) >> 1);
|
|
2547
|
-
// Reconstruct the PTS as a float. Avoid bitwise operations to combine
|
|
2548
|
-
// because bitwise ops treat the values as 32-bit ints.
|
|
2549
|
-
const pts = ptsHigh3 * (1 << 30) + ptsLow30;
|
|
2550
|
-
return pts / shaka.hls.HlsParser.TS_TIMESCALE_;
|
|
2551
2186
|
}
|
|
2187
|
+
|
|
2188
|
+
return references;
|
|
2552
2189
|
}
|
|
2553
2190
|
|
|
2554
2191
|
/**
|
|
@@ -2615,11 +2252,10 @@ shaka.hls.HlsParser = class {
|
|
|
2615
2252
|
|
|
2616
2253
|
if (contentType == ContentType.TEXT) {
|
|
2617
2254
|
// The extension map didn't work.
|
|
2618
|
-
if (
|
|
2255
|
+
if (codecs == 'vtt' || codecs == 'wvtt') {
|
|
2619
2256
|
// If codecs is 'vtt', it's WebVTT.
|
|
2620
|
-
// If there was no codecs string, assume HLS text streams are WebVTT.
|
|
2621
2257
|
return 'text/vtt';
|
|
2622
|
-
} else {
|
|
2258
|
+
} else if (codecs && codecs !== '') {
|
|
2623
2259
|
// Otherwise, assume MP4-embedded text, since text-based formats tend
|
|
2624
2260
|
// not to have a codecs string at all.
|
|
2625
2261
|
return 'application/mp4';
|
|
@@ -2644,6 +2280,11 @@ shaka.hls.HlsParser = class {
|
|
|
2644
2280
|
const contentMimeType = response.headers['content-type'];
|
|
2645
2281
|
|
|
2646
2282
|
if (!contentMimeType) {
|
|
2283
|
+
if (contentType == ContentType.TEXT) {
|
|
2284
|
+
// If there was no codecs string and no content-type, assume HLS text
|
|
2285
|
+
// streams are WebVTT.
|
|
2286
|
+
return 'text/vtt';
|
|
2287
|
+
}
|
|
2647
2288
|
// If the HLS content is lacking in both MIME type metadata and
|
|
2648
2289
|
// segment file extensions, we fall back to assuming it's MP4.
|
|
2649
2290
|
const fallbackMimeType = map['mp4'];
|
|
@@ -2809,6 +2450,33 @@ shaka.hls.HlsParser = class {
|
|
|
2809
2450
|
return op.promise;
|
|
2810
2451
|
}
|
|
2811
2452
|
|
|
2453
|
+
/**
|
|
2454
|
+
* @param {!shaka.hls.Tag} drmTag
|
|
2455
|
+
* @param {string} mimeType
|
|
2456
|
+
* @return {?shaka.extern.DrmInfo}
|
|
2457
|
+
* @private
|
|
2458
|
+
*/
|
|
2459
|
+
static fairplayDrmParser_(drmTag, mimeType) {
|
|
2460
|
+
if (mimeType == 'video/mp2t') {
|
|
2461
|
+
throw new shaka.util.Error(
|
|
2462
|
+
shaka.util.Error.Severity.CRITICAL,
|
|
2463
|
+
shaka.util.Error.Category.MANIFEST,
|
|
2464
|
+
shaka.util.Error.Code.HLS_MSE_ENCRYPTED_MP2T_NOT_SUPPORTED);
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
/*
|
|
2468
|
+
* Even if we're not able to construct initData through the HLS tag, adding
|
|
2469
|
+
* a DRMInfo will allow DRM Engine to request a media key system access
|
|
2470
|
+
* with the correct keySystem and initDataType
|
|
2471
|
+
*/
|
|
2472
|
+
const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
|
|
2473
|
+
'com.apple.fps', [
|
|
2474
|
+
{initDataType: 'sinf', initData: new Uint8Array(0)},
|
|
2475
|
+
]);
|
|
2476
|
+
|
|
2477
|
+
return drmInfo;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2812
2480
|
/**
|
|
2813
2481
|
* @param {!shaka.hls.Tag} drmTag
|
|
2814
2482
|
* @return {?shaka.extern.DrmInfo}
|
|
@@ -2871,7 +2539,10 @@ shaka.hls.HlsParser = class {
|
|
|
2871
2539
|
0x9a, 0x04, 0xf0, 0x79, 0x98, 0x40, 0x42, 0x86,
|
|
2872
2540
|
0xab, 0x92, 0xe6, 0x5b, 0xe0, 0x88, 0x5f, 0x95,
|
|
2873
2541
|
]);
|
|
2874
|
-
const
|
|
2542
|
+
const keyIds = new Set();
|
|
2543
|
+
const psshVersion = 0;
|
|
2544
|
+
const pssh =
|
|
2545
|
+
shaka.util.Pssh.createPssh(data, systemId, keyIds, psshVersion);
|
|
2875
2546
|
const drmInfo = shaka.util.ManifestParserUtils.createDrmInfo(
|
|
2876
2547
|
'com.microsoft.playready', [
|
|
2877
2548
|
{initDataType: 'cenc', initData: pssh},
|
|
@@ -2890,7 +2561,6 @@ shaka.hls.HlsParser = class {
|
|
|
2890
2561
|
* minTimestamp: number,
|
|
2891
2562
|
* maxTimestamp: number,
|
|
2892
2563
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
2893
|
-
* discontinuityToMediaSequence: !Map.<number, number>,
|
|
2894
2564
|
* canSkipSegments: boolean
|
|
2895
2565
|
* }}
|
|
2896
2566
|
*
|
|
@@ -2913,9 +2583,6 @@ shaka.hls.HlsParser = class {
|
|
|
2913
2583
|
* The maximum timestamp found in the stream.
|
|
2914
2584
|
* @property {!Map.<number, number>} mediaSequenceToStartTime
|
|
2915
2585
|
* A map of media sequence numbers to media start times.
|
|
2916
|
-
* @property {!Map.<number, number>} discontinuityToMediaSequence
|
|
2917
|
-
* A map of discontinuity sequence numbers to the media sequence number of the
|
|
2918
|
-
* segment starting with that discontinuity sequence number.
|
|
2919
2586
|
* @property {boolean} canSkipSegments
|
|
2920
2587
|
* True if the server supports delta playlist updates, and we can send a
|
|
2921
2588
|
* request for a playlist that can skip older media segments.
|
|
@@ -2959,21 +2626,6 @@ shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_ = {
|
|
|
2959
2626
|
};
|
|
2960
2627
|
|
|
2961
2628
|
|
|
2962
|
-
/**
|
|
2963
|
-
* MIME types of raw formats.
|
|
2964
|
-
* TODO(#2337): Support raw formats and share this list among parsers.
|
|
2965
|
-
*
|
|
2966
|
-
* @const {!Array.<string>}
|
|
2967
|
-
* @private
|
|
2968
|
-
*/
|
|
2969
|
-
shaka.hls.HlsParser.RAW_FORMATS_ = [
|
|
2970
|
-
'audio/aac',
|
|
2971
|
-
'audio/ac3',
|
|
2972
|
-
'audio/ec3',
|
|
2973
|
-
'audio/mpeg',
|
|
2974
|
-
];
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
2629
|
/**
|
|
2978
2630
|
* @const {!Object.<string, string>}
|
|
2979
2631
|
* @private
|
|
@@ -3031,7 +2683,7 @@ shaka.hls.HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_ = {
|
|
|
3031
2683
|
|
|
3032
2684
|
|
|
3033
2685
|
/**
|
|
3034
|
-
* @typedef {function(!shaka.hls.Tag):?shaka.extern.DrmInfo}
|
|
2686
|
+
* @typedef {function(!shaka.hls.Tag, string):?shaka.extern.DrmInfo}
|
|
3035
2687
|
* @private
|
|
3036
2688
|
*/
|
|
3037
2689
|
shaka.hls.HlsParser.DrmParser_;
|
|
@@ -3042,10 +2694,8 @@ shaka.hls.HlsParser.DrmParser_;
|
|
|
3042
2694
|
* @private
|
|
3043
2695
|
*/
|
|
3044
2696
|
shaka.hls.HlsParser.KEYFORMATS_TO_DRM_PARSERS_ = {
|
|
3045
|
-
/* TODO: https://github.com/google/shaka-player/issues/382
|
|
3046
2697
|
'com.apple.streamingkeydelivery':
|
|
3047
2698
|
shaka.hls.HlsParser.fairplayDrmParser_,
|
|
3048
|
-
*/
|
|
3049
2699
|
'urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed':
|
|
3050
2700
|
shaka.hls.HlsParser.widevineDrmParser_,
|
|
3051
2701
|
'com.microsoft.playready':
|
|
@@ -3063,28 +2713,13 @@ shaka.hls.HlsParser.PresentationType_ = {
|
|
|
3063
2713
|
LIVE: 'LIVE',
|
|
3064
2714
|
};
|
|
3065
2715
|
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
shaka.
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
* need to know the segment start time. This allows us to avoid fetching the
|
|
3077
|
-
* entire segment in many cases.
|
|
3078
|
-
*
|
|
3079
|
-
* @const {number}
|
|
3080
|
-
* @private
|
|
3081
|
-
*/
|
|
3082
|
-
shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_ = 2048;
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
shaka.media.ManifestParser.registerParserByExtension(
|
|
3086
|
-
'm3u8', () => new shaka.hls.HlsParser());
|
|
3087
|
-
shaka.media.ManifestParser.registerParserByMime(
|
|
3088
|
-
'application/x-mpegurl', () => new shaka.hls.HlsParser());
|
|
3089
|
-
shaka.media.ManifestParser.registerParserByMime(
|
|
3090
|
-
'application/vnd.apple.mpegurl', () => new shaka.hls.HlsParser());
|
|
2716
|
+
if (!shaka.util.Platform.isTizen3() &&
|
|
2717
|
+
!shaka.util.Platform.isTizen2() &&
|
|
2718
|
+
!shaka.util.Platform.isWebOS3()) {
|
|
2719
|
+
shaka.media.ManifestParser.registerParserByExtension(
|
|
2720
|
+
'm3u8', () => new shaka.hls.HlsParser());
|
|
2721
|
+
shaka.media.ManifestParser.registerParserByMime(
|
|
2722
|
+
'application/x-mpegurl', () => new shaka.hls.HlsParser());
|
|
2723
|
+
shaka.media.ManifestParser.registerParserByMime(
|
|
2724
|
+
'application/vnd.apple.mpegurl', () => new shaka.hls.HlsParser());
|
|
2725
|
+
}
|