Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

shaka-player

Package Overview
Dependencies
Maintainers
2
Versions
347
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

shaka-player - npm Package Compare versions

Comparing version 2.0.0-beta to 2.0.0-beta2

demo/controls.css

80

CHANGELOG.md

@@ -0,1 +1,81 @@

## 2.0.0-beta2 (2016-05-04)
Restored Features from v1 Missing in v2.0.0-beta:
- Track restrictions API
- https://github.com/google/shaka-player/issues/326
- https://github.com/google/shaka-player/issues/327
- Custom controls demo for live
- https://github.com/google/shaka-player/issues/322
- Trick play demo
- https://github.com/google/shaka-player/issues/328
New features:
- Reduced startup latency
- Added player.resetConfiguration()
- Added response text to HTTP errors
- https://github.com/google/shaka-player/issues/319
- Demo controls redesigned with material design icons
- Emit an error if the wrong keys are retrieved
- https://github.com/google/shaka-player/issues/301
- Human-readable errors shown in demo app
- Cache-friendly bandwidth estimation
- https://github.com/google/shaka-player/issues/324
- Improved trick play and playbackRate support
- https://github.com/google/shaka-player/issues/344
- Allow apps to reset ABR manager estimates
- https://github.com/google/shaka-player/issues/355
- Support non-zero start times for VOD
- https://github.com/google/shaka-player/issues/341
- https://github.com/google/shaka-player/issues/348
- https://github.com/google/shaka-player/issues/357
Bugfixes:
- Fix playback of DASH with unaligned Representations
- Fixed race conditions on seek
- https://github.com/google/shaka-player/issues/334
- Improved drift handling
- https://github.com/google/shaka-player/issues/330
- Fixed stack overflow in StringUtils
- https://github.com/google/shaka-player/issues/335
- Improved live support
- https://github.com/google/shaka-player/issues/331
- https://github.com/google/shaka-player/issues/339
- https://github.com/google/shaka-player/issues/340
- https://github.com/google/shaka-player/issues/351
- Fixed player.addTextTrack
- Handle CDMs which don't support the same types MSE does
- https://github.com/google/shaka-player/issues/342
- Fix audio-only encrypted playback
- https://github.com/google/shaka-player/issues/360
- Fix renaming of event properties
- https://github.com/google/shaka-player/issues/361
- Warn about missing clock sync elements in live manfiests
- https://github.com/google/shaka-player/issues/290
- Add option for default clock sync URI
- https://github.com/google/shaka-player/issues/290
- Fix crash in TextEngine when subs are turned off
Docs:
- Shaka v2 upgrade guide
- http://shaka-player-demo.appspot.com/docs/api/tutorial-upgrade.html
- Added enum values (not just names) to generated docs
- https://github.com/google/shaka-player/issues/337
Broken Compatibility compared to v2.0.0-beta:
- None!
## 1.6.5 (2016-04-08)
Bugfixes:
- Always build the same input files to a stable output
- https://github.com/google/shaka-player/pull/299
- Properly extern the 'xhr' property of HTTP errors
- https://github.com/google/shaka-player/pull/319
## 2.0.0-beta (2016-04-07)

@@ -2,0 +82,0 @@

13

demo/asset_section.js

@@ -114,3 +114,6 @@ /**

var config = { abr: {}, drm: {}, manifest: { dash: {} } };
var config = /** @type {shakaExtern.PlayerConfiguration} */(
{ abr: {}, drm: {}, manifest: { dash: {} } });
config.manifest.dash.clockSyncUri =
'//shaka-player-demo.appspot.com/time.txt';

@@ -149,4 +152,4 @@ if (!asset) {

player.configure(/** @type {shakaExtern.PlayerConfiguration} */(
config));
player.resetConfiguration();
player.configure(config);

@@ -170,2 +173,6 @@ // Configure network filters.

player.load(asset.manifestUri).then(function() {
(asset.extraText || []).forEach(function(extraText) {
player.addTextTrack(extraText.uri, extraText.language, extraText.kind,
extraText.mime, extraText.codecs);
});
}, function(reason) {

@@ -172,0 +179,0 @@ var error = /** @type {!shaka.util.Error} */(reason);

@@ -97,4 +97,6 @@ /**

* uri: string,
* language: string,
* kind: string,
* lang: string
* mime: string,
* codecs: (string|undefined)
* }}

@@ -104,6 +106,10 @@ *

* The URI of the text.
* @property {string} language
* The language of the text (e.g. 'en').
* @property {string} kind
* The kind of text (e.g. 'subtitles').
* @property {string} lang
* The language of the text (e.g. 'en').
* @property {string} mime
* The MIME type of the text (e.g. 'text/vtt')
* @property {(string|undefined)} codecs
* (optional) The codecs string, if needed to refine the MIME type.
*/

@@ -318,2 +324,23 @@ shakaAssets.ExtraText;

},
{
name: '"Dig the Uke" by Stefan Kartenberg (audio only, multicodec, Widevine)', // gjslint: disable=110
// From: http://dig.ccmixter.org/files/JeffSpeed68/53327
// Licensed under Creative Commons BY-NC 3.0.
// Free for non-commercial use with attribution.
// http://creativecommons.org/licenses/by-nc/3.0/
manifestUri: '//storage.googleapis.com/shaka-demo-assets/dig-the-uke/dash.mpd', // gjslint: disable=110
encoder: shakaAssets.Encoder.EDASH_PACKAGER,
source: shakaAssets.Source.SHAKA,
drm: [shakaAssets.KeySystem.WIDEVINE],
features: [
shakaAssets.Feature.MP4,
shakaAssets.Feature.SEGMENT_BASE,
shakaAssets.Feature.WEBM
],
licenseServers: {
'com.widevine.alpha': '//widevine-proxy.appspot.com/proxy'
}
},
// }}}

@@ -501,3 +528,3 @@

{
name: 'Tears of Steel (encrypted)',
name: 'Tears of Steel (Widevine)',
manifestUri: '//demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-dash-widevine.ism/.mpd', // gjslint: disable=110

@@ -508,10 +535,11 @@

drm: [
// TODO: The PlayReady version of this is being rejected by Edge & IE.
// shakaAssets.KeySystem.PLAYREADY,
shakaAssets.KeySystem.WIDEVINE
],
features: [
shakaAssets.Feature.EMBEDDED_TEXT,
shakaAssets.Feature.HIGH_DEFINITION,
shakaAssets.Feature.MP4,
shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE,
shakaAssets.Feature.SUBTITLES,
shakaAssets.Feature.TTML,
shakaAssets.Feature.ULTRA_HIGH_DEFINITION

@@ -522,6 +550,28 @@ ],

'com.widevine.alpha': '//widevine-proxy.appspot.com/proxy'
// TODO: Locate PlayReady server URI.
}
},
{
name: 'Tears of Steel (PlayReady)',
manifestUri: '//demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-dash-playready.ism/.mpd', // gjslint: disable=110
encoder: shakaAssets.Encoder.UNIFIED_STREAMING,
source: shakaAssets.Source.UNIFIED_STREAMING,
drm: [
shakaAssets.KeySystem.PLAYREADY
],
features: [
shakaAssets.Feature.EMBEDDED_TEXT,
shakaAssets.Feature.HIGH_DEFINITION,
shakaAssets.Feature.MP4,
shakaAssets.Feature.SEGMENT_TEMPLATE_TIMELINE,
shakaAssets.Feature.SUBTITLES,
shakaAssets.Feature.TTML,
shakaAssets.Feature.ULTRA_HIGH_DEFINITION
],
licenseServers: {
'com.microsoft.playready': '//playready.directtaps.net/pr/svc/rightsmanager.asmx?PlayRight=1&UseSimpleNonPersistentLicense=1' // gjslint: disable=110
}
},
{
name: 'Tears of Steel (subtitles)',

@@ -562,3 +612,3 @@ manifestUri: '//demo.unified-streaming.com/video/tears-of-steel/tears-of-steel-ru.ism/.mpd', // gjslint: disable=110

name: 'Live sim (2s segments)',
manifestUri: '//vm2.dashif.org/livesim/testpic_2s/Manifest.mpd',
manifestUri: '//vm2.dashif.org/livesim/utc_head/testpic_2s/Manifest.mpd',

@@ -576,3 +626,3 @@ encoder: shakaAssets.Encoder.UNKNOWN,

name: 'Live sim (6s segments)',
manifestUri: '//vm2.dashif.org/livesim/testpic_6s/Manifest.mpd',
manifestUri: '//vm2.dashif.org/livesim/utc_head/testpic_6s/Manifest.mpd',

@@ -593,3 +643,3 @@ encoder: shakaAssets.Encoder.UNKNOWN,

{
name: 'Big Buck Bunny',
name: 'Big Buck Bunny (Live)',
manifestUri: '//wowzaec2demo.streamlock.net/live/bigbuckbunny/manifest_mpm4sav_mvtime.mpd', // gjslint: disable=110

@@ -677,3 +727,3 @@

{
name: 'Tears Of Steel',
name: 'Tears Of Steel (external text)',
manifestUri: '//ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TearsOfSteel_WAMEH264SmoothStreaming720p.ism/manifest(format=mpd-time-csf)', // gjslint: disable=110

@@ -683,14 +733,17 @@ extraText: [

uri: '//ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TOS-en.vtt', // gjslint: disable=110
language: 'en',
kind: 'subtitle',
lang: 'en'
mime: 'text/vtt'
},
{
uri: '//ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TOS-es.vtt', // gjslint: disable=110
language: 'es',
kind: 'subtitle',
lang: 'es'
mime: 'text/vtt'
},
{
uri: '//ams-samplescdn.streaming.mediaservices.windows.net/11196e3d-2f40-4835-9a4d-fc52751b0323/TOS-fr.vtt', // gjslint: disable=110
language: 'fr',
kind: 'subtitle',
lang: 'fr'
mime: 'text/vtt'
}

@@ -697,0 +750,0 @@ ],

@@ -29,2 +29,4 @@ /**

'keyup', shakaDemo.onConfigKeyUp_);
document.getElementById('showTrickPlay').addEventListener(
'change', shakaDemo.onTrickPlayChange_);
document.getElementById('enableAdaptation').addEventListener(

@@ -62,1 +64,11 @@ 'change', shakaDemo.onAdaptationChange_);

};
/**
* @param {!Event} event
* @private
*/
shakaDemo.onTrickPlayChange_ = function(event) {
// Show/hide trick play controls.
shakaDemo.controls_.showTrickPlay(event.target.checked);
};

@@ -35,2 +35,6 @@ /**

/** @private {ShakaControls} */
shakaDemo.controls_ = null;
/**

@@ -110,2 +114,5 @@ * Initialize the application.

shakaDemo.controls_ = new ShakaControls();
shakaDemo.controls_.init(shakaDemo.video_, shakaDemo.player_);
// If a custom asset was given in the URL, select it now.

@@ -143,3 +150,3 @@ if ('asset' in params) {

console.error('Player error', error);
var message = 'Error code ' + error.code;
var message = error.message || ('Error code ' + error.code);
var errorDisplay = document.getElementById('errorDisplay');

@@ -146,0 +153,0 @@ errorDisplay.textContent = message;

## Architecture Diagrams
![Shaka data flow diagram](dataflow.gv.png)
![Shaka ownership diagram](ownership.gv.png)
![Shaka data flow diagram](dataflow.gv.png)

@@ -8,3 +8,3 @@ # Basic Usage

3. In your application's JavaScript:
1. Install polyfills.
1. Install Shaka's polyfills.
2. Check for browser support.

@@ -15,7 +15,2 @@ 3. Create a Player object to wrap the video element.

*NOTE: For simplicity, we will use `alert()` for errors in this tutorial.
In a real application, you would want a more appropriate mechanism to display
errors to your users.*
```html

@@ -49,3 +44,5 @@ <!DOCTYPE html>

// Check to see if the browser supports the basic APIs Shaka needs.
// This is an asynchronous check.
shaka.Player.support().then(function(support) {
// This executes when the asynchronous check is complete.
if (support.supported) {

@@ -56,3 +53,3 @@ // Everything looks good!

// This browser does not have the minimum set of APIs we need.
alert('Browser not supported!');
console.error('Browser not supported!');
}

@@ -74,5 +71,7 @@ });

// Try to load a manifest.
// This is an asynchronous process.
player.load(manifestUri).then(function() {
// The video has now been loaded!
}).catch(onError);
// This runs if the asynchronous load is successful.
console.log('The video has now been loaded!');
}).catch(onError); // onError is executed if the asynchronous load fails.
}

@@ -87,4 +86,3 @@

// Log the error.
console.error(error);
alert('Error code ' + error.code);
console.error('Error code', error.code, 'object', error);
}

@@ -91,0 +89,0 @@

@@ -15,3 +15,3 @@ # Debugging the Uncompiled Library

This causes an alert that says "Error code 4000". What now?
This causes an error in the console that says "Error code 4000". What now?

@@ -53,9 +53,13 @@

1. Closure's base library
2. A dependency file which maps Shaka's class names to source files
3. The uncompiled library's bootstrap file, "shaka-player.uncompiled.js"
1. Closure's base library. (This is a small JavaScript library related to the
Closure compiler used when we build Shaka.) This is what loads Shaka without
requiring you to list all 50+ source files.
2. A dependency file which maps Shaka's class names to source files. This is
how Closure's base library can locate all of Shaka's individual source files.
3. The uncompiled Shaka library's bootstrap file, "shaka-player.uncompiled.js".
This uses Closure to load the top-level parts of the library. Each of those
files in turn load their internal dependencies.
The uncompiled library's bootstrap file will use Closure to "require" each of
the exported classes in the library. This causes the entire library to be
loaded, one uncompiled source file at a time.
Once we're using the uncompiled library, we will be able to see detailed logs
and line numbers for errors. It is difficult to debug without this.

@@ -82,3 +86,3 @@ ```html

s…a.u…l.Error {category: 4, code: 4000, data: Array[1],
Error {category: 4, code: 4000, data: Array[1],
message: "Shaka Error MANIFEST.UNABLE_TO_GUESS_MANIFEST_TYPE ()",

@@ -91,3 +95,6 @@ stack: "Error: Shaka Error… at http://localhost/shaka/lib/player.js:300:35"}

includes a message that gives the full human-readable name of the error:
`MANIFEST.UNABLE_TO_GUESS_MANIFEST_TYPE`.
`MANIFEST.UNABLE_TO_GUESS_MANIFEST_TYPE`. The `MANIFEST` part is the textual
name for category: 4, and `UNABLE_TO_GUESS_MANIFEST_TYPE` is the textual name
for code: 4000. (A full list can be found in the docs for
{@link shaka.util.Error}).

@@ -132,3 +139,3 @@ There's also a `stack` field showing the context in which it was generated:

A request for "" is interpretted as a relative URL. What we actually requested
A request for "" is interpreted as a relative URL. What we actually requested
was the index page for the folder the HTML is in.

@@ -135,0 +142,0 @@

@@ -44,5 +44,5 @@ # DRM Configuration

If this is the only `<ContentProtection>` element in the manifest, Shaka will
try {@link shaka.dash.ContentProtection.defaultKeySystems\_ all key systems
it knows}. If the browser supports it and you configured a license server URL
for it, we'll use it.
try {@linksource shaka.dash.ContentProtection.defaultKeySystems\_ all key
systems it knows}. If the browser supports it and you configured a license
server URL for it, we'll use it.

@@ -49,0 +49,0 @@

@@ -14,3 +14,5 @@ [

] } },
{ "plugins": { "title": "Plugins and Customizing the Build" } }
{ "plugins": { "title": "Plugins and Customizing the Build" } },
{ "architecture": { "title": "Architecture Diagrams" } },
{ "upgrade": { "title": "Shaka v2 Upgrade Guide" } }
]

@@ -5,3 +5,3 @@ # License Server Authentication

that it only delivers licenses to paying users. In this tutorial, we're going
to use different license server endpoints that require different forms of
to use various license server endpoints that require various forms of
authentication.

@@ -8,0 +8,0 @@

@@ -12,6 +12,5 @@ # Network and Buffering Configuration

license requests), `manifest.retryParameters` (for manifest requests), and
`streaming.retryParameters` (for segment requests).
`streaming.retryParameters` (for segment requests). All three structures are
identical:
They all look the same, though:
```js

@@ -18,0 +17,0 @@ retryParameters: {

# Plugins and Customizing the Build
Shaka has a plugin system to make it easier to extend and customize the
library. Plugins can be written outside the library (in your application), or
library. The plugin interfaces are here to allow you extend or customize Shaka
Player in one of these areas: manifest parsing, subtitle and caption parsing,
networking, ABR, and polyfills. Each of these parts of the system has its own
plugin interface. Even our "built-in" parsers, such as DASH and WebVTT, are
actually just plugins we build by default.
Plugins can be written outside the library (in your application), or
they can be built into the library to take advantage of the [Closure compiler].

@@ -9,9 +15,9 @@

Manifest parsing, subtitle and caption parsing, networking, ABR, and polyfills
are all done through plugins. Even our "built-in" parsers, such as DASH and
WebVTT, are actually just default plugins.
#### Plugins
We will only cover plugin concepts at a high level here and will not go into
detailed examples of plugin development. If you are interested in customizing
or extending Shaka in one of these areas, please see the links below.
A plugin registers itself with a "core" component. These are the various

@@ -25,3 +31,3 @@ plugin interfaces and the default plugins that Shaka provides:

- Default manifest parser plugins:
- DASH: {@link shaka.dash.DashParser}
- DASH: {@linksource shaka.dash.DashParser}

@@ -32,3 +38,3 @@ __Subtitle/caption parsers__

- Default text parser plugins:
- WebVTT: {@link shaka.media.VttTextParser}
- WebVTT: {@linksource shaka.media.VttTextParser}

@@ -39,4 +45,4 @@ __Networking plugins__

- Default networking plugins:
- HTTP(S): {@link shaka.net.HttpPlugin}
- data URIs: {@link shaka.net.DataUriPlugin}
- HTTP(S): {@linksource shaka.net.HttpPlugin}
- data URIs: {@linksource shaka.net.DataUriPlugin}

@@ -47,3 +53,3 @@ __ABR plugins__

- Must implement the {@link shakaExtern.AbrManager} interface
- Default AbrManager implementation: {@link shaka.abr.SimpleAbrManager}
- Default AbrManager implementation: {@linksource shaka.abr.SimpleAbrManager}

@@ -54,7 +60,9 @@ __Polyfills__

- Default polyfills:
- prefixed fullscreen implementations: {@link shaka.polyfill.Fullscreen}
- prefixed video QoE metrics: {@link shaka.polyfill.VideoPlaybackQuality}
- prefixed fullscreen implementations:
{@linksource shaka.polyfill.Fullscreen}
- prefixed video QoE metrics:
{@linksource shaka.polyfill.VideoPlaybackQuality}
- prefixed EME implementations for IE 11 and very old versions of embedded
Chrome/Chromium: {@link shaka.polyfill.MediaKeys}
- Promise implementation for IE 11: {@link shaka.polyfill.Promise}
Chrome/Chromium: {@linksource shaka.polyfill.MediaKeys}
- Promise implementation for IE 11: {@linksource shaka.polyfill.Promise}

@@ -61,0 +69,0 @@

@@ -11,4 +11,27 @@ # Welcome to Shaka Player

## Getting Started
#### Prerequisites
You can build Shaka on Linux, Windows, or Mac.
To get the sources and compile the library, you will need:
* Git 1.7.10+ {@link https://git-scm.com/downloads}
* Python 2.7.x {@link https://www.python.org/downloads/}
* Java Runtime Environment 7+ {@link https://java.com/en/download/}
Additionally, to build the documentation and run the tests, you will need:
* npm v1.3.12+ {@link https://nodejs.org/en/download/}
To quickly install these prerequisites on Ubuntu or Debian, run:
```sh
sudo apt-get install git python2.7 openjdk-7-jre-headless npm
sudo npm install -g npm # Upgrade npm to the latest
# Add a symlink missing on some systems:
sudo ln -s /usr/bin/nodejs /usr/local/bin/node
```
Installation instructions for other operating systems are not provided here.
(We can't possibly document them all.) You can follow the links above to
download and install the prerequisites.
#### Get the source

@@ -29,8 +52,8 @@

#### Join the mailing list
#### Join the community
If you want to discuss Shaka Player developement or receive notifications when
a new version is released, you should join our [mailing list].
If you want to discuss Shaka Player development or receive notifications when
a new version is released, you should join our [users group].
[mailing list]: https://groups.google.com/forum/#!forum/shaka-player-users
[users group]: https://groups.google.com/forum/#!forum/shaka-player-users

@@ -37,0 +60,0 @@

@@ -114,3 +114,3 @@ /**

* content. The initDataType values and the formats that they correspond to
* are specified {@link http://goo.gl/hKBdff here}.
* are specified {@link https://goo.gl/TNjYwn here}.
*

@@ -268,3 +268,6 @@ * @property {!Uint8Array} initData

* kind: (string|undefined),
* keyId: ?string
* encrypted: boolean,
* keyId: ?string,
* allowedByApplication: boolean,
* allowedByKeySystem: boolean
* }}

@@ -325,2 +328,5 @@ *

* @see https://goo.gl/k1HWA6
* @property {boolean} encrypted
* <i>Defaults to false.</i><br>
* True if the stream is encrypted.
* @property {?string} keyId

@@ -331,2 +337,10 @@ * <i>Defaults to null (i.e., unencrypted or key ID unknown).</i> <br>

* stream.
* @property {boolean} allowedByApplication
* <i>Defaults to true.</i><br>
* Set by the Player to indicate whether the stream is allowed to be played
* by the application.
* @property {boolean} allowedByKeySystem
* <i>Defaults to true.</i><br>
* Set by the Player to indicate whether the stream is allowed to be played
* by the key system.
*

@@ -333,0 +347,0 @@ * @exportDoc

@@ -40,2 +40,3 @@ /**

* was made by the application through selectTrack.
* @exportDoc
*/

@@ -87,2 +88,3 @@ shakaExtern.StreamChoice;

* A history of the stream changes.
* @exportDoc
*/

@@ -132,2 +134,3 @@ shakaExtern.Stats;

* (only for video tracks) The height of the track in pixels.
* @exportDoc
*/

@@ -139,2 +142,49 @@ shakaExtern.Track;

* @typedef {{
* minWidth: number,
* maxWidth: number,
* minHeight: number,
* maxHeight: number,
* minPixels: number,
* maxPixels: number,
*
* minAudioBandwidth: number,
* maxAudioBandwidth: number,
* minVideoBandwidth: number,
* maxVideoBandwidth: number
* }}
*
* @description
* An object describing application restrictions on what tracks can play. All
* restrictions must be fulfilled for a track to be playable. If a track does
* not meet the restrictions, it will not appear in the track list and it will
* not be played.
*
* @property {number} minWidth
* The minimum width of a video track, in pixels.
* @property {number} maxWidth
* The maximum width of a video track, in pixels.
* @property {number} minHeight
* The minimum height of a video track, in pixels.
* @property {number} maxHeight
* The maximum height of a video track, in pixels.
* @property {number} minPixels
* The minimum number of total pixels in a video track (i.e. width * height).
* @property {number} maxPixels
* The maximum number of total pixels in a video track (i.e. width * height).
*
* @property {number} minAudioBandwidth
* The minimum bandwidth of an audio track, in bit/sec.
* @property {number} maxAudioBandwidth
* The maximum bandwidth of an audio track, in bit/sec.
* @property {number} minVideoBandwidth
* The minimum bandwidth of a video track, in bit/sec.
* @property {number} maxVideoBandwidth
* The maximum bandwidth of a video track, in bit/sec.
* @exportDoc
*/
shakaExtern.Restrictions;
/**
* @typedef {{
* manifest: Object.<string, boolean>,

@@ -240,3 +290,4 @@ * media: Object.<string, boolean>,

* @typedef {{
* customScheme: shakaExtern.DashContentProtectionCallback
* customScheme: shakaExtern.DashContentProtectionCallback,
* clockSyncUri: string
* }}

@@ -248,2 +299,6 @@ *

* ContentProtection node. Return null if not recognized.
* @property {string} clockSyncUri
* A default clock sync URI to be used with live streams which do not
* contain any clock sync information. The "Date" header from this URI
* will be used to determine the current time.
*

@@ -317,2 +372,3 @@ * @exportDoc

* bit/sec.
* @exportDoc
*/

@@ -329,3 +385,4 @@ shakaExtern.AbrConfiguration;

* preferredAudioLanguage: string,
* preferredTextLanguage: string
* preferredTextLanguage: string,
* restrictions: shakaExtern.Restrictions
* }}

@@ -352,4 +409,7 @@ *

* to run again, and may change the active text track.
* @property {shakaExtern.Restrictions} restrictions
* The application restrictions to apply to the tracks. The track must
* meet all the restrictions to be playable.
* @exportDoc
*/
shakaExtern.PlayerConfiguration;

@@ -73,2 +73,10 @@ /**

this.minBytes_ = 65536;
/**
* Minimum amount of time (in ms), under which samples are discarded.
* @private {number}
* @const
*/
this.minTime_ = 200;
};

