mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-13 15:46:46 +03:00
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:
+1
-4
@@ -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
@@ -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.';
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -79,14 +79,14 @@ the end of the tutorial.
|
||||
```js
|
||||
// myapp.js
|
||||
|
||||
function initApp() {
|
||||
async function initApp() {
|
||||
// Install built-in polyfills to patch browser incompatibilities.
|
||||
shaka.polyfill.installAll();
|
||||
|
||||
// Check to see if the browser supports the basic APIs Shaka needs.
|
||||
if (shaka.Player.isBrowserSupported()) {
|
||||
// Everything looks good!
|
||||
initPlayer();
|
||||
await initPlayer();
|
||||
} else {
|
||||
// This browser does not have the minimum set of APIs we need.
|
||||
console.error('Browser not supported!');
|
||||
@@ -99,10 +99,11 @@ function initApp() {
|
||||
window.addEventListener('offline', updateOnlineStatus);
|
||||
}
|
||||
|
||||
function initPlayer() {
|
||||
async function initPlayer() {
|
||||
// Create a Player instance.
|
||||
const video = document.getElementById('video');
|
||||
const player = new shaka.Player(video);
|
||||
const player = new shaka.Player();
|
||||
await player.attach(video);
|
||||
|
||||
// Attach player and storage to the window to make it easy to access
|
||||
// in the JS console and so we can access it in other methods.
|
||||
@@ -437,14 +438,14 @@ That’s it! For your convenience, here is the completed code:
|
||||
```js
|
||||
// myapp.js
|
||||
|
||||
function initApp() {
|
||||
async function initApp() {
|
||||
// Install built-in polyfills to patch browser incompatibilities.
|
||||
shaka.polyfill.installAll();
|
||||
|
||||
// Check to see if the browser supports the basic APIs Shaka needs.
|
||||
if (shaka.Player.isBrowserSupported()) {
|
||||
// Everything looks good!
|
||||
initPlayer();
|
||||
await initPlayer();
|
||||
} else {
|
||||
// This browser does not have the minimum set of APIs we need.
|
||||
console.error('Browser not supported!');
|
||||
@@ -457,10 +458,11 @@ function initApp() {
|
||||
window.addEventListener('offline', updateOnlineStatus);
|
||||
}
|
||||
|
||||
function initPlayer() {
|
||||
async function initPlayer() {
|
||||
// Create a Player instance.
|
||||
const video = document.getElementById('video');
|
||||
const player = new shaka.Player(video);
|
||||
const player = new shaka.Player();
|
||||
await player.attach(video);
|
||||
|
||||
// Attach player and storage to the window to make it easy to access
|
||||
// in the JS console and so we can access it in other methods.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
|
||||
|
||||
@@ -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_) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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_;
|
||||
@@ -172,7 +172,6 @@ shaka.util.FakeEvent.EventName = {
|
||||
MediaQualityChanged: 'mediaqualitychanged',
|
||||
Metadata: 'metadata',
|
||||
OnStateChange: 'onstatechange',
|
||||
OnStateIdle: 'onstateidle',
|
||||
RateChange: 'ratechange',
|
||||
SegmentAppended: 'segmentappended',
|
||||
SessionDataEvent: 'sessiondata',
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ describe('Player Src Equals', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await player.destroy();
|
||||
player.releaseAllMutexes();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
|
||||
@@ -37,6 +37,7 @@ describe('Player Src Equals', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await player.destroy();
|
||||
player.releaseAllMutexes();
|
||||
|
||||
eventManager.release();
|
||||
});
|
||||
|
||||
+5
-27
@@ -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', () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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 =
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user