diff --git a/build/types/core b/build/types/core index a5d7544c1..5e5edae9c 100644 --- a/build/types/core +++ b/build/types/core @@ -53,10 +53,6 @@ +../../lib/polyfill/all.js -+../../lib/routing/node.js -+../../lib/routing/payload.js -+../../lib/routing/walker.js - +../../lib/cea/dummy_cea_parser.js +../../lib/cea/dummy_caption_decoder.js @@ -101,6 +97,7 @@ +../../lib/util/mp4_generator.js +../../lib/util/mp4_parser.js +../../lib/util/multi_map.js ++../../lib/util/mutex.js +../../lib/util/networking.js +../../lib/util/object_utils.js +../../lib/util/operation_manager.js diff --git a/demo/main.js b/demo/main.js index 2a5ca2c15..68596bdac 100644 --- a/demo/main.js +++ b/demo/main.js @@ -709,15 +709,15 @@ shakaDemo.Main = class { // Does the browser support the asset's manifest type? if (asset.features.includes(shakaAssets.Feature.DASH) && - !this.support_.manifest['mpd']) { + !this.support_.manifest['application/dash+xml']) { return 'Your browser does not support MPEG-DASH manifests.'; } if (asset.features.includes(shakaAssets.Feature.HLS) && - !this.support_.manifest['m3u8']) { + !this.support_.manifest['application/x-mpegurl']) { return 'Your browser does not support HLS manifests.'; } if (asset.features.includes(shakaAssets.Feature.MSS) && - !this.support_.manifest['ism']) { + !this.support_.manifest['application/vnd.ms-sstr+xml']) { return 'Your browser does not support MSS manifests.'; } diff --git a/docs/tutorials/basic-usage.md b/docs/tutorials/basic-usage.md index 6b12e5f88..492492725 100644 --- a/docs/tutorials/basic-usage.md +++ b/docs/tutorials/basic-usage.md @@ -52,7 +52,8 @@ function initApp() { async function initPlayer() { // Create a Player instance. const video = document.getElementById('video'); - const player = new shaka.Player(video); + const player = new shaka.Player(); + async player.attach(video); // Attach player to the window to make it easy to access in the JS console. window.player = player; diff --git a/docs/tutorials/errors.md b/docs/tutorials/errors.md index 7b12978b8..cc0978e11 100644 --- a/docs/tutorials/errors.md +++ b/docs/tutorials/errors.md @@ -17,7 +17,8 @@ const handleError = (error) => { } }; -const player = new shaka.Player(video); +const player = new shaka.Player(); +await player.attach(video); // handle errors that occur after load player.addEventListener('error', handleError); diff --git a/docs/tutorials/offline.md b/docs/tutorials/offline.md index 2bb580767..cdffcdadb 100644 --- a/docs/tutorials/offline.md +++ b/docs/tutorials/offline.md @@ -79,14 +79,14 @@ the end of the tutorial. ```js // myapp.js -function initApp() { +async function initApp() { // Install built-in polyfills to patch browser incompatibilities. shaka.polyfill.installAll(); // Check to see if the browser supports the basic APIs Shaka needs. if (shaka.Player.isBrowserSupported()) { // Everything looks good! - initPlayer(); + await initPlayer(); } else { // This browser does not have the minimum set of APIs we need. console.error('Browser not supported!'); @@ -99,10 +99,11 @@ function initApp() { window.addEventListener('offline', updateOnlineStatus); } -function initPlayer() { +async function initPlayer() { // Create a Player instance. const video = document.getElementById('video'); - const player = new shaka.Player(video); + const player = new shaka.Player(); + await player.attach(video); // Attach player and storage to the window to make it easy to access // in the JS console and so we can access it in other methods. @@ -437,14 +438,14 @@ That’s it! For your convenience, here is the completed code: ```js // myapp.js -function initApp() { +async function initApp() { // Install built-in polyfills to patch browser incompatibilities. shaka.polyfill.installAll(); // Check to see if the browser supports the basic APIs Shaka needs. if (shaka.Player.isBrowserSupported()) { // Everything looks good! - initPlayer(); + await initPlayer(); } else { // This browser does not have the minimum set of APIs we need. console.error('Browser not supported!'); @@ -457,10 +458,11 @@ function initApp() { window.addEventListener('offline', updateOnlineStatus); } -function initPlayer() { +async function initPlayer() { // Create a Player instance. const video = document.getElementById('video'); - const player = new shaka.Player(video); + const player = new shaka.Player(); + await player.attach(video); // Attach player and storage to the window to make it easy to access // in the JS console and so we can access it in other methods. diff --git a/docs/tutorials/ui.md b/docs/tutorials/ui.md index 11e995fd3..08646514a 100644 --- a/docs/tutorials/ui.md +++ b/docs/tutorials/ui.md @@ -203,7 +203,8 @@ constructor. ```js // "local" because it is for local playback only, as opposed to the player proxy // object, which will route your calls to the ChromeCast receiver as necessary. -const localPlayer = new shaka.Player(videoElement); +const localPlayer = new shaka.Player(); +await localPlayer.attach(videoElement); // "Overlay" because the UI will add DOM elements inside the container, // to visually overlay the video element const ui = new shaka.ui.Overlay(localPlayer, videoContainerElement, diff --git a/lib/cast/cast_receiver.js b/lib/cast/cast_receiver.js index eda1f8ac7..97fb08ec3 100644 --- a/lib/cast/cast_receiver.js +++ b/lib/cast/cast_receiver.js @@ -408,7 +408,7 @@ shaka.cast.CastReceiver = class extends shaka.util.FakeEventTarget { } catch (error) { // Pass any errors through to the app. goog.asserts.assert(error instanceof shaka.util.Error, - 'Wrong error type!'); + 'Wrong error type! Error: ' + error); const eventType = shaka.util.FakeEvent.EventName.Error; const data = (new Map()).set('detail', error); const event = new shaka.util.FakeEvent(eventType, data); diff --git a/lib/dash/dash_parser.js b/lib/dash/dash_parser.js index 67dc7542a..cd89ec7dc 100644 --- a/lib/dash/dash_parser.js +++ b/lib/dash/dash_parser.js @@ -2254,8 +2254,6 @@ shaka.dash.DashParser.GenerateSegmentIndexFunction; shaka.dash.DashParser.StreamInfo; -shaka.media.ManifestParser.registerParserByExtension( - 'mpd', () => new shaka.dash.DashParser()); shaka.media.ManifestParser.registerParserByMime( 'application/dash+xml', () => new shaka.dash.DashParser()); shaka.media.ManifestParser.registerParserByMime( diff --git a/lib/hls/hls_parser.js b/lib/hls/hls_parser.js index 059a5fead..63d397c92 100644 --- a/lib/hls/hls_parser.js +++ b/lib/hls/hls_parser.js @@ -4003,8 +4003,6 @@ shaka.hls.HlsParser.PresentationType_ = { LIVE: 'LIVE', }; -shaka.media.ManifestParser.registerParserByExtension( - 'm3u8', () => new shaka.hls.HlsParser()); shaka.media.ManifestParser.registerParserByMime( 'application/x-mpegurl', () => new shaka.hls.HlsParser()); shaka.media.ManifestParser.registerParserByMime( diff --git a/lib/media/manifest_parser.js b/lib/media/manifest_parser.js index 264d4950d..3d5798c02 100644 --- a/lib/media/manifest_parser.js +++ b/lib/media/manifest_parser.js @@ -6,10 +6,8 @@ goog.provide('shaka.media.ManifestParser'); -goog.require('goog.Uri'); +goog.require('shaka.Deprecate'); goog.require('shaka.log'); -goog.require('shaka.net.NetworkingEngine'); -goog.require('shaka.net.NetworkingUtils'); goog.require('shaka.util.Error'); goog.require('shaka.util.Platform'); @@ -29,7 +27,9 @@ shaka.media.ManifestParser = class { * @export */ static registerParserByExtension(extension, parserFactory) { - shaka.media.ManifestParser.parsersByExtension[extension] = parserFactory; + shaka.Deprecate.deprecateFeature(5, + 'ManifestParser', + 'Please use an ManifestParser with registerParserByMime function.'); } @@ -71,9 +71,6 @@ shaka.media.ManifestParser = class { for (const type in ManifestParser.parsersByMime) { support[type] = true; } - for (const type in ManifestParser.parsersByExtension) { - support[type] = true; - } } // Make sure all well-known types are tested as well, just to show an @@ -87,14 +84,6 @@ shaka.media.ManifestParser = class { // SmoothStreaming 'application/vnd.ms-sstr+xml', ]; - const testExtensions = { - // DASH - 'mpd': 'application/dash+xml', - // HLS - 'm3u8': 'application/x-mpegurl', - // SmoothStreaming - 'ism': 'application/vnd.ms-sstr+xml', - }; for (const type of testMimeTypes) { // Only query our parsers for MSE-enabled platforms. Otherwise, query a @@ -106,18 +95,6 @@ shaka.media.ManifestParser = class { } } - for (const extension in testExtensions) { - // Only query our parsers for MSE-enabled platforms. Otherwise, query a - // temporary media element for native support for these MIME type for the - // extension. - if (shaka.util.Platform.supportsMediaSource()) { - support[extension] = !!ManifestParser.parsersByExtension[extension]; - } else { - const type = testExtensions[extension]; - support[extension] = shaka.util.Platform.supportsMediaType(type); - } - } - return support; } @@ -127,12 +104,10 @@ shaka.media.ManifestParser = class { * parse the manifest at |uri|. * * @param {string} uri - * @param {!shaka.net.NetworkingEngine} netEngine - * @param {shaka.extern.RetryParameters} retryParams * @param {?string} mimeType - * @return {!Promise.} + * @return {shaka.extern.ManifestParser.Factory} */ - static async getFactory(uri, netEngine, retryParams, mimeType) { + static getFactory(uri, mimeType) { const ManifestParser = shaka.media.ManifestParser; // Try using the MIME type we were given. @@ -146,34 +121,6 @@ shaka.media.ManifestParser = class { 'Could not determine manifest type using MIME type ', mimeType); } - const extension = ManifestParser.getExtension(uri); - if (extension) { - const factory = ManifestParser.parsersByExtension[extension]; - if (factory) { - return factory; - } - - shaka.log.warning( - 'Could not determine manifest type for extension ', extension); - } else { - shaka.log.warning('Could not find extension for ', uri); - } - - if (!mimeType) { - mimeType = await shaka.net.NetworkingUtils.getMimeType( - uri, netEngine, retryParams); - - if (mimeType) { - const factory = shaka.media.ManifestParser.parsersByMime[mimeType]; - if (factory) { - return factory; - } - - shaka.log.warning('Could not determine manifest type using MIME type', - mimeType); - } - } - throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.MANIFEST, @@ -183,35 +130,15 @@ shaka.media.ManifestParser = class { /** - * @param {string} uri - * @return {string} - */ - static getExtension(uri) { - const uriObj = new goog.Uri(uri); - const uriPieces = uriObj.getPath().split('/'); - const uriFilename = uriPieces.pop(); - const filenamePieces = uriFilename.split('.'); - - // Only one piece means there is no extension. - if (filenamePieces.length == 1) { - return ''; - } - - return filenamePieces.pop().toLowerCase(); - } - - - /** - * Determines whether or not this URI and MIME type are supported by our own + * Determines whether or not the MIME type is supported by our own * manifest parsers on this platform. This takes into account whether or not * MediaSource is available, as well as which parsers are registered to the * system. * - * @param {string} uri * @param {string} mimeType * @return {boolean} */ - static isSupported(uri, mimeType) { + static isSupported(mimeType) { // Without MediaSource, our own parsers are useless. if (!shaka.util.Platform.supportsMediaSource()) { return false; @@ -221,11 +148,6 @@ shaka.media.ManifestParser = class { return true; } - const extension = shaka.media.ManifestParser.getExtension(uri); - if (extension in shaka.media.ManifestParser.parsersByExtension) { - return true; - } - return false; } }; @@ -273,11 +195,3 @@ shaka.media.ManifestParser.AccessibilityPurpose = { shaka.media.ManifestParser.parsersByMime = {}; -/** - * Contains the parser factory functions indexed by file extension. - * - * @type {!Object.} - */ -shaka.media.ManifestParser.parsersByExtension = {}; - - diff --git a/lib/media/media_source_engine.js b/lib/media/media_source_engine.js index d0a8a5f56..505e14519 100644 --- a/lib/media/media_source_engine.js +++ b/lib/media/media_source_engine.js @@ -514,8 +514,8 @@ shaka.media.MediaSourceEngine = class { shaka.util.Error.Category.MEDIA, shaka.util.Error.Code.MEDIA_SOURCE_OPERATION_THREW, exception, - 'The mediaSource_ status was' + this.mediaSource_.readyState + - 'expected \'open\''); + 'The mediaSource_ status was ' + this.mediaSource_.readyState + + ' expected \'open\''); } if (this.sequenceMode_) { diff --git a/lib/mss/mss_parser.js b/lib/mss/mss_parser.js index c52c1a690..8c676f92e 100644 --- a/lib/mss/mss_parser.js +++ b/lib/mss/mss_parser.js @@ -1086,7 +1086,5 @@ shaka.mss.MssParser.Context; */ shaka.mss.MssParser.TimeRange; -shaka.media.ManifestParser.registerParserByExtension( - 'ism', () => new shaka.mss.MssParser()); shaka.media.ManifestParser.registerParserByMime( 'application/vnd.ms-sstr+xml', () => new shaka.mss.MssParser()); diff --git a/lib/offline/storage.js b/lib/offline/storage.js index 59d8d5f6a..b63006a02 100644 --- a/lib/offline/storage.js +++ b/lib/offline/storage.js @@ -12,6 +12,7 @@ goog.require('shaka.log'); goog.require('shaka.media.DrmEngine'); goog.require('shaka.media.ManifestParser'); goog.require('shaka.net.NetworkingEngine'); +goog.require('shaka.net.NetworkingUtils'); goog.require('shaka.offline.DownloadInfo'); goog.require('shaka.offline.DownloadManager'); goog.require('shaka.offline.OfflineUri'); @@ -271,10 +272,13 @@ shaka.offline.Storage = class { goog.asserts.assert( this.networkingEngine_, 'Should not call |store| after |destroy|'); - const factory = await shaka.media.ManifestParser.getFactory( + if (!mimeType) { + mimeType = await shaka.net.NetworkingUtils.getMimeType( + uri, this.networkingEngine_, config.manifest.retryParameters); + } + + const factory = shaka.media.ManifestParser.getFactory( uri, - this.networkingEngine_, - config.manifest.retryParameters, mimeType || null); return factory(); diff --git a/lib/player.js b/lib/player.js index e427f315e..48db9f772 100644 --- a/lib/player.js +++ b/lib/player.js @@ -7,8 +7,9 @@ goog.provide('shaka.Player'); goog.require('goog.asserts'); -goog.require('shaka.Deprecate'); +goog.require('goog.Uri'); goog.require('shaka.config.AutoShowText'); +goog.require('shaka.Deprecate'); goog.require('shaka.log'); goog.require('shaka.media.AdaptationSetCriteria'); goog.require('shaka.media.BufferingObserver'); @@ -32,13 +33,11 @@ goog.require('shaka.media.StreamingEngine'); goog.require('shaka.media.TimeRangesUtils'); goog.require('shaka.net.NetworkingEngine'); goog.require('shaka.net.NetworkingUtils'); -goog.require('shaka.routing.Walker'); goog.require('shaka.text.SimpleTextDisplayer'); goog.require('shaka.text.StubTextDisplayer'); goog.require('shaka.text.TextEngine'); goog.require('shaka.text.UITextDisplayer'); goog.require('shaka.text.WebVttGenerator'); -goog.require('shaka.util.AbortableOperation'); goog.require('shaka.util.BufferUtils'); goog.require('shaka.util.CmcdManager'); goog.require('shaka.util.ConfigUtils'); @@ -51,6 +50,7 @@ goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); goog.require('shaka.util.MediaReadyState'); goog.require('shaka.util.MimeUtils'); +goog.require('shaka.util.Mutex'); goog.require('shaka.util.ObjectUtils'); goog.require('shaka.util.Platform'); goog.require('shaka.util.PlayerConfiguration'); @@ -60,8 +60,6 @@ goog.require('shaka.util.StreamUtils'); goog.require('shaka.util.Timer'); goog.require('shaka.lcevc.Dec'); goog.requireType('shaka.media.PresentationTimeline'); -goog.requireType('shaka.routing.Node'); -goog.requireType('shaka.routing.Payload'); /** @@ -87,18 +85,6 @@ goog.requireType('shaka.routing.Payload'); * @exportDoc */ -/** - * @event shaka.Player.StateIdleEvent - * @description Fired when the player has stopped changing states and will - * remain idle until a new state change request (e.g. load, - * attach, etc.) is made. - * @property {string} type - * 'onstateidle' - * @property {string} state - * The name of the state that the player stopped in. - * @exportDoc - */ - /** * @event shaka.Player.EmsgEvent * @description Fired when an emsg box is found in a segment. @@ -525,6 +511,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget { /** @private {shaka.media.Playhead} */ this.playhead_ = null; + /** + * Incremented whenever a top-level operation (load, attach, etc) is + * performed. + * Used to determine if a load operation has been interrupted. + * @private {number} + */ + this.operationId_ = 0; + + /** @private {!shaka.util.Mutex} */ + this.mutex_ = new shaka.util.Mutex(); + /** * The playhead observers are used to monitor the position of the playhead * and some other source of data (e.g. buffered content), and raise events. @@ -578,6 +575,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { /** @private {?string} */ this.assetUri_ = null; + /** @private {?number} */ + this.startTime_ = null; + /** @private {boolean} */ this.fullyLoaded_ = false; @@ -640,15 +640,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { /** @private {!Array.} */ this.cleanupOnUnload_ = []; - /** - * This playback start position will be used when - * updateStartTime() has been called to provide an updated - * start position during the media loading process. - * - * @private {?number} - */ - this.updatedStartTime_ = null; - if (dependencyInjector) { dependencyInjector(this); } @@ -674,117 +665,6 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.retryStreaming(); }); - /** @private {shaka.routing.Node} */ - this.detachNode_ = {name: 'detach'}; - /** @private {shaka.routing.Node} */ - this.attachNode_ = {name: 'attach'}; - /** @private {shaka.routing.Node} */ - this.unloadNode_ = {name: 'unload'}; - /** @private {shaka.routing.Node} */ - this.parserNode_ = {name: 'manifest-parser'}; - /** @private {shaka.routing.Node} */ - this.manifestNode_ = {name: 'manifest'}; - /** @private {shaka.routing.Node} */ - this.mediaSourceNode_ = {name: 'media-source'}; - /** @private {shaka.routing.Node} */ - this.drmNode_ = {name: 'drm-engine'}; - /** @private {shaka.routing.Node} */ - this.loadNode_ = {name: 'load'}; - /** @private {shaka.routing.Node} */ - this.srcEqualsDrmNode_ = {name: 'src-equals-drm-engine'}; - /** @private {shaka.routing.Node} */ - this.srcEqualsNode_ = {name: 'src-equals'}; - - const AbortableOperation = shaka.util.AbortableOperation; - - const actions = new Map(); - actions.set(this.attachNode_, (has, wants) => { - return AbortableOperation.notAbortable(this.onAttach_(has, wants)); - }); - actions.set(this.detachNode_, (has, wants) => { - return AbortableOperation.notAbortable(this.onDetach_(has, wants)); - }); - actions.set(this.unloadNode_, (has, wants) => { - return AbortableOperation.notAbortable(this.onUnload_(has, wants)); - }); - actions.set(this.mediaSourceNode_, (has, wants) => { - const p = this.onInitializeMediaSourceEngine_(has, wants); - return AbortableOperation.notAbortable(p); - }); - actions.set(this.parserNode_, (has, wants) => { - const p = this.onInitializeParser_(has, wants); - return AbortableOperation.notAbortable(p); - }); - actions.set(this.manifestNode_, (has, wants) => { - // This action is actually abortable, so unlike the other callbacks, this - // one will return an abortable operation. - return this.onParseManifest_(has, wants); - }); - actions.set(this.drmNode_, (has, wants) => { - const p = this.onInitializeDrm_(has, wants); - return AbortableOperation.notAbortable(p); - }); - actions.set(this.loadNode_, (has, wants) => { - return AbortableOperation.notAbortable(this.onLoad_(has, wants)); - }); - - actions.set(this.srcEqualsDrmNode_, (has, wants) => { - const p = this.onInitializeSrcEqualsDrm_(has, wants); - return AbortableOperation.notAbortable(p); - }); - actions.set(this.srcEqualsNode_, (has, wants) => { - return this.onSrcEquals_(has, wants); - }); - - /** @private {shaka.routing.Walker.Implementation} */ - const walkerImplementation = { - getNext: (at, has, goingTo, wants) => { - return this.getNextStep_(at, has, goingTo, wants); - }, - enterNode: (node, has, wants) => { - this.dispatchEvent(this.makeEvent_( - /* name= */ shaka.util.FakeEvent.EventName.OnStateChange, - /* data= */ (new Map()).set('state', node.name))); - - const action = actions.get(node); - return action(has, wants); - }, - handleError: async (has, error) => { - shaka.log.warning('The walker saw an error:'); - if (error instanceof shaka.util.Error) { - shaka.log.warning('Error Code:', error.code); - } else { - shaka.log.warning('Error Message:', error.message); - shaka.log.warning('Error Stack:', error.stack); - } - - // Regardless of what state we were in, if there is an error, we unload. - // This ensures that any initialized system will be torn-down and we - // will go back to a safe foundation. We assume that the media element - // is always safe to use after an error. - await this.onUnload_(has, shaka.Player.createEmptyPayload_()); - - // There are only two nodes that come before we start loading content, - // attach and detach. If we have a media element, it means we were - // attached to the element, and we can safely return to the attach state - // (we assume that the video element is always re-usable). We favor - // returning to the attach node since it means that the app won't need - // to re-attach if it saw an error. - return has.mediaElement ? this.attachNode_ : this.detachNode_; - }, - onIdle: (node) => { - this.dispatchEvent(this.makeEvent_( - /* name= */ shaka.util.FakeEvent.EventName.OnStateIdle, - /* data= */ (new Map()).set('state', node.name))); - }, - }; - - /** @private {shaka.routing.Walker} */ - this.walker_ = new shaka.routing.Walker( - this.detachNode_, - shaka.Player.createEmptyPayload_(), - walkerImplementation); - /** @private {shaka.util.Timer} */ this.checkVariantsTimer_ = new shaka.util.Timer(() => this.checkVariants_()); @@ -793,6 +673,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // the LAST thing we do in the constructor because conceptually it relies on // player having been initialized. if (mediaElement) { + shaka.Deprecate.deprecateFeature(5, + 'Player', + 'Please migrate from initializing Player with a mediaElement; ' + + 'use the attach method instead.'); this.attach(mediaElement, /* initializeMediaSource= */ true); } } @@ -837,7 +721,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const tracks = this.getVariantTracks(); if (tracks && tracks[0] && tracks[0].videoMimeType == - shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_['ts']) { + shaka.Player.EXTENSIONS_TO_MIME_TYPES_['ts']) { const edge = shaka.util.Platform.isEdge() || shaka.util.Platform.isLegacyEdge(); if (edge) { @@ -883,51 +767,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // If LCEVC Decoder exists close it. this.closeLcevcDec_(); + const detachPromise = this.detach(); + // Mark as "dead". This should stop external-facing calls from changing our // internal state any more. This will stop calls to |attach|, |detach|, etc. // from interrupting our final move to the detached state. this.loadMode_ = shaka.Player.LoadMode.DESTROYED; - const payload = shaka.Player.createEmptyPayload_(); - payload.keepAdManager = false; - - // Because we have set |loadMode_| to |DESTROYED| we can't call |detach|. We - // must talk to |this.walker_| directly. - const events = this.walker_.startNewRoute((currentPayload) => { - return { - node: this.detachNode_, - payload: payload, - interruptible: false, - }; - }); - - // Wait until the detach has finished so that we don't interrupt it by - // calling |destroy| on |this.walker_|. To avoid failing here, we always - // resolve the promise. - await new Promise((resolve) => { - events.onStart = () => { - shaka.log.info('Preparing to destroy walker...'); - }; - events.onEnd = () => { - resolve(); - }; - events.onCancel = () => { - goog.asserts.assert(false, - 'Our final detach call should never be cancelled.'); - resolve(); - }; - events.onError = () => { - goog.asserts.assert(false, - 'Our final detach call should never see an error'); - resolve(); - }; - events.onSkip = () => { - goog.asserts.assert(false, - 'Our final detach call should never be skipped'); - resolve(); - }; - }); - await this.walker_.destroy(); + await detachPromise; // Tear-down the event managers to ensure handlers stop firing. if (this.globalEventManager_) { @@ -1075,56 +922,60 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Tell the player to use mediaElement for all load - * requests until detach or destroy are called. - * - *

- * Calling attach with initializedMediaSource=true - * will tell the player to take the initial load step and initialize media - * source. - * - *

- * Calls to attach will interrupt any in-progress calls to - * load but cannot interrupt calls to attach, - * detach, or unload. + * Makes a fires an event corresponding to entering a state of the loading + * process. + * @param {string} nodeName + * @private + */ + makeStateChangeEvent_(nodeName) { + this.dispatchEvent(this.makeEvent_( + /* name= */ shaka.util.FakeEvent.EventName.OnStateChange, + /* data= */ (new Map()).set('state', nodeName))); + } + + /** + * Attaches the player to a media element. + * If the player was already attached to a media element, first detaches from + * that media element. * * @param {!HTMLMediaElement} mediaElement * @param {boolean=} initializeMediaSource * @return {!Promise} * @export */ - attach(mediaElement, initializeMediaSource = true) { + async attach(mediaElement, initializeMediaSource = true) { // Do not allow the player to be used after |destroy| is called. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) { - return Promise.reject(this.createAbortLoadError_()); + throw this.createAbortLoadError_(); } - const payload = shaka.Player.createEmptyPayload_(); - payload.mediaElement = mediaElement; - - // If the platform does not support media source, we will never want to - // initialize media source. - if (!shaka.util.Platform.supportsMediaSource()) { - initializeMediaSource = false; + if (this.video_ && this.video_ != mediaElement) { + await this.detach(); } - const destination = initializeMediaSource ? - this.mediaSourceNode_ : - this.attachNode_; + if (await this.atomicOperationAcquireMutex_('attach')) { + return; + } - // Do not allow this route to be interrupted because calls after this attach - // call will depend on the media element being attached. - const events = this.walker_.startNewRoute((currentPayload) => { - return { - node: destination, - payload: payload, - interruptible: false, - }; - }); + try { + this.makeStateChangeEvent_('attach'); - // List to the events that can occur with our request. - events.onStart = () => shaka.log.info('Starting attach...'); - return this.wrapWalkerListenersWithPromise_(events); + const onError = (error) => this.onVideoError_(error); + this.attachEventManager_.listen(mediaElement, 'error', onError); + this.video_ = mediaElement; + + // Only initialize media source if the platform supports it. + if (initializeMediaSource && + shaka.util.Platform.supportsMediaSource() && + !this.mediaSourceEngine_) { + await this.initializeMediaSourceEngineInner_(); + } + } catch (error) { + await this.detach(); + throw error; + } finally { + this.mutex_.release(); + } } @@ -1140,131 +991,243 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Tell the player to stop using its current media element. If the player is: - *

    - *
  • detached, this will do nothing, - *
  • attached, this will release the media element, - *
  • loading, this will abort loading, unload, and release the media - * element, - *
  • playing content, this will stop playback, unload, and release the - * media element. - *
+ * Detach the player from the current media element. Leaves the player in a + * state where it cannot play media, until it has been attached to something + * else. * - *

- * Calls to detach will interrupt any in-progress calls to - * load but cannot interrupt calls to attach, - * detach, or unload. + * @param {boolean=} keepAdManager * * @return {!Promise} * @export */ - detach() { + async detach(keepAdManager = false) { // Do not allow the player to be used after |destroy| is called. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) { - return Promise.reject(this.createAbortLoadError_()); + throw this.createAbortLoadError_(); } - // Tell the walker to go "detached", but do not allow it to be interrupted. - // If it could be interrupted it means that our media element could fall out - // of sync. - const events = this.walker_.startNewRoute((currentPayload) => { - return { - node: this.detachNode_, - payload: shaka.Player.createEmptyPayload_(), - interruptible: false, - }; - }); + await this.unload(/* initializeMediaSource= */ false, keepAdManager); - events.onStart = () => shaka.log.info('Starting detach...'); - return this.wrapWalkerListenersWithPromise_(events); + if (await this.atomicOperationAcquireMutex_('detach')) { + return; + } + + try { + // If we were going from "detached" to "detached" we wouldn't have + // a media element to detach from. + if (this.video_) { + this.attachEventManager_.removeAll(); + this.video_ = null; + } + + this.makeStateChangeEvent_('detach'); + + if (this.adManager_ && !keepAdManager) { + // The ad manager is specific to the video, so detach it too. + this.adManager_.release(); + } + } finally { + this.mutex_.release(); + } } /** - * Tell the player to either return to: - *

    - *
  • detached (when it does not have a media element), - *
  • attached (when it has a media element and - * initializedMediaSource=false) - *
  • media source initialized (when it has a media element and - * initializedMediaSource=true) - *
- * - *

- * Calls to unload will interrupt any in-progress calls to - * load but cannot interrupt calls to attach, - * detach, or unload. + * Tries to acquire the mutex, and then returns if the operation should end + * early due to someone else starting a mutex-acquiring operation. + * Meant for operations that can't be interrupted midway through (e.g. + * everything but load). + * @param {string} mutexIdentifier + * @return {!Promise.} endEarly If false, the calling context will + * need to release the mutex. + * @private + */ + async atomicOperationAcquireMutex_(mutexIdentifier) { + const operationId = ++this.operationId_; + await this.mutex_.acquire(mutexIdentifier); + if (operationId != this.operationId_) { + this.mutex_.release(); + return true; + } + return false; + } + + /** + * Unloads the currently playing stream, if any. * * @param {boolean=} initializeMediaSource * @param {boolean=} keepAdManager * @return {!Promise} * @export */ - unload(initializeMediaSource = true, keepAdManager = false) { - // Do not allow the player to be used after |destroy| is called. - if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) { - return Promise.reject(this.createAbortLoadError_()); + async unload(initializeMediaSource = true, keepAdManager = false) { + // Set the load mode to unload right away so that all the public methods + // will stop using the internal components. We need to make sure that we + // are not overriding the destroyed state because we will unload when we are + // destroying the player. + if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) { + this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED; } - this.fullyLoaded_ = false; - - // If the platform does not support media source, we will never want to - // initialize media source. - if (!shaka.util.Platform.supportsMediaSource()) { - initializeMediaSource = false; + if (await this.atomicOperationAcquireMutex_('unload')) { + return; } - // If LCEVC Decoder exists close it. - this.closeLcevcDec_(); + try { + this.fullyLoaded_ = false; + this.makeStateChangeEvent_('unload'); - // Since we are going either to attached or detached (through unloaded), we - // can't allow it to be interrupted or else we could lose track of what - // media element we are suppose to use. - // - // Using the current payload, we can determine which node we want to go to. - // If we have a media element, we want to go back to attached. If we have no - // media element, we want to go back to detached. - const payload = shaka.Player.createEmptyPayload_(); - payload.keepAdManager = keepAdManager; - - const events = this.walker_.startNewRoute((currentPayload) => { - // When someone calls |unload| we can either be before attached or - // detached (there is nothing stopping someone from calling |detach| when - // we are already detached). - // - // If we are attached to the correct element, we can tear down the - // previous playback components and go to the attached media source node - // depending on whether or not the caller wants to pre-init media source. - // - // If we don't have a media element, we assume that we are already at the - // detached node - but only the walker knows that. To ensure we are - // actually there, we tell the walker to go to detach. While this is - // technically unnecessary, it ensures that we are in the state we want - // to be in and ready for the next request. - let destination = null; - - if (currentPayload.mediaElement && initializeMediaSource) { - destination = this.mediaSourceNode_; - } else if (currentPayload.mediaElement) { - destination = this.attachNode_; - } else { - destination = this.detachNode_; + // If the platform does not support media source, we will never want to + // initialize media source. + if (initializeMediaSource && !shaka.util.Platform.supportsMediaSource()) { + initializeMediaSource = false; } - goog.asserts.assert(destination, 'We should have picked a destination.'); + // If LCEVC Decoder exists close it. + this.closeLcevcDec_(); - // Copy over the media element because we want to keep using the same - // element - the other values don't matter. - payload.mediaElement = currentPayload.mediaElement; + // Run any general cleanup tasks now. This should be here at the top, + // right after setting loadMode_, so that internal components still exist + // as they did when the cleanup tasks were registered in the array. + const cleanupTasks = this.cleanupOnUnload_.map((cb) => cb()); + this.cleanupOnUnload_ = []; + await Promise.all(cleanupTasks); - return { - node: destination, - payload: payload, - interruptible: false, - }; - }); + // Dispatch the unloading event. + this.dispatchEvent( + this.makeEvent_(shaka.util.FakeEvent.EventName.Unloading)); - events.onStart = () => shaka.log.info('Starting unload...'); - return this.wrapWalkerListenersWithPromise_(events); + // Release the region timeline, which is created when parsing the + // manifest. + if (this.regionTimeline_) { + this.regionTimeline_.release(); + this.regionTimeline_ = null; + } + + // In most cases we should have a media element. The one exception would + // be if there was an error and we, by chance, did not have a media + // element. + if (this.video_) { + this.loadEventManager_.removeAll(); + } + + // Stop the variant checker timer + this.checkVariantsTimer_.stop(); + + // Some observers use some playback components, shutting down the + // observers first ensures that they don't try to use the playback + // components mid-destroy. + if (this.playheadObservers_) { + this.playheadObservers_.release(); + this.playheadObservers_ = null; + } + + // Stop the parser early. Since it is at the start of the pipeline, it + // should be start early to avoid is pushing new data downstream. + if (this.parser_) { + await this.parser_.stop(); + this.parser_ = null; + this.parserFactory_ = null; + } + + // Abr Manager will tell streaming engine what to do, so we need to stop + // it before we destroy streaming engine. Unlike with the other + // components, we do not release the instance, we will reuse it in later + // loads. + if (this.abrManager_) { + await this.abrManager_.stop(); + } + + // Streaming engine will push new data to media source engine, so we need + // to shut it down before destroy media source engine. + if (this.streamingEngine_) { + await this.streamingEngine_.destroy(); + this.streamingEngine_ = null; + } + + if (this.playRateController_) { + this.playRateController_.release(); + this.playRateController_ = null; + } + + // Playhead is used by StreamingEngine, so we can't destroy this until + // after StreamingEngine has stopped. + if (this.playhead_) { + this.playhead_.release(); + this.playhead_ = null; + } + + // Media source engine holds onto the media element, and in order to + // detach the media keys (with drm engine), we need to break the + // connection between media source engine and the media element. + if (this.mediaSourceEngine_) { + await this.mediaSourceEngine_.destroy(); + this.mediaSourceEngine_ = null; + } + + if (this.adManager_ && !keepAdManager) { + this.adManager_.onAssetUnload(); + } + + // In order to unload a media element, we need to remove the src attribute + // and then load again. When we destroy media source engine, this will be + // done for us, but for src=, we need to do it here. + // + // DrmEngine requires this to be done before we destroy DrmEngine itself. + if (this.video_ && this.video_.src) { + // TODO: Investigate this more. Only reproduces on Firefox 69. + // Introduce a delay before detaching the video source. We are seeing + // spurious Promise rejections involving an AbortError in our tests + // otherwise. + await new Promise( + (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1)); + + this.video_.removeAttribute('src'); + this.video_.load(); + // Remove all track nodes + while (this.video_.lastChild) { + this.video_.removeChild(this.video_.firstChild); + } + } + + if (this.drmEngine_) { + await this.drmEngine_.destroy(); + this.drmEngine_ = null; + } + + this.assetUri_ = null; + this.bufferObserver_ = null; + + if (this.manifest_) { + for (const variant of this.manifest_.variants) { + for (const stream of [variant.audio, variant.video]) { + if (stream && stream.segmentIndex) { + stream.segmentIndex.release(); + } + } + } + for (const stream of this.manifest_.textStreams) { + if (stream.segmentIndex) { + stream.segmentIndex.release(); + } + } + } + + this.manifest_ = null; + this.stats_ = new shaka.util.Stats(); // Replace with a clean object. + this.lastTextFactory_ = null; + + this.externalSrcEqualsThumbnailsStreams_ = []; + + // Make sure that the app knows of the new buffering state. + this.updateBufferState_(); + } finally { + this.mutex_.release(); + } + + if (initializeMediaSource && shaka.util.Platform.supportsMediaSource() && + !this.mediaSourceEngine_) { + await this.initializeMediaSourceEngineInner_(); + } } /** @@ -1277,120 +1240,191 @@ shaka.Player = class extends shaka.util.FakeEventTarget { * @export */ updateStartTime(startTime) { - this.updatedStartTime_ = startTime; + this.startTime_ = startTime; } /** - * Tell the player to load the content at assetUri and start - * playback at startTime. Before calling load, - * a call to attach must have succeeded. - * - *

- * Calls to load will interrupt any in-progress calls to - * load but cannot interrupt calls to attach, - * detach, or unload. + * Loads a new stream. + * If another stream was already playing, first unloads that stream. * * @param {string} assetUri * @param {?number=} startTime * When startTime is null or * undefined, playback will start at the default start time (0 * for VOD and liveEdge for LIVE). - * @param {string=} mimeType + * @param {?string=} mimeType * @return {!Promise} * @export */ - load(assetUri, startTime, mimeType) { - this.updatedStartTime_ = null; - - this.fullyLoaded_ = false; - + async load(assetUri, startTime = null, mimeType) { // Do not allow the player to be used after |destroy| is called. if (this.loadMode_ == shaka.Player.LoadMode.DESTROYED) { - return Promise.reject(this.createAbortLoadError_()); + throw this.createAbortLoadError_(); } - // We dispatch the loading event when someone calls |load| because we want - // to surface the user intent. - this.dispatchEvent(this.makeEvent_(shaka.util.FakeEvent.EventName.Loading)); + // Quickly acquire the mutex, so this will wait for other top-level + // operations. + await this.mutex_.acquire('load'); + this.mutex_.release(); - // Right away we know what the asset uri and start-of-load time are. We will - // fill-in the rest of the information later. - const payload = shaka.Player.createEmptyPayload_(); - payload.uri = assetUri; - payload.startTimeOfLoad = Date.now() / 1000; - if (mimeType) { - payload.mimeType = mimeType; - } - - // Because we allow |startTime| to be optional, it means that it will be - // |undefined| when not provided. This means that we need to re-map - // |undefined| to |null| while preserving |0| as a meaningful value. - if (startTime !== undefined) { - payload.startTime = startTime; - } - - // TODO: Refactor to determine whether it's a manifest or not, and whether - // or not we can play it. Then we could return a better error than - // UNABLE_TO_GUESS_MANIFEST_TYPE for WebM in Safari. - const useSrcEquals = this.shouldUseSrcEquals_(payload); - const destination = useSrcEquals ? this.srcEqualsNode_ : this.loadNode_; - - // Allow this request to be interrupted, this will allow other requests to - // cancel a load and quickly start a new load. - const events = this.walker_.startNewRoute((currentPayload) => { - if (currentPayload.mediaElement == null) { - // Because we return null, this "new route" will not be used. - return null; - } - - // Keep using whatever media element we have right now. - payload.mediaElement = currentPayload.mediaElement; - - return { - node: destination, - payload: payload, - interruptible: true, - }; - }); - - // Stats are for a single playback/load session. Stats must be initialized - // before we allow calls to |updateStateHistory|. - this.stats_ = new shaka.util.Stats(); - - // Load's request is a little different, so we can't use our normal - // listeners-to-promise method. It is the only request where we may skip the - // request, so we need to set the on skip callback to reject with a specific - // error. - events.onStart = - () => shaka.log.info('Starting load of ' + assetUri + '...'); - return new Promise((resolve, reject) => { - events.onSkip = () => reject(new shaka.util.Error( + if (!this.video_) { + throw new shaka.util.Error( shaka.util.Error.Severity.CRITICAL, shaka.util.Error.Category.PLAYER, - shaka.util.Error.Code.NO_VIDEO_ELEMENT)); + shaka.util.Error.Code.NO_VIDEO_ELEMENT); + } - events.onEnd = () => { - resolve(); - // We dispatch the loaded event when the load promise is resolved - this.dispatchEvent( - this.makeEvent_(shaka.util.FakeEvent.EventName.Loaded)); - }; - events.onCancel = () => reject(this.createAbortLoadError_()); - events.onError = (e) => reject(e); - }); + if (this.assetUri_) { + await this.unload(/* initializeMediaSource= */ false); + } + + // Add a mechanism to detect if the load process has been interrupted by a + // call to another top-level operation (unload, load, etc). + const operationId = ++this.operationId_; + const detectInterruption = () => { + if (this.operationId_ != operationId) { + throw this.createAbortLoadError_(); + } + }; + + /** + * Wraps a given operation with mutex.acquire and mutex.release, along with + * calls to detectInterruption, to catch any other top-level calls happening + * while waiting for the mutex. + * @param {function():!Promise} operation + * @param {string} mutexIdentifier + * @return {!Promise} + */ + const mutexWrapOperation = async (operation, mutexIdentifier) => { + try { + await this.mutex_.acquire(mutexIdentifier); + detectInterruption(); + await operation(); + detectInterruption(); + } finally { + this.mutex_.release(); + } + }; + + try { + this.startTime_ = startTime; + this.fullyLoaded_ = false; + + // We dispatch the loading event when someone calls |load| because we want + // to surface the user intent. + this.dispatchEvent(this.makeEvent_( + shaka.util.FakeEvent.EventName.Loading)); + + const startTimeOfLoad = Date.now() / 1000; + + // Stats are for a single playback/load session. Stats must be initialized + // before we allow calls to |updateStateHistory|. + this.stats_ = new shaka.util.Stats(); + + this.assetUri_ = assetUri; + + if (!mimeType) { + await mutexWrapOperation(async () => { + mimeType = await this.guessMimeType_(); + }, 'guessMimeType_'); + } + const shouldUseSrcEquals = this.shouldUseSrcEquals_(assetUri, mimeType); + if (shouldUseSrcEquals) { + await mutexWrapOperation(async () => { + goog.asserts.assert(mimeType, 'We should know the mimeType by now!'); + await this.initializeSrcEqualsDrmInner_(mimeType); + }, 'initializeSrcEqualsDrmInner_'); + await mutexWrapOperation(async () => { + goog.asserts.assert(mimeType, 'We should know the mimeType by now!'); + await this.srcEqualsInner_(startTimeOfLoad, mimeType); + }, 'srcEqualsInner_'); + } else { + if (!this.mediaSourceEngine_) { + await mutexWrapOperation(async () => { + await this.initializeMediaSourceEngineInner_(); + }, 'initializeMediaSourceEngineInner_'); + } + await mutexWrapOperation(async () => { + await this.parseManifestInner_(mimeType); + }, 'parseManifestInner_'); + await mutexWrapOperation(async () => { + await this.initializeDrmInner_(); + }, 'initializeDrmInner_'); + await mutexWrapOperation(async () => { + await this.loadInner_(startTimeOfLoad); + }, 'loadInner_'); + } + this.dispatchEvent(this.makeEvent_( + shaka.util.FakeEvent.EventName.Loaded)); + } catch (error) { + if (error.code != shaka.util.Error.Code.LOAD_INTERRUPTED) { + await this.unload(/* initializeMediaSource= */ false); + } + throw error; + } } /** - * Check if src= should be used to load the asset at |uri|. Assume that media - * source is the default option, and that src= is for special cases. + * Determines the mimeType of the given asset, if we are not told that inside + * the loading process. * - * @param {shaka.routing.Payload} payload + * @return {!Promise.} mimeType + * @private + */ + async guessMimeType_() { + goog.asserts.assert(this.assetUri_, 'should have a uri by now.'); + + // If we don't have a MIME type, try to guess based on the file extension. + // Try using the uri extension. + const extension = this.getExtension_(this.assetUri_); + if (shaka.Player.EXTENSIONS_TO_MIME_TYPES_[extension]) { + return shaka.Player.EXTENSIONS_TO_MIME_TYPES_[extension]; + } + + // If no MIME type is provided, and we can't base it on extension, make a + // HEAD request to determine it. + goog.asserts.assert(this.networkingEngine_, 'Should have a net engine!'); + const retryParams = this.config_.manifest.retryParameters; + let mimeType = await shaka.net.NetworkingUtils.getMimeType( + this.assetUri_, this.networkingEngine_, retryParams); + if (mimeType == 'application/x-mpegurl' && shaka.util.Platform.isApple()) { + mimeType = 'application/vnd.apple.mpegurl'; + } + return mimeType; + } + + + /** + * @param {string} uri + * @return {string} + * @private + */ + getExtension_(uri) { + const uriObj = new goog.Uri(uri); + const uriPieces = uriObj.getPath().split('/'); + const uriFilename = uriPieces.pop(); + const filenamePieces = uriFilename.split('.'); + + // Only one piece means there is no extension. + if (filenamePieces.length == 1) { + return ''; + } + + return filenamePieces.pop().toLowerCase(); + } + + /** + * Determines if we should use src equals, based on the the mimeType (if + * known), the URI, and platform information. + * + * @param {string} assetUri + * @param {?string=} mimeType * @return {boolean} * |true| if the content should be loaded with src=, |false| if the content * should be loaded with MediaSource. * @private */ - shouldUseSrcEquals_(payload) { + shouldUseSrcEquals_(assetUri, mimeType) { const Platform = shaka.util.Platform; const MimeUtils = shaka.util.MimeUtils; @@ -1400,29 +1434,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return true; } - // The most accurate way to tell the player how to load the content is via - // MIME type. We can fall back to features of the URI if needed. - let mimeType = payload.mimeType; - const uri = payload.uri || ''; - - // If we don't have a MIME type, try to guess based on the file extension. - // TODO: Too generic to belong to ManifestParser now. Refactor. - if (!mimeType) { - // Try using the uri extension. - const extension = shaka.media.ManifestParser.getExtension(uri); - mimeType = shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[extension]; - } - - // TODO: The load graph system has a design limitation that requires routing - // destination to be chosen synchronously. This means we can only make the - // right choice about src= consistently if we have a well-known file - // extension or API-provided MIME type. Detection of MIME type from a HEAD - // request (as is done for manifest types) can't be done yet. - if (mimeType) { // If we have a MIME type, check if the browser can play it natively. // This will cover both single files and native HLS. - const mediaElement = payload.mediaElement || Platform.anyMediaElement(); + const mediaElement = this.video_ || Platform.anyMediaElement(); const canPlayNatively = mediaElement.canPlayType(mimeType) != ''; // If we can't play natively, then src= isn't an option. @@ -1431,7 +1446,7 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } const canPlayMediaSource = - shaka.media.ManifestParser.isSupported(uri, mimeType); + shaka.media.ManifestParser.isSupported(mimeType); // If MediaSource isn't an option, the native option is our only chance. if (!canPlayMediaSource) { @@ -1445,17 +1460,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // We would prefer MediaSource in some cases, and src= in others. For // example, Android has native HLS, but we'd prefer our own MediaSource // version there. + if (MimeUtils.isHlsType(mimeType)) { + // Native HLS can be preferred on any platform via this flag: + if (this.config_.streaming.preferNativeHls) { + return true; + } - // Native HLS can be preferred on any platform via this flag: - if (MimeUtils.isHlsType(mimeType) && - this.config_.streaming.preferNativeHls) { - return true; - } - - // For Safari, we have an older flag which only applies to this one - // browser: - if (Platform.isApple()) { - return this.config_.streaming.useNativeHlsOnSafari; + // For Safari, we have an older flag which only applies to this one + // browser: + if (Platform.isApple()) { + return this.config_.streaming.useNativeHlsOnSafari; + } } // In all other cases, we prefer MediaSource. @@ -1469,290 +1484,35 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * This should only be called by the load graph when it is time to attach to - * a media element. The only times this may be called are when we are being - * asked to re-attach to the current media element, or attach to a new media - * element while not attached to a media element. - * - * This method assumes that it is safe for it to execute, the load-graph is - * responsible for ensuring all assumptions are true. - * - * Attaching to a media element is defined as: - * - Registering error listeners to the media element. - * - Caching the video element for use outside of the load graph. - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants - * @return {!Promise} - * @private - */ - onAttach_(has, wants) { - // If we don't have a media element yet, it means we are entering - // "attach" from another node. - // - // If we have a media element, it should match |wants.mediaElement| - // because it means we are going from "attach" to "attach". - // - // These constraints should be maintained and guaranteed by the routing - // logic in |getNextStep_|. - goog.asserts.assert( - has.mediaElement == null || has.mediaElement == wants.mediaElement, - 'The routing logic failed. MediaElement requirement failed.'); - - if (has.mediaElement == null) { - has.mediaElement = wants.mediaElement; - - const onError = (error) => this.onVideoError_(error); - this.attachEventManager_.listen(has.mediaElement, 'error', onError); - } - - this.video_ = has.mediaElement; - - return Promise.resolve(); - } - - /** - * This should only be called by the load graph when it is time to detach from - * a media element. The only times this may be called are when we are being - * asked to detach from the current media element, or detach when we are - * already detached. - * - * This method assumes that it is safe for it to execute, the load-graph is - * responsible for ensuring all assumptions are true. - * - * Detaching from a media element is defined as: - * - Removing error listeners from the media element. - * - Dropping the cached reference to the video element. - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants - * @return {!Promise} - * @private - */ - onDetach_(has, wants) { - // If we were going from "detached" to "detached" we wouldn't have - // a media element to detach from. - if (has.mediaElement) { - this.attachEventManager_.removeAll(); - has.mediaElement = null; - } - - if (this.adManager_ && !wants.keepAdManager) { - // The ad manager is specific to the video, so detach it too. - this.adManager_.release(); - } - - // Clear our cached copy of the media element. - this.video_ = null; - - return Promise.resolve(); - } - - /** - * This should only be called by the load graph when it is time to unload all - * currently initialized playback components. Unlike the other load actions, - * this action is built to be more general. We need to do this because we - * don't know what state the player will be in before unloading (including - * after an error occurred in the middle of a transition). - * - * This method assumes that any component could be |null| and should be safe - * to call from any point in the load graph. - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants - * @return {!Promise} - * @private - */ - async onUnload_(has, wants) { - // Set the load mode to unload right away so that all the public methods - // will stop using the internal components. We need to make sure that we - // are not overriding the destroyed state because we will unload when we are - // destroying the player. - if (this.loadMode_ != shaka.Player.LoadMode.DESTROYED) { - this.loadMode_ = shaka.Player.LoadMode.NOT_LOADED; - } - - // Run any general cleanup tasks now. This should be here at the top, right - // after setting loadMode_, so that internal components still exist as they - // did when the cleanup tasks were registered in the array. - const cleanupTasks = this.cleanupOnUnload_.map((cb) => cb()); - this.cleanupOnUnload_ = []; - await Promise.all(cleanupTasks); - - // Dispatch the unloading event. - this.dispatchEvent( - this.makeEvent_(shaka.util.FakeEvent.EventName.Unloading)); - - // Remove everything that has to do with loading content from our payload - // since we are releasing everything that depended on it. - has.mimeType = null; - has.startTime = null; - has.uri = null; - - // Release the region timeline, which is created when parsing the manifest. - if (this.regionTimeline_) { - this.regionTimeline_.release(); - this.regionTimeline_ = null; - } - - // In most cases we should have a media element. The one exception would - // be if there was an error and we, by chance, did not have a media element. - if (has.mediaElement) { - this.loadEventManager_.removeAll(); - } - - // Stop the variant checker timer - this.checkVariantsTimer_.stop(); - - // Some observers use some playback components, shutting down the observers - // first ensures that they don't try to use the playback components - // mid-destroy. - if (this.playheadObservers_) { - this.playheadObservers_.release(); - this.playheadObservers_ = null; - } - - // Stop the parser early. Since it is at the start of the pipeline, it - // should be start early to avoid is pushing new data downstream. - if (this.parser_) { - await this.parser_.stop(); - this.parser_ = null; - this.parserFactory_ = null; - } - - // Abr Manager will tell streaming engine what to do, so we need to stop - // it before we destroy streaming engine. Unlike with the other components, - // we do not release the instance, we will reuse it in later loads. - if (this.abrManager_) { - await this.abrManager_.stop(); - } - - // Streaming engine will push new data to media source engine, so we need - // to shut it down before destroy media source engine. - if (this.streamingEngine_) { - await this.streamingEngine_.destroy(); - this.streamingEngine_ = null; - } - - if (this.playRateController_) { - this.playRateController_.release(); - this.playRateController_ = null; - } - - // Playhead is used by StreamingEngine, so we can't destroy this until after - // StreamingEngine has stopped. - if (this.playhead_) { - this.playhead_.release(); - this.playhead_ = null; - } - - // Media source engine holds onto the media element, and in order to detach - // the media keys (with drm engine), we need to break the connection between - // media source engine and the media element. - if (this.mediaSourceEngine_) { - await this.mediaSourceEngine_.destroy(); - this.mediaSourceEngine_ = null; - } - - if (this.adManager_ && !wants.keepAdManager) { - this.adManager_.onAssetUnload(); - } - - // In order to unload a media element, we need to remove the src attribute - // and then load again. When we destroy media source engine, this will be - // done for us, but for src=, we need to do it here. - // - // DrmEngine requires this to be done before we destroy DrmEngine itself. - if (has.mediaElement && has.mediaElement.src) { - // TODO: Investigate this more. Only reproduces on Firefox 69. - // Introduce a delay before detaching the video source. We are seeing - // spurious Promise rejections involving an AbortError in our tests - // otherwise. - await new Promise( - (resolve) => new shaka.util.Timer(resolve).tickAfter(0.1)); - - has.mediaElement.removeAttribute('src'); - has.mediaElement.load(); - // Remove all track nodes - while (has.mediaElement.lastChild) { - has.mediaElement.removeChild(has.mediaElement.firstChild); - } - } - - if (this.drmEngine_) { - await this.drmEngine_.destroy(); - this.drmEngine_ = null; - } - - this.assetUri_ = null; - this.bufferObserver_ = null; - - if (this.manifest_) { - for (const variant of this.manifest_.variants) { - for (const stream of [variant.audio, variant.video]) { - if (stream && stream.segmentIndex) { - stream.segmentIndex.release(); - } - } - } - for (const stream of this.manifest_.textStreams) { - if (stream.segmentIndex) { - stream.segmentIndex.release(); - } - } - } - - this.manifest_ = null; - this.stats_ = new shaka.util.Stats(); // Replace with a clean stats object. - this.lastTextFactory_ = null; - - this.externalSrcEqualsThumbnailsStreams_ = []; - - // Make sure that the app knows of the new buffering state. - this.updateBufferState_(); - } - - /** - * This should only be called by the load graph when it is time to initialize - * media source engine. The only time this may be called is when we are - * attached to the same media element as in the request. - * - * This method assumes that it is safe for it to execute. The load-graph is - * responsible for ensuring all assumptions are true. - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants + * Initializes the media source engine. * * @return {!Promise} * @private */ - async onInitializeMediaSourceEngine_(has, wants) { + async initializeMediaSourceEngineInner_() { goog.asserts.assert( shaka.util.Platform.supportsMediaSource(), - 'We should not be initializing media source on a platform that does ' + - 'not support media source.'); + 'We should not be initializing media source on a platform that ' + + 'does not support media source.'); goog.asserts.assert( - has.mediaElement, + this.video_, 'We should have a media element when initializing media source.'); - goog.asserts.assert( - has.mediaElement == wants.mediaElement, - '|has| and |wants| should have the same media element when ' + - 'initializing media source.'); - goog.asserts.assert( this.mediaSourceEngine_ == null, 'We should not have a media source engine yet.'); + this.makeStateChangeEvent_('media-source'); + // When changing text visibility we need to update both the text displayer - // and streaming engine because we don't always stream text. To ensure that - // text displayer and streaming engine are always in sync, wait until they - // are both initialized before setting the initial value. + // and streaming engine because we don't always stream text. To ensure + // that the text displayer and streaming engine are always in sync, wait + // until they are both initialized before setting the initial value. const textDisplayerFactory = this.config_.textDisplayFactory; const textDisplayer = textDisplayerFactory(); this.lastTextFactory_ = textDisplayerFactory; const mediaSourceEngine = this.createMediaSourceEngine( - has.mediaElement, + this.video_, textDisplayer, (metadata, offset, endTime) => { this.processTimedMetadataMediaSrc_(metadata, offset, endTime); @@ -1771,114 +1531,51 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } /** - * Create the parser for the asset located at |wants.uri|. This should only be - * called as part of the load graph. + * Pick an initialize a manifest parser, then have it download and parse the + * manifest. * - * This method assumes that it is safe for it to execute, the load-graph is - * responsible for ensuring all assumptions are true. - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants + * @param {?string=} mimeType * @return {!Promise} * @private */ - async onInitializeParser_(has, wants) { + async parseManifestInner_(mimeType) { goog.asserts.assert( - has.mediaElement, + this.assetUri_, + 'should have a uri when making the parser.'); + goog.asserts.assert( + this.video_, 'We should have a media element when initializing the parser.'); - goog.asserts.assert( - has.mediaElement == wants.mediaElement, - '|has| and |wants| should have the same media element when ' + - 'initializing the parser.'); - goog.asserts.assert( this.networkingEngine_, 'Need networking engine when initializing the parser.'); + goog.asserts.assert( + this.cmcdManager_, + 'Need CMCD manager to populate manifest request data.'); goog.asserts.assert( this.config_, 'Need player config when initializing the parser.'); - // We are going to "lock-in" the mime type and uri since they are - // what we are going to use to create our parser and parse the manifest. - has.mimeType = wants.mimeType; - has.uri = wants.uri; - - goog.asserts.assert( - has.uri, - 'We should have an asset uri when initializing the parsing.'); - - // Store references to things we asserted so that we don't need to reassert - // them again later. - const assetUri = has.uri; + // Store references to things we asserted so that we don't need to + // reassert them again later. const networkingEngine = this.networkingEngine_; - // Save the uri so that it can be used outside of the load-graph. - this.assetUri_ = assetUri; + this.makeStateChangeEvent_('manifest-parser'); // Create the parser that we will use to parse the manifest. - this.parserFactory_ = await shaka.media.ManifestParser.getFactory( - assetUri, - networkingEngine, - this.config_.manifest.retryParameters, - has.mimeType); + this.parserFactory_ = shaka.media.ManifestParser.getFactory( + this.assetUri_, + mimeType || null); goog.asserts.assert(this.parserFactory_, 'Must have manifest parser'); this.parser_ = this.parserFactory_(); const manifestConfig = shaka.util.ObjectUtils.cloneObject(this.config_.manifest); // Don't read video segments if the player is attached to an audio element - if (wants.mediaElement && wants.mediaElement.nodeName === 'AUDIO') { + if (this.video_ && this.video_.nodeName === 'AUDIO') { manifestConfig.disableVideo = true; } this.parser_.configure(manifestConfig); - } - - /** - * Parse the manifest at |has.uri| using the parser that should have already - * been created. This should only be called as part of the load graph. - * - * This method assumes that it is safe for it to execute, the load-graph is - * responsible for ensuring all assumptions are true. - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants - * @return {!shaka.util.AbortableOperation} - * @private - */ - onParseManifest_(has, wants) { - goog.asserts.assert( - has.mimeType == wants.mimeType, - '|has| and |wants| should have the same mime type when parsing.'); - goog.asserts.assert( - has.uri == wants.uri, - '|has| and |wants| should have the same uri when parsing.'); - - goog.asserts.assert( - has.uri, - '|has| should have a valid uri when parsing.'); - goog.asserts.assert( - has.uri == this.assetUri_, - '|has.uri| should match the cached asset uri.'); - - goog.asserts.assert( - this.networkingEngine_, - 'Need networking engine to parse manifest.'); - goog.asserts.assert( - this.cmcdManager_, - 'Need CMCD manager to populate manifest request data.'); - goog.asserts.assert( - this.config_, - 'Need player config to parse manifest.'); - - goog.asserts.assert( - this.parser_, - '|this.parser_| should have been set in an earlier step.'); - - // Store references to things we asserted so that we don't need to reassert - // them again later. - const assetUri = has.uri; - const networkingEngine = this.networkingEngine_; // This will be needed by the parser once it starts parsing, so we will // initialize it now even through it appears a little out-of-place. @@ -1919,7 +1616,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // Called when the parser finds a timeline region. This can be called // before we start playback or during playback (live/in-progress // manifest). - onTimelineRegionAdded: (region) => this.regionTimeline_.addRegion(region), + onTimelineRegionAdded: (region) => { + this.regionTimeline_.addRegion(region); + }, onEvent: (event) => this.dispatchEvent(event), onError: (error) => this.onError_(error), @@ -1955,57 +1654,41 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const startTime = Date.now() / 1000; - return new shaka.util.AbortableOperation(/* promise= */ (async () => { - this.manifest_ = await this.parser_.start(assetUri, playerInterface); + this.makeStateChangeEvent_('manifest'); - // This event is fired after the manifest is parsed, but before any - // filtering takes place. - const event = - this.makeEvent_(shaka.util.FakeEvent.EventName.ManifestParsed); - this.dispatchEvent(event); + this.manifest_ = await this.parser_.start( + this.assetUri_, playerInterface); - // We require all manifests to have at least one variant. - if (this.manifest_.variants.length == 0) { - throw new shaka.util.Error( - shaka.util.Error.Severity.CRITICAL, - shaka.util.Error.Category.MANIFEST, - shaka.util.Error.Code.NO_VARIANTS); - } + // This event is fired after the manifest is parsed, but before any + // filtering takes place. + const event = + this.makeEvent_(shaka.util.FakeEvent.EventName.ManifestParsed); + this.dispatchEvent(event); - // Make sure that all variants are either: audio-only, video-only, or - // audio-video. - shaka.Player.filterForAVVariants_(this.manifest_); + // We require all manifests to have at least one variant. + if (this.manifest_.variants.length == 0) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.MANIFEST, + shaka.util.Error.Code.NO_VARIANTS); + } - const now = Date.now() / 1000; - const delta = now - startTime; - this.stats_.setManifestTime(delta); - })(), /* onAbort= */ () => { - shaka.log.info('Aborting parser step...'); - return this.parser_.stop(); - }); + // Make sure that all variants are either: audio-only, video-only, or + // audio-video. + shaka.Player.filterForAVVariants_(this.manifest_); + + const now = Date.now() / 1000; + const delta = now - startTime; + this.stats_.setManifestTime(delta); } /** - * This should only be called by the load graph when it is time to initialize - * drmEngine. The only time this may be called is when we are attached a - * media element and have parsed a manifest. + * Initializes the DRM engine. * - * The load-graph is responsible for ensuring all assumptions made by this - * method are valid before executing it. - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants * @return {!Promise} * @private */ - async onInitializeDrm_(has, wants) { - goog.asserts.assert( - has.mimeType == wants.mimeType, - 'The load graph should have ensured the mime types matched.'); - goog.asserts.assert( - has.uri == wants.uri, - 'The load graph should have ensured the uris matched'); - + async initializeDrmInner_() { goog.asserts.assert( this.networkingEngine_, '|onInitializeDrm_| should never be called after |destroy|'); @@ -2016,9 +1699,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.manifest_, '|this.manifest_| should have been set in an earlier step.'); goog.asserts.assert( - has.mediaElement, + this.video_, 'We should have a media element when initializing the DRM Engine.'); + this.makeStateChangeEvent_('drm-engine'); + const startTime = Date.now() / 1000; let firstEvent = true; @@ -2041,12 +1726,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const now = Date.now() / 1000; const delta = now - startTime; this.stats_.setDrmTime(delta); - // LCEVC data by itself is not encrypted in DRM protected streams and - // can therefore be accessed and decoded as normal. However, the LCEVC - // decoder needs access to the VideoElement output in order to apply - // the enhancement. In DRM contexts where the browser CDM restricts - // access from our decoder, the enhancement cannot be applied and - // therefore the LCEVC output canvas is hidden accordingly. + // LCEVC data by itself is not encrypted in DRM protected streams + // and can therefore be accessed and decoded as normal. However, + // the LCEVC decoder needs access to the VideoElement output in + // order to apply the enhancement. In DRM contexts where the + // browser CDM restricts access from our decoder, the enhancement + // cannot be applied and therefore the LCEVC output canvas is + // hidden accordingly. if (this.lcevcDec_) { this.lcevcDec_.hideCanvas(); } @@ -2060,66 +1746,31 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.manifest_.variants, this.manifest_.offlineSessionIds); - await this.drmEngine_.attach(has.mediaElement); + await this.drmEngine_.attach(this.video_); - // Now that we have drm information, filter the manifest (again) so that we - // can ensure we only use variants with the selected key system. + // Now that we have drm information, filter the manifest (again) so that + // we can ensure we only use variants with the selected key system. await this.filterManifest_(this.manifest_); } /** - * This should only be called by the load graph when it is time to load all - * playback components needed for playback. The only times this may be called - * is when we are attached to the same media element as in the request. + * Starts loading the content described by the parsed manifest. * - * This method assumes that it is safe for it to execute, the load-graph is - * responsible for ensuring all assumptions are true. - * - * Loading is defined as: - * - Attaching all playback-related listeners to the media element - * - Initializing playback and observers - * - Initializing ABR Manager - * - Initializing Streaming Engine - * - Starting playback at |wants.startTime| - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants + * @param {number} startTimeOfLoad + * @return {!Promise} * @private */ - async onLoad_(has, wants) { + async loadInner_(startTimeOfLoad) { goog.asserts.assert( - has.mimeType == wants.mimeType, - '|has| and |wants| should have the same mime type when loading.'); + this.video_, 'We should have a media element by now.'); goog.asserts.assert( - has.uri == wants.uri, - '|has| and |wants| should have the same uri when loading.'); - + this.manifest_, 'The manifest should already be parsed.'); goog.asserts.assert( - has.mediaElement, - 'We should have a media element when loading.'); - goog.asserts.assert( - !isNaN(wants.startTimeOfLoad), - '|wants| should tell us when the load was originally requested'); + this.assetUri_, 'We should have an asset uri by now.'); - // Since we are about to start playback, we will lock in the start time as - // something we are now depending on. - has.startTime = wants.startTime; - - // If updateStartTime() has been called since load() was invoked use the - // requested startTime - if (this.updatedStartTime_ != null) { - has.startTime = this.updatedStartTime_; - this.updatedStartTime_ = null; - } - - // Store a reference to values in |has| after asserting so that closure will - // know that they will still be non-null between calls to await. - const mediaElement = has.mediaElement; - const assetUri = has.uri; - - // Save the uri so that it can be used outside of the load-graph. - this.assetUri_ = assetUri; + this.makeStateChangeEvent_('load'); + const mediaElement = this.video_; this.playRateController_ = new shaka.media.PlayRateController({ getRate: () => mediaElement.playbackRate, getDefaultRate: () => mediaElement.defaultPlaybackRate, @@ -2129,13 +1780,14 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const updateStateHistory = () => this.updateStateHistory_(); const onRateChange = () => this.onRateChange_(); - this.loadEventManager_.listen(mediaElement, 'playing', updateStateHistory); + this.loadEventManager_.listen( + mediaElement, 'playing', updateStateHistory); this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory); this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory); this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange); - // Check status of the LCEVC Dec Object and reset or - // create or close based on config + // Check the status of the LCEVC Dec Object. Reset, create, or close + // depending on the config. this.setupLcevc_(this.config_); const abrFactory = this.config_.abrFactory; @@ -2249,13 +1901,13 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.config_.playRangeStart, this.config_.playRangeEnd); - this.playhead_ = this.createPlayhead(has.startTime); + this.playhead_ = this.createPlayhead(this.startTime_ || 0); this.playheadObservers_ = - this.createPlayheadObserversForMSE_(has.startTimeOfLoad); + this.createPlayheadObserversForMSE_(startTimeOfLoad); - // We need to start the buffer management code near the end because it will - // set the initial buffering state and that depends on other components - // being initialized. + // We need to start the buffer management code near the end because it + // will set the initial buffering state and that depends on other + // components being initialized. const rebufferThreshold = Math.max( this.manifest_.minBufferTime, this.config_.streaming.rebufferingGoal); this.startBufferManagement_(mediaElement, rebufferThreshold); @@ -2268,14 +1920,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.switchVariant_(initialVariant, /* fromAdaptation= */ true, /* clearBuffer= */ false, /* safeMargin= */ 0); - // Now that we have initial streams, we may adjust the start time to align - // to a segment boundary. + // Now that we have initial streams, we may adjust the start time to + // align to a segment boundary. if (this.config_.streaming.startAtSegmentBoundary) { - const startTime = this.playhead_.getTime(); - const adjustedTime = - await this.adjustStartTime_(initialVariant, startTime); - - this.playhead_.setStartTime(adjustedTime); + this.playhead_.setStartTime(await this.adjustStartTime_( + initialVariant, this.playhead_.getTime())); } } @@ -2298,7 +1947,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.setInitialTextState_(initialVariant, initialTextStream); } - // Don't initialize with a text stream unless we should be streaming text. + // Don't initialize with a text stream unless we should be streaming + // text. if (initialTextStream && this.shouldStreamText_()) { this.streamingEngine_.switchTextStream(initialTextStream); } @@ -2314,7 +1964,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.onAbrStatusChanged_(); } - // Dispatch a 'trackschanged' event now that all initial filtering is done. + // Dispatch a 'trackschanged' event now that all initial filtering is + // done. this.onTracksChanged_(); // Now that we've filtered out variants that aren't compatible with the @@ -2328,8 +1979,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { const hasPrimary = this.manifest_.variants.some((v) => v.primary); if (!this.config_.preferredAudioLanguage && !hasPrimary) { - shaka.log.warning('No preferred audio language set. We have chosen an ' + - 'arbitrary language initially'); + shaka.log.warning('No preferred audio language set. ' + + 'We have chosen an arbitrary language initially'); } const isLive = this.isLive(); @@ -2349,24 +2000,19 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // Wait for the 'loadedmetadata' event to measure load() latency. this.loadEventManager_.listenOnce(mediaElement, 'loadedmetadata', () => { const now = Date.now() / 1000; - const delta = now - wants.startTimeOfLoad; + const delta = now - startTimeOfLoad; this.stats_.setLoadLatency(delta); }); } /** - * This should only be called by the load graph when it is time to initialize - * drmEngine for src= playbacks. + * Initializes the DRM engine for use by src equals. * - * The load-graph is responsible for ensuring all assumptions made by this - * method are valid before executing it. - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants + * @param {string} mimeType * @return {!Promise} * @private */ - async onInitializeSrcEqualsDrm_(has, wants) { + async initializeSrcEqualsDrmInner_(mimeType) { const ContentType = shaka.util.ManifestParserUtils.ContentType; goog.asserts.assert( @@ -2404,20 +2050,11 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.drmEngine_.configure(this.config_.drm); - const uri = wants.uri || ''; - const extension = shaka.media.ManifestParser.getExtension(uri); - let mimeType = shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[extension]; - if (mimeType == 'application/x-mpegurl' && shaka.util.Platform.isApple()) { - mimeType = 'application/vnd.apple.mpegurl'; - } - if (!mimeType) { - mimeType = 'video/mp4'; - } - // TODO: Instead of feeding DrmEngine with Variants, we should refactor // DrmEngine so that it takes a minimal config derived from Variants. In // cases like this one or in removal of stored content, the details are - // largely unimportant. We should have a saner way to initialize DrmEngine. + // largely unimportant. We should have a saner way to initialize + // DrmEngine. // That would also insulate DrmEngine from manifest changes in the future. // For now, that is time-consuming and this synthetic Variant is easy, so // I'm putting it off. Since this is only expected to be used for native @@ -2435,10 +2072,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { groupId: null, createSegmentIndex: () => Promise.resolve(), segmentIndex: null, - mimeType: wants.mimeType ? - shaka.util.MimeUtils.getBasicType(wants.mimeType) : mimeType, - codecs: wants.mimeType ? - shaka.util.MimeUtils.getCodecs(wants.mimeType) : '', + mimeType: mimeType ? shaka.util.MimeUtils.getBasicType(mimeType) : '', + codecs: mimeType ? shaka.util.MimeUtils.getCodecs(mimeType) : '', encrypted: true, drmInfos: [], // Filled in by DrmEngine config. keyIds: new Set(), @@ -2467,46 +2102,28 @@ shaka.Player = class extends shaka.util.FakeEventTarget { this.drmEngine_.setSrcEquals(/* srcEquals= */ true); await this.drmEngine_.initForPlayback( [variant], /* offlineSessionIds= */ []); - await this.drmEngine_.attach(has.mediaElement); + await this.drmEngine_.attach(this.video_); } /** - * This should only be called by the load graph when it is time to set-up the - * media element to play content using src=. The only times this may be called - * is when we are attached to the same media element as in the request. + * Passes the asset URI along to the media element, so it can be played src + * equals style. * - * This method assumes that it is safe for it to execute, the load-graph is - * responsible for ensuring all assumptions are true. - * - * @param {shaka.routing.Payload} has - * @param {shaka.routing.Payload} wants - * @return {!shaka.util.AbortableOperation} + * @param {number} startTimeOfLoad + * @param {string} mimeType + * @return {!Promise} * * @private */ - onSrcEquals_(has, wants) { - goog.asserts.assert( - has.mediaElement, - 'We should have a media element when loading.'); - goog.asserts.assert( - wants.uri, - '|has| should have a valid uri when loading.'); - goog.asserts.assert( - !isNaN(wants.startTimeOfLoad), - '|wants| should tell us when the load was originally requested'); - goog.asserts.assert( - this.video_ == has.mediaElement, - 'The video element should match our media element'); + async srcEqualsInner_(startTimeOfLoad, mimeType) { + this.makeStateChangeEvent_('src-equals'); - // Lock-in the values that we are using so that the routing logic knows what - // we have. - has.uri = wants.uri; - has.startTime = wants.startTime; + goog.asserts.assert( + this.video_, 'We should have a media element when loading.'); + goog.asserts.assert( + this.assetUri_, 'We should have a valid uri when loading.'); - // Save the uri so that it can be used outside of the load-graph. - this.assetUri_ = has.uri; - - const mediaElement = has.mediaElement; + const mediaElement = this.video_; this.playhead_ = new shaka.media.SrcEqualsPlayhead(mediaElement); @@ -2517,8 +2134,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { unloaded = true; }); - if (has.startTime != null) { - this.playhead_.setStartTime(has.startTime); + if (this.startTime_ != null) { + this.playhead_.setStartTime(this.startTime_); } this.playRateController_ = new shaka.media.PlayRateController({ @@ -2528,16 +2145,17 @@ shaka.Player = class extends shaka.util.FakeEventTarget { movePlayhead: (delta) => { mediaElement.currentTime += delta; }, }); - // We need to start the buffer management code near the end because it will - // set the initial buffering state and that depends on other components - // being initialized. + // We need to start the buffer management code near the end because it + // will set the initial buffering state and that depends on other + // components being initialized. const rebufferThreshold = this.config_.streaming.rebufferingGoal; this.startBufferManagement_(mediaElement, rebufferThreshold); // Add all media element listeners. const updateStateHistory = () => this.updateStateHistory_(); const onRateChange = () => this.onRateChange_(); - this.loadEventManager_.listen(mediaElement, 'playing', updateStateHistory); + this.loadEventManager_.listen( + mediaElement, 'playing', updateStateHistory); this.loadEventManager_.listen(mediaElement, 'pause', updateStateHistory); this.loadEventManager_.listen(mediaElement, 'ended', updateStateHistory); this.loadEventManager_.listen(mediaElement, 'ratechange', onRateChange); @@ -2547,24 +2165,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // automatically. // See https://github.com/shaka-project/shaka-player/issues/2483 if (mediaElement.preload != 'none') { - this.loadEventManager_.listenOnce(mediaElement, 'loadedmetadata', () => { - const now = Date.now() / 1000; - const delta = now - wants.startTimeOfLoad; - this.stats_.setLoadLatency(delta); - }); + this.loadEventManager_.listenOnce( + mediaElement, 'loadedmetadata', () => { + const now = Date.now() / 1000; + const delta = now - startTimeOfLoad; + this.stats_.setLoadLatency(delta); + }); } // The audio tracks are only available on Safari at the moment, but this // drives the tracks API for Safari's native HLS. So when they change, // fire the corresponding Shaka Player event. if (mediaElement.audioTracks) { - this.loadEventManager_.listen( - mediaElement.audioTracks, 'addtrack', () => this.onTracksChanged_()); - this.loadEventManager_.listen( - mediaElement.audioTracks, 'removetrack', + this.loadEventManager_.listen(mediaElement.audioTracks, 'addtrack', + () => this.onTracksChanged_()); + this.loadEventManager_.listen(mediaElement.audioTracks, 'removetrack', + () => this.onTracksChanged_()); + this.loadEventManager_.listen(mediaElement.audioTracks, 'change', () => this.onTracksChanged_()); - this.loadEventManager_.listen( - mediaElement.audioTracks, 'change', () => this.onTracksChanged_()); } if (mediaElement.textTracks) { @@ -2600,13 +2218,10 @@ shaka.Player = class extends shaka.util.FakeEventTarget { () => this.onTracksChanged_()); } - const extension = shaka.media.ManifestParser.getExtension(has.uri); - const mimeType = - shaka.Player.SRC_EQUAL_EXTENSIONS_TO_MIME_TYPES_[extension]; - // By setting |src| we are done "loading" with src=. We don't need to set // the current time because |playhead| will do that for us. - mediaElement.src = this.cmcdManager_.appendSrcData(has.uri, mimeType); + mediaElement.src = this.cmcdManager_.appendSrcData( + this.assetUri_, mimeType); // Tizen 3 / WebOS won't load anything unless you call load() explicitly, // no matter the value of the preload attribute. This is harmful on some @@ -2620,15 +2235,15 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // initialized. this.loadMode_ = shaka.Player.LoadMode.SRC_EQUALS; - // The event doesn't mean as much for src= playback, since we don't control - // streaming. But we should fire it in this path anyway since some - // applications may be expecting it as a life-cycle event. + // The event doesn't mean as much for src= playback, since we don't + // control streaming. But we should fire it in this path anyway since + // some applications may be expecting it as a life-cycle event. this.dispatchEvent( this.makeEvent_(shaka.util.FakeEvent.EventName.Streaming)); // The "load" Promise is resolved when we have loaded the metadata. If we - // wait for the full data, that won't happen on Safari until the play button - // is hit. + // wait for the full data, that won't happen on Safari until the play + // button is hit. const fullyLoaded = new shaka.util.PublicPromise(); shaka.util.MediaReadyState.waitForReadyState(mediaElement, HTMLMediaElement.HAVE_METADATA, @@ -2638,7 +2253,8 @@ shaka.Player = class extends shaka.util.FakeEventTarget { fullyLoaded.resolve(); }); - // We can't switch to preferred languages, though, until the data is loaded. + // We can't switch to preferred languages, though, until the data is + // loaded. shaka.util.MediaReadyState.waitForReadyState(mediaElement, HTMLMediaElement.HAVE_CURRENT_DATA, this.loadEventManager_, @@ -2679,9 +2295,9 @@ shaka.Player = class extends shaka.util.FakeEventTarget { } else if (mediaElement.preload == 'none') { shaka.log.alwaysWarn( 'With