@@ -94,3 +102,7 @@

durationMs, numBytes) {
if (numBytes < this.minBytes_) {
// Ignore small requests. This will protect against both caching and
// outliers. Small requests in size will tend to vary a lot in the time they
// take, and overhead of JavaScript timings have a big effect. Also, cached
// responses will always return in a small time.
if (numBytes < this.minBytes_ || durationMs < this.minTime_) {
return;

@@ -97,0 +109,0 @@ }

@@ -32,2 +32,3 @@ /**

* @implements {shakaExtern.AbrManager}
* @export
*/

@@ -325,5 +326,8 @@ shaka.abr.SimpleAbrManager = function() {

shaka.abr.SimpleAbrManager.sortStreamsByBandwidth_ = function(streamSet) {
return streamSet.streams.slice(0).sort(
function(s1, s2) { return s1.bandwidth - s2.bandwidth; });
return streamSet.streams.slice(0)
.filter(function(s) {
return s.allowedByApplication && s.allowedByKeySystem;
})
.sort(function(s1, s2) { return s1.bandwidth - s2.bandwidth; });
};

@@ -107,3 +107,3 @@ /**

*/
shaka.dash.DashParser.DEFAULT_SUGGESTED_PRESENTATION_DELAY_ = 2;
shaka.dash.DashParser.DEFAULT_SUGGESTED_PRESENTATION_DELAY_ = 5;

@@ -163,2 +163,3 @@

* @typedef {{
* presentationTimeline: !shaka.media.PresentationTimeline,
* period: ?shaka.dash.DashParser.InheritanceFrame,

@@ -168,4 +169,3 @@ * periodInfo: ?shaka.dash.DashParser.PeriodInfo,

* representation: ?shaka.dash.DashParser.InheritanceFrame,
* bandwidth: (number|undefined),
* maxSegmentDuration: number
* bandwidth: (number|undefined)
* }}

@@ -176,2 +176,4 @@ *

*
* @property {!shaka.media.PresentationTimeline} presentationTimeline
* The PresentationTimeline.
* @property {?shaka.dash.DashParser.InheritanceFrame} period

@@ -187,4 +189,2 @@ * The inheritance from the Period element.

* The bandwidth of the Representation.
* @property {number} maxSegmentDuration
* The largest segment duration among the streams which have been parsed.
*/

@@ -309,3 +309,4 @@ shaka.dash.DashParser.Context;

return this.requestManifest_().then(function() {
this.setUpdateTimer_(0);
if (this.networkingEngine_)
this.setUpdateTimer_(0);
return this.manifest_;

@@ -410,17 +411,7 @@ }.bind(this));

var baseUris = shaka.dash.MpdUtils.resolveUris(manifestBaseUris, uris);
/** @type {shaka.dash.DashParser.Context} */
var context = {
period: null,
periodInfo: null,
adaptationSet: null,
representation: null,
bandwidth: undefined,
maxSegmentDuration: 1
};
var minBufferTime =
XmlUtils.parseAttr(mpd, 'minBufferTime', XmlUtils.parseDuration);
this.updatePeriod_ =
XmlUtils.parseAttr(mpd, 'minimumUpdatePeriod', XmlUtils.parseDuration) ||
0;
this.updatePeriod_ = /** @type {number} */ (XmlUtils.parseAttr(
mpd, 'minimumUpdatePeriod', XmlUtils.parseDuration, -1));

@@ -431,3 +422,3 @@ var presentationStartTime = XmlUtils.parseAttr(

mpd, 'timeShiftBufferDepth', XmlUtils.parseDuration);
var suggestedDelay = XmlUtils.parseAttr(
var suggestedPresentationDelay = XmlUtils.parseAttr(
mpd, 'suggestedPresentationDelay', XmlUtils.parseDuration);

@@ -437,2 +428,31 @@ var maxSegmentDuration = XmlUtils.parseAttr(

var presentationTimeline;
if (this.manifest_) {
presentationTimeline = this.manifest_.presentationTimeline;
} else {
if (presentationStartTime != null) {
// Offset the start time by @suggestedPresentationDelay. This has the
// effect of making the segments become available later than they
// actually are, so it causes a delay. Note that the DASH spec
// recommends that a default @suggestedPresentationDelay be used when
// one is not explicitly specified.
presentationStartTime +=
suggestedPresentationDelay != null ?
suggestedPresentationDelay :
shaka.dash.DashParser.DEFAULT_SUGGESTED_PRESENTATION_DELAY_;
}
presentationTimeline =
new shaka.media.PresentationTimeline(presentationStartTime);
}
/** @type {shaka.dash.DashParser.Context} */
var context = {
presentationTimeline: presentationTimeline,
period: null,
periodInfo: null,
adaptationSet: null,
representation: null,
bandwidth: undefined
};
var periodsAndDuration = this.parsePeriods_(context, baseUris, mpd);

@@ -442,5 +462,20 @@ var duration = periodsAndDuration.duration;

// Cannot return until we calculate the clock offset.
presentationTimeline.setDuration(duration || Number.POSITIVE_INFINITY);
presentationTimeline.setSegmentAvailabilityDuration(
segmentAvailabilityDuration != null ?
segmentAvailabilityDuration :
Number.POSITIVE_INFINITY);
// Use @maxSegmentDuration to override smaller, derived values.
presentationTimeline.notifyMaxSegmentDuration(maxSegmentDuration || 1);
if (this.manifest_) {
// This is a manifest update, so we're done.
return Promise.resolve();
}
// This is the first manifest parse, so we cannot return until we calculate
// the clock offset.
var timingElements = XmlUtils.findChildren(mpd, 'UTCTiming');
return this.parseUtcTiming_(timingElements).then(function(offset) {
var isLive = presentationTimeline.isLive();
return this.parseUtcTiming_(timingElements, isLive).then(function(offset) {
// Detect calls to stop().

@@ -450,45 +485,9 @@ if (!this.networkingEngine_)

// Determine |segmentAvailabilityDuration|.
if (presentationStartTime == null) {
// If this is not live, then ignore segment availability.
segmentAvailabilityDuration = null;
} else if (segmentAvailabilityDuration == null) {
// If there is no availability given and it's live, then the segments
// will always be available.
segmentAvailabilityDuration = Number.POSITIVE_INFINITY;
}
presentationTimeline.setClockOffset(offset);
if (this.manifest_) {
this.manifest_.presentationTimeline.setSegmentAvailabiliyDuration(
segmentAvailabilityDuration);
} else {
if (presentationStartTime != null) {
// Offset the start time by @suggestedPresentationDelay. This has the
// effect of making the segments become available later than they
// actually are, so it causes a delay. Note that the DASH spec
// recommends that a default @suggestedPresentationDelay be used when
// one is not explicitly specified.
presentationStartTime +=
suggestedDelay != null ?
suggestedDelay :
shaka.dash.DashParser.DEFAULT_SUGGESTED_PRESENTATION_DELAY_;
}
shaka.log.v1('maxSegmentDuration:',
'explict=' + maxSegmentDuration,
'derived=' + context.maxSegmentDuration);
var timeline = new shaka.media.PresentationTimeline(
duration || Number.POSITIVE_INFINITY,
presentationStartTime,
segmentAvailabilityDuration,
maxSegmentDuration || context.maxSegmentDuration,
offset);
this.manifest_ = {
periods: periods,
presentationTimeline: timeline,
minBufferTime: minBufferTime || 0
};
}
this.manifest_ = {
presentationTimeline: presentationTimeline,
periods: periods,
minBufferTime: minBufferTime || 0
};
}.bind(this));

@@ -607,2 +606,10 @@ };

// If the period doesn't have an ID, give it one based on its start time.
if (!context.period.id) {
shaka.log.info(
'No Period ID given for Period with start time ' + periodInfo.start +
', Assigning a default');
context.period.id = '__shaka_period_' + periodInfo.start;
}
var adaptationSetNodes =

@@ -737,9 +744,9 @@ XmlUtils.findChildren(periodInfo.node, 'AdaptationSet');

streamInfo = shaka.dash.SegmentBase.createStream(
context, requestInitSegment, this.segmentIndexMap_);
context, requestInitSegment);
} else if (context.representation.segmentList) {
streamInfo = shaka.dash.SegmentList.createStream(
context, this.segmentIndexMap_, this.manifest_);
context, this.segmentIndexMap_);
} else if (context.representation.segmentTemplate) {
streamInfo = shaka.dash.SegmentTemplate.createStream(
context, requestInitSegment, this.segmentIndexMap_, this.manifest_);
context, requestInitSegment, this.segmentIndexMap_, !!this.manifest_);
} else {

@@ -792,3 +799,6 @@ goog.asserts.assert(context.representation.contentType == 'text',

kind: kind,
keyId: keyId
encrypted: contentProtection.drmInfos.length > 0,
keyId: keyId,
allowedByApplication: true,
allowedByKeySystem: true
};

@@ -805,3 +815,3 @@ };

goog.asserts.assert(this.updateTimer_, 'Should only be called by timer');
goog.asserts.assert(this.updatePeriod_ > 0,
goog.asserts.assert(this.updatePeriod_ >= 0,
'There should be an update period');

@@ -845,3 +855,3 @@

shaka.dash.DashParser.prototype.setUpdateTimer_ = function(offset) {
if (this.updatePeriod_ == 0)
if (this.updatePeriod_ < 0)
return;

@@ -1068,10 +1078,28 @@ goog.asserts.assert(this.updateTimer_ == null,

* @param {!Array.<!Element>} elems
* @param {boolean} isLive
* @return {!Promise.<number>}
* @private
*/
shaka.dash.DashParser.prototype.parseUtcTiming_ = function(elems) {
shaka.dash.DashParser.prototype.parseUtcTiming_ = function(elems, isLive) {
var schemesAndValues = elems.map(function(elem) {
return {
scheme: elem.getAttribute('schemeIdUri'),
value: elem.getAttribute('value')
};
});
// If there's nothing specified in the manifest, but we have a default from
// the config, use that.
var clockSyncUri = this.config_.dash.clockSyncUri;
if (isLive && !schemesAndValues.length && clockSyncUri) {
schemesAndValues.push({
scheme: 'urn:mpeg:dash:utc:http-head:2014',
value: clockSyncUri
});
}
var Functional = shaka.util.Functional;
return Functional.createFallbackPromiseChain(elems, function(elem) {
var scheme = elem.getAttribute('schemeIdUri');
var value = elem.getAttribute('value');
return Functional.createFallbackPromiseChain(schemesAndValues, function(sv) {
var scheme = sv.scheme;
var value = sv.value;
switch (scheme) {

@@ -1104,4 +1132,10 @@ // See DASH IOP Guidelines Section 4.7

}
}.bind(this))
.catch(function() { return 0; });
}.bind(this)).catch(function() {
if (isLive) {
shaka.log.warning(
'A UTCTiming element should always be given in live manifests! ' +
'This content may not play on clients with bad clocks!');
}
return 0;
});
};

@@ -1108,0 +1142,0 @@

@@ -103,3 +103,3 @@ /**

if (time !== null) {
goog.asserts.assert(Math.abs(time - Math.round(time)) < 0.01,
goog.asserts.assert(Math.abs(time - Math.round(time)) < 0.2,
'Calculated $Time$ values must be close to integers!');

@@ -305,2 +305,4 @@ time = Math.round(time);

if (firstReference.startTime <= tolerance) {
// Note: if the segment actually starts past 0, the video element should
// automatically jump the gap since the gap is small.
references[0] =

@@ -307,0 +309,0 @@ new shaka.media.SegmentReference(

@@ -77,8 +77,6 @@ /**

* @param {shaka.dash.DashParser.RequestInitSegmentCallback} requestInitSegment
* @param {!Object.<string, !shaka.media.SegmentIndex>} segmentIndexMap
* @throws shaka.util.Error When there is a parsing error.
* @return {shaka.dash.DashParser.StreamInfo}
*/
shaka.dash.SegmentBase.createStream = function(
context, requestInitSegment, segmentIndexMap) {
shaka.dash.SegmentBase.createStream = function(context, requestInitSegment) {
goog.asserts.assert(context.representation.segmentBase,

@@ -125,2 +123,4 @@ 'Should only be called with SegmentBase');

startByte, endByte, containerType, presentationTimeOffset) {
var presentationTimeline = context.presentationTimeline;
var periodStartTime = context.periodInfo.start;
var periodDuration = context.periodInfo.duration;

@@ -157,2 +157,3 @@

shaka.dash.MpdUtils.fitSegmentReferences(periodDuration, references);
presentationTimeline.notifySegments(periodStartTime, references);

@@ -159,0 +160,0 @@ // Since containers are never updated, we don't need to store the

@@ -41,7 +41,5 @@ /**

* @param {!Object.<string, !shaka.media.SegmentIndex>} segmentIndexMap
* @param {?shakaExtern.Manifest} manifest
* @return {shaka.dash.DashParser.StreamInfo}
*/
shaka.dash.SegmentList.createStream = function(
context, segmentIndexMap, manifest) {
shaka.dash.SegmentList.createStream = function(context, segmentIndexMap) {
goog.asserts.assert(context.representation.segmentList,

@@ -72,10 +70,8 @@ 'Should only be called with SegmentList');

if (segmentIndex) {
goog.asserts.assert(manifest, 'This should be an update');
segmentIndex.merge(references);
segmentIndex.evict(
manifest.presentationTimeline.getSegmentAvailabilityStart());
context.presentationTimeline.getSegmentAvailabilityStart());
} else {
context.maxSegmentDuration = references.reduce(
function(max, r) { return Math.max(max, r.endTime - r.startTime); },
context.maxSegmentDuration);
context.presentationTimeline.notifySegments(
context.periodInfo.start, references);
segmentIndex = new shaka.media.SegmentIndex(references);

@@ -82,0 +78,0 @@ if (id)

@@ -40,3 +40,3 @@ /**

* @param {!Object.<string, !shaka.media.SegmentIndex>} segmentIndexMap
* @param {?shakaExtern.Manifest} manifest
* @param {boolean} isUpdate True if the manifest is being updated.
* @throws shaka.util.Error When there is a parsing error.

@@ -46,3 +46,3 @@ * @return {shaka.dash.DashParser.StreamInfo}

shaka.dash.SegmentTemplate.createStream = function(
context, requestInitSegment, segmentIndexMap, manifest) {
context, requestInitSegment, segmentIndexMap, isUpdate) {
goog.asserts.assert(context.representation.segmentTemplate,

@@ -63,2 +63,6 @@ 'Should only be called with SegmentTemplate');

} else if (info.segmentDuration) {
if (!isUpdate) {
context.presentationTimeline.notifyMaxSegmentDuration(
info.segmentDuration);
}
segmentIndexFunctions = SegmentTemplate.createFromDuration_(context, info);

@@ -79,10 +83,8 @@ } else {

if (segmentIndex) {
goog.asserts.assert(manifest, 'This should be an update');
segmentIndex.merge(references);
segmentIndex.evict(
manifest.presentationTimeline.getSegmentAvailabilityStart());
context.presentationTimeline.getSegmentAvailabilityStart());
} else {
context.maxSegmentDuration = references.reduce(
function(max, r) { return Math.max(max, r.endTime - r.startTime); },
context.maxSegmentDuration);
context.presentationTimeline.notifySegments(
context.periodInfo.start, references);
segmentIndex = new shaka.media.SegmentIndex(references);

@@ -89,0 +91,0 @@ if (id)

@@ -23,3 +23,6 @@ /**

* @namespace shaka.log
* @summary A console logging framework which is compiled out for deployment.
* @summary
* A console logging framework which is compiled out for deployment. This is
* only available when using the uncompiled version.
* @exportDoc
*/

@@ -31,2 +34,3 @@

* @enum {number}
* @exportDoc
*/

@@ -85,2 +89,3 @@ shaka.log.Level = {

* @param {number} level
* @exportDoc
*/

@@ -87,0 +92,0 @@ shaka.log.setLevel = function(level) {

@@ -46,2 +46,5 @@ /**

/** @private {Array.<string>} */
this.supportedTypes_ = null;
/** @private {!Array.<shakaExtern.DrmInfo>} */

@@ -86,4 +89,12 @@ this.drmInfos_ = [];

/**
* @typedef {{initData: !Uint8Array, session: !MediaKeySession}}
* @typedef {{
* loaded: boolean,
* initData: !Uint8Array,
* session: !MediaKeySession
* }}
*
* @description A record to track sessions and suppress duplicate init data.
* @property {boolean} loaded
* True once the key status has been updated (to a non-pending state). This
* does not mean the session is 'usable'.
* @property {!Uint8Array} initData

@@ -119,2 +130,3 @@ * The init data used to create the session.

this.drmInfos_ = [];
this.supportedTypes_ = null;
this.mediaKeys_ = null;

@@ -240,2 +252,6 @@ this.video_ = null;

// Listen to 'waitingforkey' to detect key ID not found.
this.eventManager_.listen(
this.video_, 'waitingforkey', this.onWaitingForKey_.bind(this));
return Promise.all([setMediaKeys, setServerCertificate]).then(function() {

@@ -278,2 +294,13 @@ if (this.destroyed_) return Promise.reject();

/**
* Returns an array of the media types supported by the current key system.
* These will be full mime types (e.g. 'video/webm; codecs="vp8"').
*
* @return {Array.<string>}
*/
shaka.media.DrmEngine.prototype.getSupportedTypes = function() {
return this.supportedTypes_;
};
/**
* @param {!shakaExtern.Manifest} manifest

@@ -317,3 +344,2 @@ * @param {boolean} offline True if we are storing or loading offline content.

label: drmInfo.keySystem,
mimeTypes: {}, // tracked by us, ignored by EME
drmInfos: [] // tracked by us, ignored by EME

@@ -341,12 +367,18 @@ };

streamSet.streams.forEach(function(stream) {
var mimeType = stream.mimeType;
var fullMimeType = stream.mimeType;
if (stream.codecs) {
fullMimeType += '; codecs="' + stream.codecs + '"';
}
// Edge 13 fails this negotiation if the same MIME type appears twice.
if (!config.mimeTypes[mimeType]) {
capabilities.push({
robustness: robustness,
contentType: mimeType
});
config.mimeTypes[mimeType] = true;
} // !(mimeType in mimeTypeSet)
// Edge 13 fails this negotiation with NotSupportedError if more than
// one entry is given, even if each entry individually would be
// supported. Bug filed: https://goo.gl/vr2Vle
if (drmInfo.keySystem == 'com.microsoft.playready' &&
capabilities.length) {
return;
}
capabilities.push({
robustness: robustness,
contentType: fullMimeType
});
}.bind(this)); // streamSet.streams.forEach

@@ -414,2 +446,15 @@ }.bind(this)); // streamSet.drmInfos.forEach

// Store the capabilities of the key system.
var realConfig = mediaKeySystemAccess.getConfiguration();
var audioCaps = realConfig.audioCapabilities || [];
var videoCaps = realConfig.videoCapabilities || [];
var caps = audioCaps.concat(videoCaps);
this.supportedTypes_ = caps.map(function(c) { return c.contentType; });
if (this.supportedTypes_.length == 0) {
// Edge 13 does not report capabilities. To work around this, set the
// supported types to null, which Player will use as a signal that the
// information is not available.
this.supportedTypes_ = null;
}
var originalConfig = configsByKeySystem[mediaKeySystemAccess.keySystem];

@@ -435,2 +480,3 @@ this.drmInfos_ = originalConfig.drmInfos;

this.drmInfos_ = [];
this.supportedTypes_ = null;
return Promise.reject(new shaka.util.Error(

@@ -603,2 +649,20 @@ shaka.util.Error.Category.DRM,

/**
* @param {Event} event
* @private
*/
shaka.media.DrmEngine.prototype.onWaitingForKey_ = function(event) {
if (this.activeSessions_.some(function(s) { return !s.loaded; })) {
// There are still sessions being loaded, one of them might be the required
// key. Once the request is complete, we will get another waitingforkey
// event if we still don't have the keys.
return;
}
// We don't have some of the required keys, so dispatch an error.
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.DRM, shaka.util.Error.Code.WRONG_KEYS));
};
/**
* @param {!MediaEncryptedEvent} event

@@ -652,3 +716,4 @@ * @private

var p = session.generateRequest(initDataType, initData.buffer);
this.activeSessions_.push({initData: initData, session: session});
this.activeSessions_.push(
{initData: initData, session: session, loaded: false});

@@ -785,2 +850,10 @@ p.catch(function(error) {

if (keyStatusMap.forEach === undefined) {
// Older versions of Firefox (<= 46) use the old MediaKeyStatusMap API, so
// just forego checking key statuses on these versions: newer versions of
// Firefox use the new MediaKeyStatusMap API.
shaka.log.debug('keyStatuses.forEach missing!');
keyStatusMap = [];
}
/** @type {!Object.<string, string>} */

@@ -830,2 +903,12 @@ var keyStatusByKeyId = {};

if (status != 'status-pending' && status != 'internal-error') {
// The session has been loaded, update the active sessions.
var activeSession = this.activeSessions_.filter(function(s) {
return s.session == session;
})[0];
goog.asserts.assert(activeSession != null,
'Unexpected session in key status map');
activeSession.loaded = true;
}
var keyIdHex = shaka.util.Uint8ArrayUtils.toHex(new Uint8Array(keyId));

@@ -832,0 +915,0 @@ keyStatusByKeyId[keyIdHex] = status;

@@ -549,2 +549,3 @@ /**

goog.asserts.assert(operation, 'Spurious updateend event!');
if (!operation) return;
goog.asserts.assert(!this.sourceBuffers_[contentType].updating,

@@ -551,0 +552,0 @@ 'SourceBuffer should not be updating on updateend!');

@@ -63,7 +63,8 @@ /**

/**
* The playhead's initial position in seconds.
* @private {number}
* The playhead's initial position in seconds, or null if it should
* automatically be calculated later.
* @private {?number}
* @const
*/
this.startTime_;
this.startTime_ = startTime;

@@ -83,18 +84,6 @@ /** @private {?function(boolean)} */

/** @private {number} */
this.lastPlaybackRate_ = 0;
this.playbackRate_ = 1;
// Set the start time.
if (startTime == null) {
if (timeline.getDuration() < Number.POSITIVE_INFINITY) {
startTime = timeline.getSegmentAvailabilityStart();
} else {
// For live presentations, ensure that the startup buffering goal can be
// met.
startTime =
Math.max(timeline.getSegmentAvailabilityEnd() - rebufferingGoal,
timeline.getSegmentAvailabilityStart());
}
}
this.startTime_ = startTime;
shaka.log.debug('Starting the presentation at ' + startTime + ' seconds...');
/** @private {?number} */
this.trickPlayIntervalId_ = null;

@@ -108,2 +97,4 @@ // Check if the video has already loaded some metadata.

}
this.eventManager_.listen(video, 'ratechange', this.onRateChange_.bind(this));
};

@@ -117,2 +108,7 @@

if (this.trickPlayIntervalId_ != null) {
window.clearInterval(this.trickPlayIntervalId_);
this.trickPlayIntervalId_ = null;
}
this.video_ = null;

@@ -139,10 +135,11 @@ this.timeline_ = null;

shaka.media.Playhead.prototype.getTime = function() {
var time = this.video_.readyState > 0 ?
this.video_.currentTime :
this.startTime_;
// Although we restrict the video's currentTime elsewhere, clamp it here to
// ensure any timing issues (e.g., the user agent seeks and calls this
// function before we receive the 'seeking' event) don't cause us to return a
// time outside the segment availability window.
return this.clampTime_(time);
if (this.video_.readyState > 0) {
// Although we restrict the video's currentTime elsewhere, clamp it here to
// ensure any timing issues (e.g., the user agent seeks and calls this
// function before we receive the 'seeking' event) don't cause us to return
// a time outside the segment availability window.
return this.clampTime_(this.video_.currentTime);
}
return this.getStartTime_();
};

@@ -152,2 +149,29 @@

/**
* Gets the playhead's initial position in seconds.
*
* @return {number}
* @private
*/
shaka.media.Playhead.prototype.getStartTime_ = function() {
if (this.startTime_) {
return this.clampTime_(this.startTime_);
}
var startTime;
if (this.timeline_.getDuration() < Number.POSITIVE_INFINITY) {
// If the presentation is VOD, or if the presentation is live but has
// finished broadcasting, then start from the beginning.
startTime = this.timeline_.getEarliestStart();
} else {
// Otherwise, start near the live-edge, but ensure that the startup
// buffering goal can be met
startTime = Math.max(
this.timeline_.getSegmentAvailabilityEnd() - this.rebufferingGoal_,
this.timeline_.getEarliestStart());
}
return startTime;
};
/**
* Stops the playhead for buffering, or resumes the playhead after buffering.

@@ -159,17 +183,6 @@ *

shaka.media.Playhead.prototype.setBuffering = function(buffering) {
if (buffering && !this.buffering_) {
this.lastPlaybackRate_ = this.video_.playbackRate;
this.video_.playbackRate = 0;
this.buffering_ = true;
this.onBuffering_(true);
} else if (!buffering && this.buffering_) {
if (this.video_.playbackRate == 0) {
// The app hasn't set a new playback rate, so restore the old one.
this.video_.playbackRate = this.lastPlaybackRate_;
} else {
// There's nothing we could have done to stop the app from setting a new
// rate, so we don't need to do anything here.
}
this.buffering_ = false;
this.onBuffering_(false);
if (buffering != this.buffering_) {
this.buffering_ = buffering;
this.setPlaybackRate(this.playbackRate_);
this.onBuffering_(buffering);
}

@@ -180,2 +193,54 @@ };

/**
* Gets the current effective playback rate. This may be negative even if the
* browser does not directly support rewinding.
* @return {number}
*/
shaka.media.Playhead.prototype.getPlaybackRate = function() {
return this.playbackRate_;
};
/**
* Sets the playback rate.
* @param {number} rate
*/
shaka.media.Playhead.prototype.setPlaybackRate = function(rate) {
if (this.trickPlayIntervalId_ != null) {
window.clearInterval(this.trickPlayIntervalId_);
this.trickPlayIntervalId_ = null;
}
this.playbackRate_ = rate;
// All major browsers support playback rates above zero. Only need fake
// trick play for negative rates.
this.video_.playbackRate = (this.buffering_ || rate < 0) ? 0 : rate;
if (!this.buffering_ && rate < 0) {
// Defer creating the timer until we stop buffering. This function will be
// called again from setBuffering().
this.trickPlayIntervalId_ = window.setInterval(function() {
this.video_.currentTime += rate / 4;
}.bind(this), 250);
}
};
/**
* Handles a 'ratechange' event.
*
* @private
*/
shaka.media.Playhead.prototype.onRateChange_ = function() {
// NOTE: This will not allow explicitly setting the playback rate to 0 while
// the playback rate is negative. Pause will still work.
var expectedRate =
this.buffering_ || this.playbackRate_ < 0 ? 0 : this.playbackRate_;
if (this.video_.playbackRate != expectedRate) {
shaka.log.debug('Video playback rate changed to', this.video_.playbackRate);
this.setPlaybackRate(this.video_.playbackRate);
}
};
/**
* Handles a 'loadedmetadata' event.

@@ -187,8 +252,15 @@ *

this.eventManager_.unlisten(this.video_, 'loadedmetadata');
this.eventManager_.listen(this.video_, 'seeking', this.onSeeking_.bind(this));
// Move the real playhead to the start time.
var targetTime = this.clampTime_(this.startTime_);
if (this.video_.currentTime != targetTime)
var targetTime = this.getStartTime_();
if (this.video_.currentTime.toFixed(3) == targetTime.toFixed(3)) {
this.eventManager_.listen(
this.video_, 'seeking', this.onSeeking_.bind(this));
this.eventManager_.listen(
this.video_, 'playing', this.onPlaying_.bind(this));
} else {
this.eventManager_.listen(
this.video_, 'seeking', this.onSeekingToStartTime_.bind(this));
this.video_.currentTime = targetTime;
}
};

@@ -198,2 +270,17 @@

/**
* Handles the 'seeking' event from the initial jump to the start time (if
* there is one).
*
* @private
*/
shaka.media.Playhead.prototype.onSeekingToStartTime_ = function() {
goog.asserts.assert(this.video_.readyState > 0,
'readyState should be greater than 0');
this.eventManager_.unlisten(this.video_, 'seeking');
this.eventManager_.listen(this.video_, 'seeking', this.onSeeking_.bind(this));
this.eventManager_.listen(this.video_, 'playing', this.onPlaying_.bind(this));
};
/**
* Handles a 'seeking' event.

@@ -211,33 +298,3 @@ *

if (targetTime != currentTime) {
shaka.log.debug('Repositioning playhead...',
'currentTime=' + currentTime,
'targetTime=' + targetTime);
// Triggers another call to onSeeking_().
this.video_.currentTime = targetTime;
// Sometimes, IE and Edge ignore this re-seek. Check every 100ms and try
// again if need be, up to 10 tries.
// Delay stats over 100 runs of a reseeking integration test:
// IE - 0ms - 47%
// IE - 100ms - 63%
// Edge - 0ms - 2%
// Edge - 100ms - 40%
// Edge - 200ms - 32%
// Edge - 300ms - 24%
// Edge - 400ms - 2%
// Chrome - 0ms - 100%
// TODO: File a bug on IE/Edge about this.
var tries = 0;
var recheck = (function() {
if (!this.video_) return;
if (tries++ >= 10) return;
if (this.video_.currentTime == currentTime) {
// Sigh. Try again.
this.video_.currentTime = targetTime;
setTimeout(recheck, 100);
}
}).bind(this);
setTimeout(recheck, 100);
this.movePlayhead_(currentTime, targetTime);
return;

@@ -252,4 +309,21 @@ }

/**
* Computes a time to reposition the playhead to after a seek.
* Handles a 'playing' event.
*
* @private
*/
shaka.media.Playhead.prototype.onPlaying_ = function() {
goog.asserts.assert(this.video_.readyState > 0,
'readyState should be greater than 0');
var currentTime = this.video_.currentTime;
var targetTime = this.reposition_(currentTime);
if (targetTime != currentTime)
this.movePlayhead_(currentTime, targetTime);
};
/**
* Computes a new playhead position that's within the presentation timeline.
*
* @param {number} currentTime

@@ -260,15 +334,16 @@ * @return {number} The time to reposition the playhead to.

shaka.media.Playhead.prototype.reposition_ = function(currentTime) {
var availabilityDuration = this.timeline_.getSegmentAvailabilityDuration();
var live = (availabilityDuration != null) &&
(availabilityDuration < Number.POSITIVE_INFINITY);
var timeline = this.timeline_;
var start = timeline.getEarliestStart();
var end = timeline.getSegmentAvailabilityEnd();
var start = this.timeline_.getSegmentAvailabilityStart();
var end = this.timeline_.getSegmentAvailabilityEnd();
if (!live) {
if (!timeline.isLive() ||
timeline.getSegmentAvailabilityDuration() == Number.POSITIVE_INFINITY) {
// If the presentation is live but has an infinite segment availability
// duration then we can treat it as VOD since the start of the window is
// not moving.
if (currentTime < start) {
shaka.log.v1('Seek before start.');
shaka.log.v1('Playhead before start.');
return start;
} else if (currentTime > end) {
shaka.log.v1('Seek past end.');
shaka.log.v1('Playhead past end.');
return end;

@@ -285,3 +360,3 @@ }

if (currentTime >= safe && currentTime <= end) {
shaka.log.v1('Seek in safe region.');
shaka.log.v1('Playhead in safe region.');
return currentTime;

@@ -293,6 +368,6 @@ }

if ((bufferedAhead != 0) && (currentTime >= left && currentTime <= end)) {
shaka.log.v1('Seek outside safe region & in buffered region.');
shaka.log.v1('Playhead outside safe region & in buffered region.');
return currentTime;
} else if (currentTime > end) {
shaka.log.v1('Seek past end.');
shaka.log.v1('Playhead past end.');
return end;

@@ -303,3 +378,3 @@ } else if ((end < safe) && (currentTime >= left && currentTime <= end)) {

// don't have to do anything.
shaka.log.v1('Seek outside safe region & in unbuffered region,',
shaka.log.v1('Playhead outside safe region & in unbuffered region,',
'but cannot reposition the playhead.');

@@ -310,4 +385,4 @@ return currentTime;

// It's not safe to buffer from |currentTime|, so reposition the playhead.
shaka.log.v1('Seek outside safe region & in unbuffered region,',
'or seek before start');
shaka.log.v1('Playhead outside safe region & in unbuffered region,',
'or playhead before start');
return Math.min(safe + 2, end);

@@ -318,2 +393,43 @@ };

/**
* Moves the playhead to the target time, triggering a call to onSeeking_().
*
* @param {number} currentTime
* @param {number} targetTime
* @private
*/
shaka.media.Playhead.prototype.movePlayhead_ = function(
currentTime, targetTime) {
shaka.log.debug('Moving playhead...',
'currentTime=' + currentTime,
'targetTime=' + targetTime);
this.video_.currentTime = targetTime;
// Sometimes, IE and Edge ignore re-seeks. Check every 100ms and try
// again if need be, up to 10 tries.
// Delay stats over 100 runs of a re-seeking integration test:
// IE - 0ms - 47%
// IE - 100ms - 63%
// Edge - 0ms - 2%
// Edge - 100ms - 40%
// Edge - 200ms - 32%
// Edge - 300ms - 24%
// Edge - 400ms - 2%
// Chrome - 0ms - 100%
// TODO: File a bug on IE/Edge about this.
var tries = 0;
var recheck = (function() {
if (!this.video_) return;
if (tries++ >= 10) return;
if (this.video_.currentTime == currentTime) {
// Sigh. Try again.
this.video_.currentTime = targetTime;
setTimeout(recheck, 100);
}
}).bind(this);
setTimeout(recheck, 100);
};
/**
* Clamps the given time to the segment availability window.

@@ -326,3 +442,3 @@ *

shaka.media.Playhead.prototype.clampTime_ = function(time) {
var start = this.timeline_.getSegmentAvailabilityStart();
var start = this.timeline_.getEarliestStart();
if (time < start) return start;

@@ -329,0 +445,0 @@

@@ -25,18 +25,4 @@ /**

*
* @param {number} duration The presentation's duration in seconds.
* POSITIVE_INFINITY indicates that the presentation continues indefinitely.
* @param {?number} presentationStartTime The wall-clock time, in seconds,
* when the presentation started or will start. Only required for live.
* @param {?number} segmentAvailabilityDuration
* The amount of time, in seconds, that the start of a segment remains
* available after the live-edge moves past the end of the segment.
* POSITIVE_INFINITY indicates that segments remain available indefinitely.
* For example, if your live presentation has a 5 minute DVR window and your
* segments are typically 10 seconds long then you should set this value to 4
* minutes and 50 seconds. Only required for live.
* @param {number} maxSegmentDuration The maximum duration, in seconds, of any
* one segment in the presentation.
* @param {number} clockOffset
* The difference between the client clock and the media server time, in
* milliseconds (i.e. ServerTime = Date.now() + clockOffset).
*

@@ -49,29 +35,20 @@ * @see {shakaExtern.Manifest}

*/
shaka.media.PresentationTimeline = function(
duration,
presentationStartTime,
segmentAvailabilityDuration,
maxSegmentDuration,
clockOffset) {
goog.asserts.assert(duration > 0, 'Timeline duration must be > 0');
goog.asserts.assert(
(presentationStartTime === null &&
segmentAvailabilityDuration === null) ||
(presentationStartTime >= 0 && segmentAvailabilityDuration > 0),
'Timeline start time and duration must be > 0 or null');
shaka.media.PresentationTimeline = function(presentationStartTime) {
/** @private {?number} */
this.presentationStartTime_ = presentationStartTime;
/** @private {number} */
this.duration_ = duration;
this.duration_ = Number.POSITIVE_INFINITY;
/** @private {?number} */
this.presentationStartTime_ = presentationStartTime;
/** @private {number} */
this.segmentAvailabilityDuration_ = Number.POSITIVE_INFINITY;
/** @private {?number} */
this.segmentAvailabilityDuration_ = segmentAvailabilityDuration;
this.maxSegmentDuration_ = 1;
/** @private {number} */
this.maxSegmentDuration_ = maxSegmentDuration;
this.maxFirstSegmentStartTime_ = 0;
/** @private {number} */
this.clockOffset_ = clockOffset;
this.clockOffset_ = 0;
};

@@ -90,3 +67,3 @@

/**
* Sets the presentation's duration. The duration may be updated at any time.
* Sets the presentation's duration.
*

@@ -97,3 +74,3 @@ * @param {number} duration The presentation's duration in seconds.

shaka.media.PresentationTimeline.prototype.setDuration = function(duration) {
goog.asserts.assert(duration > 0, 'Timeline duration must be > 0');
goog.asserts.assert(duration > 0, 'duration must be > 0');
this.duration_ = duration;

@@ -104,5 +81,23 @@ };

/**
* @return {?number} The presentation's segment availability duration.
* Always returns null for video-on-demand, and never returns null for live.
* Sets the clock offset, which is the the difference between the client's clock
* and the server's clock, in milliseconds (i.e., serverTime = Date.now() +
* clockOffset).
*
* @param {number} offset The clock offset, in ms.
*/
shaka.media.PresentationTimeline.prototype.setClockOffset = function(offset) {
this.clockOffset_ = offset;
};
/**
* Gets the presentation's segment availability duration, which is the amount
* of time, in seconds, that the start of a segment remains available after the
* live-edge moves past the end of that segment. POSITIVE_INFINITY indicates
* that segments remain available indefinitely. For example, if your live
* presentation has a 5 minute DVR window and your segments are 10 seconds long
* then the segment availability duration should be 4 minutes and 50 seconds.
*
* @return {number} The presentation's segment availability duration.
*/
shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityDuration =

@@ -115,16 +110,12 @@ function() {

/**
* Updates the presentation's segment availability duration. The segment
* availability duration can only be updated for live.
* Sets the presentation's segment availability duration. The segment
* availability duration should only be set for live.
*
* @param {?number} segmentAvailabilityDuration The presentation's new segment
* availability duration in seconds.
* @param {number} segmentAvailabilityDuration The presentation's new segment
* availability duration in seconds.
*/
shaka.media.PresentationTimeline.prototype.setSegmentAvailabiliyDuration =
shaka.media.PresentationTimeline.prototype.setSegmentAvailabilityDuration =
function(segmentAvailabilityDuration) {
goog.asserts.assert(
(this.segmentAvailabilityDuration_ == null &&
segmentAvailabilityDuration == null) ||
(this.segmentAvailabilityDuration_ != null &&
segmentAvailabilityDuration != null),
'Segment availability duration can only be updated for live');
goog.asserts.assert(segmentAvailabilityDuration >= 0,
'segmentAvailabilityDuration must be >= 0');
this.segmentAvailabilityDuration_ = segmentAvailabilityDuration;

@@ -135,2 +126,77 @@ };

/**
* Gives PresentationTimeline a Stream's segments so it can size and position
* the segment availability window, and account for missing segment
* information. This function should be called once for each Stream (no more,
* no less).
*
* @param {number} periodStartTime
* @param {!Array.<!shaka.media.SegmentReference>} references
*/
shaka.media.PresentationTimeline.prototype.notifySegments = function(
periodStartTime, references) {
if (references.length == 0)
return;
this.maxSegmentDuration_ = references.reduce(
function(max, r) { return Math.max(max, r.endTime - r.startTime); },
this.maxSegmentDuration_);
if (periodStartTime == 0) {
this.maxFirstSegmentStartTime_ =
Math.max(this.maxFirstSegmentStartTime_, references[0].startTime);
}
shaka.log.v1('notifySegments:',
'maxSegmentDuration=' + this.maxSegmentDuration_,
'maxFirstSegmentStartTime=' + this.maxFirstSegmentStartTime_);
};
/**
* Gives PresentationTimeline a Stream's maximum segment duration so it can
* size and position the segment availability window. This function should be
* called once for each Stream (no more, no less), but does not have to be
* called if notifySegments() is called instead for a particular stream.
*
* @param {number} maxSegmentDuration The maximum segment duration for a
* particular stream.
*/
shaka.media.PresentationTimeline.prototype.notifyMaxSegmentDuration = function(
maxSegmentDuration) {
this.maxSegmentDuration_ = Math.max(
this.maxSegmentDuration_, maxSegmentDuration);
shaka.log.v1('notifyNewSegmentDuration:',
'maxSegmentDuration=' + this.maxSegmentDuration_);
};
/**
* @return {boolean} True if the presentation is live; otherwise, return
* false.
*/
shaka.media.PresentationTimeline.prototype.isLive = function() {
return this.duration_ == Number.POSITIVE_INFINITY ||
this.segmentAvailabilityDuration_ < Number.POSITIVE_INFINITY;
};
/**
* Gets the presentation's current earliest, available timestamp. This value
* may be greater than the presentation's current segment availability start
* time if segment information is missing or does not exist at the beginning of
* the segment availability window.
*
* @return {number} The presentation's current earliest, available timestamp,
* in seconds, relative to the start of the presentation.
*/
shaka.media.PresentationTimeline.prototype.getEarliestStart = function() {
var maxFirstSegmentStartTime = Math.min(
this.maxFirstSegmentStartTime_, this.getSegmentAvailabilityEnd());
return Math.max(maxFirstSegmentStartTime,
this.getSegmentAvailabilityStart());
};
/**
* Gets the presentation's current segment availability start time. Segments

@@ -140,11 +206,8 @@ * ending at or before this time should be assumed to be unavailable.

* @return {number} The current segment availability start time, in seconds,
* relative to the start of the presentation. Always returns 0 for
* video-on-demand.
* relative to the start of the presentation.
*/
shaka.media.PresentationTimeline.prototype.getSegmentAvailabilityStart =
function() {
if (this.presentationStartTime_ == null ||
this.segmentAvailabilityDuration_ == Number.POSITIVE_INFINITY) {
if (this.segmentAvailabilityDuration_ == Number.POSITIVE_INFINITY)
return 0;
}

@@ -166,3 +229,3 @@ return Math.max(

function() {
if (this.presentationStartTime_ == null)
if (this.presentationStartTime_ == null || !this.isLive())
return this.duration_;

@@ -169,0 +232,0 @@

@@ -132,7 +132,6 @@ /**

} else {
// when period is changed, fitSegmentReference will
// expand last segment to the start of the next period
// so, it is valid to have end time updated to the
// last segment reference in a period
if (r1.endTime != r2.endTime) {
// When period is changed, fitSegmentReference will expand the last
// segment to the start of the next period. So, it is valid to have end
// time updated to the last segment reference in a period
if (Math.abs(r1.endTime - r2.endTime) > 0.1) {
goog.asserts.assert(r2.endTime > r1.endTime &&

@@ -139,0 +138,0 @@ i == this.references_.length - 1 &&

@@ -175,5 +175,4 @@ /**

* stream: shakaExtern.Stream,
* lastSegmentPeriodIndex: ?number,
* lastStream: ?shakaExtern.Stream,
* lastSegmentReference: shaka.media.SegmentReference,
* drift: ?number,
* needInitSegment: boolean,

@@ -198,12 +197,6 @@ * needRebuffering: boolean,

* The current Stream.
* @property {?number} lastSegmentPeriodIndex
* The index of the Period of the last segment that was appended.
* @property {?shakaExtern.Stream} lastStream
* The Stream of the last segment that was appended.
* @property {shaka.media.SegmentReference} lastSegmentReference
* The SegmentReference of the last segment that was appended.
* @property {?number} drift
* The number of seconds that the segments' timestamps are offset from the
* SegmentReferences' timestamps. For example, a positive value indicates
* that the segments are ahead of the SegmentReferences. Note that the
* segments' timestamps are the true values; however, the drift should
* never be very large for valid content.
* @property {boolean} needInitSegment

@@ -485,7 +478,3 @@ * True indicates that |stream|'s init segment must be inserted before the

if (mediaState.waitingToClearBuffer) {
// The only reason we should be waiting to clear the buffer is if we're
// performing an update.
shaka.log.debug(logPrefix, 'seeked: unbuffered seek: already waiting');
goog.asserts.assert(mediaState.performingUpdate,
'expected performingUpdate to be true');
continue;

@@ -559,5 +548,4 @@ }

type: type,
lastSegmentPeriodIndex: null,
lastStream: null,
lastSegmentReference: null,
drift: null,
needInitSegment: true,

@@ -572,4 +560,4 @@ needRebuffering: false,

};
this.scheduleUpdate_(this.mediaStates_[type], 0);
}
this.scheduleUpdate_(this.mediaStates_[type], 0);
}

@@ -802,10 +790,6 @@ }.bind(this));

mediaState, playheadTime, bufferedAhead, bufferEnd);
if (timeNeeded == null)
return null;
shaka.log.v2(logPrefix, 'timeNeeded=' + timeNeeded);
var timeline = this.manifest_.presentationTimeline;
// Check if we've buffered to the end of the presentation.
if (timeNeeded >= timeline.getDuration()) {
if (timeNeeded >= this.manifest_.presentationTimeline.getDuration()) {
// We shouldn't rebuffer if the playhead is close to the end of the

@@ -847,23 +831,13 @@ // presentation.

// Check segment availability.
var availabilityStart = timeline.getSegmentAvailabilityStart();
var availabilityEnd = timeline.getSegmentAvailabilityEnd();
if ((timeNeeded < availabilityStart) || (timeNeeded > availabilityEnd)) {
// The next segment is not available. In the usual case, this occurs when
// we've buffered to the live-edge of a live presentation; in the
// degenerate case, this occurs if the playhead is forced outside the
// segment availability window; either way try another update in a second.
shaka.log.v1(logPrefix,
'next segment is outside segment availability window:',
'playheadTime=' + playheadTime,
'timeNeeded=' + timeNeeded,
'availabilityStart=' + availabilityStart,
'availabilityEnd=' + availabilityEnd);
var reference = this.getSegmentReferenceNeeded_(
mediaState, playheadTime, currentPeriodIndex);
if (!reference) {
// The segment could not be found, does not exist, or is not available. In
// any case just try again... if the manifest is incomplete or is not being
// updated then we'll idle forever; otherwise, we'll end up getting a
// SegmentReference eventually.
return 1;
}
var reference = this.getSegmentReference_(
mediaState, playheadTime, currentPeriodIndex);
this.fetchAndAppend_(mediaState, playheadTime, currentPeriodIndex, reference);
return null;

@@ -882,4 +856,3 @@ };

* @param {?number} bufferEnd
* @return {?number} The next timestamp needed or null if the playhead is is in
* an unbuffered region behind the buffer.
* @return {number} The next timestamp needed.
* @throws {!shaka.util.Error} if the buffer is inconsistent with our

@@ -897,4 +870,5 @@ * expectations.

// the next timestamp we need is actually larger than |bufferEnd|; and
// 2. there may be drift, but we need drift free times when comparing times
// against presentation and Period boundaries.
// 2. there may be drift (the timestamps in the segments are ahead/behind
// of the timestamps in the manifest), but we need drift free times when
// comparing times against presentation and Period boundaries.

@@ -905,3 +879,3 @@ if (bufferedAhead == 0) {

// The buffer is empty.
if (mediaState.lastSegmentPeriodIndex != null ||
if (mediaState.lastStream != null ||
mediaState.lastSegmentReference) {

@@ -916,4 +890,9 @@ shaka.log.error(logPrefix, 'lastSegmentReference should be null');

} else if (bufferEnd > playheadTime) {
// The user agent seeked backwards but seeked() was not called or has not
// been called yet (because it's a race). Assume seeked() will be called.
// We may find ourseleves in this state for two reasons:
// 1. there is a significant amount of positive drift; or
// 2. the user agent seeked backwards but seeked() was not called or has
// not been called yet (because it's a race).
// For case 1 we'll idle forever, and for case 2 we'll end up buffering a
// segment, removing it, and then buffering it again (note that this case
// should be rare).
shaka.log.debug(logPrefix,

@@ -923,14 +902,14 @@ 'playhead in unbuffered region (behind buffer):',

'bufferEnd=' + bufferEnd);
return null;
} else {
// We may find ourseleves in this state for three reasons:
// We may find ourseleves in this state for four reasons:
// 1. the playhead is exactly at the end of the buffer;
// 2. the browser allowed the playhead to proceed past the end of
// the buffer (either under normal or accelerated playback rates); or
// 3. the user agent seeked forwards but seeked() was not called or has
// the buffer (either under normal or accelerated playback rates);
// 3. there is a significant amount of negative drift; or
// 4. the user agent seeked forwards but seeked() was not called or has
// not been called yet (because it's a race).
// For cases 1 and 2 we'll end up buffering the next segment we want
// anyways, and for case 3 we'll end up buffering the next segment and
// then just removing it and buffering it again (note that this case
// should be rare).
// anyways, for case 3 we'll end up buffering behind the playhead until
// we catch up, and for case 4 we'll proceed as in case 2 of the previous
// block.
shaka.log.debug(logPrefix,

@@ -944,3 +923,3 @@ 'playhead in unbuffered region (ahead of buffer):',

// The buffer is non-empty.
if (mediaState.lastSegmentPeriodIndex == null ||
if (mediaState.lastStream == null ||
mediaState.lastSegmentReference == null) {

@@ -954,4 +933,6 @@ shaka.log.error(logPrefix, 'lastSegmentReference should not be null');

return this.manifest_.periods[mediaState.lastSegmentPeriodIndex].startTime +
mediaState.lastSegmentReference.endTime;
var lastPeriodIndex =
this.findPeriodContainingStream_(mediaState.lastStream);
var lastPeriod = this.manifest_.periods[lastPeriodIndex];
return lastPeriod.startTime + mediaState.lastSegmentReference.endTime;
};

@@ -966,49 +947,56 @@

* @param {number} currentPeriodIndex
* @return {!shaka.media.SegmentReference} The SegmentReference of the
* next segment needed.
* @throws {!shaka.util.Error} If the next segment does not exist.
* @return {shaka.media.SegmentReference} The SegmentReference of the
* next segment needed, or null if a segment could not be found, does not
* exist, or is not available.
* @private
*/
shaka.media.StreamingEngine.prototype.getSegmentReference_ = function(
shaka.media.StreamingEngine.prototype.getSegmentReferenceNeeded_ = function(
mediaState, playheadTime, currentPeriodIndex) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
if (mediaState.lastSegmentReference &&
mediaState.stream == mediaState.lastStream) {
// Something is buffered from the same Stream.
var position = mediaState.lastSegmentReference.position + 1;
shaka.log.v2(logPrefix, 'next position known:', 'position=' + position);
return this.getSegmentReferenceIfAvailable_(
mediaState, currentPeriodIndex, position);
}
var position;
if (!mediaState.lastSegmentReference) {
if (mediaState.lastSegmentReference) {
// Something is buffered from another Stream.
goog.asserts.assert(mediaState.lastStream, 'lastStream should not be null');
shaka.log.v1(logPrefix, 'next position unknown: another Stream buffered');
var lastPeriodIndex =
this.findPeriodContainingStream_(mediaState.lastStream);
var lastPeriod = this.manifest_.periods[lastPeriodIndex];
position = this.lookupSegmentPosition_(
mediaState,
lastPeriod.startTime + mediaState.lastSegmentReference.endTime,
currentPeriodIndex);
} else {
// Nothing is buffered.
goog.asserts.assert(!mediaState.lastStream, 'lastStream should be null');
shaka.log.v1(logPrefix, 'next position unknown: nothing buffered');
position = this.lookupSegmentPosition_(
mediaState, playheadTime, currentPeriodIndex);
} else {
if (currentPeriodIndex == mediaState.lastSegmentPeriodIndex) {
// Something is buffered from the same Period.
position = mediaState.lastSegmentReference.position + 1;
shaka.log.v2(logPrefix, 'using next position:', 'position=' + position);
} else {
// Something is buffered from another Period.
shaka.log.v1(logPrefix, 'next position unknown: another Period buffered');
goog.asserts.assert(mediaState.lastSegmentPeriodIndex != null,
'lastSegmentPeriodIndex should not be null!');
var lastPeriodStart =
this.manifest_.periods[mediaState.lastSegmentPeriodIndex].startTime;
position = this.lookupSegmentPosition_(
mediaState,
lastPeriodStart + mediaState.lastSegmentReference.endTime,
currentPeriodIndex);
}
}
var reference = mediaState.stream.getSegmentReference(position);
if (!reference) {
shaka.log.error(logPrefix,
'invalid segment index: SegmentReference does not exist:',
'currentPeriodIndex=' + currentPeriodIndex,
'position=' + position);
throw new shaka.util.Error(
shaka.util.Error.Category.STREAMING,
shaka.util.Error.Code.INVALID_SEGMENT_INDEX,
mediaState.type,
currentPeriodIndex,
position);
}
if (position == null)
return null;
// If there's positive drift then we need to get the previous segment;
// however, we don't actually know how much drift there is, so we must
// unconditionally get the previous segment. If it turns out that there's
// non-positive drift then we'll just end up buffering beind the playhead a
// little more than we needed.
var optimalPosition = Math.max(0, position - 1);
var reference =
this.getSegmentReferenceIfAvailable_(
mediaState, currentPeriodIndex, optimalPosition) ||
this.getSegmentReferenceIfAvailable_(
mediaState, currentPeriodIndex, position);
return reference;

@@ -1019,24 +1007,22 @@ };

/**
* Looks up the position of the next segment needed.
* Looks up the position of the segment containing the given timestamp.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} time relative to presentation timeline
* @param {number} presentationTime The timestamp needed, relative to the
* start of the presentation.
* @param {number} currentPeriodIndex
* @return {number}
* @throws {!shaka.util.Error} If the next segment does not exist.
* @return {?number} A segment position, or null if a segment was not be found.
* @private
*/
shaka.media.StreamingEngine.prototype.lookupSegmentPosition_ = function(
mediaState, time, currentPeriodIndex) {
mediaState, presentationTime, currentPeriodIndex) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
var currentPeriod = this.manifest_.periods[currentPeriodIndex];
shaka.log.v1(logPrefix,
'looking up next position:',
'time=' + time,
'currentPeriod.startTime=' + currentPeriod.startTime,
'mediaState.drift=' + mediaState.drift);
shaka.log.debug(logPrefix,
'looking up segment:',
'presentationTime=' + presentationTime,
'currentPeriod.startTime=' + currentPeriod.startTime);
var lookupTime = time - currentPeriod.startTime - mediaState.drift;
lookupTime = Math.max(lookupTime, 0);
var lookupTime = Math.max(0, presentationTime - currentPeriod.startTime);
var position = mediaState.stream.findSegmentPosition(lookupTime);

@@ -1046,13 +1032,5 @@

shaka.log.warning(logPrefix,
'next segment does not exist:',
'lookupTime=' + lookupTime,
'time=' + time,
'cannot find segment:',
'currentPeriod.startTime=' + currentPeriod.startTime,
'mediaState.drift=' + mediaState.drift);
throw new shaka.util.Error(
shaka.util.Error.Category.STREAMING,
shaka.util.Error.Code.SEGMENT_DOES_NOT_EXIST,
mediaState.type,
currentPeriodIndex,
time);
'lookupTime=' + lookupTime);
}

@@ -1065,2 +1043,46 @@

/**
* Gets the SegmentReference at the given position if it's available.
*
* @param {shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} currentPeriodIndex
* @param {number} position
* @return {shaka.media.SegmentReference}
*
* @private
*/
shaka.media.StreamingEngine.prototype.getSegmentReferenceIfAvailable_ =
function(mediaState, currentPeriodIndex, position) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
var currentPeriod = this.manifest_.periods[currentPeriodIndex];
var reference = mediaState.stream.getSegmentReference(position);
if (!reference) {
shaka.log.v1(logPrefix,
'segment does not exist:',
'currentPeriod.startTime=' + currentPeriod.startTime,
'position=' + position);
return null;
}
var timeline = this.manifest_.presentationTimeline;
var availabilityStart = timeline.getSegmentAvailabilityStart();
var availabilityEnd = timeline.getSegmentAvailabilityEnd();
if ((currentPeriod.startTime + reference.endTime < availabilityStart) ||
(currentPeriod.startTime + reference.startTime > availabilityEnd)) {
shaka.log.v2(logPrefix,
'segment is not available:',
'currentPeriod.startTime=' + currentPeriod.startTime,
'reference.startTime=' + reference.startTime,
'reference.endTime=' + reference.endTime,
'availabilityStart=' + availabilityStart,
'availabilityEnd=' + availabilityEnd);
return null;
}
return reference;
};
/**
* Fetches and appends the given segment; sets up the given MediaState's

@@ -1085,3 +1107,2 @@ * associated SourceBuffer and evicts segments if either are required

'currentPeriod.startTime=' + currentPeriod.startTime,
'mediaState.drift=' + mediaState.drift,
'reference.position=' + reference.position,

@@ -1096,2 +1117,3 @@ 'reference.startTime=' + reference.startTime,

// callbacks too.
var stream = mediaState.stream;

@@ -1126,13 +1148,6 @@ // Compute the append window end.

playheadTime,
currentPeriodIndex,
stream,
reference,
appendWindowEnd,
results[1]);
}.bind(this)).then(function() {
if (this.destroyed_) return;
return this.handleDrift_(mediaState,
playheadTime,
currentPeriodIndex,
reference);
}.bind(this)).then(function() {
mediaState.performingUpdate = false;

@@ -1147,3 +1162,3 @@

if (!this.destroyed_)
this.handleStartup_(mediaState);
this.handleStartup_(mediaState, stream);

@@ -1217,10 +1232,8 @@ shaka.log.v1(logPrefix, 'finished fetch and append');

/**
* Appends the given segment, evicts segments if required to append, and
* computes drift if required.
* Appends the given segment and evicts content if required to append.
*
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} playheadTime
* @param {number} currentPeriodIndex
* @param {shakaExtern.Stream} stream
* @param {!shaka.media.SegmentReference} reference
* @param {?number} appendWindowEnd
* @param {!ArrayBuffer} segment

@@ -1231,4 +1244,3 @@ * @return {!Promise}

shaka.media.StreamingEngine.prototype.append_ = function(
mediaState, playheadTime, currentPeriodIndex, reference,
appendWindowEnd, segment) {
mediaState, playheadTime, stream, reference, segment) {
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);

@@ -1246,3 +1258,4 @@

mediaState.lastSegmentPeriodIndex = currentPeriodIndex;
// We must use |stream| because switch() may have been called.
mediaState.lastStream = stream;
mediaState.lastSegmentReference = reference;

@@ -1265,14 +1278,7 @@

mediaState, playheadTime) {
var MapUtils = shaka.util.MapUtils;
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
shaka.log.v2(logPrefix, 'checking buffer length');
// Get the earliest start time.
goog.asserts.assert(this.mediaStates_, 'Must be initialized');
var mediaStates = MapUtils.values(this.mediaStates_);
var times = mediaStates.map(function(state) {
return this.mediaSourceEngine_.bufferStart(state.type);
}.bind(this)).filter(shaka.util.Functional.isNotNull);
if (times.length == 0) {
var startTime = this.mediaSourceEngine_.bufferStart(mediaState.type);
if (startTime == null) {
shaka.log.v2(logPrefix,

@@ -1284,7 +1290,2 @@ 'buffer behind okay because nothing buffered:',

}
var startTime = Math.min.apply(Math, times);
goog.asserts.assert(startTime != Number.POSITIVE_INFINITY,
'startTime should not be infinity');
var bufferedBehind = playheadTime - startTime;

@@ -1310,8 +1311,4 @@

var async = mediaStates.map(function(state) {
return this.mediaSourceEngine_.remove(
state.type, startTime, startTime + overflow);
}.bind(this));
return Promise.all(async).then(function() {
return this.mediaSourceEngine_.remove(
mediaState.type, startTime, startTime + overflow).then(function() {
if (this.destroyed_) return;

@@ -1324,50 +1321,2 @@ shaka.log.v1(logPrefix, 'evicted ' + overflow + ' seconds');

/**
* Handles drift.
*
* @param {!shaka.media.StreamingEngine.MediaState_} mediaState
* @param {number} playheadTime
* @param {number} currentPeriodIndex
* @param {!shaka.media.SegmentReference} reference
* @return {!Promise}
* @private
*/
shaka.media.StreamingEngine.prototype.handleDrift_ = function(
mediaState, playheadTime, currentPeriodIndex, reference) {
if (mediaState.drift != null)
return Promise.resolve();
var logPrefix = shaka.media.StreamingEngine.logPrefix_(mediaState);
var currentPeriod = this.manifest_.periods[currentPeriodIndex];
var bufferStart = this.mediaSourceEngine_.bufferStart(mediaState.type);
if (bufferStart == null) {
// The segment did not contain any actual media content.
shaka.log.error(logPrefix, 'bad segment');
return Promise.reject(new shaka.util.Error(
shaka.util.Error.Category.STREAMING,
shaka.util.Error.Code.BAD_SEGMENT,
mediaState.type));
}
mediaState.drift =
bufferStart - reference.startTime - currentPeriod.startTime;
shaka.log.debug(logPrefix, 'drift=', mediaState.drift);
// If there is positive drift or large negative drift then the playhead
// may not be within the segment we just appended.
var bufferedAhead = this.mediaSourceEngine_.bufferedAheadOf(
mediaState.type, playheadTime);
if (bufferedAhead == 0) {
// Clear the buffer and try again.
shaka.log.debug(logPrefix,
'playhead outside first segment:',
'bufferStart=' + bufferStart);
mediaState.waitingToClearBuffer = true;
}
return Promise.resolve();
};
/**
* Sets up all known Periods when startup completes; otherwise, does nothing.

@@ -1377,5 +1326,7 @@ *

* MediaState updated.
* @param {shakaExtern.Stream} stream
* @private
*/
shaka.media.StreamingEngine.prototype.handleStartup_ = function(mediaState) {
shaka.media.StreamingEngine.prototype.handleStartup_ = function(
mediaState, stream) {
var Functional = shaka.util.Functional;

@@ -1392,13 +1343,4 @@ var MapUtils = shaka.util.MapUtils;

// Startup completes once we have buffered at least one segment from each
// MediaState, have handled positive or large negative drift, and are not
// clearing the buffer. Hence, the following three cases:
// 1. if |drift| is null then we never appended anything;
// 2. if |drift| is non-null but we're clearing the buffer then either
// there was positive or large negative drift, or the user agent
// seeked; and
// 3. if |drift| is non-null and we're not clearing the buffer but the
// buffer is empty then there was positive or large negative
// drift but we never recovered.
return ms.drift != null &&
!ms.waitingToClearBuffer &&
// MediaState.
return !ms.waitingToClearBuffer &&
!ms.clearingBuffer &&

@@ -1413,3 +1355,5 @@ ms.lastSegmentReference;

var currentPeriodIndex = this.findPeriodContainingStream_(mediaState.stream);
// We must use |stream| because switch() may have been called.
var currentPeriodIndex = this.findPeriodContainingStream_(stream);
goog.asserts.assert(

@@ -1643,3 +1587,3 @@ mediaStates.every(function(ms) {

shaka.log.debug(logPrefix, 'cleared buffer');
mediaState.lastSegmentPeriodIndex = null;
mediaState.lastStream = null;
mediaState.lastSegmentReference = null;

@@ -1646,0 +1590,0 @@ mediaState.clearingBuffer = false;

@@ -62,7 +62,8 @@ /**

} else {
if (!COMPILED) {
var responseText =
shaka.util.StringUtils.fromBytesAutoDetect(target.response);
shaka.log.debug('HTTP error text:', responseText);
}
var responseText = null;
try {
responseText = shaka.util.StringUtils.fromBytesAutoDetect(
target.response);
} catch (exception) {}
shaka.log.debug('HTTP error text:', responseText);
reject(new shaka.util.Error(

@@ -72,3 +73,4 @@ shaka.util.Error.Category.NETWORK,

uri,
target.status));
target.status,
responseText));
}

@@ -75,0 +77,0 @@ };

@@ -81,2 +81,3 @@ /**

* !Promise.<shakaExtern.Response>}
* @exportDoc
*/

@@ -92,2 +93,3 @@ shaka.net.NetworkingEngine.SchemePlugin;

* shakaExtern.Request)}
* @exportDoc
*/

@@ -103,2 +105,3 @@ shaka.net.NetworkingEngine.RequestFilter;

* shakaExtern.Response)}
* @exportDoc
*/

@@ -105,0 +108,0 @@ shaka.net.NetworkingEngine.ResponseFilter;

@@ -114,3 +114,5 @@ /**

/** @private {!Object.<string, shakaExtern.Stream>} */
/**
* @private {!Object.<string, {stream: shakaExtern.Stream, clear: boolean}>}
*/
this.deferredSwitches_ = {};

@@ -165,3 +167,3 @@

*/
goog.define('GIT_VERSION', 'v2.0.0-beta-debug');
goog.define('GIT_VERSION', 'v2.0.0-beta2-debug');

@@ -203,2 +205,11 @@

/**
* @event shaka.Player.TextTrackVisibilityEvent
* @description Fired when text track visibility changes.
* @property {string} type
* 'texttrackvisibility'
* @exportDoc
*/
/**
* @event shaka.Player.TracksChangedEvent

@@ -221,5 +232,2 @@ * @description Fired when the list of tracks changes.

// TODO: SeekRangeChangedEvent
/**

@@ -495,6 +503,8 @@ * Query the browser/platform and the plugins for manifest, media, and DRM

var audioLangChanged = config.preferredAudioLanguage &&
config.preferredAudioLanguage !== this.config_.preferredAudioLanguage;
var textLangChanged = config.preferredTextLanguage &&
config.preferredTextLanguage !== this.config_.preferredTextLanguage;
var audioLangChanged = config.preferredAudioLanguage ?
config.preferredAudioLanguage !== this.config_.preferredAudioLanguage :
false;
var textLangChanged = config.preferredTextLanguage ?
config.preferredTextLanguage !== this.config_.preferredTextLanguage :
false;

@@ -509,2 +519,14 @@ if (config.abr && config.abr.manager &&

this.applyConfig_(audioLangChanged, textLangChanged);
};
/**
* Apply config changes.
* @param {boolean} audioLangChanged
* @param {boolean} textLangChanged
* @private
*/
shaka.Player.prototype.applyConfig_ = function(
audioLangChanged, textLangChanged) {
if (this.parser_) {

@@ -519,2 +541,5 @@ this.parser_.configure(this.config_.manifest);

// Need to apply the restrictions to every period.
this.manifest_.periods.forEach(this.applyRestrictions_.bind(this));
// If the languages have changed, then choose streams again.

@@ -524,14 +549,15 @@ var period = this.streamingEngine_.getCurrentPeriod();

var chosen = this.chooseStreams_(period);
// Get the active streams to check if it is restricted. If it is, then
// AbrManager will make a new choice and we will apply it below.
var activeStreams = this.streamingEngine_.getActiveStreams();
for (var kind in chosen) {
if ((kind == 'audio' && audioLangChanged) ||
(kind == 'text' && textLangChanged)) {
if (this.switchingPeriods_) {
this.deferredSwitches_[kind] = chosen[kind];
} else {
this.streamingEngine_.switch(kind, chosen[kind],
/* clearBuffer */ true);
}
}
}
var streamsToSwitch = shaka.util.MapUtils.filter(chosen,
function(type, stream) {
return (type == 'audio' && audioLangChanged) ||
(type == 'text' && textLangChanged) ||
(activeStreams[type] &&
(!activeStreams[type].allowedByApplication ||
!activeStreams[type].allowedByKeySystem));
});
this.deferredSwitch_(streamsToSwitch, /* clearBuffer */ true);
}

@@ -570,2 +596,30 @@

/**
* Reset configuration to default.
* @export
*/
shaka.Player.prototype.resetConfiguration = function() {
var config = this.defaultConfig_();
var audioLangChanged = config.preferredAudioLanguage ?
config.preferredAudioLanguage !== this.config_.preferredAudioLanguage :
false;
var textLangChanged = config.preferredTextLanguage ?
config.preferredTextLanguage !== this.config_.preferredTextLanguage :
false;
if (config.abr && config.abr.manager &&
config.abr.manager != this.config_.abr.manager) {
this.config_.abr.manager.stop();
config.abr.manager.init(this.switch_.bind(this));
}
// Don't call mergeConfigObjects_(), since that would not reset open-ended
// dictionaries like drm.servers.
this.config_ = this.defaultConfig_();
this.applyConfig_(audioLangChanged, textLangChanged);
};
/**
* @return {shaka.net.NetworkingEngine} A reference to the Player's networking

@@ -587,5 +641,5 @@ * engine. Applications may use this to make requests through Shaka's

shaka.Player.prototype.isLive = function() {
if (!this.manifest_) return false;
var timeline = this.manifest_.presentationTimeline;
return timeline.getDuration() == Number.POSITIVE_INFINITY;
return this.manifest_ ?
this.manifest_.presentationTimeline.isLive() :
false;
};

@@ -595,2 +649,19 @@

/**
* Get the seekable range for the current stream.
* @return {{start: number, end: number}}
* @export
*/
shaka.Player.prototype.seekRange = function() {
var start = 0;
var end = 0;
if (this.manifest_) {
var timeline = this.manifest_.presentationTimeline;
start = timeline.getSegmentAvailabilityStart();
end = timeline.getSegmentAvailabilityEnd();
}
return {'start': start, 'end': end};
};
/**
* @return {boolean} True if the Player is in a buffering state.

@@ -620,2 +691,14 @@ * @export

/**
* Gets the current effective playback rate. If using trick play, it will
* return the current trick play rate; otherwise, it will return the video
* playback rate.
* @return {number}
* @export
*/
shaka.Player.prototype.getPlaybackRate = function() {
return this.playhead_ ? this.playhead_.getPlaybackRate() : 0;
};
/**
* Skip through the content without playing. Simulated using repeated seeks.

@@ -632,3 +715,4 @@ *

shaka.Player.prototype.trickPlay = function(rate) {
// TODO: Trick play
if (this.playhead_)
this.playhead_.setPlaybackRate(rate);
};

@@ -642,3 +726,4 @@

shaka.Player.prototype.cancelTrickPlay = function() {
// TODO: Trick play
if (this.playhead_)
this.playhead_.setPlaybackRate(1);
};

@@ -666,14 +751,18 @@

var activeStream = activeStreams[streamSet.type];
return streamSet.streams.map(function(stream) {
return {
id: stream.id,
active: activeStream == stream,
type: streamSet.type,
bandwidth: stream.bandwidth,
language: streamSet.language,
kind: stream.kind || null,
width: stream.width || null,
height: stream.height || null
};
});
return streamSet.streams
.filter(function(stream) {
return stream.allowedByApplication && stream.allowedByKeySystem;
})
.map(function(stream) {
return {
id: stream.id,
active: activeStream == stream,
type: streamSet.type,
bandwidth: stream.bandwidth,
language: streamSet.language,
kind: stream.kind || null,
width: stream.width || null,
height: stream.height || null
};
});
})

@@ -711,2 +800,9 @@ .reduce(Functional.collapseArrays, []);

// Double check that the track is allowed to be played.
if (!stream.allowedByApplication || !stream.allowedByKeySystem) {
shaka.log.error('Unable to switch to track with id "' + track.id +
'" because it is restricted.');
return;
}
// Add an entry to the history.

@@ -726,13 +822,5 @@ this.switchHistory_.push({

if (this.switchingPeriods_) {
// We are switching Periods so we cannot switch yet, so wait until
// chooseStreams_ is called and handle there. The buffer does not have any
// data from this Period, so we can ignore |opt_clearBuffer|.
this.deferredSwitches_[track.type] = stream;
} else {
// Since text tracks are small and likely fully buffered (some are just a
// single segment), always clear the buffer for text tracks.
this.streamingEngine_.switch(track.type, stream,
opt_clearBuffer || track.type == 'text');
}
var streamsToSwitch = {};
streamsToSwitch[track.type] = stream;
this.deferredSwitch_(streamsToSwitch, opt_clearBuffer);
};

@@ -757,3 +845,4 @@

shaka.Player.prototype.setTextTrackVisibility = function(on) {
this.textTrack_.mode = on ? 'showing' : 'disabled';
this.textTrack_.mode = on ? 'showing' : 'hidden';
this.onTextTrackVisibility_();
};

@@ -814,3 +903,3 @@

uri, language, kind, mime, opt_codec) {
if (!this.manifest_) {
if (!this.streamingEngine_) {
shaka.log.error(

@@ -861,3 +950,6 @@ 'Must call load() and wait for it to resolve before adding text ' +

kind: kind,
keyId: null
encrypted: false,
keyId: null,
allowedByApplication: true,
allowedByKeySystem: true
};

@@ -868,3 +960,3 @@ /** @type {shakaExtern.StreamSet} */

type: 'text',
primary: true,
primary: false,
drmInfos: [],

@@ -878,2 +970,18 @@ streams: [stream]

period.streamSets.push(streamSet);
// This will disable AbrManager but it will be enabled again below.
var chosen = this.chooseStreams_(period);
if (chosen['text'] == stream) {
this.deferredSwitch_({'text': stream});
}
if (this.config_.abr.enabled && !this.switchingPeriods_) {
this.config_.abr.manager.enable();
} else {
this.config_.abr.manager.disable();
}
this.onTracksChanged_();
return {

@@ -889,3 +997,3 @@ id: stream.id,

};
});
}.bind(this));
};

@@ -906,5 +1014,5 @@

var track = this.video_.textTracks[i];
track.mode = 'hidden';
track.mode = 'disabled';
if (track.id == shaka.Player.TextTrackId_) {
if (track.label == shaka.Player.TextTrackLabel_) {
this.textTrack_ = track;

@@ -920,5 +1028,5 @@ }

this.textTrack_ = this.video_.addTextTrack(
'subtitles', shaka.Player.TextTrackId_);
this.textTrack_.mode = 'hidden';
'subtitles', shaka.Player.TextTrackLabel_);
}
this.textTrack_.mode = 'hidden';

@@ -980,5 +1088,18 @@ // TODO: test that in all cases, the built-in CC controls in the video element

*/
shaka.Player.TextTrackId_ = 'Shaka Player TextTrack';
shaka.Player.TextTrackLabel_ = 'Shaka Player TextTrack';
/**
* Determines if the given stream set has any playable streams.
* @param {shakaExtern.StreamSet} streamSet
* @return {boolean}
* @private
*/
shaka.Player.hasPlayableStreams_ = function(streamSet) {
return streamSet.streams.some(function(stream) {
return stream.allowedByApplication && stream.allowedByKeySystem;
});
};
// TODO: consider moving config-parsing to another file.

@@ -1092,3 +1213,4 @@ /**

if (node) return null;
}
},
clockSyncUri: ''
}

@@ -1109,3 +1231,15 @@ },

preferredAudioLanguage: '',
preferredTextLanguage: ''
preferredTextLanguage: '',
restrictions: {
minWidth: 0,
maxWidth: Number.POSITIVE_INFINITY,
minHeight: 0,
maxHeight: Number.POSITIVE_INFINITY,
minPixels: 0,
maxPixels: Number.POSITIVE_INFINITY,
minAudioBandwidth: 0,
maxAudioBandwidth: Number.POSITIVE_INFINITY,
minVideoBandwidth: 0,
maxVideoBandwidth: Number.POSITIVE_INFINITY
}
};

@@ -1134,6 +1268,51 @@ };

*/
shaka.Player.prototype.applyRestrictions_ = function(period) {
var restrictions = this.config_.restrictions;
var tracksChanged = false;
period.streamSets.forEach(function(streamSet) {
streamSet.streams.forEach(function(stream) {
var originalAllowed = stream.allowedByApplication;
stream.allowedByApplication = true;
if (streamSet.type == 'video') {
if (stream.width < restrictions.minWidth ||
stream.width > restrictions.maxWidth ||
stream.height < restrictions.minHeight ||
stream.height > restrictions.maxHeight ||
(stream.width * stream.height) < restrictions.minPixels ||
(stream.width * stream.height) > restrictions.maxPixels ||
stream.bandwidth < restrictions.minVideoBandwidth ||
stream.bandwidth > restrictions.maxVideoBandwidth) {
stream.allowedByApplication = false;
}
} else if (streamSet.type == 'audio') {
if (stream.bandwidth < restrictions.minAudioBandwidth ||
stream.bandwidth > restrictions.maxAudioBandwidth) {
stream.allowedByApplication = false;
}
}
if (originalAllowed != stream.allowedByApplication)
tracksChanged = true;
});
});
if (tracksChanged)
this.onTracksChanged_();
// We may be playing a restricted stream; but the calling code will handle it.
};
/**
* @param {shakaExtern.Period} period
* @private
*/
shaka.Player.prototype.filterPeriod_ = function(period) {
var keySystem = '';
var supportedMimeTypes = null;
if (this.drmEngine_ && this.drmEngine_.initialized()) {
keySystem = this.drmEngine_.keySystem();
supportedMimeTypes = this.drmEngine_.getSupportedTypes();
}

@@ -1190,2 +1369,9 @@

}
if (supportedMimeTypes && stream.encrypted &&
supportedMimeTypes.indexOf(fullMimeType) < 0) {
streamSet.streams.splice(j, 1);
--j;
continue;
}
}

@@ -1199,3 +1385,7 @@

if (period.streamSets.length == 0) {
this.applyRestrictions_(period);
var hasPlayableStreamSets =
period.streamSets.some(shaka.Player.hasPlayableStreams_);
if (!hasPlayableStreamSets) {
this.onError_(new shaka.util.Error(

@@ -1208,2 +1398,22 @@ shaka.util.Error.Category.MANIFEST,

/**
* Switches to the given streams, deferring switches if needed.
* @param {!Object.<string, shakaExtern.Stream>} streamsByType
* @param {boolean=} opt_clearBuffer
* @private
*/
shaka.Player.prototype.deferredSwitch_ = function(
streamsByType, opt_clearBuffer) {
for (var type in streamsByType) {
var stream = streamsByType[type];
var clear = opt_clearBuffer || type == 'text';
if (this.switchingPeriods_) {
this.deferredSwitches_[type] = {stream: stream, clear: clear};
} else {
this.streamingEngine_.switch(type, stream, clear);
}
}
};
/** @private */

@@ -1250,3 +1460,3 @@ shaka.Player.prototype.updateStats_ = function() {

var event = new shaka.util.FakeEvent('buffering', { buffering: buffering });
var event = new shaka.util.FakeEvent('buffering', { 'buffering': buffering });
this.dispatchEvent(event);

@@ -1278,2 +1488,3 @@ };

var LanguageUtils = shaka.util.LanguageUtils;
var hasPlayableStreams = shaka.Player.hasPlayableStreams_;

@@ -1284,3 +1495,3 @@ // Choose the first stream set listed as the default.

period.streamSets.forEach(function(set) {
if (set.type in streamSetsByType) return;
if (!hasPlayableStreams(set) || set.type in streamSetsByType) return;
streamSetsByType[set.type] = set;

@@ -1291,3 +1502,3 @@ });

period.streamSets.forEach(function(set) {
if (set.primary)
if (hasPlayableStreams(set) && set.primary)
streamSetsByType[set.type] = set;

@@ -1307,2 +1518,5 @@ });

period.streamSets.forEach(function(set) {
if (!hasPlayableStreams(set))
return;
/** @type {string} */

@@ -1326,2 +1540,11 @@ var pref;

// If there are no unrestricted streams, issue an error.
var manifestIsPlayable =
shaka.util.MapUtils.values(streamSetsByType).some(hasPlayableStreams);
if (!manifestIsPlayable) {
this.onError_(new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.ALL_STREAMS_RESTRICTED));
}
var chosen = this.config_.abr.manager.chooseStreams(streamSetsByType);

@@ -1340,2 +1563,3 @@

this.textTrack_.mode = 'showing';
this.onTextTrackVisibility_();
}

@@ -1367,4 +1591,6 @@ }

// Override the chosen streams with the ones picked in selectTrack.
for (var kind in this.deferredSwitches_) {
chosen[kind] = this.deferredSwitches_[kind];
for (var type in this.deferredSwitches_) {
// We are choosing initial tracks, so no segments from this Period have
// been downloaded yet.
chosen[type] = this.deferredSwitches_[type].stream;
}

@@ -1405,4 +1631,5 @@ this.deferredSwitches_ = {};

// If we still have deferred switches, switch now.
for (var kind in this.deferredSwitches_) {
this.streamingEngine_.switch(kind, this.deferredSwitches_[kind]);
for (var type in this.deferredSwitches_) {
var info = this.deferredSwitches_[type];
this.streamingEngine_.switch(type, info.stream, info.clear);
}

@@ -1486,2 +1713,9 @@ this.deferredSwitches_ = {};

/** @private */
shaka.Player.prototype.onTextTrackVisibility_ = function() {
var event = new shaka.util.FakeEvent('texttrackvisibility');
this.dispatchEvent(event);
};
/**

@@ -1494,3 +1728,3 @@ * @param {!shaka.util.Error} error

var event = new shaka.util.FakeEvent('error', { detail: error });
var event = new shaka.util.FakeEvent('error', { 'detail': error });
this.dispatchEvent(event);

@@ -1538,3 +1772,38 @@ };

shaka.Player.prototype.onKeyStatus_ = function(keyStatusMap) {
// TODO: use key status information
goog.asserts.assert(this.streamingEngine_, 'Should have been initialized.');
var allowedStatuses = ['usable', 'status-pending', 'output-downscaled'];
var period = this.streamingEngine_.getCurrentPeriod();
var tracksChanged = false;
period.streamSets.forEach(function(streamSet) {
streamSet.streams.forEach(function(stream) {
var originalAllowed = stream.allowedByKeySystem;
if (stream.keyId && stream.keyId in keyStatusMap) {
// Only update the status if it is in the map. If it is not in the map,
// then it was not updated and so it's status has not changed.
stream.allowedByKeySystem =
allowedStatuses.indexOf(keyStatusMap[stream.keyId]) >= 0;
}
if (originalAllowed != stream.allowedByKeySystem)
tracksChanged = true;
});
});
// This will disable AbrManager.
var chosen = this.chooseStreams_(period);
if (this.config_.abr.enabled && !this.switchingPeriods_)
this.config_.abr.manager.enable();
// If we are playing a restricted track, we will need to switch.
var activeStreams = this.streamingEngine_.getActiveStreams();
var streamsToSwitch = shaka.util.MapUtils.filter(chosen,
function(type, stream) {
return activeStreams[type] && !activeStreams[type].allowedByKeySystem;
});
this.deferredSwitch_(streamsToSwitch, /* clearBuffer */ true);
if (tracksChanged)
this.onTracksChanged_();
};

@@ -138,2 +138,4 @@ /**

* <br> error.data[1] is the status code.
* <br> error.data[2] is the response text, or null if the response could not
* be interpretted as text.
*/

@@ -349,3 +351,9 @@ 'BAD_HTTP_STATUS': 1001,

/**
* There exist some playable streams; however because of restrictions imposed
* by the application or the key system, all of them are not useable.
*/
'ALL_STREAMS_RESTRICTED': 4012,
/**

@@ -363,45 +371,2 @@ * The StreamingEngine appended a segment but the SourceBuffer is empty, or

/**
* The StreamingEngine cannot append the next segment because the segment's
* corresponding SegmentReference does not exist (i.e., findSegmentPosition()
* succeeded but getSegmentReference() failed) or the segment's corresponding
* SegmentReference has an invalid time range.
*
* This is a non-recoverable error.
*
* <br> error.data[0] is the type of content which caused the error.
* <br> error.data[1] is the index of the Period.
* <br> error.data[2] is the position of the segment.
*/
'INVALID_SEGMENT_INDEX': 5001,
/**
* The StreamingEngine cannot append the next segment because the next
* segment does not exist (i.e., findSegmentPosition() failed). This can
* occur for three reasons:
* 1. there is positive drift (the segments' timestamps are ahead of
* the manifest's timestamps) and the playhead is outside the drifted
* segment availability window.
* 2. the manifest is not updating fast enough for live presentations; or
* 3. the manifest is not complete.
* The first case is a recoverable error; recovery may be attempted by
* repositioning the playhead under a segment.
*
* <br> error.data[0] is the type of content which caused the error.
* <br> error.data[1] is the index of the Period.
* <br> error.data[2] is the timestamp needed.
*/
'SEGMENT_DOES_NOT_EXIST': 5002,
/**
* The StreamingEngine inserted a media segment, but the segment did not
* contain any actual media content.
*
* This is likely a non-recoverable error; however, recovery may be attempted
* by seeking forwards or backwards (e.g., nudging the playhead).
*
* <br> error.data[0] is the type of content which caused the error.
*/
'BAD_SEGMENT': 5004,
/**
* The StreamingEngine called onChooseStreams() but the callback receiver

@@ -478,3 +443,13 @@ * did not return the correct number or type of Streams.

*/
'ENCRYPTED_CONTENT_WITHOUT_DRM_INFO': 6010
'ENCRYPTED_CONTENT_WITHOUT_DRM_INFO': 6010,
/**
* The media is encrypted with keys that were not fetched. This can happen
* under several conditions:
* - The license server (or proxy) returned the wrong keys.
* - The PSSH in the manifest does not contain the required key IDs.
* - The PSSH in the media does not contain the required key IDs. This
* means that the PSSH does not match the key ID in the media.
*/
'WRONG_KEYS': 6011
};

@@ -66,1 +66,19 @@ /**

};
/**
* Creates a new object where the values are filtered out acording to a
* predicate.
*
* @param {!Object.<KEY, VALUE>} object
* @param {function(KEY, VALUE):boolean} callback
* @return {!Object.<KEY, VALUE>}
* @template KEY,VALUE
*/
shaka.util.MapUtils.filter = function(object, callback) {
return Object.keys(object).reduce(function(ret, key) {
if (callback(key, object[key]))
ret[key] = object[key];
return ret;
}, {});
};

@@ -39,3 +39,3 @@ /**

// http://stackoverflow.com/a/13691499
var utf8 = String.fromCharCode.apply(null, new Uint8Array(data));
var utf8 = shaka.util.StringUtils.fromCharCode_(new Uint8Array(data));
// This converts each character in the string to an escape sequence. If the

@@ -89,4 +89,4 @@ // character is in the ASCII range, it is not converted; otherwise it is

// Use a DataView to ensure correct endianness.
var arr = [];
var length = data.byteLength / 2;
var arr = new Uint16Array(length);
var dataView = new DataView(buffer);

@@ -96,3 +96,3 @@ for (var i = 0; i < length; i++) {

}
return String.fromCharCode.apply(null, arr);
return shaka.util.StringUtils.fromCharCode_(arr);
};

@@ -165,1 +165,20 @@

};
/**
* Creates a new string from the given array of char codes.
*
* @param {!ITypedArray} args
* @return {string}
* @private
*/
shaka.util.StringUtils.fromCharCode_ = function(args) {
var max = 16000;
var ret = '';
for (var i = 0; i < args.length; i += max) {
var subArray = args.subarray(i, i + max);
ret += String.fromCharCode.apply(null, subArray);
}
return ret;
};
{
"name": "shaka-player",
"description": "DASH/EME video player library",
"version": "2.0.0-beta",
"version": "2.0.0-beta2",
"homepage": "https://github.com/google/shaka-player",

@@ -14,3 +14,3 @@ "author": "Google",

"devDependencies": {
"htmlhint": "0.9.x",
"htmlhint": "0.9.12",
"in-publish": "2.x",

@@ -17,0 +17,0 @@ "jasmine-ajax": "3.2.x",

@@ -40,3 +40,3 @@ /**

retryParameters: retry,
dash: { customScheme: callback }
dash: { clockSyncUri: '', customScheme: callback }
});

@@ -43,0 +43,0 @@ dashParser.start('http://example.com', netEngine, filterPeriod, fail)

@@ -37,3 +37,3 @@ /**

retryParameters: retry,
dash: { customScheme: function(node) { return null; } }
dash: { clockSyncUri: '', customScheme: function(node) { return null; } }
});

@@ -187,5 +187,5 @@ });

// 15 seconds for @timeShiftBufferDepth and the first segment
// duration.
Date.now = function() { return (2 * 15) * 1000; };
// 15 seconds for @timeShiftBufferDepth, the first segment duration,
// and the @suggestedPresentationDuration.
Date.now = function() { return (2 * 15 + 5) * 1000; };
return delayForUpdatePeriod().then(function() {

@@ -485,71 +485,113 @@ // The first reference should have been evicted.

it('uses @maxSegmentDuration', function(done) {
var manifest = [
'<MPD type="dynamic" suggestedPresentationDelay="PT0S"',
' minimumUpdatePeriod="PT5S"',
' timeShiftBufferDepth="PT2M"',
' maxSegmentDuration="PT15S"',
' availabilityStartTime="1970-01-01T00:05:00Z">',
' <Period id="1">',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'<SegmentTemplate startNumber="1" media="s$Number$.mp4" duration="2" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
fakeNetEngine.setResponseMapAsText({'dummy://foo': manifest});
describe('maxSegmentDuration', function() {
it('uses @maxSegmentDuration', function(done) {
var manifest = [
'<MPD type="dynamic" suggestedPresentationDelay="PT0S"',
' minimumUpdatePeriod="PT5S"',
' timeShiftBufferDepth="PT2M"',
' maxSegmentDuration="PT15S"',
' availabilityStartTime="1970-01-01T00:05:00Z">',
' <Period id="1">',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'<SegmentTemplate media="s$Number$.mp4" duration="2" />',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
fakeNetEngine.setResponseMapAsText({'dummy://foo': manifest});
Date.now = function() { return 600000; /* 10 minutes */ };
parser.start('dummy://foo', fakeNetEngine, newPeriod, errorCallback)
.then(function(manifest) {
expect(manifest).toBeTruthy();
var timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
expect(timeline.getSegmentAvailabilityStart()).toBe(165);
expect(timeline.getSegmentAvailabilityEnd()).toBe(285);
})
.catch(fail)
.then(done);
});
Date.now = function() { return 600000; /* 10 minutes */ };
parser.start('dummy://foo', fakeNetEngine, newPeriod, errorCallback)
.then(function(manifest) {
expect(manifest).toBeTruthy();
var timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
expect(timeline.getSegmentAvailabilityStart()).toBe(165);
expect(timeline.getSegmentAvailabilityEnd()).toBe(285);
})
.catch(fail)
.then(done);
});
it('derives @maxSegmentDuration if omitted', function(done) {
var manifest = [
'<MPD type="dynamic" suggestedPresentationDelay="PT0S"',
' minimumUpdatePeriod="PT5S"',
' timeShiftBufferDepth="PT2M"',
' availabilityStartTime="1970-01-01T00:05:00Z">',
' <Period id="1">',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'<SegmentTemplate startNumber="1" media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S t="0" d="7" />',
' <S d="8" />',
' <S d="6" />',
' </SegmentTimeline>',
'</SegmentTemplate>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
fakeNetEngine.setResponseMapAsText({'dummy://foo': manifest});
it('derived from SegmentTemplate w/ SegmentTimeline', function(done) {
var lines = [
'<SegmentTemplate media="s$Number$.mp4">',
' <SegmentTimeline>',
' <S t="0" d="7" />',
' <S d="8" />',
' <S d="6" />',
' </SegmentTimeline>',
'</SegmentTemplate>'
];
testDerived(lines, done);
});
Date.now = function() { return 600000; /* 10 minutes */ };
parser.start('dummy://foo', fakeNetEngine, newPeriod, errorCallback)
.then(function(manifest) {
expect(manifest).toBeTruthy();
var timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
it('derived from SegmentTemplate w/ @duration', function(done) {
var lines = [
'<SegmentTemplate media="s$Number$.mp4" duration="8" />'
];
testDerived(lines, done);
});
// NOTE: the largest segment is 8 seconds long.
expect(timeline.getSegmentAvailabilityStart()).toBe(172);
expect(timeline.getSegmentAvailabilityEnd()).toBe(292);
})
.catch(fail)
.then(done);
it('derived from SegmentList', function(done) {
var lines = [
'<SegmentList duration="8">',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
'</SegmentList>'
];
testDerived(lines, done);
});
it('derived from SegmentList w/ SegmentTimeline', function(done) {
var lines = [
'<SegmentList duration="8">',
' <SegmentTimeline>',
' <S t="0" d="5" />',
' <S d="4" />',
' <S d="8" />',
' </SegmentTimeline>',
' <SegmentURL media="s1.mp4" />',
' <SegmentURL media="s2.mp4" />',
'</SegmentList>'
];
testDerived(lines, done);
});
function testDerived(lines, done) {
var template = [
'<MPD type="dynamic" suggestedPresentationDelay="PT0S"',
' minimumUpdatePeriod="PT5S"',
' timeShiftBufferDepth="PT2M"',
' availabilityStartTime="1970-01-01T00:05:00Z">',
' <Period id="1">',
' <AdaptationSet id="2" mimeType="video/mp4">',
' <Representation id="3" bandwidth="500">',
' <BaseURL>http://example.com</BaseURL>',
'%(contents)s',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
var manifest = sprintf(template, { contents: lines.join('\n') });
fakeNetEngine.setResponseMapAsText({'dummy://foo': manifest});
Date.now = function() { return 600000; /* 10 minutes */ };
parser.start('dummy://foo', fakeNetEngine, newPeriod, errorCallback)
.then(function(manifest) {
expect(manifest).toBeTruthy();
var timeline = manifest.presentationTimeline;
expect(timeline).toBeTruthy();
// NOTE: the largest segment is 8 seconds long in each test.
expect(timeline.getSegmentAvailabilityStart()).toBe(172);
expect(timeline.getSegmentAvailabilityEnd()).toBe(292);
})
.catch(fail)
.then(done);
}
});

@@ -659,47 +701,31 @@

it('interrupts UTCTiming requests', function(done) {
var delay = fakeNetEngine.delayNextRequest();
Util.delay(0.2, realTimeout).then(function() {
// This is the initial manifest request.
expect(fakeNetEngine.request.calls.count()).toBe(1);
fakeNetEngine.expectRequest(manifestUri, manifestRequestType);
fakeNetEngine.request.calls.reset();
// Resolve the manifest request and wait on the UTCTiming request.
delay.resolve();
delay = fakeNetEngine.delayNextRequest();
return Util.delay(0.2, realTimeout);
}).then(function() {
// This is the first UTCTiming request.
expect(fakeNetEngine.request.calls.count()).toBe(1);
fakeNetEngine.expectRequest(dateUri, dateRequestType);
fakeNetEngine.request.calls.reset();
// Interrupt the parser, then fail the request.
parser.stop();
delay.reject();
return Util.delay(0.1, realTimeout);
}).then(function() {
// Wait for another update period.
return delayForUpdatePeriod();
}).then(function() {
// No more updates should occur.
expect(fakeNetEngine.request).not.toHaveBeenCalled();
}).catch(fail).then(done);
parser.start('dummy://foo', fakeNetEngine, newPeriod, errorCallback)
.then(function(manifest) {
expect(manifest).toBeTruthy();
fakeNetEngine.expectRequest(manifestUri, manifestRequestType);
fakeNetEngine.request.calls.reset();
var delay = fakeNetEngine.delayNextRequest();
return delayForUpdatePeriod().then(function() {
// Resolve the manifest request and wait on the UTCTiming
// request.
expect(fakeNetEngine.request.calls.count()).toBe(1);
fakeNetEngine.expectRequest(manifestUri, manifestRequestType);
fakeNetEngine.request.calls.reset();
delay.resolve();
delay = fakeNetEngine.delayNextRequest();
return Util.delay(0.2, realTimeout);
}).then(function() {
// This is the first UTCTiming request. Reject it so it makes
// the second request.
expect(fakeNetEngine.request.calls.count()).toBe(1);
fakeNetEngine.expectRequest(dateUri, dateRequestType);
fakeNetEngine.request.calls.reset();
delay.reject();
delay = fakeNetEngine.delayNextRequest();
return Util.delay(0.2, realTimeout);
}).then(function() {
expect(fakeNetEngine.request.calls.count()).toBe(1);
fakeNetEngine.expectRequest(dateUri, dateRequestType);
fakeNetEngine.request.calls.reset();
parser.stop();
delay.resolve();
return Util.delay(0.1, realTimeout);
}).then(function() {
// Wait for another update period.
return delayForUpdatePeriod();
}).then(function() {
// A second update should not occur.
expect(fakeNetEngine.request).not.toHaveBeenCalled();
});
})
.catch(fail)
.then(done);
.catch(fail);
});

@@ -706,0 +732,0 @@ });

@@ -489,2 +489,3 @@ /**

' availabilityStartTime="1970-01-01T00:00:00Z"',
' timeShiftBufferDepth="PT60S"',
' maxSegmentDuration="PT5S"',

@@ -491,0 +492,0 @@ ' suggestedPresentationDelay="PT0S">',

@@ -20,2 +20,5 @@ /**

var Dash;
var fakeNetEngine;
var parser;
var filterPeriod = function() {};

@@ -26,2 +29,7 @@ beforeAll(function() {

beforeEach(function() {
fakeNetEngine = new shaka.test.FakeNetworkingEngine();
parser = shaka.test.Dash.makeDashParser();
});
shaka.test.Dash.makeTimelineTests('SegmentList', '', [

@@ -136,2 +144,39 @@ '<SegmentURL media="s1.mp4" />',

describe('presentation timeline', function() {
it('returns correct earliest start time', function(done) {
var source = [
'<MPD>',
' <Period duration="PT60S">',
' <AdaptationSet mimeType="video/webm">',
' <BaseURL>http://example.com</BaseURL>',
' <Representation bandwidth="100">',
' <SegmentList>',
' <SegmentTimeline>',
' <S t="0" d="10" />',
' </SegmentTimeline>',
' <SegmentURL media="1-100.mp4" />',
' </SegmentList>',
' </Representation>',
' <Representation bandwidth="200">',
' <SegmentList>',
' <SegmentTimeline>',
' <S t="4" d="10" />',
' </SegmentTimeline>',
' <SegmentURL media="1-200.mp4" />',
' </SegmentList>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
fakeNetEngine.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', fakeNetEngine, filterPeriod, fail)
.then(function(manifest) {
var timeline = manifest.presentationTimeline;
expect(timeline.getEarliestStart()).toBe(4);
}).catch(fail).then(done);
});
});
describe('rejects streams with', function() {

@@ -138,0 +183,0 @@ it('no @duration or SegmentTimeline', function(done) {

@@ -344,2 +344,37 @@ /**

describe('presentation timeline', function() {
it('returns correct earliest start time', function(done) {
var source = [
'<MPD>',
' <Period duration="PT60S">',
' <AdaptationSet mimeType="video/webm">',
' <BaseURL>http://example.com</BaseURL>',
' <Representation bandwidth="100">',
' <SegmentTemplate media="$Number$-$Bandwidth$.mp4">',
' <SegmentTimeline>',
' <S t="0" d="10" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
' </Representation>',
' <Representation bandwidth="200">',
' <SegmentTemplate media="$Number$-$Bandwidth$.mp4">',
' <SegmentTimeline>',
' <S t="4" d="10" />',
' </SegmentTimeline>',
' </SegmentTemplate>',
' </Representation>',
' </AdaptationSet>',
' </Period>',
'</MPD>'
].join('\n');
fakeNetEngine.setResponseMapAsText({'dummy://foo': source});
parser.start('dummy://foo', fakeNetEngine, filterPeriod, fail)
.then(function(manifest) {
var timeline = manifest.presentationTimeline;
expect(timeline.getEarliestStart()).toBe(4);
}).catch(fail).then(done);
});
});
describe('rejects streams with', function() {

@@ -346,0 +381,0 @@ it('bad container type', function(done) {

@@ -24,3 +24,2 @@ /**

var manifest;
var config;

@@ -106,3 +105,3 @@ var onErrorSpy;

networkingEngine, onErrorSpy, onKeyStatusSpy);
config = {
var config = {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),

@@ -120,26 +119,14 @@ clearKeys: {},

var drmInfos = [{
keySystem: 'com.widevine.alpha',
distinctiveIdentifierRequired: false,
persistentStateRequired: false
},{
keySystem: 'com.microsoft.playready',
distinctiveIdentifierRequired: false,
persistentStateRequired: false
}];
manifest = new shaka.test.ManifestGenerator()
.addPeriod(0)
.addStreamSet('video')
.addDrmInfo('com.widevine.alpha')
.addDrmInfo('com.microsoft.playready')
.addStream(1).mime('video/mp4', 'avc1.640015').encrypted(true)
.addStreamSet('audio')
.addDrmInfo('com.widevine.alpha')
.addDrmInfo('com.microsoft.playready')
.addStream(1).mime('audio/mp4', 'mp4a.40.2').encrypted(true)
.build();
manifest = {
periods: [{
streamSets: [{
type: 'video',
drmInfos: drmInfos,
streams: [{mimeType: 'video/mp4', codecs: 'avc1.640015'}]
},{
type: 'audio',
drmInfos: drmInfos,
streams: [{mimeType: 'audio/mp4', codecs: 'mp4a.40.2'}]
}]
}]
};
eventManager = new shaka.util.EventManager();

@@ -175,13 +162,2 @@

function checkKeySystems() {
// Our test asset for this suite can use any of these key systems:
if (!support['com.widevine.alpha'] &&
!support['com.microsoft.playready']) {
// pending() throws a special exception that Jasmine uses to skip a test.
// It can only be used from inside it(), not describe() or beforeEach().
pending('Skipping DrmEngine tests.');
// The rest of the test will not run.
}
}
describe('basic flow', function() {

@@ -279,3 +255,156 @@ it('gets a license and can play encrypted segments', function(done) {

});
});
}); // describe('basic flow')
describe('missing keys error', function() {
it('fires when manifest PSSH does not match key ID', function(done) {
setBadManifestData();
runMissingKeyTest(done);
});
it('fires even if the license request is delayed', function(done) {
setBadManifestData();
// Delay the license request by 3 seconds.
var originalRequest = networkingEngine.request;
networkingEngine.request = jasmine.createSpy('request');
networkingEngine.request.and.callFake(function(type, request) {
return shaka.test.Util.delay(3).then(function() {
return originalRequest.call(networkingEngine, type, request);
});
});
runMissingKeyTest(done);
});
it('fires when license server returns wrong key ID', function(done) {
// TODO: Update once we get a PlayReady license server that will return
// the wrong key IDs.
if (!support['com.widevine.alpha']) {
pending('Skipping DrmEngine tests.');
}
var config = {
retryParameters: shaka.net.NetworkingEngine.defaultRetryParameters(),
clearKeys: {},
advanced: {},
servers: {
'com.widevine.alpha': '//widevine-proxy.appspot.com/proxy'
}
};
drmEngine.configure(config);
networkingEngine.clearAllRequestFilters();
runMissingKeyTest(done);
});
function setBadManifestData() {
// Override the init data from the media. This is from Axinom's other
// test asset so we get a key response but for the wrong key ID.
manifest = new shaka.test.ManifestGenerator()
.addPeriod(0)
.addStreamSet('video')
.addDrmInfo('com.widevine.alpha')
.addCencInitData(
'AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQFTDToGkE' +
'RGqRoTOhFaqMQQ==')
.addDrmInfo('com.microsoft.playready')
.addCencInitData(
'AAAB5HBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAcTEAQAAAQABALoB' +
'PABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQA' +
'cAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQA' +
'LgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkA' +
'UgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0A' +
'IgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQA' +
'RQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsA' +
'RQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8A' +
'QQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwA' +
'SwBJAEQAPgBvAE4ATQB3AEYAUQBSAHAAYQBrAFMAUgBvAFQATwBoAEYA' +
'YQBxAE0AUQBRAD0APQA8AC8ASwBJAEQAPgA8AC8ARABBAFQAQQA+ADwA' +
'LwBXAFIATQBIAEUAQQBEAEUAUgA+AA==')
.addStream(1).mime('video/mp4', 'avc1.640015').encrypted(true)
.addStreamSet('audio')
.addDrmInfo('com.widevine.alpha')
.addDrmInfo('com.microsoft.playready')
.addStream(1).mime('audio/mp4', 'mp4a.40.2').encrypted(true)
.build();
networkingEngine.clearAllRequestFilters();
networkingEngine.registerRequestFilter(function(type, request) {
if (type != shaka.net.NetworkingEngine.RequestType.LICENSE) return;
request.headers['X-AxDRM-Message'] = [
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V',
'5X2lkIjoiNjllNTQwODgtZTllMC00NTMwLThjMWEtMWViNmRjZDBkMTRlIiwibWV',
'zc2FnZSI6eyJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImtleXMiOlt7Iml',
'kIjoiMTUzMGQzYTAtNjkwNC00NDZhLTkxYTEtMzNhMTE1YWE4YzQxIn0seyJpZCI',
'6ImM4M2ViNjM5LWU2NjQtNDNmOC1hZTk4LTQwMzliMGMxM2IyZCJ9LHsiaWQiOiI',
'zZDhjYzc2Mi0yN2FjLTQwMGYtOTg5Zi04YWI1ZGM3ZDc3NzUifSx7ImlkIjoiYmQ',
'4ZGFkNTgtMDMyZC00YzI1LTg5ZmEtYzdiNzEwZTgyYWMyIn1dfX0.9t18lFmZFVH',
'MzpoZxYDyqOS0Bk_evGhTBw_F2JnAK2k'
].join('');
});
}
function runMissingKeyTest(done) {
checkKeySystems();
// The only error should be key ID not found.
var onErrorCalled = new shaka.util.PublicPromise();
onErrorSpy.and.callFake(function(error) {
onErrorCalled.resolve();
shaka.test.Util.expectToEqualError(
error,
new shaka.util.Error(
shaka.util.Error.Category.DRM,
shaka.util.Error.Code.WRONG_KEYS));
});
eventManager.listen(video, 'error', function() {
fail('MediaError code ' + video.error.code);
var extended = video.error.msExtendedCode;
if (extended) {
if (extended < 0) {
extended += Math.pow(2, 32);
}
fail('MediaError msExtendedCode ' + extended.toString(16));
}
});
drmEngine.init(manifest, /* offline */ false).then(function() {
return drmEngine.attach(video);
}).then(function() {
return mediaSourceEngine.appendBuffer(
'video', videoInitSegment, null, null);
}).then(function() {
return mediaSourceEngine.appendBuffer(
'audio', audioInitSegment, null, null);
}).then(function() {
// waitingforkeys only fires once we are trying to play.
return mediaSourceEngine.appendBuffer(
'video', videoSegment, null, null);
}).then(function() {
return mediaSourceEngine.appendBuffer(
'audio', audioSegment, null, null);
}).then(function() {
video.play();
// Try to play for 6 seconds.
return Promise.race([shaka.test.Util.delay(6), onErrorCalled]);
}).then(function() {
// There should have been an error and the video should not play.
expect(video.currentTime).toBe(0);
expect(onErrorSpy).toHaveBeenCalled();
}).catch(fail).then(done);
}
}); // describe('missing keys')
function checkKeySystems() {
// Our test asset for this suite can use any of these key systems:
if (!support['com.widevine.alpha'] &&
!support['com.microsoft.playready']) {
// pending() throws a special exception that Jasmine uses to skip a test.
// It can only be used from inside it(), not describe() or beforeEach().
pending('Skipping DrmEngine tests.');
// The rest of the test will not run.
}
}
});

