/** * @license * 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. */ goog.provide('shaka.player.Player'); goog.require('shaka.asserts'); goog.require('shaka.log'); goog.require('shaka.media.EmeManager'); goog.require('shaka.player.AudioTrack'); goog.require('shaka.player.Defaults'); goog.require('shaka.player.Restrictions'); goog.require('shaka.player.Stats'); goog.require('shaka.player.TextStyle'); goog.require('shaka.player.TextTrack'); goog.require('shaka.player.VideoTrack'); goog.require('shaka.timer'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.FakeEvent'); goog.require('shaka.util.FakeEventTarget'); goog.require('shaka.util.MapUtils'); /** * @event shaka.player.Player.ErrorEvent * @description Fired when a playback error occurs. * Bubbles up through the Player. * @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 */ /** * @event shaka.player.Player.BufferingEvent * @description Fired when the player's buffering state changes. * @property {string} type 'bufferingStart' or 'bufferingEnd' * @export */ /** * Creates a Player. * * @param {!HTMLVideoElement} video The video element. * * @fires shaka.media.IStream.AdaptationEvent * @fires shaka.player.StreamVideoSource.SeekRangeChangedEvent * @fires shaka.player.Player.BufferingEvent * @fires shaka.player.Player.ErrorEvent * * @constructor * @struct * @extends {shaka.util.FakeEventTarget} * @export */ shaka.player.Player = function(video) { shaka.util.FakeEventTarget.call(this, null); /** * The video element. * @private {!HTMLVideoElement} */ this.video_ = video; /** * The video source object. * @private {shaka.player.IVideoSource} */ this.videoSource_ = null; /** @private {!shaka.util.EventManager} */ this.eventManager_ = new shaka.util.EventManager(); /** @private {shaka.media.EmeManager} */ this.emeManager_ = null; /** @private {?number} */ this.rewindTimer_ = null; /** @private {number} */ this.seekRangeStart_ = 0; /** @private {?number} */ this.watchdogTimer_ = null; /** @private {boolean} */ this.buffering_ = false; /** @private {!shaka.player.Stats} */ this.stats_ = new shaka.player.Stats; /** @private {!Object.} */ this.videoSourceConfig_ = { 'enableAdaptation': true, 'streamBufferSize': shaka.player.Defaults.STREAM_BUFFER_SIZE, 'liveStreamEndTimeout': shaka.player.Defaults.STREAM_BUFFER_SIZE, 'licenseRequestTimeout': shaka.player.Defaults.LICENSE_REQUEST_TIMEOUT, 'mpdRequestTimeout': shaka.player.Defaults.MPD_REQUEST_TIMEOUT, 'segmentRequestTimeout': shaka.player.Defaults.SEGMENT_REQUEST_TIMEOUT, 'preferredLanguage': shaka.player.Defaults.PREFERRED_LANGUAGE, 'restrictions': new shaka.player.Restrictions() }; /** @private {number} */ this.playbackRate_ = 1.0; /** @private {?number} */ this.playbackStartTime_ = null; }; goog.inherits(shaka.player.Player, shaka.util.FakeEventTarget); /** * @define {string} A version number taken from git at compile time. */ goog.define('GIT_VERSION', 'v1.5.0-debug'); /** * @const {string} * @export */ shaka.player.Player.version = GIT_VERSION; /** * Determines if the browser has all of the necessary APIs to support the Shaka * Player. This check may not pass if polyfills have not been installed. * * @return {boolean} * @export */ shaka.player.Player.isBrowserSupported = function() { return true && // MSE is needed for adaptive streaming. !!window.MediaSource && // EME is needed for protected content. !!window.MediaKeys && // Indicates recent EME APIs. !!window.navigator && !!window.navigator.requestMediaKeySystemAccess && !!window.MediaKeySystemAccess && !!window.MediaKeySystemAccess.prototype.getConfiguration && // Promises are used frequently for asynchronous operations. !!window.Promise && // Fullscreen API. !!Element.prototype.requestFullscreen && !!document.exitFullscreen && 'fullscreenElement' in document && // Uint8Array is used frequently for parsing binary data !!window.Uint8Array; }; /** * Determines if the specified MIME type and codec is supported by the browser. * * @param {string} fullMimeType A MIME type, which should include codec info. * @return {boolean} true if the type is supported. * @export */ shaka.player.Player.isTypeSupported = function(fullMimeType) { var supported; if (fullMimeType == 'text/vtt') { supported = !!window.VTTCue; } else { supported = MediaSource.isTypeSupported(fullMimeType); } shaka.log.info('+', fullMimeType, supported ? 'is' : 'is not', 'supported'); return supported; }; /** * Sets style attributes for text tracks. * * @param {!shaka.player.TextStyle} style * @export */ shaka.player.Player.setTextStyle = function(style) { var element = document.getElementById(shaka.player.Player.STYLE_ELEMENT_ID_); if (!element) { element = document.createElement('style'); element.id = shaka.player.Player.STYLE_ELEMENT_ID_; document.head.appendChild(element); } var sheet = element.sheet; while (sheet.cssRules.length) { sheet.deleteRule(0); } sheet.insertRule('::cue { ' + style.toCSS() + ' }', 0); }; /** * Destroys the player. * @return {!Promise} A promise, resolved when destroy has finished. * @suppress {checkTypes} to set otherwise non-nullable types to null. * @export */ shaka.player.Player.prototype.destroy = function() { return this.unload().then(shaka.util.TypedBind(this, function() { this.eventManager_.destroy(); this.eventManager_ = null; this.video_ = null; })).catch(function() {}); }; /** * Stop playback and unload the current video source. Makes the player ready * for reuse. Also resets any statistics gathered. * * MediaKeys must be unloaded asynchronously, but all other resources are * removed synchronously. * * @return {!Promise} A promise, resolved when MediaKeys is removed. * @export */ shaka.player.Player.prototype.unload = function() { if (!this.videoSource_) { // Nothing to unload. return Promise.resolve(); } if (this.buffering_) { this.endBufferingState_(); } // Stop playback. this.video_.pause(); // Stop listening for events and timers. this.eventManager_.removeAll(); this.cancelWatchdogTimer_(); this.cancelRewindTimer_(); // Release all EME resources. if (this.emeManager_) { this.emeManager_.destroy(); this.emeManager_ = null; } // Remove the video source. this.video_.src = ''; // Only clear mediaKeys after clearing the source. var p = this.video_.setMediaKeys(null); if (this.videoSource_) { this.videoSource_.destroy(); this.videoSource_ = null; } // Reset state. this.buffering_ = false; this.stats_ = new shaka.player.Stats(); return p; }; /** * Loads the specified video source and starts playback. If a video source has * already been loaded, this calls unload() for you before loading the new * source. * * @param {!shaka.player.IVideoSource} videoSource The IVideoSource object. The * Player takes ownership of |videoSource|. * @return {!Promise} * @export */ shaka.player.Player.prototype.load = function(videoSource) { var p = this.unload(); shaka.asserts.assert(this.videoSource_ == null); shaka.asserts.assert(this.emeManager_ == null); if (this.video_.autoplay) { shaka.timer.begin('load'); this.eventManager_.listen(this.video_, 'timeupdate', this.onFirstTimestamp_.bind(this)); } videoSource.configure(this.videoSourceConfig_); videoSource.setPlaybackStartTime(this.playbackStartTime_); this.playbackStartTime_ = null; var loaded = p.then(shaka.util.TypedBind(this, function() { return videoSource.load(); })); loaded.catch(shaka.util.TypedBind(this, /** @param {*} error */ function(error) { // Clean up the local copy, since we haven't set this.videoSource_ yet. videoSource.destroy(); // Pass the error along. return Promise.reject(error); })); return loaded.then(shaka.util.TypedBind(this, function() { if (!this.video_) return this.rejectDestroyed_(); this.videoSource_ = videoSource; this.eventManager_.listen(this.videoSource_, 'seekrangechanged', this.onSeekRangeChanged_.bind(this)); this.emeManager_ = new shaka.media.EmeManager( this, this.video_, this.videoSource_); return this.emeManager_.initialize(); }) ).then(shaka.util.TypedBind(this, function() { if (!this.video_) return this.rejectDestroyed_(); this.setVideoEventListeners_(); return this.videoSource_.attach(this, this.video_); }) ).then(shaka.util.TypedBind(this, function() { if (!this.video_) return this.rejectDestroyed_(); this.startWatchdogTimer_(); }) ).catch(shaka.util.TypedBind(this, /** @param {*} error */ function(error) { if (!this.video_) return this.rejectDestroyed_(); if (error.type != 'destroy') { // Even though we return a rejected promise, we still want to // dispatch an error event to ensure that the application is aware of // all errors from the player. var event = shaka.util.FakeEvent.createErrorEvent(error); this.dispatchEvent(event); } return this.unload().then(function() { return Promise.reject(error); }); }) ); }; /** * @return {!Promise} * @private */ shaka.player.Player.prototype.rejectDestroyed_ = function() { var error = new Error('Player destroyed'); error.type = 'destroy'; return Promise.reject(error); }; /** * Sets the video's event listeners. * * @private */ shaka.player.Player.prototype.setVideoEventListeners_ = function() { this.eventManager_.listen(this.video_, 'error', this.onError_.bind(this)); this.eventManager_.listen(this.video_, 'playing', this.onPlaying_.bind(this)); this.eventManager_.listen(this.video_, 'pause', this.onPause_.bind(this)); }; /** * Time update event handler. Will be removed once the first update is seen. * * @param {!Event} event * @private */ shaka.player.Player.prototype.onFirstTimestamp_ = function(event) { shaka.timer.end('load'); this.stats_.logPlaybackLatency(shaka.timer.get('load')); this.eventManager_.unlisten(this.video_, 'timeupdate'); }; /** * Video error event handler. * * @param {!Event} event * @private */ shaka.player.Player.prototype.onError_ = function(event) { if (!this.video_.error) { // This occurred during testing with prefixed EME. Ignore errors we can't // interpret, on the assumption that this is a browser bug. shaka.log.debug('Uninterpretable error event!', event); return; } 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 errorEvent = shaka.util.FakeEvent.createErrorEvent(error); this.dispatchEvent(errorEvent); }; /** * Video playing event handler. Fires any time the video starts playing. * * @param {!Event} event * @private */ shaka.player.Player.prototype.onPlaying_ = function(event) { shaka.log.debug('onPlaying_', event); shaka.timer.begin('playing'); // Start rewind timer if playback rate should be negative and a rewind timer // is not already set. if (!this.rewindTimer_ && this.playbackRate_ < 0) { this.video_.playbackRate = 0; this.onRewindTimer_( this.video_.currentTime, Date.now(), this.playbackRate_); } if (this.buffering_) { this.endBufferingState_(); } }; /** * Video pause event handler. Fires any time the video stops for any reason, * including before a 'seeking' or 'ended' event. * * @param {!Event} event * @private */ shaka.player.Player.prototype.onPause_ = function(event) { shaka.log.debug('onPause_', event); shaka.timer.end('playing'); var elapsed = shaka.timer.get('playing'); if (!isNaN(elapsed)) { this.stats_.logPlayTime(elapsed); } this.cancelRewindTimer_(); }; /** * Handler for seek range events. * * @param {!Event} event * @private */ shaka.player.Player.prototype.onSeekRangeChanged_ = function(event) { this.seekRangeStart_ = event['start']; }; /** * Gets updated stats about the player. * * @return {!shaka.player.Stats} * @export */ shaka.player.Player.prototype.getStats = function() { if (!this.video_.paused) { // Update play time, which is still progressing. shaka.timer.end('playing'); var elapsed = shaka.timer.get('playing'); if (!isNaN(elapsed)) { this.stats_.logPlayTime(elapsed); shaka.timer.begin('playing'); } } this.stats_.updateVideoStats(this.video_); return this.stats_; }; /** * Gets the available video tracks. * * @return {!Array.} * @export */ shaka.player.Player.prototype.getVideoTracks = function() { if (!this.videoSource_) return []; return this.videoSource_.getVideoTracks(); }; /** * Gets the available audio tracks. * * @return {!Array.} * @export */ shaka.player.Player.prototype.getAudioTracks = function() { if (!this.videoSource_) return []; return this.videoSource_.getAudioTracks(); }; /** * Gets the available text tracks. * * @return {!Array.} * @export */ shaka.player.Player.prototype.getTextTracks = function() { if (!this.videoSource_) return []; return this.videoSource_.getTextTracks(); }; /** * Select a video track by ID. This can interfere with automatic bitrate * adaptation, so you should disable adaptation, via * {@link shaka.player.Player#configure}, if you intend to use manual video * track selection. * * @param {number} id The |id| field of the desired VideoTrack object. * @param {boolean=} opt_clearBuffer If true (and by default), removes the * previous stream's content before switching to the new stream. * * @return {boolean} True if the specified VideoTrack was found. * @export */ shaka.player.Player.prototype.selectVideoTrack = function(id, opt_clearBuffer) { if (!this.videoSource_) return false; var clearBuffer = (opt_clearBuffer == undefined) ? true : opt_clearBuffer; return this.videoSource_.selectVideoTrack(id, clearBuffer); }; /** * Select an audio track by ID. * * @param {number} id The |id| field of the desired AudioTrack object. * @param {boolean=} opt_clearBuffer If true (and by default), removes the * previous stream's content before switching to the new stream. * * @return {boolean} True if the specified AudioTrack was found. * @export */ shaka.player.Player.prototype.selectAudioTrack = function(id, opt_clearBuffer) { if (!this.videoSource_) return false; var clearBuffer = (opt_clearBuffer == undefined) ? true : opt_clearBuffer; return this.videoSource_.selectAudioTrack(id, clearBuffer); }; /** * Select a text track by ID. * * @param {number} id The |id| field of the desired TextTrack object. * * @return {boolean} True if the specified TextTrack was found. * @export */ shaka.player.Player.prototype.selectTextTrack = function(id) { if (!this.videoSource_) return false; return this.videoSource_.selectTextTrack(id, false); }; /** * Enable or disable the text track. Has no effect if called before * load() resolves. * * @param {boolean} enabled * @export */ shaka.player.Player.prototype.enableTextTrack = function(enabled) { if (!this.videoSource_) return; this.videoSource_.enableTextTrack(enabled); }; /** * @param {number} rate The playback rate. * Negative values will rewind the video. * Positive values less than 1.0 will trigger slow-motion playback. * Positive values greater than 1.0 will trigger fast-forward. * 0.0 is similar to pausing the video. * Some UAs will not play audio at rates less than 0.25 or 0.5 or greater * than 4.0 or 5.0, but this behavior is not specified. * No audio will be played while rewinding. * @export */ shaka.player.Player.prototype.setPlaybackRate = function(rate) { // Cancel any rewind we might be in the middle of. this.cancelRewindTimer_(); if (rate >= 0) { // Slow-mo or fast-forward are handled natively by the UA. this.video_.playbackRate = rate; // Only rewind when not paused. } else if (!this.video_.paused) { // Rewind is not supported by any UA to date (2015), so we fake it. // http://crbug.com/33099 this.video_.playbackRate = 0; this.onRewindTimer_(this.video_.currentTime, Date.now(), rate); } this.playbackRate_ = rate; }; /** * Returns the current playbackRate. * @return {number} * @export */ shaka.player.Player.prototype.getPlaybackRate = function() { return this.playbackRate_; }; /** * @param {number} startTime Desired time (in seconds) for playback * to begin from. * @export */ shaka.player.Player.prototype.setPlaybackStartTime = function(startTime) { this.playbackStartTime_ = startTime; }; /** * @return {boolean} * @export */ shaka.player.Player.prototype.isLive = function() { return this.videoSource_ ? this.videoSource_.isLive() : false; }; /** *

* Configures the Player. Configuration options are set via key-value pairs. The * default options are defined in {@link shaka.player.Defaults}. *

* * The following configuration options are supported: * * * @example * player.configure({'enableAdaptation': false}); * player.configure({'preferredLanguage': 'en', * 'streamBufferSize': 15}); * * @param {Object.} config A configuration object, which contains * the configuration options as key-value pairs. * @throws TypeError if a configuration option has the wrong type. * @throws RangeError if a configuration option is out of range. * @export */ shaka.player.Player.prototype.configure = function(config) { if (!config) return; // Alias. var MapUtils = shaka.util.MapUtils; var enableAdaptation = MapUtils.getBoolean(config, 'enableAdaptation'); if (enableAdaptation != null) { this.videoSourceConfig_['enableAdaptation'] = enableAdaptation; } var streamBufferSize = MapUtils.getNumber(config, 'streamBufferSize', 0); if (streamBufferSize != null) { this.videoSourceConfig_['streamBufferSize'] = streamBufferSize; } var liveStreamEndedTimeout = MapUtils.getNumber(config, 'liveStreamEndTimeout', 0); if (liveStreamEndedTimeout != null) { this.videoSourceConfig_['liveStreamEndTimeout'] = liveStreamEndedTimeout; } var licenseRequestTimeout = MapUtils.getNumber(config, 'licenseRequestTimeout', 0); if (licenseRequestTimeout != null) { this.videoSourceConfig_['licenseRequestTimeout'] = licenseRequestTimeout; } var mpdRequestTimeout = MapUtils.getNumber(config, 'mpdRequestTimeout', 0); if (mpdRequestTimeout != null) { this.videoSourceConfig_['mpdRequestTimeout'] = mpdRequestTimeout; } var segmentRequestTimeout = MapUtils.getNumber(config, 'segmentRequestTimeout', 0); if (segmentRequestTimeout != null) { this.videoSourceConfig_['segmentRequestTimeout'] = segmentRequestTimeout; } var preferredLanguage = MapUtils.getString(config, 'preferredLanguage'); if (preferredLanguage != null) { this.videoSourceConfig_['preferredLanguage'] = preferredLanguage; } var restrictions = MapUtils.getAsInstanceType( config, 'restrictions', shaka.player.Restrictions); if (restrictions != null) { this.videoSourceConfig_['restrictions'] = restrictions.clone(); } if (this.videoSource_) { this.videoSource_.configure(this.videoSourceConfig_); } }; /** * Gets the Player's configuration. * * @return {!Object.} A configuration object. * @see {@link shaka.player.Player#configure} * @export */ shaka.player.Player.prototype.getConfiguration = function() { return this.videoSourceConfig_; }; /** * Cancels the rewind timer, if any. * @private */ shaka.player.Player.prototype.cancelRewindTimer_ = function() { if (this.rewindTimer_) { window.clearTimeout(this.rewindTimer_); this.rewindTimer_ = null; } }; /** * Starts the watchdog timer. * @private */ shaka.player.Player.prototype.startWatchdogTimer_ = function() { this.cancelWatchdogTimer_(); this.watchdogTimer_ = window.setTimeout(this.onWatchdogTimer_.bind(this), 100); }; /** * Cancels the watchdog timer, if any. * @private */ shaka.player.Player.prototype.cancelWatchdogTimer_ = function() { if (this.watchdogTimer_) { window.clearTimeout(this.watchdogTimer_); this.watchdogTimer_ = null; } }; /** * Called on a recurring timer to simulate rewind. * @param {number} startVideoTime * @param {number} startWallTime * @param {number} rate * @private */ shaka.player.Player.prototype.onRewindTimer_ = function(startVideoTime, startWallTime, rate) { shaka.asserts.assert(rate < 0); shaka.asserts.assert(!this.video_.paused); this.rewindTimer_ = null; var offset = ((Date.now() - startWallTime) / 1000) * rate; // For live content the seek start time may increase over time, so to avoid // any races between this function and onSeekRangeChanged_() use a larger // fudge factor. var fudge = this.isLive() ? 1 : 0.05; if (this.video_.currentTime < this.seekRangeStart_ + fudge) { // Hit the beginning (or near enough), so pause. this.video_.pause(); } else { var goal = Math.max(this.seekRangeStart_, startVideoTime + offset); this.video_.currentTime = goal; var callback = this.onRewindTimer_.bind( this, startVideoTime, startWallTime, rate); this.rewindTimer_ = window.setTimeout( callback, shaka.player.Player.REWIND_UPDATE_INTERVAL_ * 1000); } }; /** * Called to enter a buffering state. * @private */ shaka.player.Player.prototype.enterBufferingState_ = function() { this.buffering_ = true; this.video_.pause(); this.stats_.logBufferingEvent(); shaka.timer.begin('buffering'); shaka.log.debug('Buffering...'); this.dispatchEvent(shaka.util.FakeEvent.create({type: 'bufferingStart'})); }; /** * Called to leave a buffering state, either due to unloading a video source, * unpausing a video, or because of the watchdog's decision. * @private */ shaka.player.Player.prototype.endBufferingState_ = function() { shaka.asserts.assert(this.buffering_); shaka.log.debug('Buffering complete.'); shaka.timer.end('buffering'); this.stats_.logBufferingTime(shaka.timer.get('buffering')); this.buffering_ = false; this.dispatchEvent(shaka.util.FakeEvent.create({type: 'bufferingEnd'})); }; /** * Called on a recurring timer to detect buffering events. * @private */ shaka.player.Player.prototype.onWatchdogTimer_ = function() { this.startWatchdogTimer_(); if (this.video_.ended || this.video_.seeking) return; var bufferedProp = this.video_.buffered; // Counter-intuitively, the play head can advance audio-only while the video // is buffering. |bufferedProp| will show the intersection of buffered // ranges for both audio and video, so it's an accurate way to determine if // we are buffering or not. The 'stalled', 'waiting', and 'suspended' events // do not work for this purpose as of Chrome 38. Nor will video.readyState. var bufferEnd = bufferedProp.length ? bufferedProp.end(bufferedProp.length - 1) : 0; var buffered = Math.max(bufferEnd - this.video_.currentTime, 0); var fudgeFactor = shaka.player.Player.BUFFERED_FUDGE_FACTOR_; var threshold = shaka.player.Player.UNDERFLOW_THRESHOLD_; var durationProp = this.video_.duration; var d = isNaN(durationProp) ? 0 : Math.max(durationProp - fudgeFactor, 0); var atEnd = (bufferEnd >= d) || (this.video_.currentTime >= d); if (!this.buffering_) { // If there are no buffered ranges but the playhead is at the end of // the video then we shouldn't enter a buffering state. if (!this.video_.paused && !atEnd && buffered < threshold) { this.enterBufferingState_(); } } else { var bufferingGoal = this.videoSource_.getBufferingGoal(); if (atEnd || buffered > bufferingGoal) { this.endBufferingState_(); this.video_.play(); } } }; /** * The threshold for underflow, in seconds. If there is less than this amount * of data buffered, we will consider the player to be out of data. * * @private {number} * @const */ shaka.player.Player.UNDERFLOW_THRESHOLD_ = 0.2; /** * A fudge factor to apply to buffered ranges and durations to determine if the * video has buffered all available content. * * @private {number} * @const */ shaka.player.Player.BUFFERED_FUDGE_FACTOR_ = 0.05; /** * The number of seconds for each rewind update interval. * * @private {number} * @const */ shaka.player.Player.REWIND_UPDATE_INTERVAL_ = 0.25; /** * 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.' }; /** * The ID of a style element used to control text styles. * * @private {string} * @const */ shaka.player.Player.STYLE_ELEMENT_ID_ = 'ShakaPlayerTextStyle';