shaka-player 3.2.2 → 3.3.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/ISSUE_TEMPLATE/bug_report.md +1 -1
- package/.github/ISSUE_TEMPLATE/feature_request.md +1 -1
- package/.github/ISSUE_TEMPLATE/question.md +1 -1
- package/AUTHORS +6 -5
- package/CHANGELOG.md +247 -0
- package/CONTRIBUTORS +4 -2
- package/build/types/core +3 -0
- package/build/types/offline +1 -0
- package/build/types/polyfill +1 -0
- package/build/types/ui +2 -0
- package/demo/common/assets.js +18 -0
- package/demo/common/message_ids.js +13 -1
- package/demo/config.js +31 -1
- package/demo/index.html +1 -1
- package/demo/locales/en.json +12 -0
- package/demo/locales/source.json +48 -0
- package/dist/controls.css +26 -2
- package/dist/controls.css.map +1 -1
- package/dist/demo.compiled.debug.js +19 -16
- package/dist/demo.compiled.debug.map +3 -3
- package/dist/demo.compiled.js +19 -16
- package/dist/demo.compiled.map +3 -3
- package/dist/deps.js +18 -11
- package/dist/locales.js +28 -26
- package/dist/receiver.compiled.debug.js +11 -9
- package/dist/receiver.compiled.debug.map +3 -3
- package/dist/receiver.compiled.js +11 -9
- package/dist/receiver.compiled.map +3 -3
- package/dist/shaka-player.compiled.d.ts +205 -15
- package/dist/shaka-player.compiled.debug.d.ts +205 -15
- package/dist/shaka-player.compiled.debug.externs.js +256 -6
- package/dist/shaka-player.compiled.debug.js +406 -354
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +256 -6
- package/dist/shaka-player.compiled.js +826 -800
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +221 -16
- package/dist/shaka-player.ui.debug.d.ts +221 -16
- package/dist/shaka-player.ui.debug.externs.js +283 -6
- package/dist/shaka-player.ui.debug.js +547 -476
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +283 -6
- package/dist/shaka-player.ui.js +1019 -980
- package/dist/shaka-player.ui.map +4 -4
- package/docs/design/bg-fetch-after.gv +29 -0
- package/docs/design/bg-fetch-after.gv.png +0 -0
- package/docs/design/bg-fetch-before.gv +18 -0
- package/docs/design/bg-fetch-before.gv.png +0 -0
- package/docs/design/bg-fetch.md +134 -0
- package/docs/tutorials/blob-url.md +15 -0
- package/docs/tutorials/index.json +1 -0
- package/docs/tutorials/ui-customization.md +66 -0
- package/docs/tutorials/ui.md +5 -5
- package/externs/ima.js +3 -0
- package/externs/shaka/ads.js +5 -0
- package/externs/shaka/manifest.js +12 -1
- package/externs/shaka/manifest_parser.js +10 -1
- package/externs/shaka/net.js +20 -4
- package/externs/shaka/offline.js +31 -2
- package/externs/shaka/player.js +130 -5
- package/externs/shaka/text.js +14 -0
- package/externs/webcrypto.js +18 -0
- package/lib/abr/ewma.js +14 -0
- package/lib/abr/ewma_bandwidth_estimator.js +16 -2
- package/lib/abr/simple_abr_manager.js +6 -0
- package/lib/ads/client_side_ad.js +8 -0
- package/lib/ads/client_side_ad_manager.js +8 -4
- package/lib/ads/server_side_ad.js +7 -0
- package/lib/cast/cast_utils.js +1 -0
- package/lib/dash/dash_parser.js +48 -14
- package/lib/dash/segment_base.js +25 -1
- package/lib/dash/segment_list.js +18 -9
- package/lib/dash/segment_template.js +23 -10
- package/lib/hls/hls_parser.js +86 -23
- package/lib/media/quality_observer.js +291 -0
- package/lib/media/segment_index.js +6 -0
- package/lib/media/segment_reference.js +18 -3
- package/lib/media/streaming_engine.js +42 -4
- package/lib/net/http_fetch_plugin.js +31 -11
- package/lib/net/http_plugin_utils.js +1 -0
- package/lib/net/http_xhr_plugin.js +40 -14
- package/lib/net/networking_engine.js +81 -18
- package/lib/offline/download_info.js +68 -0
- package/lib/offline/download_manager.js +13 -5
- package/lib/offline/indexeddb/base_storage_cell.js +35 -0
- package/lib/offline/indexeddb/storage_mechanism.js +1 -1
- package/lib/offline/indexeddb/v5_storage_cell.js +5 -0
- package/lib/offline/storage.js +393 -284
- package/lib/offline/stored_content_utils.js +2 -0
- package/lib/player.js +206 -7
- package/lib/polyfill/random_uuid.js +52 -0
- package/lib/text/cue.js +12 -0
- package/lib/text/ttml_text_parser.js +25 -2
- package/lib/text/ui_text_displayer.js +2 -2
- package/lib/util/cmcd_manager.js +826 -0
- package/lib/util/error.js +6 -0
- package/lib/util/language_utils.js +12 -0
- package/lib/util/mutex.js +52 -0
- package/lib/util/periods.js +98 -64
- package/lib/util/player_configuration.js +18 -0
- package/package.json +1 -1
- package/shaka-player.uncompiled.js +3 -0
- package/test/dash/dash_parser_content_protection_unit.js +2 -0
- package/test/dash/dash_parser_live_unit.js +101 -1
- package/test/dash/dash_parser_manifest_unit.js +35 -1
- package/test/dash/dash_parser_segment_base_unit.js +2 -0
- package/test/dash/dash_parser_segment_list_unit.js +2 -1
- package/test/dash/dash_parser_segment_template_unit.js +2 -1
- package/test/hls/hls_live_unit.js +2 -0
- package/test/hls/hls_parser_unit.js +2 -0
- package/test/media/quality_observer_unit.js +139 -0
- package/test/media/segment_reference_unit.js +16 -1
- package/test/media/streaming_engine_integration.js +2 -1
- package/test/media/streaming_engine_unit.js +2 -0
- package/test/net/http_plugin_unit.js +20 -15
- package/test/offline/storage_integration.js +108 -4
- package/test/player_integration.js +99 -0
- package/test/test/util/dash_parser_util.js +4 -0
- package/test/test/util/fake_ad.js +7 -0
- package/test/test/util/fake_drm_engine.js +9 -2
- package/test/test/util/manifest_generator.js +3 -2
- package/test/test/util/test_scheme.js +14 -1
- package/test/text/ttml_text_parser_unit.js +47 -0
- package/test/ui/ui_unit.js +250 -0
- package/test/util/cmcd_manager_unit.js +277 -0
- package/ui/airplay_button.js +1 -0
- package/ui/audio_language_selection.js +7 -0
- package/ui/cast_button.js +1 -0
- package/ui/context_menu.js +109 -0
- package/ui/controls.js +41 -7
- package/ui/controls.less +1 -0
- package/ui/enums.js +4 -0
- package/ui/externs/ui.js +28 -1
- package/ui/fast_forward_button.js +14 -5
- package/ui/fullscreen_button.js +1 -0
- package/ui/less/buttons.less +0 -4
- package/ui/less/containers.less +74 -0
- package/ui/less/overflow_menu.less +0 -10
- package/ui/less/tooltip.less +97 -0
- package/ui/locales/en.json +1 -0
- package/ui/locales/source.json +4 -0
- package/ui/loop_button.js +5 -0
- package/ui/mute_button.js +2 -1
- package/ui/overflow_menu.js +5 -25
- package/ui/pip_button.js +5 -0
- package/ui/play_button.js +2 -2
- package/ui/playback_rate_selection.js +8 -9
- package/ui/resolution_selection.js +7 -0
- package/ui/rewind_button.js +15 -6
- package/ui/seek_bar.js +4 -2
- package/ui/settings_menu.js +36 -4
- package/ui/small_play_button.js +1 -0
- package/ui/statistics_button.js +253 -0
- package/ui/text_selection.js +11 -4
- package/ui/ui.js +30 -0
- package/ui/volume_bar.js +1 -1
|
@@ -31,7 +31,7 @@ shaka.dash.SegmentTemplate = class {
|
|
|
31
31
|
* @param {shaka.dash.DashParser.Context} context
|
|
32
32
|
* @param {shaka.dash.DashParser.RequestInitSegmentCallback}
|
|
33
33
|
* requestInitSegment
|
|
34
|
-
* @param {!Object.<string, !shaka.
|
|
34
|
+
* @param {!Object.<string, !shaka.extern.Stream>} streamMap
|
|
35
35
|
* @param {boolean} isUpdate True if the manifest is being updated.
|
|
36
36
|
* @param {number} segmentLimit The maximum number of segments to generate for
|
|
37
37
|
* a SegmentTemplate with fixed duration.
|
|
@@ -39,8 +39,8 @@ shaka.dash.SegmentTemplate = class {
|
|
|
39
39
|
* @return {shaka.dash.DashParser.StreamInfo}
|
|
40
40
|
*/
|
|
41
41
|
static createStreamInfo(
|
|
42
|
-
context, requestInitSegment,
|
|
43
|
-
|
|
42
|
+
context, requestInitSegment, streamMap, isUpdate, segmentLimit,
|
|
43
|
+
periodDurationMap) {
|
|
44
44
|
goog.asserts.assert(context.representation.segmentTemplate,
|
|
45
45
|
'Should only be called with SegmentTemplate');
|
|
46
46
|
const SegmentTemplate = shaka.dash.SegmentTemplate;
|
|
@@ -86,10 +86,14 @@ shaka.dash.SegmentTemplate = class {
|
|
|
86
86
|
/** @type {shaka.media.SegmentIndex} */
|
|
87
87
|
let segmentIndex = null;
|
|
88
88
|
let id = null;
|
|
89
|
+
let stream = null;
|
|
89
90
|
if (context.period.id && context.representation.id) {
|
|
90
91
|
// Only check/store the index if period and representation IDs are set.
|
|
91
92
|
id = context.period.id + ',' + context.representation.id;
|
|
92
|
-
|
|
93
|
+
stream = streamMap[id];
|
|
94
|
+
if (stream) {
|
|
95
|
+
segmentIndex = stream.segmentIndex;
|
|
96
|
+
}
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
const references = SegmentTemplate.createFromTimeline_(
|
|
@@ -117,9 +121,6 @@ shaka.dash.SegmentTemplate = class {
|
|
|
117
121
|
context.presentationTimeline.getSegmentAvailabilityStart());
|
|
118
122
|
} else {
|
|
119
123
|
segmentIndex = new shaka.media.SegmentIndex(references);
|
|
120
|
-
if (id && context.dynamic) {
|
|
121
|
-
segmentIndexMap[id] = segmentIndex;
|
|
122
|
-
}
|
|
123
124
|
}
|
|
124
125
|
context.presentationTimeline.notifySegments(references);
|
|
125
126
|
|
|
@@ -127,8 +128,20 @@ shaka.dash.SegmentTemplate = class {
|
|
|
127
128
|
segmentIndex.fit(periodStart, periodEnd);
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
if (stream && context.dynamic) {
|
|
132
|
+
stream.segmentIndex = segmentIndex;
|
|
133
|
+
}
|
|
134
|
+
|
|
130
135
|
return {
|
|
131
|
-
generateSegmentIndex: () =>
|
|
136
|
+
generateSegmentIndex: () => {
|
|
137
|
+
// If segmentIndex is deleted, or segmentIndex's references are
|
|
138
|
+
// released by closeSegmentIndex(), we should set the value of
|
|
139
|
+
// segmentIndex again.
|
|
140
|
+
if (!segmentIndex || segmentIndex.isEmpty()) {
|
|
141
|
+
segmentIndex.merge(references);
|
|
142
|
+
}
|
|
143
|
+
return Promise.resolve(segmentIndex);
|
|
144
|
+
},
|
|
132
145
|
};
|
|
133
146
|
}
|
|
134
147
|
}
|
|
@@ -569,8 +582,8 @@ shaka.dash.SegmentTemplate = class {
|
|
|
569
582
|
baseUris, [filledTemplate]);
|
|
570
583
|
return resolvedUris;
|
|
571
584
|
};
|
|
572
|
-
|
|
573
|
-
return new shaka.media.InitSegmentReference(getUris, 0, null);
|
|
585
|
+
const qualityInfo = shaka.dash.SegmentBase.createQualityInfo(context);
|
|
586
|
+
return new shaka.media.InitSegmentReference(getUris, 0, null, qualityInfo);
|
|
574
587
|
}
|
|
575
588
|
};
|
|
576
589
|
|
package/lib/hls/hls_parser.js
CHANGED
|
@@ -25,6 +25,7 @@ goog.require('shaka.net.DataUriPlugin');
|
|
|
25
25
|
goog.require('shaka.net.NetworkingEngine');
|
|
26
26
|
goog.require('shaka.util.ArrayUtils');
|
|
27
27
|
goog.require('shaka.util.BufferUtils');
|
|
28
|
+
goog.require('shaka.util.CmcdManager');
|
|
28
29
|
goog.require('shaka.util.DataViewReader');
|
|
29
30
|
goog.require('shaka.util.Error');
|
|
30
31
|
goog.require('shaka.util.FakeEvent');
|
|
@@ -332,7 +333,8 @@ shaka.hls.HlsParser = class {
|
|
|
332
333
|
const segments = await this.createSegments_(
|
|
333
334
|
streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
|
|
334
335
|
stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables,
|
|
335
|
-
streamInfo.discontinuityToMediaSequence
|
|
336
|
+
streamInfo.discontinuityToMediaSequence, stream.codecs,
|
|
337
|
+
stream.bandwidth);
|
|
336
338
|
|
|
337
339
|
stream.segmentIndex.mergeAndEvict(
|
|
338
340
|
segments, this.presentationTimeline_.getSegmentAvailabilityStart());
|
|
@@ -763,7 +765,7 @@ shaka.hls.HlsParser = class {
|
|
|
763
765
|
const videoRange = tag.getAttributeValue('VIDEO-RANGE');
|
|
764
766
|
|
|
765
767
|
const streamInfos = await this.createStreamInfosForVariantTag_(tag,
|
|
766
|
-
resolution, frameRate);
|
|
768
|
+
resolution, frameRate, bandwidth);
|
|
767
769
|
|
|
768
770
|
if (streamInfos) {
|
|
769
771
|
goog.asserts.assert(streamInfos.audio.length ||
|
|
@@ -797,10 +799,11 @@ shaka.hls.HlsParser = class {
|
|
|
797
799
|
* @param {!shaka.hls.Tag} tag
|
|
798
800
|
* @param {?string} resolution
|
|
799
801
|
* @param {?string} frameRate
|
|
802
|
+
* @param {number} bandwidth
|
|
800
803
|
* @return {!Promise.<?shaka.hls.HlsParser.StreamInfos>}
|
|
801
804
|
* @private
|
|
802
805
|
*/
|
|
803
|
-
async createStreamInfosForVariantTag_(tag, resolution, frameRate) {
|
|
806
|
+
async createStreamInfosForVariantTag_(tag, resolution, frameRate, bandwidth) {
|
|
804
807
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
805
808
|
/** @type {!Array.<string>} */
|
|
806
809
|
let allCodecs = this.getCodecsForVariantTag_(tag);
|
|
@@ -870,7 +873,8 @@ shaka.hls.HlsParser = class {
|
|
|
870
873
|
let streamInfo;
|
|
871
874
|
if (!ignoreStream) {
|
|
872
875
|
streamInfo =
|
|
873
|
-
await this.createStreamInfoFromVariantTag_(tag, allCodecs, type
|
|
876
|
+
await this.createStreamInfoFromVariantTag_(tag, allCodecs, type,
|
|
877
|
+
bandwidth);
|
|
874
878
|
}
|
|
875
879
|
if (streamInfo) {
|
|
876
880
|
res[streamInfo.stream.type] = [streamInfo];
|
|
@@ -893,15 +897,12 @@ shaka.hls.HlsParser = class {
|
|
|
893
897
|
*/
|
|
894
898
|
getCodecsForVariantTag_(tag) {
|
|
895
899
|
// These are the default codecs to assume if none are specified.
|
|
896
|
-
// The video codec is H.264, with baseline profile and level 3.0.
|
|
897
|
-
// http://blog.pearce.org.nz/2013/11/what-does-h264avc1-codecs-parameters.html
|
|
898
|
-
// The audio codec is "low-complexity" AAC.
|
|
899
900
|
const defaultCodecsArray = [];
|
|
900
901
|
if (!this.config_.disableVideo) {
|
|
901
|
-
defaultCodecsArray.push(
|
|
902
|
+
defaultCodecsArray.push(this.config_.hls.defaultVideoCodec);
|
|
902
903
|
}
|
|
903
904
|
if (!this.config_.disableAudio) {
|
|
904
|
-
defaultCodecsArray.push(
|
|
905
|
+
defaultCodecsArray.push(this.config_.hls.defaultAudioCodec);
|
|
905
906
|
}
|
|
906
907
|
const defaultCodecs = defaultCodecsArray.join(',');
|
|
907
908
|
|
|
@@ -1290,10 +1291,11 @@ shaka.hls.HlsParser = class {
|
|
|
1290
1291
|
* @param {!shaka.hls.Tag} tag
|
|
1291
1292
|
* @param {!Array.<string>} allCodecs
|
|
1292
1293
|
* @param {string} type
|
|
1294
|
+
* @param {number} bandwidth
|
|
1293
1295
|
* @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
|
|
1294
1296
|
* @private
|
|
1295
1297
|
*/
|
|
1296
|
-
async createStreamInfoFromVariantTag_(tag, allCodecs, type) {
|
|
1298
|
+
async createStreamInfoFromVariantTag_(tag, allCodecs, type, bandwidth) {
|
|
1297
1299
|
goog.asserts.assert(tag.name == 'EXT-X-STREAM-INF',
|
|
1298
1300
|
'Should only be called on variant tags!');
|
|
1299
1301
|
const verbatimMediaPlaylistUri = this.variableSubstitution_(
|
|
@@ -1309,7 +1311,7 @@ shaka.hls.HlsParser = class {
|
|
|
1309
1311
|
codecs, type, /* language= */ 'und', /* primary= */ false,
|
|
1310
1312
|
/* name= */ null, /* channelcount= */ null, closedCaptions,
|
|
1311
1313
|
/* characteristics= */ null, /* forced= */ false,
|
|
1312
|
-
/* spatialAudio= */ false);
|
|
1314
|
+
/* spatialAudio= */ false, bandwidth);
|
|
1313
1315
|
if (streamInfo == null) {
|
|
1314
1316
|
return null;
|
|
1315
1317
|
}
|
|
@@ -1336,12 +1338,13 @@ shaka.hls.HlsParser = class {
|
|
|
1336
1338
|
* @param {?string} characteristics
|
|
1337
1339
|
* @param {boolean} forced
|
|
1338
1340
|
* @param {boolean} spatialAudio
|
|
1341
|
+
* @param {(number|undefined)} bandwidth
|
|
1339
1342
|
* @return {!Promise.<?shaka.hls.HlsParser.StreamInfo>}
|
|
1340
1343
|
* @private
|
|
1341
1344
|
*/
|
|
1342
1345
|
async createStreamInfo_(verbatimMediaPlaylistUri, codecs, type, language,
|
|
1343
1346
|
primary, name, channelsCount, closedCaptions, characteristics, forced,
|
|
1344
|
-
spatialAudio) {
|
|
1347
|
+
spatialAudio, bandwidth = undefined) {
|
|
1345
1348
|
// TODO: Refactor, too many parameters
|
|
1346
1349
|
let absoluteMediaPlaylistUri = shaka.hls.Utils.constructAbsoluteUri(
|
|
1347
1350
|
this.masterPlaylistUri_, verbatimMediaPlaylistUri);
|
|
@@ -1459,7 +1462,7 @@ shaka.hls.HlsParser = class {
|
|
|
1459
1462
|
try {
|
|
1460
1463
|
segments = await this.createSegments_(verbatimMediaPlaylistUri,
|
|
1461
1464
|
playlist, type, mimeType, mediaSequenceToStartTime, mediaVariables,
|
|
1462
|
-
discontinuityToMediaSequence);
|
|
1465
|
+
discontinuityToMediaSequence, codecs, bandwidth);
|
|
1463
1466
|
} catch (error) {
|
|
1464
1467
|
if (error.code == shaka.util.Error.Code.HLS_INTERNAL_SKIP_STREAM) {
|
|
1465
1468
|
shaka.log.alwaysWarn('Skipping unsupported HLS stream',
|
|
@@ -1887,11 +1890,14 @@ shaka.hls.HlsParser = class {
|
|
|
1887
1890
|
* @param {!Map.<number, number>} mediaSequenceToStartTime
|
|
1888
1891
|
* @param {!Map.<string, string>} variables
|
|
1889
1892
|
* @param {!Map.<number, number>} discontinuityToMediaSequence
|
|
1893
|
+
* @param {string} codecs
|
|
1894
|
+
* @param {(number|undefined)} bandwidth
|
|
1890
1895
|
* @return {!Promise<!Array.<!shaka.media.SegmentReference>>}
|
|
1891
1896
|
* @private
|
|
1892
1897
|
*/
|
|
1893
1898
|
async createSegments_(verbatimMediaPlaylistUri, playlist, type, mimeType,
|
|
1894
|
-
mediaSequenceToStartTime, variables, discontinuityToMediaSequence
|
|
1899
|
+
mediaSequenceToStartTime, variables, discontinuityToMediaSequence,
|
|
1900
|
+
codecs, bandwidth) {
|
|
1895
1901
|
/** @type {Array.<!shaka.hls.Segment>} */
|
|
1896
1902
|
const hlsSegments = playlist.segments;
|
|
1897
1903
|
goog.asserts.assert(hlsSegments.length, 'Playlist should have segments!');
|
|
@@ -1934,7 +1940,7 @@ shaka.hls.HlsParser = class {
|
|
|
1934
1940
|
this.playlistStartTime_ = await this.getStartTime_(
|
|
1935
1941
|
verbatimMediaPlaylistUri, initSegmentRef, mimeType,
|
|
1936
1942
|
position, /* isDiscontinuity= */ false,
|
|
1937
|
-
hlsSegments[0], variables, type);
|
|
1943
|
+
hlsSegments[0], variables, type, codecs, bandwidth);
|
|
1938
1944
|
}
|
|
1939
1945
|
firstStartTime = this.playlistStartTime_;
|
|
1940
1946
|
}
|
|
@@ -1987,7 +1993,8 @@ shaka.hls.HlsParser = class {
|
|
|
1987
1993
|
// eslint-disable-next-line no-await-in-loop
|
|
1988
1994
|
timestampOffset = await this.getTimestampOffset_(
|
|
1989
1995
|
discontintuitySequenceNum, verbatimMediaPlaylistUri, initSegmentRef,
|
|
1990
|
-
mimeType, position, item, variables, startTime, type
|
|
1996
|
+
mimeType, position, item, variables, startTime, type, codecs,
|
|
1997
|
+
bandwidth);
|
|
1991
1998
|
}
|
|
1992
1999
|
|
|
1993
2000
|
// If the stream is low latency and the user has not configured the
|
|
@@ -2041,13 +2048,16 @@ shaka.hls.HlsParser = class {
|
|
|
2041
2048
|
* @param {!Map.<string, string>} variables
|
|
2042
2049
|
* @param {number} startTime
|
|
2043
2050
|
* @param {string} type
|
|
2051
|
+
* @param {string} codecs
|
|
2052
|
+
* @param {number|undefined} bandwidth
|
|
2044
2053
|
* @return {!Promise.<number>}
|
|
2045
2054
|
* @throws {shaka.util.Error}
|
|
2046
2055
|
* @private
|
|
2047
2056
|
*/
|
|
2048
2057
|
async getTimestampOffset_(discontintuitySequenceNum,
|
|
2049
2058
|
verbatimMediaPlaylistUri, initSegmentRef,
|
|
2050
|
-
mimeType, mediaSequenceNumber, segment, variables, startTime, type
|
|
2059
|
+
mimeType, mediaSequenceNumber, segment, variables, startTime, type,
|
|
2060
|
+
codecs, bandwidth) {
|
|
2051
2061
|
let timestampOffset = 0;
|
|
2052
2062
|
if (this.discontinuityToTso_.has(discontintuitySequenceNum)) {
|
|
2053
2063
|
timestampOffset =
|
|
@@ -2056,7 +2066,7 @@ shaka.hls.HlsParser = class {
|
|
|
2056
2066
|
const mediaStartTime = await this.getStartTime_(
|
|
2057
2067
|
verbatimMediaPlaylistUri, initSegmentRef, mimeType,
|
|
2058
2068
|
mediaSequenceNumber, /* isDiscontinuity= */ true, segment,
|
|
2059
|
-
variables, type);
|
|
2069
|
+
variables, type, codecs, bandwidth);
|
|
2060
2070
|
timestampOffset = startTime - mediaStartTime;
|
|
2061
2071
|
shaka.log.v1('Segment timestampOffset =', timestampOffset);
|
|
2062
2072
|
this.discontinuityToTso_.set(
|
|
@@ -2070,10 +2080,14 @@ shaka.hls.HlsParser = class {
|
|
|
2070
2080
|
* segment if we have to.
|
|
2071
2081
|
*
|
|
2072
2082
|
* @param {!shaka.media.AnySegmentReference} reference
|
|
2083
|
+
* @param {string} type
|
|
2084
|
+
* @param {string} mimeType
|
|
2085
|
+
* @param {string} codecs
|
|
2086
|
+
* @param {number|undefined} bandwidth
|
|
2073
2087
|
* @return {!Promise.<shaka.extern.Response>}
|
|
2074
2088
|
* @private
|
|
2075
2089
|
*/
|
|
2076
|
-
async fetchStartOfSegment_(reference) {
|
|
2090
|
+
async fetchStartOfSegment_(reference, type, mimeType, codecs, bandwidth) {
|
|
2077
2091
|
const requestType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
|
|
2078
2092
|
|
|
2079
2093
|
// Create two requests:
|
|
@@ -2087,6 +2101,20 @@ shaka.hls.HlsParser = class {
|
|
|
2087
2101
|
reference.endByte,
|
|
2088
2102
|
this.config_.retryParameters);
|
|
2089
2103
|
|
|
2104
|
+
// We can only add partial CMCD data here because the stream
|
|
2105
|
+
// and manifest objects are still being created
|
|
2106
|
+
this.playerInterface_.modifySegmentRequest(
|
|
2107
|
+
fullRequest,
|
|
2108
|
+
{
|
|
2109
|
+
type: type,
|
|
2110
|
+
init: reference instanceof shaka.media.InitSegmentReference,
|
|
2111
|
+
duration: reference.endTime - reference.startTime,
|
|
2112
|
+
mimeType: mimeType,
|
|
2113
|
+
codecs: codecs,
|
|
2114
|
+
bandwidth: bandwidth,
|
|
2115
|
+
},
|
|
2116
|
+
);
|
|
2117
|
+
|
|
2090
2118
|
if (this.config_.hls.useFullSegmentsForStartTime) {
|
|
2091
2119
|
return this.makeNetworkRequest_(fullRequest, requestType);
|
|
2092
2120
|
}
|
|
@@ -2097,6 +2125,18 @@ shaka.hls.HlsParser = class {
|
|
|
2097
2125
|
reference.startByte + shaka.hls.HlsParser.START_OF_SEGMENT_SIZE_ - 1,
|
|
2098
2126
|
this.config_.retryParameters);
|
|
2099
2127
|
|
|
2128
|
+
this.playerInterface_.modifySegmentRequest(
|
|
2129
|
+
partialRequest,
|
|
2130
|
+
{
|
|
2131
|
+
type: type,
|
|
2132
|
+
init: reference instanceof shaka.media.InitSegmentReference,
|
|
2133
|
+
duration: reference.endTime - reference.startTime,
|
|
2134
|
+
mimeType: mimeType,
|
|
2135
|
+
codecs: codecs,
|
|
2136
|
+
bandwidth: bandwidth,
|
|
2137
|
+
},
|
|
2138
|
+
);
|
|
2139
|
+
|
|
2100
2140
|
// TODO(vaage): The need to do fall back requests is not likely to be unique
|
|
2101
2141
|
// to here. It would be nice if the fallback(s) could be included into
|
|
2102
2142
|
// the same abortable operation as the original request.
|
|
@@ -2144,12 +2184,14 @@ shaka.hls.HlsParser = class {
|
|
|
2144
2184
|
* @param {!shaka.hls.Segment} segment
|
|
2145
2185
|
* @param {!Map.<string, string>} variables
|
|
2146
2186
|
* @param {string} type
|
|
2187
|
+
* @param {string} codecs
|
|
2188
|
+
* @param {number|undefined} bandwidth
|
|
2147
2189
|
* @return {!Promise.<number>}
|
|
2148
2190
|
* @private
|
|
2149
2191
|
*/
|
|
2150
2192
|
async getStartTime_(
|
|
2151
2193
|
verbatimMediaPlaylistUri, initSegmentRef, mimeType, mediaSequenceNumber,
|
|
2152
|
-
isDiscontinuity, segment, variables, type) {
|
|
2194
|
+
isDiscontinuity, segment, variables, type, codecs, bandwidth) {
|
|
2153
2195
|
const segmentRef = this.createSegmentReference_(
|
|
2154
2196
|
initSegmentRef,
|
|
2155
2197
|
/* previousReference= */ null,
|
|
@@ -2217,10 +2259,22 @@ shaka.hls.HlsParser = class {
|
|
|
2217
2259
|
if (mimeType == 'video/mp4' || mimeType == 'audio/mp4') {
|
|
2218
2260
|
// We also need the init segment to get the correct timescale. But if the
|
|
2219
2261
|
// stream is self-initializing, use the same response for both.
|
|
2220
|
-
const fetches = [this.fetchStartOfSegment_(
|
|
2262
|
+
const fetches = [this.fetchStartOfSegment_(
|
|
2263
|
+
segmentRef,
|
|
2264
|
+
type,
|
|
2265
|
+
mimeType,
|
|
2266
|
+
codecs,
|
|
2267
|
+
bandwidth,
|
|
2268
|
+
)];
|
|
2221
2269
|
|
|
2222
2270
|
if (initSegmentRef) {
|
|
2223
|
-
fetches.push(this.fetchStartOfSegment_(
|
|
2271
|
+
fetches.push(this.fetchStartOfSegment_(
|
|
2272
|
+
initSegmentRef,
|
|
2273
|
+
type,
|
|
2274
|
+
mimeType,
|
|
2275
|
+
codecs,
|
|
2276
|
+
bandwidth,
|
|
2277
|
+
));
|
|
2224
2278
|
}
|
|
2225
2279
|
|
|
2226
2280
|
const responses = await Promise.all(fetches);
|
|
@@ -2236,7 +2290,13 @@ shaka.hls.HlsParser = class {
|
|
|
2236
2290
|
}
|
|
2237
2291
|
|
|
2238
2292
|
if (mimeType == 'video/mp2t') {
|
|
2239
|
-
const response = await this.fetchStartOfSegment_(
|
|
2293
|
+
const response = await this.fetchStartOfSegment_(
|
|
2294
|
+
segmentRef,
|
|
2295
|
+
type,
|
|
2296
|
+
mimeType,
|
|
2297
|
+
codecs,
|
|
2298
|
+
bandwidth,
|
|
2299
|
+
);
|
|
2240
2300
|
goog.asserts.assert(response.data, 'Should have a response body!');
|
|
2241
2301
|
return this.getStartTimeFromTsSegment_(
|
|
2242
2302
|
verbatimMediaPlaylistUri, response.uri, response.data);
|
|
@@ -2614,6 +2674,9 @@ shaka.hls.HlsParser = class {
|
|
|
2614
2674
|
const request = shaka.net.NetworkingEngine.makeRequest(
|
|
2615
2675
|
[absoluteUri], this.config_.retryParameters);
|
|
2616
2676
|
|
|
2677
|
+
const format = shaka.util.CmcdManager.StreamingFormat.HLS;
|
|
2678
|
+
this.playerInterface_.modifyManifestRequest(request, {format: format});
|
|
2679
|
+
|
|
2617
2680
|
return this.makeNetworkRequest_(request, requestType);
|
|
2618
2681
|
}
|
|
2619
2682
|
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/*! @license
|
|
2
|
+
* Shaka Player
|
|
3
|
+
* Copyright 2016 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
goog.provide('shaka.media.QualityObserver');
|
|
8
|
+
|
|
9
|
+
goog.require('shaka.media.IPlayheadObserver');
|
|
10
|
+
goog.require('shaka.log');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Monitors the quality of content being appended to the source
|
|
14
|
+
* buffers and fires onQualityChange events when the media quality
|
|
15
|
+
* at the playhead changes.
|
|
16
|
+
*
|
|
17
|
+
* @implements {shaka.media.IPlayheadObserver}
|
|
18
|
+
* @final
|
|
19
|
+
*/
|
|
20
|
+
shaka.media.QualityObserver = class {
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new QualityObserver.
|
|
23
|
+
*
|
|
24
|
+
* @param {!function():!shaka.extern.BufferedInfo} getBufferedInfo
|
|
25
|
+
* Buffered info is needed to purge QualityChanges that are no
|
|
26
|
+
* longer relevant.
|
|
27
|
+
*/
|
|
28
|
+
constructor(getBufferedInfo) {
|
|
29
|
+
/**
|
|
30
|
+
* @private {!Map.<string,!shaka.media.QualityObserver.ContentTypeState>}
|
|
31
|
+
*/
|
|
32
|
+
this.contentTypeStates_ = new Map();
|
|
33
|
+
|
|
34
|
+
/** @private {shaka.media.QualityObserver.EventListener} */
|
|
35
|
+
this.onQualityChange_ = (mediaQuality, position) => {};
|
|
36
|
+
|
|
37
|
+
/** @private function():!shaka.extern.BufferedInfo */
|
|
38
|
+
this.getBufferedInfo_ = getBufferedInfo;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Set all the listeners. This overrides any previous calls to |setListeners|.
|
|
43
|
+
*
|
|
44
|
+
* @param {shaka.media.QualityObserver.EventListener} onQualityChange
|
|
45
|
+
*/
|
|
46
|
+
setListeners(onQualityChange) {
|
|
47
|
+
this.onQualityChange_ = onQualityChange;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @override */
|
|
51
|
+
release() {
|
|
52
|
+
this.contentTypeStates_.clear();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get the ContenTypeState for a contentType, creating a new
|
|
57
|
+
* one if necessary.
|
|
58
|
+
*
|
|
59
|
+
* @param {!string} contentType
|
|
60
|
+
* The contend type e.g. "video" or "audio".
|
|
61
|
+
* @return {!shaka.media.QualityObserver.ContentTypeState}
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
getContentTypeState_(contentType) {
|
|
65
|
+
let contentTypeState = this.contentTypeStates_.get(contentType);
|
|
66
|
+
if (!contentTypeState) {
|
|
67
|
+
contentTypeState = {
|
|
68
|
+
qualityChangePositions: [],
|
|
69
|
+
currentQuality: null,
|
|
70
|
+
contentType: contentType,
|
|
71
|
+
};
|
|
72
|
+
this.contentTypeStates_.set(contentType, contentTypeState);
|
|
73
|
+
}
|
|
74
|
+
return contentTypeState;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Adds a QualityChangePosition for the contentType identified by
|
|
79
|
+
* the mediaQuality.contentType.
|
|
80
|
+
*
|
|
81
|
+
* @param {!shaka.extern.MediaQualityInfo} mediaQuality
|
|
82
|
+
* @param {!number} position
|
|
83
|
+
* Position in seconds of the quality change.
|
|
84
|
+
*/
|
|
85
|
+
addMediaQualityChange(mediaQuality, position) {
|
|
86
|
+
const contentTypeState =
|
|
87
|
+
this.getContentTypeState_(mediaQuality.contentType);
|
|
88
|
+
|
|
89
|
+
// Remove unneeded QualityChangePosition(s) before adding the new one
|
|
90
|
+
this.purgeQualityChangePositions_(contentTypeState);
|
|
91
|
+
|
|
92
|
+
const newChangePosition = {
|
|
93
|
+
mediaQuality: mediaQuality,
|
|
94
|
+
position: position,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const changePositions = contentTypeState.qualityChangePositions;
|
|
98
|
+
const insertBeforeIndex = changePositions.findIndex(
|
|
99
|
+
(qualityChange) => (qualityChange.position >= position));
|
|
100
|
+
|
|
101
|
+
if (insertBeforeIndex >= 0) {
|
|
102
|
+
const duplicatePositions =
|
|
103
|
+
(changePositions[insertBeforeIndex].position == position) ? 1 : 0;
|
|
104
|
+
changePositions.splice(
|
|
105
|
+
insertBeforeIndex, duplicatePositions, newChangePosition);
|
|
106
|
+
} else {
|
|
107
|
+
changePositions.push(newChangePosition);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Determines the media quality at a specific position in the source buffer.
|
|
113
|
+
*
|
|
114
|
+
* @param {!number} position
|
|
115
|
+
* Position in seconds
|
|
116
|
+
* @param {!shaka.media.QualityObserver.ContentTypeState} contentTypeState
|
|
117
|
+
* @return {?shaka.extern.MediaQualityInfo}
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
static getMediaQualityAtPosition_(position, contentTypeState) {
|
|
121
|
+
// The qualityChangePositions must be ordered by position ascending
|
|
122
|
+
// Find the last QualityChangePosition prior to the position
|
|
123
|
+
const changePositions = contentTypeState.qualityChangePositions;
|
|
124
|
+
for (let i = changePositions.length - 1; i >= 0; i--) {
|
|
125
|
+
const qualityChange = changePositions[i];
|
|
126
|
+
if (qualityChange.position <= position) {
|
|
127
|
+
return qualityChange.mediaQuality;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Determines if two MediaQualityInfo objects are the same or not.
|
|
135
|
+
*
|
|
136
|
+
* @param {?shaka.extern.MediaQualityInfo} mq1
|
|
137
|
+
* @param {?shaka.extern.MediaQualityInfo} mq2
|
|
138
|
+
* @return {boolean}
|
|
139
|
+
* @private
|
|
140
|
+
*/
|
|
141
|
+
static mediaQualitiesAreTheSame_(mq1, mq2) {
|
|
142
|
+
if (mq1 === mq2) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (!mq1 || !mq2) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
return (mq1.bandwidth == mq2.bandwidth) &&
|
|
149
|
+
(mq1.audioSamplingRate == mq2.audioSamplingRate) &&
|
|
150
|
+
(mq1.codecs == mq2.codecs) &&
|
|
151
|
+
(mq1.contentType == mq2.contentType) &&
|
|
152
|
+
(mq1.frameRate == mq2.frameRate) &&
|
|
153
|
+
(mq1.height == mq2.height) &&
|
|
154
|
+
(mq1.mimeType == mq2.mimeType) &&
|
|
155
|
+
(mq1.channelsCount == mq2.channelsCount) &&
|
|
156
|
+
(mq1.pixelAspectRatio == mq2.pixelAspectRatio) &&
|
|
157
|
+
(mq1.width == mq2.width);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** @override */
|
|
161
|
+
poll(positionInSeconds, wasSeeking) {
|
|
162
|
+
for (const contentTypeState of this.contentTypeStates_.values()) {
|
|
163
|
+
const qualityAtPosition =
|
|
164
|
+
shaka.media.QualityObserver.getMediaQualityAtPosition_(
|
|
165
|
+
positionInSeconds, contentTypeState);
|
|
166
|
+
if (qualityAtPosition &&
|
|
167
|
+
!shaka.media.QualityObserver.mediaQualitiesAreTheSame_(
|
|
168
|
+
contentTypeState.currentQuality, qualityAtPosition)) {
|
|
169
|
+
if (this.positionIsBuffered_(
|
|
170
|
+
positionInSeconds, qualityAtPosition.contentType)) {
|
|
171
|
+
contentTypeState.currentQuality = qualityAtPosition;
|
|
172
|
+
shaka.log.debug('Media quality changed at position ' +
|
|
173
|
+
positionInSeconds + ' ' + JSON.stringify(qualityAtPosition));
|
|
174
|
+
this.onQualityChange_(
|
|
175
|
+
qualityAtPosition, positionInSeconds);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Determine if a position is buffered for a given content type.
|
|
183
|
+
*
|
|
184
|
+
* @param {!number} position
|
|
185
|
+
* @param {!string} contentType
|
|
186
|
+
* @private
|
|
187
|
+
*/
|
|
188
|
+
positionIsBuffered_(position, contentType) {
|
|
189
|
+
const bufferedInfo = this.getBufferedInfo_();
|
|
190
|
+
const bufferedRanges = bufferedInfo[contentType];
|
|
191
|
+
if (bufferedRanges && bufferedRanges.length > 0) {
|
|
192
|
+
const bufferStart = bufferedRanges[0].start;
|
|
193
|
+
const bufferEnd = bufferedRanges[bufferedRanges.length - 1].end;
|
|
194
|
+
if (position >= bufferStart && position < bufferEnd) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Removes the QualityChangePosition(s) that are not relevant to the buffered
|
|
203
|
+
* content of the specified contentType. Note that this function is
|
|
204
|
+
* invoked just before adding the quality change info associated with
|
|
205
|
+
* the next media segment to be appended.
|
|
206
|
+
*
|
|
207
|
+
* @param {!shaka.media.QualityObserver.ContentTypeState} contentTypeState
|
|
208
|
+
* @private
|
|
209
|
+
*/
|
|
210
|
+
purgeQualityChangePositions_(contentTypeState) {
|
|
211
|
+
const bufferedInfo = this.getBufferedInfo_();
|
|
212
|
+
const bufferedRanges = bufferedInfo[contentTypeState.contentType];
|
|
213
|
+
|
|
214
|
+
if (bufferedRanges && bufferedRanges.length > 0) {
|
|
215
|
+
const bufferStart = bufferedRanges[0].start;
|
|
216
|
+
const bufferEnd = bufferedRanges[bufferedRanges.length - 1].end;
|
|
217
|
+
const oldChangePositions = contentTypeState.qualityChangePositions;
|
|
218
|
+
contentTypeState.qualityChangePositions =
|
|
219
|
+
oldChangePositions.filter(
|
|
220
|
+
(qualityChange, index) => {
|
|
221
|
+
// Remove all but last quality change before bufferStart.
|
|
222
|
+
if ((qualityChange.position <= bufferStart) &&
|
|
223
|
+
(index + 1 < oldChangePositions.length) &&
|
|
224
|
+
(oldChangePositions[index + 1].position <= bufferStart)) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
// Remove all quality changes after bufferEnd.
|
|
228
|
+
if (qualityChange.position >= bufferEnd) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
// Nothing is buffered; so remove all quality changes.
|
|
235
|
+
contentTypeState.qualityChangePositions = [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* @typedef {function(shaka.extern.MediaQualityInfo, number)}
|
|
242
|
+
*
|
|
243
|
+
* @description
|
|
244
|
+
* A callback function used to notify the player when media quality changes
|
|
245
|
+
* are detected at the playhead.
|
|
246
|
+
*
|
|
247
|
+
* The first argument is information about media quality at the playhead
|
|
248
|
+
* position.
|
|
249
|
+
*
|
|
250
|
+
* The second argument is the playhead position in seconds.
|
|
251
|
+
*/
|
|
252
|
+
shaka.media.QualityObserver.EventListener;
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @typedef {{
|
|
256
|
+
* mediaQuality: !shaka.extern.MediaQualityInfo,
|
|
257
|
+
* position: !number
|
|
258
|
+
* }}
|
|
259
|
+
*
|
|
260
|
+
* @description
|
|
261
|
+
* Identifies the position of a media quality change in the
|
|
262
|
+
* source buffer.
|
|
263
|
+
*
|
|
264
|
+
* @property {shaka.extern.MediaQualityInfo} !mediaQuality
|
|
265
|
+
* The new media quality for content after position in the source buffer.
|
|
266
|
+
* @property {number} !position
|
|
267
|
+
* A position in seconds in the source buffer
|
|
268
|
+
*/
|
|
269
|
+
shaka.media.QualityObserver.QualityChangePosition;
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @typedef {{
|
|
273
|
+
* qualityChangePositions:
|
|
274
|
+
* !Array.<shaka.media.QualityObserver.QualityChangePosition>,
|
|
275
|
+
* currentQuality: ?shaka.extern.MediaQualityInfo,
|
|
276
|
+
* contentType: !string
|
|
277
|
+
* }}
|
|
278
|
+
*
|
|
279
|
+
* @description
|
|
280
|
+
* Contains media quality information for a specific content type
|
|
281
|
+
* e.g video or audio.
|
|
282
|
+
*
|
|
283
|
+
* @property {!Array.<shaka.media.QualityObserver.QualityChangePosition>}
|
|
284
|
+
* qualityChangePositions
|
|
285
|
+
* Quality changes ordered by position ascending.
|
|
286
|
+
* @property {?shaka.media.MediaQualityInfo} currentMediaQuality
|
|
287
|
+
* The media quality at the playhead position.
|
|
288
|
+
* @property {string} contentType
|
|
289
|
+
* The contentType e.g. 'video' or 'audio'
|
|
290
|
+
*/
|
|
291
|
+
shaka.media.QualityObserver.ContentTypeState;
|
|
@@ -427,6 +427,12 @@ shaka.media.SegmentIndex = class {
|
|
|
427
427
|
return new shaka.media.SegmentIterator(this, index, partialSegmentIndex);
|
|
428
428
|
}
|
|
429
429
|
|
|
430
|
+
/**
|
|
431
|
+
* @return {boolean}
|
|
432
|
+
*/
|
|
433
|
+
isEmpty() {
|
|
434
|
+
return this.references.length == 0;
|
|
435
|
+
}
|
|
430
436
|
|
|
431
437
|
/**
|
|
432
438
|
* Create a SegmentIndex for a single segment of the given start time and
|