@@ -59,27 +59,11 @@ /**

beforeEach(function() {
manifest = {
periods: [{
streamSets: [{
type: 'video',
drmInfos: [{
keySystem: 'drm.abc',
distinctiveIdentifierRequired: false,
persistentStateRequired: false
}],
streams: [
{mimeType: 'video/foo', codecs: 'vbar'}
]
},{
type: 'audio',
drmInfos: [{
keySystem: 'drm.def',
distinctiveIdentifierRequired: false,
persistentStateRequired: false
}],
streams: [
{mimeType: 'audio/foo', codecs: 'abar'}
]
}]
}]
};
manifest = new shaka.test.ManifestGenerator()
.addPeriod(0)
.addStreamSet('video')
.addDrmInfo('drm.abc')
.addStream(0).mime('video/foo', 'vbar').encrypted(true)
.addStreamSet('audio')
.addDrmInfo('drm.def')
.addStream(1).mime('audio/foo', 'abar').encrypted(true)
.build();

@@ -163,2 +147,14 @@ // Reset spies.

it('detects content type capabilities of key system', function(done) {
requestMediaKeySystemAccessSpy.and.callFake(
fakeRequestMediaKeySystemAccess.bind(null, ['drm.abc']));
drmEngine.init(manifest, /* offline */ false).then(function() {
expect(drmEngine.initialized()).toBe(true);
expect(drmEngine.getSupportedTypes()).toEqual([
'audio/webm', 'video/mp4; codecs="fake"'
]);
}).catch(fail).then(done);
});
it('tries the second key system if the first fails', function(done) {

@@ -248,3 +244,3 @@ // Accept drm.def, but not drm.abc.

videoCapabilities: [jasmine.objectContaining({
contentType: 'video/foo'
contentType: 'video/foo; codecs="vbar"'
})],

@@ -258,3 +254,3 @@ distinctiveIdentifier: 'optional',

audioCapabilities: [jasmine.objectContaining({
contentType: 'audio/foo'
contentType: 'audio/foo; codecs="abar"'
})],

@@ -345,6 +341,6 @@ videoCapabilities: undefined,

videoCapabilities: [jasmine.objectContaining({
contentType: 'video/foo'
contentType: 'video/foo; codecs="vbar"'
})],
audioCapabilities: [jasmine.objectContaining({
contentType: 'audio/foo'
contentType: 'audio/foo; codecs="abar"'
})],

@@ -1310,5 +1306,11 @@ distinctiveIdentifier: 'required'

keySystem: '',
getConfiguration: function() {},
getConfiguration: jasmine.createSpy('getConfiguration'),
createMediaKeys: jasmine.createSpy('createMediaKeys')
};
mksa.getConfiguration.and.callFake(function() {
return {
audioCapabilities: [{contentType: 'audio/webm'}],
videoCapabilities: [{contentType: 'video/mp4; codecs="fake"'}]
};
});
mksa.createMediaKeys.and.callFake(function() {

@@ -1315,0 +1317,0 @@ return Promise.resolve(mockMediaKeys);

@@ -90,36 +90,66 @@ /**

it('gives stats about current stream', function(done) {
// This is tested more in player_unit.js. This is here to test the public
// API and to check for renaming.
var asset = '//storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd';
describe('getStats', function() {
it('gives stats about current stream', function(done) {
// This is tested more in player_unit.js. This is here to test the public
// API and to check for renaming.
var asset =
'//storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd';
player.load(asset).then(function() {
video.play();
return waitForEvent(video, 'timeupdate', 10);
}).then(function() {
var stats = player.getStats();
var expected = {
width: jasmine.any(Number),
height: jasmine.any(Number),
streamBandwidth: jasmine.any(Number),
player.load(asset).then(function() {
video.play();
return waitForEvent(video, 'timeupdate', 10);
}).then(function() {
var stats = player.getStats();
var expected = {
width: jasmine.any(Number),
height: jasmine.any(Number),
streamBandwidth: jasmine.any(Number),
decodedFrames: jasmine.any(Number),
droppedFrames: jasmine.any(Number),
estimatedBandwidth: jasmine.any(Number),
playTime: jasmine.any(Number),
bufferingTime: jasmine.any(Number),
decodedFrames: jasmine.any(Number),
droppedFrames: jasmine.any(Number),
estimatedBandwidth: jasmine.any(Number),
playTime: jasmine.any(Number),
bufferingTime: jasmine.any(Number),
// We should have loaded the first Period by now, so we should have a
// history.
switchHistory: jasmine.arrayContaining([{
timestamp: jasmine.any(Number),
id: jasmine.any(Number),
type: 'video',
fromAdaptation: true
}])
};
expect(stats).toEqual(expected);
}).catch(fail).then(done);
// We should have loaded the first Period by now, so we should have a
// history.
switchHistory: jasmine.arrayContaining([{
timestamp: jasmine.any(Number),
id: jasmine.any(Number),
type: 'video',
fromAdaptation: true
}])
};
expect(stats).toEqual(expected);
}).catch(fail).then(done);
});
});
describe('setTextTrackVisibility', function() {
// Using mode='disabled' on TextTrack causes cues to go null, which leads
// to a crash in TextEngine. This validates that we do not trigger this
// behavior when changing visibility of text.
it('does not cause cues to be null', function(done) {
var asset =
'//storage.googleapis.com/shaka-demo-assets/angel-one/dash.mpd';
var textTrack = video.textTracks[0];
player.load(asset).then(function() {
video.play();
return waitForEvent(video, 'timeupdate', 10);
}).then(function() {
// This should not be null initially.
expect(textTrack.cues).not.toBe(null);
player.setTextTrackVisibility(true);
// This should definitely not be null when visible.
expect(textTrack.cues).not.toBe(null);
player.setTextTrackVisibility(false);
// This should not transition to null when invisible.
expect(textTrack.cues).not.toBe(null);
}).catch(fail).then(done);
});
});
describe('plays', function() {

@@ -157,2 +187,4 @@ window.shakaAssets.testAssets.forEach(function(asset) {

config.abr.enabled = false;
config.manifest.dash.clockSyncUri =
'//shaka-player-demo.appspot.com/time.txt';
if (asset.licenseServers)

@@ -186,3 +218,3 @@ config.drm.servers = asset.licenseServers;

if (video.ended) {
expect(video.currentTime).toBeCloseTo(video.duration);
expect(video.currentTime).toBeCloseTo(video.duration, 0.1);
} else {

@@ -199,3 +231,3 @@ expect(video.currentTime).toBeGreaterThan(20);

expect(video.ended).toBe(true);
expect(video.currentTime).toBeCloseTo(video.duration);
expect(video.currentTime).toBeCloseTo(video.duration, 0.1);
});

@@ -202,0 +234,0 @@ }

@@ -500,3 +500,4 @@ /**

var stream = period.streamSets[0].streams[1];
expect(streamingEngine.switch).toHaveBeenCalledWith('audio', stream);
expect(streamingEngine.switch)
.toHaveBeenCalledWith('audio', stream, false);
});

@@ -798,2 +799,169 @@ });

describe('restrictions', function() {
beforeEach(function(done) {
manifest = new shaka.test.ManifestGenerator()
.addPeriod(0)
.addStreamSet('audio')
.addStream(1).bandwidth(500)
.addStream(2).disallowByKeySystem()
.addStream(3).bandwidth(1500)
.addStreamSet('video')
.addStream(4).size(100, 100).bandwidth(300).keyId('abc')
.addStream(5).size(200, 1500)
.addStream(6).size(5, 5)
.addStream(7).size(100, 100).bandwidth(1500)
.addStream(8).size(1500, 200)
.addStream(9).size(900, 900)
.addStream(10).size(100, 100).bandwidth(10)
.addStream(11).bandwidth(200).mime('video/webm')
.build();
var factory = shaka.test.FakeManifestParser.createFactory(manifest);
player.load('', 0, factory).then(function() {
// "initialize" the current period.
chooseStreams(manifest.periods[0]);
canSwitch();
}).catch(fail).then(done);
});
it('switches if active is restricted by application', function() {
var activeVideo = getActiveTrack('video');
expect(activeVideo.id).toBe(4);
// AbrManager should choose the second track since the first is
// restricted.
abrManager.chooseIndex = 1;
abrManager.chooseStreams.calls.reset();
player.configure({restrictions: {maxVideoBandwidth: 200}});
expect(abrManager.chooseStreams).toHaveBeenCalled();
expect(manifest.periods[0].streamSets[1].streams[0].id).toBe(4);
expect(manifest.periods[0].streamSets[1].streams[0].allowedByApplication)
.toBe(false);
activeVideo = getActiveTrack('video');
expect(activeVideo.id).toBe(5);
});
it('switches if active is restricted by key status', function() {
var activeVideo = getActiveTrack('video');
expect(activeVideo.id).toBe(4);
// AbrManager should choose the second track since the first is
// restricted.
abrManager.chooseIndex = 1;
abrManager.chooseStreams.calls.reset();
onKeyStatus({'abc': 'expired'});
expect(abrManager.chooseStreams).toHaveBeenCalled();
expect(manifest.periods[0].streamSets[1].streams[0].id).toBe(4);
expect(manifest.periods[0].streamSets[1].streams[0].allowedByKeySystem)
.toBe(false);
activeVideo = getActiveTrack('video');
expect(activeVideo.id).toBe(5);
});
it('removes based on key status', function() {
expect(player.getTracks().length).toBe(9);
onKeyStatus({'abc': 'expired'});
var tracks = player.getTracks();
expect(tracks.length).toBe(8);
expectDoesNotInclude(tracks, 4);
});
it('removes if key system does not support codec', function() {
// Should already be removed from filterPeriod_
var tracks = player.getTracks();
expect(tracks.length).toBe(9);
expectDoesNotInclude(tracks, 11);
});
it('removes based on bandwidth', function() {
player.configure(
{restrictions: {minVideoBandwidth: 100, maxVideoBandwidth: 1000}});
var tracks = player.getTracks();
expect(tracks.length).toBe(7);
expectDoesNotInclude(tracks, 7);
expectDoesNotInclude(tracks, 10);
});
it('removes based on pixels', function() {
player.configure({restrictions: {minPixels: 100, maxPixels: 800 * 800}});
var tracks = player.getTracks();
expect(tracks.length).toBe(7);
expectDoesNotInclude(tracks, 6);
expectDoesNotInclude(tracks, 9);
});
it('removes based on width', function() {
player.configure({restrictions: {minWidth: 100, maxWidth: 1000}});
var tracks = player.getTracks();
expect(tracks.length).toBe(7);
expectDoesNotInclude(tracks, 6);
expectDoesNotInclude(tracks, 8);
});
it('removes based on height', function() {
player.configure({restrictions: {minHeight: 100, maxHeight: 1000}});
var tracks = player.getTracks();
expect(tracks.length).toBe(7);
expectDoesNotInclude(tracks, 5);
expectDoesNotInclude(tracks, 6);
});
it('issues error if no streams are playable', function() {
var onError = jasmine.createSpy('error event');
onError.and.callFake(function(e) {
var error = e.detail;
shaka.test.Util.expectToEqualError(
error,
new shaka.util.Error(
shaka.util.Error.Category.MANIFEST,
shaka.util.Error.Code.ALL_STREAMS_RESTRICTED));
});
player.addEventListener('error', onError);
player.configure(
{restrictions: {maxAudioBandwidth: 0, maxVideoBandwidth: 0}});
expect(onError).toHaveBeenCalled();
});
/**
* Gets the currently active track.
* @param {string} type
* @return {shakaExtern.Track}
*/
function getActiveTrack(type) {
var activeTracks = player.getTracks().filter(function(track) {
return track.type == type && track.active;
});
expect(activeTracks.length).toBe(1);
return activeTracks[0];
}
/**
* Expects that the given track list does not include a track with the
* given ID.
* @param {!Array.<shakaExtern.Track>} tracks
* @param {number} id
*/
function expectDoesNotInclude(tracks, id) {
var containsId = tracks.some(function(track) { return track.id == id; });
expect(containsId).toBe(false);
}
/**
* @param {!Object.<string, string>} keyStatusMap
* @suppress {accessControls}
*/
function onKeyStatus(keyStatusMap) {
player.onKeyStatus_(keyStatusMap);
}
});
/**

@@ -800,0 +968,0 @@ * @param {!Object=} opt_period

@@ -29,2 +29,8 @@ /**

// Callback to Playhead to simulate 'playing' event from |video|.
var videoOnPlaying;
// Callback to Playhead to simulate 'ratechange' event from |video|.
var videoOnRateChange;
// Callback to us from Playhead when the buffering state changes.

@@ -42,2 +48,3 @@ var onBuffering;

videoOnSeeking = undefined;
videoOnPlaying = undefined;

@@ -52,2 +59,6 @@ onBuffering = jasmine.createSpy('onBuffering');

videoOnSeeking = f;
} else if (eventName == 'playing') {
videoOnPlaying = f;
} else if (eventName == 'ratechange') {
videoOnRateChange = f;
} else {

@@ -58,2 +69,4 @@ throw new Error('Unexpected event:' + eventName);

timeline.isLive.and.returnValue(false);
timeline.getEarliestStart.and.returnValue(5);
timeline.getSegmentAvailabilityStart.and.returnValue(5);

@@ -84,3 +97,3 @@ timeline.getSegmentAvailabilityEnd.and.returnValue(60);

'loadedmetadata', videoOnLoadedMetadata, false);
expect(video.addEventListener.calls.count()).toBe(1);
expect(video.addEventListener.calls.count()).toBe(2);

@@ -95,3 +108,3 @@ expect(playhead.getTime()).toBe(5);

'seeking', videoOnSeeking, false);
expect(video.addEventListener.calls.count()).toBe(2);
expect(video.addEventListener.calls.count()).toBe(3);

@@ -141,2 +154,3 @@ expect(playhead.getTime()).toBe(5);

video.playbackRate = 2;
videoOnRateChange();

@@ -162,3 +176,3 @@ playhead.setBuffering(false);

it('clamps seeks for live', function() {
it('clamps playhead after seeking for live', function() {
video.readyState = HTMLMediaElement.HAVE_METADATA;

@@ -178,2 +192,4 @@

timeline.isLive.and.returnValue(true);
timeline.getEarliestStart.and.returnValue(5);
timeline.getSegmentAvailabilityStart.and.returnValue(5);

@@ -190,5 +206,2 @@ timeline.getSegmentAvailabilityEnd.and.returnValue(60);

expect(playhead.getTime()).toBe(5);
expect(video.currentTime).toBe(5);
// Calling videoOnSeeking() is like dispatching a 'seeking' event. So, each

@@ -198,2 +211,6 @@ // time we change the video's current time or Playhead changes the video's

videoOnSeeking();
expect(video.currentTime).toBe(5);
expect(playhead.getTime()).toBe(5);
// left = start + 1 = 5 + 1 = 6

@@ -317,3 +334,3 @@ // safe = left + rebufferingGoal = 6 + 10 = 16

it('clamps seeks for VOD', function() {
it('clamps playhead after seeking for VOD', function() {
video.readyState = HTMLMediaElement.HAVE_METADATA;

@@ -333,2 +350,4 @@

timeline.isLive.and.returnValue(false);
timeline.getEarliestStart.and.returnValue(5);
timeline.getSegmentAvailabilityStart.and.returnValue(5);

@@ -345,4 +364,5 @@ timeline.getSegmentAvailabilityEnd.and.returnValue(60);

videoOnSeeking();
expect(video.currentTime).toBe(5);
expect(playhead.getTime()).toBe(5);
expect(video.currentTime).toBe(5);

@@ -370,2 +390,79 @@ // Seek past end.

it('clamps playhead after resuming', function() {
video.readyState = HTMLMediaElement.HAVE_METADATA;
video.buffered = {
length: 1,
start: function(i) {
if (i == 0) return 5;
throw new Error('Unexpected index');
},
end: function(i) {
if (i == 0) return 35;
throw new Error('Unexpected index');
}
};
// Live case:
timeline.isLive.and.returnValue(true);
timeline.getEarliestStart.and.returnValue(5);
timeline.getSegmentAvailabilityStart.and.returnValue(5);
timeline.getSegmentAvailabilityEnd.and.returnValue(60);
timeline.getSegmentAvailabilityDuration.and.returnValue(30);
playhead = new shaka.media.Playhead(
video,
timeline,
10 /* rebufferingGoal */,
5 /* startTime */,
onBuffering, onSeek);
videoOnSeeking();
expect(video.currentTime).toBe(5);
expect(playhead.getTime()).toBe(5);
// Simulate pausing.
timeline.getEarliestStart.and.returnValue(10);
timeline.getSegmentAvailabilityStart.and.returnValue(10);
timeline.getSegmentAvailabilityEnd.and.returnValue(70);
timeline.getSegmentAvailabilityDuration.and.returnValue(30);
// left = start + 1 = 10 + 1 = 11
// safe = left + rebufferingGoal = 11 + 10 = 21
// The playhead should move to 23 (safe + 2) after resuming, which will
// cause a 'seeking' event.
videoOnPlaying();
expect(video.currentTime).toBe(23);
videoOnSeeking();
expect(playhead.getTime()).toBe(23);
expect(onSeek).toHaveBeenCalled();
// VOD case:
timeline.isLive.and.returnValue(false);
timeline.getEarliestStart.and.returnValue(5);
timeline.getSegmentAvailabilityStart.and.returnValue(5);
timeline.getSegmentAvailabilityEnd.and.returnValue(60);
timeline.getSegmentAvailabilityDuration.and.returnValue(30);
playhead = new shaka.media.Playhead(
video,
timeline,
10 /* rebufferingGoal */,
5 /* startTime */,
onBuffering, onSeek);
// Simulate pausing.
timeline.getEarliestStart.and.returnValue(10);
timeline.getSegmentAvailabilityStart.and.returnValue(10);
timeline.getSegmentAvailabilityEnd.and.returnValue(70);
timeline.getSegmentAvailabilityDuration.and.returnValue(30);
videoOnPlaying();
expect(video.currentTime).toBe(10);
videoOnSeeking();
expect(playhead.getTime()).toBe(10);
expect(onSeek).toHaveBeenCalled();
});
function createMockVideo() {

@@ -389,2 +486,4 @@ return {

jasmine.createSpy('getSegmentAvailabilityDuration'),
isLive: jasmine.createSpy('isLive'),
getEarliestStart: jasmine.createSpy('getEarliestStart'),
getSegmentAvailabilityStart:

@@ -391,0 +490,0 @@ jasmine.createSpy('getSegmentAvailabilityStart'),

@@ -21,2 +21,3 @@ /**

var baseTime;
var infinity;

@@ -27,2 +28,3 @@ beforeEach(function() {

Date.now = function() { return baseTime.getTime(); };
infinity = Number.POSITIVE_INFINITY;
});

@@ -40,12 +42,37 @@

/**
* Creates a PresentationTimeline.
*
* @param {number} duration
* @param {?number} presentationStartTime
* @param {number} segmentAvailabilityDuration
* @param {number} maxSegmentDuration
* @param {number} clockOffset
*
* @return {shaka.media.PresentationTimeline}
*/
function makePresentationTimeline(
duration,
presentationStartTime,
segmentAvailabilityDuration,
maxSegmentDuration,
clockOffset) {
var timeline = new shaka.media.PresentationTimeline(presentationStartTime);
timeline.setDuration(duration || infinity);
timeline.setSegmentAvailabilityDuration(segmentAvailabilityDuration);
timeline.notifyMaxSegmentDuration(maxSegmentDuration);
timeline.setClockOffset(clockOffset);
return timeline;
}
describe('getSegmentAvailabilityStart', function() {
it('returns 0 for VOD', function() {
setElapsed(0);
var timeline1 = new shaka.media.PresentationTimeline(
60, null, null, 10, 0);
var timeline1 = makePresentationTimeline(
60, null, infinity, 10, 0);
expect(timeline1.getSegmentAvailabilityStart()).toBe(0);
setElapsed(0);
var timeline2 = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, null, null, 10, 0);
var timeline2 = makePresentationTimeline(
infinity, null, infinity, 10, 0);
expect(timeline2.getSegmentAvailabilityStart()).toBe(0);

@@ -56,4 +83,4 @@ });

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, Date.now() / 1000.0, 20, 10, 0);
var timeline = makePresentationTimeline(
infinity, Date.now() / 1000.0, 20, 10, 0);
expect(timeline.getSegmentAvailabilityStart()).toBe(0);

@@ -82,3 +109,3 @@

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
var timeline = makePresentationTimeline(
60, Date.now() / 1000.0, 20, 10, 0);

@@ -108,4 +135,4 @@ expect(timeline.getSegmentAvailabilityStart()).toBe(0);

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
60, Date.now() / 1000.0, Number.POSITIVE_INFINITY, 10, 0);
var timeline = makePresentationTimeline(
60, Date.now() / 1000.0, infinity, 10, 0);
expect(timeline.getSegmentAvailabilityStart()).toBe(0);

@@ -127,11 +154,10 @@

setElapsed(0);
var timeline1 = new shaka.media.PresentationTimeline(
60, null, null, 10, 0);
var timeline1 = makePresentationTimeline(
60, null, infinity, 10, 0);
expect(timeline1.getSegmentAvailabilityEnd()).toBe(60);
setElapsed(0);
var timeline2 = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, null, null, 10, 0);
expect(timeline2.getSegmentAvailabilityEnd()).toBe(
Number.POSITIVE_INFINITY);
var timeline2 = makePresentationTimeline(
infinity, null, infinity, 10, 0);
expect(timeline2.getSegmentAvailabilityEnd()).toBe(infinity);
});

