2cce0c18 by Jon-Carlos Rivera

Multiple audio track support (#681)

* Support for multiple alternate audio tracks

* Separated segment loading logic into a reusable piece of functionality so that we can have more than one segment loader and they can manage the segment fetch behavior unique to each playlist they are loading from

* Introduced the MasterPlaylistController to coordinate the loading of the master playlist and separate the behavior of the player from videojs-contrib-hls.js leaving the latter to be the glue between HLS and the video element's events

* Added the SourceUpdater to manage the asynchronous bahavior of SourceBuffers and MediaSource objects so that we can treat it as a non-blocking work queue

* Added parsing for MediaGroups in master playlists

* Added support for AudioTrackList objects and events

* Add support for every HLS mime type possible (#684)

* Flash live fixes (#682)
1 parent d33786f1
Showing 93 changed files with 2975 additions and 172 deletions
sudo: false
sudo: required
dist: trusty
language: node_js
addons:
firefox: "latest"
node_js:
- "stable"
notifications:
......@@ -14,6 +13,7 @@ notifications:
use_notice: true
# Set up a virtual screen for Firefox.
before_script:
- export CHROME_BIN=/usr/bin/google-chrome
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
env:
......@@ -22,4 +22,8 @@ env:
- secure: AnduYGXka5ft1x7V3SuVYqvlKLvJGhUaRNFdy4UDJr3ZVuwpQjE4TMDG8REmJIJvXfHbh4qY4N1cFSGnXkZ4bH21Xk0v9DLhsxbarKz+X2BvPgXs+Af9EQ6vLEy/5S1vMLxfT5+y+Ec5bVNGOsdUZby8Y21CRzSg6ADN9kwPGlE=
addons:
sauce_connect: true
firefox: latest
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
......
......@@ -9,6 +9,8 @@ Play back HLS with video.js, even where it's not natively supported.
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
- [Getting Started](#getting-started)
- [Known Issues](#known-issues)
- [IE11](#ie11)
- [Documentation](#documentation)
- [Options](#options)
- [withCredentials](#withcredentials)
......@@ -44,7 +46,7 @@ and include it in your page along with video.js:
type="application/x-mpegURL">
</video>
<script src="video.js"></script>
<script src="videojs-hls.min.js"></script>
<script src="videojs-contrib-hls.min.js"></script>
<script>
var player = videojs('example-video');
player.play();
......@@ -53,6 +55,16 @@ player.play();
Check out our [live example](http://videojs.github.io/videojs-contrib-hls/) if you're having trouble.
## Known Issues
Issues that are currenty know about with workarounds. If you want to
help find a solution that would be appreciated!
### IE11
In some IE11 setups there are issues working with it's native HTML
SourceBuffers functionality. This leads to various issues, such as
videos stopping playback with media decode errors. The known workaround
for this issues is to force the player to use flash when running on IE11.
## Documentation
[HTTP Live Streaming](https://developer.apple.com/streaming/) (HLS) has
become a de-facto standard for streaming video on mobile devices
......@@ -89,8 +101,7 @@ are some highlights:
- mid-segment quality switching
- AES-128 segment encryption
- CEA-608 captions are automatically translated into standard HTML5
[caption text
tracks](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track)
[caption text tracks][0]
- Timed ID3 Metadata is automatically translated into HTML5 metedata
text tracks
- Highly customizable adaptive bitrate selection
......@@ -98,6 +109,10 @@ are some highlights:
- Cross-domain credentials support with CORS
- Tight integration with video.js and a philosophy of exposing as much
as possible with standard HTML APIs
- Stream with multiple audio tracks and switching to those audio tracks
(see the docs folder) for info
[0]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/track
### Options
......@@ -106,11 +121,22 @@ initialization. You can pass in options just like you would for other
parts of video.js:
```javascript
videojs(video, {
// html5 for html hls
videojs(video, {html5: {
hls: {
withCredentials: true
}
});
}});
// or
// flash for flash hls
videojs(video, {flash: {
hls: {
withCredentials: true
}
}});
```
#### withCredentials
......@@ -289,26 +315,8 @@ and most CDNs should have no trouble turning CORS on for your account.
### Testing
For testing, you can either run `npm test` or use `grunt` directly.
If you use `npm test`, it will only run the karma and end-to-end tests using chrome.
You can specify which browsers you want the tests to run via grunt's `test` task.
You can use either grunt-style arguments or comma separated arguments:
```
grunt test:chrome:firefox # grunt-style
grunt test:chrome,firefox # comma-separated
```
Possible options are:
* `chromecanary`
* `phantomjs`
* `opera`
* `chrome`<sup>1</sup>
* `safari`<sup>1, 2</sup>
* `firefox`<sup>1</sup>
* `ie`<sup>1</sup>
_<sup>1</sup>supported end-to-end browsers_<br />
_<sup>2</sup>requires the [SafariDriver extension]( https://code.google.com/p/selenium/wiki/SafariDriver) to be installed_
For testing, you run `npm run test`. This will run tests using any of the
browsers that karma-detect-browsers detects on your machine.
## Release History
Check out the [changelog](CHANGELOG.md) for a summary of each release.
......
# Multiple Alternative Audio Tracks
## General
m3u8 manifests with multiple audio streams will have those streams added to `video.js` in an `AudioTrackList`. The `AudioTrackList` can be accessed using `player.audioTracks()` or `tech.audioTracks()`.
## Mapping m3u8 metadata to AudioTracks
The mapping between `AudioTrack` and the parsed m3u8 file is fairly straight forward. The table below shows the mapping
| m3u8 | AudioTrack |
|---------|------------|
| label | label |
| lang | language |
| default | enabled |
| ??? | kind |
| ??? | id |
As you can see m3u8's do not have a property for `AudioTrack.id`, which means that we let `video.js` randomly generate the id for `AudioTrack`s. This will have no real impact on any part of the system as we do not use the `id` anywhere.
The other property that does not have a mapping in the m3u8 is `AudioTrack.kind`. It was decided that we would set the `kind` to `main` when `default` is set to `true` and in all other cases we set it to `alternative`
Below is a basic example of a mapping
m3u8 layout
``` JavaScript
{
'media-group-1': [{
'audio-track-1': {
default: true,
lang: 'eng'
},
'audio-track-2': {
default: true,
lang: 'fr'
}
}]
}
```
Corresponding AudioTrackList when media-group-1 is used (before any tracks have been changed)
``` JavaScript
[{
label: 'audio-tracks-1',
enabled: true,
language: 'eng',
kind: 'main',
id: 'random'
}, {
label: 'audio-tracks-2',
enabled: false,
language: 'fr',
kind: 'alternative',
id: 'random'
}]
```
## Startup (how tracks are added and used)
> AudioTrack & AudioTrackList live in video.js
1. `HLS` creates a `MasterPlaylistController` and watches for the `loadedmetadata` event
1. `HLS` parses the m3u8 using the `MasterPlaylistController`
1. `MasterPlaylistController` creates a `PlaylistLoader` for the master m3u8
1. `MasterPlaylistController` creates `PlaylistLoader`s for every audio playlist
1. `MasterPlaylistController` creates a `SegmentLoader` for the main m3u8
1. `MasterPlaylistController` creates a `SegmentLoader` for a potential audio playlist
1. `HLS` sees the `loadedmetadata` and finds the currently selected MediaGroup and all the metadata
1. `HLS` removes all `AudioTrack`s from the `AudioTrackList`
1. `HLS` created `AudioTrack`s for the MediaGroup and adds them to the `AudioTrackList`
1. `HLS` calls `MasterPlaylistController`s `useAudio` with no arguments (causes it to use the currently enabled audio)
1. `MasterPlaylistController` turns off the current audio `PlaylistLoader` if it is on
1. `MasterPlaylistController` maps the `label` to the `PlaylistLoader` containing the audio
1. `MasterPlaylistController` turns on that `PlaylistLoader` and the Corresponding `SegmentLoader` (master or audio only)
1. `MediaSource`/`mux.js` determine how to mux
## How tracks are switched
> AudioTrack & AudioTrackList live in video.js
1. `HLS` is setup to watch for the `changed` event on the `AudioTrackList`
1. User selects a new `AudioTrack` from a menu (where only one track can be enabled)
1. `AudioTrackList` enables the new `Audiotrack` and disables all others
1. `AudioTrackList` triggers a `changed` event
1. `HLS` sees the `changed` event and finds the newly enabled `AudioTrack`
1. `HLS` sends the `label` for the new `AudioTrack` to `MasterPlaylistController`s `useAudio` function
1. `MasterPlaylistController` turns off the current audio `PlaylistLoader` if it is on
1. `MasterPlaylistController` maps the `label` to the `PlaylistLoader` containing the audio
1. `MasterPlaylistController` maps the `label` to the `PlaylistLoader` containing the audio
1. `MasterPlaylistController` turns on that `PlaylistLoader` and the Corresponding `SegmentLoader` (master or audio only)
1. `MediaSource`/`mux.js` determine how to mux
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Index</title>
</head>
<body>
<ul>
<li><a href="multiple-alternative-audio-tracks">Multiple Alternative Audio Tracks</a></li>
</ul>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Multiple Alternative Audio Tracks - Example</title>
<link href="/node_modules/video.js/dist/video-js.css" rel="stylesheet">
</head>
<body>
<h1>Multiple Alternative Audio Tracks</h1>
<p>Check the source of this page and the console for detailed information on this example</p>
<video id="maat-player" class="video-js vjs-default-skin" controls>
<source src="https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8" type="application/x-mpegURL">
</video>
<div id="audioTrackChoice">
<select id="enabled-audio-track" name="enabled-audio-track">
</select>
</div>
<script src="/node_modules/video.js/dist/video.js"></script>
<script src="/dist/videojs-contrib-hls.js"></script>
<script>
(function(window, videojs) {
var player = window.player = videojs('maat-player');
var audioTrackList = player.audioTracks();
var audioTrackSelect = document.getElementById("enabled-audio-track");
// watch for a change on the select element
// then change the enabled audio track
// only one can be enabled at a time, but video.js will
// handle that for us, all we need to do is enable the new
// track
audioTrackSelect.addEventListener('change', function() {
var track = audioTrackList[this.selectedIndex];
console.log('User switched to track ' + track.label);
track.enabled = true;
});
// watch for changes that will be triggered by any change
// to enabled on any audio track. Manually or through the
// select element
audioTrackList.on('change', function() {
for (var i = 0; i < audioTrackList.length; i++) {
var track = audioTrackList[i];
if (track.enabled) {
console.log('A new ' + track.label + ' has been enabled!');
}
}
});
// will be fired twice in this example
audioTrackList.on('addtrack', function() {
console.log('a track has been added to the audio track list');
});
// will not be fired at all unless you call
// audioTrackList.removeTrack(trackObj)
// we typically will not need to do this unless we have to load
// another video for some reason
audioTrackList.on('removetrack', function() {
console.log('a track has been removed from the audio track list');
});
// getting all the possible audio tracks from the track list
// get all of thier properties
// add each track to the select on the page
// this is all filled out by HLS when it parses the m3u8
player.on('loadeddata', function() {
console.log('There are ' + audioTrackList.length + ' audio tracks');
for (var i = 0; i < audioTrackList.length; i++) {
var track = audioTrackList[i];
var option = document.createElement("option");
option.text = track.label;
if (track.enabled) {
option.selected = true;
}
audioTrackSelect.add(option, i);
console.log('Track ' + (i + 1));
['label', 'enabled', 'language', 'id', 'kind'].forEach(function(prop) {
console.log(" " + prop + ": " + track[prop]);
});
}
});
}(window, window.videojs));
</script>
</body>
</html>
......@@ -43,6 +43,7 @@
<ul>
<li><a href="/test/">Run unit tests in browser.</a></li>
<li><a href="/docs/api/">Read generated docs.</a></li>
<li><a href="/examples">Browse Examples</a></li>
</ul>
<script src="/node_modules/video.js/dist/video.js"></script>
......@@ -50,6 +51,7 @@
<script>
(function(window, videojs) {
var player = window.player = videojs('videojs-contrib-hls-player');
// hook up the video switcher
var loadUrl = document.getElementById('load-url');
var url = document.getElementById('url');
......
......@@ -28,8 +28,7 @@
"docs:api": "jsdoc src -r -d docs/api",
"docs:toc": "doctoc README.md",
"lint": "vjsstandard",
"prestart": "npm-run-all docs build",
"start": "npm-run-all -p start:* watch:*",
"start": "npm-run-all -p watch start:*",
"start:serve": "babel-node scripts/server.js",
"pretest": "npm-run-all lint build",
"test": "karma start test/karma/detected.js",
......@@ -40,7 +39,10 @@
"preversion": "npm test",
"version": "npm run build",
"watch": "npm-run-all -p watch:*",
"watch:js": "watchify src/videojs-contrib-hls.js -t babelify -v -o dist/videojs-contrib-hls.js",
"watch:docs": "nodemon --watch src/ --exec npm run docs",
"watch:js": "npm-run-all -p watch:js:babel watch:js:browserify",
"watch:js:babel": "npm run build:js:babel -- --watch",
"watch:js:browserify": "watchify . -v -o dist/videojs-contrib-hls.js",
"watch:test": "npm-run-all -p watch:test:*",
"watch:test:js": "node scripts/watch-test.js",
"watch:test:manifest": "node -e \"var b=require('./scripts/manifest-data.js'); b.watch();\"",
......@@ -73,21 +75,19 @@
},
"files": [
"CONTRIBUTING.md",
"dist-test/",
"dist/",
"docs/",
"es5/",
"index.html",
"scripts/",
"src/",
"test/",
"utils/"
"test/"
],
"dependencies": {
"pkcs7": "^0.2.2",
"video.js": "^5.2.1",
"videojs-contrib-media-sources": "^3.0.0",
"videojs-swf": "^5.0.0"
"video.js": "^5.10.1",
"videojs-contrib-media-sources": "^3.1.0",
"videojs-swf": "^5.0.2"
},
"devDependencies": {
"babel": "^5.8.0",
......@@ -111,6 +111,7 @@
"karma-safari-launcher": "^0.1.0",
"lodash-compat": "^3.10.0",
"minimist": "^1.2.0",
"nodemon": "^1.9.1",
"npm-run-all": "^1.2.0",
"portscanner": "^1.0.0",
"qunitjs": "^1.18.0",
......
......@@ -17,7 +17,7 @@ glob('test/**/*.test.js', function(err, files) {
};
b.on('log', function(msg) {
process.stdout.write(msg + '\n');
process.stdout.write(msg + ' dist-test/videojs-contrib-hls.js\n');
});
b.on('update', bundle);
......
/**
* @file bin-utils.js
*/
/**
* convert a TimeRange to text
*
* @param {TimeRange} range the timerange to use for conversion
* @param {Number} i the iterator on the range to convert
*/
const textRange = function(range, i) {
return range.start(i) + '-' + range.end(i);
};
/**
* format a number as hex string
*
* @param {Number} e The number
* @param {Number} i the iterator
*/
const formatHexString = function(e, i) {
let value = e.toString(16);
......@@ -14,6 +30,9 @@ const formatAsciiString = function(e) {
return '.';
};
/**
* utils to help dump binary data to the console
*/
const utils = {
hexDump(data) {
let bytes = Array.prototype.slice.call(data);
......
/*
* aes.js
/**
* @file decrypter/aes.js
*
* This file contains an adaptation of the AES decryption algorithm
* from the Standford Javascript Cryptography Library. That work is
......@@ -96,7 +96,7 @@ let aesTables = null;
* Schedule out an AES key for both encryption and decryption. This
* is a low-level class. Use a cipher mode to do bulk encryption.
*
* @constructor
* @class AES
* @param key {Array} The key as an array of 4, 6 or 8 words.
*/
export default class AES {
......@@ -184,13 +184,14 @@ export default class AES {
/**
* Decrypt 16 bytes, specified as four 32-bit words.
* @param encrypted0 {number} the first word to decrypt
* @param encrypted1 {number} the second word to decrypt
* @param encrypted2 {number} the third word to decrypt
* @param encrypted3 {number} the fourth word to decrypt
* @param out {Int32Array} the array to write the decrypted words
*
* @param {Number} encrypted0 the first word to decrypt
* @param {Number} encrypted1 the second word to decrypt
* @param {Number} encrypted2 the third word to decrypt
* @param {Number} encrypted3 the fourth word to decrypt
* @param {Int32Array} out the array to write the decrypted words
* into
* @param offset {number} the offset into the output array to start
* @param {Number} offset the offset into the output array to start
* writing results
* @return {Array} The plaintext.
*/
......
/**
* @file decrypter/async-stream.js
*/
import Stream from '../stream';
/**
* A wrapper around the Stream class to use setTiemout
* and run stream "jobs" Asynchronously
*
* @class AsyncStream
* @extends Stream
*/
export default class AsyncStream extends Stream {
constructor() {
......@@ -11,6 +17,12 @@ export default class AsyncStream extends Stream {
this.delay = 1;
this.timeout_ = null;
}
/**
* process an async job
*
* @private
*/
processJob_() {
this.jobs.shift()();
if (this.jobs.length) {
......@@ -20,6 +32,12 @@ export default class AsyncStream extends Stream {
this.timeout_ = null;
}
}
/**
* push a job into the stream
*
* @param {Function} job the job to push into the stream
*/
push(job) {
this.jobs.push(job);
if (!this.timeout_) {
......
/*
* decrypter.js
/**
* @file decrypter/decrypter.js
*
* An asynchronous implementation of AES-128 CBC decryption with
* PKCS#7 padding.
......@@ -20,12 +20,12 @@ const ntoh = function(word) {
(word >>> 24);
};
/* eslint-disable max-len */
/**
* Decrypt bytes using AES-128 with CBC and PKCS#7 padding.
* @param encrypted {Uint8Array} the encrypted bytes
* @param key {Uint32Array} the bytes of the decryption key
* @param initVector {Uint32Array} the initialization vector (IV) to
*
* @param {Uint8Array} encrypted the encrypted bytes
* @param {Uint32Array} key the bytes of the decryption key
* @param {Uint32Array} initVector the initialization vector (IV) to
* use for the first round of CBC.
* @return {Uint8Array} the decrypted bytes
*
......@@ -33,7 +33,6 @@ const ntoh = function(word) {
* @see http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_.28CBC.29
* @see https://tools.ietf.org/html/rfc2315
*/
/* eslint-enable max-len */
export const decrypt = function(encrypted, key, initVector) {
// word-level access to the encrypted bytes
let encrypted32 = new Int32Array(encrypted.buffer,
......@@ -106,6 +105,12 @@ export const decrypt = function(encrypted, key, initVector) {
* The `Decrypter` class that manages decryption of AES
* data through `AsyncStream` objects and the `decrypt`
* function
*
* @param {Uint8Array} encrypted the encrypted bytes
* @param {Uint32Array} key the bytes of the decryption key
* @param {Uint32Array} initVector the initialization vector (IV) to
* @param {Function} done the function to run when done
* @class Decrypter
*/
export class Decrypter {
constructor(encrypted, key, initVector, done) {
......@@ -137,6 +142,20 @@ export class Decrypter {
done(null, unpad(decrypted));
});
}
/**
* a getter for step the maximum number of bytes to process at one time
*
* @return {Number} the value of step 32000
*/
static get STEP() {
// 4 * 8000;
return 32000;
}
/**
* @private
*/
decryptChunk_(encrypted, key, initVector, decrypted) {
return function() {
let bytes = decrypt(encrypted, key, initVector);
......@@ -146,10 +165,6 @@ export class Decrypter {
}
}
// the maximum number of bytes to process at one time
// 4 * 8000;
Decrypter.STEP = 32000;
export default {
Decrypter,
decrypt
......
/*
* index.js
/**
* @file decrypter/index.js
*
* Index module to easily import the primary components of AES-128
* decryption. Like this:
......
/**
* @file hls-audio-track.js
*/
import {AudioTrack} from 'video.js';
import PlaylistLoader from './playlist-loader';
/**
* HlsAudioTrack extends video.js audio tracks but adds HLS
* specific data storage such as playlist loaders, mediaGroups
* and default/autoselect
*
* @param {Object} options options to create HlsAudioTrack with
* @class HlsAudioTrack
* @extends AudioTrack
*/
export default class HlsAudioTrack extends AudioTrack {
constructor(options) {
super({
kind: options.default ? 'main' : 'alternative',
enabled: options.default || false,
language: options.language,
label: options.label
});
this.hls = options.hls;
this.autoselect = options.autoselect || false;
this.default = options.default || false;
this.withCredentials = options.withCredentials || false;
this.mediaGroups_ = [];
this.addLoader(options.mediaGroup, options.resolvedUri);
}
/**
* get a PlaylistLoader from this track given a mediaGroup name
*
* @param {String} mediaGroup the mediaGroup to get the loader for
* @return {PlaylistLoader|Null} the PlaylistLoader or null
*/
getLoader(mediaGroup) {
for (let i = 0; i < this.mediaGroups_.length; i++) {
let mgl = this.mediaGroups_[i];
if (mgl.mediaGroup === mediaGroup) {
return mgl.loader;
}
}
}
/**
* add a PlaylistLoader given a mediaGroup, and a uri. for a combined track
* we store null for the playlistloader
*
* @param {String} mediaGroup the mediaGroup to get the loader for
* @param {String} uri the uri to get the audio track/mediaGroup from
*/
addLoader(mediaGroup, uri = null) {
let loader = null;
if (uri) {
// TODO: this should probably happen upstream in Master Playlist
// Controller when we can switch PlaylistLoader sources
// then we can just store the uri here instead
loader = new PlaylistLoader(uri, this.hls, this.withCredentials);
}
this.mediaGroups_.push({mediaGroup, loader});
}
/**
* remove a playlist loader from a track given the mediaGroup
*
* @param {String} mediaGroup the mediaGroup to remove
*/
removeLoader(mediaGroup) {
for (let i = 0; i < this.mediaGroups_.length; i++) {
let mgl = this.mediaGroups_[i];
if (mgl.mediaGroup === mediaGroup) {
if (mgl.loader) {
mgl.loader.dispose();
}
this.mediaGroups_.splice(i, 1);
return;
}
}
}
/**
* Dispose of this audio track and
* the playlist loader that it holds inside
*/
dispose() {
let i = this.mediaGroups_.length;
while (i--) {
this.removeLoader(this.mediaGroups_[i].mediaGroup);
}
}
}
/**
* @file m3u8/index.js
*
* Utilities for parsing M3U8 files. If the entire manifest is available,
* `Parser` will create an object representation with enough detail for managing
* playback. `ParseStream` and `LineStream` are lower-level parsing primitives
......
/**
* @file m3u8/line-stream.js
*/
import Stream from '../stream';
/**
* A stream that buffers string input and generates a `data` event for each
* line.
*
* @class LineStream
* @extends Stream
*/
export default class LineStream extends Stream {
constructor() {
......@@ -11,7 +18,8 @@ export default class LineStream extends Stream {
/**
* Add new data to be parsed.
* @param data {string} the text to process
*
* @param {String} data the text to process
*/
push(data) {
let nextNewline;
......
/**
* @file m3u8/parse-stream.js
*/
import Stream from '../stream';
// "forgiving" attribute list psuedo-grammar:
// attributes -> keyvalue (',' keyvalue)*
// keyvalue -> key '=' value
// key -> [^=]*
// value -> '"' [^"]* '"' | [^,]*
/**
* "forgiving" attribute list psuedo-grammar:
* attributes -> keyvalue (',' keyvalue)*
* keyvalue -> key '=' value
* key -> [^=]*
* value -> '"' [^"]* '"' | [^,]*
*/
const attributeSeparator = function() {
let key = '[^=]*';
let value = '"[^"]*"|[^,]*';
......@@ -13,6 +18,11 @@ const attributeSeparator = function() {
return new RegExp('(?:^|,)(' + keyvalue + ')');
};
/**
* Parse attributes from a line given the seperator
*
* @param {String} attributes the attibute line to parse
*/
const parseAttributes = function(attributes) {
// split the string using attributes as the separator
let attrs = attributes.split(attributeSeparator());
......@@ -57,6 +67,9 @@ const parseAttributes = function(attributes) {
* `#EXT-X-MEDIA-SEQUENCE` becomes `media-sequence` when parsed. Unrecognized
* tags are given the tag type `unknown` and a single additional property
* `data` with the remainder of the input.
*
* @class ParseStream
* @extends Stream
*/
export default class ParseStream extends Stream {
constructor() {
......@@ -65,7 +78,8 @@ export default class ParseStream extends Stream {
/**
* Parses an additional line of input.
* @param line {string} a single line of an M3U8 file to parse
*
* @param {String} line a single line of an M3U8 file to parse
*/
push(line) {
let match;
......@@ -254,6 +268,18 @@ export default class ParseStream extends Stream {
this.trigger('data', event);
return;
}
match = (/^#EXT-X-MEDIA:?(.*)$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'media'
};
if (match[1]) {
event.attributes = parseAttributes(match[1]);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-ENDLIST/).exec(line);
if (match) {
this.trigger('data', {
......
/**
* @file m3u8/parser.js
*/
import Stream from '../stream' ;
import LineStream from './line-stream';
import ParseStream from './parse-stream';
......@@ -20,6 +23,9 @@ import {mergeOptions} from 'video.js';
* underlying input is somewhat nonsensical. It emits `info` and `warning`
* events during the parse if it encounters input that seems invalid or
* requires some property of the manifest object to be defaulted.
*
* @class Parser
* @extends Stream
*/
export default class Parser extends Stream {
constructor() {
......@@ -34,6 +40,14 @@ export default class Parser extends Stream {
let currentUri = {};
let key;
let noop = function() {};
let defaultMediaGroups = {
'AUDIO': {},
'VIDEO': {},
'CLOSED-CAPTIONS': {},
'SUBTITLES': {}
};
// group segments into numbered timelines delineated by discontinuities
let currentTimeline = 0;
// the manifest is empty until the parse stream begins delivering data
this.manifest = {
......@@ -43,6 +57,9 @@ export default class Parser extends Stream {
// update the manifest with the m3u8 entry from the parse stream
this.parseStream.on('data', function(entry) {
let mediaGroup;
let rendition;
({
tag() {
// switch based on the tag type
......@@ -96,7 +113,6 @@ export default class Parser extends Stream {
}
this.manifest.segments = uris;
},
key() {
if (!entry.attributes) {
......@@ -149,6 +165,7 @@ export default class Parser extends Stream {
return;
}
this.manifest.discontinuitySequence = entry.number;
currentTimeline = entry.number;
},
'playlist-type'() {
if (!(/VOD|EVENT/).test(entry.playlistType)) {
......@@ -161,6 +178,8 @@ export default class Parser extends Stream {
},
'stream-inf'() {
this.manifest.playlists = uris;
this.manifest.mediaGroups =
this.manifest.mediaGroups || defaultMediaGroups;
if (!entry.attributes) {
this.trigger('warn', {
......@@ -175,7 +194,48 @@ export default class Parser extends Stream {
currentUri.attributes = mergeOptions(currentUri.attributes,
entry.attributes);
},
media() {
this.manifest.mediaGroups =
this.manifest.mediaGroups || defaultMediaGroups;
if (!(entry.attributes &&
entry.attributes.TYPE &&
entry.attributes['GROUP-ID'] &&
entry.attributes.NAME)) {
this.trigger('warn', {
message: 'ignoring incomplete or missing media group'
});
return;
}
// find the media group, creating defaults as necessary
let mediaGroupType = this.manifest.mediaGroups[entry.attributes.TYPE];
mediaGroupType[entry.attributes['GROUP-ID']] =
mediaGroupType[entry.attributes['GROUP-ID']] || {};
mediaGroup = mediaGroupType[entry.attributes['GROUP-ID']];
// collect the rendition metadata
rendition = {
default: (/yes/i).test(entry.attributes.DEFAULT)
};
if (rendition.default) {
rendition.autoselect = true;
} else {
rendition.autoselect = (/yes/i).test(entry.attributes.AUTOSELECT);
}
if (entry.attributes.LANGUAGE) {
rendition.language = entry.attributes.LANGUAGE;
}
if (entry.attributes.URI) {
rendition.uri = entry.attributes.URI;
}
// insert the new rendition
mediaGroup[entry.attributes.NAME] = rendition;
},
discontinuity() {
currentTimeline += 1;
currentUri.discontinuity = true;
this.manifest.discontinuityStarts.push(uris.length);
},
......@@ -215,6 +275,7 @@ export default class Parser extends Stream {
if (key) {
currentUri.key = key;
}
currentUri.timeline = currentTimeline;
// prepare for the next URI
currentUri = {};
......@@ -229,7 +290,8 @@ export default class Parser extends Stream {
/**
* Parse the input string and update the manifest object.
* @param chunk {string} a potentially incomplete portion of the manifest
*
* @param {String} chunk a potentially incomplete portion of the manifest
*/
push(chunk) {
this.lineStream.push(chunk);
......@@ -246,4 +308,3 @@ export default class Parser extends Stream {
}
}
......
/**
* @file playlist.js
*
* Playlist related utilities.
*/
import {createTimeRange} from 'video.js';
......@@ -13,6 +15,14 @@ let Playlist = {
UNSAFE_LIVE_SEGMENTS: 3
};
/**
* walk backward until we find a duration we can use
* or return a failure
*
* @param {Playlist} playlist the playlist to walk through
* @param {Number} endSequence the mediaSequence to stop walking on
*/
const backwardDuration = function(playlist, endSequence) {
let result = 0;
let i = endSequence - playlist.mediaSequence;
......@@ -48,6 +58,13 @@ const backwardDuration = function(playlist, endSequence) {
return { result, precise: false };
};
/**
* walk forward until we find a duration we can use
* or return a failure
*
* @param {Playlist} playlist the playlist to walk through
* @param {Number} endSequence the mediaSequence to stop walking on
*/
const forwardDuration = function(playlist, endSequence) {
let result = 0;
let segment;
......@@ -83,13 +100,15 @@ const forwardDuration = function(playlist, endSequence) {
* playlist. The duration of a subinterval of the available segments
* may be calculated by specifying an end index.
*
* @param playlist {object} a media playlist object
* @param endSequence {number} (optional) an exclusive upper boundary
* @param {Object} playlist a media playlist object
* @param {Number=} endSequence an exclusive upper boundary
* for the playlist. Defaults to playlist length.
* @return {number} the duration between the first available segment
* @param {Number} expired the amount of time that has dropped
* off the front of the playlist in a live scenario
* @return {Number} the duration between the first available segment
* and end index.
*/
const intervalDuration = function(playlist, endSequence) {
const intervalDuration = function(playlist, endSequence, expired) {
let backward;
let forward;
......@@ -120,7 +139,7 @@ const intervalDuration = function(playlist, endSequence) {
}
// return the less-precise, playlist-based duration estimate
return backward.result;
return backward.result + expired;
};
/**
......@@ -128,23 +147,23 @@ const intervalDuration = function(playlist, endSequence) {
* are specified, the duration will be for the subset of the media
* timeline between those two indices. The total duration for live
* playlists is always Infinity.
* @param playlist {object} a media playlist object
* @param endSequence {number} (optional) an exclusive upper
*
* @param {Object} playlist a media playlist object
* @param {Number=} endSequence an exclusive upper
* boundary for the playlist. Defaults to the playlist media
* sequence number plus its length.
* @param includeTrailingTime {boolean} (optional) if false, the
* interval between the final segment and the subsequent segment
* will not be included in the result
* @return {number} the duration between the start index and end
* @param {Number=} expired the amount of time that has
* dropped off the front of the playlist in a live scenario
* @return {Number} the duration between the start index and end
* index.
*/
export const duration = function(playlist, endSequence, includeTrailingTime) {
export const duration = function(playlist, endSequence, expired) {
if (!playlist) {
return 0;
}
if (typeof includeTrailingTime === 'undefined') {
includeTrailingTime = true;
if (typeof expired !== 'number') {
expired = 0;
}
// if a slice of the total duration is not requested, use
......@@ -164,7 +183,7 @@ export const duration = function(playlist, endSequence, includeTrailingTime) {
// calculate the total duration based on the segment durations
return intervalDuration(playlist,
endSequence,
includeTrailingTime);
expired);
};
/**
......@@ -174,16 +193,24 @@ export const duration = function(playlist, endSequence, includeTrailingTime) {
* seekable implementation for live streams would need to offset
* these values by the duration of content that has expired from the
* stream.
* @param playlist {object} a media playlist object
*
* @param {Object} playlist a media playlist object
* @param {Number=} expired the amount of time that has
* dropped off the front of the playlist in a live scenario
* @return {TimeRanges} the periods of time that are valid targets
* for seeking
*/
export const seekable = function(playlist) {
export const seekable = function(playlist, expired) {
let start;
let end;
let endSequence;
if (typeof expired !== 'number') {
expired = 0;
}
// without segments, there are no seekable ranges
if (!playlist.segments) {
if (!playlist || !playlist.segments) {
return createTimeRange();
}
// when the playlist is complete, the entire duration is seekable
......@@ -194,15 +221,142 @@ export const seekable = function(playlist) {
// live playlists should not expose three segment durations worth
// of content from the end of the playlist
// https://tools.ietf.org/html/draft-pantos-http-live-streaming-16#section-6.3.3
start = intervalDuration(playlist, playlist.mediaSequence);
start = intervalDuration(playlist, playlist.mediaSequence, expired);
endSequence = Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS);
end = intervalDuration(playlist,
playlist.mediaSequence +
Math.max(0, playlist.segments.length - Playlist.UNSAFE_LIVE_SEGMENTS));
playlist.mediaSequence + endSequence,
expired);
return createTimeRange(start, end);
};
/**
* Determine the index of the segment that contains a specified
* playback position in a media playlist.
*
* @param {Object} playlist the media playlist to query
* @param {Number} time The number of seconds since the earliest
* possible position to determine the containing segment for
* @param {Number=} expired the duration of content, in
* seconds, that has been removed from this playlist because it
* expired
* @return {Number} The number of the media segment that contains
* that time position.
*/
export const getMediaIndexForTime_ = function(playlist, time, expired) {
let i;
let segment;
let originalTime = time;
let numSegments = playlist.segments.length;
let lastSegment = numSegments - 1;
let startIndex;
let endIndex;
let knownStart;
let knownEnd;
if (!playlist) {
return 0;
}
// when the requested position is earlier than the current set of
// segments, return the earliest segment index
if (time < 0) {
return 0;
}
expired = expired || 0;
// find segments with known timing information that bound the
// target time
for (i = 0; i < numSegments; i++) {
segment = playlist.segments[i];
if (segment.end) {
if (segment.end > time) {
knownEnd = segment.end;
endIndex = i;
break;
} else {
knownStart = segment.end;
startIndex = i + 1;
}
}
}
// time was equal to or past the end of the last segment in the playlist
if (startIndex === numSegments) {
return numSegments;
}
// use the bounds we just found and playlist information to
// estimate the segment that contains the time we are looking for
if (typeof startIndex !== 'undefined') {
// We have a known-start point that is before our desired time so
// walk from that point forwards
time = time - knownStart;
for (i = startIndex; i < (endIndex || numSegments); i++) {
segment = playlist.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
if (i >= endIndex) {
// We haven't found a segment but we did hit a known end point
// so fallback to interpolating between the segment index
// based on the known span of the timeline we are dealing with
// and the number of segments inside that span
return startIndex + Math.floor(
((originalTime - knownStart) / (knownEnd - knownStart)) *
(endIndex - startIndex));
}
// We _still_ haven't found a segment so load the last one
return lastSegment;
} else if (typeof endIndex !== 'undefined') {
// We _only_ have a known-end point that is after our desired time so
// walk from that point backwards
time = knownEnd - time;
for (i = endIndex; i >= 0; i--) {
segment = playlist.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
// We haven't found a segment so load the first one if time is zero
if (time === 0) {
return 0;
}
return -1;
}
// We known nothing so walk from the front of the playlist,
// subtracting durations until we find a segment that contains
// time and return it
time = time - expired;
if (time < 0) {
return -1;
}
for (i = 0; i < numSegments; i++) {
segment = playlist.segments[i];
time -= segment.duration;
if (time < 0) {
return i;
}
}
// We are out of possible candidates so load the last one...
// The last one is the least likely to overlap a buffer and therefore
// the one most likely to tell us something about the timeline
return lastSegment;
};
Playlist.duration = duration;
Playlist.seekable = seekable;
Playlist.getMediaIndexForTime_ = getMediaIndexForTime_;
// exports
export default Playlist;
......
/**
* ranges
*
* Utilities for working with TimeRanges.
*
*/
import videojs from 'video.js';
// Fudge factor to account for TimeRanges rounding
const TIME_FUDGE_FACTOR = 1 / 30;
const filterRanges = function(timeRanges, predicate) {
let results = [];
let i;
if (timeRanges && timeRanges.length) {
// Search for ranges that match the predicate
for (i = 0; i < timeRanges.length; i++) {
if (predicate(timeRanges.start(i), timeRanges.end(i))) {
results.push([timeRanges.start(i), timeRanges.end(i)]);
}
}
}
return videojs.createTimeRanges(results);
};
/**
* Attempts to find the buffered TimeRange that contains the specified
* time.
* @param {TimeRanges} buffered - the TimeRanges object to query
* @param {number} time - the time to filter on.
* @returns {TimeRanges} a new TimeRanges object
*/
const findRange = function(buffered, time) {
return filterRanges(buffered, function(start, end) {
return start - TIME_FUDGE_FACTOR <= time &&
end + TIME_FUDGE_FACTOR >= time;
});
};
/**
* Returns the TimeRanges that begin at or later than the specified
* time.
* @param {TimeRanges} timeRanges - the TimeRanges object to query
* @param {number} time - the time to filter on.
* @returns {TimeRanges} a new TimeRanges object.
*/
const findNextRange = function(timeRanges, time) {
return filterRanges(timeRanges, function(start) {
return start - TIME_FUDGE_FACTOR >= time;
});
};
/**
* Search for a likely end time for the segment that was just appened
* based on the state of the `buffered` property before and after the
* append. If we fin only one such uncommon end-point return it.
* @param {TimeRanges} original - the buffered time ranges before the update
* @param {TimeRanges} update - the buffered time ranges after the update
* @returns {Number|null} the end time added between `original` and `update`,
* or null if one cannot be unambiguously determined.
*/
const findSoleUncommonTimeRangesEnd = function(original, update) {
let i;
let start;
let end;
let result = [];
let edges = [];
// In order to qualify as a possible candidate, the end point must:
// 1) Not have already existed in the `original` ranges
// 2) Not result from the shrinking of a range that already existed
// in the `original` ranges
// 3) Not be contained inside of a range that existed in `original`
const overlapsCurrentEnd = function(span) {
return (span[0] <= end && span[1] >= end);
};
if (original) {
// Save all the edges in the `original` TimeRanges object
for (i = 0; i < original.length; i++) {
start = original.start(i);
end = original.end(i);
edges.push([start, end]);
}
}
if (update) {
// Save any end-points in `update` that are not in the `original`
// TimeRanges object
for (i = 0; i < update.length; i++) {
start = update.start(i);
end = update.end(i);
if (edges.some(overlapsCurrentEnd)) {
continue;
}
// at this point it must be a unique non-shrinking end edge
result.push(end);
}
}
// we err on the side of caution and return null if didn't find
// exactly *one* differing end edge in the search above
if (result.length !== 1) {
return null;
}
return result[0];
};
/**
* Calculate the intersection of two TimeRanges
* @param {TimeRanges} bufferA
* @param {TimeRanges} bufferB
* @returns {TimeRanges} The interesection of `bufferA` with `bufferB`
*/
const bufferIntersection = function(bufferA, bufferB) {
let start = null;
let end = null;
let arity = 0;
let extents = [];
let ranges = [];
if (!bufferA || !bufferA.length || !bufferB || !bufferB.length) {
return videojs.createTimeRange();
}
// Handle the case where we have both buffers and create an
// intersection of the two
let count = bufferA.length;
// A) Gather up all start and end times
while (count--) {
extents.push({time: bufferA.start(count), type: 'start'});
extents.push({time: bufferA.end(count), type: 'end'});
}
count = bufferB.length;
while (count--) {
extents.push({time: bufferB.start(count), type: 'start'});
extents.push({time: bufferB.end(count), type: 'end'});
}
// B) Sort them by time
extents.sort(function(a, b) {
return a.time - b.time;
});
// C) Go along one by one incrementing arity for start and decrementing
// arity for ends
for (count = 0; count < extents.length; count++) {
if (extents[count].type === 'start') {
arity++;
// D) If arity is ever incremented to 2 we are entering an
// overlapping range
if (arity === 2) {
start = extents[count].time;
}
} else if (extents[count].type === 'end') {
arity--;
// E) If arity is ever decremented to 1 we leaving an
// overlapping range
if (arity === 1) {
end = extents[count].time;
}
}
// F) Record overlapping ranges
if (start !== null && end !== null) {
ranges.push([start, end]);
start = null;
end = null;
}
}
return videojs.createTimeRanges(ranges);
};
/**
* Calculates the percentage of `segmentRange` that overlaps the
* `buffered` time ranges.
* @param {TimeRanges} segmentRange - the time range that the segment covers
* @param {TimeRanges} buffered - the currently buffered time ranges
* @returns {Number} percent of the segment currently buffered
*/
const calculateBufferedPercent = function(segmentRange, buffered) {
let segmentDuration = segmentRange.end(0) - segmentRange.start(0);
let intersection = bufferIntersection(segmentRange, buffered);
let overlapDuration = 0;
let count = intersection.length;
while (count--) {
overlapDuration += intersection.end(count) - intersection.start(count);
}
return (overlapDuration / segmentDuration) * 100;
};
export default {
findRange,
findNextRange,
findSoleUncommonTimeRangesEnd,
calculateBufferedPercent,
TIME_FUDGE_FACTOR
};
/**
* @file resolve-url.js
*/
import document from 'global/document';
/* eslint-disable max-len */
/**
* Constructs a new URI by interpreting a path relative to another
* URI.
* @param basePath {string} a relative or absolute URI
* @param path {string} a path part to combine with the base
* @return {string} a URI that is equivalent to composing `base`
* with `path`
*
* @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue
* @param {String} basePath a relative or absolute URI
* @param {String} path a path part to combine with the base
* @return {String} a URI that is equivalent to composing `base`
* with `path`
*/
/* eslint-enable max-len */
const resolveUrl = function(basePath, path) {
// use the base element to get the browser to handle URI resolution
let oldBase = document.querySelector('base');
......
/**
* @file source-updater.js
*/
import videojs from 'video.js';
/**
* A queue of callbacks to be serialized and applied when a
* MediaSource and its associated SourceBuffers are not in the
* updating state. It is used by the segment loader to update the
* underlying SourceBuffers when new data is loaded, for instance.
*
* @class SourceUpdater
* @param {MediaSource} mediaSource the MediaSource to create the
* SourceBuffer from
* @param {String} mimeType the desired MIME type of the underlying
* SourceBuffer
*/
export default class SourceUpdater {
constructor(mediaSource, mimeType) {
let createSourceBuffer = () => {
this.sourceBuffer_ = mediaSource.addSourceBuffer(mimeType);
// run completion handlers and process callbacks as updateend
// events fire
this.sourceBuffer_.addEventListener('updateend', () => {
let pendingCallback = this.pendingCallback_;
this.pendingCallback_ = null;
if (pendingCallback) {
pendingCallback();
}
});
this.sourceBuffer_.addEventListener('updateend',
this.runCallback_.bind(this));
this.runCallback_();
};
this.callbacks_ = [];
this.pendingCallback_ = null;
this.timestampOffset_ = 0;
this.mediaSource = mediaSource;
if (mediaSource.readyState === 'closed') {
mediaSource.addEventListener('sourceopen', createSourceBuffer);
} else {
createSourceBuffer();
}
}
/**
* Aborts the current segment and resets the segment parser.
*
* @param {Function} done function to call when done
* @see http://w3c.github.io/media-source/#widl-SourceBuffer-abort-void
*/
abort(done) {
this.queueCallback_(() => {
this.sourceBuffer_.abort();
}, done);
}
/**
* Queue an update to append an ArrayBuffer.
*
* @param {ArrayBuffer} bytes
* @param {Function} done the function to call when done
* @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-appendBuffer-void-ArrayBuffer-data
*/
appendBuffer(bytes, done) {
this.queueCallback_(() => {
this.sourceBuffer_.appendBuffer(bytes);
}, done);
}
/**
* Indicates what TimeRanges are buffered in the managed SourceBuffer.
*
* @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-buffered
*/
buffered() {
if (!this.sourceBuffer_) {
return videojs.createTimeRanges();
}
return this.sourceBuffer_.buffered;
}
/**
* Queue an update to set the duration.
*
* @param {Double} duration what to set the duration to
* @see http://www.w3.org/TR/media-source/#widl-MediaSource-duration
*/
duration(duration) {
this.queueCallback_(() => {
this.sourceBuffer_.duration = duration;
});
}
/**
* Queue an update to remove a time range from the buffer.
*
* @param {Number} start where to start the removal
* @param {Number} end where to end the removal
* @see http://www.w3.org/TR/media-source/#widl-SourceBuffer-remove-void-double-start-unrestricted-double-end
*/
remove(start, end) {
this.queueCallback_(() => {
this.sourceBuffer_.remove(start, end);
});
}
/**
* wether the underlying sourceBuffer is updating or not
*
* @return {Boolean} the updating status of the SourceBuffer
*/
updating() {
return !this.sourceBuffer_ || this.sourceBuffer_.updating;
}
/**
* Set/get the timestampoffset on the SourceBuffer
*
* @return {Number} the timestamp offset
*/
timestampOffset(offset) {
if (typeof offset !== 'undefined') {
this.queueCallback_(() => {
this.sourceBuffer_.timestampOffset = offset;
});
this.timestampOffset_ = offset;
}
return this.timestampOffset_;
}
/**
* que a callback to run
*/
queueCallback_(callback, done) {
this.callbacks_.push([callback.bind(this), done]);
this.runCallback_();
}
/**
* run a queued callback
*/
runCallback_() {
let callbacks;
if (this.sourceBuffer_ &&
!this.sourceBuffer_.updating &&
this.callbacks_.length) {
callbacks = this.callbacks_.shift();
this.pendingCallback_ = callbacks[1];
callbacks[0]();
}
}
/**
* dispose of the source updater and the underlying sourceBuffer
*/
dispose() {
if (this.sourceBuffer_ && this.mediaSource.readyState === 'open') {
this.sourceBuffer_.abort();
}
}
}
/**
* @file stream.js
*/
/**
* A lightweight readable stream implemention that handles event dispatching.
*
* @class Stream
*/
export default class Stream {
constructor() {
......@@ -8,8 +13,9 @@ export default class Stream {
/**
* Add a listener for a specified event type.
* @param type {string} the event name
* @param listener {function} the callback to be invoked when an event of
*
* @param {String} type the event name
* @param {Function} listener the callback to be invoked when an event of
* the specified type occurs
*/
on(type, listener) {
......@@ -21,9 +27,11 @@ export default class Stream {
/**
* Remove a listener for a specified event type.
* @param type {string} the event name
* @param listener {function} a function previously registered for this
*
* @param {String} type the event name
* @param {Function} listener a function previously registered for this
* type of event through `on`
* @return {Boolean} if we could turn it off or not
*/
off(type, listener) {
let index;
......@@ -39,7 +47,8 @@ export default class Stream {
/**
* Trigger an event of the specified type on this stream. Any additional
* arguments to this function are passed as parameters to event listeners.
* @param type {string} the event name
*
* @param {String} type the event name
*/
trigger(type) {
let callbacks;
......@@ -79,7 +88,8 @@ export default class Stream {
* Forwards all `data` events on this stream to the destination stream. The
* destination stream should provide a method `push` to receive the data
* events as they arrive.
* @param destination {stream} the stream that will receive all `data` events
*
* @param {Stream} destination the stream that will receive all `data` events
* @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
*/
pipe(destination) {
......
/**
* @file xhr.js
*/
/**
* A wrapper for videojs.xhr that tracks bandwidth.
*
* @param {Object} options options for the XHR
* @param {Function} callback the callback to call when done
* @return {Request} the xhr request that is going to be made
*/
import {xhr as videojsXHR, mergeOptions} from 'video.js';
......
import HlsAudioTrack from '../src/hls-audio-track';
import QUnit from 'qunit';
// Most of these tests will be done in video.js.AudioTrack unit tests
QUnit.module('HlsAudioTrack - Props');
QUnit.test('verify that props are readonly and can be set', function() {
let props = {
default: true,
language: 'en',
label: 'English',
autoselect: true,
withCredentials: true,
// below props won't be used, its used for checking
kind: 'main'
};
let track = new HlsAudioTrack(props);
for (let k in props) {
QUnit.equal(track[k], props[k], `${k} should be stored in track`);
}
});
QUnit.test('can start with a mediaGroup that has a uri', function() {
let props = {
default: true,
language: 'en',
label: 'English',
autoselect: true,
mediaGroup: 'foo',
withCredentials: true,
resolvedUri: 'http://some.test.url/playlist.m3u8',
// below props won't be used, its used for checking
enabled: true,
kind: 'main'
};
let track = new HlsAudioTrack(props);
QUnit.equal(track.mediaGroups_.length, 1, 'loader was created');
let loader = track.getLoader('foo');
QUnit.ok(loader, 'can getLoader on foo');
track.dispose();
QUnit.equal(track.mediaGroups_.length, 0, 'loader disposed');
});
QUnit.test('can start with a mediaGroup that has no uri', function() {
let props = {
default: true,
language: 'en',
label: 'English',
autoselect: true,
mediaGroup: 'foo',
withCredentials: true,
// below props won't be used, its used for checking
enabled: true,
kind: 'main'
};
let track = new HlsAudioTrack(props);
QUnit.equal(track.mediaGroups_.length, 1, 'mediaGroupLoader was created for foo');
QUnit.ok(!track.getLoader('foo'), 'can getLoader on foo, but it is undefined');
track.dispose();
QUnit.equal(track.mediaGroups_.length, 0, 'loaders disposed');
});
QUnit.module('HlsAudioTrack - Loader', {
beforeEach() {
this.track = new HlsAudioTrack({
mediaGroup: 'default',
default: true,
language: 'en',
label: 'English',
autoselect: true,
withCredentials: true
});
},
afterEach() {
this.track.dispose();
QUnit.equal(this.track.mediaGroups_.length, 0, 'zero loaders after dispose');
}
});
QUnit.test('can add a playlist loader', function() {
QUnit.equal(this.track.mediaGroups_.length, 1, '1 loader to start');
this.track.addLoader('foo', 'someurl');
this.track.addLoader('bar', 'someurl');
this.track.addLoader('baz', 'someurl');
QUnit.equal(this.track.mediaGroups_.length, 4, 'now has four loaders');
});
QUnit.test('can remove playlist loader', function() {
QUnit.equal(this.track.mediaGroups_.length, 1, 'one loaders to start');
this.track.addLoader('foo', 'someurl');
this.track.addLoader('baz', 'someurl');
QUnit.equal(this.track.mediaGroups_.length, 3, 'now has three loaders');
this.track.removeLoader('baz');
QUnit.equal(this.track.mediaGroups_.length, 2, 'now has two loaders');
});
......@@ -10,13 +10,10 @@ var DEFAULTS = {
'node_modules/sinon/pkg/sinon-ie.js',
'node_modules/video.js/dist/video.js',
'node_modules/video.js/dist/video-js.css',
'test/**/*.test.js'
],
exclude: [
'test/data/**'
],
exclude: [],
plugins: [
'karma-browserify',
......@@ -43,6 +40,13 @@ var DEFAULTS = {
noParse: [
'test/data/**',
]
},
customLaunchers: {
travisChrome: {
base: 'Chrome',
flags: ['--no-sandbox']
}
}
};
......
......@@ -4,12 +4,10 @@ var common = require('./common');
module.exports = function(config) {
// Travis CI should run in its available Firefox headless browser.
if (process.env.TRAVIS) {
config.set(common({
browsers: ['Firefox'],
plugins: ['karma-firefox-launcher']
browsers: ['travisChrome'],
plugins: ['karma-chrome-launcher']
}))
} else {
config.set(common({
......
import Playlist from '../src/playlist';
import PlaylistLoader from '../src/playlist-loader';
import QUnit from 'qunit';
import xhrFactory from '../src/xhr';
import { useFakeEnvironment } from './test-helpers';
QUnit.module('Playlist Duration');
QUnit.test('total duration for live playlists is Infinity', function() {
......@@ -376,3 +380,251 @@ QUnit.test('seekable end accounts for non-standard target durations', function()
9 - (2 + 2 + 1),
'allows seeking no further than three segments from the end');
});
QUnit.module('Playlist Media Index For Time', {
beforeEach() {
this.env = useFakeEnvironment();
this.clock = this.env.clock;
this.requests = this.env.requests;
this.fakeHls = {
xhr: xhrFactory()
};
},
afterEach() {
this.env.restore();
}
});
QUnit.test('can get media index by playback position for non-live videos', function() {
let media;
let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
loader.load();
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXTINF:6,\n' +
'2.ts\n' +
'#EXT-X-ENDLIST\n'
);
media = loader.media();
QUnit.equal(Playlist.getMediaIndexForTime_(media, -1), 0,
'the index is never less than zero');
QUnit.equal(Playlist.getMediaIndexForTime_(media, 0), 0, 'time zero is index zero');
QUnit.equal(Playlist.getMediaIndexForTime_(media, 3), 0, 'time three is index zero');
QUnit.equal(Playlist.getMediaIndexForTime_(media, 10), 2, 'time 10 is index 2');
QUnit.equal(Playlist.getMediaIndexForTime_(media, 22), 2,
'time greater than the length is index 2');
});
QUnit.test('returns the lower index when calculating for a segment boundary', function() {
let media;
let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
loader.load();
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXTINF:4,\n' +
'0.ts\n' +
'#EXTINF:5,\n' +
'1.ts\n' +
'#EXT-X-ENDLIST\n'
);
media = loader.media();
QUnit.equal(Playlist.getMediaIndexForTime_(media, 4), 1, 'rounds up exact matches');
QUnit.equal(Playlist.getMediaIndexForTime_(media, 3.7), 0, 'rounds down');
QUnit.equal(Playlist.getMediaIndexForTime_(media, 4.5), 1, 'rounds up at 0.5');
});
QUnit.test(
'accounts for non-zero starting segment time when calculating media index',
function() {
let media;
let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
loader.load();
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n'
);
loader.media().segments[0].end = 154;
media = loader.media();
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 0),
-1,
'the lowest returned value is negative one'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 45),
-1,
'expired content returns negative one'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 75),
-1,
'expired content returns negative one'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100),
0,
'calculates the earliest available position'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100 + 2),
0,
'calculates within the first segment'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100 + 2),
0,
'calculates within the first segment'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100 + 4),
1,
'calculates within the second segment'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100 + 4.5),
1,
'calculates within the second segment'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100 + 6),
1,
'calculates within the second segment'
);
loader.media().segments[1].end = 159;
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 159),
2,
'returns number of segments when time is equal to end of last segment'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 159.1),
2,
'returns number of segments when time is past end of last segment'
);
});
QUnit.test('prefers precise segment timing when tracking expired time', function() {
let media;
let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
loader.load();
loader.trigger('firstplay');
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n'
);
// setup the loader with an "imprecise" value as if it had been
// accumulating segment durations as they expire
loader.expired_ = 160;
// annotate the first segment with a start time
// this number would be coming from the Source Buffer in practice
loader.media().segments[0].end = 150;
media = loader.media();
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 149),
0,
'prefers the value on the first segment'
);
// trigger a playlist refresh
this.clock.tick(10 * 1000);
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1002\n' +
'#EXTINF:5,\n' +
'1002.ts\n'
);
media = loader.media();
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 150 + 4 + 1),
0,
'tracks precise expired times'
);
});
QUnit.test('accounts for expired time when calculating media index', function() {
let media;
let loader = new PlaylistLoader('media.m3u8', this.fakeHls);
let expired = 150;
loader.load();
this.requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-MEDIA-SEQUENCE:1001\n' +
'#EXTINF:4,\n' +
'1001.ts\n' +
'#EXTINF:5,\n' +
'1002.ts\n'
);
media = loader.media();
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 0, expired),
-1,
'expired content returns a negative index'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 75, expired),
-1,
'expired content returns a negative index'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100, expired),
0,
'calculates the earliest available position'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100 + 2, expired),
0,
'calculates within the first segment'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100 + 2, expired),
0,
'calculates within the first segment'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100 + 4.5, expired),
1,
'calculates within the second segment'
);
QUnit.equal(
Playlist.getMediaIndexForTime_(media, 50 + 100 + 6, expired),
1,
'calculates within the second segment'
);
});
......
import Ranges from '../src/ranges';
import {createTimeRanges} from 'video.js';
import QUnit from 'qunit';
QUnit.module('TimeRanges Utilities');
QUnit.test('finds the overlapping time range', function() {
let range = Ranges.findRange(createTimeRanges([[0, 5], [6, 12]]), 3);
QUnit.equal(range.length, 1, 'found one range');
QUnit.equal(range.end(0), 5, 'inside the first buffered region');
range = Ranges.findRange(createTimeRanges([[0, 5], [6, 12]]), 6);
QUnit.equal(range.length, 1, 'found one range');
QUnit.equal(range.end(0), 12, 'inside the second buffered region');
});
QUnit.module('Buffer Inpsection');
QUnit.test('detects time range end-point changed by updates', function() {
let edge;
// Single-range changes
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
createTimeRanges([[0, 11]]));
QUnit.strictEqual(edge, 11, 'detected a forward addition');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[5, 10]]),
createTimeRanges([[0, 10]]));
QUnit.strictEqual(edge, null, 'ignores backward addition');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[5, 10]]),
createTimeRanges([[0, 11]]));
QUnit.strictEqual(edge, 11,
'detected a forward addition & ignores a backward addition');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
createTimeRanges([[0, 9]]));
QUnit.strictEqual(edge, null,
'ignores a backwards addition resulting from a shrinking range');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
createTimeRanges([[2, 7]]));
QUnit.strictEqual(edge, null,
'ignores a forward & backwards addition resulting from a shrinking ' +
'range');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[2, 10]]),
createTimeRanges([[0, 7]]));
QUnit.strictEqual(
edge,
null,
'ignores a forward & backwards addition resulting from a range shifted backward'
);
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[2, 10]]),
createTimeRanges([[5, 15]]));
QUnit.strictEqual(edge, 15,
'detected a forwards addition resulting from a range shifted foward');
// Multiple-range changes
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10]]),
createTimeRanges([[0, 11], [12, 15]]));
QUnit.strictEqual(edge, null, 'ignores multiple new forward additions');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10], [20, 40]]),
createTimeRanges([[20, 50]]));
QUnit.strictEqual(edge, 50, 'detected a forward addition & ignores range removal');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10], [20, 40]]),
createTimeRanges([[0, 50]]));
QUnit.strictEqual(edge, 50, 'detected a forward addition & ignores merges');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 10], [20, 40]]),
createTimeRanges([[0, 40]]));
QUnit.strictEqual(edge, null, 'ignores merges');
// Empty input
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges(),
createTimeRanges([[0, 11]]));
QUnit.strictEqual(edge, 11, 'handle an empty original TimeRanges object');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 11]]),
createTimeRanges());
QUnit.strictEqual(edge, null, 'handle an empty update TimeRanges object');
// Null input
edge = Ranges.findSoleUncommonTimeRangesEnd(null, createTimeRanges([[0, 11]]));
QUnit.strictEqual(edge, 11, 'treat null original buffer as an empty TimeRanges object');
edge = Ranges.findSoleUncommonTimeRangesEnd(createTimeRanges([[0, 11]]), null);
QUnit.strictEqual(edge, null, 'treat null update buffer as an empty TimeRanges object');
});
import SourceUpdater from '../src/source-updater';
import QUnit from 'qunit';
import videojs from 'video.js';
import { useFakeMediaSource } from './test-helpers';
QUnit.module('Source Updater', {
beforeEach() {
this.mse = useFakeMediaSource();
this.mediaSource = new videojs.MediaSource();
},
afterEach() {
this.mse.restore();
}
});
QUnit.test('waits for sourceopen to create a source buffer', function() {
new SourceUpdater(this.mediaSource, 'video/mp2t'); // eslint-disable-line no-new
QUnit.equal(this.mediaSource.sourceBuffers.length, 0,
'waited to create the source buffer');
this.mediaSource.trigger('sourceopen');
QUnit.equal(this.mediaSource.sourceBuffers.length, 1, 'created one source buffer');
QUnit.equal(this.mediaSource.sourceBuffers[0].mimeType_, 'video/mp2t',
'assigned the correct MIME type');
});
QUnit.test('runs a callback when the source buffer is created', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
let sourceBuffer;
updater.appendBuffer(new Uint8Array([0, 1, 2]));
this.mediaSource.trigger('sourceopen');
sourceBuffer = this.mediaSource.sourceBuffers[0];
QUnit.equal(sourceBuffer.updates_.length, 1, 'called the source buffer once');
QUnit.deepEqual(sourceBuffer.updates_[0].append, new Uint8Array([0, 1, 2]),
'appended the bytes');
});
QUnit.test('runs the completion callback when updateend fires', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
let updateends = 0;
let sourceBuffer;
this.mediaSource.trigger('sourceopen');
sourceBuffer = this.mediaSource.sourceBuffers[0];
updater.appendBuffer(new Uint8Array([0, 1, 2]), function() {
updateends++;
});
updater.appendBuffer(new Uint8Array([2, 3, 4]), function() {
throw new Error('Wrong completion callback invoked!');
});
QUnit.equal(updateends, 0, 'no completions yet');
sourceBuffer.trigger('updateend');
QUnit.equal(updateends, 1, 'ran the completion callback');
});
QUnit.test('runs the next callback after updateend fires', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
let sourceBuffer;
updater.appendBuffer(new Uint8Array([0, 1, 2]));
this.mediaSource.trigger('sourceopen');
sourceBuffer = this.mediaSource.sourceBuffers[0];
updater.appendBuffer(new Uint8Array([2, 3, 4]));
QUnit.equal(sourceBuffer.updates_.length, 1, 'delayed the update');
sourceBuffer.trigger('updateend');
QUnit.equal(sourceBuffer.updates_.length, 2, 'updated twice');
QUnit.deepEqual(sourceBuffer.updates_[1].append, new Uint8Array([2, 3, 4]),
'appended the bytes');
});
QUnit.test('runs only one callback at a time', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
let sourceBuffer;
updater.appendBuffer(new Uint8Array([0]));
updater.appendBuffer(new Uint8Array([1]));
this.mediaSource.trigger('sourceopen');
sourceBuffer = this.mediaSource.sourceBuffers[0];
updater.appendBuffer(new Uint8Array([2]));
QUnit.equal(sourceBuffer.updates_.length, 1, 'queued some updates');
QUnit.deepEqual(sourceBuffer.updates_[0].append, new Uint8Array([0]),
'ran the first update');
sourceBuffer.trigger('updateend');
QUnit.equal(sourceBuffer.updates_.length, 2, 'queued some updates');
QUnit.deepEqual(sourceBuffer.updates_[1].append, new Uint8Array([1]),
'ran the second update');
updater.appendBuffer(new Uint8Array([3]));
sourceBuffer.trigger('updateend');
QUnit.equal(sourceBuffer.updates_.length, 3, 'queued the updates');
QUnit.deepEqual(sourceBuffer.updates_[2].append, new Uint8Array([2]),
'ran the third update');
sourceBuffer.trigger('updateend');
QUnit.equal(sourceBuffer.updates_.length, 4, 'finished the updates');
QUnit.deepEqual(sourceBuffer.updates_[3].append, new Uint8Array([3]),
'ran the fourth update');
});
QUnit.test('runs updates immediately if possible', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
let sourceBuffer;
this.mediaSource.trigger('sourceopen');
sourceBuffer = this.mediaSource.sourceBuffers[0];
updater.appendBuffer(new Uint8Array([0]));
QUnit.equal(sourceBuffer.updates_.length, 1, 'ran an update');
QUnit.deepEqual(sourceBuffer.updates_[0].append, new Uint8Array([0]),
'appended the bytes');
});
QUnit.test('supports abort', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
let sourceBuffer;
updater.abort();
this.mediaSource.trigger('sourceopen');
sourceBuffer = this.mediaSource.sourceBuffers[0];
QUnit.ok(sourceBuffer.updates_[0].abort, 'aborted the source buffer');
});
QUnit.test('supports buffered', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
QUnit.equal(updater.buffered().length, 0, 'buffered is empty');
this.mediaSource.trigger('sourceopen');
QUnit.ok(updater.buffered(), 'buffered is defined');
});
QUnit.test('supports removeBuffer', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
let sourceBuffer;
this.mediaSource.trigger('sourceopen');
sourceBuffer = this.mediaSource.sourceBuffers[0];
updater.remove(1, 14);
QUnit.equal(sourceBuffer.updates_.length, 1, 'ran an update');
QUnit.deepEqual(sourceBuffer.updates_[0].remove, [1, 14], 'removed the time range');
});
QUnit.test('supports setting duration', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
let sourceBuffer;
this.mediaSource.trigger('sourceopen');
sourceBuffer = this.mediaSource.sourceBuffers[0];
updater.duration(21);
QUnit.equal(sourceBuffer.updates_.length, 1, 'ran an update');
QUnit.deepEqual(sourceBuffer.updates_[0].duration, 21, 'changed duration');
});
QUnit.test('supports timestampOffset', function() {
let updater = new SourceUpdater(this.mediaSource, 'video/mp2t');
let sourceBuffer;
this.mediaSource.trigger('sourceopen');
sourceBuffer = this.mediaSource.sourceBuffers[0];
QUnit.equal(updater.timestampOffset(), 0, 'intialized to zero');
updater.timestampOffset(21);
QUnit.equal(updater.timestampOffset(), 21, 'reflects changes immediately');
QUnit.equal(sourceBuffer.timestampOffset, 21, 'applied the update');
updater.appendBuffer(new Uint8Array(2));
updater.timestampOffset(14);
QUnit.equal(updater.timestampOffset(), 14, 'reflects changes immediately');
QUnit.equal(sourceBuffer.timestampOffset, 21, 'queues application after updates');
sourceBuffer.trigger('updateend');
QUnit.equal(sourceBuffer.timestampOffset, 14, 'applied the update');
});
import document from 'global/document';
import sinon from 'sinon';
import videojs from 'video.js';
import QUnit from 'qunit';
/* eslint-disable no-unused-vars */
// needed so MediaSource can be registered with videojs
import MediaSource from 'videojs-contrib-media-sources';
/* eslint-enable */
import testDataManifests from './test-manifests.js';
import xhrFactory from '../src/xhr';
// a SourceBuffer that tracks updates but otherwise is a noop
class MockSourceBuffer extends videojs.EventTarget {
constructor() {
super();
this.updates_ = [];
this.updating = false;
this.on('updateend', function() {
this.updating = false;
});
this.buffered = videojs.createTimeRanges();
this.duration_ = NaN;
Object.defineProperty(this, 'duration', {
get() {
return this.duration_;
},
set(duration) {
this.updates_.push({
duration
});
this.duration_ = duration;
}
});
}
abort() {
this.updates_.push({
abort: true
});
}
appendBuffer(bytes) {
this.updates_.push({
append: bytes
});
this.updating = true;
}
remove(start, end) {
this.updates_.push({
remove: [start, end]
});
}
}
class MockMediaSource extends videojs.EventTarget {
constructor() {
super();
this.readyState = 'closed';
this.on('sourceopen', function() {
this.readyState = 'open';
});
this.sourceBuffers = [];
this.duration = NaN;
this.seekable = videojs.createTimeRange();
}
addSeekableRange_(start, end) {
this.seekable = videojs.createTimeRange(start, end);
}
addSourceBuffer(mime) {
let sourceBuffer = new MockSourceBuffer();
sourceBuffer.mimeType_ = mime;
this.sourceBuffers.push(sourceBuffer);
return sourceBuffer;
}
endOfStream(error) {
this.readyState = 'closed';
this.error_ = error;
}
}
export const useFakeMediaSource = function() {
let RealMediaSource = videojs.MediaSource;
let realCreateObjectURL = window.URL.createObjectURL;
let id = 0;
videojs.MediaSource = MockMediaSource;
videojs.MediaSource.supportsNativeMediaSources =
RealMediaSource.supportsNativeMediaSources;
videojs.URL.createObjectURL = function() {
id++;
return 'blob:videojs-contrib-hls-mock-url' + id;
};
return {
restore() {
videojs.MediaSource = RealMediaSource;
videojs.URL.createObjectURL = realCreateObjectURL;
}
};
};
let fakeEnvironment = {
requests: [],
restore() {
this.clock.restore();
videojs.xhr.XMLHttpRequest = window.XMLHttpRequest;
this.xhr.restore();
['warn', 'error'].forEach((level) => {
if (this.log && this.log[level] && this.log[level].restore) {
QUnit.equal(this.log[level].callCount, 0, `no unexpected logs on ${level}`);
this.log[level].restore();
}
});
}
};
export const useFakeEnvironment = function() {
fakeEnvironment.log = {};
['warn', 'error'].forEach((level) => {
// you can use .log[level].args to get args
sinon.stub(videojs.log, level);
fakeEnvironment.log[level] = videojs.log[level];
Object.defineProperty(videojs.log[level], 'calls', {
get() {
// reset callCount to 0 so they don't have to
let callCount = this.callCount;
this.callCount = 0;
return callCount;
}
});
});
fakeEnvironment.clock = sinon.useFakeTimers();
fakeEnvironment.xhr = sinon.useFakeXMLHttpRequest();
fakeEnvironment.requests.length = 0;
fakeEnvironment.xhr.onCreate = function(xhr) {
fakeEnvironment.requests.push(xhr);
};
videojs.xhr.XMLHttpRequest = fakeEnvironment.xhr;
return fakeEnvironment;
};
// patch over some methods of the provided tech so it can be tested
// synchronously with sinon's fake timers
export const mockTech = function(tech) {
if (tech.isMocked_) {
// make this function idempotent because HTML and Flash based
// playback have very different lifecycles. For HTML, the tech
// is available on player creation. For Flash, the tech isn't
// ready until the source has been loaded and one tick has
// expired.
return;
}
tech.isMocked_ = true;
tech.src_ = null;
tech.time_ = null;
tech.paused_ = !tech.autoplay();
tech.paused = function() {
return tech.paused_;
};
if (!tech.currentTime_) {
tech.currentTime_ = tech.currentTime;
}
tech.currentTime = function() {
return tech.time_ === null ? tech.currentTime_() : tech.time_;
};
tech.setSrc = function(src) {
tech.src_ = src;
};
tech.src = function(src) {
if (src !== null) {
return tech.setSrc(src);
}
return tech.src_ === null ? tech.src : tech.src_;
};
tech.currentSrc_ = tech.currentSrc;
tech.currentSrc = function() {
return tech.src_ === null ? tech.currentSrc_() : tech.src_;
};
tech.play_ = tech.play;
tech.play = function() {
tech.play_();
tech.paused_ = false;
tech.trigger('play');
};
tech.pause_ = tech.pause_;
tech.pause = function() {
tech.pause_();
tech.paused_ = true;
tech.trigger('pause');
};
tech.setCurrentTime = function(time) {
tech.time_ = time;
setTimeout(function() {
tech.trigger('seeking');
setTimeout(function() {
tech.trigger('seeked');
}, 1);
}, 1);
};
};
export const createPlayer = function(options) {
let video;
let player;
video = document.createElement('video');
video.className = 'video-js';
document.querySelector('#qunit-fixture').appendChild(video);
player = videojs(video, options || {
flash: {
swf: ''
}
});
player.buffered = function() {
return videojs.createTimeRange(0, 0);
};
mockTech(player.tech_);
return player;
};
export const openMediaSource = function(player, clock) {
// ensure the Flash tech is ready
player.tech_.triggerReady();
clock.tick(1);
// mock the tech *after* it has finished loading so that we don't
// mock a tech that will be unloaded on the next tick
mockTech(player.tech_);
player.tech_.hls.xhr = xhrFactory();
// simulate the sourceopen event
player.tech_.hls.mediaSource.readyState = 'open';
player.tech_.hls.mediaSource.dispatchEvent({
type: 'sourceopen',
swfId: player.tech_.el().id
});
};
export const standardXHRResponse = function(request) {
if (!request.url) {
return;
}
let contentType = 'application/json';
// contents off the global object
let manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
if (manifestName) {
manifestName = manifestName[1];
} else {
manifestName = request.url;
}
if (/\.m3u8?/.test(request.url)) {
contentType = 'application/vnd.apple.mpegurl';
} else if (/\.ts/.test(request.url)) {
contentType = 'video/MP2T';
}
request.response = new Uint8Array(16).buffer;
request.respond(200, { 'Content-Type': contentType },
testDataManifests[manifestName]);
};
// return an absolute version of a page-relative URL
export const absoluteUrl = function(relativeUrl) {
return window.location.protocol + '//' +
window.location.host +
(window.location.pathname
.split('/')
.slice(0, -1)
.concat(relativeUrl)
.join('/')
);
};
This diff could not be displayed because it is too large.
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "http://example.com/00001.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "https://example.com/00002.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "//example.com/00003.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "http://example.com/00004.ts"
}
],
......
......@@ -9,6 +9,7 @@
"offset": 0
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -17,6 +18,7 @@
"offset": 522828
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -25,6 +27,7 @@
"offset": 1110328
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -33,6 +36,7 @@
"offset": 1823412
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -41,6 +45,7 @@
"offset": 2299992
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -49,6 +54,7 @@
"offset": 2835604
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -57,6 +63,7 @@
"offset": 3042780
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -65,6 +72,7 @@
"offset": 3498680
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -73,6 +81,7 @@
"offset": 4155928
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -81,6 +90,7 @@
"offset": 4727636
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -89,6 +99,7 @@
"offset": 5212676
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -97,6 +108,7 @@
"offset": 5921812
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -105,6 +117,7 @@
"offset": 6651816
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -113,6 +126,7 @@
"offset": 7108092
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -121,6 +135,7 @@
"offset": 7576776
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -129,6 +144,7 @@
"offset": 8021772
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -137,6 +153,7 @@
"offset": 8353216
},
"duration": 1.4167,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -9,6 +9,7 @@
"offset": 0
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
{
allowCache: true,
discontinuityStarts: [],
mediaGroups: {
// TYPE
AUDIO: {
// GROUP-ID
"audio": {
// NAME
"English": {
language: 'eng',
autoselect: true,
default: true,
uri: "eng/prog_index.m3u8"
},
// NAME
"Français": {
language: "fre",
autoselect: true,
default: false,
uri: "fre/prog_index.m3u8"
},
// NAME
"Espanol": {
language: "sp",
autoselect: true,
default: false,
uri: "sp/prog_index.m3u8"
}
}
},
VIDEO: {},
"CLOSED-CAPTIONS": {},
SUBTITLES: {}
},
playlists: [{
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 195023,
CODECS: "avc1.42e00a,mp4a.40.2",
AUDIO: 'audio'
},
timeline: 0,
uri: "lo/prog_index.m3u8"
}, {
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 591680,
CODECS: "avc1.42e01e,mp4a.40.2",
AUDIO: 'audio'
},
timeline: 0,
uri: "hi/prog_index.m3u8"
}]
}
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="avc1.42e00a,mp4a.40.2",AUDIO="audio"
lo/prog_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="avc1.42e01e,mp4a.40.2",AUDIO="audio"
hi/prog_index.m3u8
\ No newline at end of file
{
allowCache: true,
discontinuityStarts: [],
mediaGroups: {
AUDIO: {
aac: {
English: {
autoselect: true,
default: true,
language: "eng",
uri: "eng/prog_index.m3u8"
}
}
},
VIDEO: {
"500kbs": {
Angle1: {
autoselect: true,
default: true
},
Angle2: {
autoselect: true,
default: false,
uri: "Angle2/500kbs/prog_index.m3u8"
},
Angle3: {
autoselect: true,
default: false,
uri: "Angle3/500kbs/prog_index.m3u8"
}
}
},
"CLOSED-CAPTIONS": {},
SUBTITLES: {}
},
playlists: [{
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 754857,
CODECS: "mp4a.40.2,avc1.4d401e",
AUDIO: "aac",
VIDEO: "500kbs"
},
timeline: 0,
uri: "Angle1/500kbs/prog_index.m3u8"
}]
}
#EXTM3U
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/500kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/500kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="500kbs",AUDIO="aac"
Angle1/500kbs/prog_index.m3u8
\ No newline at end of file
......@@ -10,6 +10,7 @@
"height": 224
}
},
"timeline": 0,
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
},
{
......@@ -17,6 +18,7 @@
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"timeline": 0,
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
},
{
......@@ -28,6 +30,7 @@
"height": 224
}
},
"timeline": 0,
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
},
{
......@@ -39,8 +42,15 @@
"height": 540
}
},
"timeline": 0,
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}
],
"discontinuityStarts": []
"discontinuityStarts": [],
"mediaGroups": {
"VIDEO": {},
"AUDIO": {},
"CLOSED-CAPTIONS": {},
"SUBTITLES": {}
}
}
......
......@@ -5,6 +5,7 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -13,6 +14,7 @@
"offset": 522828
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -21,6 +23,7 @@
"offset": 0
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video2.ts"
},
{
......@@ -29,6 +32,7 @@
"offset": 1823412
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -37,6 +41,7 @@
"offset": 2299992
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -45,6 +50,7 @@
"offset": 2835604
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -53,6 +59,7 @@
"offset": 3042780
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -61,6 +68,7 @@
"offset": 3498680
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -69,6 +77,7 @@
"offset": 4155928
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -77,6 +86,7 @@
"offset": 4727636
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -85,6 +95,7 @@
"offset": 5212676
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -93,6 +104,7 @@
"offset": 5921812
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -101,6 +113,7 @@
"offset": 6651816
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -109,6 +122,7 @@
"offset": 7108092
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -117,6 +131,7 @@
"offset": 7576776
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -125,6 +140,7 @@
"offset": 8021772
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -133,6 +149,7 @@
"offset": 8353216
},
"duration": 1.4167,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -9,6 +9,7 @@
"offset": 0
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -5,19 +5,23 @@
"segments": [
{
"duration": 10,
"timeline": 3,
"uri": "001.ts"
},
{
"duration": 19,
"timeline": 3,
"uri": "002.ts"
},
{
"discontinuity": true,
"duration": 10,
"timeline": 4,
"uri": "003.ts"
},
{
"duration": 11,
"timeline": 4,
"uri": "004.ts"
}
],
......
......@@ -5,41 +5,50 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "001.ts"
},
{
"duration": 19,
"timeline": 0,
"uri": "002.ts"
},
{
"discontinuity": true,
"duration": 10,
"timeline": 1,
"uri": "003.ts"
},
{
"duration": 11,
"timeline": 1,
"uri": "004.ts"
},
{
"discontinuity": true,
"duration": 10,
"timeline": 2,
"uri": "005.ts"
},
{
"duration": 10,
"timeline": 2,
"uri": "006.ts"
},
{
"duration": 10,
"timeline": 2,
"uri": "007.ts"
},
{
"discontinuity": true,
"duration": 10,
"timeline": 3,
"uri": "008.ts"
},
{
"duration": 16,
"timeline": 3,
"uri": "009.ts"
}
],
......
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "/00001.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/subdir/00002.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/00003.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/00004.ts"
}
],
......
......@@ -9,6 +9,7 @@
"offset": 0
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"timeline": 0,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"timeline": 0,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"timeline": 0,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
......
......@@ -4,26 +4,32 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
},
{
"duration": 8,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
}
],
......
......@@ -10,6 +10,7 @@
"height": 224
}
},
"timeline": 0,
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
},
{
......@@ -17,6 +18,7 @@
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"timeline": 0,
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
},
{
......@@ -28,6 +30,7 @@
"height": 224
}
},
"timeline": 0,
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
},
{
......@@ -39,8 +42,15 @@
"height": 540
}
},
"timeline": 0,
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}
],
"discontinuityStarts": []
"discontinuityStarts": [],
"mediaGroups": {
"VIDEO": {},
"AUDIO": {},
"CLOSED-CAPTIONS": {},
"SUBTITLES": {}
}
}
......
......@@ -6,6 +6,7 @@
"segments": [
{
"duration": 2.833,
"timeline": 0,
"key": {
"method": "AES-128",
"uri": "https://priv.example.com/key.php?r=52"
......@@ -14,6 +15,7 @@
},
{
"duration": 15,
"timeline": 0,
"key": {
"method": "AES-128",
"uri": "https://priv.example.com/key.php?r=52"
......@@ -22,6 +24,7 @@
},
{
"duration": 13.333,
"timeline": 0,
"key": {
"method": "AES-128",
"uri": "https://priv.example.com/key.php?r=52"
......@@ -30,6 +33,7 @@
},
{
"duration": 15,
"timeline": 0,
"key": {
"method": "AES-128",
"uri": "https://priv.example.com/key.php?r=53"
......@@ -38,6 +42,7 @@
},
{
"duration": 14,
"timeline": 0,
"key": {
"method": "AES-128",
"uri": "https://priv.example.com/key.php?r=54",
......@@ -47,6 +52,7 @@
},
{
"duration": 15,
"timeline": 0,
"uri": "http://media.example.com/fileSequence53-B.ts"
}
],
......
......@@ -5,26 +5,32 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
},
{
"duration": 8,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
}
],
......
......@@ -4,6 +4,7 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
}
],
......
......@@ -9,6 +9,7 @@
"offset": 0
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -17,6 +18,7 @@
"offset": 522828
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -25,6 +27,7 @@
"offset": 1110328
},
"duration": 5,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -33,6 +36,7 @@
"offset": 1823412
},
"duration": 9.7,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -41,6 +45,7 @@
"offset": 2299992
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -49,6 +54,7 @@
"offset": 2835604
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -57,6 +63,7 @@
"offset": 3042780
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -65,6 +72,7 @@
"offset": 3498680
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -73,6 +81,7 @@
"offset": 4155928
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -81,6 +90,7 @@
"offset": 4727636
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -89,6 +99,7 @@
"offset": 5212676
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -97,6 +108,7 @@
"offset": 5921812
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -105,6 +117,7 @@
"offset": 6651816
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -113,6 +126,7 @@
"offset": 7108092
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -121,6 +135,7 @@
"offset": 7576776
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -129,6 +144,7 @@
"offset": 8021772
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -137,6 +153,7 @@
"offset": 8353216
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -9,6 +9,7 @@
"offset": 0
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"timeline": 0,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"timeline": 0,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"timeline": 0,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
......
......@@ -4,26 +4,32 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
},
{
"duration": 8,
"timeline": 0,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
}
],
......
......@@ -9,6 +9,7 @@
"offset": 0
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -17,6 +18,7 @@
"offset": 522828
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -25,6 +27,7 @@
"offset": 1110328
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -33,6 +36,7 @@
"offset": 1823412
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -41,6 +45,7 @@
"offset": 2299992
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -49,6 +54,7 @@
"offset": 2835604
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -57,6 +63,7 @@
"offset": 3042780
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -65,6 +72,7 @@
"offset": 3498680
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -73,6 +81,7 @@
"offset": 4155928
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -81,6 +90,7 @@
"offset": 4727636
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -89,6 +99,7 @@
"offset": 5212676
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -97,6 +108,7 @@
"offset": 5921812
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -105,6 +117,7 @@
"offset": 6651816
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -113,6 +126,7 @@
"offset": 7108092
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -121,6 +135,7 @@
"offset": 7576776
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -129,6 +144,7 @@
"offset": 8021772
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -137,6 +153,7 @@
"offset": 8353216
},
"duration": 1.4167,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -5,14 +5,17 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 8,
"timeline": 0,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 8,
"timeline": 0,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
}
],
......
......@@ -4,38 +4,47 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "001.ts"
},
{
"duration": 19,
"timeline": 0,
"uri": "002.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "003.ts"
},
{
"duration": 11,
"timeline": 0,
"uri": "004.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "005.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "006.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "007.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "008.ts"
},
{
"duration": 16,
"timeline": 0,
"uri": "009.ts"
}
],
......
......@@ -4,6 +4,7 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
],
......
......@@ -4,22 +4,27 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/gogo/00002.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/gogo/00003.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/gogo/00004.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/gogo/00005.ts"
}
],
......
......@@ -4,6 +4,7 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
],
......
......@@ -10,6 +10,7 @@
"height": 224
}
},
"timeline": 0,
"uri": "media.m3u8"
},
{
......@@ -17,6 +18,7 @@
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"timeline": 0,
"uri": "media1.m3u8"
},
{
......@@ -28,6 +30,7 @@
"height": 224
}
},
"timeline": 0,
"uri": "media2.m3u8"
},
{
......@@ -39,8 +42,15 @@
"height": 540
}
},
"timeline": 0,
"uri": "media3.m3u8"
}
],
"discontinuityStarts": []
"discontinuityStarts": [],
"mediaGroups": {
"VIDEO": {},
"AUDIO": {},
"CLOSED-CAPTIONS": {},
"SUBTITLES": {}
}
}
......
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "media-00001.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "media-00002.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "media-00003.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "media-00004.ts"
}
],
......
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"timeline": 0,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"timeline": 0,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"timeline": 0,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
......
......@@ -4,10 +4,12 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "00001.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "00002.ts"
}
],
......
......@@ -5,14 +5,17 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"timeline": 0,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"timeline": 0,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"timeline": 0,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
......
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 8,
"timeline": 0,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 8,
"timeline": 0,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 8,
"timeline": 0,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
......
{
allowCache: true,
discontinuityStarts: [],
mediaGroups: {
AUDIO: {
"audio-lo": {
"English": {
autoselect: true,
default: true,
language: "eng",
uri: "englo/prog_index.m3u8"
},
"Français": {
autoselect: true,
default: false,
language: "fre",
uri: "frelo/prog_index.m3u8"
},
"Espanol": {
autoselect: true,
default: false,
language: "sp",
uri: "splo/prog_index.m3u8"
}
},
"audio-hi": {
"English": {
autoselect: true,
default: true,
language: "eng",
uri: "eng/prog_index.m3u8"
},
"Français": {
autoselect: true,
default: false,
language: "fre",
uri: "fre/prog_index.m3u8"
},
"Espanol": {
autoselect: true,
default: false,
language: "sp",
uri: "sp/prog_index.m3u8"
}
}
},
VIDEO: {},
"CLOSED-CAPTIONS": {},
SUBTITLES: {}
},
playlists: [{
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 195023,
CODECS: "mp4a.40.5",
AUDIO: "audio-lo",
},
timeline: 0,
uri: "lo/prog_index.m3u8"
}, {
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 260000,
CODECS: "avc1.42e01e,mp4a.40.2",
AUDIO: "audio-lo"
},
timeline: 0,
uri: "lo2/prog_index.m3u8"
}, {
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 591680,
CODECS: "mp4a.40.2, avc1.64001e",
AUDIO: "audio-hi"
},
timeline: 0,
uri: "hi/prog_index.m3u8"
}, {
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 650000,
CODECS: "avc1.42e01e,mp4a.40.2",
AUDIO: "audio-hi"
},
timeline: 0,
uri: "hi2/prog_index.m3u8"
}]
}
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="englo/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="frelo/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="splo/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5", AUDIO="audio-lo"
lo/prog_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-lo"
lo2/prog_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e", AUDIO="audio-hi"
hi/prog_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-hi"
hi2/prog_index.m3u8
{
allowCache: true,
discontinuityStarts: [],
mediaGroups: {
AUDIO: {
"audio-lo": {
"English": {
autoselect: true,
default: true,
language: "eng",
},
"Français": {
autoselect: true,
default: false,
language: "fre",
uri: "frelo/prog_index.m3u8"
},
"Espanol": {
autoselect: true,
default: false,
language: "sp",
uri: "splo/prog_index.m3u8"
}
},
"audio-hi": {
"English": {
autoselect: true,
default: true,
language: "eng",
uri: "eng/prog_index.m3u8"
},
"Français": {
autoselect: true,
default: false,
language: "fre",
uri: "fre/prog_index.m3u8"
},
"Espanol": {
autoselect: true,
default: false,
language: "sp",
uri: "sp/prog_index.m3u8"
}
}
},
VIDEO: {},
"CLOSED-CAPTIONS": {},
SUBTITLES: {}
},
playlists: [{
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 195023,
CODECS: "mp4a.40.5",
AUDIO: "audio-lo",
},
timeline: 0,
uri: "lo/prog_index.m3u8"
}, {
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 260000,
CODECS: "avc1.42e01e,mp4a.40.2",
AUDIO: "audio-lo"
},
timeline: 0,
uri: "lo2/prog_index.m3u8"
}, {
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 591680,
CODECS: "mp4a.40.2, avc1.64001e",
AUDIO: "audio-hi"
},
timeline: 0,
uri: "hi/prog_index.m3u8"
}, {
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 650000,
CODECS: "avc1.42e01e,mp4a.40.2",
AUDIO: "audio-hi"
},
timeline: 0,
uri: "hi2/prog_index.m3u8"
}]
}
#EXTM3U
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="frelo/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-lo",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="splo/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="eng",NAME="English",AUTOSELECT=YES, DEFAULT=YES,URI="eng/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="fre",NAME="Français",AUTOSELECT=YES, DEFAULT=NO,URI="fre/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio-hi",LANGUAGE="sp",NAME="Espanol",AUTOSELECT=YES, DEFAULT=NO,URI="sp/prog_index.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=195023,CODECS="mp4a.40.5", AUDIO="audio-lo"
lo/prog_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=260000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-lo"
lo2/prog_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=591680,CODECS="mp4a.40.2, avc1.64001e", AUDIO="audio-hi"
hi/prog_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=650000,CODECS="avc1.42e01e,mp4a.40.2", AUDIO="audio-hi"
hi2/prog_index.m3u8
......@@ -4,19 +4,23 @@
"targetDuration": 10,
"segments": [
{
"uri": "001.ts"
"uri": "001.ts",
"timeline": 0
},
{
"uri": "002.ts",
"duration": 9
"duration": 9,
"timeline": 0
},
{
"uri": "003.ts",
"duration": 7
"duration": 7,
"timeline": 0
},
{
"uri": "004.ts",
"duration": 10
"duration": 10,
"timeline": 0
}
],
"discontinuitySequence": 0,
......
{
allowCache: true,
discontinuityStarts: [],
mediaGroups: {
AUDIO: {
aac: {
English: {
autoselect: true,
default: true,
language: "eng",
uri: "eng/prog_index.m3u8"
}
}
},
VIDEO: {
"200kbs": {
Angle1: {
autoselect: true,
default: true
},
Angle2: {
autoselect: true,
default: false,
uri: "Angle2/200kbs/prog_index.m3u8"
},
Angle3: {
autoselect: true,
default: false,
uri: "Angle3/200kbs/prog_index.m3u8"
}
},
"500kbs": {
Angle1: {
autoselect: true,
default: true
},
Angle2: {
autoselect: true,
default: false,
uri: "Angle2/500kbs/prog_index.m3u8"
},
Angle3: {
autoselect: true,
default: false,
uri: "Angle3/500kbs/prog_index.m3u8"
}
}
},
"CLOSED-CAPTIONS": {},
SUBTITLES: {}
},
playlists: [{
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 300000,
CODECS: "mp4a.40.2,avc1.4d401e",
AUDIO: "aac",
VIDEO: "200kbs"
},
timeline: 0,
uri: "Angle1/200kbs/prog_index.m3u"
}, {
attributes: {
"PROGRAM-ID": 1,
BANDWIDTH: 754857,
CODECS: "mp4a.40.2,avc1.4d401e",
AUDIO: "aac",
VIDEO: "500kbs"
},
timeline: 0,
uri: "Angle1/500kbs/prog_index.m3u8"
}]
}
#EXTM3U
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/200kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="200kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/200kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle1",AUTOSELECT=YES,DEFAULT=YES
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle2",AUTOSELECT=YES,DEFAULT=NO,URI="Angle2/500kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="500kbs",NAME="Angle3",AUTOSELECT=YES,DEFAULT=NO,URI="Angle3/500kbs/prog_index.m3u8"
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="eng",NAME="English",AUTOSELECT=YES,DEFAULT=YES,URI="eng/prog_index.m3u8"
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=300000,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="200kbs",AUDIO="aac"
Angle1/200kbs/prog_index.m3u
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=754857,CODECS="mp4a.40.2,avc1.4d401e",VIDEO="500kbs",AUDIO="aac"
Angle1/500kbs/prog_index.m3u8
\ No newline at end of file
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"timeline": 0,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"timeline": 0,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"timeline": 0,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
......
......@@ -9,6 +9,7 @@
"offset": 0
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -17,6 +18,7 @@
"offset": 522828
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -25,6 +27,7 @@
"offset": 1110328
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -33,6 +36,7 @@
"offset": 1823412
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -41,6 +45,7 @@
"offset": 2299992
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -49,6 +54,7 @@
"offset": 2835604
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -57,6 +63,7 @@
"offset": 3042780
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -65,6 +72,7 @@
"offset": 3498680
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -73,6 +81,7 @@
"offset": 4155928
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -81,6 +90,7 @@
"offset": 4727636
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -89,6 +99,7 @@
"offset": 5212676
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -97,6 +108,7 @@
"offset": 5921812
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -105,6 +117,7 @@
"offset": 6651816
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -113,6 +126,7 @@
"offset": 7108092
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -121,6 +135,7 @@
"offset": 7576776
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -129,6 +144,7 @@
"offset": 8021772
},
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
},
{
......@@ -137,6 +153,7 @@
"offset": 8353216
},
"duration": 1.4167,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -5,6 +5,7 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
}
],
......
......@@ -5,11 +5,19 @@
"attributes": {
"PROGRAM-ID": 1
},
"timeline": 0,
"uri": "media.m3u8"
},
{
"timeline": 0,
"uri": "media1.m3u8"
}
],
"discontinuityStarts": []
"discontinuityStarts": [],
"mediaGroups": {
"VIDEO": {},
"AUDIO": {},
"CLOSED-CAPTIONS": {},
"SUBTITLES": {}
}
}
......
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 6.64,
"timeline": 0,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"timeline": 0,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"timeline": 0,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"timeline": 0,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
......
......@@ -5,6 +5,7 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "hls_450k_video.ts"
}
],
......
......@@ -5,18 +5,22 @@
"segments": [
{
"duration": 10,
"timeline": 0,
"uri": "http://example.com/00001.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "https://example.com/00002.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "//example.com/00003.ts"
},
{
"duration": 10,
"timeline": 0,
"uri": "http://example.com/00004.ts"
}
],
......
(function(videojs) {
var Component = videojs.getComponent('Component');
// -----------------
// AudioTrackMenuItem
// -----------------
//
var MenuItem = videojs.getComponent('MenuItem');
var AudioTrackMenuItem = videojs.extend(MenuItem, {
constructor: function(player, options) {
var track = options.track;
var tracks = player.audioTracks();
options.label = track.label || track.language || 'Unknown';
options.selected = track.enabled;
MenuItem.call(this, player, options);
this.track = track;
if (tracks) {
var changeHandler = videojs.bind(this, this.handleTracksChange);
tracks.addEventListener('change', changeHandler);
this.on('dispose', function() {
tracks.removeEventListener('change', changeHandler);
});
}
},
handleClick: function(event) {
var kind = this.track.kind;
var tracks = this.player_.audioTracks();
MenuItem.prototype.handleClick.call(this, event);
if (!tracks) return;
for (var i = 0; i < tracks.length; i++) {
var track = tracks[i];
if (track === this.track) {
track.enabled = true;
}
}
},
handleTracksChange: function(event) {
this.selected(this.track.enabled);
}
});
Component.registerComponent('AudioTrackMenuItem', AudioTrackMenuItem);
// -----------------
// AudioTrackButton
// -----------------
//
var MenuButton = videojs.getComponent('MenuButton');
var AudioTrackButton = videojs.extend(MenuButton, {
constructor: function(player, options) {
MenuButton.call(this, player, options);
this.el_.setAttribute('aria-label','Audio Menu');
var tracks = this.player_.audioTracks();
if (this.items.length <= 1) {
this.hide();
}
if (!tracks) {
return;
}
var updateHandler = videojs.bind(this, this.update);
tracks.addEventListener('removetrack', updateHandler);
tracks.addEventListener('addtrack', updateHandler);
this.player_.on('dispose', function() {
tracks.removeEventListener('removetrack', updateHandler);
tracks.removeEventListener('addtrack', updateHandler);
});
},
buildCSSClass() {
return 'vjs-subtitles-button ' + MenuButton.prototype.buildCSSClass.call(this);
},
createItems: function(items) {
items = items || [];
var tracks = this.player_.audioTracks();
if (!tracks) {
return items;
}
for (var i = 0; i < tracks.length; i++) {
var track = tracks[i];
items.push(new AudioTrackMenuItem(this.player_, {
'selectable': true,
'track': track
}));
}
return items;
}
});
Component.registerComponent('AudioTrackButton', AudioTrackButton);
})(window.videojs);
......@@ -9,28 +9,16 @@
<!-- video.js -->
<script src="../../node_modules/video.js/dist/video.js"></script>
<!-- Media Sources plugin -->
<script src="../../node_modules/videojs-contrib-media-sources/dist/videojs-media-sources.js"></script>
<!-- HLS plugin -->
<script src="../../src/videojs-hls.js"></script>
<!-- m3u8 handling -->
<script src="../../src/xhr.js"></script>
<script src="../../src/stream.js"></script>
<script src="../../src/m3u8/m3u8-parser.js"></script>
<script src="../../src/playlist.js"></script>
<script src="../../src/playlist-loader.js"></script>
<script src="../../dist/videojs-contrib-hls.js"></script>
<script src="../../node_modules/pkcs7/dist/pkcs7.unpad.js"></script>
<script src="../../src/decrypter.js"></script>
<!-- Track Selector plugin -->
<script src="audio-track-selector.js"></script>
<!-- player stats visualization -->
<link href="stats.css" rel="stylesheet">
<script src="../switcher/js/vendor/d3.min.js"></script>
<!-- debugging -->
<script src="../../src/bin-utils.js"></script>
<style>
body {
font-family: Arial, sans-serif;
......@@ -43,13 +31,12 @@
padding: 0 5px;
margin: 20px 0;
}
</style>
<script>
if (window.location.search === '?flash') {
videojs.options.techOrder = ['flash'];
input {
margin-top: 15px;
min-width: 450px;
padding: 5px;
}
</script>
</style>
</head>
<body>
......@@ -57,18 +44,50 @@
<p>The video below is an <a href="https://developer.apple.com/library/ios/documentation/networkinginternet/conceptual/streamingmediaguide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40008332-CH1-SW1">HTTP Live Stream</a>. On desktop browsers other than Safari, the HLS plugin will polyfill support for the format on top of the video.js Flash tech.</p>
<p>Due to security restrictions in Flash, you will have to load this page over HTTP(S) to see the example in action.</p>
</div>
<video id="video"
class="video-js vjs-default-skin"
height="300"
width="600"
controls>
<source
src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"
type="application/x-mpegURL">
<source
src="http://s3.amazonaws.com/_bc_dml/example-content/bipbop-id3/index.m3u8"
type="application/x-mpegURL">
</video>
<div id="fixture">
</div>
<form id="load-url">
<label>
URL to Load:
<input id="url-to-load" type="url" value="https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8">
</label>
<br>
<label>
Player Options:
<input id="player-options" type="text" value='{}'>
</label>
<br>
<label>
URL Content Type:
<input id="url-content-type" type="text" value="application/x-mpegURL">
</label>
<br>
<label>
Default Caption Track Label (blank for none):
<input id="caption-track" type="text" value="">
</label>
<br>
<label>
Technology Mode:
<input type="radio" name="technology-mode" value="auto" checked> Auto
<input type="radio" name="technology-mode" value="html"> HTML
<input type="radio" name="technology-mode" value="flash"> Flash
</label>
<br>
<label>
Autoplay:
<input type="radio" name="autoplay" value="on"> On
<input type="radio" name="autoplay" value="off" checked> Off
</label>
<br>
<br>
<button type="submit">Load</button>
</form>
<br>
<section class="stats">
<div class="player-stats">
<h2>Player Stats</h2>
......@@ -109,9 +128,97 @@
<script src="stats.js"></script>
<script>
videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf';
// initialize the player
var player = videojs('video').ready(function() {
function getCheckedValue(name) {
var radios = document.getElementsByName(name);
var value;
for (var i = 0, length = radios.length; i < length; i++) {
if (radios[i].checked) {
value = radios[i].value;
break;
}
}
return value;
}
function createPlayer(cb) {
if (window.stats_timer) {
clearInterval(window.stats_timer);
}
// dispose of existing player
if(window.player) {
window.player.dispose();
}
// create video element in the dom
var video = document.createElement('video');
video.id = 'videojs-contrib-hls-player';
video.className = 'video-js vjs-default-skin';
video.setAttribute('controls', true);
video.setAttribute('height', 300);
video.setAttribute('width', 600);
document.querySelector('#fixture').appendChild(video);
var techRadios = document.getElementsByName('technology-mode');
var techMode = getCheckedValue('technology-mode') || 'auto';
var autoplay = getCheckedValue('autoplay') || 'off';
var captionTrack = document.getElementById('caption-track').value || "";
var url = document.getElementById('url-to-load').value || "";
var options = {};
// try to parse options from the form
try {
options = JSON.parse(document.getElementById('player-options').value);
} catch(err) {
console.log("Reseting options to {}, JSON Parse Error:", err);
}
if(typeof options.techOrder === 'undefined') {
if (techMode === 'html') {
options.techOrder = ['html5'];
} else if (techMode === 'flash') {
options.techOrder = ['flash'];
}
}
if(typeof options.autoplay === 'undefined') {
if (autoplay === 'on') {
options.autoplay = true;
} else if (techMode === 'off') {
options.autoplay = false;
}
}
var type = document.getElementById('url-content-type').value;
// use the form data to add a src to the player
try {
window.player = videojs(video.id, options);
if(captionTrack) {
// hackey way to show captions
window.player.on('loadeddata', function(event) {
textTracks = this.textTracks().tracks_;
for(var i = 0; i < textTracks.length; i++) {
if(textTracks[i].label === captionTrack) {
console.log("Found matching track, going to show " + captionTrack);
textTracks[i].mode = "showing";
break;
}
}
});
}
window.player.src({
src: url,
type: type
});
cb(player);
} catch(err) {
console.log("caught an error trying to create and add src to player:", err);
}
}
function setup(player) {
player.ready(function() {
// ------------
// Audio Track Switcher
// ------------
player.controlBar.addChild('AudioTrackButton', {}, 13);
// ------------
// Player Stats
......@@ -128,7 +235,7 @@
currentTimeStat.textContent = player.currentTime().toFixed(1);
});
window.setInterval(function() {
window.stats_timer = window.setInterval(function() {
var bufferedText = '', oldStart, oldEnd, i;
// buffered
......@@ -184,23 +291,18 @@
videojs.Hls.displayStats(document.querySelector('.switching-stats'), player);
videojs.Hls.displayCues(document.querySelector('.segment-timeline'), player);
});
// -----------
// Tech Switch
// -----------
var techSwitch = document.createElement('a');
techSwitch.className = 'tech-switch';
if (player.el().querySelector('video')) {
techSwitch.href = window.location.origin + window.location.pathname + '?flash';
techSwitch.appendChild(document.createTextNode('Switch to the Flash tech'));
} else {
techSwitch.href = window.location.origin + window.location.pathname;
techSwitch.appendChild(document.createTextNode('Stop forcing Flash'));
}
document.body.insertBefore(techSwitch, document.querySelector('.stats'));
(function(window, videojs) {
videojs.options.flash.swf = '../../node_modules/videojs-swf/dist/video-js.swf';
createPlayer(setup);
// hook up the video switcher
document.getElementById('load-url').addEventListener('submit', function(event) {
event.preventDefault();
createPlayer(setup);
return false;
});
}(window, window.videojs));
</script>
</body>
</html>
......