diff --git a/externs/iterator.js b/externs/iterator.js new file mode 100644 index 0000000000..11d16c3011 --- /dev/null +++ b/externs/iterator.js @@ -0,0 +1,34 @@ +/** + * Copyright 2015 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Iterator externs missing from closure. + * @externs + */ + + + +/** + * @interface + * @template VALUE + * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/The_Iterator_protocol + */ +function Iterator() {} + + +/** + * @return {{value:VALUE, done:boolean}} + */ +Iterator.prototype.next = function() {}; + diff --git a/externs/mediakeys.js b/externs/mediakeys.js index 120f05b260..21f4c4597a 100644 --- a/externs/mediakeys.js +++ b/externs/mediakeys.js @@ -23,10 +23,6 @@ var BufferSource; -/** @typedef {!Object.} */ -var MediaKeyStatuses; - - /** * @param {string} keySystem * @param {Array.=} opt_supportedConfigurations @@ -43,14 +39,6 @@ Navigator.prototype.requestMediaKeySystemAccess = HTMLMediaElement.prototype.mediaKeys; -/** - * NOTE: Not yet implemented as of Chrome 40. Type may be incorrect. - * @type {string} - * @const - */ -HTMLMediaElement.prototype.waitingFor; - - /** * @param {MediaKeys} mediaKeys * @return {!Promise} @@ -68,7 +56,6 @@ MediaKeySystemAccess.prototype.createMediaKeys = function() {}; /** - * NOTE: Not yet implemented as of Chrome 40. Type may be incorrect. * @return {Object} */ MediaKeySystemAccess.prototype.getConfiguration = function() {}; @@ -99,6 +86,60 @@ MediaKeys.prototype.setServerCertificate = function(serverCertificate) {}; +/** + * @interface + */ +function MediaKeyStatusMap() {} + + +/** + * @type {number} + * @const + */ +MediaKeyStatusMap.prototype.size; + + +/** + * Array entry 0 is the key, 1 is the value. + * @return {Iterator.>} + */ +MediaKeyStatusMap.prototype.entries = function() {}; + + +/** + * The functor is called with each value. + * @param {function(string)} fn + */ +MediaKeyStatusMap.prototype.forEach = function(fn) {}; + + +/** + * @param {!BufferSource} keyId + * @return {string|undefined} + */ +MediaKeyStatusMap.prototype.get = function(keyId) {}; + + +/** + * @param {!BufferSource} keyId + * @return {boolean} + */ +MediaKeyStatusMap.prototype.has = function(keyId) {}; + + +/** + * @return {Iterator.} + */ +MediaKeyStatusMap.prototype.keys = function() {}; + + +/** + * @return {Iterator.} + */ +MediaKeyStatusMap.prototype.values = function() {}; + + + /** * @interface * @extends {EventTarget} @@ -128,8 +169,7 @@ MediaKeySession.prototype.closed; /** - * NOTE: Not yet implemented as of Chrome 40. Type may be incorrect. - * @type {!MediaKeyStatuses} + * @type {!MediaKeyStatusMap} * @const */ MediaKeySession.prototype.keyStatuses; diff --git a/lib/player/player.js b/lib/player/player.js index 86469e5557..b9fb2675bc 100644 --- a/lib/player/player.js +++ b/lib/player/player.js @@ -40,6 +40,10 @@ goog.require('shaka.util.Uint8ArrayUtils'); * @property {string} type 'error' * @property {boolean} bubbles true * @property {!Error} detail An object which contains details on the error. + * The error's 'type' property will help you identify the specific error + * condition and display an appropriate message or error indicator to the + * user. The error's 'message' property contains English text which can + * be useful during debugging. * @export */ @@ -406,6 +410,7 @@ shaka.player.Player.prototype.generateFakeEncryptedEvents_ = * @private */ shaka.player.Player.prototype.setVideoEventListeners_ = function() { + this.eventManager_.listen(this.video_, 'error', this.onError_.bind(this)); // TODO(story 1891509): Connect these events to the UI. this.eventManager_.listen(this.video_, 'play', this.onPlay_.bind(this)); this.eventManager_.listen(this.video_, 'playing', this.onPlaying_.bind(this)); @@ -443,9 +448,12 @@ shaka.player.Player.prototype.onEncrypted_ = function(event) { var session = this.mediaKeys_.createSession(); this.sessions_.push(session); - this.eventManager_.listen( - session, 'message', /** @type {shaka.util.EventManager.ListenerType} */( + this.eventManager_.listen(session, 'message', + /** @type {shaka.util.EventManager.ListenerType} */( this.onSessionMessage_.bind(this))); + this.eventManager_.listen(session, 'keystatuseschange', + /** @type {shaka.util.EventManager.ListenerType} */( + this.onKeyStatusChange_.bind(this))); var p = session.generateRequest(event.initDataType, event.initData); this.requestGenerated_[initDataKey] = true; @@ -476,6 +484,29 @@ shaka.player.Player.prototype.onSessionMessage_ = function(event) { }; +/** + * EME status-change handler. + * + * @param {!Event} event + * @private + */ +shaka.player.Player.prototype.onKeyStatusChange_ = function(event) { + shaka.log.info('onKeyStatusChange_', event); + var session = /** @type {!MediaKeySession} */(event.target); + var map = session.keyStatuses; + var i = map.values(); + for (var v = i.next(); !v.done; v = i.next()) { + var message = shaka.player.Player.KEY_STATUS_ERROR_MAP_[v.value]; + if (message) { + var error = new Error(message); + error.type = v.value; + var event = shaka.util.FakeEvent.createErrorEvent(error); + this.dispatchEvent(event); + } + } +}; + + /** * Requests a license. * @@ -538,6 +569,31 @@ shaka.player.Player.prototype.onFirstTimestamp_ = function(event) { }; +/** + * Video error event handler. + * + * @param {!Event} event + * @private + */ +shaka.player.Player.prototype.onError_ = function(event) { + var code = this.video_.error.code; + if (code == MediaError['MEDIA_ERR_ABORTED']) { + // Ignore this error code, which should only occur when navigating away or + // deliberately stopping playback of HTTP content. + return; + } + + shaka.log.debug('onError_', event, code); + var message = shaka.player.Player.MEDIA_ERROR_MAP_[code] || + 'Unknown playback error.'; + + var error = new Error(message); + error.type = 'playback'; + var event = shaka.util.FakeEvent.createErrorEvent(error); + this.dispatchEvent(event); +}; + + /** * Video play event handler. * @@ -978,3 +1034,40 @@ shaka.player.Player.prototype.onWatchdogTimer_ = function() { */ shaka.player.Player.UNDERFLOW_THRESHOLD_ = 0.050; + +/** + * A map of key statuses to errors. Not every key status appears in the map, + * in which case that key status is not treated as an error. + * + * @private {!Object.} + * @const + */ +shaka.player.Player.KEY_STATUS_ERROR_MAP_ = { + 'output-not-allowed': 'The required output protection is not available.', + 'expired': 'A required key has expired and the content cannot be decrypted.', + 'internal-error': 'An unknown error has occurred in the CDM.' +}; + + +/** + * A map of MediaError codes to error messages. The JS interpreter won't take + * a symbolic name as a key, so the symbolic names for these error codes appear + * in comments after the number. + * + * @private {!Object.} + * @const + */ +shaka.player.Player.MEDIA_ERROR_MAP_ = { + // This should not occur for DASH sources, but may occur for HTTP sources. + 2: // MediaError.MEDIA_ERR_NETWORK + 'A network failure occured while loading media content.', + + 3: // MediaError.MEDIA_ERR_DECODE + 'The browser failed to decode the media content.', + + // This is also unlikely for DASH sources, but HTTP sources do not check + // browser support before beginning playback. + 4: // MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED + 'The browser does not support the media content.' +}; + diff --git a/lib/polyfill/patchedmediakeys_v01b.js b/lib/polyfill/patchedmediakeys_v01b.js index 5a5cadbb7b..6dcdc5964f 100644 --- a/lib/polyfill/patchedmediakeys_v01b.js +++ b/lib/polyfill/patchedmediakeys_v01b.js @@ -476,9 +476,9 @@ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession = /** @type {!shaka.util.PublicPromise} */ this.closed = new shaka.util.PublicPromise(); - /** @type {!MediaKeyStatuses} */ - this.keyStatuses = {}; - // TODO: key status and 'keyschange' events unsupported + /** @type {!MediaKeyStatusMap} */ + this.keyStatuses = + new shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap(); }; goog.inherits(shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession, shaka.util.FakeEventTarget); @@ -516,7 +516,7 @@ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.ready = // internally, that the session is no longer new. This allows us to set // the messageType attribute of 'message' events. this.expiration = Number.POSITIVE_INFINITY; - // TODO: key status and 'keyschange' events unsupported + this.updateKeyStatus_('usable'); if (this.updatePromise_) { this.updatePromise_.resolve(); @@ -551,6 +551,17 @@ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.handleError = error.method = 'update'; this.updatePromise_.reject(error); this.updatePromise_ = null; + } else { + // This mapping of key statuses is imperfect at best. + var code = event.errorCode.code; + var systemCode = event.systemCode; + if (code == MediaKeyError['MEDIA_KEYERR_OUTPUT']) { + this.updateKeyStatus_('output-not-allowed'); + } else if (systemCode == 1) { + this.updateKeyStatus_('expired'); + } else { + this.updateKeyStatus_('internal-error'); + } } }; @@ -677,6 +688,20 @@ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.update_ = }; +/** + * Update key status and dispatch a 'keystatuseschange' event. + * + * @param {string} status + * @private + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype. + updateKeyStatus_ = function(status) { + this.keyStatuses.setStatus(status); + var event = shaka.util.FakeEvent.create({type: 'keystatuseschange'}); + this.dispatchEvent(event); +}; + + /** @override */ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.generateRequest = function(initDataType, initData) { @@ -750,3 +775,161 @@ shaka.polyfill.PatchedMediaKeys.v01b.MediaKeySession.prototype.remove = return this.close(); }; + + +/** + * An implementation of Iterator. + * + * @param {!Array.} values + * + * @constructor + * @implements {Iterator} + * @template VALUE + */ +shaka.polyfill.PatchedMediaKeys.v01b.Iterator = function(values) { + /** @private {!Array.} */ + this.values_ = values; + + /** @private {number} */ + this.index_ = 0; +}; + + +/** + * @return {{value:VALUE, done:boolean}} + */ +shaka.polyfill.PatchedMediaKeys.v01b.Iterator.prototype.next = function() { + if (this.index_ >= this.values_.length) { + return {value: undefined, done: true}; + } + return {value: this.values_[this.index_++], done: false}; +}; + + + +/** + * An implementation of MediaKeyStatusMap. + * This fakes a map with a single key ID. + * + * @constructor + * @implements {MediaKeyStatusMap} + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap = function() { + /** + * @type {number} + */ + this.size = 0; + + /** + * @private {string|undefined} + */ + this.status_ = undefined; +}; + + +/** + * @const {!Uint8Array} + * @private + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.KEY_ID_ = + shaka.util.Uint8ArrayUtils.fromString('FAKE_KEY_ID'); + + +/** + * An internal method used by the session to set key status. + * @param {string|undefined} status + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.setStatus = + function(status) { + this.size = status == undefined ? 0 : 1; + this.status_ = status; +}; + + +/** + * Array entry 0 is the key, 1 is the value. + * @return {Iterator.>} + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.entries = + function() { + var fakeKeyId = + shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.KEY_ID_; + /** @type {!Array.>} */ + var arr = []; + if (this.status_) { + arr.push([fakeKeyId, this.status_]); + } + return new shaka.polyfill.PatchedMediaKeys.v01b.Iterator(arr); +}; + + +/** + * The functor is called with each value. + * @param {function(string)} fn + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.forEach = + function(fn) { + if (this.status_) { + fn(this.status_); + } +}; + + +/** + * @param {!BufferSource} keyId + * @return {string|undefined} + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.get = + function(keyId) { + if (this.has(keyId)) { + return this.status_; + } + return undefined; +}; + + +/** + * @param {!BufferSource} keyId + * @return {boolean} + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.has = + function(keyId) { + var fakeKeyId = + shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.KEY_ID_; + if (this.status_ && + shaka.util.Uint8ArrayUtils.equal(new Uint8Array(keyId), fakeKeyId)) { + return true; + } + return false; +}; + + +/** + * @return {Iterator.} + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.keys = + function() { + var fakeKeyId = + shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.KEY_ID_; + /** @type {!Array.} */ + var arr = []; + if (this.status_) { + arr.push(fakeKeyId); + } + return new shaka.polyfill.PatchedMediaKeys.v01b.Iterator(arr); +}; + + +/** + * @return {Iterator.} + */ +shaka.polyfill.PatchedMediaKeys.v01b.MediaKeyStatusMap.prototype.values = + function() { + /** @type {!Array.} */ + var arr = []; + if (this.status_) { + arr.push(this.status_); + } + return new shaka.polyfill.PatchedMediaKeys.v01b.Iterator(arr); +}; +