@@ -141,4 +167,4 @@

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, Date.now() / 1000.0, 20, 10, 0);
var timeline = makePresentationTimeline(
infinity, Date.now() / 1000.0, 20, 10, 0);
expect(timeline.getSegmentAvailabilityEnd()).toBe(0);

@@ -167,3 +193,3 @@

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
var timeline = makePresentationTimeline(
60, Date.now() / 1000.0, 20, 10, 0);

@@ -195,9 +221,9 @@ expect(timeline.getSegmentAvailabilityEnd()).toBe(0);

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
60, null, null, 10, 0);
var timeline = makePresentationTimeline(
60, null, infinity, 10, 0);
expect(timeline.getDuration()).toBe(60);
timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, null, null, 10, 0);
expect(timeline.getDuration()).toBe(Number.POSITIVE_INFINITY);
timeline = makePresentationTimeline(
infinity, null, infinity, 10, 0);
expect(timeline.getDuration()).toBe(infinity);
});

@@ -207,9 +233,9 @@

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
var timeline = makePresentationTimeline(
60, Date.now() / 1000.0, 20, 10, 0);
expect(timeline.getDuration()).toBe(60);
timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, Date.now() / 1000.0, 20, 10, 0);
expect(timeline.getDuration()).toBe(Number.POSITIVE_INFINITY);
timeline = makePresentationTimeline(
infinity, Date.now() / 1000.0, 20, 10, 0);
expect(timeline.getDuration()).toBe(infinity);
});

