shaka-player 4.2.1 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/build-and-test.yaml +42 -22
- package/.github/workflows/compute-incremental-coverage.py +14 -3
- package/.github/workflows/custom-actions/set-commit-status/action.yaml +1 -5
- package/.github/workflows/deflake.yaml +28 -0
- package/.github/workflows/demo-version-index.yaml +1 -1
- package/.github/workflows/nightly-demo.yaml +1 -1
- package/.github/workflows/release-please.yaml +5 -5
- package/.github/workflows/report-incremental-coverage.yaml +1 -1
- package/.github/workflows/selenium-lab-tests.yaml +21 -10
- package/AUTHORS +4 -0
- package/CHANGELOG.md +36 -0
- package/CONTRIBUTORS +4 -0
- package/demo/common/message_ids.js +1 -0
- package/demo/config.js +3 -1
- package/demo/locales/en.json +1 -0
- package/demo/locales/source.json +4 -0
- package/dist/demo.compiled.debug.js +7 -6
- package/dist/demo.compiled.debug.map +3 -3
- package/dist/demo.compiled.js +7 -6
- package/dist/demo.compiled.map +3 -3
- package/dist/deps.js +4 -4
- package/dist/locales.js +32 -32
- package/dist/receiver.compiled.debug.js +4 -3
- package/dist/receiver.compiled.debug.map +3 -3
- package/dist/receiver.compiled.js +4 -3
- package/dist/receiver.compiled.map +3 -3
- package/dist/shaka-player.compiled.d.ts +3 -2
- package/dist/shaka-player.compiled.debug.d.ts +3 -2
- package/dist/shaka-player.compiled.debug.externs.js +25 -24
- package/dist/shaka-player.compiled.debug.js +268 -259
- package/dist/shaka-player.compiled.debug.map +4 -4
- package/dist/shaka-player.compiled.externs.js +25 -24
- package/dist/shaka-player.compiled.js +749 -746
- package/dist/shaka-player.compiled.map +4 -4
- package/dist/shaka-player.ui.d.ts +3 -2
- package/dist/shaka-player.ui.debug.d.ts +3 -2
- package/dist/shaka-player.ui.debug.externs.js +25 -24
- package/dist/shaka-player.ui.debug.js +243 -234
- package/dist/shaka-player.ui.debug.map +4 -4
- package/dist/shaka-player.ui.externs.js +25 -24
- package/dist/shaka-player.ui.js +908 -904
- package/dist/shaka-player.ui.map +4 -4
- package/docs/jsdoc-template/publish.js +33 -0
- package/docs/jsdoc-template/tmpl/container.tmpl +4 -1
- package/docs/jsdoc.conf.json +1 -0
- package/docs/tutorials/welcome.md +2 -2
- package/externs/shaka/manifest_parser.js +4 -1
- package/externs/shaka/player.js +7 -1
- package/lib/ads/client_side_ad_manager.js +12 -2
- package/lib/cast/cast_sender.js +39 -14
- package/lib/cea/cea608_memory.js +4 -0
- package/lib/hls/hls_parser.js +48 -15
- package/lib/media/drm_engine.js +59 -7
- package/lib/media/media_source_engine.js +43 -15
- package/lib/media/playhead.js +1 -1
- package/lib/media/segment_index.js +4 -1
- package/lib/media/streaming_engine.js +79 -14
- package/lib/offline/indexeddb/storage_mechanism.js +24 -0
- package/lib/offline/storage.js +1 -0
- package/lib/player.js +17 -5
- package/lib/polyfill/media_capabilities.js +7 -6
- package/lib/text/ttml_text_parser.js +69 -87
- package/lib/text/vtt_text_parser.js +1 -1
- package/lib/util/error.js +7 -0
- package/lib/util/periods.js +9 -9
- package/lib/util/platform.js +19 -5
- package/lib/util/player_configuration.js +4 -0
- package/lib/util/pssh.js +2 -1
- package/lib/util/stream_utils.js +2 -1
- package/lib/util/xml_utils.js +45 -6
- package/package.json +1 -1
- package/test/cea/cea608_memory_unit.js +88 -0
- package/test/dash/dash_parser_content_protection_unit.js +1 -0
- package/test/dash/dash_parser_live_unit.js +1 -0
- package/test/dash/dash_parser_manifest_unit.js +1 -0
- package/test/dash/dash_parser_segment_base_unit.js +1 -0
- package/test/dash/dash_parser_segment_list_unit.js +1 -0
- package/test/dash/dash_parser_segment_template_unit.js +1 -0
- package/test/hls/hls_live_unit.js +169 -247
- package/test/hls/hls_parser_unit.js +60 -2
- package/test/media/drm_engine_unit.js +61 -0
- package/test/media/media_source_engine_integration.js +36 -0
- package/test/media/media_source_engine_unit.js +118 -1
- package/test/media/segment_index_unit.js +28 -2
- package/test/media/streaming_engine_integration.js +9 -7
- package/test/media/streaming_engine_unit.js +29 -0
- package/test/offline/offline_integration.js +3 -3
- package/test/offline/storage_integration.js +30 -0
- package/test/player_integration.js +2 -2
- package/test/test/util/dash_parser_util.js +2 -0
- package/test/test/util/ttml_utils.js +0 -2
- package/test/text/ttml_text_parser_unit.js +46 -0
- package/test/text/vtt_text_parser_unit.js +23 -1
- package/test/ui/text_displayer_layout_unit.js +17 -2
- package/test/util/periods_unit.js +149 -11
- package/test/util/xml_utils_unit.js +64 -0
- package/ui/controls.js +0 -7
|
@@ -440,6 +440,7 @@ function buildNav(members) {
|
|
|
440
440
|
[members.externals, 'Externals', seen, linktoExternal],
|
|
441
441
|
[members.namespaces, 'Namespaces', seen, linkto],
|
|
442
442
|
[members.classes, 'Classes', seen, linkto],
|
|
443
|
+
[members.enums, 'Enums', seen, linkto],
|
|
443
444
|
[members.interfaces, 'Interfaces', seen, linkto],
|
|
444
445
|
[members.events, 'Events', seen, linkto],
|
|
445
446
|
[members.mixins, 'Mixins', seen, linkto],
|
|
@@ -484,6 +485,7 @@ function fixUpType(type) {
|
|
|
484
485
|
exports.publish = (taffyData, opts, tutorials) => {
|
|
485
486
|
let classes;
|
|
486
487
|
let conf;
|
|
488
|
+
let enums;
|
|
487
489
|
let externals;
|
|
488
490
|
let files;
|
|
489
491
|
let fromDir;
|
|
@@ -733,6 +735,31 @@ exports.publish = (taffyData, opts, tutorials) => {
|
|
|
733
735
|
});
|
|
734
736
|
|
|
735
737
|
members = helper.getMembers(data);
|
|
738
|
+
members.enums = [];
|
|
739
|
+
|
|
740
|
+
data().each(doclet => {
|
|
741
|
+
if (doclet.isEnum) {
|
|
742
|
+
const hasParent = !!docletMap[doclet.memberof];
|
|
743
|
+
|
|
744
|
+
// Enums without parents (not attached to a class, but directly to a
|
|
745
|
+
// namespace) aren't recognized by jsdoc by default and need to be
|
|
746
|
+
// massaged into the docs separately.
|
|
747
|
+
if (!hasParent) {
|
|
748
|
+
// Temporarily call this a "class", so that we can fix the URL.
|
|
749
|
+
doclet.kind = 'class';
|
|
750
|
+
// Regenerate the URL, so that this enum gets its own page
|
|
751
|
+
// instead of a URL that links to an ID within a non-existent
|
|
752
|
+
// parent page.
|
|
753
|
+
delete helper.longnameToUrl[doclet.longname];
|
|
754
|
+
helper.createLink(doclet);
|
|
755
|
+
// Mark this as an enum (custom type), so that it doesn't get
|
|
756
|
+
// slotted with the classes.
|
|
757
|
+
doclet.kind = 'enum';
|
|
758
|
+
members.enums.push(doclet);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
|
|
736
763
|
members.tutorials = tutorials.children;
|
|
737
764
|
if (conf.default.sortTutorialsByConfigOrder) {
|
|
738
765
|
members.tutorials.sort((a, b) => {
|
|
@@ -778,6 +805,7 @@ exports.publish = (taffyData, opts, tutorials) => {
|
|
|
778
805
|
|
|
779
806
|
// set up the lists that we'll use to generate pages
|
|
780
807
|
classes = taffy(members.classes);
|
|
808
|
+
enums = taffy(members.enums);
|
|
781
809
|
modules = taffy(members.modules);
|
|
782
810
|
namespaces = taffy(members.namespaces);
|
|
783
811
|
mixins = taffy(members.mixins);
|
|
@@ -786,6 +814,7 @@ exports.publish = (taffyData, opts, tutorials) => {
|
|
|
786
814
|
|
|
787
815
|
Object.keys(helper.longnameToUrl).forEach(longname => {
|
|
788
816
|
const myClasses = helper.find(classes, {longname: longname});
|
|
817
|
+
const myEnums = helper.find(enums, {longname: longname});
|
|
789
818
|
const myExternals = helper.find(externals, {longname: longname});
|
|
790
819
|
const myInterfaces = helper.find(interfaces, {longname: longname});
|
|
791
820
|
const myMixins = helper.find(mixins, {longname: longname});
|
|
@@ -800,6 +829,10 @@ exports.publish = (taffyData, opts, tutorials) => {
|
|
|
800
829
|
generate(`Class: ${myClasses[0].longname}`, myClasses, helper.longnameToUrl[longname]);
|
|
801
830
|
}
|
|
802
831
|
|
|
832
|
+
if (myEnums.length) {
|
|
833
|
+
generate(`Enum: ${myEnums[0].longname}`, myEnums, helper.longnameToUrl[longname]);
|
|
834
|
+
}
|
|
835
|
+
|
|
803
836
|
if (myNamespaces.length) {
|
|
804
837
|
generate(`Namespace: ${myNamespaces[0].longname}`, myNamespaces, helper.longnameToUrl[longname]);
|
|
805
838
|
}
|
|
@@ -144,7 +144,10 @@
|
|
|
144
144
|
return m.longname && m.longname.indexOf('module:') !== 0;
|
|
145
145
|
});
|
|
146
146
|
}
|
|
147
|
-
|
|
147
|
+
|
|
148
|
+
// Skip enums, since they already have their values listed under
|
|
149
|
+
// "Properties".
|
|
150
|
+
if (members && members.length && members.forEach && doc.kind != 'enum') {
|
|
148
151
|
?>
|
|
149
152
|
<h3 class="subsection-title">Members</h3>
|
|
150
153
|
|
package/docs/jsdoc.conf.json
CHANGED
|
@@ -66,8 +66,8 @@ python build/all.py
|
|
|
66
66
|
|
|
67
67
|
Alternatively you can use a docker container:
|
|
68
68
|
```sh
|
|
69
|
-
|
|
70
|
-
docker
|
|
69
|
+
docker build -t shaka-player-build build/docker
|
|
70
|
+
docker run -v $(pwd):/usr/src --user $(id -u):$(id -g) shaka-player-build
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
The output is:
|
|
@@ -111,7 +111,8 @@ shaka.extern.ManifestParser = class {
|
|
|
111
111
|
* onError: function(!shaka.util.Error),
|
|
112
112
|
* isLowLatencyMode: function():boolean,
|
|
113
113
|
* isAutoLowLatencyMode: function():boolean,
|
|
114
|
-
* enableLowLatencyMode: function()
|
|
114
|
+
* enableLowLatencyMode: function(),
|
|
115
|
+
* updateDuration: function()
|
|
115
116
|
* }}
|
|
116
117
|
*
|
|
117
118
|
* @description
|
|
@@ -147,6 +148,8 @@ shaka.extern.ManifestParser = class {
|
|
|
147
148
|
* Return true if auto low latency streaming mode is enabled.
|
|
148
149
|
* @property {function()} enableLowLatencyMode
|
|
149
150
|
* Enable low latency streaming mode.
|
|
151
|
+
* @property {function()} updateDuration
|
|
152
|
+
* Update the presentation duration based on PresentationTimeline.
|
|
150
153
|
* @exportDoc
|
|
151
154
|
*/
|
|
152
155
|
shaka.extern.ManifestParser.PlayerInterface;
|
package/externs/shaka/player.js
CHANGED
|
@@ -632,7 +632,8 @@ shaka.extern.AdvancedDrmConfiguration;
|
|
|
632
632
|
* logLicenseExchange: boolean,
|
|
633
633
|
* updateExpirationTime: number,
|
|
634
634
|
* preferredKeySystems: !Array.<string>,
|
|
635
|
-
* keySystemsMapping: !Object.<string, string
|
|
635
|
+
* keySystemsMapping: !Object.<string, string>,
|
|
636
|
+
* parseInbandPsshEnabled: boolean
|
|
636
637
|
* }}
|
|
637
638
|
*
|
|
638
639
|
* @property {shaka.extern.RetryParameters} retryParameters
|
|
@@ -675,6 +676,11 @@ shaka.extern.AdvancedDrmConfiguration;
|
|
|
675
676
|
* Specifies the priorties of available DRM key systems.
|
|
676
677
|
* @property {Object.<string, string>} keySystemsMapping
|
|
677
678
|
* A map of key system name to key system name.
|
|
679
|
+
* @property {boolean} parseInbandPsshEnabled
|
|
680
|
+
* <i>Defaults to true on Xbox One, and false for all other browsers.</i><br>
|
|
681
|
+
* When true parse DRM init data from pssh boxes in media and init segments
|
|
682
|
+
* and ignore 'encrypted' events.
|
|
683
|
+
* This is required when using in-band key rotation on Xbox One.
|
|
678
684
|
*
|
|
679
685
|
* @exportDoc
|
|
680
686
|
*/
|
|
@@ -15,7 +15,6 @@ goog.require('goog.asserts');
|
|
|
15
15
|
goog.require('shaka.ads.ClientSideAd');
|
|
16
16
|
goog.require('shaka.util.IReleasable');
|
|
17
17
|
|
|
18
|
-
|
|
19
18
|
/**
|
|
20
19
|
* A class responsible for client-side ad interactions.
|
|
21
20
|
* @implements {shaka.util.IReleasable}
|
|
@@ -205,13 +204,24 @@ shaka.ads.ClientSideAdManager = class {
|
|
|
205
204
|
this.video_.offsetHeight, viewMode);
|
|
206
205
|
});
|
|
207
206
|
this.resizeObserver_.observe(this.video_);
|
|
207
|
+
} else {
|
|
208
|
+
this.eventManager_.listen(document, 'fullscreenchange', () => {
|
|
209
|
+
const viewMode = this.isFullScreenEnabled_() ?
|
|
210
|
+
google.ima.ViewMode.FULLSCREEN : google.ima.ViewMode.NORMAL;
|
|
211
|
+
this.imaAdsManager_.resize(this.video_.offsetWidth,
|
|
212
|
+
this.video_.offsetHeight, viewMode);
|
|
213
|
+
});
|
|
208
214
|
}
|
|
209
215
|
|
|
210
216
|
// Single video and overlay ads will start at this time
|
|
211
217
|
// TODO (ismena): Need a better inderstanding of what this does.
|
|
212
218
|
// The docs say it's called to 'start playing the ads,' but I haven't
|
|
213
219
|
// seen the ads actually play until requestAds() is called.
|
|
214
|
-
|
|
220
|
+
// Note: We listen for a play event to avoid autoplay issues that might
|
|
221
|
+
// crash IMA.
|
|
222
|
+
this.video_.addEventListener('play', () => {
|
|
223
|
+
this.imaAdsManager_.start();
|
|
224
|
+
}, {once: true});
|
|
215
225
|
} catch (adError) {
|
|
216
226
|
// If there was a problem with the VAST response,
|
|
217
227
|
// we we won't be getting an ad. Hide ad UI if we showed it already
|
package/lib/cast/cast_sender.js
CHANGED
|
@@ -192,21 +192,14 @@ shaka.cast.CastSender = class {
|
|
|
192
192
|
// If the API becomes available before this instance dies, init will be
|
|
193
193
|
// called again.
|
|
194
194
|
|
|
195
|
-
//
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// be overwritten.
|
|
202
|
-
if (!window.__onGCastApiAvailable) {
|
|
203
|
-
window.__onGCastApiAvailable = shaka.cast.CastSender.onSdkLoaded_;
|
|
204
|
-
}
|
|
205
|
-
if (window.__onGCastApiAvailable != shaka.cast.CastSender.onSdkLoaded_) {
|
|
206
|
-
shaka.log.alwaysWarn('A global Cast SDK hook is already installed! ' +
|
|
207
|
-
'Shaka Player will be unable to receive a notification when the ' +
|
|
208
|
-
'Cast SDK is ready.');
|
|
195
|
+
// Check if our callback is already installed.
|
|
196
|
+
if (window.__onGCastApiAvailable !== CastSender.onGCastApiAvailable_) {
|
|
197
|
+
// Save pre-existing __onGCastApiAvailable in order to restore later.
|
|
198
|
+
CastSender.__onGCastApiAvailable_ =
|
|
199
|
+
window.__onGCastApiAvailable || null;
|
|
200
|
+
window.__onGCastApiAvailable = CastSender.onGCastApiAvailable_;
|
|
209
201
|
}
|
|
202
|
+
|
|
210
203
|
return;
|
|
211
204
|
}
|
|
212
205
|
|
|
@@ -757,6 +750,9 @@ shaka.cast.CastSender.hasReceivers_ = false;
|
|
|
757
750
|
/** @private {chrome.cast.Session} */
|
|
758
751
|
shaka.cast.CastSender.session_ = null;
|
|
759
752
|
|
|
753
|
+
/** @private {?function(boolean)} */
|
|
754
|
+
shaka.cast.CastSender.__onGCastApiAvailable_ = null;
|
|
755
|
+
|
|
760
756
|
/**
|
|
761
757
|
* A set of all living CastSender instances. The constructor and destroy
|
|
762
758
|
* methods will add and remove instances from this set.
|
|
@@ -784,3 +780,32 @@ shaka.cast.CastSender.onSdkLoaded_ = (loaded) => {
|
|
|
784
780
|
}
|
|
785
781
|
}
|
|
786
782
|
};
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* @param {boolean} available
|
|
786
|
+
* @private
|
|
787
|
+
*/
|
|
788
|
+
shaka.cast.CastSender.onGCastApiAvailable_ = (available) => {
|
|
789
|
+
// Restore callback from saved.
|
|
790
|
+
if (shaka.cast.CastSender.__onGCastApiAvailable_) {
|
|
791
|
+
window.__onGCastApiAvailable =
|
|
792
|
+
shaka.cast.CastSender.__onGCastApiAvailable_;
|
|
793
|
+
} else {
|
|
794
|
+
delete window.__onGCastApiAvailable;
|
|
795
|
+
}
|
|
796
|
+
shaka.cast.CastSender.__onGCastApiAvailable_ = null;
|
|
797
|
+
|
|
798
|
+
// A note about this value: In our testing environment, we load both
|
|
799
|
+
// uncompiled and compiled code. This global callback in uncompiled mode
|
|
800
|
+
// can be overwritten by the same in compiled mode. The two versions will
|
|
801
|
+
// each have their own instances_ map. Therefore the callback must have a
|
|
802
|
+
// name, as opposed to being anonymous. This way, the CastSender tests
|
|
803
|
+
// can invoke the named static method instead of using a global that could
|
|
804
|
+
// be overwritten.
|
|
805
|
+
shaka.cast.CastSender.onSdkLoaded_(available);
|
|
806
|
+
|
|
807
|
+
// call restored callback (if any)
|
|
808
|
+
if (typeof window.__onGCastApiAvailable === 'function') {
|
|
809
|
+
window.__onGCastApiAvailable(available);
|
|
810
|
+
}
|
|
811
|
+
};
|
package/lib/cea/cea608_memory.js
CHANGED
|
@@ -184,6 +184,10 @@ shaka.cea.Cea608Memory = class {
|
|
|
184
184
|
* @param {number} count Count of rows to move.
|
|
185
185
|
*/
|
|
186
186
|
moveRows(dst, src, count) {
|
|
187
|
+
if (src < 0 || dst < 0) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
187
191
|
if (dst >= src) {
|
|
188
192
|
for (let i = count-1; i >= 0; i--) {
|
|
189
193
|
this.rows_[dst + i] = this.rows_[src + i].map((e) => e);
|
package/lib/hls/hls_parser.js
CHANGED
|
@@ -284,6 +284,21 @@ shaka.hls.HlsParser = class {
|
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
await Promise.all(updates);
|
|
287
|
+
|
|
288
|
+
this.notifySegments_();
|
|
289
|
+
|
|
290
|
+
// If any hasEndList is false, the stream is still live.
|
|
291
|
+
const stillLive = streamInfos.some((s) => s.hasEndList == false);
|
|
292
|
+
if (!stillLive) {
|
|
293
|
+
// Convert the presentation to VOD and set the duration.
|
|
294
|
+
const PresentationType = shaka.hls.HlsParser.PresentationType_;
|
|
295
|
+
this.setPresentationType_(PresentationType.VOD);
|
|
296
|
+
|
|
297
|
+
const maxTimestamps = streamInfos.map((s) => s.maxTimestamp);
|
|
298
|
+
// The duration is the minimum of the end times of all streams.
|
|
299
|
+
this.presentationTimeline_.setDuration(Math.min(...maxTimestamps));
|
|
300
|
+
this.playerInterface_.updateDuration();
|
|
301
|
+
}
|
|
287
302
|
}
|
|
288
303
|
|
|
289
304
|
/**
|
|
@@ -294,7 +309,6 @@ shaka.hls.HlsParser = class {
|
|
|
294
309
|
* @private
|
|
295
310
|
*/
|
|
296
311
|
async updateStream_(streamInfo) {
|
|
297
|
-
const PresentationType = shaka.hls.HlsParser.PresentationType_;
|
|
298
312
|
const manifestUri = streamInfo.absoluteMediaPlaylistUri;
|
|
299
313
|
const uriObj = new goog.Uri(manifestUri);
|
|
300
314
|
if (this.lowLatencyMode_ && streamInfo.canSkipSegments) {
|
|
@@ -327,6 +341,7 @@ shaka.hls.HlsParser = class {
|
|
|
327
341
|
streamInfo.verbatimMediaPlaylistUri, playlist, stream.type,
|
|
328
342
|
stream.mimeType, streamInfo.mediaSequenceToStartTime, mediaVariables,
|
|
329
343
|
stream.codecs);
|
|
344
|
+
this.segmentsToNotifyByStream_.push(segments);
|
|
330
345
|
|
|
331
346
|
stream.segmentIndex.mergeAndEvict(
|
|
332
347
|
segments, this.presentationTimeline_.getSegmentAvailabilityStart());
|
|
@@ -347,10 +362,10 @@ shaka.hls.HlsParser = class {
|
|
|
347
362
|
shaka.hls.Utils.getFirstTagWithName(playlist.tags, 'EXT-X-ENDLIST');
|
|
348
363
|
|
|
349
364
|
if (endListTag) {
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
|
|
353
|
-
|
|
365
|
+
// Flag this for later. We don't convert the whole presentation into VOD
|
|
366
|
+
// until we've seen the ENDLIST tag for all active playlists.
|
|
367
|
+
streamInfo.hasEndList = true;
|
|
368
|
+
streamInfo.maxTimestamp = newestSegment.endTime;
|
|
354
369
|
}
|
|
355
370
|
}
|
|
356
371
|
|
|
@@ -1707,6 +1722,7 @@ shaka.hls.HlsParser = class {
|
|
|
1707
1722
|
maxTimestamp: lastEndTime,
|
|
1708
1723
|
mediaSequenceToStartTime,
|
|
1709
1724
|
canSkipSegments,
|
|
1725
|
+
hasEndList: false,
|
|
1710
1726
|
};
|
|
1711
1727
|
}
|
|
1712
1728
|
|
|
@@ -2426,7 +2442,12 @@ shaka.hls.HlsParser = class {
|
|
|
2426
2442
|
const extension = parsedUri.getPath().split('.').pop();
|
|
2427
2443
|
const map = HlsParser.EXTENSION_MAP_BY_CONTENT_TYPE_[contentType];
|
|
2428
2444
|
|
|
2429
|
-
|
|
2445
|
+
let mimeType = map[extension];
|
|
2446
|
+
if (mimeType) {
|
|
2447
|
+
return mimeType;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
mimeType = HlsParser.RAW_FORMATS_TO_MIME_TYPES_[extension];
|
|
2430
2451
|
if (mimeType) {
|
|
2431
2452
|
return mimeType;
|
|
2432
2453
|
}
|
|
@@ -2557,8 +2578,11 @@ shaka.hls.HlsParser = class {
|
|
|
2557
2578
|
try {
|
|
2558
2579
|
await this.update();
|
|
2559
2580
|
|
|
2560
|
-
|
|
2561
|
-
this.
|
|
2581
|
+
// This may have converted to VOD, in which case we stop updating.
|
|
2582
|
+
if (this.isLive_()) {
|
|
2583
|
+
const delay = this.updatePlaylistDelay_;
|
|
2584
|
+
this.updatePlaylistTimer_.tickAfter(/* seconds= */ delay);
|
|
2585
|
+
}
|
|
2562
2586
|
} catch (error) {
|
|
2563
2587
|
// Detect a call to stop() during this.update()
|
|
2564
2588
|
if (!this.playerInterface_) {
|
|
@@ -2749,7 +2773,8 @@ shaka.hls.HlsParser = class {
|
|
|
2749
2773
|
* absoluteMediaPlaylistUri: string,
|
|
2750
2774
|
* maxTimestamp: number,
|
|
2751
2775
|
* mediaSequenceToStartTime: !Map.<number, number>,
|
|
2752
|
-
* canSkipSegments: boolean
|
|
2776
|
+
* canSkipSegments: boolean,
|
|
2777
|
+
* hasEndList: boolean
|
|
2753
2778
|
* }}
|
|
2754
2779
|
*
|
|
2755
2780
|
* @description
|
|
@@ -2772,6 +2797,8 @@ shaka.hls.HlsParser = class {
|
|
|
2772
2797
|
* @property {boolean} canSkipSegments
|
|
2773
2798
|
* True if the server supports delta playlist updates, and we can send a
|
|
2774
2799
|
* request for a playlist that can skip older media segments.
|
|
2800
|
+
* @property {boolean} hasEndList
|
|
2801
|
+
* True if the stream has an EXT-X-ENDLIST tag.
|
|
2775
2802
|
*/
|
|
2776
2803
|
shaka.hls.HlsParser.StreamInfo;
|
|
2777
2804
|
|
|
@@ -2789,6 +2816,18 @@ shaka.hls.HlsParser.StreamInfo;
|
|
|
2789
2816
|
shaka.hls.HlsParser.StreamInfos;
|
|
2790
2817
|
|
|
2791
2818
|
|
|
2819
|
+
/**
|
|
2820
|
+
* @const {!Object.<string, string>}
|
|
2821
|
+
* @private
|
|
2822
|
+
*/
|
|
2823
|
+
shaka.hls.HlsParser.RAW_FORMATS_TO_MIME_TYPES_ = {
|
|
2824
|
+
'aac': 'audio/aac',
|
|
2825
|
+
'ac3': 'audio/ac3',
|
|
2826
|
+
'ec3': 'audio/ec3',
|
|
2827
|
+
'mp3': 'audio/mpeg',
|
|
2828
|
+
};
|
|
2829
|
+
|
|
2830
|
+
|
|
2792
2831
|
/**
|
|
2793
2832
|
* @const {!Object.<string, string>}
|
|
2794
2833
|
* @private
|
|
@@ -2803,12 +2842,6 @@ shaka.hls.HlsParser.AUDIO_EXTENSIONS_TO_MIME_TYPES_ = {
|
|
|
2803
2842
|
'cmfa': 'audio/mp4',
|
|
2804
2843
|
// MPEG2-TS also uses video/ for audio: https://bit.ly/TsMse
|
|
2805
2844
|
'ts': 'video/mp2t',
|
|
2806
|
-
|
|
2807
|
-
// Raw formats:
|
|
2808
|
-
'aac': 'audio/aac',
|
|
2809
|
-
'ac3': 'audio/ac3',
|
|
2810
|
-
'ec3': 'audio/ec3',
|
|
2811
|
-
'mp3': 'audio/mpeg',
|
|
2812
2845
|
};
|
|
2813
2846
|
|
|
2814
2847
|
|
package/lib/media/drm_engine.js
CHANGED
|
@@ -18,14 +18,17 @@ goog.require('shaka.util.FakeEvent');
|
|
|
18
18
|
goog.require('shaka.util.IDestroyable');
|
|
19
19
|
goog.require('shaka.util.Iterables');
|
|
20
20
|
goog.require('shaka.util.Lazy');
|
|
21
|
+
goog.require('shaka.util.ManifestParserUtils');
|
|
21
22
|
goog.require('shaka.util.MapUtils');
|
|
22
23
|
goog.require('shaka.util.MimeUtils');
|
|
23
24
|
goog.require('shaka.util.Platform');
|
|
25
|
+
goog.require('shaka.util.Pssh');
|
|
24
26
|
goog.require('shaka.util.PublicPromise');
|
|
25
27
|
goog.require('shaka.util.StreamUtils');
|
|
26
28
|
goog.require('shaka.util.StringUtils');
|
|
27
29
|
goog.require('shaka.util.Timer');
|
|
28
30
|
goog.require('shaka.util.Uint8ArrayUtils');
|
|
31
|
+
goog.require('shaka.util.XmlUtils');
|
|
29
32
|
|
|
30
33
|
|
|
31
34
|
/** @implements {shaka.util.IDestroyable} */
|
|
@@ -404,6 +407,7 @@ shaka.media.DrmEngine = class {
|
|
|
404
407
|
if (this.mediaKeysAttached_) {
|
|
405
408
|
await this.mediaKeysAttached_;
|
|
406
409
|
|
|
410
|
+
this.destroyer_.ensureNotDestroyed();
|
|
407
411
|
return;
|
|
408
412
|
}
|
|
409
413
|
|
|
@@ -481,14 +485,12 @@ shaka.media.DrmEngine = class {
|
|
|
481
485
|
|
|
482
486
|
/**
|
|
483
487
|
* We can attach media keys before the playback actually begins when:
|
|
484
|
-
* -
|
|
485
|
-
* having webkitneedkey / msneedkey event, which will be translated as
|
|
486
|
-
* an encrypted event by the polyfills
|
|
488
|
+
* - If we are not using FairPlay Modern EME
|
|
487
489
|
* - Some initData already has been generated (through the manifest)
|
|
488
490
|
* - In case of an offline session
|
|
489
491
|
*/
|
|
490
492
|
if (manifestInitData ||
|
|
491
|
-
|
|
493
|
+
this.currentDrmInfo_.keySystem !== 'com.apple.fps' ||
|
|
492
494
|
this.offlineSessionIds_.length) {
|
|
493
495
|
await this.attachMediaKeys_();
|
|
494
496
|
}
|
|
@@ -497,7 +499,11 @@ shaka.media.DrmEngine = class {
|
|
|
497
499
|
|
|
498
500
|
// Explicit init data for any one stream or an offline session is
|
|
499
501
|
// sufficient to suppress 'encrypted' events for all streams.
|
|
500
|
-
|
|
502
|
+
// Also suppress 'encrypted' events when parsing in-band ppsh
|
|
503
|
+
// from media segments because that serves the same purpose as the
|
|
504
|
+
// 'encrypted' events.
|
|
505
|
+
if (!manifestInitData && !this.offlineSessionIds_.length &&
|
|
506
|
+
!this.config_.parseInbandPsshEnabled) {
|
|
501
507
|
this.eventManager_.listen(
|
|
502
508
|
this.video_, 'encrypted', (e) => this.onEncryptedEvent_(e));
|
|
503
509
|
}
|
|
@@ -668,6 +674,14 @@ shaka.media.DrmEngine = class {
|
|
|
668
674
|
}
|
|
669
675
|
}
|
|
670
676
|
|
|
677
|
+
// If there are pre-existing sessions that have all been loaded
|
|
678
|
+
// then reset the allSessionsLoaded_ promise, which can now be
|
|
679
|
+
// used to wait for new sesssions to be loaded
|
|
680
|
+
if (this.activeSessions_.size > 0 && this.areAllSessionsLoaded_()) {
|
|
681
|
+
this.allSessionsLoaded_.resolve();
|
|
682
|
+
this.allSessionsLoaded_ = new shaka.util.PublicPromise();
|
|
683
|
+
this.allSessionsLoaded_.catch(() => {});
|
|
684
|
+
}
|
|
671
685
|
this.createSession(initDataType, initData,
|
|
672
686
|
this.currentDrmInfo_.sessionType);
|
|
673
687
|
}
|
|
@@ -1492,7 +1506,8 @@ shaka.media.DrmEngine = class {
|
|
|
1492
1506
|
return;
|
|
1493
1507
|
}
|
|
1494
1508
|
shaka.log.debug('Unwrapping PlayReady request.');
|
|
1495
|
-
const dom =
|
|
1509
|
+
const dom = shaka.util.XmlUtils.parseXmlString(xml, 'PlayReadyKeyMessage');
|
|
1510
|
+
goog.asserts.assert(dom, 'Failed to parse PlayReady XML!');
|
|
1496
1511
|
|
|
1497
1512
|
// Set request headers.
|
|
1498
1513
|
const headers = dom.getElementsByTagName('HttpHeader');
|
|
@@ -1576,7 +1591,7 @@ shaka.media.DrmEngine = class {
|
|
|
1576
1591
|
hasExpiredKeys = true;
|
|
1577
1592
|
}
|
|
1578
1593
|
|
|
1579
|
-
const keyIdHex = shaka.util.Uint8ArrayUtils.toHex(keyId);
|
|
1594
|
+
const keyIdHex = shaka.util.Uint8ArrayUtils.toHex(keyId).slice(0, 32);
|
|
1580
1595
|
|
|
1581
1596
|
this.keyStatusByKeyId_.set(keyIdHex, status);
|
|
1582
1597
|
});
|
|
@@ -2327,6 +2342,43 @@ shaka.media.DrmEngine = class {
|
|
|
2327
2342
|
}
|
|
2328
2343
|
}
|
|
2329
2344
|
}
|
|
2345
|
+
|
|
2346
|
+
/**
|
|
2347
|
+
* Parse pssh from a media segment and announce new initData
|
|
2348
|
+
*
|
|
2349
|
+
* @param {shaka.util.ManifestParserUtils.ContentType} contentType
|
|
2350
|
+
* @param {!BufferSource} mediaSegment
|
|
2351
|
+
* @return {!Promise<void>}
|
|
2352
|
+
*/
|
|
2353
|
+
parseInbandPssh(contentType, mediaSegment) {
|
|
2354
|
+
if (!this.config_.parseInbandPsshEnabled) {
|
|
2355
|
+
return Promise.resolve();
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
2359
|
+
if (![ContentType.AUDIO, ContentType.VIDEO].includes(contentType)) {
|
|
2360
|
+
return Promise.resolve();
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
const pssh = new shaka.util.Pssh(
|
|
2364
|
+
shaka.util.BufferUtils.toUint8(mediaSegment));
|
|
2365
|
+
|
|
2366
|
+
let totalLength = 0;
|
|
2367
|
+
for (const data of pssh.data) {
|
|
2368
|
+
totalLength += data.length;
|
|
2369
|
+
}
|
|
2370
|
+
if (totalLength == 0) {
|
|
2371
|
+
return Promise.resolve();
|
|
2372
|
+
}
|
|
2373
|
+
const combinedData = new Uint8Array(totalLength);
|
|
2374
|
+
let pos = 0;
|
|
2375
|
+
for (const data of pssh.data) {
|
|
2376
|
+
combinedData.set(data, pos);
|
|
2377
|
+
pos += data.length;
|
|
2378
|
+
}
|
|
2379
|
+
this.newInitData('cenc', combinedData);
|
|
2380
|
+
return this.allSessionsLoaded_;
|
|
2381
|
+
}
|
|
2330
2382
|
};
|
|
2331
2383
|
|
|
2332
2384
|
|
|
@@ -522,9 +522,13 @@ shaka.media.MediaSourceEngine = class {
|
|
|
522
522
|
* @param {?boolean} hasClosedCaptions True if the buffer contains CEA closed
|
|
523
523
|
* captions
|
|
524
524
|
* @param {boolean=} seeked True if we just seeked
|
|
525
|
+
* @param {boolean=} adaptation True if we just automatically switched active
|
|
526
|
+
* variant(s).
|
|
525
527
|
* @return {!Promise}
|
|
526
528
|
*/
|
|
527
|
-
async appendBuffer(
|
|
529
|
+
async appendBuffer(
|
|
530
|
+
contentType, data, reference, hasClosedCaptions, seeked = false,
|
|
531
|
+
adaptation = false) {
|
|
528
532
|
const ContentType = shaka.util.ManifestParserUtils.ContentType;
|
|
529
533
|
|
|
530
534
|
if (contentType == ContentType.TEXT) {
|
|
@@ -625,6 +629,14 @@ shaka.media.MediaSourceEngine = class {
|
|
|
625
629
|
|
|
626
630
|
await this.enqueueOperation_(
|
|
627
631
|
contentType, () => this.append_(contentType, data));
|
|
632
|
+
// If the input buffer passed to SourceBuffer#appendBuffer() does not
|
|
633
|
+
// contain a complete media segment, the call will exit while the
|
|
634
|
+
// SourceBuffer's append state is
|
|
635
|
+
// still PARSING_MEDIA_SEGMENT. Reset the parser state by calling
|
|
636
|
+
// abort() to safely reset timestampOffset to 'originalOffset'.
|
|
637
|
+
// https://www.w3.org/TR/media-source-2/#sourcebuffer-segment-parser-loop
|
|
638
|
+
await this.enqueueOperation_(
|
|
639
|
+
contentType, () => this.abort_(contentType));
|
|
628
640
|
|
|
629
641
|
// Reset the offset and append window.
|
|
630
642
|
sourceBuffer.timestampOffset = originalOffset;
|
|
@@ -638,10 +650,17 @@ shaka.media.MediaSourceEngine = class {
|
|
|
638
650
|
const textOffset = (reference.startTime || 0) - (mediaStartTime || 0);
|
|
639
651
|
this.textSequenceModeOffset_.resolve(textOffset);
|
|
640
652
|
|
|
641
|
-
//
|
|
653
|
+
// Clear the buffer.
|
|
642
654
|
await this.enqueueOperation_(
|
|
643
655
|
contentType,
|
|
644
656
|
() => this.remove_(contentType, 0, this.mediaSource_.duration));
|
|
657
|
+
|
|
658
|
+
// Finally, flush the buffer in case of choppy video start on HLS fMP4.
|
|
659
|
+
if (contentType == ContentType.VIDEO) {
|
|
660
|
+
await this.enqueueOperation_(
|
|
661
|
+
contentType,
|
|
662
|
+
() => this.flush_(contentType));
|
|
663
|
+
}
|
|
645
664
|
}
|
|
646
665
|
|
|
647
666
|
// Now switch to sequence mode and fall through to our normal operations.
|
|
@@ -650,10 +669,15 @@ shaka.media.MediaSourceEngine = class {
|
|
|
650
669
|
|
|
651
670
|
if (reference && this.sequenceMode_ && contentType != ContentType.TEXT) {
|
|
652
671
|
// In sequence mode, for non-text streams, if we just cleared the buffer
|
|
653
|
-
// and are performing an unbuffered seek
|
|
654
|
-
// timestampOffset on the sourceBuffer.
|
|
655
|
-
if (seeked) {
|
|
672
|
+
// and are either performing an unbuffered seek or handling an automatic
|
|
673
|
+
// adaptation, we need to set a new timestampOffset on the sourceBuffer.
|
|
674
|
+
if (seeked || adaptation) {
|
|
656
675
|
const timestampOffset = reference.startTime;
|
|
676
|
+
// The logic to call abort() before setting the timestampOffset is
|
|
677
|
+
// extended during unbuffered seeks or automatic adaptations; it is
|
|
678
|
+
// possible for the append state to be PARSING_MEDIA_SEGMENT from the
|
|
679
|
+
// previous SourceBuffer#appendBuffer() call.
|
|
680
|
+
this.enqueueOperation_(contentType, () => this.abort_(contentType));
|
|
657
681
|
this.enqueueOperation_(
|
|
658
682
|
contentType,
|
|
659
683
|
() => this.setTimestampOffset_(contentType, timestampOffset));
|
|
@@ -849,21 +873,25 @@ shaka.media.MediaSourceEngine = class {
|
|
|
849
873
|
}
|
|
850
874
|
|
|
851
875
|
/**
|
|
852
|
-
* We only support increasing duration at this time. Decreasing duration
|
|
853
|
-
* causes the MSE removal algorithm to run, which results in an 'updateend'
|
|
854
|
-
* event. Supporting this scenario would be complicated, and is not currently
|
|
855
|
-
* needed.
|
|
856
|
-
*
|
|
857
876
|
* @param {number} duration
|
|
858
877
|
* @return {!Promise}
|
|
859
878
|
*/
|
|
860
879
|
async setDuration(duration) {
|
|
861
|
-
goog.asserts.assert(
|
|
862
|
-
isNaN(this.mediaSource_.duration) ||
|
|
863
|
-
this.mediaSource_.duration <= duration,
|
|
864
|
-
'duration cannot decrease: ' + this.mediaSource_.duration + ' -> ' +
|
|
865
|
-
duration);
|
|
866
880
|
await this.enqueueBlockingOperation_(() => {
|
|
881
|
+
// Reducing the duration causes the MSE removal algorithm to run, which
|
|
882
|
+
// triggers an 'updateend' event to fire. To handle this scenario, we
|
|
883
|
+
// have to insert a dummy operation into the beginning of each queue,
|
|
884
|
+
// which the 'updateend' handler will remove.
|
|
885
|
+
if (duration < this.mediaSource_.duration) {
|
|
886
|
+
for (const contentType in this.sourceBuffers_) {
|
|
887
|
+
const dummyOperation = {
|
|
888
|
+
start: () => {},
|
|
889
|
+
p: new shaka.util.PublicPromise(),
|
|
890
|
+
};
|
|
891
|
+
this.queues_[contentType].unshift(dummyOperation);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
867
895
|
this.mediaSource_.duration = duration;
|
|
868
896
|
});
|
|
869
897
|
}
|
package/lib/media/playhead.js
CHANGED
|
@@ -294,7 +294,7 @@ shaka.media.MediaSourcePlayhead = class {
|
|
|
294
294
|
|
|
295
295
|
/** @override */
|
|
296
296
|
getStallsDetected() {
|
|
297
|
-
return this.stallDetector_.getStallsDetected();
|
|
297
|
+
return this.stallDetector_ ? this.stallDetector_.getStallsDetected() : 0;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
300
|
/** @override */
|
|
@@ -364,7 +364,10 @@ shaka.media.SegmentIndex = class {
|
|
|
364
364
|
lastReference.partialReferences,
|
|
365
365
|
lastReference.tilesLayout,
|
|
366
366
|
lastReference.tileDuration,
|
|
367
|
-
lastReference.syncTime
|
|
367
|
+
lastReference.syncTime,
|
|
368
|
+
lastReference.status,
|
|
369
|
+
lastReference.hlsAes128Key,
|
|
370
|
+
);
|
|
368
371
|
}
|
|
369
372
|
|
|
370
373
|
|