From 26d2e9895943d221bbb567132f451b45ad8d0f2f Mon Sep 17 00:00:00 2001 From: Sandra Lokshina Date: Wed, 13 May 2020 11:57:30 -0700 Subject: [PATCH] Add a util for handling media readyState related logic. Fixes #2555. Change-Id: I735064952bc425bfb18d6c5681921ae1691eb348 --- build/types/core | 1 + lib/media/playhead.js | 14 ++++---- lib/media/video_wrapper.js | 44 ++++++++------------------ lib/player.js | 31 +++++++++--------- lib/polyfill/patchedmediakeys_apple.js | 12 +++---- lib/polyfill/patchedmediakeys_ms.js | 15 +++------ lib/util/media_ready_state_utils.js | 37 ++++++++++++++++++++++ 7 files changed, 83 insertions(+), 71 deletions(-) create mode 100644 lib/util/media_ready_state_utils.js diff --git a/build/types/core b/build/types/core index ec4fcdb422..c6d8a0c3dc 100644 --- a/build/types/core +++ b/build/types/core @@ -75,6 +75,7 @@ +../../lib/util/manifest_filter.js +../../lib/util/manifest_parser_utils.js +../../lib/util/map_utils.js ++../../lib/util/media_ready_state_utils.js +../../lib/util/mime_utils.js +../../lib/util/mp4_parser.js +../../lib/util/multi_map.js diff --git a/lib/media/playhead.js b/lib/media/playhead.js index aa4674108a..0b5ff68940 100644 --- a/lib/media/playhead.js +++ b/lib/media/playhead.js @@ -12,6 +12,7 @@ goog.require('shaka.log'); goog.require('shaka.media.GapJumpingController'); goog.require('shaka.media.TimeRangesUtils'); goog.require('shaka.media.VideoWrapper'); +goog.require('shaka.util.MediaReadyState'); goog.require('shaka.util.IReleasable'); goog.require('shaka.util.Timer'); @@ -88,13 +89,12 @@ shaka.media.SrcEqualsPlayhead = class { this.mediaElement_.currentTime = newTime; } }; - if (this.mediaElement_.readyState == 0) { - this.eventManager_.listenOnce( - this.mediaElement_, 'loadeddata', onLoaded); - } else { - // It's already loaded. - onLoaded(); - } + + shaka.util.MediaReadyState.waitForReadyState(this.mediaElement_, + HTMLMediaElement.HAVE_CURRENT_DATA, + this.eventManager_, () => { + onLoaded(); + }); } /** @override */ diff --git a/lib/media/video_wrapper.js b/lib/media/video_wrapper.js index be6a669178..26694bc7eb 100644 --- a/lib/media/video_wrapper.js +++ b/lib/media/video_wrapper.js @@ -10,6 +10,7 @@ goog.require('goog.asserts'); goog.require('shaka.log'); goog.require('shaka.util.EventManager'); goog.require('shaka.util.IReleasable'); +goog.require('shaka.util.MediaReadyState'); goog.require('shaka.util.Timer'); @@ -52,11 +53,12 @@ shaka.media.VideoWrapper = class { // ready. If the video element is not ready, we cannot set the time. To work // around this, we will wait for the "loadedmetadata" event which tells us // that the media element is now ready. - if (video.readyState > 0) { - this.setStartTime_(startTime); - } else { - this.delaySetStartTime_(startTime); - } + shaka.util.MediaReadyState.waitForReadyState(this.video_, + HTMLMediaElement.HAVE_METADATA, + this.eventManager_, + () => { + this.setStartTime_(this.startTime_); + }); } @@ -96,35 +98,15 @@ shaka.media.VideoWrapper = class { if (this.video_.readyState > 0) { this.mover_.moveTo(time); } else { - this.delaySetStartTime_(time); + shaka.util.MediaReadyState.waitForReadyState(this.video_, + HTMLMediaElement.HAVE_METADATA, + this.eventManager_, + () => { + this.setStartTime_(this.startTime_); + }); } } - /** - * If the media element is not ready, we can't set |currentTime|. To work - * around this we will listen for the "loadedmetadata" event so that we can - * set the start time once the element is ready. - * - * @param {number} startTime - * @private - */ - delaySetStartTime_(startTime) { - const readyEvent = 'loadedmetadata'; - - // Since we are going to override what the start time should be, we need to - // save it so that |getTime| can return the most accurate start time - // possible. - this.startTime_ = startTime; - - // The media element is not ready to accept changes to current time. We need - // to cache them and then execute them once the media element is ready. - this.eventManager_.unlisten(this.video_, readyEvent); - - this.eventManager_.listenOnce(this.video_, readyEvent, () => { - this.setStartTime_(startTime); - }); - } - /** * Set the start time for the content. The given start time will be ignored if diff --git a/lib/player.js b/lib/player.js index 03527c1392..7c0736bb84 100644 --- a/lib/player.js +++ b/lib/player.js @@ -35,6 +35,7 @@ goog.require('shaka.util.Functional'); goog.require('shaka.util.IDestroyable'); goog.require('shaka.util.LanguageUtils'); goog.require('shaka.util.ManifestParserUtils'); +goog.require('shaka.util.MediaReadyState'); goog.require('shaka.util.MimeUtils'); goog.require('shaka.util.Platform'); goog.require('shaka.util.PlayerConfiguration'); @@ -2049,26 +2050,24 @@ shaka.Player = class extends shaka.util.FakeEventTarget { // This is fully loaded when we have loaded the first frame. const fullyLoaded = new shaka.util.PublicPromise(); - if (this.video_.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) { - this.setupPreferredAudioOnSrc_(); - this.setupPreferredTextOnSrc_(); - // Already done! - fullyLoaded.resolve(); - } else if (this.video_.error) { + shaka.util.MediaReadyState.waitForReadyState(this.video_, + HTMLMediaElement.HAVE_CURRENT_DATA, + this.eventManager_, + () => { + this.setupPreferredAudioOnSrc_(); + this.setupPreferredTextOnSrc_(); + fullyLoaded.resolve(); + }); + + if (this.video_.error) { // Already failed! fullyLoaded.reject(this.videoErrorToShakaError_()); - } else { - // Wait for success or failure. - this.eventManager_.listenOnce(this.video_, 'loadeddata', () => { - this.setupPreferredAudioOnSrc_(); - this.setupPreferredTextOnSrc_(); - fullyLoaded.resolve(); - }); - this.eventManager_.listenOnce(this.video_, 'error', () => { - fullyLoaded.reject(this.videoErrorToShakaError_()); - }); } + this.eventManager_.listenOnce(this.video_, 'error', () => { + fullyLoaded.reject(this.videoErrorToShakaError_()); + }); + // Call video.load. This is required for iOS Safari to properly fire events. has.mediaElement.load(); diff --git a/lib/polyfill/patchedmediakeys_apple.js b/lib/polyfill/patchedmediakeys_apple.js index 43129a9c2f..56cdf54615 100644 --- a/lib/polyfill/patchedmediakeys_apple.js +++ b/lib/polyfill/patchedmediakeys_apple.js @@ -390,13 +390,11 @@ shaka.polyfill.PatchedMediaKeysApple.MediaKeys = class { // Some browsers require that readyState >=1 before mediaKeys can be // set, so check this and wait for loadedmetadata if we are not in the // correct state - if (media.readyState >= 1) { - media.webkitSetMediaKeys(this.nativeMediaKeys_); - } else { - this.eventManager_.listenOnce(media, 'loadedmetadata', () => { - media.webkitSetMediaKeys(this.nativeMediaKeys_); - }); - } + shaka.util.MediaReadyState.waitForReadyState(media, + HTMLMediaElement.HAVE_METADATA, + this.eventManager_, () => { + media.webkitSetMediaKeys(this.nativeMediaKeys_); + }); return Promise.resolve(); } catch (exception) { diff --git a/lib/polyfill/patchedmediakeys_ms.js b/lib/polyfill/patchedmediakeys_ms.js index c4421e9af8..82f54d7b31 100644 --- a/lib/polyfill/patchedmediakeys_ms.js +++ b/lib/polyfill/patchedmediakeys_ms.js @@ -339,21 +339,16 @@ shaka.polyfill.PatchedMediaKeysMs.MediaKeys = class { /** @type {shaka.util.EventManager.ListenerType} */ (PatchedMediaKeysMs.onMsNeedKey_)); - const setMediaKeysDeferred = () => { - media.msSetMediaKeys(this.nativeMediaKeys_); - media.removeEventListener('loadedmetadata', setMediaKeysDeferred); - }; - // Wrap native HTMLMediaElement.msSetMediaKeys with a Promise. try { // IE11/Edge requires that readyState >=1 before mediaKeys can be set, // so check this and wait for loadedmetadata if we are not in the // correct state - if (media.readyState >= 1) { - media.msSetMediaKeys(this.nativeMediaKeys_); - } else { - media.addEventListener('loadedmetadata', setMediaKeysDeferred); - } + shaka.util.MediaReadyState.waitForReadyState(media, + HTMLMediaElement.HAVE_METADATA, + this.eventManager_, () => { + media.msSetMediaKeys(this.nativeMediaKeys_); + }); return Promise.resolve(); } catch (exception) { diff --git a/lib/util/media_ready_state_utils.js b/lib/util/media_ready_state_utils.js new file mode 100644 index 0000000000..7ae4d3d68d --- /dev/null +++ b/lib/util/media_ready_state_utils.js @@ -0,0 +1,37 @@ +/** @license + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +goog.provide('shaka.util.MediaReadyState'); + + +shaka.util.MediaReadyState = class { + /* + * @param {!HTMLMediaElement} mediaElement + * @param {HTMLMediaElement.ReadyState} readyState + * @param {!function()} callback + */ + static waitForReadyState(mediaElement, readyState, eventManager, callback) { + if (readyState == HTMLMediaElement.HAVE_NOTHING || + mediaElement.readyState >= readyState) { + callback(); + } else { + const MediaReadyState = shaka.util.MediaReadyState; + const eventName = + MediaReadyState.READY_STATES_TO_EVENT_NAMES_.get(readyState); + eventManager.listenOnce(mediaElement, eventName, callback); + } + } +}; + +/** + * @const {!Map.} + * @private + */ +shaka.util.MediaReadyState.READY_STATES_TO_EVENT_NAMES_ = new Map([ + [HTMLMediaElement.HAVE_METADATA, 'loadedmetadata'], + [HTMLMediaElement.HAVE_CURRENT_DATA, 'loadeddata'], + [HTMLMediaElement.HAVE_FUTURE_DATA, 'canplay'], + [HTMLMediaElement.HAVE_ENOUGH_DATA, 'canplaythrough'], +]);