@@ -221,4 +247,4 @@ });

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
60, null, null, 10, 0);
var timeline = makePresentationTimeline(
60, null, infinity, 10, 0);
expect(timeline.getSegmentAvailabilityEnd()).toBe(60);

@@ -232,4 +258,4 @@

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, Date.now() / 1000.0, 20, 10, 0);
var timeline = makePresentationTimeline(
infinity, Date.now() / 1000.0, 20, 10, 0);

@@ -249,39 +275,35 @@ setElapsed(40);

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(60, null, null, 10, 0);
expect(timeline.getSegmentAvailabilityDuration()).toBeNull();
var timeline = makePresentationTimeline(
60, null, infinity, 10, 0);
expect(timeline.getSegmentAvailabilityDuration()).toBe(infinity);
timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, Date.now() / 1000.0, 20, 10, 0);
timeline = makePresentationTimeline(
infinity, Date.now() / 1000.0, 20, 10, 0);
expect(timeline.getSegmentAvailabilityDuration()).toBe(20);
timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY,
Date.now() / 1000.0,
Number.POSITIVE_INFINITY,
10,
0);
expect(timeline.getSegmentAvailabilityDuration()).toBe(
Number.POSITIVE_INFINITY);
timeline = makePresentationTimeline(
infinity, Date.now() / 1000.0, infinity, 10, 0);
expect(timeline.getSegmentAvailabilityDuration()).toBe(infinity);
});
it('setSegmentAvailabiliyDuration', function() {
it('setSegmentAvailabilityDuration', function() {
setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(60, null, null, 10, 0);
expect(timeline.getSegmentAvailabilityDuration()).toBeNull();
var timeline = makePresentationTimeline(
60, null, infinity, 10, 0);
expect(timeline.getSegmentAvailabilityDuration()).toBe(infinity);
timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, Date.now() / 1000.0, 20, 10, 0);
timeline.setSegmentAvailabiliyDuration(7);
timeline = makePresentationTimeline(
infinity, Date.now() / 1000.0, 20, 10, 0);
timeline.setSegmentAvailabilityDuration(7);
expect(timeline.getSegmentAvailabilityDuration()).toBe(7);
timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, Date.now() / 1000.0, 20, 10, 0);
timeline.setSegmentAvailabiliyDuration(Number.POSITIVE_INFINITY);
expect(timeline.getSegmentAvailabilityDuration()).toBe(
Number.POSITIVE_INFINITY);
timeline = makePresentationTimeline(
infinity, Date.now() / 1000.0, 20, 10, 0);
timeline.setSegmentAvailabilityDuration(infinity);
expect(timeline.getSegmentAvailabilityDuration()).toBe(infinity);
timeline = new shaka.media.PresentationTimeline(
60, null, null, 10, 0);
timeline.setSegmentAvailabiliyDuration(null);
expect(timeline.getSegmentAvailabilityDuration()).toBe(null);
timeline = makePresentationTimeline(
60, null, infinity, 10, 0);
timeline.setSegmentAvailabilityDuration(infinity);
expect(timeline.getSegmentAvailabilityDuration()).toBe(infinity);
});

