chore: Remove state engine (#5752)

The state engine mechanism, designed for the player class, was
over-engineered. The structure of the class makes debugging player
errors unnecessarily annoying, by obfuscating the code-path the error
followed, and in general
has created a significant amount of technical debt.
This changes the player to use an async-await setup for the top-level
operations, laying things out much more cleanly
and linearly.

---------

Co-authored-by: Álvaro Velad Galván <ladvan91@hotmail.com>
This commit is contained in:
theodab
2023-10-30 23:59:08 -07:00
committed by GitHub
parent c457db835f
commit 4425dca283
43 changed files with 907 additions and 2836 deletions
+1 -4
View File
@@ -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
+3 -3
View File
@@ -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.';
}
+2 -1
View File
@@ -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;
+2 -1
View File
@@ -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);
+10 -8
View File
@@ -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 @@ Thats 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.
+2 -1
View File
@@ -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,
+1 -1
View File
@@ -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);
-2
View File
@@ -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(
-2
View File
@@ -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(
+8 -94
View File
@@ -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.<shaka.extern.ManifestParser.Factory>}
* @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.<string, shaka.extern.ManifestParser.Factory>}
*/
shaka.media.ManifestParser.parsersByExtension = {};
+2 -2
View File
@@ -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_) {
-2
View File
@@ -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());
+7 -3
View File
@@ -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();
+638 -1305
View File
File diff suppressed because it is too large Load Diff
-23
View File
@@ -1,23 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.routing.Node');
/**
* @typedef {{
* name: string
* }}
*
* @description
* A node is the one of the two fundamental units used to build graphs. It
* represents the position within a graph.
*
* @property {string} name
* A human-readable name for this node. While this should not be used in
* production, the name helps identify nodes when debugging.
*/
shaka.routing.Node;
-47
View File
@@ -1,47 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.routing.Payload');
/**
* @typedef {{
* mediaElement: HTMLMediaElement,
* mimeType: ?string,
* startTime: ?number,
* startTimeOfLoad: number,
* uri: ?string,
* keepAdManager: boolean
* }}
*
* @description
* The payload is the information to "deliver" to our destination. When
* moving from node-to-node, the payload may be modified.
*
* @property {HTMLMediaElement} mediaElement
* The media element that we are or will be using.
*
* @property {?string} mimeType
* The mime type of the content that we will parse. This will be used when
* picking which parser to use.
*
* @property {?number} startTime
* The time (in seconds) where playback should start. When |null| we will
* use the content's default start time (0 for VOD and live edge for LIVE).
*
* @property {number} startTimeOfLoad
* The time (in seconds) of when a load request is created. This is used to
* track the latency between when the call to |Player.load| and the start
* of playback. When the payload is not for a load request, this should be
* NaN.
*
* @property {?string} uri
* The address of the content that will be loaded.
*
* @property {boolean} keepAdManager
* Indicates if the adManager shouldn't destroyed.
*/
shaka.routing.Payload;
-587
View File
@@ -1,587 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.routing.Walker');
goog.require('goog.asserts');
goog.require('shaka.routing.Node');
goog.require('shaka.routing.Payload');
goog.require('shaka.util.Destroyer');
goog.require('shaka.util.Error');
goog.require('shaka.util.IDestroyable');
goog.require('shaka.util.PublicPromise');
goog.requireType('shaka.util.AbortableOperation');
/**
* The walker moves through a graph node-by-node executing asynchronous work
* as it enters each node.
*
* The walker accepts requests for where it should go next. Requests are queued
* and executed in FIFO order. If the current request can be interrupted, it
* will be cancelled and the next request started.
*
* A request says "I want to change where we are going". When the walker is
* ready to change destinations, it will resolve the request, allowing the
* destination to differ based on the current state and not the state when
* the request was appended.
*
* Example (from shaka.Player):
* When we unload, we need to either go to the attached or detached state based
* on whether or not we have a video element.
*
* When we are asked to unload, we don't know what other pending requests may
* be ahead of us (there could be attach requests or detach requests). We need
* to wait until its our turn to know if:
* - we should go to the attach state because we have a media element
* - we should go to the detach state because we don't have a media element
*
* The walker allows the caller to specify if a route can or cannot be
* interrupted. This is to allow potentially dependent routes to wait until
* other routes have finished.
*
* Example (from shaka.Player):
* A request to load content depends on an attach request finishing. We don't
* want load request to interrupt an attach request. By marking the attach
* request as non-interruptible we ensure that calling load before attach
* finishes will work.
*
* @implements {shaka.util.IDestroyable}
* @final
*/
shaka.routing.Walker = class {
/**
* Create a new walker that starts at |startingAt| and with |startingWith|.
* The instance of |startingWith| will be the one that the walker holds and
* uses for its life. No one else should reference it.
*
* The per-instance behaviour for the walker is provided via |implementation|
* which is used to connect this walker with the "outside world".
*
* @param {shaka.routing.Node} startingAt
* @param {shaka.routing.Payload} startingWith
* @param {shaka.routing.Walker.Implementation} implementation
*/
constructor(startingAt, startingWith, implementation) {
/** @private {?shaka.routing.Walker.Implementation} */
this.implementation_ = implementation;
/** @private {shaka.routing.Node} */
this.currentlyAt_ = startingAt;
/** @private {shaka.routing.Payload} */
this.currentlyWith_ = startingWith;
/**
* When we run out of work to do, we will set this promise so that when
* new work is added (and this is not null) it can be resolved. The only
* time when this should be non-null is when we are waiting for more work.
*
* @private {?shaka.util.PublicPromise}
*/
this.waitForWork_ = null;
/** @private {!Array.<shaka.routing.Walker.Request_>} */
this.requests_ = [];
/** @private {?shaka.routing.Walker.ActiveRoute_} */
this.currentRoute_ = null;
/** @private {?shaka.util.AbortableOperation} */
this.currentStep_ = null;
/**
* Hold a reference to the main loop's promise so that we know when it has
* exited. This will determine when |destroy| can resolve. Purposely make
* the main loop start next interpreter cycle so that the constructor will
* finish before it starts.
*
* @private {!Promise}
*/
this.mainLoopPromise_ = Promise.resolve().then(() => this.mainLoop_());
/** @private {!shaka.util.Destroyer} */
this.destroyer_ = new shaka.util.Destroyer(() => this.doDestroy_());
}
/**
* Get the current routing payload.
*
* @return {shaka.routing.Payload}
*/
getCurrentPayload() {
return this.currentlyWith_;
}
/** @override */
destroy() {
return this.destroyer_.destroy();
}
/** @private */
async doDestroy_() {
// If we are executing a current step, we want to interrupt it so that we
// can force the main loop to terminate.
if (this.currentStep_) {
this.currentStep_.abort();
}
// If we are waiting for more work, we want to wake-up the main loop so that
// it can exit on its own.
this.unblockMainLoop_();
// Wait for the main loop to terminate so that an async operation won't
// try and use state that we released.
await this.mainLoopPromise_;
// Any routes that we are not going to finish, we need to cancel. If we
// don't do this, those listening will be left hanging.
if (this.currentRoute_) {
this.currentRoute_.listeners.onCancel();
}
for (const request of this.requests_) {
request.listeners.onCancel();
}
// Release anything that could hold references to anything outside of this
// class.
this.currentRoute_ = null;
this.requests_ = [];
this.implementation_ = null;
}
/**
* Ask the walker to start a new route. When the walker is ready to start a
* new route, it will call |create| and |create| will provide the walker with
* a new route to execute.
*
* If any previous calls to |startNewRoute| created non-interruptible routes,
* |create| won't be called until all previous non-interruptible routes have
* finished.
*
* This method will return a collection of listeners that the caller can hook
* into. Any listener that the caller is interested should be assigned
* immediately after calling |startNewRoute| or else they could miss the event
* they want to listen for.
*
* @param {function(shaka.routing.Payload):?shaka.routing.Walker.Route} create
* @return {shaka.routing.Walker.Listeners}
*/
startNewRoute(create) {
const listeners = {
onStart: () => {},
onEnd: () => {},
onCancel: () => {},
onError: (error) => {},
onSkip: () => {},
onEnter: () => {},
};
this.requests_.push({
create: create,
listeners: listeners,
});
// If we are in the middle of a step, try to abort it. If this is successful
// the main loop will error and the walker will enter recovery mode.
if (this.currentStep_) {
this.currentStep_.abort();
}
// Tell the main loop that new work is available. If the main loop was not
// blocked, this will be a no-op.
this.unblockMainLoop_();
return listeners;
}
/**
* @return {!Promise}
* @private
*/
async mainLoop_() {
while (!this.destroyer_.destroyed()) {
// eslint-disable-next-line no-await-in-loop
await this.doOneThing_();
}
}
/**
* Do one thing to move the walker closer to its destination. This can be:
* 1. Starting a new route.
* 2. Taking one more step/finishing a route.
* 3. Wait for a new route.
*
* @return {!Promise}
* @private
*/
doOneThing_() {
if (this.tryNewRoute_()) {
return Promise.resolve();
}
if (this.currentRoute_) {
return this.takeNextStep_();
}
goog.asserts.assert(this.waitForWork_ == null,
'We should not have a promise yet.');
// We have no more work to do. We will wait until new work has been provided
// via request route or until we are destroyed.
this.implementation_.onIdle(this.currentlyAt_);
// Wait on a new promise so that we can be resolved by |waitForWork|. This
// avoids us acting like a busy-wait.
this.waitForWork_ = new shaka.util.PublicPromise();
return this.waitForWork_;
}
/**
* Check if the walker can start a new route. There are a couple ways this can
* happen:
* 1. We have a new request but no current route
* 2. We have a new request and our current route can be interrupted
*
* @return {boolean}
* |true| when a new route was started (regardless of reason) and |false|
* when no new route was started.
*
* @private
*/
tryNewRoute_() {
goog.asserts.assert(
this.currentStep_ == null,
'We should never have a current step between taking steps.');
if (this.requests_.length == 0) {
return false;
}
// If the current route cannot be interrupted, we can't start a new route.
if (this.currentRoute_ && !this.currentRoute_.interruptible) {
return false;
}
// Stop any previously active routes. Even if we don't pick-up a new route,
// this route should stop.
if (this.currentRoute_) {
this.currentRoute_.listeners.onCancel();
this.currentRoute_ = null;
}
// Create and start the next route. We may not take any steps because it may
// be interrupted by the next request.
const request = this.requests_.shift();
const newRoute = request.create(this.currentlyWith_);
// Based on the current state of |payload|, a new route may not be
// possible. In these cases |create| will return |null| to signal that
// we should just stop the current route and move onto the next request
// (in the next main loop iteration).
if (newRoute) {
request.listeners.onStart();
// Convert the route created from the request's create method to an
// active route.
this.currentRoute_ = {
node: newRoute.node,
payload: newRoute.payload,
interruptible: newRoute.interruptible,
listeners: request.listeners,
};
} else {
request.listeners.onSkip();
}
return true;
}
/**
* Move forward one step on our current route. This assumes that we have a
* current route. A couple things can happen when moving forward:
* 1. An error - if an error occurs, it will signal an error occurred,
* attempt to recover, and drop the route.
* 2. Move - if no error occurs, we will move forward. When we arrive at
* our destination, it will signal the end and drop the route.
*
* In the event of an error or arriving at the destination, we drop the
* current route. This allows us to pick-up a new route next time the main
* loop iterates.
*
* @return {!Promise}
* @private
*/
async takeNextStep_() {
goog.asserts.assert(
this.currentRoute_,
'We need a current route to take the next step.');
// Figure out where we are supposed to go next.
this.currentlyAt_ = this.implementation_.getNext(
this.currentlyAt_,
this.currentlyWith_,
this.currentRoute_.node,
this.currentRoute_.payload);
this.currentRoute_.listeners.onEnter(this.currentlyAt_);
// Enter the new node, this is where things can go wrong since it is
// possible for "supported errors" to occur - errors that the code using
// the walker can't predict but can recover from.
try {
// TODO: This is probably a false-positive. See eslint/eslint#11687.
// eslint-disable-next-line require-atomic-updates
this.currentStep_ = this.implementation_.enterNode(
/* node= */ this.currentlyAt_,
/* has= */ this.currentlyWith_,
/* wants= */ this.currentRoute_.payload);
await this.currentStep_.promise;
this.currentStep_ = null;
// If we are at the end of the route, we need to signal it and clear the
// route so that we will pick-up a new route next iteration.
if (this.currentlyAt_ == this.currentRoute_.node) {
this.currentRoute_.listeners.onEnd();
this.currentRoute_ = null;
}
} catch (error) {
if (error.code == shaka.util.Error.Code.OPERATION_ABORTED) {
goog.asserts.assert(
this.currentRoute_.interruptible,
'Do not put abortable steps in non-interruptible routes!');
this.currentRoute_.listeners.onCancel();
} else {
// There was an error with this route, so we going to abandon it and
// resolve the error. We don't reset the payload because the payload may
// still contain useful information.
this.currentRoute_.listeners.onError(error);
}
// The route and step are done. Clear them before we handle the error or
// else we may attempt to abort |currentStep_| when handling the error.
this.currentRoute_ = null;
this.currentStep_ = null;
// Still need to handle error because aborting an operation could leave us
// in an unexpected state.
this.currentlyAt_ = await this.implementation_.handleError(
this.currentlyWith_,
error);
}
}
/**
* If the main loop is blocked waiting for new work, then resolve the promise
* so that the next iteration of the main loop can execute.
*
* @private
*/
unblockMainLoop_() {
if (this.waitForWork_) {
this.waitForWork_.resolve();
this.waitForWork_ = null;
}
}
};
/**
* @typedef {{
* getNext: function(
* shaka.routing.Node,
* shaka.routing.Payload,
* shaka.routing.Node,
* shaka.routing.Payload):shaka.routing.Node,
* enterNode: function(
* shaka.routing.Node,
* shaka.routing.Payload,
* shaka.routing.Payload):!shaka.util.AbortableOperation,
* handleError: function(
* shaka.routing.Payload,
* !Error):!Promise.<shaka.routing.Node>,
* onIdle: function(shaka.routing.Node)
* }}
*
* @description
* There are some parts of the walker that will be per-instance. This type
* provides those per-instance parts.
*
* @property {function(
* shaka.routing.Node,
* shaka.routing.Payload,
* shaka.routing.Node,
* shaka.routing.Payload):shaka.routing.Node getNext
* Get the next node that the walker should move to. This method will be
* passed (in this order) the current node, current payload, destination
* node, and destination payload.
*
* @property {function(
* shaka.routing.Node,
* shaka.routing.Payload,
* shaka.routing.Payload):!Promise} enterNode
* When the walker moves into a node, it will call |enterNode| and allow the
* implementation to change the current payload. This method will be passed
* (in this order) the node the walker is entering, the current payload, and
* the destination payload. This method should NOT modify the destination
* payload.
*
* @property {function(
* shaka.routing.Payload,
* !Error):!Promise.<shaka.routing.Node> handleError
* This is the callback for when |enterNode| fails. It is passed the current
* payload and the error. If a step is aborted, the error will be
* OPERATION_ABORTED. It should reset all external dependences, modify the
* payload, and return the new current node. Calls to |handleError| should
* always resolve and the walker should always be able to continue operating.
*
* @property {function(shaka.routing.Node)} onIdle
* This is the callback for when the walker has finished processing all route
* requests and needs to wait for more work. |onIdle| will be passed the
* current node. After |onIdle| has been called, the walker will block until
* a new request is made, or the walker is destroyed.
*/
shaka.routing.Walker.Implementation;
/**
* @typedef {{
* onStart: function(),
* onEnd: function(),
* onCancel: function(),
* onError: function(!Error),
* onSkip: function(),
* onEnter: function(shaka.routing.Node)
* }}
*
* @description
* The collection of callbacks that the walker will call while executing a
* route. By setting these immediately after calling |startNewRoute|
* the user can react to route-specific events.
*
* @property {function()} onStart
* The callback for when the walker has accepted the route and will soon take
* the first step unless interrupted. Either |onStart| or |onSkip| will be
* called.
*
* @property {function()} onEnd
* The callback for when the walker has reached the end of the route. For
* every route that had |onStart| called, either |onEnd|, |onCancel|, or
* |onError| will be called.
*
* @property {function()} onCancel
* The callback for when the walker is stopping a route before getting to the
* end. This will be called either when a new route is interrupting the route,
* or the walker is being destroyed mid-route. |onCancel| will only be called
* when a route has been interrupted by another route or the walker is being
* destroyed.
*
* @property {function()} onError
* The callback for when the walker failed to execute the route because an
* unexpected error occurred. The walker will enter a recovery mode and the
* route will be abandoned.
*
* @property {function()} onSkip
* The callback for when the walker was ready to start the route, but the
* create-method returned |null|.
*
* @property {function()} onEnter
* The callback for when the walker enters a node. This will allow us to
* track the progress of the walker within a per-route scope.
*/
shaka.routing.Walker.Listeners;
/**
* @typedef {{
* node: shaka.routing.Node,
* payload: shaka.routing.Payload,
* interruptible: boolean
* }}
*
* @description
* The public description of where the walker should go. This is created
* when the callback given to |startNewRoute| is called by the walker.
*
* @property {shaka.routing.Node} node
* The node that the walker should move towards. This will be passed to
* |shaka.routing.Walker.Implementation.getNext| to help determine where to
* go next.
*
* @property {shaka.routing.Payload| payload
* The payload that the walker should have once it arrives at |node|. This
* will be passed to the |shaka.routing.Walker.Implementation.getNext| to
* help determine where to go next.
*
* @property {boolean} interruptible
* Whether or not this route can be interrupted by another request. When
* |true| this route will be interrupted so that a pending request can be
* resolved. When |false|, the route will be allowed to finished before
* resolving the next request.
*/
shaka.routing.Walker.Route;
/**
* @typedef {{
* node: shaka.routing.Node,
* payload: shaka.routing.Payload,
* interruptible: boolean,
* listeners: shaka.routing.Walker.Listeners
* }}
*
* @description
* The active route is the walker's internal representation of a route. It
* is the union of |shaka.routing.Walker.Request_| and the
* |shaka.routing.Walker.Route| created by |shaka.routing.Walker.Request_|.
*
* @property {shaka.routing.Node} node
* The node that the walker should move towards. This will be passed to
* |shaka.routing.Walker.Implementation.getNext| to help determine where to
* go next.
*
* @property {shaka.routing.Payload| payload
* The payload that the walker should have once it arrives at |node|. This
* will be passed to the |shaka.routing.Walker.Implementation.getNext| to
* help determine where to go next.
*
* @property {boolean} interruptible
* Whether or not this route can be interrupted by another request. When
* |true| this route will be interrupted so that a pending request can be
* resolved. When |false|, the route will be allowed to finished before
* resolving the next request.
*
* @property {shaka.routing.Walker.Listeners} listeners
* The listeners that the walker can used to communicate with whoever
* requested the route.
*
* @private
*/
shaka.routing.Walker.ActiveRoute_;
/**
* @typedef {{
* create: function(shaka.routing.Payload):?shaka.routing.Walker.Route,
* listeners: shaka.routing.Walker.Listeners
* }}
*
* @description
* The request is how users can talk to the walker. They can give the walker
* a request and when the walker is ready, it will resolve the request by
* calling |create|.
*
* @property {
* function(shaka.routing.Payload):?shaka.routing.Walker.Route} create
* The function called when the walker is ready to start a new route. This can
* return |null| to say that the request was not possible and should be
* skipped.
*
* @property {shaka.routing.Walker.Listeners} listeners
* The collection of callbacks that the walker will use to talk to whoever
* provided the request.
*
* @private
*/
shaka.routing.Walker.Request_;
-1
View File
@@ -172,7 +172,6 @@ shaka.util.FakeEvent.EventName = {
MediaQualityChanged: 'mediaqualitychanged',
Metadata: 'metadata',
OnStateChange: 'onstatechange',
OnStateIdle: 'onstateidle',
RateChange: 'ratechange',
SegmentAppended: 'segmentappended',
SessionDataEvent: 'sessiondata',
+59
View File
@@ -0,0 +1,59 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
goog.provide('shaka.util.Mutex');
goog.require('shaka.log');
/**
* @summary A simple mutex.
*/
shaka.util.Mutex = class {
/** Constructs the mutex. */
constructor() {
/** @private {?string} */
this.acquiredIdentifier = null;
/** @private {!Array.<function()>} */
this.unlockQueue = [];
}
/**
* Acquires the mutex, as soon as possible.
* @param {string} identifier
* @return {!Promise}
*/
async acquire(identifier) {
shaka.log.v2(identifier + ' has requested mutex');
if (this.acquiredIdentifier) {
await new Promise((resolve) => this.unlockQueue.push(resolve));
}
this.acquiredIdentifier = identifier;
shaka.log.v2(identifier + ' has acquired mutex');
}
/**
* Releases your hold on the mutex.
*/
release() {
shaka.log.v2(this.acquiredIdentifier + ' has released mutex');
if (this.unlockQueue.length > 0) {
this.unlockQueue.shift()();
} else {
this.acquiredIdentifier = null;
}
}
/**
* Completely releases the mutex. Meant for use by the tests.
*/
releaseAll() {
while (this.acquiredIdentifier) {
this.release();
}
}
};
-1
View File
@@ -50,7 +50,6 @@ goog.require('shaka.polyfill.VTTCue');
goog.require('shaka.polyfill.VideoPlayPromise');
goog.require('shaka.polyfill.VideoPlaybackQuality');
goog.require('shaka.polyfill');
goog.require('shaka.routing.Walker');
goog.require('shaka.text.Cue');
goog.require('shaka.text.LrcTextParser');
goog.require('shaka.text.Mp4TtmlParser');
+3 -2
View File
@@ -16,10 +16,11 @@ describe('Ad manager', () => {
/** @type {google.ima.AdsRenderingSettings} */
let adsRenderingSettings;
beforeEach(() => {
beforeEach(async () => {
window['google'] = null;
mockVideo = new shaka.test.FakeVideo();
player = new shaka.Player(mockVideo);
player = new shaka.Player();
await player.attach(mockVideo, /* initializeMediaSource= */ false);
adManager = player.getAdManager();
expect(adManager instanceof shaka.ads.AdManager).toBe(true);
+3 -4
View File
@@ -65,7 +65,7 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
support = await shaka.media.DrmEngine.probeSupport();
});
beforeEach(() => {
beforeEach(async () => {
mockReceiverApi = createMockReceiverApi();
const mockCanDisplayType = jasmine.createSpy('canDisplayType');
@@ -87,7 +87,8 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
document.body.appendChild(video);
player = new shaka.Player(video);
player = new shaka.Player();
await player.attach(video);
receiver = new CastReceiver(video, player);
toRestore = [];
@@ -320,8 +321,6 @@ filterDescribe('CastReceiver', castReceiverIntegrationSupport, () => {
// Add wrappers to various methods along player.load to make sure that,
// at each stage, the cast receiver can form an update message without
// causing an error.
waitForUpdateMessageWrapper(
shaka.media.ManifestParser, 'ManifestParser', 'getFactory');
waitForUpdateMessageWrapper(
// eslint-disable-next-line no-restricted-syntax
shaka.test.TestScheme.ManifestParser.prototype, 'ManifestParser',
+1
View File
@@ -27,6 +27,7 @@ describe('CastUtils', () => {
'getManifestParserFactory', // Would not serialize.
'setVideoContainer',
'getActiveSessionsMetadata',
'releaseAllMutexes', // Very specific to the inner workings of the player.
// Test helper methods (not @export'd)
'createDrmEngine',
@@ -26,7 +26,8 @@ describe('Codec Switching', () => {
beforeEach(async () => {
await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
player = new compiledShaka.Player(video);
player = new compiledShaka.Player();
await player.attach(video);
// Disable stall detection, which can interfere with playback tests.
player.configure('streaming.stallEnabled', false);
+2 -1
View File
@@ -31,7 +31,8 @@ describe('DashParser', () => {
beforeEach(async () => {
await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
player = new compiledShaka.Player(video);
player = new compiledShaka.Player();
await player.attach(video);
// Disable stall detection, which can interfere with playback tests.
player.configure('streaming.stallEnabled', false);
+3 -2
View File
@@ -38,8 +38,9 @@ describe('MSS Player', () => {
await shaka.test.Loader.loadShaka(getClientArg('uncompiled'));
});
beforeEach(() => {
player = new compiledShaka.Player(video);
beforeEach(async () => {
player = new compiledShaka.Player();
await player.attach(video);
// Make sure we are playing the lowest res available to avoid test flake
// based on network issues. Note that disabling ABR and setting a low
+2 -1
View File
@@ -30,7 +30,8 @@ filterDescribe('Offline', supportsStorage, () => {
});
beforeEach(async () => {
player = new shaka.Player(video);
player = new shaka.Player();
await player.attach(video);
player.addEventListener('error', fail);
// Disable stall detection, which can interfere with playback tests.
+3 -2
View File
@@ -736,15 +736,16 @@ filterDescribe('Storage', storageSupport, () => {
const videoElement = /** @type {!HTMLVideoElement} */(
document.createElement('video'));
beforeEach(() => {
beforeEach(async () => {
netEngine = makeNetworkEngine();
// Use a real Player since Storage only uses the configuration and
// networking engine. This allows us to use Player.configure in these
// tests.
player = new shaka.Player(videoElement, ((player) => {
player = new shaka.Player(null, ((player) => {
player.createNetworkingEngine = () => netEngine;
}));
await player.attach(videoElement);
storage = new shaka.offline.Storage(player);
+4 -2
View File
@@ -33,8 +33,9 @@ describe('Player', () => {
support = await compiledShaka.Player.probeSupport();
});
beforeEach(() => {
player = new compiledShaka.Player(video);
beforeEach(async () => {
player = new compiledShaka.Player();
await player.attach(video);
// Make sure we are playing the lowest res available to avoid test flake
// based on network issues. Note that disabling ABR and setting a low
@@ -68,6 +69,7 @@ describe('Player', () => {
eventManager.release();
await player.destroy();
player.releaseAllMutexes();
});
afterAll(() => {
+9 -4
View File
@@ -32,7 +32,8 @@ describe('Player', () => {
beforeEach(async () => {
await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
player = new compiledShaka.Player(video);
player = new compiledShaka.Player();
await player.attach(video);
// Disable stall detection, which can interfere with playback tests.
player.configure('streaming.stallEnabled', false);
@@ -53,6 +54,7 @@ describe('Player', () => {
eventManager.release();
await player.destroy();
player.releaseAllMutexes();
});
afterAll(() => {
@@ -83,7 +85,8 @@ describe('Player', () => {
// Unlike the other tests in this file, this uses an uncompiled build of
// Shaka, so that we don't need to expose shaka.util.Timer.activeTimers.
player = new shaka.Player(video);
player = new shaka.Player();
await player.attach(video);
// Disable stall detection, which can interfere with playback tests.
player.configure('streaming.stallEnabled', false);
@@ -672,7 +675,8 @@ describe('Player', () => {
// uncompiled version. Then we will get assertions.
eventManager.unlisten(player, 'error');
await player.destroy();
player = new shaka.Player(video); // NOTE: MUST BE UNCOMPILED
player = new shaka.Player(); // NOTE: MUST BE UNCOMPILED
await player.attach(video);
player.configure({abr: {enabled: false}});
eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
@@ -707,7 +711,8 @@ describe('Player', () => {
// uncompiled version. Then we will get assertions.
eventManager.unlisten(player, 'error');
await player.destroy();
player = new shaka.Player(video); // NOTE: MUST BE UNCOMPILED
player = new shaka.Player(); // NOTE: MUST BE UNCOMPILED
await player.attach(video);
player.configure({abr: {enabled: false}});
eventManager.listen(player, 'error', Util.spyFunc(onErrorSpy));
+47 -151
View File
@@ -15,8 +15,8 @@ describe('Player Load Graph', () => {
/** @type {!jasmine.Spy} */
let stateChangeSpy;
/** @type {!jasmine.Spy} */
let stateIdleSpy;
/** @type {?string} */
let lastStateChange = null;
beforeAll(() => {
video = shaka.test.UiUtils.createVideoElement();
@@ -29,57 +29,29 @@ describe('Player Load Graph', () => {
beforeEach(() => {
stateChangeSpy = jasmine.createSpy('stateChange');
stateIdleSpy = jasmine.createSpy('stateIdle');
lastStateChange = null;
});
/**
* @param {HTMLMediaElement} attachedTo
*/
function createPlayer(attachedTo) {
player = new shaka.Player(attachedTo);
function createPlayer() {
player = new shaka.Player();
player.addEventListener(
'onstatechange',
shaka.test.Util.spyFunc(stateChangeSpy));
player.addEventListener(
'onstateidle',
shaka.test.Util.spyFunc(stateIdleSpy));
player.addEventListener('onstatechange', (event) => {
lastStateChange = event['state'];
});
}
// Even though some test will destroy the player, we want to make sure that
// we don't allow the player to stay attached to the video element.
afterEach(async () => {
await player.destroy();
});
it('attach and initialize media source when constructed with media element',
async () => {
expect(video.src).toBeFalsy();
createPlayer(/* attachedTo= */ video);
// Wait until we enter the media source state.
await new Promise((resolve) => {
whenEnteringState('media-source', resolve);
});
expect(video.src).toBeTruthy();
});
it('does not set video.src when no video is provided', async () => {
expect(video.src).toBeFalsy();
createPlayer(/* attachedTo= */ null);
// Wait until the player has hit an idle state (no more internal loading
// actions).
await spyIsCalled(stateIdleSpy);
expect(video.src).toBeFalsy();
player.releaseAllMutexes();
});
it('attach + initializeMediaSource=true will initialize media source',
async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
expect(video.src).toBeFalsy();
await player.attach(video, /* initializeMediaSource= */ true);
@@ -88,7 +60,7 @@ describe('Player Load Graph', () => {
it('attach + initializeMediaSource=false will not intialize media source',
async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
expect(video.src).toBeFalsy();
await player.attach(video, /* initializeMediaSource= */ false);
@@ -97,7 +69,7 @@ describe('Player Load Graph', () => {
it('unload + initializeMediaSource=false does not initialize media source',
async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
await player.load('test:sintel');
@@ -108,7 +80,7 @@ describe('Player Load Graph', () => {
it('unload + initializeMediaSource=true initializes media source',
async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
await player.load('test:sintel');
@@ -117,50 +89,8 @@ describe('Player Load Graph', () => {
expect(video.src).toBeTruthy();
});
// There was a bug when calling unload before calling load would cause
// the load to continue before the (first) unload was complete.
// https://github.com/shaka-project/shaka-player/issues/612
it('load will wait for unload to finish', async () => {
createPlayer(/* attachedTo= */ null);
await player.attach(video);
await player.load('test:sintel');
// We are going to call |unload| and |load| right after each other. What
// we expect to see is that the player is fully unloaded before the load
// occurs.
const unload = player.unload();
const load = player.load('test:sintel');
await unload;
await load;
expect(getVisitedStates()).toEqual([
'attach',
// First call to |load|.
'media-source',
'manifest-parser',
'manifest',
'drm-engine',
'load',
// Our call to |unload| would have started the transition to
// "unloaded", but since we called |load| right away, the transition
// to "unloaded" was most likely done by the call to |load|.
'unload',
'attach',
'media-source',
'manifest-parser',
'manifest',
'drm-engine',
'load',
]);
});
it('load and unload can be called multiple times', async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
@@ -180,7 +110,6 @@ describe('Player Load Graph', () => {
'drm-engine',
'load',
'unload',
'attach',
'media-source',
// Load and unload 2
@@ -189,13 +118,12 @@ describe('Player Load Graph', () => {
'drm-engine',
'load',
'unload',
'attach',
'media-source',
]);
});
it('load can be called multiple times', async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
@@ -215,7 +143,6 @@ describe('Player Load Graph', () => {
// Load 2
'unload',
'attach',
'media-source',
'manifest-parser',
'manifest',
@@ -224,7 +151,6 @@ describe('Player Load Graph', () => {
// Load 3
'unload',
'attach',
'media-source',
'manifest-parser',
'manifest',
@@ -234,7 +160,7 @@ describe('Player Load Graph', () => {
});
it('load will interrupt load', async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
@@ -247,23 +173,8 @@ describe('Player Load Graph', () => {
await load2;
});
it('unload will interrupt load', async () => {
createPlayer(/* attachedTo= */ null);
await player.attach(video);
const load = player.load('test:sintel');
const unload = player.unload();
await expectAsync(load).toBeRejected();
await unload;
// We should never have gotten into the loaded state.
expect(getVisitedStates()).not.toContain('load');
});
it('destroy will interrupt load', async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
@@ -280,7 +191,7 @@ describe('Player Load Graph', () => {
// When |destroy| is called, the player should move through the unload state
// on its way to the detached state.
it('destroy will unload and then detach', async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
@@ -305,7 +216,7 @@ describe('Player Load Graph', () => {
// |unload| after another |unload| call should just have the player re-enter
// the state it was waiting in.
it('unloading multiple times is okay', async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
@@ -326,12 +237,10 @@ describe('Player Load Graph', () => {
// |player.unload| (first call)
'unload',
'attach',
'media-source',
// |player.unload| (second call)
'unload',
'attach',
'media-source',
]);
});
@@ -339,7 +248,7 @@ describe('Player Load Graph', () => {
// When we destroy, it will allow a current unload operation to occur even
// though we are going to unload and detach as part of |destroy|.
it('destroy will not interrupt unload', async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
await player.load('test:sintel');
@@ -355,7 +264,7 @@ describe('Player Load Graph', () => {
// afterEach), this test will explicitly express our intentions to support
// the use-case of calling |destroy| multiple times on a player instance.
it('destroying multiple times is okay', async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
await player.attach(video);
await player.load('test:sintel');
@@ -369,7 +278,7 @@ describe('Player Load Graph', () => {
// instance when loading.
it('pre-initialized media source is used when player continues loading',
async () => {
createPlayer(/* attachedTo= */ null);
createPlayer();
// After we attach and initialize media source, we should just see
// two states in our history.
@@ -409,7 +318,7 @@ describe('Player Load Graph', () => {
* @return {!Promise}
*/
async function testInterruptAfter(state) {
createPlayer(/* attachedTo= */ null);
createPlayer();
let pendingUnload;
whenEnteringState(state, () => {
@@ -446,7 +355,7 @@ describe('Player Load Graph', () => {
describe('error handling', () => {
beforeEach(() => {
createPlayer(/* attachedTo= */ null);
createPlayer();
});
it('returns to attach after load error', async () => {
@@ -472,12 +381,12 @@ describe('Player Load Graph', () => {
// Wait a couple interrupter cycles to allow the player to enter idle
// state.
const event = await spyIsCalled(stateIdleSpy);
await shaka.test.Util.delay(/* seconds= */ 0.25);
// Since attached and loaded in the same interrupter cycle, there won't be
// any idle time until we finish failing to load. We expect to idle in
// attach.
expect(event.state).toBe('attach');
expect(lastStateChange).toBe('unload');
});
});
@@ -492,8 +401,8 @@ describe('Player Load Graph', () => {
mediaSource = window.MediaSource;
window['MediaSource'] = undefined;
createPlayer(/* attachTo= */ null);
await spyIsCalled(stateIdleSpy);
createPlayer();
await shaka.test.Util.delay(/* seconds= */ 0.25);
});
afterEach(() => {
@@ -508,8 +417,8 @@ describe('Player Load Graph', () => {
// should stop at the attach state.
player.attach(video, /* initMediaSource= */ true);
const event = await spyIsCalled(stateIdleSpy);
expect(event.state).toBe('attach');
await shaka.test.Util.delay(/* seconds= */ 0.25);
expect(lastStateChange).toBe('attach');
});
it('loading ignores media source path', async () => {
@@ -520,8 +429,8 @@ describe('Player Load Graph', () => {
// src= path.
player.load(SMALL_MP4_CONTENT_URI);
const event = await spyIsCalled(stateIdleSpy);
expect(event.state).toBe('src-equals');
await shaka.test.Util.delay(/* seconds= */ 0.25);
expect(lastStateChange).toBe('src-equals');
});
it('unloading ignores init media source flag', async () => {
@@ -533,8 +442,8 @@ describe('Player Load Graph', () => {
// don't have media source, it should stop at the attach state.
player.unload(/* initMediaSource= */ true);
const event = await spyIsCalled(stateIdleSpy);
expect(event.state).toBe('attach');
await shaka.test.Util.delay(/* seconds= */ 0.25);
expect(lastStateChange).toBe('unload');
});
});
@@ -543,9 +452,8 @@ describe('Player Load Graph', () => {
// and then telling it to go to one of our destination states (e.g. attach,
// load with media source, load with src=).
describe('routing', () => {
beforeEach(async () => {
createPlayer(/* attachedTo= */ null);
await spyIsCalled(stateIdleSpy);
beforeEach(() => {
createPlayer();
});
it('goes from detach to detach', async () => {
@@ -600,7 +508,7 @@ describe('Player Load Graph', () => {
it('goes from media source to media source', async () => {
await startIn('media-source');
await goTo('media-source');
await goTo('media-source', 'attach'); // doesn't remake media source
});
it('goes from media source to load', async () => {
@@ -625,7 +533,7 @@ describe('Player Load Graph', () => {
it('goes from load to media source', async () => {
await startIn('load');
await goTo('media-source');
await goTo('media-source', 'attach'); // doesn't remake media source
});
it('goes from load to load', async () => {
@@ -677,7 +585,7 @@ describe('Player Load Graph', () => {
it('goes from manifest parser to media source', async () => {
await passingThrough('manifest-parser', () => {
return goTo('media-source');
return goTo('media-source', 'attach'); // doesn't remake media source
});
});
@@ -707,7 +615,7 @@ describe('Player Load Graph', () => {
it('goes from manifest to media source', async () => {
await passingThrough('manifest', () => {
return goTo('media-source');
return goTo('media-source', 'attach'); // doesn't remake media source
});
});
@@ -737,7 +645,7 @@ describe('Player Load Graph', () => {
it('goes from drm engine to media source', async () => {
await passingThrough('drm-engine', () => {
return goTo('media-source');
return goTo('media-source', 'attach'); // doesn't remake media source
});
});
@@ -818,15 +726,8 @@ describe('Player Load Graph', () => {
const action = actions.get(state);
expect(action).toBeTruthy();
// Do not wait for the action to complete, our idle spy makes us wait. We
// want to know where we stop, so using the idle spy is more accurate in
// this situation.
action();
// Make sure that the player stops in the state that we asked it go to.
const event = await spyIsCalled(stateIdleSpy);
expect(event.state).toBe(state);
await action();
expect(lastStateChange).toBe(state);
}
/**
@@ -891,9 +792,10 @@ describe('Player Load Graph', () => {
* starting the state change.
*
* @param {string} state
* @param {string=} expectedState
* @return {!Promise}
*/
async function goTo(state) {
async function goTo(state, expectedState) {
/** @type {!Map.<string, function():!Promise>} */
const actions = new Map()
.set('detach', () => {
@@ -914,14 +816,8 @@ describe('Player Load Graph', () => {
const action = actions.get(state);
expect(action).toBeTruthy();
// Do not wait for the action to complete, our idle spy make us wait. We
// want to know where we stop, so using the idle spy is more accurate in
// this situation.
action();
const event = await spyIsCalled(stateIdleSpy);
expect(event.state).toBe(state);
await action();
expect(lastStateChange).toBe(expectedState || state);
}
});
+1
View File
@@ -29,6 +29,7 @@ describe('Player Src Equals', () => {
afterEach(async () => {
await player.destroy();
player.releaseAllMutexes();
});
afterAll(() => {
+1
View File
@@ -37,6 +37,7 @@ describe('Player Src Equals', () => {
afterEach(async () => {
await player.destroy();
player.releaseAllMutexes();
eventManager.release();
});
+5 -27
View File
@@ -48,7 +48,7 @@ describe('Player', () => {
/** @type {!shaka.test.FakeVideo} */
let video;
beforeEach(() => {
beforeEach(async () => {
// By default, errors are a failure.
logErrorSpy = jasmine.createSpy('shaka.log.error');
logErrorSpy.calls.reset();
@@ -124,6 +124,7 @@ describe('Player', () => {
open: jasmine.createSpy('open').and.returnValue(Promise.resolve()),
destroy:
jasmine.createSpy('destroy').and.returnValue(Promise.resolve()),
setTextDisplayer: jasmine.createSpy('setTextDisplayer'),
setUseEmbeddedText: jasmine.createSpy('setUseEmbeddedText'),
getUseEmbeddedText: jasmine.createSpy('getUseEmbeddedText'),
setSegmentRelativeVttTiming:
@@ -151,7 +152,8 @@ describe('Player', () => {
}
video = new shaka.test.FakeVideo(20);
player = new shaka.Player(video, dependencyInjector);
player = new shaka.Player(null, dependencyInjector);
await player.attach(video);
player.configure({
// Ensures we don't get a warning about missing preference.
preferredAudioLanguage: 'en',
@@ -170,6 +172,7 @@ describe('Player', () => {
try {
await player.destroy();
} finally {
player.releaseAllMutexes();
shaka.log.error = originalLogError;
shaka.log.alwaysError = originalLogError;
shaka.log.warning = originalLogWarn;
@@ -234,31 +237,6 @@ describe('Player', () => {
expect(mediaSourceEngine.destroy).toHaveBeenCalled();
expect(drmEngine.destroy).toHaveBeenCalled();
});
// TODO(vaage): Re-enable once the parser is integrated into the load graph
// better.
xit('destroys parser first when interrupting load', async () => {
const p = shaka.test.Util.shortDelay();
/** @type {!shaka.test.FakeManifestParser} */
const parser = new shaka.test.FakeManifestParser(manifest);
parser.start.and.returnValue(p);
parser.stop.and.callFake(() => {
expect(abrManager.stop).not.toHaveBeenCalled();
expect(abrManager.release).not.toHaveBeenCalled();
expect(networkingEngine.destroy).not.toHaveBeenCalled();
});
shaka.media.ManifestParser.registerParserByMime(
fakeMimeType, () => parser);
const load = player.load(fakeManifestUri, 0, fakeMimeType);
await shaka.test.Util.shortDelay();
await player.destroy();
expect(abrManager.stop).toHaveBeenCalled();
expect(abrManager.release).toHaveBeenCalled();
expect(networkingEngine.destroy).toHaveBeenCalled();
expect(parser.stop).toHaveBeenCalled();
await expectAsync(load).toBeRejected();
});
});
describe('load/unload', () => {
-505
View File
@@ -1,505 +0,0 @@
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
describe('Walker', () => {
const AbortableOperation = shaka.util.AbortableOperation;
// For our tests, we will not use the payload in our routing logic. To
// avoid distracting the reader with payload details, hide them here.
const payload = {
factory: null,
mediaElement: null,
mimeType: null,
startTime: null,
startTimeOfLoad: NaN,
uri: null,
keepAdManager: false,
};
// The graph topology that we will be using for our tests.
//
// [ A ] ---> [ B ] ---> [ E ]
// ^ |
// | v
// [ D ] <--- [ C ]
//
/** @type {shaka.routing.Node} */
const nodeA = {name: 'a'};
/** @type {shaka.routing.Node} */
const nodeB = {name: 'b'};
/** @type {shaka.routing.Node} */
const nodeC = {name: 'c'};
/** @type {shaka.routing.Node} */
const nodeD = {name: 'd'};
/** @type {shaka.routing.Node} */
const nodeE = {name: 'e'};
/**
* @param {shaka.routing.Node} at
* @param {shaka.routing.Node} goingTo
* @return {shaka.routing.Node}
*/
function getNext(at, goingTo) {
// In this graph, where you start determines where you go. The one exception
// is node B, which acts as a fork between nodes C and E. This fork is
// important in testing interrupts.
let goTo = null;
if (at == nodeA) {
goTo = nodeB;
}
if (at == nodeB) {
goTo = goingTo == nodeE ? nodeE : nodeC;
}
if (at == nodeC) {
goTo = nodeD;
}
if (at == nodeD) {
goTo = nodeA;
}
goog.asserts.assert(goTo, 'We should have found a next step.');
return goTo;
}
/** @type {!shaka.routing.Walker} */
let walker;
/** @type {!jasmine.Spy} */
let enterNodeSpy;
/** @type {!jasmine.Spy} */
let handleErrorSpy;
/** @type {!jasmine.Spy} */
let idleSpy;
beforeEach(() => {
enterNodeSpy = jasmine.createSpy('enterNode');
enterNodeSpy.and.returnValue(AbortableOperation.completed(undefined));
handleErrorSpy = jasmine.createSpy('handleError');
idleSpy = jasmine.createSpy('idle');
const implementation = {
getNext: (at, has, goingTo, wants) => getNext(at, goingTo),
enterNode: shaka.test.Util.spyFunc(enterNodeSpy),
handleError: shaka.test.Util.spyFunc(handleErrorSpy),
onIdle: shaka.test.Util.spyFunc(idleSpy),
};
walker = new shaka.routing.Walker(nodeA, payload, implementation);
});
afterEach(async () => {
await walker.destroy();
});
it('enters idle after initialization', async () => {
await waitOnSpy(idleSpy);
});
it('enters idle after completing route', async () => {
// Execute a route but then wait a couple interrupter cycles to allow the
// walker time to idle.
await completesRoute(startNewRoute(nodeD, /* interruptible= */ false));
await waitOnSpy(idleSpy);
});
it('enters idle after error', async () => {
// The specific error does not matter.
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.BAD_ENCODING);
enterNodeSpy.and.callFake((at) => {
return at == nodeC ?
shaka.util.AbortableOperation.failed(error) :
shaka.util.AbortableOperation.completed(undefined);
});
handleErrorSpy.and.returnValue(nodeA);
// Go to nodeD, passing through nodeC. This will fail, calling handleError,
// and returning the walker to nodeA. Wait a couple interrupter cycles to
// allow the walker time to idle. The route needs to be interruptible since
// we are going to return an aborted operation.
await failsRoute(startNewRoute(nodeD, /* interruptible= */ true));
await waitOnSpy(idleSpy);
});
// The walker should move node-by-node, so starting in node A (see beforeEach)
// and going to nodeD, we should see the walker enter nodeB, nodeC, and nodeD.
// We won't see it enter nodeA because it starts there and therefore never
// "enters" that node.
it('moves node-by-node', async () => {
// We don't expect any errors in this test.
handleErrorSpy.and.callFake(fail);
await completesRoute(startNewRoute(nodeD, /* interruptible= */ false));
const steps = getStepsTaken();
expect(steps).toEqual([nodeB, nodeC, nodeD]);
});
// We want to make sure that if a route is registered as interruptible, we
// can interrupt the route and start a new route.
//
// For this we will have the walker start going to node E, but when it
// enters node B (the fork) we will interrupt it and tell it to go to
// node A.
it('can interrupt interruptible routes', async () => {
// We don't expect any errors in this test.
handleErrorSpy.and.callFake(fail);
/** @type {!shaka.util.PublicPromise} */
const atA = new shaka.util.PublicPromise();
const interrupt = () => {
const goToA = startNewRoute(nodeA, /* interruptible= */ false);
goToA.onEnd = () => atA.resolve();
};
const goingToE = startNewRoute(nodeE, /* interruptible= */ true);
goingToE.onEnter = (node) => {
if (node == nodeB) {
interrupt();
}
};
await atA;
const steps = getStepsTaken();
expect(steps).toEqual([nodeB, nodeC, nodeD, nodeA]);
});
// We want to make sure that a non-interruptible route cannot be interrupted
// by starting a new route. To do this, we are going to start off by going to
// node c. When we get to node b, we will try to start a new route to node e.
//
// If the route was interrupted, we would go to straight to node e (node b is
// a fork in the graph). However, since we expect the first route to finish,
// we expect to see the walker go to node c and then continue around to get to
// node e.
it('cannot interrupt non-interruptible routes', async () => {
// We don't expect any errors in this test.
handleErrorSpy.and.callFake(fail);
/** @type {!shaka.util.PublicPromise} */
const atE = new shaka.util.PublicPromise();
const interrupt = () => {
const goToE = startNewRoute(nodeE, /* interruptible= */ false);
goToE.onEnd = () => atE.resolve();
};
// Create a "trap" so that once we enter node B (the fork) that we will
// issue a new route - this will ensure that we are trying to interrupt a
// route mid-execution.
const goingToC = startNewRoute(nodeC, /* interruptible= */ false);
goingToC.onEnter = (node) => {
interrupt();
};
await atE;
const steps = getStepsTaken();
expect(steps).toEqual([
// First route.
nodeB, nodeC,
// Second route.
nodeD, nodeA, nodeB, nodeE,
]);
});
// We do not want to execute steps for a route that will be interrupted
// right after it starts. For this example, we queue-up three routes:
// 1. Non-interruptible
// 2. Interruptible
// 3. Interruptible
//
// What we expect to see is that Route 1 finishes, Route 2 starts but takes
// no steps, and Route 3 finishes.
it('does not take steps for route interrupted before starting', async () => {
/**
* When a route starts, it will assign this value to its id (1, 2, or 3)
* so we always know who was the most recent route to start.
*
* @type {?number}
*/
let currentRoute = null;
/**
* We use this set to know who took steps. When the walker takes a step
* we will add |currentRoute| to this set. That way we will know what
* route we took steps on.
*
* @type {!Array.<?number>}
*/
const tookSteps = [];
enterNodeSpy.and.callFake((node) => {
tookSteps.push(currentRoute);
return AbortableOperation.completed(undefined);
});
const route1 = startNewRoute(nodeD, /* interruptible= */ false);
route1.onStart = () => {
currentRoute = 1;
};
const route2 = startNewRoute(nodeC, /* interruptible= */ true);
route2.onStart = () => {
currentRoute = 2;
};
const route3 = startNewRoute(nodeE, /* interruptible= */ true);
route3.onStart = () => {
currentRoute = 3;
};
// Wait until we get to the end of route 3, that should be the end.
await completesRoute(route3);
// Make sure we had the correct routes when taking each step.
expect(tookSteps).toEqual([
1, // A to B
1, // B to C
1, // C to D
3, // D to A
3, // A to B
3, // B to E
]);
});
// When we destroy the walker, it should cancel all routes - even the
// non-interruptible routes.
it('cancels all routes when destroyed', async () => {
// Start-up a couple routes, and then destroy the walker. We expect to see
// both routes have their |onCancel| callbacks called. We make the first
// be non-interruptible and the second route interruptible so that we can
// see both types be cancelled by |destroy|. The non-interruptible route
// must before first or else it would interrupt the other route.
const goToC = startNewRoute(nodeC, /* interruptible= */ false);
const goToB = startNewRoute(nodeB, /* interruptible= */ true);
/** @type {!jasmine.Spy} */
const canceledCSpy = jasmine.createSpy('cancel c');
goToC.onCancel = shaka.test.Util.spyFunc(canceledCSpy);
/** @type {!jasmine.Spy} */
const canceledBSpy = jasmine.createSpy('cancel b');
goToB.onCancel = shaka.test.Util.spyFunc(canceledBSpy);
await walker.destroy();
expect(canceledCSpy).toHaveBeenCalled();
expect(canceledBSpy).toHaveBeenCalled();
});
it('calls handleError when step fails', async () => {
// The specific error does not matter.
const error = new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.TEXT,
shaka.util.Error.Code.BAD_ENCODING);
// Make the walker fail when it hits node C. This should allow us to
// exercise the |handleError| path.
enterNodeSpy.and.callFake((at) => {
return at == nodeC ?
shaka.util.AbortableOperation.failed(error) :
shaka.util.AbortableOperation.completed(undefined);
});
// We want to handle the error and return to a safe state, so just put the
// walker back at node A.
handleErrorSpy.and.returnValue(nodeA);
// Go to D (passing through C). This should throw an error, so wait for the
// error to be seen. The route must be abortable because we are going to
// throw an abortable error.
await failsRoute(startNewRoute(nodeD, /* interruptible= */ true));
expect(handleErrorSpy).toHaveBeenCalled();
});
// When we interrupt a route that has a step that can be aborted, it should
// abort the operation and enter the error recovery mode.
//
// To model this we will make a node be a never-resolving node. We will enter
// the node and then get stuck. From there we will request a new route, the
// blocked op will abort, and then we will go to our new destination.
it('can abort current step', async () => {
// Because we need a node to start at after resetting, we will just use A.
// There is no special reason for node A.
handleErrorSpy.and.returnValue(Promise.resolve(nodeA));
// Block when we enter node C so that we can re-route to node E and finish a
// path.
blockWalkerAt(nodeC);
// Wait for us to enter node d before continuing. We introduce a small delay
// to ensure that we are "stuck" on the abortable operation.
const goingToD = startNewRoute(nodeD, /* interruptible= */ true);
await waitUntilEntering(goingToD, nodeC);
await shaka.test.Util.shortDelay();
await completesRoute(startNewRoute(nodeE, /* interruptible= */ true));
expect(handleErrorSpy).toHaveBeenCalled();
});
// If we are in the middle of a node and |destroy| is called, we want to
// ensure that we exit as soon as possible. If the current step is abortable
// then we want to abort.
it('can abort current step with destroy', async () => {
// Because we need a node to start at after resetting, we will just use A.
// There is no special reason for node A.
handleErrorSpy.and.returnValue(Promise.resolve(nodeA));
// Block when we enter node C so that we can re-route to node E and finish a
// path.
blockWalkerAt(nodeC);
// Wait for us to enter node d before continuing. We introduce a small delay
// to ensure that we are "stuck" on the abortable operation.
const goingToD = startNewRoute(nodeD, /* interruptible= */ true);
await waitUntilEntering(goingToD, nodeC);
await shaka.test.Util.shortDelay();
// We are "stuck" in nodeC. We will now destroy the walker which should
// abort the nodeC step, enter error recovery mode, and then shutdown.
await walker.destroy();
expect(handleErrorSpy).toHaveBeenCalled();
});
/**
* Ask the walker to start a new route. Since the requests from our tests
* are very basic, wrapping the call should not hide too much information.
*
* @param {shaka.routing.Node} goingTo
* @param {boolean} interruptible
*
* @return {shaka.routing.Walker.Listeners}
*/
function startNewRoute(goingTo, interruptible) {
return walker.startNewRoute((currentPayload) => {
return {
node: goingTo,
payload: payload,
interruptible: interruptible,
};
});
}
/**
* Get the series of nodes that the walker went through during its "journey".
*
* @return {!Array.<shaka.routing.Node>}
*/
function getStepsTaken() {
// Use |onNode| to get the steps that completed.
return enterNodeSpy.calls.allArgs().map((args) => args[0]);
}
/**
* Configure the |enterNodeSpy| so that we will block when we enter |node|. In
* order to unblock, the route will need to be interrupted.
*
* @param {shaka.routing.Node} node
*/
function blockWalkerAt(node) {
/** @type {!shaka.util.AbortableOperation} */
const completedOp = AbortableOperation.completed(undefined);
/** @type {!shaka.util.PublicPromise} */
const waitForever = new shaka.util.PublicPromise();
/** @type {!shaka.util.AbortableOperation} */
const blockingOp = new AbortableOperation(
waitForever,
() => {
waitForever.reject(new shaka.util.Error(
shaka.util.Error.Severity.CRITICAL,
shaka.util.Error.Category.PLAYER,
shaka.util.Error.Code.OPERATION_ABORTED));
return waitForever;
});
enterNodeSpy.and.callFake((at) => {
return at == node ? blockingOp : completedOp;
});
}
/**
* Create a promise that will resolve when we enter |node| while on the route
* that produced |events|.
*
* @param {shaka.routing.Walker.Listeners} events
* @param {shaka.routing.Node} node
* @return {!Promise}
*/
function waitUntilEntering(events, node) {
return new Promise((resolve) => {
events.onEnter = (node) => {
if (node == nodeC) {
resolve();
}
};
});
}
/**
* Create a promise from a walker's route's listeners. This assumes that the
* route should finish. The promise will resolve if the route completes and
* will reject if the route fails to complete for any reason.
*
* @param {shaka.routing.Walker.Listeners} events
* @return {!Promise}
*/
function completesRoute(events) {
return new Promise((resolve, reject) => {
events.onEnd = resolve;
events.onCancel = reject;
events.onError = reject;
});
}
/**
* Create a promise from a walker's route's listeners. This assumes that the
* route should not finish. The promise will resolve if the route fails and
* will reject if the route completes.
*
* @param {shaka.routing.Walker.Listeners} events
* @return {!Promise}
*/
function failsRoute(events) {
return new Promise((resolve, reject) => {
events.onEnd = reject;
events.onCancel = resolve;
events.onError = resolve;
});
}
/**
* Wrap a spy in a promise so that the promise will resolve once the spy is
* called.
*
* @param {!jasmine.Spy} spy
* @return {!Promise}
*/
function waitOnSpy(spy) {
return new Promise((resolve) => {
spy.and.callFake(resolve);
});
}
});
+4 -3
View File
@@ -9,10 +9,11 @@ shaka.test.UiUtils = class {
* @param {!HTMLElement} videoContainer
* @param {!HTMLMediaElement} video
* @param {!Object=} config
* @return {!shaka.ui.Overlay}
* @return {!Promise.<!shaka.ui.Overlay>}
*/
static createUIThroughAPI(videoContainer, video, config) {
const player = new shaka.Player(video);
static async createUIThroughAPI(videoContainer, video, config) {
const player = new shaka.Player();
await player.attach(video);
// Create UI
config = config || {};
const ui = new shaka.ui.Overlay(player, videoContainer, video);
+1 -1
View File
@@ -37,7 +37,7 @@ filterDescribe('Cue layout', shaka.test.TextLayoutTests.supported, () => {
// Set up UI controls. The video element is in a paused state by
// default, so the controls should be shown. The video is not in the
// DOM and is purely temporary.
const player = new shaka.Player(null);
const player = new shaka.Player();
ui = new shaka.ui.Overlay(
player, /** @type {!HTMLElement} */(helper.videoContainer),
shaka.test.UiUtils.createVideoElement());
+32 -2
View File
@@ -4,7 +4,36 @@
* SPDX-License-Identifier: Apache-2.0
*/
describe('Transmuxer Player', () => {
/**
* For unknown reasons, these tests fail in the test labs for Edge on Windows,
* in ways that do not seem to be unrelated to transmuxers.
* Practical testing has not found any sign that playback is actually broken in
* Edge, so these tests are disabled on Edge for the time being.
* TODO(#5834): Remove this filter once the tests are fixed.
* @return {boolean}
*/
function checkNoBrokenEdge() {
const chromeVersion = shaka.util.Platform.chromeVersion();
if (shaka.util.Platform.isWindows() && shaka.util.Platform.isEdge() &&
chromeVersion && chromeVersion <= 118) {
// When the tests fail, it's due to the manifest parser failing to find a
// factory. Attempt to find a factory first, to avoid filtering the tests
// when running in a non-broken Edge environment.
const uri = 'fakeuri.m3u8';
const mimeType = 'application/x-mpegurl';
/* eslint-disable no-restricted-syntax */
try {
shaka.media.ManifestParser.getFactory(uri, mimeType);
return true;
} catch (error) {
return false;
}
/* eslint-enable no-restricted-syntax */
}
return true;
}
filterDescribe('Transmuxer Player', checkNoBrokenEdge, () => {
const Util = shaka.test.Util;
/** @type {!jasmine.Spy} */
@@ -73,7 +102,8 @@ describe('Transmuxer Player', () => {
beforeEach(async () => {
await shaka.test.TestScheme.createManifests(compiledShaka, '_compiled');
player = new compiledShaka.Player(video);
player = new compiledShaka.Player();
await player.attach(video);
player.configure('mediaSource.forceTransmux', true);
player.configure('streaming.useNativeHlsOnSafari', false);
+2 -2
View File
@@ -25,14 +25,14 @@ describe('Ad UI', () => {
shaka.Player.setAdManagerFactory(() => new shaka.test.FakeAdManager());
});
beforeEach(() => {
beforeEach(async () => {
container =
/** @type {!HTMLElement} */ (document.createElement('div'));
document.body.appendChild(container);
video = shaka.test.UiUtils.createVideoElement();
container.appendChild(video);
UiUtils.createUIThroughAPI(container, video);
await UiUtils.createUIThroughAPI(container, video);
adManager = video['ui'].getControls().getPlayer().getAdManager();
});
+17 -16
View File
@@ -36,9 +36,9 @@ describe('UI Customization', () => {
container.appendChild(video);
});
it('only the specified controls are created', () => {
it('only the specified controls are created', async () => {
const config = {controlPanelElements: ['time_and_duration', 'mute']};
UiUtils.createUIThroughAPI(container, video, config);
await UiUtils.createUIThroughAPI(container, video, config);
// Only current time and mute button should've been created
UiUtils.confirmElementFound(container, 'shaka-current-time');
@@ -49,9 +49,9 @@ describe('UI Customization', () => {
UiUtils.confirmElementMissing(container, 'shaka-overflow-menu-button');
});
it('only the specified overflow menu buttons are created', () => {
it('only the specified overflow menu buttons are created', async () => {
const config = {overflowMenuButtons: ['cast']};
UiUtils.createUIThroughAPI(container, video, config);
await UiUtils.createUIThroughAPI(container, video, config);
UiUtils.confirmElementFound(container, 'shaka-cast-button');
@@ -59,30 +59,31 @@ describe('UI Customization', () => {
});
it('seek bar only created when configured', async () => {
const ui =
UiUtils.createUIThroughAPI(container, video, {addSeekBar: false});
const ui = await UiUtils.createUIThroughAPI(
container, video, {addSeekBar: false});
UiUtils.confirmElementMissing(container, 'shaka-seek-bar');
await ui.destroy();
UiUtils.createUIThroughAPI(container, video, {addSeekBar: true});
await UiUtils.createUIThroughAPI(container, video, {addSeekBar: true});
UiUtils.confirmElementFound(container, 'shaka-seek-bar');
});
it('big play button only created when configured', async () => {
const ui =
UiUtils.createUIThroughAPI(container, video, {addBigPlayButton: false});
const ui = await UiUtils.createUIThroughAPI(
container, video, {addBigPlayButton: false});
UiUtils.confirmElementMissing(container, 'shaka-play-button-container');
UiUtils.confirmElementMissing(container, 'shaka-play-button');
await ui.destroy();
UiUtils.createUIThroughAPI(container, video, {addBigPlayButton: true});
await UiUtils.createUIThroughAPI(
container, video, {addBigPlayButton: true});
UiUtils.confirmElementFound(container, 'shaka-play-button-container');
UiUtils.confirmElementFound(container, 'shaka-play-button');
});
it('settings menus are positioned lower when seek bar is absent', () => {
it('settings menus are lower when seek bar is absent', async () => {
const config = {addSeekBar: false};
UiUtils.createUIThroughAPI(container, video, config);
await UiUtils.createUIThroughAPI(container, video, config);
function confirmLowPosition(className) {
const elements =
@@ -101,7 +102,7 @@ describe('UI Customization', () => {
confirmLowPosition('shaka-playback-rates');
});
it('controls are created in specified order', () => {
it('controls are created in specified order', async () => {
const config = {
controlPanelElements: [
'mute',
@@ -110,7 +111,7 @@ describe('UI Customization', () => {
],
};
UiUtils.createUIThroughAPI(container, video, config);
await UiUtils.createUIThroughAPI(container, video, config);
const controlsButtonPanels =
container.getElementsByClassName('shaka-controls-button-panel');
@@ -131,7 +132,7 @@ describe('UI Customization', () => {
it('layout can be re-configured after the creation', async () => {
const config = {controlPanelElements: ['time_and_duration', 'mute']};
const ui = UiUtils.createUIThroughAPI(container, video, config);
const ui = await UiUtils.createUIThroughAPI(container, video, config);
// Only current time and mute button should've been created
UiUtils.confirmElementFound(container, 'shaka-current-time');
@@ -177,7 +178,7 @@ describe('UI Customization', () => {
it('cast proxy and controls are unchanged by reconfiguration', async () => {
const config = {controlPanelElements: ['time_and_duration', 'mute']};
/** @type {!shaka.ui.Overlay} */
const ui = UiUtils.createUIThroughAPI(container, video, config);
const ui = await UiUtils.createUIThroughAPI(container, video, config);
const eventManager = new shaka.util.EventManager();
const waiter = new shaka.test.Waiter(eventManager);
+2 -1
View File
@@ -45,7 +45,8 @@ describe('UI', () => {
videoContainer = shaka.util.Dom.createHTMLElement('div');
videoContainer.appendChild(video);
document.body.appendChild(videoContainer);
player = new compiledShaka.Player(video);
player = new compiledShaka.Player();
await player.attach(video);
// Create UI
// Add all of the buttons we have
+25 -19
View File
@@ -36,14 +36,14 @@ describe('UI', () => {
/** @type {!HTMLVideoElement} */
let video;
beforeEach(() => {
beforeEach(async () => {
videoContainer =
/** @type {!HTMLElement} */ (document.createElement('div'));
document.body.appendChild(videoContainer);
video = shaka.test.UiUtils.createVideoElement();
videoContainer.appendChild(video);
UiUtils.createUIThroughAPI(videoContainer, video);
await UiUtils.createUIThroughAPI(videoContainer, video);
});
it('has all the basic elements', () => {
@@ -193,7 +193,8 @@ describe('UI', () => {
],
doubleClickForFullscreen: false,
};
const ui = UiUtils.createUIThroughAPI(videoContainer, video, config);
const ui = await UiUtils.createUIThroughAPI(
videoContainer, video, config);
const controls = ui.getControls();
const spy = spyOn(controls, 'toggleFullScreen');
@@ -216,8 +217,8 @@ describe('UI', () => {
/** @type {!HTMLElement} */
let controlsContainer;
beforeEach(() => {
const ui = UiUtils.createUIThroughAPI(videoContainer, video);
beforeEach(async () => {
const ui = await UiUtils.createUIThroughAPI(videoContainer, video);
player = ui.getControls().getLocalPlayer();
const controlsContainers =
videoContainer.getElementsByClassName('shaka-controls-container');
@@ -246,13 +247,14 @@ describe('UI', () => {
/** @type {!HTMLElement} */
let overflowMenu;
beforeEach(() => {
beforeEach(async () => {
const config = {
controlPanelElements: [
'overflow_menu',
],
};
const ui = UiUtils.createUIThroughAPI(videoContainer, video, config);
const ui = await UiUtils.createUIThroughAPI(
videoContainer, video, config);
player = ui.getControls().getLocalPlayer();
const overflowMenus =
@@ -323,8 +325,8 @@ describe('UI', () => {
/** @type {!HTMLElement} */
let controlsButtonPanel;
it('has default elements', () => {
UiUtils.createUIThroughAPI(videoContainer, video);
it('has default elements', async () => {
await UiUtils.createUIThroughAPI(videoContainer, video);
const controlsButtonPanels = videoContainer.getElementsByClassName(
'shaka-controls-button-panel');
@@ -357,7 +359,7 @@ describe('UI', () => {
}
});
it('is accessible', () => {
it('is accessible', async () => {
function confirmAriaLabel(className) {
const elements =
controlsButtonPanel.getElementsByClassName(className);
@@ -376,7 +378,7 @@ describe('UI', () => {
],
};
UiUtils.createUIThroughAPI(videoContainer, video, config);
await UiUtils.createUIThroughAPI(videoContainer, video, config);
const controlsButtonPanels = videoContainer.getElementsByClassName(
'shaka-controls-button-panel');
expect(controlsButtonPanels.length).toBe(1);
@@ -403,14 +405,15 @@ describe('UI', () => {
/** @type {!Element} */
let languageMenuButton;
beforeEach(() => {
beforeEach(async () => {
const config = {
controlPanelElements: [
'quality',
'language',
],
};
const ui = UiUtils.createUIThroughAPI(videoContainer, video, config);
const ui = await UiUtils.createUIThroughAPI(
videoContainer, video, config);
player = ui.getControls().getLocalPlayer();
const resolutionsMenus =
@@ -473,7 +476,7 @@ describe('UI', () => {
/** @type {shaka.ui.Controls} */
let controls;
beforeEach(() => {
beforeEach(async () => {
const config = {
controlPanelElements: [
'overflow_menu',
@@ -482,7 +485,8 @@ describe('UI', () => {
'quality',
],
};
const ui = UiUtils.createUIThroughAPI(videoContainer, video, config);
const ui = await UiUtils.createUIThroughAPI(
videoContainer, video, config);
controls = ui.getControls();
player = controls.getLocalPlayer();
@@ -696,7 +700,7 @@ describe('UI', () => {
/** @type {!HTMLElement} */
let contextMenu;
beforeEach(() => {
beforeEach(async () => {
const config = {
customContextMenu: true,
contextMenuElements: [
@@ -705,7 +709,8 @@ describe('UI', () => {
'fakeElement',
],
};
const ui = UiUtils.createUIThroughAPI(videoContainer, video, config);
const ui = await UiUtils.createUIThroughAPI(
videoContainer, video, config);
controlsContainer = ui.getControls().getControlsContainer();
@@ -745,7 +750,7 @@ describe('UI', () => {
/** @type {!HTMLElement} */
let statisticsContainer;
beforeEach(() => {
beforeEach(async () => {
const config = {
customContextMenu: true,
contextMenuElements: [
@@ -753,7 +758,8 @@ describe('UI', () => {
],
statisticsList: Object.keys(new shaka.util.Stats().getBlob()),
};
const ui = UiUtils.createUIThroughAPI(videoContainer, video, config);
const ui = await UiUtils.createUIThroughAPI(
videoContainer, video, config);
player = ui.getControls().getLocalPlayer();
const statisticsButtons =
+3 -2
View File
@@ -425,8 +425,7 @@ shaka.ui.Overlay = class {
*/
static async setupUIandAutoLoad_(container, video, canvas) {
// Create the UI
const player = new shaka.Player(
shaka.util.Dom.asHTMLMediaElement(video));
const player = new shaka.Player();
const ui = new shaka.ui.Overlay(player,
shaka.util.Dom.asHTMLElement(container),
shaka.util.Dom.asHTMLMediaElement(video));
@@ -489,6 +488,8 @@ shaka.ui.Overlay = class {
shaka.log.error('Error auto-loading asset', e);
}
}
await player.attach(shaka.util.Dom.asHTMLMediaElement(video));
}
};