@@ -293,4 +315,4 @@

setElapsed(0);
var timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY, Date.now() / 1000.0, 10, 5, 10000);
var timeline = makePresentationTimeline(
infinity, Date.now() / 1000.0, 10, 5, 10000);
expect(timeline.getSegmentAvailabilityEnd()).toBeCloseTo(5);

@@ -297,0 +319,0 @@ });

@@ -241,3 +241,3 @@ /**

makeReference(2, 20, 30, uri(20)),
makeReference(3, 30, 49.987, uri(30))
makeReference(3, 30, 49.887, uri(30))
];

@@ -244,0 +244,0 @@ var index1 = new shaka.media.SegmentIndex(references1);

@@ -43,21 +43,22 @@ /**

// Keep unsorted.
audioStreamSet = /** @type {shakaExtern.StreamSet} */({
streams: /** @type {!Array.<shakaExtern.Stream>} */([
{bandwidth: 4e5}, // 400 kbps
{bandwidth: 6e5},
{bandwidth: 5e5},
{bandwidth: null}
])
});
audioStreamSet = new shaka.test.ManifestGenerator()
.addPeriod(0)
.addStreamSet('audio')
.addStream(0).bandwidth(4e5) // 400 kbps
.addStream(1).bandwidth(6e5)
.addStream(2).bandwidth(5e5)
.build()
.periods[0].streamSets[0];
// Keep unsorted.
videoStreamSet = /** @type {shakaExtern.StreamSet} */({
streams: /** @type {!Array.<shakaExtern.Stream>} */([
{bandwidth: 5e5}, // 500 kbps, initial selection
{bandwidth: 1e6}, // 1000 kbps
{bandwidth: 3e6},
{bandwidth: 2e6},
{bandwidth: 2e6} // Identical on purpose.
])
});
videoStreamSet = new shaka.test.ManifestGenerator()
.addPeriod(0)
.addStreamSet('video')
.addStream(1).bandwidth(5e5) // 500 kbps, initial selection
.addStream(2).bandwidth(1e6) // 1000 kbps
.addStream(3).bandwidth(3e6)
.addStream(4).bandwidth(2e6)
.addStream(5).bandwidth(2e6) // Identical on purpose.
.build()
.periods[0].streamSets[0];

@@ -108,2 +109,16 @@ streamSetsByType = {

it('won\'t choose restricted streams', function() {
// No available audio streams.
audioStreamSet.streams[0].allowedByKeySystem = false;
audioStreamSet.streams[1].allowedByApplication = false;
audioStreamSet.streams[2].allowedByApplication = false;
// Disallow the initial guess.
videoStreamSet.streams[0].allowedByApplication = false;
var streamsByType = abrManager.chooseStreams(streamSetsByType);
expect(streamsByType['audio']).toBeFalsy();
expect(streamsByType['video']).toBeTruthy();
expect(streamsByType['video'].bandwidth).toBe(1e6);
});
it('can handle empty StreamSets', function() {

@@ -140,6 +155,6 @@ var streamsByType = abrManager.chooseStreams({});

expect(switchCallback).toHaveBeenCalledWith(jasmine.objectContaining({
'audio': {bandwidth: audioBandwidth},
'video': {bandwidth: videoBandwidth}
}));
expect(switchCallback).toHaveBeenCalledWith({
'audio': jasmine.objectContaining({bandwidth: audioBandwidth}),
'video': jasmine.objectContaining({bandwidth: videoBandwidth})
});
}).catch(fail).then(done);

@@ -194,6 +209,6 @@ });

expect(switchCallback).toHaveBeenCalledWith(jasmine.objectContaining({
'audio': {bandwidth: audioBandwidth},
'video': {bandwidth: videoBandwidth}
}));
expect(switchCallback).toHaveBeenCalledWith({
'audio': jasmine.objectContaining({bandwidth: audioBandwidth}),
'video': jasmine.objectContaining({bandwidth: videoBandwidth})
});
}).catch(fail).then(done);

@@ -245,6 +260,6 @@ });

expect(switchCallback).toHaveBeenCalledWith(jasmine.objectContaining({
'audio': {bandwidth: audioBandwidth},
'video': {bandwidth: videoBandwidth}
}));
expect(switchCallback).toHaveBeenCalledWith({
'audio': jasmine.objectContaining({bandwidth: audioBandwidth}),
'video': jasmine.objectContaining({bandwidth: videoBandwidth})
});
}).catch(fail).then(done);

@@ -251,0 +266,0 @@ });

@@ -443,5 +443,6 @@ /**

// [(segmentAvailabilityEnd - rebufferingGoal) / segmentDuration] + 1
// Then -1 to account for drift safe buffering.
var segmentType = shaka.net.NetworkingEngine.RequestType.SEGMENT;
netEngine.expectRequest('1_video_24', segmentType);
netEngine.expectRequest('1_audio_29', segmentType);
netEngine.expectRequest('1_video_23', segmentType);
netEngine.expectRequest('1_audio_28', segmentType);
video.play();

@@ -468,5 +469,9 @@ });

// Seek outside the availability window right away. The playhead
// should adjust the video's current time.
video.currentTime = timeline.segmentAvailabilityEnd + 120;
// Use setTimeout to ensure the playhead has performed it's initial
// seeking.
setTimeout(function() {
// Seek outside the availability window right away. The playhead
// should adjust the video's current time.
video.currentTime = timeline.segmentAvailabilityEnd + 120;
}, 50);
});

@@ -492,6 +497,10 @@

// Seek outside the availability window right away. The playhead
// should adjust the video's current time.
video.currentTime = timeline.segmentAvailabilityStart - 120;
expect(video.currentTime).toBeGreaterThan(0);
// Use setTimeout to ensure the playhead has performed it's initial
// seeking.
setTimeout(function() {
// Seek outside the availability window right away. The playhead
// should adjust the video's current time.
video.currentTime = timeline.segmentAvailabilityStart - 120;
expect(video.currentTime).toBeGreaterThan(0);
}, 50);
});

@@ -498,0 +507,0 @@

@@ -534,5 +534,5 @@ /**

// Since we started playback from segment 11, the first 10 segments
// should not be buffered.
for (var i = 0; i <= 9; ++i) {
// Since we started playback from segment 11, segments 10 through 14
// should be buffered.
for (var i = 0; i <= 8; ++i) {
expect(mediaSourceEngine.segments.audio[i]).toBeFalsy();

@@ -543,3 +543,3 @@ expect(mediaSourceEngine.segments.video[i]).toBeFalsy();

for (var i = 10; i <= 13; ++i) {
for (var i = 9; i <= 13; ++i) {
expect(mediaSourceEngine.segments.audio[i]).toBeTruthy();

@@ -871,5 +871,5 @@ expect(mediaSourceEngine.segments.video[i]).toBeTruthy();

expect(mediaSourceEngine.segments).toEqual({
audio: [false, true, false, false],
video: [false, true, false, false],
text: [false, true, false, false]
audio: [true, true, false, false],
video: [true, true, false, false],
text: [true, true, false, false]
});

@@ -897,5 +897,5 @@

expect(mediaSourceEngine.segments).toEqual({
audio: [false, true, true, true],
video: [false, true, true, true],
text: [false, true, true, true]
audio: [true, true, true, true],
video: [true, true, true, true],
text: [true, true, true, true]
});

@@ -962,5 +962,5 @@

expect(mediaSourceEngine.segments).toEqual({
audio: [false, true, false, false],
video: [false, true, false, false],
text: [false, true, false, false]
audio: [true, true, false, false],
video: [true, true, false, false],
text: [true, true, false, false]
});

@@ -993,5 +993,5 @@

expect(mediaSourceEngine.segments).toEqual({
audio: [false, true, true, true],
video: [false, true, true, true],
text: [false, true, true, true]
audio: [true, true, true, true],
video: [true, true, true, true],
text: [true, true, true, true]
});

@@ -1033,5 +1033,5 @@

expect(mediaSourceEngine.segments).toEqual({
audio: [false, true, false, false],
video: [false, true, false, false],
text: [false, true, false, false]
audio: [true, true, false, false],
video: [true, true, false, false],
text: [true, true, false, false]
});

@@ -1063,5 +1063,5 @@

expect(mediaSourceEngine.segments).toEqual({
audio: [false, true, true, true],
video: [false, true, true, true],
text: [false, true, true, true]
audio: [true, true, true, true],
video: [true, true, true, true],
text: [true, true, true, true]
});

@@ -1111,5 +1111,5 @@

expect(mediaSourceEngine.segments).toEqual({
audio: [false, false, false, true],
video: [false, false, false, true],
text: [false, false, false, true]
audio: [false, false, true, true],
video: [false, false, true, true],
text: [false, false, true, true]
});

@@ -1183,4 +1183,7 @@

it('outside segment availability window', function(done) {
playhead.getTime.and.returnValue(100);
timeline.segmentAvailabilityStart = 90;
timeline.segmentAvailabilityEnd = 110;
playhead.getTime.and.returnValue(90);
onChooseStreams.and.callFake(function(period) {

@@ -1196,12 +1199,11 @@ expect(period).toBe(manifest.periods[0]);

onStartupComplete.and.callFake(function() {
setupFakeGetTime(100);
setupFakeGetTime(90);
// Seek forward to an unbuffered and unavailable region in the second
// Period (note: |availabilityEnd| defaults to 120).
// Set playing to false since the playhead can't move at the seek
// target.
expect(playhead.getTime()).toBe(100);
expect(timeline.getSegmentAvailabilityStart()).toBe(100);
expect(timeline.getSegmentAvailabilityEnd()).toBe(120);
playheadTime += 25;
// Period; set playing to false since the playhead can't move at the
// seek target.
expect(playhead.getTime()).toBe(90);
expect(timeline.getSegmentAvailabilityStart()).toBe(90);
expect(timeline.getSegmentAvailabilityEnd()).toBe(110);
playheadTime += 35;
playing = false;

@@ -1219,4 +1221,5 @@ streamingEngine.seeked();

// Eventually StreamingEngine should request the first segment of the
// second Period when it becomes available.
// Eventually StreamingEngine should request the first segment (since
// it needs the second segment) of the second Period when it becomes
// available.
var originalAppendBuffer =

@@ -1227,4 +1230,4 @@ shaka.test.FakeMediaSourceEngine.prototype.appendBuffer;

expect(playhead.getTime()).toBe(125);
expect(timeline.getSegmentAvailabilityStart()).toBe(105);
expect(timeline.getSegmentAvailabilityEnd()).toBe(125);
expect(timeline.getSegmentAvailabilityStart()).toBe(100);
expect(timeline.getSegmentAvailabilityEnd()).toBe(120);
playing = true;

@@ -1491,3 +1494,2 @@ var p = originalAppendBuffer.call(

// TODO: Add tests for eviction with drift.
describe('VOD drift', function() {

@@ -1604,10 +1606,2 @@ beforeEach(function() {

// Verify buffers.
// Note: after appending segment 11 of the first Period,
// StreamingEngine should clear the buffer and then append segment 12
// of the first Period as the playhead is under that Segment when
// accounting for drift. When transitioning into the second Period,
// StreamingEngine should append the second segment of the second
// Period. It should not append the first segment of the second Period
// since that segment exists entirely outside the second Period's
// boundaries.
expect(mediaSourceEngine.initSegments).toEqual({

@@ -1619,3 +1613,3 @@ audio: [false, true],

for (var i = 0; i <= 10; ++i) {
for (var i = 0; i <= 8; ++i) {
expect(mediaSourceEngine.segments.audio[i]).toBeFalsy();

@@ -1626,15 +1620,8 @@ expect(mediaSourceEngine.segments.video[i]).toBeFalsy();

// Second segment of first Period.
expect(mediaSourceEngine.segments.audio[11]).toBeTruthy();
expect(mediaSourceEngine.segments.video[11]).toBeTruthy();
expect(mediaSourceEngine.segments.text[11]).toBeTruthy();
for (var i = 9; i <= 13; ++i) {
expect(mediaSourceEngine.segments.audio[i]).toBeTruthy();
expect(mediaSourceEngine.segments.video[i]).toBeTruthy();
expect(mediaSourceEngine.segments.text[i]).toBeTruthy();
}
expect(mediaSourceEngine.segments.audio[12]).toBeFalsy();
expect(mediaSourceEngine.segments.video[12]).toBeFalsy();
expect(mediaSourceEngine.segments.text[12]).toBeFalsy();
expect(mediaSourceEngine.segments.audio[13]).toBeTruthy();
expect(mediaSourceEngine.segments.video[13]).toBeTruthy();
expect(mediaSourceEngine.segments.text[13]).toBeTruthy();
return streamingEngine.destroy();

@@ -1641,0 +1628,0 @@ }).catch(fail).then(done);

@@ -36,5 +36,5 @@ /**

// Unicode characters where the last is split into a surrogate pair.
var arr = [0x00, 0x46, 0x38, 0x01, 0xdc, 0x37, 0x5d, 0x00];
var arr = [0x00, 0x46, 0x38, 0x01, 0xd8, 0x01, 0xdc, 0x37];
var buffer = new Uint8Array(arr).buffer;
expect(StringUtils.fromUTF16(buffer, false)).toBe('F\u3801\udc37\u5d00');
expect(StringUtils.fromUTF16(buffer, false)).toBe('F\u3801\ud801\udc37');
});

@@ -45,5 +45,5 @@

// Unicode characters where the last is split into a surrogate pair.
var arr = [0x46, 0x00, 0x01, 0x38, 0x37, 0xdc, 0x00, 0x5d];
var arr = [0x46, 0x00, 0x01, 0x38, 0x01, 0xd8, 0x37, 0xdc];
var buffer = new Uint8Array(arr).buffer;
expect(StringUtils.fromUTF16(buffer, true)).toBe('F\u3801\udc37\u5d00');
expect(StringUtils.fromUTF16(buffer, true)).toBe('F\u3801\ud801\udc37');
});

@@ -107,2 +107,9 @@

});
it('does not cause stack overflow, #335', function() {
var buffer = new Uint8Array(8e5).buffer; // Well above arg count limit.
expect(StringUtils.fromUTF8(buffer).length).toBe(buffer.byteLength);
expect(StringUtils.fromUTF16(buffer, true).length)
.toBe(buffer.byteLength / 2);
});
});

@@ -30,3 +30,6 @@ /**

retryParameters: retry,
dash: { customScheme: function(node) { return null; } }
dash: {
customScheme: function(node) { return null; },
clockSyncUri: ''
}
});

@@ -33,0 +36,0 @@ return parser;

@@ -32,8 +32,6 @@ /**

shaka.test.ManifestGenerator = function() {
var timeline = new shaka.media.PresentationTimeline(
Number.POSITIVE_INFINITY /* duration */,
0 /* presentationStartTime */,
Number.POSITIVE_INFINITY /* segmentAvailabilityDuration */,
10 /* maxSegmentDuration */,
0 /* clockOffset */);
var timeline = new shaka.media.PresentationTimeline(0);
timeline.setSegmentAvailabilityDuration(Number.POSITIVE_INFINITY);
timeline.notifyMaxSegmentDuration(10);
/** @private {shakaExtern.Manifest} */

@@ -246,5 +244,24 @@ this.manifest_ = {

var drmInfo = this.currentDrmInfo_();
if (!drmInfo.initData)
drmInfo.initData = [];
drmInfo.initData.push({initData: buffer, initDataType: type});
return this;
};
/**
* Adds a new 'cenc' init data to the current DRM info.
*
* @param {string} base64
* @return {!shaka.test.ManifestGenerator}
*/
shaka.test.ManifestGenerator.prototype.addCencInitData = function(base64) {
var drmInfo = this.currentDrmInfo_();
if (!drmInfo.initData)
drmInfo.initData = [];
var buffer = shaka.util.Uint8ArrayUtils.fromBase64(base64);
drmInfo.initData.push({initData: buffer, initDataType: 'cenc'});
return this;
};
// }}}

@@ -277,3 +294,6 @@

kind: undefined,
keyId: null
encrypted: false,
keyId: null,
allowedByApplication: true,
allowedByKeySystem: true
};

@@ -422,2 +442,15 @@ stream.createSegmentIndex.and.callFake(

/**
* Sets the encrypted flag of the current stream.
*
* @param {boolean} encrypted
* @return {!shaka.test.ManifestGenerator}
*/
shaka.test.ManifestGenerator.prototype.encrypted = function(encrypted) {
var stream = this.currentStream_();
stream.encrypted = encrypted;
return this;
};
/**
* Sets the key ID of the current stream.

@@ -433,2 +466,26 @@ *

};
/**
* Sets that the current stream is disallowed by the application.
*
* @return {!shaka.test.ManifestGenerator}
*/
shaka.test.ManifestGenerator.prototype.disallowByApplication = function() {
var stream = this.currentStream_();
stream.allowedByApplication = false;
return this;
};
/**
* Sets that the current stream is disallowed by the key system.
*
* @return {!shaka.test.ManifestGenerator}
*/
shaka.test.ManifestGenerator.prototype.disallowByKeySystem = function() {
var stream = this.currentStream_();
stream.allowedByKeySystem = false;
return this;
};
// }}}

@@ -435,0 +492,0 @@

@@ -97,5 +97,6 @@ /**

var ret = jasmine.createSpyObj(
'FakeDrmEngine',
['destroy', 'configure', 'init', 'attach', 'initialized', 'keySystem']);
var ret = jasmine.createSpyObj('FakeDrmEngine', [
'destroy', 'configure', 'init', 'attach', 'initialized', 'keySystem',
'getSupportedTypes'
]);
ret.destroy.and.callFake(resolve);

@@ -106,2 +107,5 @@ ret.init.and.callFake(resolve);

ret.keySystem.and.returnValue('com.example.fake');
// See shaka.test.ManifestGenerator.protototype.createStream.
ret.getSupportedTypes.and.returnValue(
['video/mp4; codecs="avc1.4d401f"']);
return ret;

@@ -108,0 +112,0 @@ };

@@ -114,2 +114,4 @@ /**

jasmine.createSpy('getSegmentAvailabilityDuration'),
isLive: jasmine.createSpy('isLive'),
getEarliestStart: jasmine.createSpy('getEarliestStart'),
getSegmentAvailabilityStart:

@@ -125,2 +127,10 @@ jasmine.createSpy('getSegmentAvailabilityStart'),

timeline.isLive.and.callFake(function() {
return presentationDuration == Number.POSITIVE_INFINITY;
});
timeline.getEarliestStart.and.callFake(function() {
return timeline.segmentAvailabilityStart;
});
timeline.getSegmentAvailabilityStart.and.callFake(function() {

@@ -137,3 +147,3 @@ return timeline.segmentAvailabilityStart;

timeline.segmentAvailabilityEnd - timeline.segmentAvailabilityStart :
null;
Number.POSITIVE_INFINITY;
});

@@ -140,0 +150,0 @@

@@ -142,2 +142,3 @@ /*global env: true */

longnameToUrl: {},
longnameToSource: {},
urlToLongname: {}

@@ -397,2 +398,11 @@ };

var longnameToSourceUrl = {};
for (var name in linkMap.longnameToSource) {
var data = linkMap.longnameToSource[name];
var path = data[0];
var lineno = data[1];
var html = longnameToUrl[path];
longnameToSourceUrl[name] = html + '#line' + lineno;
}
function extractLeadingText(string, completeTag) {

@@ -448,2 +458,21 @@ var tagIndex = string.indexOf(completeTag);

function processSource(string, tagInfo) {
var leading = extractLeadingText(string, tagInfo.completeTag);
var linkText = leading.leadingText;
var split;
var target;
string = leading.string;
split = splitLinkText(tagInfo.text);
target = split.target;
linkText = linkText || split.linkText;
monospace = useMonospace(tagInfo.tag, tagInfo.text);
return string.replace( tagInfo.completeTag, buildLink(target, linkText, {
linkMap: longnameToSourceUrl,
monospace: monospace
}) );
}
var replacers = {

@@ -453,2 +482,3 @@ link: processLink,

linkplain: processLink,
linksource: processSource,
tutorial: processTutorial

@@ -782,2 +812,7 @@ };

exports.registerSourceLink = function(longname, path, lineno) {
linkMap.longnameToSource[longname] = [path, lineno];
};
/**

@@ -784,0 +819,0 @@ * Get a longname's filename if one has been registered; otherwise, generate a unique filename, then

@@ -563,2 +563,4 @@ /*global env: true */

doclet.meta.shortpath = docletPath;
helper.registerSourceLink(doclet.longname, docletPath,
doclet.meta.lineno);
}

@@ -565,0 +567,0 @@ }

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc