216ff405 by David LaPalomento

Merge pull request #7 from brightcove/feature/basic-playback

Single-bitrate playback
2 parents 7c5d2ac4 2d70b254
Showing 103 changed files with 5291 additions and 1022 deletions
/node_modules/
*~
*.iml
*.swp
tmp/**
\ No newline at end of file
......
language: node_js
before_script:
- npm install -g grunt-cli
notifications:
hipchat:
rooms:
secure: l5TTd5JuPAW883PtcyaIBcJI9Chr9JpsZPQAEUBKAgIEwzuS6y7t5arlkS1PwH6gi1FADzYDf+OXSIou4GkTSrIetnBcT/SAgF0gBKgIhj+eRkuCfZ4VaC7BPhfZ0hgYRE+5Ejf5BM2MJafRm0pj7OlqG4xKrQZwtuV1te5r3JY=
'use strict';
var basename = require('path').basename;
module.exports = function(grunt) {
// Project configuration.
......@@ -12,7 +14,7 @@ module.exports = function(grunt) {
' Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %> */\n',
// Task configuration.
clean: {
files: ['dist']
files: ['build', 'dist', 'tmp']
},
concat: {
options: {
......@@ -26,13 +28,8 @@ module.exports = function(grunt) {
'src/h264-stream.js',
'src/aac-stream.js',
'src/segment-parser.js',
'src/segment-controller.js',
'src/m3u8/m3u8.js',
'src/m3u8/m3u8-tag-types.js',
'src/m3u8/m3u8-parser.js',
'src/manifest-controller.js',
'src/segment-controller.js',
'src/hls-playback-controller.js'],
'src/m3u8/m3u8-parser.js'
],
dest: 'dist/videojs.hls.js'
},
},
......@@ -65,7 +62,10 @@ module.exports = function(grunt) {
options: {
jshintrc: 'test/.jshintrc'
},
src: ['test/**/*.js', '!test/tsSegment.js', '!test/fixtures/*.js']
src: ['test/**/*.js',
'!test/tsSegment.js',
'!test/fixtures/*.js',
'!test/manifest/**']
},
},
watch: {
......@@ -92,8 +92,57 @@ module.exports = function(grunt) {
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch');
grunt.registerTask('manifests-to-js', 'Wrap the test fixtures and output' +
' so they can be loaded in a browser',
function() {
var
jsManifests = 'window.manifests = {\n',
jsExpected = 'window.expected = {\n';
grunt.file.recurse('test/manifest/',
function(abspath, root, sub, filename) {
if ((/\.m3u8$/).test(abspath)) {
// translate this manifest
jsManifests += ' \'' + basename(filename, '.m3u8') + '\': ' +
grunt.file.read(abspath)
.split('\n')
// quote and concatenate
.map(function(line) {
return ' \'' + line + '\\n\' +\n';
}).join('')
// strip leading spaces and the trailing '+'
.slice(4, -3);
jsManifests += ',\n';
}
if ((/\.json$/).test(abspath)) {
// append the JSON
jsExpected += ' "' + basename(filename, '.json') + '": ' +
grunt.file.read(abspath) + ',\n';
}
});
// clean up and close the objects
jsManifests = jsManifests.slice(0, -2);
jsManifests += '\n};\n';
jsExpected = jsExpected.slice(0, -2);
jsExpected += '\n};\n';
// write out the manifests
grunt.file.write('tmp/manifests.js', jsManifests);
grunt.file.write('tmp/expected.js', jsExpected);
});
// Default task.
grunt.registerTask('default',
['jshint', 'qunit', 'clean', 'concat', 'uglify']);
['clean',
'jshint',
'manifests-to-js',
'qunit',
'concat',
'uglify']);
};
......
[![Build Status](https://travis-ci.org/brightcove/videojs-contrib-hls.png)](https://travis-ci.org/brightcove/videojs-contrib-hls)
# video.js HLS Plugin
A video.js plugin that plays HLS video on platforms that don't support it but have Flash.
## Getting Started
Download the [production version][min] or the [development version][max].
[min]: https://raw.bithub.com/dlapalomento/video-js-hls/master/dist/videojs-hls.min.js
[max]: https://raw.bithub.com/dlapalomento/video-js-hls/master/dist/videojs-hls.js
In your web page:
Download the [plugin](https://raw.github.com/videojs/videojs-contrib-hls/master/dist/videojs-hls.min.js). On your web page:
```html
<script src="video.js"></script>
<script src="dist/videojs-hls.min.js"></script>
<script src="videojs-hls.min.js"></script>
<script>
var player = videojs('video');
player.hls();
player.hls('http://example.com/video.m3u8');
player.play();
</script>
```
## Documentation
_(Coming soon)_
[HTTP Live Streaming](https://developer.apple.com/streaming/) (HLS) has
become a de-facto standard for streaming video on mobile devices
thanks to its native support on iOS and Android. There are a number of
reasons independent of platform to recommend the format, though:
- Supports (client-driven) adaptive bitrate selection
- Delivered over standard HTTP ports
- Simple, text-based manifest format
- No proprietary streaming servers required
Unfortunately, all the major desktop browsers except for Safari are
missing HLS support. That leaves web developers in the unfortunate
position of having to maintain alternate renditions of the same video
and potentially having to forego HTML-based video entirely to provide
the best desktop viewing experience.
This plugin attempts to address that situation by providing a polyfill
for HLS on browsers that have Flash support. You can deploy a single
HLS stream, code against the regular HTML5 video APIs, and create a
fast, high-quality video experience across all the big web device
categories.
The videojs-hls plugin is still working towards a 1.0 release so it
may not fit your requirements today. Specifically, there is _no_
support for:
- Alternate audio and video tracks
- Subtitles
- Segment codecs _other than_ H.264 with AAC audio
- Live streams
- Internet Explorer < 10
### Runtime Properties
#### player.hls.master
Type: `object`
An object representing the parsed master playlist. If a media playlist
is loaded directly, a master playlist with only one entry will be
created.
#### player.hls.media
Type: `object`
An object representing the currently selected media playlist. This is
the playlist that is being referred to when a additional video data
needs to be downloaded.
#### player.hls.mediaIndex
Type: `number`
The index of the next video segment to be downloaded from
`player.hls.media`.
#### player.hls.selectPlaylist
Type: `function`
A function that returns the media playlist object to use to download
the next segment. It is invoked by the plugin immediately before a new
segment is downloaded. You can override this function to provide your
adaptive streaming logic. You must, however, be sure to return a valid
media playlist object that is present in `player.hls.master`.
### Events
#### loadedmetadata
Fired after the first media playlist is downloaded for a stream.
#### loadedmanifest
Fired immediately after a new master or media playlist has been
downloaded. By default, the plugin only downloads playlists as they
are needed.
## Hosting Considerations
Unlike a native HLS implementation, the HLS plugin has to comply with
the browser's security policies. That means that all the files that
make up the stream must be served from the same domain as the page
hosting the video player or from a server that has appropriate [CORS
headers](https://developer.mozilla.org/en-US/docs/HTTP/Access_control_CORS)
configured. Easy [instructions are
available](http://enable-cors.org/server.html) for popular webservers
and most CDNs should have no trouble turning CORS on for your account.
## MBR Rendition Selection Logic
In situations where manifests have multiple renditions, the player will
go through the following algorithm to determine the best rendition by
bandwidth and viewport dimensions.
## Examples
_(Coming soon)_
- Start on index 0 as defined in the HLS Spec (link above)
- On a successful load complete per segment determine the following;
- player.hls.bandwidth set to value as segment byte size over download time
- Viewport width/height as determined by player.width()/player.height()
- Playlists mapped and sorted by BANDWIDTH less than or equal to 1.1x player.hls.bandwidth
- Best playlist variant by BANDWIDTH determined
- Subset of bandwidth appropriate renditions mapped
- Subset validated for RESOLUTION attributes less than or equal to player dimensions
- Best playlist variant by RESOLUTION determined
- Result is as follows;
- [Best RESOLUTION variant] OR [Best BANDWIDTH variant] OR [inital playlist in manifest]
## Release History
_(Nothing yet)_
......
......@@ -4,16 +4,16 @@
<meta charset="utf-8">
<title>video.js HLS Plugin Example</title>
<link href="node_modules/video.js/video-js.css" rel="stylesheet">
<link href="node_modules/video.js/dist/video-js/video-js.css" rel="stylesheet">
<!-- video.js -->
<script src="node_modules/video.js/video.dev.js"></script>
<script src="node_modules/video.js/dist/video-js/video.js"></script>
<!-- Media Sources plugin -->
<script src="node_modules/videojs-media-sources/videojs-media-sources.js"></script>
<script src="node_modules/videojs-contrib-media-sources/videojs-media-sources.js"></script>
<!-- HLS plugin -->
<script src="src/video-js-hls.js"></script>
<script src="src/videojs-hls.js"></script>
<!-- segment handling -->
<script src="src/flv-tag.js"></script>
......@@ -21,15 +21,10 @@
<script src="src/h264-stream.js"></script>
<script src="src/aac-stream.js"></script>
<script src="src/segment-parser.js"></script>
<script src="src/segment-controller.js"></script>
<!-- m3u8 handling -->
<script src="src/m3u8/m3u8.js"></script>
<script src="src/m3u8/m3u8-tag-types.js"></script>
<script src="src/stream.js"></script>
<script src="src/m3u8/m3u8-parser.js"></script>
<script src="src/manifest-controller.js"></script>
<script src="src/segment-controller.js"></script>
<script src="src/hls-playback-controller.js"></script>
<!-- example MPEG2-TS segments -->
<!-- bipbop -->
......@@ -44,20 +39,17 @@
height="300"
width="600"
controls>
<source
src="http://solutions.brightcove.com/jwhisenant/hls/apple/bipbop/bipbopall.m3u8"
type="application/x-mpegURL">
</video>
<script>
var video, mediaSource;
videojs.options.flash.swf = 'node_modules/video.js/dist/video-js/video-js.swf';
// initialize the player
videojs.options.flash.swf = 'node_modules/videojs-media-sources/video-js-with-mse.swf';
video = videojs('video',{},function(){
this.playbackController = new window.videojs.hls.HLSPlaybackController(this);
this.playbackController.loadManifest('test/fixtures/prog_index.m3u8', function(data) {
console.log(data);
});
});
</script>
var player = videojs('video');
// initialize the plugin
player.hls();
</script>
</body>
</html>
......
......@@ -6,7 +6,7 @@
},
"license": "Apache 2",
"scripts": {
"test": "grunt qunit"
"test": "grunt"
},
"devDependencies": {
"grunt-contrib-jshint": "~0.6.0",
......@@ -18,7 +18,7 @@
"grunt": "~0.4.1"
},
"dependencies": {
"video.js": "~4.2.2",
"videojs-contrib-media-sources": "git+ssh://git@github.com/videojs/videojs-contrib-media-sources.git"
"video.js": "git+https://github.com/dmlap/video-js.git#v4.3.0-10",
"videojs-contrib-media-sources": "git+https://github.com/dmlap/videojs-contrib-media-sources.git#hotfix/misc-fixes"
}
}
......
......@@ -296,8 +296,8 @@ hls.FlvTag = function(type, extraData) {
// trim down the byte buffer to what is actually being used
this.bytes = this.bytes.subarray(0, this.length);
this.frameTime = hls.FlvTag.frameTime(this.bytes);
console.assert(this.bytes.byteLength === this.length);
return this;
};
};
......
(function(window) {
var
ManifestController = window.videojs.hls.ManifestController,
SegmentController = window.videojs.hls.SegmentController,
MediaSource = window.videojs.MediaSource,
SegmentParser = window.videojs.hls.SegmentParser;
window.videojs.hls.HLSPlaybackController = function(player) {
var self = this;
self.player = player;
self.mediaSource = new MediaSource();
self.parser = new SegmentParser();
self.manifestLoaded = false;
self.currentSegment = 0;
// register external callbacks
self.rendition = function(rendition) {
self.currentRendition = rendition;
self.loadManifestWithMediaSources(self.currentRendition.url, self.onM3U8LoadComplete, self.onM3U8LoadError, self.onM3U8Update);
};
self.loadManifestWithMediaSources = function(manifestUrl,onDataCallback) {
self.manifestController = new ManifestController();
self.manifestController.loadManifest(manifestUrl, self.onM3U8LoadComplete, self.onM3U8LoadError, self.onM3U8Update);
if (onDataCallback) {
self.manifestLoadCompleteCallback = onDataCallback;
}
};
self.loadManifest = function(manifestUrl, onDataCallback) {
self.mediaSource.addEventListener('sourceopen', function() {
// feed parsed bytes into the player
self.sourceBuffer = self.mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
self.parser = new SegmentParser();
self.sourceBuffer.appendBuffer(self.parser.getFlvHeader(), self.player);
if (onDataCallback) {
self.manifestLoadCompleteCallback = onDataCallback;
}
self.manifestController = new ManifestController();
self.manifestController.loadManifest(manifestUrl, self.onM3U8LoadComplete, self.onM3U8LoadError, self.onM3U8Update);
}, false);
self.player.src({
src: window.videojs.URL.createObjectURL(self.mediaSource),
type: "video/flv"
});
};
self.onM3U8LoadComplete = function(m3u8) {
if (m3u8.invalidReasons.length === 0) {
if (m3u8.isPlaylist) {
self.currentPlaylist = m3u8;
self.rendition(self.currentPlaylist.playlistItems[0]);
} else {
self.currentManifest = m3u8;
self.manifestLoaded = true;
self.loadSegment(self.currentManifest.mediaItems[0]);
if (self.manifestLoadCompleteCallback) {
self.manifestLoadCompleteCallback(m3u8);
}
}
}
};
self.onM3U8LoadError = function() {};
self.onM3U8Update = function() {};
self.loadSegment = function(segment) {
self.segmentController = new SegmentController();
self.segmentController.loadSegment(segment.url, self.onSegmentLoadComplete, self.onSegmentLoadError);
};
self.onSegmentLoadComplete = function(segment) {
self.parser.parseSegmentBinaryData(segment.binaryData);
while (self.parser.tagsAvailable()) {
self.sourceBuffer.appendBuffer(self.parser.getNextTag().bytes, self.player);
}
if (self.currentSegment < self.currentManifest.mediaItems.length-1) {
self.loadNextSegment();
}
};
self.loadNextSegment = function() {
self.currentSegment++;
self.loadSegment(self.currentManifest.mediaItems[self.currentSegment]);
};
self.onSegmentLoadError = function() {};
};
})(this);
(function(window) {
var M3U8 = window.videojs.hls.M3U8;
/**
* Utilities for parsing M3U8 files. If the entire manifest is available,
* `Parser` will create a object representation with enough detail for managing
* playback. `ParseStream` and `LineStream` are lower-level parsing primitives
* that do not assume the entirety of the manifest is ready and expose a
* ReadableStream-like interface.
*/
(function(videojs, parseInt, isFinite, mergeOptions, undefined) {
var
noop = function() {},
parseAttributes = function(attributes) {
var
attrs = attributes.split(','),
i = attrs.length,
result = {},
attr;
window.videojs.hls.M3U8Parser = function() {
var
self = this,
tagTypes = window.videojs.hls.m3u8TagType,
lines = [],
data;
self.getTagType = function(lineData) {
for (var s in tagTypes) {
if (lineData.indexOf(tagTypes[s]) === 0) {
return tagTypes[s];
while (i--) {
attr = attrs[i].split('=');
attr[0] = attr[0].replace(/^\s+|\s+$/g, '');
// This is not sexy, but gives us the resulting object we want.
if (attr[1]) {
attr[1] = attr[1].replace(/^\s+|\s+$/g, '');
if (attr[1].indexOf('"') !== -1) {
attr[1] = attr[1].split('"')[1];
}
result[attr[0]] = attr[1];
} else {
attrs[i - 1] = attrs[i - 1] + ',' + attr[0];
}
}
};
return result;
},
Stream = videojs.hls.Stream,
LineStream,
ParseStream,
Parser;
self.getTagValue = function(lineData) {
for (var s in tagTypes) {
if (lineData.indexOf(tagTypes[s]) === 0) {
return lineData.substr(tagTypes[s].length);
}
/**
* A stream that buffers string input and generates a `data` event for each
* line.
*/
LineStream = function() {
var buffer = '';
LineStream.prototype.init.call(this);
/**
* Add new data to be parsed.
* @param data {string} the text to process
*/
this.push = function(data) {
var nextNewline;
buffer += data;
nextNewline = buffer.indexOf('\n');
for (; nextNewline > -1; nextNewline = buffer.indexOf('\n')) {
this.trigger('data', buffer.substring(0, nextNewline));
buffer = buffer.substring(nextNewline + 1);
}
};
};
LineStream.prototype = new Stream();
self.parse = function(rawDataString) {
data = new M3U8();
/**
* A line-level M3U8 parser event stream. It expects to receive input one
* line at a time and performs a context-free parse of its contents. A stream
* interpretation of a manifest can be useful if the manifest is expected to
* be too large to fit comfortably into memory or the entirety of the input
* is not immediately available. Otherwise, it's probably much easier to work
* with a regular `Parser` object.
*
* Produces `data` events with an object that captures the parser's
* interpretation of the input. That object has a property `tag` that is one
* of `uri`, `comment`, or `tag`. URIs only have a single additional
* property, `line`, which captures the entirety of the input without
* interpretation. Comments similarly have a single additional property
* `text` which is the input without the leading `#`.
*
* Tags always have a property `tagType` which is the lower-cased version of
* the M3U8 directive without the `#EXT` or `#EXT-X-` prefix. For instance,
* `#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.
*/
ParseStream = function() {
ParseStream.prototype.init.call(this);
};
ParseStream.prototype = new Stream();
/**
* Parses an additional line of input.
* @param line {string} a single line of an M3U8 file to parse
*/
ParseStream.prototype.push = function(line) {
var match, event;
if (line.length === 0) {
// ignore empty lines
return;
}
if (self.directory) {
data.directory = self.directory;
}
// URIs
if (line[0] !== '#') {
this.trigger('data', {
type: 'uri',
uri: line
});
return;
}
// Comments
if (line.indexOf('#EXT') !== 0) {
this.trigger('data', {
type: 'comment',
text: line.slice(1)
});
return;
}
if (rawDataString === undefined || rawDataString.length <= 0) {
data.invalidReasons.push("Empty Manifest");
return;
// Tags
match = /^#EXTM3U/.exec(line);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'm3u'
});
return;
}
match = (/^#EXTINF:?([0-9\.]*)?,?(.*)?$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'inf'
};
if (match[1]) {
event.duration = parseFloat(match[1], 10);
}
lines = rawDataString.split('\n');
if (match[2]) {
event.title = match[2];
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-TARGETDURATION:?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'targetduration'
};
if (match[1]) {
event.duration = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#ZEN-TOTAL-DURATION:?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'totalduration'
};
if (match[1]) {
event.duration = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-VERSION:?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'version'
};
if (match[1]) {
event.version = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-MEDIA-SEQUENCE:?(\-?[0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'media-sequence'
};
if (match[1]) {
event.number = parseInt(match[1], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-PLAYLIST-TYPE:?(.*)?$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'playlist-type'
};
if (match[1]) {
event.playlistType = match[1];
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-BYTERANGE:?([0-9.]*)?@?([0-9.]*)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'byterange'
};
if (match[1]) {
event.length = parseInt(match[1], 10);
}
if (match[2]) {
event.offset = parseInt(match[2], 10);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-ALLOW-CACHE:?(YES|NO)?/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'allow-cache'
};
if (match[1]) {
event.allowed = !(/NO/).test(match[1]);
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-STREAM-INF:?(.*)$/).exec(line);
if (match) {
event = {
type: 'tag',
tagType: 'stream-inf'
};
if (match[1]) {
event.attributes = parseAttributes(match[1]);
lines.forEach(function(value,index) {
var segment, rendition, attributes;
if (event.attributes.RESOLUTION) {
(function() {
var
split = event.attributes.RESOLUTION.split('x'),
resolution = {};
if (split[0]) {
resolution.width = parseInt(split[0], 10);
}
if (split[1]) {
resolution.height = parseInt(split[1], 10);
}
event.attributes.RESOLUTION = resolution;
})();
}
if (event.attributes.BANDWIDTH) {
event.attributes.BANDWIDTH = parseInt(event.attributes.BANDWIDTH, 10);
}
if (event.attributes['PROGRAM-ID']) {
event.attributes['PROGRAM-ID'] = parseInt(event.attributes['PROGRAM-ID'], 10);
}
}
this.trigger('data', event);
return;
}
match = (/^#EXT-X-ENDLIST/).exec(line);
if (match) {
this.trigger('data', {
type: 'tag',
tagType: 'endlist'
});
return;
}
switch (self.getTagType(value)) {
case tagTypes.EXTM3U:
data.hasValidM3UTag = (index === 0);
if (!data.hasValidM3UTag) {
data.invalidReasons.push("Invalid EXTM3U Tag");
}
break;
// unknown tag type
this.trigger('data', {
type: 'tag',
data: line.slice(4, line.length)
});
};
case tagTypes.DISCONTINUITY:
break;
/**
* A parser for M3U8 files. The current interpretation of the input is
* exposed as a property `manifest` on parser objects. It's just two lines to
* create and parse a manifest once you have the contents available as a string:
*
* ```js
* var parser = new videojs.m3u8.Parser();
* parser.push(xhr.responseText);
* ```
*
* New input can later be applied to update the manifest object by calling
* `push` again.
*
* The parser attempts to create a usable manifest object even if the
* 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.
*/
Parser = function() {
var
self = this,
uris = [],
currentUri = {};
Parser.prototype.init.call(this);
case tagTypes.PLAYLIST_TYPE:
if (self.getTagValue(value) === "VOD" ||
self.getTagValue(value) === "EVENT") {
data.playlistType = self.getTagValue(value);
this.lineStream = new LineStream();
this.parseStream = new ParseStream();
this.lineStream.pipe(this.parseStream);
} else {
data.invalidReasons.push("Invalid Playlist Type Value");
}
break;
case tagTypes.EXTINF:
segment = {
url: "unknown",
byterange: -1,
targetDuration: data.targetDuration
};
if (self.getTagType(lines[index + 1]) === tagTypes.BYTERANGE) {
segment.byterange = self.getTagValue(lines[index + 1]).split('@');
segment.url = lines[index + 2];
} else {
segment.url = lines[index + 1];
}
// the manifest is empty until the parse stream begins delivering data
this.manifest = {
allowCache: true
};
if (segment.url.indexOf("http") === -1 && self.directory) {
if (data.directory[data.directory.length-1] === segment.url[0] &&
segment.url[0] === "/") {
segment.url = segment.url.substr(1);
}
segment.url = self.directory + segment.url;
}
data.mediaItems.push(segment);
break;
case tagTypes.STREAM_INF:
rendition = {};
attributes = value.substr(tagTypes.STREAM_INF.length).split(',');
attributes.forEach(function(attrValue) {
if (isNaN(attrValue.split('=')[1])) {
rendition[attrValue.split('=')[0].toLowerCase()] = attrValue.split('=')[1];
if (rendition[attrValue.split('=')[0].toLowerCase()].split('x').length === 2) {
rendition.resolution = {
width: parseInt(rendition[attrValue.split('=')[0].toLowerCase()].split('x')[0],10),
height: parseInt(rendition[attrValue.split('=')[0].toLowerCase()].split('x')[1],10)
};
// update the manifest with the m3u8 entry from the parse stream
this.parseStream.on('data', function(entry) {
({
tag: function() {
// switch based on the tag type
(({
'allow-cache': function() {
this.manifest.allowCache = entry.allowed;
if (!('allowed' in entry)) {
this.trigger('info', {
message: 'defaulting allowCache to YES'
});
this.manifest.allowCache = true;
}
} else {
rendition[attrValue.split('=')[0].toLowerCase()] = parseInt(attrValue.split('=')[1],10);
}
});
},
'byterange': function() {
var byterange = {};
if ('length' in entry) {
currentUri.byterange = byterange;
byterange.length = entry.length;
if (self.getTagType(lines[index + 1]) === tagTypes.BYTERANGE) {
rendition.byterange = self.getTagValue(lines[index + 1]).split('@');
rendition.url = lines[index + 2];
} else {
rendition.url = lines[index + 1];
}
data.isPlaylist = true;
data.playlistItems.push(rendition);
break;
if (!('offset' in entry)) {
this.trigger('info', {
message: 'defaulting offset to zero'
});
entry.offset = 0;
}
}
if ('offset' in entry) {
currentUri.byterange = byterange;
byterange.offset = entry.offset;
}
},
'inf': function() {
if (!this.manifest.playlistType) {
this.manifest.playlistType = 'VOD';
this.trigger('info', {
message: 'defaulting playlist type to VOD'
});
}
if (!('mediaSequence' in this.manifest)) {
this.manifest.mediaSequence = 0;
this.trigger('info', {
message: 'defaulting media sequence to zero'
});
}
if (entry.duration >= 0) {
currentUri.duration = entry.duration;
}
case tagTypes.TARGETDURATION:
data.targetDuration = parseFloat(self.getTagValue(value).split(',')[0]);
break;
this.manifest.segments = uris;
case tagTypes.ZEN_TOTAL_DURATION:
data.totalDuration = parseFloat(self.getTagValue(value));
break;
},
'media-sequence': function() {
if (!isFinite(entry.number)) {
this.trigger('warn', {
message: 'ignoring invalid media sequence: ' + entry.number
});
return;
}
this.manifest.mediaSequence = entry.number;
},
'playlist-type': function() {
if (!(/VOD|EVENT/).test(entry.playlistType)) {
this.trigger('warn', {
message: 'ignoring unknown playlist type: ' + entry.playlist
});
return;
}
this.manifest.playlistType = entry.playlistType;
},
'stream-inf': function() {
this.manifest.playlists = uris;
case tagTypes.VERSION:
data.version = parseFloat(self.getTagValue(value));
break;
if (!entry.attributes) {
this.trigger('warn', {
message: 'ignoring empty stream-inf attributes'
});
return;
}
case tagTypes.MEDIA_SEQUENCE:
data.mediaSequence = parseInt(self.getTagValue(value),10);
break;
if (!currentUri.attributes) {
currentUri.attributes = {};
}
currentUri.attributes = mergeOptions(currentUri.attributes,
entry.attributes);
},
'targetduration': function() {
if (!isFinite(entry.duration) || entry.duration < 0) {
this.trigger('warn', {
message: 'ignoring invalid target duration: ' + entry.duration
});
return;
}
this.manifest.targetDuration = entry.duration;
},
'totalduration': function() {
if (!isFinite(entry.duration) || entry.duration < 0) {
this.trigger('warn', {
message: 'ignoring invalid total duration: ' + entry.duration
});
return;
}
this.manifest.totalDuration = entry.duration;
}
})[entry.tagType] || noop).call(self);
},
uri: function() {
currentUri.uri = entry.uri;
uris.push(currentUri);
case tagTypes.ALLOW_CACHE:
if (self.getTagValue(value) === "YES" || self.getTagValue(value) === "NO") {
data.allowCache = self.getTagValue(value);
} else {
data.invalidReasons.push("Invalid ALLOW_CACHE Value");
// if no explicit duration was declared, use the target duration
if (this.manifest.targetDuration &&
!('duration' in currentUri)) {
this.trigger('warn', {
message: 'defaulting segment duration to the target duration'
});
currentUri.duration = this.manifest.targetDuration;
}
break;
case tagTypes.ENDLIST:
data.hasEndTag = true;
break;
// prepare for the next URI
currentUri = {};
},
comment: function() {
// comments are not important for playback
}
});
})[entry.type].call(self);
});
};
Parser.prototype = new Stream();
/**
* Parse the input string and update the manifest object.
* @param chunk {string} a potentially incomplete portion of the manifest
*/
Parser.prototype.push = function(chunk) {
this.lineStream.push(chunk);
};
/**
* Flush any remaining input. This can be handy if the last line of an M3U8
* manifest did not contain a trailing newline but the file has been
* completely received.
*/
Parser.prototype.end = function() {
// flush any buffered input
this.lineStream.push('\n');
};
return data;
};
window.videojs.m3u8 = {
LineStream: LineStream,
ParseStream: ParseStream,
Parser: Parser
};
})(this);
})(window.videojs, window.parseInt, window.isFinite, window.videojs.util.mergeOptions);
......
(function(window) {
window.videojs.hls.m3u8TagType = {
/*
* Derived from the HTTP Live Streaming Spec V8
* http://tools.ietf.org/html/draft-pantos-http-live-streaming-08
*/
/**
* Identifies manifest as Extended M3U - must be present on first line!
*/
EXTM3U:"#EXTM3U",
/**
* Specifies duration.
* Syntax: #EXTINF:<duration>,<title>
* Example: #EXTINF:10,
*/
EXTINF:"#EXTINF:",
/**
* Indicates that a media segment is a sub-range of the resource identified by its media URI.
* Syntax: #EXT-X-BYTERANGE:<n>[@o]
*/
BYTERANGE:"#EXT-X-BYTERANGE:",
/**
* Specifies the maximum media segment duration - applies to entire manifest.
* Syntax: #EXT-X-TARGETDURATION:<s>
* Example: #EXT-X-TARGETDURATION:10
*/
TARGETDURATION:"#EXT-X-TARGETDURATION:",
/**
* Specifies the sequence number of the first URI in a manifest.
* Syntax: #EXT-X-MEDIA-SEQUENCE:<i>
* Example: #EXT-X-MEDIA-SEQUENCE:50
*/
MEDIA_SEQUENCE:"#EXT-X-MEDIA-SEQUENCE:",
/**
* Specifies a method by which media segments can be decrypted, if encryption is present.
* Syntax: #EXT-X-KEY:<attribute-list>
* Note: This is likely irrelevant in the context of the Flash Player.
*/
KEY:"#EXT-X-KEY:",
/**
* Associates the first sample of a media segment with an absolute date and/or time. Applies only to the next media URI.
* Syntax: #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>
* Example: #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00
*/
PROGRAM_DATE_TIME:"#EXT-X-PROGRAM-DATE-TIME:",
/**
* Indicates whether the client MAY or MUST NOT cache downloaded media segments for later replay.
* Syntax: #EXT-X-ALLOW-CACHE:<YES|NO>
* Note: This is likely irrelevant in the context of the Flash Player.
*/
ALLOW_CACHE:"#EXT-X-ALLOW_CACHE:",
/**
* Provides mutability information about the manifest.
* Syntax: #EXT-X-PLAYLIST-TYPE:<EVENT|VOD>
*/
PLAYLIST_TYPE:"#EXT-X-PLAYLIST-TYPE:",
/**
* Indicates that no more media segments will be added to the manifest. May occur ONCE, anywhere in the mainfest file.
*/
ENDLIST:"#EXT-X-ENDLIST",
/**
* Used to relate Playlists that contain alternative renditions of the same content.
* Syntax: #EXT-X-MEDIA:<attribute-list>
*/
MEDIA:"#EXT-X-MEDIA:",
/**
* Identifies a media URI as a Playlist file containing a multimedia presentation and provides information about that presentation.
* Syntax: #EXT-X-STREAM-INF:<attribute-list>
* <URI>
*/
STREAM_INF:"#EXT-X-STREAM-INF:",
/**
* Indicates an encoding discontinuity between the media segment that follows it and the one that preceded it.
*/
DISCONTINUITY:"#EXT-X-DISCONTINUITY",
/**
* Indicates that each media segment in the manifest describes a single I-frame.
*/
I_FRAMES_ONLY:"#EXT-X-I-FRAMES-ONLY",
/**
* Identifies a manifest file containing the I-frames of a multimedia presentation. It stands alone, in that it does not apply to a particular URI in the manifest.
* Syntax: #EXT-X-I-FRAME-STREAM-INF:<attribute-list>
*/
I_FRAME_STREAM_INF:"#EXT-X-I-FRAME-STREAM-INF:",
/**
* Indicates the compatibility version of the Playlist file.
* Syntax: #EXT-X-VERSION:<n>
*/
VERSION:"#EXT-X-VERSION:",
/**
* Indicates the total duration as reported by Zencoder.
* Syntax: #ZEN-TOTAL-DURATION:<n>
*/
ZEN_TOTAL_DURATION: "#ZEN-TOTAL-DURATION:"
};
})(this);
(function (window) {
window.videojs.hls.M3U8 = function () {
this.directory = "";
this.allowCache = "NO";
this.playlistItems = [];
this.mediaItems = [];
this.iFrameItems = [];
this.invalidReasons = [];
this.hasValidM3UTag = false;
this.hasEndTag = false;
this.targetDuration = -1;
this.totalDuration = -1;
this.isPlaylist = false;
this.playlistType = "";
this.mediaSequence = -1;
this.version = -1;
};
})(this);
(function (window) {
var
M3U8Parser = window.videojs.hls.M3U8Parser;
window.videojs.hls.ManifestController = function() {
var self = this;
self.loadManifest = function(manifestUrl, onDataCallback, onErrorCallback, onUpdateCallback) {
self.url = manifestUrl;
if (onDataCallback) {
self.onDataCallback = onDataCallback;
}
if (onErrorCallback) {
self.onErrorCallback = onErrorCallback;
}
if (onUpdateCallback) {
self.onUpdateCallback = onUpdateCallback;
}
window.vjs.get(manifestUrl, self.onManifestLoadComplete, self.onManifestLoadError);
};
self.parseManifest = function(dataAsString) {
self.parser = new M3U8Parser();
self.parser.directory = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/.exec(self.url).slice(1)[1];
self.data = self.parser.parse(dataAsString);
return self.data;
};
self.onManifestLoadComplete = function(response) {
var output = self.parseManifest(response);
if (self.onDataCallback !== undefined) {
self.onDataCallback(output);
}
};
self.onManifestLoadError = function(err) {
if (self.onErrorCallback !== undefined) {
self.onErrorCallback((err !== undefined) ? err : null);
}
};
};
})(this);
(function(window) {
window.videojs.hls.SegmentController = function() {
var self = this;
self.loadSegment = function(segmentUrl, onDataCallback, onErrorCallback, onUpdateCallback) {
var request = new XMLHttpRequest();
self.url = segmentUrl;
self.onDataCallback = onDataCallback;
self.onErrorCallback = onErrorCallback;
self.onUpdateCallback = onUpdateCallback;
self.requestTimestamp = +new Date();
request.open('GET', segmentUrl, true);
request.responseType = 'arraybuffer';
request.onload = function() {
self.onSegmentLoadComplete(new Uint8Array(request.response));
};
request.send(null);
};
self.parseSegment = function(incomingData) {
self.data = {};
self.data.binaryData = incomingData;
self.data.url = self.url;
self.data.isCached = false;
self.data.requestTimestamp = self.requestTimestamp;
self.data.responseTimestamp = self.responseTimestamp;
self.data.byteLength = incomingData.byteLength;
self.data.isCached = parseInt(self.responseTimestamp - self.requestTimestamp,10) < 75;
self.data.throughput = self.calculateThroughput(self.data.byteLength, self.requestTimestamp ,self.responseTimestamp);
return self.data;
};
self.calculateThroughput = function(dataAmount, startTime, endTime) {
return Math.round(dataAmount / (endTime - startTime) * 1000) * 8;
};
self.onSegmentLoadComplete = function(response) {
var output;
self.responseTimestamp = +new Date();
output = self.parseSegment(response);
if (self.onDataCallback !== undefined) {
self.onDataCallback(output);
}
};
self.onSegmentLoadError = function(error) {
if (error) {
throw error;
}
if (self.onErrorCallback !== undefined) {
self.onErrorCallback(error);
}
};
};
})(this);
......@@ -4,24 +4,30 @@
FlvTag = videojs.hls.FlvTag,
H264Stream = videojs.hls.H264Stream,
AacStream = videojs.hls.AacStream,
m2tsPacketSize = 188;
console.assert(H264Stream);
console.assert(AacStream);
window.videojs.hls.SegmentParser = function() {
MP2T_PACKET_LENGTH,
STREAM_TYPES;
/**
* An object that incrementally transmuxes MPEG2 Trasport Stream
* chunks into an FLV.
*/
videojs.hls.SegmentParser = function() {
var
self = this,
parseTSPacket,
pmtPid,
streamBuffer = new Uint8Array(m2tsPacketSize),
streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH),
streamBufferByteCount = 0,
videoPid,
h264Stream = new H264Stream(),
audioPid,
aacStream = new AacStream(),
seekToKeyFrame = false;
// expose the stream metadata
self.stream = {
// the mapping between transport stream programs and the PIDs
// that form their elementary streams
programMapTable: {}
};
// For information on the FLV format, see
// http://download.macromedia.com/f4v/video_file_format_spec_v10_1.pdf.
// Technically, this function returns the header and a metadata FLV tag
......@@ -146,24 +152,24 @@
// reconstruct the first packet. The rest of the packets will be
// parsed directly from data
if (streamBufferByteCount > 0) {
if (data.byteLength + streamBufferByteCount < m2tsPacketSize) {
if (data.byteLength + streamBufferByteCount < MP2T_PACKET_LENGTH) {
// the current data is less than a single m2ts packet, so stash it
// until we receive more
// ?? this seems to append streamBuffer onto data and then just give up. I'm not sure why that would be interesting.
videojs.log('data.length + streamBuffer.length < m2tsPacketSize ??');
videojs.log('data.length + streamBuffer.length < MP2T_PACKET_LENGTH ??');
streamBuffer.readBytes(data, data.length, streamBuffer.length);
return;
} else {
// we have enough data for an m2ts packet
// process it immediately
dataSlice = data.subarray(0, m2tsPacketSize - streamBufferByteCount);
dataSlice = data.subarray(0, MP2T_PACKET_LENGTH - streamBufferByteCount);
streamBuffer.set(dataSlice, streamBufferByteCount);
parseTSPacket(streamBuffer);
// reset the buffer
streamBuffer = new Uint8Array(m2tsPacketSize);
streamBuffer = new Uint8Array(MP2T_PACKET_LENGTH);
streamBufferByteCount = 0;
}
}
......@@ -178,7 +184,7 @@
}
// base case: not enough data to parse a m2ts packet
if (data.byteLength - dataPosition < m2tsPacketSize) {
if (data.byteLength - dataPosition < MP2T_PACKET_LENGTH) {
if (data.byteLength - dataPosition > 0) {
// there are bytes remaining, save them for next time
streamBuffer.set(data.subarray(dataPosition),
......@@ -189,8 +195,8 @@
}
// attempt to parse a m2ts packet
if (parseTSPacket(data.subarray(dataPosition, dataPosition + m2tsPacketSize))) {
dataPosition += m2tsPacketSize;
if (parseTSPacket(data.subarray(dataPosition, dataPosition + MP2T_PACKET_LENGTH))) {
dataPosition += MP2T_PACKET_LENGTH;
} else {
// If there was an error parsing a TS packet. it could be
// because we are not TS packet aligned. Step one forward by
......@@ -201,24 +207,31 @@
}
};
/**
* Parses a video/mp2t packet and appends the underlying video and
* audio data onto h264stream and aacStream, respectively.
* @param data {Uint8Array} the bytes of an MPEG2-TS packet,
* including the sync byte.
* @return {boolean} whether a valid packet was encountered
*/
// TODO add more testing to make sure we dont walk past the end of a TS
// packet!
parseTSPacket = function(data) { // :ByteArray):Boolean {
var
offset = 0, // :uint
end = offset + m2tsPacketSize, // :uint
// Don't look for a sync byte. We handle that in
// parseSegmentBinaryData()
end = offset + MP2T_PACKET_LENGTH, // :uint
// Payload Unit Start Indicator
pusi = !!(data[offset + 1] & 0x40), // mask: 0100 0000
// PacketId
// packet identifier (PID), a unique identifier for the elementary
// stream this packet describes
pid = (data[offset + 1] & 0x1F) << 8 | data[offset + 2], // mask: 0001 1111
// adaptation_field_control, whether this header is followed by an
// adaptation field, a payload, or both
afflag = (data[offset + 3] & 0x30 ) >>> 4,
aflen, // :uint
patTableId, // :int
patCurrentNextIndicator, // Boolean
patSectionLength, // :uint
......@@ -231,8 +244,8 @@
pts, // :uint
dts, // :uint
pmtTableId, // :int
pmtCurrentNextIndicator, // :Boolean
pmtProgramDescriptorsLength,
pmtSectionLength, // :uint
streamType, // :int
......@@ -243,42 +256,64 @@
// corrupt stream detection
// cc = (data[offset + 3] & 0x0F);
// Done with TS header
// move past the header
offset += 4;
if (afflag > 0x01) { // skip most of the adaption field
aflen = data[offset];
offset += aflen + 1;
// if an adaption field is present, its length is specified by
// the fifth byte of the PES header. The adaptation field is
// used to specify some forms of timing and control data that we
// do not currently use.
if (afflag > 0x01) {
offset += data[offset] + 1;
}
// Handle a Program Association Table (PAT). PATs map PIDs to
// individual programs. If this transport stream was being used
// for television broadcast, a program would probably be
// equivalent to a channel. In HLS, it would be very unusual to
// create an mp2t stream with multiple programs.
if (0x0000 === pid) {
// always test for PMT first! (becuse other variables default to 0)
// if pusi is set we must skip X bytes (PSI pointer field)
offset += pusi ? 1 + data[offset] : 0;
// The PAT may be split into multiple sections and those
// sections may be split into multiple packets. If a PAT
// section starts in this packet, PUSI will be true and the
// first byte of the playload will indicate the offset from
// the current position to the start of the section.
if (pusi) {
offset += 1 + data[offset];
}
patTableId = data[offset];
console.assert(0x00 === patTableId, 'patTableId should be 0x00');
if (patTableId !== 0x00) {
videojs.log('the table_id of the PAT should be 0x00 but was' +
patTableId.toString(16));
}
// the current_next_indicator specifies whether this PAT is
// currently applicable or is part of the next table to become
// active
patCurrentNextIndicator = !!(data[offset + 5] & 0x01);
if (patCurrentNextIndicator) {
// section_length specifies the number of bytes following
// its position to the end of this section
patSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2];
offset += 8; // skip past PSI header
// We currently only support streams with 1 program
patSectionLength = (patSectionLength - 9) / 4;
if (1 !== patSectionLength) {
// move past the rest of the PSI header to the first program
// map table entry
offset += 8;
// we don't handle streams with more than one program, so
// raise an exception if we encounter one
// section_length = rest of header + (n * entry length) + CRC
// = 5 + (n * 4) + 4
if ((patSectionLength - 5 - 4) / 4 !== 1) {
throw new Error("TS has more that 1 program");
}
// if we ever support more that 1 program (unlikely) loop over them here
// var programNumber = data[offset + 0] << 8 | data[offset + 1];
// var programId = (data[offset+2] & 0x1F) << 8 | data[offset + 3];
pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
// the Program Map Table (PMT) associates the underlying
// video and audio streams with a unique PID
self.stream.pmtPid = (data[offset + 2] & 0x1F) << 8 | data[offset + 3];
}
// We could test the CRC here to detect corruption with extra CPU cost
} else if (videoPid === pid || audioPid === pid) {
} else if (pid === self.stream.programMapTable[STREAM_TYPES.h264] ||
pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
if (pusi) {
// comment out for speed
if (0x00 !== data[offset + 0] || 0x00 !== data[offset + 1] || 0x01 !== data[offset + 2]) {
......@@ -322,60 +357,81 @@
// Skip past "optional" portion of PTS header
offset += pesHeaderLength;
if (videoPid === pid) {
if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
// Stash this frame for future use.
// console.assert(videoFrames.length < 3);
h264Stream.setNextTimeStamp(pts,
dts,
dataAlignmentIndicator);
} else if (audioPid === pid) {
} else if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
aacStream.setNextTimeStamp(pts,
pesPacketSize,
dataAlignmentIndicator);
}
}
if (audioPid === pid) {
if (pid === self.stream.programMapTable[STREAM_TYPES.adts]) {
aacStream.writeBytes(data, offset, end - offset);
} else if (videoPid === pid) {
} else if (pid === self.stream.programMapTable[STREAM_TYPES.h264]) {
h264Stream.writeBytes(data, offset, end - offset);
}
} else if (pmtPid === pid) {
// TODO sanity check data[offset]
// if pusi is set we must skip X bytes (PSI pointer field)
offset += (pusi ? 1 + data[offset] : 0);
pmtTableId = data[offset];
console.assert(0x02 === pmtTableId);
} else if (self.stream.pmtPid === pid) {
// similarly to the PAT, jump to the first byte of the section
if (pusi) {
offset += 1 + data[offset];
}
if (data[offset] !== 0x02) {
videojs.log('The table_id of a PMT should be 0x02 but was ' +
data[offset].toString(16));
}
// whether this PMT is currently applicable or is part of the
// next table to become active
pmtCurrentNextIndicator = !!(data[offset + 5] & 0x01);
if (pmtCurrentNextIndicator) {
audioPid = videoPid = 0;
pmtSectionLength = (data[offset + 1] & 0x0F) << 8 | data[offset + 2];
// overwrite any existing program map table
self.stream.programMapTable = {};
// section_length specifies the number of bytes following
// its position to the end of this section
pmtSectionLength = (data[offset + 1] & 0x0f) << 8 | data[offset + 2];
// subtract the length of the program info descriptors
pmtProgramDescriptorsLength = (data[offset + 10] & 0x0f) << 8 | data[offset + 11];
pmtSectionLength -= pmtProgramDescriptorsLength;
// skip CRC and PSI data we dont care about
// rest of header + CRC = 9 + 4
pmtSectionLength -= 13;
offset += 12; // skip past PSI header and some PMT data
// align offset to the first entry in the PMT
offset += 12 + pmtProgramDescriptorsLength;
// iterate through the entries
while (0 < pmtSectionLength) {
// the type of data carried in the PID this entry describes
streamType = data[offset + 0];
// the PID for this entry
elementaryPID = (data[offset + 1] & 0x1F) << 8 | data[offset + 2];
ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
offset += 5 + ESInfolength;
pmtSectionLength -= 5 + ESInfolength;
if (0x1B === streamType) {
if (0 !== videoPid) {
if (streamType === STREAM_TYPES.h264) {
if (self.stream.programMapTable[streamType] &&
self.stream.programMapTable[streamType] !== elementaryPID) {
throw new Error("Program has more than 1 video stream");
}
videoPid = elementaryPID;
} else if (0x0F === streamType) {
if (0 !== audioPid) {
self.stream.programMapTable[streamType] = elementaryPID;
} else if (streamType === STREAM_TYPES.adts) {
if (self.stream.programMapTable[streamType] &&
self.stream.programMapTable[streamType] !== elementaryPID) {
throw new Error("Program has more than 1 audio Stream");
}
audioPid = elementaryPID;
self.stream.programMapTable[streamType] = elementaryPID;
}
// TODO add support for MP3 audio
// the length of the entry descriptor
ESInfolength = (data[offset + 3] & 0x0F) << 8 | data[offset + 4];
// move to the first byte after the end of this entry
offset += 5 + ESInfolength;
pmtSectionLength -= 5 + ESInfolength;
}
}
// We could test the CRC here to detect corruption with extra CPU cost
......@@ -390,6 +446,10 @@
return true;
};
self.getTags = function() {
return h264Stream.tags;
};
self.stats = {
h264Tags: function() {
return h264Stream.tags.length;
......@@ -399,4 +459,12 @@
}
};
};
// MPEG2-TS constants
videojs.hls.SegmentParser.MP2T_PACKET_LENGTH = MP2T_PACKET_LENGTH = 188;
videojs.hls.SegmentParser.STREAM_TYPES = STREAM_TYPES = {
h264: 0x1b,
adts: 0x0f
};
})(window);
......
/**
* A lightweight readable stream implemention that handles event dispatching.
* Objects that inherit from streams should call init in their constructors.
*/
(function(videojs, undefined) {
var Stream = function() {
this.init = function() {
var listeners = {};
/**
* 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
* the specified type occurs
*/
this.on = function(type, listener) {
if (!listeners[type]) {
listeners[type] = [];
}
listeners[type].push(listener);
};
/**
* Remove a listener for a specified event type.
* @param type {string} the event name
* @param listener {function} a function previously registered for this
* type of event through `on`
*/
this.off = function(type, listener) {
var index;
if (!listeners[type]) {
return false;
}
index = listeners[type].indexOf(listener);
listeners[type].splice(index, 1);
return index > -1;
};
/**
* 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
*/
this.trigger = function(type) {
var callbacks, i, length, args;
callbacks = listeners[type];
if (!callbacks) {
return;
}
args = Array.prototype.slice.call(arguments, 1);
length = callbacks.length;
for (i = 0; i < length; ++i) {
callbacks[i].apply(this, args);
}
};
};
};
/**
* 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
* @see http://nodejs.org/api/stream.html#stream_readable_pipe_destination_options
*/
Stream.prototype.pipe = function(destination) {
this.on('data', function(data) {
destination.push(data);
});
};
videojs.hls.Stream = Stream;
})(window.videojs);
/*
* video-js-hls
*
*
* Copyright (c) 2013 Brightcove
* All rights reserved.
*/
(function(window) {
window.videojs.hls = {};
})(this);
/*
* video-js-hls
*
*
* Copyright (c) 2013 Brightcove
* All rights reserved.
*/
(function(window, videojs, document, undefined) {
videojs.hls = {
/**
* Whether the browser has built-in HLS support.
*/
supportsNativeHls: (function() {
var
video = document.createElement('video'),
xMpegUrl,
vndMpeg;
// native HLS is definitely not supported if HTML5 video isn't
if (!videojs.Html5.isSupported()) {
return false;
}
xMpegUrl = video.canPlayType('application/x-mpegURL');
vndMpeg = video.canPlayType('application/vnd.apple.mpegURL');
return (/probably|maybe/).test(xMpegUrl) ||
(/probably|maybe/).test(vndMpeg);
})()
};
var
// the desired length of video to maintain in the buffer, in seconds
goalBufferLength = 5,
// a fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
bandwidthVariance = 1.1,
/**
* A comparator function to sort two playlist object by bandwidth.
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the bandwidth attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the bandwidth of right is greater than left and
* exactly zero if the two are equal.
*/
playlistBandwidth = function(left, right) {
var leftBandwidth, rightBandwidth;
if (left.attributes && left.attributes.BANDWIDTH) {
leftBandwidth = left.attributes.BANDWIDTH;
}
leftBandwidth = leftBandwidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.BANDWIDTH) {
rightBandwidth = right.attributes.BANDWIDTH;
}
rightBandwidth = rightBandwidth || window.Number.MAX_VALUE;
return leftBandwidth - rightBandwidth;
},
/**
* A comparator function to sort two playlist object by resolution (width).
* @param left {object} a media playlist object
* @param right {object} a media playlist object
* @return {number} Greater than zero if the resolution.width attribute of
* left is greater than the corresponding attribute of right. Less
* than zero if the resolution.width of right is greater than left and
* exactly zero if the two are equal.
*/
playlistResolution = function(left, right) {
var leftWidth, rightWidth;
if (left.attributes && left.attributes.RESOLUTION && left.attributes.RESOLUTION.width) {
leftWidth = left.attributes.RESOLUTION.width;
}
leftWidth = leftWidth || window.Number.MAX_VALUE;
if (right.attributes && right.attributes.RESOLUTION && right.attributes.RESOLUTION.width) {
rightWidth = right.attributes.RESOLUTION.width;
}
rightWidth = rightWidth || window.Number.MAX_VALUE;
// NOTE - Fallback to bandwidth sort as appropriate in cases where multiple renditions
// have the same media dimensions/ resolution
if (leftWidth === rightWidth && left.attributes.BANDWIDTH && right.attributes.BANDWIDTH) {
return left.attributes.BANDWIDTH - right.attributes.BANDWIDTH;
} else {
return leftWidth - rightWidth;
}
},
/**
* TODO - Document this great feature.
*
* @param playlist
* @param time
* @returns int
*/
getMediaIndexByTime = function(playlist, time) {
var index, counter, timeRanges, currentSegmentRange;
timeRanges = [];
for (index = 0; index < playlist.segments.length; index++) {
currentSegmentRange = {};
currentSegmentRange.start = (index === 0) ? 0 : timeRanges[index - 1].end;
currentSegmentRange.end = currentSegmentRange.start + playlist.segments[index].duration;
timeRanges.push(currentSegmentRange);
}
for (counter = 0; counter < timeRanges.length; counter++) {
if (time >= timeRanges[counter].start && time < timeRanges[counter].end) {
return counter;
}
}
return -1;
},
/**
* Calculate the total duration for a playlist based on segment metadata.
* @param playlist {object} a media playlist object
* @return {number} the currently known duration, in seconds
*/
totalDuration = function(playlist) {
var
duration = 0,
i = playlist.segments.length,
segment;
while (i--) {
segment = playlist.segments[i];
duration += segment.duration || playlist.targetDuration || 0;
}
return duration;
},
/**
* 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
*/
resolveUrl = function(basePath, path) {
// use the base element to get the browser to handle URI resolution
var
oldBase = document.querySelector('base'),
docHead = document.querySelector('head'),
a = document.createElement('a'),
base = oldBase,
oldHref,
result;
// prep the document
if (oldBase) {
oldHref = oldBase.href;
} else {
base = docHead.appendChild(document.createElement('base'));
}
base.href = basePath;
a.href = path;
result = a.href;
// clean up
if (oldBase) {
oldBase.href = oldHref;
} else {
docHead.removeChild(base);
}
return result;
},
/**
* Initializes the HLS plugin.
* @param options {mixed} the URL to an HLS playlist
*/
init = function(options) {
var
mediaSource = new videojs.MediaSource(),
segmentParser = new videojs.hls.SegmentParser(),
player = this,
srcUrl,
segmentXhr,
downloadPlaylist,
fillBuffer;
// if the video element supports HLS natively, do nothing
if (videojs.hls.supportsNativeHls) {
return;
}
srcUrl = (function() {
var
extname,
i = 0,
j = 0,
src = player.el().querySelector('.vjs-tech').src,
sources = player.options().sources,
techName,
length = sources.length;
// use the URL specified in options if one was provided
if (typeof options === 'string') {
return options;
} else if (options) {
return options.url;
}
// src attributes take precedence over source children
if (src) {
// assume files with the m3u8 extension are HLS
extname = (/[^#?]*(?:\/[^#?]*\.([^#?]*))/).exec(src);
if (extname && extname[1] === 'm3u8') {
return src;
}
return;
}
// find the first playable source
for (; i < length; i++) {
// ignore sources without a specified type
if (!sources[i].type) {
continue;
}
// do nothing if the source is handled by one of the standard techs
for (j in player.options().techOrder) {
techName = player.options().techOrder[j];
techName = techName[0].toUpperCase() + techName.substring(1);
if (videojs[techName].canPlaySource({ type: sources[i].type })) {
return;
}
}
// use the plugin if the MIME type specifies HLS
if ((/application\/x-mpegURL/).test(sources[i].type) ||
(/application\/vnd\.apple\.mpegURL/).test(sources[i].type)) {
return sources[i].src;
}
}
})();
if (!srcUrl) {
// do nothing until the plugin is initialized with a valid URL
videojs.log('hls: no valid playlist URL specified');
return;
}
// expose the HLS plugin state
player.hls.readyState = function() {
if (!player.hls.media) {
return 0; // HAVE_NOTHING
}
return 1; // HAVE_METADATA
};
player.on('seeking', function() {
var currentTime = player.currentTime();
player.hls.mediaIndex = getMediaIndexByTime(player.hls.media, currentTime);
if (segmentXhr) {
segmentXhr.abort();
}
fillBuffer(currentTime * 1000);
});
/**
* Chooses the appropriate media playlist based on the current
* bandwidth estimate and the player size.
* @return the highest bitrate playlist less than the currently detected
* bandwidth, accounting for some amount of bandwidth variance
*/
player.hls.selectPlaylist = function () {
var
effectiveBitrate,
sortedPlaylists = player.hls.master.playlists.slice(),
bandwidthPlaylists = [],
i = sortedPlaylists.length,
variant,
bandwidthBestVariant,
resolutionBestVariant;
sortedPlaylists.sort(playlistBandwidth);
// map playlist options by bandwidth and select
// best variant as appropriate
while (i--) {
variant = sortedPlaylists[i];
// ignore playlists without bandwidth information
if (!variant.attributes || !variant.attributes.BANDWIDTH) {
continue;
}
effectiveBitrate = variant.attributes.BANDWIDTH * bandwidthVariance;
// since the playlists are sorted in ascending order by bandwidth, the
// current variant is the best as long as its effective bitrate is
// below the current bandwidth estimate
// NOTE - only set once
if (effectiveBitrate < player.hls.bandwidth) {
bandwidthPlaylists.push(variant);
if (!bandwidthBestVariant) {
bandwidthBestVariant = variant;
}
}
}
// set index to the available bandwidth mapped renditions
i = bandwidthPlaylists.length;
// sort those by resolution [currently widths]
bandwidthPlaylists.sort(playlistResolution);
// iterate through bandwidth related playlists and find
// best rendition by player dimension
// Tests
// Seeking - find if you've seeked correctly?
// SelectPlaylist -
while (i--) {
variant = bandwidthPlaylists[i];
// ignored playlists without resolution information
if (!variant.attributes || !variant.attributes.RESOLUTION ||
!variant.attributes.RESOLUTION.width || !variant.attributes.RESOLUTION.height) {
continue;
}
if (variant.attributes.RESOLUTION.width <= player.width() &&
variant.attributes.RESOLUTION.height <= player.height()) {
resolutionBestVariant = variant;
break;
}
}
// fallback chain of variants
return resolutionBestVariant || bandwidthBestVariant || sortedPlaylists[0];
};
/**
* Download an M3U8 and update the current manifest object. If the provided
* URL is a master playlist, the default variant will be downloaded and
* parsed as well. Triggers `loadedmanifest` once for each playlist that is
* downloaded and `loadedmetadata` after at least one media playlist has
* been parsed. Whether multiple playlists were downloaded or not, when
* `loadedmetadata` fires a parsed or inferred master playlist object will
* be available as `player.hls.master`.
*
* @param url {string} a URL to the M3U8 file to process
*/
downloadPlaylist = function(url) {
var xhr = new window.XMLHttpRequest();
xhr.open('GET', url);
xhr.onreadystatechange = function() {
var i, parser, playlist, playlistUri;
if (xhr.readyState === 4) {
if (xhr.status >= 400 || this.status === 0) {
player.hls.error = {
status: xhr.status,
message: 'HLS playlist request error at URL: ' + url,
code: (xhr.status >= 500) ? 4 : 2
};
player.trigger('error');
return;
}
// readystate DONE
parser = new videojs.m3u8.Parser();
parser.push(xhr.responseText);
// master playlists
if (parser.manifest.playlists) {
player.hls.master = parser.manifest;
downloadPlaylist(resolveUrl(url, parser.manifest.playlists[0].uri));
player.trigger('loadedmanifest');
return;
}
// media playlists
if (player.hls.master) {
// merge this playlist into the master
i = player.hls.master.playlists.length;
while (i--) {
playlist = player.hls.master.playlists[i];
playlistUri = resolveUrl(srcUrl, playlist.uri);
if (playlistUri === url) {
player.hls.master.playlists[i] =
videojs.util.mergeOptions(playlist, parser.manifest);
}
}
} else {
// infer a master playlist if none was previously requested
player.hls.master = {
playlists: [parser.manifest]
};
}
// always start playback with the default rendition
if (!player.hls.media) {
player.hls.media = player.hls.master.playlists[0];
// update the duration
if (parser.manifest.totalDuration) {
player.duration(parser.manifest.totalDuration);
} else {
player.duration(totalDuration(parser.manifest));
}
// periodicaly check if the buffer needs to be refilled
player.on('timeupdate', fillBuffer);
player.trigger('loadedmanifest');
player.trigger('loadedmetadata');
fillBuffer();
return;
}
// select a playlist and download its metadata if necessary
playlist = player.hls.selectPlaylist();
if (!playlist.segments) {
downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
} else {
player.hls.media = playlist;
// update the duration
if (player.hls.media.totalDuration) {
player.duration(player.hls.media.totalDuration);
} else {
player.duration(totalDuration(player.hls.media));
}
}
player.trigger('loadedmanifest');
}
};
xhr.send(null);
};
/**
* Determines whether there is enough video data currently in the buffer
* and downloads a new segment if the buffered time is less than the goal.
* @param offset (optional) {number} the offset into the downloaded segment
* to seek to, in milliseconds
*/
fillBuffer = function(offset) {
var
buffered = player.buffered(),
bufferedTime = 0,
segment = player.hls.media.segments[player.hls.mediaIndex],
segmentUri,
startTime;
// if there is a request already in flight, do nothing
if (segmentXhr) {
return;
}
// if the video has finished downloading, stop trying to buffer
if (!segment) {
return;
}
if (buffered) {
// assuming a single, contiguous buffer region
bufferedTime = player.buffered().end(0) - player.currentTime();
}
// if there is plenty of content in the buffer, relax for awhile
if (bufferedTime >= goalBufferLength) {
return;
}
segmentUri = resolveUrl(resolveUrl(srcUrl, player.hls.media.uri || ''),
segment.uri);
// request the next segment
segmentXhr = new window.XMLHttpRequest();
segmentXhr.open('GET', segmentUri);
segmentXhr.responseType = 'arraybuffer';
segmentXhr.onreadystatechange = function() {
var playlist;
// wait until the request completes
if (this.readyState !== 4) {
return;
}
// the segment request is no longer outstanding
segmentXhr = null;
// trigger an error if the request was not successful
if (this.status >= 400) {
player.hls.error = {
status: this.status,
message: 'HLS segment request error at URL: ' + segmentUri,
code: (this.status >= 500) ? 4 : 2
};
// try moving on to the next segment
player.hls.mediaIndex++;
return;
}
// stop processing if the request was aborted
if (!this.response) {
return;
}
// calculate the download bandwidth
player.hls.segmentXhrTime = (+new Date()) - startTime;
player.hls.bandwidth = (this.response.byteLength / player.hls.segmentXhrTime) * 8 * 1000;
// transmux the segment data from MP2T to FLV
segmentParser.parseSegmentBinaryData(new Uint8Array(this.response));
// if we're refilling the buffer after a seek, scan through the muxed
// FLV tags until we find the one that is closest to the desired
// playback time
if (offset !== undefined && typeof offset === "number") {
while (segmentParser.getTags()[0].pts < offset) {
// we're seeking past this tag, so ignore it
segmentParser.getNextTag();
}
}
while (segmentParser.tagsAvailable()) {
player.hls.sourceBuffer.appendBuffer(segmentParser.getNextTag().bytes, player);
}
player.hls.mediaIndex++;
if (player.hls.mediaIndex === player.hls.media.segments.length) {
mediaSource.endOfStream();
return;
}
// figure out what stream the next segment should be downloaded from
// with the updated bandwidth information
playlist = player.hls.selectPlaylist();
if (!playlist.segments) {
downloadPlaylist(resolveUrl(srcUrl, playlist.uri));
} else {
player.hls.media = playlist;
}
};
startTime = +new Date();
segmentXhr.send(null);
};
// load the MediaSource into the player
mediaSource.addEventListener('sourceopen', function() {
// construct the video data buffer and set the appropriate MIME type
var sourceBuffer = mediaSource.addSourceBuffer('video/flv; codecs="vp6,aac"');
player.hls.sourceBuffer = sourceBuffer;
sourceBuffer.appendBuffer(segmentParser.getFlvHeader());
player.hls.mediaIndex = 0;
downloadPlaylist(srcUrl);
});
player.src([{
src: videojs.URL.createObjectURL(mediaSource),
type: "video/flv"
}]);
};
videojs.plugin('hls', function() {
var initialize = function() {
return function() {
this.hls = initialize();
init.apply(this, arguments);
};
};
initialize().apply(this, arguments);
});
})(window, window.videojs, document);
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=200000
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000
prog_index.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000
prog_index1.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000
prog_index2.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000
prog_index3.m3u8
......
(function(window, undefined) {
var
//manifestController = this.manifestController,
ParseStream = window.videojs.m3u8.ParseStream,
parseStream,
LineStream = window.videojs.m3u8.LineStream,
lineStream,
Parser = window.videojs.m3u8.Parser,
parser;
/*
M3U8 Test Suite
*/
module('LineStream', {
setup: function() {
lineStream = new LineStream();
}
});
test('empty inputs produce no tokens', function() {
var data = false;
lineStream.on('data', function() {
data = true;
});
lineStream.push('');
ok(!data, 'no tokens were produced');
});
test('splits on newlines', function() {
var lines = [];
lineStream.on('data', function(line) {
lines.push(line);
});
lineStream.push('#EXTM3U\nmovie.ts\n');
strictEqual(2, lines.length, 'two lines are ready');
strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
});
test('empty lines become empty strings', function() {
var lines = [];
lineStream.on('data', function(line) {
lines.push(line);
});
lineStream.push('\n\n');
strictEqual(2, lines.length, 'two lines are ready');
strictEqual('', lines.shift(), 'the first line is empty');
strictEqual('', lines.shift(), 'the second line is empty');
});
test('handles lines broken across appends', function() {
var lines = [];
lineStream.on('data', function(line) {
lines.push(line);
});
lineStream.push('#EXTM');
strictEqual(0, lines.length, 'no lines are ready');
lineStream.push('3U\nmovie.ts\n');
strictEqual(2, lines.length, 'two lines are ready');
strictEqual('#EXTM3U', lines.shift(), 'the first line is the first token');
strictEqual('movie.ts', lines.shift(), 'the second line is the second token');
});
test('stops sending events after deregistering', function() {
var
temporaryLines = [],
temporary = function(line) {
temporaryLines.push(line);
},
permanentLines = [],
permanent = function(line) {
permanentLines.push(line);
};
lineStream.on('data', temporary);
lineStream.on('data', permanent);
lineStream.push('line one\n');
strictEqual(temporaryLines.length, permanentLines.length, 'both callbacks receive the event');
ok(lineStream.off('data', temporary), 'a listener was removed');
lineStream.push('line two\n');
strictEqual(1, temporaryLines.length, 'no new events are received');
strictEqual(2, permanentLines.length, 'new events are still received');
});
module('ParseStream', {
setup: function() {
lineStream = new LineStream();
parseStream = new ParseStream();
lineStream.pipe(parseStream);
}
});
test('parses comment lines', function() {
var
manifest = '# a line that starts with a hash mark without "EXT" is a comment\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'comment', 'the type is comment');
strictEqual(element.text,
manifest.slice(1, manifest.length - 1),
'the comment text is parsed');
});
test('parses uri lines', function() {
var
manifest = 'any non-blank line that does not start with a hash-mark is a URI\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'uri', 'the type is uri');
strictEqual(element.uri,
manifest.substring(0, manifest.length - 1),
'the uri text is parsed');
});
test('parses unknown tag types', function() {
var
manifest = '#EXT-X-EXAMPLE-TAG:some,additional,stuff\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the type is tag');
strictEqual(element.data,
manifest.slice(4, manifest.length - 1),
'unknown tag data is preserved');
});
// #EXTM3U
test('parses #EXTM3U tags', function() {
var
manifest = '#EXTM3U\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'm3u', 'the tag type is m3u');
});
// #EXTINF
test('parses minimal #EXTINF tags', function() {
var
manifest = '#EXTINF\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'inf', 'the tag type is inf');
});
test('parses #EXTINF tags with durations', function() {
var
manifest = '#EXTINF:15\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'inf', 'the tag type is inf');
strictEqual(element.duration, 15, 'the duration is parsed');
ok(!('title' in element), 'no title is parsed');
manifest = '#EXTINF:21,\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'inf', 'the tag type is inf');
strictEqual(element.duration, 21, 'the duration is parsed');
ok(!('title' in element), 'no title is parsed');
});
test('parses #EXTINF tags with a duration and title', function() {
var
manifest = '#EXTINF:13,Does anyone really use the title attribute?\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'inf', 'the tag type is inf');
strictEqual(element.duration, 13, 'the duration is parsed');
strictEqual(element.title,
manifest.substring(manifest.indexOf(',') + 1, manifest.length - 1),
'the title is parsed');
});
// #EXT-X-TARGETDURATION
test('parses minimal #EXT-X-TARGETDURATION tags', function() {
var
manifest = '#EXT-X-TARGETDURATION\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
ok(!('duration' in element), 'no duration is parsed');
});
test('parses #EXT-X-TARGETDURATION with duration', function() {
var
manifest = '#EXT-X-TARGETDURATION:47\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'targetduration', 'the tag type is targetduration');
strictEqual(element.duration, 47, 'the duration is parsed');
});
// #EXT-X-VERSION
test('parses minimal #EXT-X-VERSION tags', function() {
var
manifest = '#EXT-X-VERSION:\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'version', 'the tag type is version');
ok(!('version' in element), 'no version is present');
});
test('parses #EXT-X-VERSION with a version', function() {
var
manifest = '#EXT-X-VERSION:99\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'version', 'the tag type is version');
strictEqual(element.version, 99, 'the version is parsed');
});
// #EXT-X-MEDIA-SEQUENCE
test('parses minimal #EXT-X-MEDIA-SEQUENCE tags', function() {
var
manifest = '#EXT-X-MEDIA-SEQUENCE\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
ok(!('number' in element), 'no number is present');
});
test('parses #EXT-X-MEDIA-SEQUENCE with sequence numbers', function() {
var
manifest = '#EXT-X-MEDIA-SEQUENCE:109\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'media-sequence', 'the tag type is media-sequence');
ok(element.number, 109, 'the number is parsed');
});
// #EXT-X-PLAYLIST-TYPE
test('parses minimal #EXT-X-PLAYLIST-TYPE tags', function() {
var
manifest = '#EXT-X-PLAYLIST-TYPE:\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
ok(!('playlistType' in element), 'no playlist type is present');
});
test('parses #EXT-X-PLAYLIST-TYPE with mutability info', function() {
var
manifest = '#EXT-X-PLAYLIST-TYPE:EVENT\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
strictEqual(element.playlistType, 'EVENT', 'the playlist type is EVENT');
manifest = '#EXT-X-PLAYLIST-TYPE:VOD\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
strictEqual(element.playlistType, 'VOD', 'the playlist type is VOD');
manifest = '#EXT-X-PLAYLIST-TYPE:nonsense\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'playlist-type', 'the tag type is playlist-type');
strictEqual(element.playlistType, 'nonsense', 'the playlist type is parsed');
});
// #EXT-X-BYTERANGE
test('parses minimal #EXT-X-BYTERANGE tags', function() {
var
manifest = '#EXT-X-BYTERANGE\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
ok(!('length' in element), 'no length is present');
ok(!('offset' in element), 'no offset is present');
});
test('parses #EXT-X-BYTERANGE with length and offset', function() {
var
manifest = '#EXT-X-BYTERANGE:45\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
strictEqual(element.length, 45, 'length is parsed');
ok(!('offset' in element), 'no offset is present');
manifest = '#EXT-X-BYTERANGE:108@16\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'byterange', 'the tag type is byterange');
strictEqual(element.length, 108, 'length is parsed');
strictEqual(element.offset, 16, 'offset is parsed');
});
// #EXT-X-ALLOW-CACHE
test('parses minimal #EXT-X-ALLOW-CACHE tags', function() {
var
manifest = '#EXT-X-ALLOW-CACHE:\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
ok(!('allowed' in element), 'no allowed is present');
});
test('parses valid #EXT-X-ALLOW-CACHE tags', function() {
var
manifest = '#EXT-X-ALLOW-CACHE:YES\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
ok(element.allowed, 'allowed is parsed');
manifest = '#EXT-X-ALLOW-CACHE:NO\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'allow-cache', 'the tag type is allow-cache');
ok(!element.allowed, 'allowed is parsed');
});
// #EXT-X-STREAM-INF
test('parses minimal #EXT-X-STREAM-INF tags', function() {
var
manifest = '#EXT-X-STREAM-INF\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
ok(!('attributes' in element), 'no attributes are present');
});
test('parses #EXT-X-STREAM-INF with common attributes', function() {
var
manifest = '#EXT-X-STREAM-INF:BANDWIDTH=14400\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
strictEqual(element.attributes.BANDWIDTH, 14400, 'bandwidth is parsed');
manifest = '#EXT-X-STREAM-INF:PROGRAM-ID=7\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
strictEqual(element.attributes['PROGRAM-ID'], 7, 'program-id is parsed');
manifest = '#EXT-X-STREAM-INF:RESOLUTION=396x224\n';
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
strictEqual(element.attributes.RESOLUTION.width, 396, 'width is parsed');
strictEqual(element.attributes.RESOLUTION.height, 224, 'heigth is parsed');
});
test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() {
var
manifest = '#EXT-X-STREAM-INF:NUMERIC=24,ALPHA=Value,MIXED=123abc\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'stream-inf', 'the tag type is stream-inf');
strictEqual(element.attributes.NUMERIC, '24', 'numeric attributes are parsed');
strictEqual(element.attributes.ALPHA, 'Value', 'alphabetic attributes are parsed');
strictEqual(element.attributes.MIXED, '123abc', 'mixed attributes are parsed');
});
// #EXT-X-ENDLIST
test('parses #EXT-X-ENDLIST tags', function() {
var
manifest = '#EXT-X-ENDLIST\n',
element;
parseStream.on('data', function(elem) {
element = elem;
});
lineStream.push(manifest);
ok(element, 'an event was triggered');
strictEqual(element.type, 'tag', 'the line type is tag');
strictEqual(element.tagType, 'endlist', 'the tag type is stream-inf');
});
test('ignores empty lines', function() {
var
manifest = '\n',
event = false;
parseStream.on('data', function() {
event = true;
});
lineStream.push(manifest);
ok(!event, 'no event is triggered');
});
module('m3u8 parser', {
setup: function() {
parser = new Parser();
}
});
test('should create a parser', function() {
notStrictEqual(parser, undefined, 'parser is defined');
});
module('m3u8s');
test('parses the example manifests as expected', function() {
var key;
for (key in window.manifests) {
if (window.expected[key]) {
parser = new Parser();
parser.push(window.manifests[key]);
deepEqual(parser.manifest,
window.expected[key],
key + '.m3u8 was parsed correctly');
}
}
});
})(window, window.console);
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "http://example.com/00001.ts"
},
{
"duration": 10,
"uri": "https://example.com/00002.ts"
},
{
"duration": 10,
"uri": "//example.com/00003.ts"
},
{
"duration": 10,
"uri": "http://example.com/00004.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:10,
http://example.com/00001.ts
#EXTINF:10,
https://example.com/00002.ts
#EXTINF:10,
//example.com/00003.ts
#EXTINF:10,
http://example.com/00004.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"byterange": {
"length": 522828,
"offset": 0
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 587500,
"offset": 522828
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 713084,
"offset": 1110328
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 476580,
"offset": 1823412
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 535612,
"offset": 2299992
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 207176,
"offset": 2835604
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 455900,
"offset": 3042780
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 657248,
"offset": 3498680
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 571708,
"offset": 4155928
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 485040,
"offset": 4727636
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 709136,
"offset": 5212676
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 730004,
"offset": 5921812
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 456276,
"offset": 6651816
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 468684,
"offset": 7108092
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 444996,
"offset": 7576776
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 331444,
"offset": 8021772
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 44556,
"offset": 8353216
},
"duration": 1.4167,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-ALLOW-CACHE:YES
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
#EXT-X-BYTERANGE:522828@0
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:587500@522828
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:713084@1110328
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:476580@1823412
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:535612@2299992
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:207176@2835604
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:455900@3042780
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:657248@3498680
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:571708@4155928
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:485040@4727636
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:709136@5212676
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:730004@5921812
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:456276@6651816
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:468684@7108092
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:444996@7576776
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:331444@8021772
hls_450k_video.ts
#EXTINF:1.4167,
#EXT-X-BYTERANGE:44556@8353216
hls_450k_video.ts
#EXT-X-ENDLIST
\ No newline at end of file
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"byterange": {
"length": 522828,
"offset": 0
},
"duration": 10,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-ALLOW-CACHE:0
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
#EXT-X-BYTERANGE:522828@0
hls_450k_video.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"playlists": [{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
"RESOLUTION": {
"width": 960,
"height": 540
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}]
}
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224
http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000
http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224
http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540
http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001
window.brightcove_playlist_data = '#EXTM3U\n'+
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224\n'+
'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001\n'+
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000\n'+
'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001\n'+
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224\n'+
'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001\n'+
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540\n'+
'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001';
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 587500,
"offset": 522828
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 713084,
"offset": 0
},
"duration": 10,
"uri": "hls_450k_video2.ts"
},
{
"byterange": {
"length": 476580,
"offset": 1823412
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 535612,
"offset": 2299992
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 207176,
"offset": 2835604
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 455900,
"offset": 3042780
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 657248,
"offset": 3498680
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 571708,
"offset": 4155928
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 485040,
"offset": 4727636
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 709136,
"offset": 5212676
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 730004,
"offset": 5921812
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 456276,
"offset": 6651816
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 468684,
"offset": 7108092
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 444996,
"offset": 7576776
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 331444,
"offset": 8021772
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 44556,
"offset": 8353216
},
"duration": 1.4167,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:587500@522828
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:713084
hls_450k_video2.ts
#EXTINF:10,
#EXT-X-BYTERANGE:476580@1823412
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:535612@2299992
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:207176@2835604
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:455900@3042780
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:657248@3498680
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:571708@4155928
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:485040@4727636
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:709136@5212676
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:730004@5921812
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:456276@6651816
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:468684@7108092
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:444996@7576776
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:331444@8021772
hls_450k_video.ts
#EXTINF:1.4167,
#EXT-X-BYTERANGE:44556@8353216
hls_450k_video.ts
#EXT-X-ENDLIST
\ No newline at end of file
{
"allowCache": false,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"byterange": {
"length": 522828,
"offset": 0
},
"duration": 10,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-ALLOW-CACHE:NO
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
#EXT-X-BYTERANGE:522828@0
hls_450k_video.ts
#EXT-X-ENDLIST
\ No newline at end of file
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "/00001.ts"
},
{
"duration": 10,
"uri": "/subdir/00002.ts"
},
{
"duration": 10,
"uri": "/00003.ts"
},
{
"duration": 10,
"uri": "/00004.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:10,
/00001.ts
#EXTINF:10,
/subdir/00002.ts
#EXTINF:10,
/00003.ts
#EXTINF:10,
/00004.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"byterange": {
"length": 522828,
"offset": 0
},
"duration": 10,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-ALLOW-CACHE:
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
#EXT-X-BYTERANGE:522828@0
hls_450k_video.ts
#EXT-X-ENDLIST
\ No newline at end of file
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXTINF:6.080,{}
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
#EXTINF:6.600,{}
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
#EXTINF:5.000,{}
/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
},
{
"duration": 8,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:
#EXT-X-TARGETDURATION:10
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts
#EXTINF:8,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
{
"allowCache": true,
"playlists": [{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
"RESOLUTION": {
"width": 960,
"height": 540
}
},
"uri": "http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001"
}]
}
#EXTM3U
#EXT-X-TARGETDURATION:
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224
http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000
http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224
http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540
http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "EVENT",
"segments": [
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
},
{
"duration": 8,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:EVENT
#EXT-X-TARGETDURATION:10
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts
#EXTINF:8,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 1,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
}
],
"targetDuration": 8
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:STRING
#EXT-X-MEDIA-SEQUENCE:1
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"byterange": {
"length": 522828,
"offset": 0
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 587500,
"offset": 522828
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 713084,
"offset": 1110328
},
"duration": 5,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 476580,
"offset": 1823412
},
"duration": 9.7,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 535612,
"offset": 2299992
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 207176,
"offset": 2835604
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 455900,
"offset": 3042780
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 657248,
"offset": 3498680
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 571708,
"offset": 4155928
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 485040,
"offset": 4727636
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 709136,
"offset": 5212676
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 730004,
"offset": 5921812
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 456276,
"offset": 6651816
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 468684,
"offset": 7108092
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 444996,
"offset": 7576776
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 331444,
"offset": 8021772
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 44556,
"offset": 8353216
},
"duration": 10,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10
#EXT-X-BYTERANGE:522828@0
hls_450k_video.ts
#EXTINF:;asljasdfii11)))00,
#EXT-X-BYTERANGE:587500@522828
hls_450k_video.ts
#EXTINF:5,
#EXT-X-BYTERANGE:713084@1110328
hls_450k_video.ts
#EXTINF:9.7,
#EXT-X-BYTERANGE:476580@1823412
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:535612@2299992
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:207176@2835604
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:455900@3042780
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:657248@3498680
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:571708@4155928
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:485040@4727636
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:709136@5212676
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:730004@5921812
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:456276@6651816
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:468684@7108092
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:444996@7576776
hls_450k_video.ts
#EXTINF:22,
#EXTINF:10,
#EXT-X-BYTERANGE:331444@8021772
hls_450k_video.ts
#EXT-X-BYTERANGE:44556@8353216
hls_450k_video.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"byterange": {
"length": 522828,
"offset": 0
},
"duration": 10,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:4
#EXT-X-ALLOW-CACHE:MAYBE
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
#EXT-X-BYTERANGE:522828@0
hls_450k_video.ts
#EXT-X-ENDLIST
\ No newline at end of file
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:gobblegobble
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXTINF:6.080,{}
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
#EXTINF:6.600,{}
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
#EXTINF:5.000,{}
/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts"
},
{
"duration": 8,
"uri": "/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:asdRASDfasdR
#EXT-X-TARGETDURATION:10
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts
#EXTINF:8,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"byterange": {
"length": 522828,
"offset": 0
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 587500,
"offset": 522828
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 713084,
"offset": 1110328
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 476580,
"offset": 1823412
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 535612,
"offset": 2299992
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 207176,
"offset": 2835604
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 455900,
"offset": 3042780
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 657248,
"offset": 3498680
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 571708,
"offset": 4155928
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 485040,
"offset": 4727636
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 709136,
"offset": 5212676
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 730004,
"offset": 5921812
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 456276,
"offset": 6651816
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 468684,
"offset": 7108092
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 444996,
"offset": 7576776
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 331444,
"offset": 8021772
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 44556,
"offset": 8353216
},
"duration": 1.4167,
"uri": "hls_450k_video.ts"
}
]
}
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:NaN
#EXT-X-VERSION:4
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
#EXT-X-BYTERANGE:522828@0
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:587500@522828
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:713084@1110328
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:476580@1823412
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:535612@2299992
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:207176@2835604
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:455900@3042780
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:657248@3498680
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:571708@4155928
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:485040@4727636
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:709136@5212676
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:730004@5921812
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:456276@6651816
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:468684@7108092
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:444996@7576776
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:331444@8021772
hls_450k_video.ts
#EXTINF:1.4167,
#EXT-X-BYTERANGE:44556@8353216
hls_450k_video.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 8,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 8,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
}
],
"targetDuration": 8
}
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
]
}
\ No newline at end of file
#EXTM3U
#ZEN-TOTAL-DURATION:50
#EXT-X-TARGETDURATION:-10
#EXTINF:10,
/test/ts-files/zencoder/gogo/00001.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/gogo/00002.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/gogo/00003.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/gogo/00004.ts"
},
{
"duration": 10,
"uri": "/test/ts-files/zencoder/gogo/00005.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#ZEN-TOTAL-DURATION:50
#EXT-X-TARGETDURATION:10
#EXTINF:10,
/test/ts-files/zencoder/gogo/00001.ts
#EXTINF:10,
/test/ts-files/zencoder/gogo/00002.ts
#EXTINF:10,
/test/ts-files/zencoder/gogo/00003.ts
#EXT-X-ENDLIST
#EXTINF:10,
/test/ts-files/zencoder/gogo/00004.ts
#EXTINF:10,
/test/ts-files/zencoder/gogo/00005.ts
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "/test/ts-files/zencoder/gogo/00001.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#ZEN-TOTAL-DURATION:10
#EXT-X-TARGETDURATION:10
#EXTINF:10,
/test/ts-files/zencoder/gogo/00001.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"playlists": [{
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 240000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "media.m3u8"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 40000
},
"uri": "media1.m3u8"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 440000,
"RESOLUTION": {
"width": 396,
"height": 224
}
},
"uri": "media2.m3u8"
}, {
"attributes": {
"PROGRAM-ID": 1,
"BANDWIDTH": 1928000,
"RESOLUTION": {
"width": 960,
"height": 540
}
},
"uri": "media3.m3u8"
}]
}
# A simple master playlist with multiple variant streams
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224
media.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=40000
media1.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224
media2.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540
media3.m3u8
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "00001.ts"
},
{
"duration": 10,
"uri": "00002.ts"
},
{
"duration": 10,
"uri": "00003.ts"
},
{
"duration": 10,
"uri": "00004.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:10,
00001.ts
#EXTINF:10,
00002.ts
#EXTINF:10,
00003.ts
#EXTINF:10,
00004.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:10,
00001.ts
#EXTINF:10,
00002.ts
#EXTINF:10,
00003.ts
#EXTINF:10,
00004.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-TARGETDURATION:10
#EXTINF:10,
00001.ts
#EXTINF:10,
00002.ts
#EXTINF:10,
00003.ts
#EXTINF:10,
00004.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXTINF:6.080,{}
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
#EXTINF:6.600,{}
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
#EXTINF:5.000,{}
/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"duration": 10,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10
hls_450k_video.ts
hls_450k_video.ts
#EXTINF:10,
hls_450k_video.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXTINF:6.080,{}
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
#EXTINF:6.600,{}
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
#EXTINF:5.000,{}
/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 8,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 8,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 8,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
}
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"targetDuration": 10,
"segments": [{
"uri": "001.ts"
}, {
"uri": "002.ts",
"duration": 9
}, {
"uri": "003.ts",
"duration": 7
}, {
"uri": "004.ts",
"duration": 10
}]
}
#EXTM3U
001.ts
#EXT-X-TARGETDURATION:9
002.ts
#EXTINF:7
003.ts
#EXT-X-TARGETDURATION:10
004.ts
\ No newline at end of file
{
"allowCache": true,
"mediaSequence": -11,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:-11
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXTINF:6.080,{}
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
#EXTINF:6.600,{}
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
#EXTINF:5.000,{}
/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"byterange": {
"length": 522828,
"offset": 0
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 587500,
"offset": 522828
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 713084,
"offset": 1110328
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 476580,
"offset": 1823412
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 535612,
"offset": 2299992
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 207176,
"offset": 2835604
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 455900,
"offset": 3042780
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 657248,
"offset": 3498680
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 571708,
"offset": 4155928
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 485040,
"offset": 4727636
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 709136,
"offset": 5212676
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 730004,
"offset": 5921812
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 456276,
"offset": 6651816
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 468684,
"offset": 7108092
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 444996,
"offset": 7576776
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 331444,
"offset": 8021772
},
"duration": 10,
"uri": "hls_450k_video.ts"
},
{
"byterange": {
"length": 44556,
"offset": 8353216
},
"duration": 1.4167,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
window.playlistData = '#EXTM3U\n'+
'#EXT-X-TARGETDURATION:10\n' +
'#EXT-X-VERSION:4\n' +
'#EXT-X-MEDIA-SEQUENCE:0\n' +
'#EXT-X-PLAYLIST-TYPE:VOD\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:522828@0\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:587500@522828\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:713084@1110328\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:476580@1823412\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:535612@2299992\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:207176@2835604\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:455900@3042780\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:657248@3498680\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:571708@4155928\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:485040@4727636\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:709136@5212676\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:730004@5921812\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:456276@6651816\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:468684@7108092\n' +
'hls_450k_video.ts' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:444996@7576776\n' +
'hls_450k_video.ts\n' +
'#EXTINF:10,\n' +
'#EXT-X-BYTERANGE:331444@8021772\n' +
'hls_450k_video.ts\n' +
'#EXTINF:1.4167,\n' +
'#EXT-X-BYTERANGE:44556@8353216\n' +
'hls_450k_video.ts\n' +
'#EXT-X-ENDLIST';
{
"allowCache": true,
"mediaSequence": 17,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
}
],
"targetDuration": 8
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:17
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXT-X-ENDLIST
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:{{{version}}}
{{#if allowCache}}#EXT-X-ALLOW-CACHE:{{{allowCache}}}{{/if}}
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
#EXT-X-BYTERANGE:522828@0
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:587500@522828
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:713084@1110328
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:476580@1823412
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:535612@2299992
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:207176@2835604
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:455900@3042780
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:657248@3498680
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:571708@4155928
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:485040@4727636
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:709136@5212676
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:730004@5921812
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:456276@6651816
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:468684@7108092
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:444996@7576776
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:331444@8021772
hls_450k_video.ts
#EXTINF:1.4167,
#EXT-X-BYTERANGE:44556@8353216
hls_450k_video.ts
#EXT-X-ENDLIST
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:{{{version}}}
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
{{#if byteRange}}#EXT-X-BYTERANGE:{{{byteRange}}}{{/if}}
//#EXT-X-BYTERANGE:522828@0
hls_450k_video.ts
#EXTINF:10,
{{#if byteRange1}}#EXT-X-BYTERANGE:{{{byteRange1}}}{{/if}}
//#EXT-X-BYTERANGE:587500@522828
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:713084@1110328
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:476580@1823412
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:535612@2299992
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:207176@2835604
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:455900@3042780
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:657248@3498680
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:571708@4155928
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:485040@4727636
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:709136@5212676
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:730004@5921812
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:456276@6651816
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:468684@7108092
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:444996@7576776
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:331444@8021772
hls_450k_video.ts
#EXTINF:1.4167,
{{#if byteRange2}}#EXT-X-BYTERANGE:{{{byteRange2}}}{{/if}}
//#EXT-X-BYTERANGE:44556@8353216
hls_450k_video.ts
#EXT-X-ENDLIST
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:{{{version}}}
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
{{#if extInf}}#EXTINF:{{{extInf}}}{{/if}}
#EXT-X-BYTERANGE:522828@0
{{#if segment}}{{{segment}}}\n{{/if}}
{{#if extInf1}}#EXTINF:{{{extInf1}}}{{/if}}
#EXT-X-BYTERANGE:587500@522828
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:713084@1110328
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:476580@1823412
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:535612@2299992
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:207176@2835604
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:455900@3042780
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:657248@3498680
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:571708@4155928
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:485040@4727636
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:709136@5212676
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:730004@5921812
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:456276@6651816
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:468684@7108092
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:444996@7576776
hls_450k_video.ts
#EXTINF:10,
#EXT-X-BYTERANGE:331444@8021772
hls_450k_video.ts
{{#if extInf2}}#EXTINF:{{{extInf2}}}{{/if}}
#EXT-X-BYTERANGE:44556@8353216
hls_450k_video.ts
#EXT-X-ENDLIST
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
{{#if mediaSequence}}#EXT-X-MEDIA-SEQUENCE:{{{mediaSequence}}}{{/if}}
{{#if mediaSequence1}}#EXT-X-MEDIA-SEQUENCE:{{{mediaSequence2}}}{{/if}}
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXTINF:6.080,{}
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
#EXTINF:6.600,{}
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
#EXTINF:5.000,{}
/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
#EXT-X-ENDLIST
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-ALLOW-CACHE:YES
{{#if targetDuration}}#EXT-X-TARGETDURATION:{{{targetDuration}}}{{/if}}
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXTINF:6.080,{}
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
#EXTINF:6.600,{}
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
#EXTINF:5.000,{}
/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
#EXT-X-ENDLIST
#EXTM3U
{{#if playlistType}}#EXT-X-PLAYLIST-TYPE:{{{playlistType}}}{{/if}}
#EXT-X-TARGETDURATION:10
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00001.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00002.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00003.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00004.ts
#EXTINF:10,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00005.ts
#EXTINF:8,
/test/ts-files/zencoder/haze/Haze_Mantel_President_encoded_1200-00006.ts
#ZEN-TOTAL-DURATION:57.9911
#EXT-X-ENDLIST
var grunt = require('grunt'),
extname = require('path').extname;
grunt.file.recurse(process.cwd(), function(path) {
var json;
if (extname(path) === '.json') {
json = grunt.file.readJSON(path);
if (json.totalDuration) {
delete json.totalDuration;
grunt.file.write(path, JSON.stringify(json, null, ' '));
}
}
});
{
"allowCache": true,
"playlists": [
{
"attributes": {
"PROGRAM-ID": 1
},
"uri": "media.m3u8"
},
{
"uri": "media1.m3u8"
}
]
}
# A simple master playlist with multiple variant streams
#EXTM3U
#EXT-X-STREAM-INF:PROGRAM-ID=1
media.m3u8
#EXT-X-STREAM-INF:
media1.m3u8
{
"allowCache": true,
"mediaSequence": 11,
"playlistType": "VOD",
"segments": [
{
"duration": 6.64,
"uri": "/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts"
},
{
"duration": 6.08,
"uri": "/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts"
},
{
"duration": 6.6,
"uri": "/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts"
},
{
"duration": 5,
"uri": "/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts"
}
],
"targetDuration": 8
}
\ No newline at end of file
#EXTM3U
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-MEDIA-SEQUENCE:11
#EXT-X-ALLOW-CACHE:YES
#EXT-X-TARGETDURATION:8
#EXTINF:6.640,{}
/test/ts-files/tvy7/8a5e2822668b5370f4eb1438b2564fb7ab12ffe1-hi720.ts
#EXTINF:6.080,{}
/test/ts-files/tvy7/56be1cef869a1c0cc8e38864ad1add17d187f051-hi720.ts
#EXTINF:6.600,{}
/test/ts-files/tvy7/549c8c77f55f049741a06596e5c1e01dacaa46d0-hi720.ts
#EXTINF:5.000,{}
/test/ts-files/tvy7/6cfa378684ffeb1c455a64dae6c103290a1f53d4-hi720.ts
#EXT-X-ENDLIST
{
"allowCache": true,
"mediaSequence": 0,
"playlistType": "VOD",
"segments": [
{
"duration": 10,
"uri": "hls_450k_video.ts"
}
],
"targetDuration": 10
}
\ No newline at end of file
#EXTM3U
#EXT-X-TARGETDURATION:10
#EXT-X-VERSION:NaN
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:10,
hls_450k_video.ts
#EXT-X-ENDLIST
(function(window) {
/*
======== A Handy Little QUnit Reference ========
http://api.qunitjs.com/
Test methods:
module(name, {[setup][ ,teardown]})
test(name, callback)
expect(numberOfAssertions)
stop(increment)
start(decrement)
Test assertions:
ok(value, [message])
equal(actual, expected, [message])
notEqual(actual, expected, [message])
deepEqual(actual, expected, [message])
notDeepEqual(actual, expected, [message])
strictEqual(actual, expected, [message])
notStrictEqual(actual, expected, [message])
throws(block, [expected], [message])
*/
var
parser,
expectedHeader = [
0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00,
0x09, 0x00, 0x00, 0x00, 0x00
],
extend = window.videojs.util.mergeOptions,
testAudioTag,
testVideoTag,
testScriptTag,
asciiFromBytes,
testScriptString,
testScriptEcmaArray,
testNalUnit;
module('segment parser', {
setup: function() {
parser = new window.videojs.hls.SegmentParser();
}
});
test('creates an flv header', function() {
var header = Array.prototype.slice.call(parser.getFlvHeader());
ok(header, 'the header is truthy');
equal(9 + 4, header.length, 'the header length is correct');
equal(header[0], 'F'.charCodeAt(0), 'the first character is "F"');
equal(header[1], 'L'.charCodeAt(0), 'the second character is "L"');
equal(header[2], 'V'.charCodeAt(0), 'the third character is "V"');
deepEqual(expectedHeader, header, 'the rest of the header is correct');
});
test('parses PMTs with program descriptors', function() {
var
makePmt = function(options) {
var
result = [],
entryCount = 0,
k,
sectionLength;
for (k in options.pids) {
entryCount++;
}
// table_id
result.push(0x02);
// section_syntax_indicator '0' reserved section_length
// 13 + (program_info_length) + (n * 5 + ES_info_length[n])
sectionLength = 13 + (5 * entryCount) + 17;
result.push(0x80 | (0xF00 & sectionLength >>> 8));
result.push(sectionLength & 0xFF);
// program_number
result.push(0x00);
result.push(0x01);
// reserved version_number current_next_indicator
result.push(0x01);
// section_number
result.push(0x00);
// last_section_number
result.push(0x00);
// reserved PCR_PID
result.push(0xe1);
result.push(0x00);
// reserved program_info_length
result.push(0xf0);
result.push(0x11); // hard-coded 17 byte descriptor
// program descriptors
result = result.concat([
0x25, 0x0f, 0xff, 0xff,
0x49, 0x44, 0x33, 0x20,
0xff, 0x49, 0x44, 0x33,
0x20, 0x00, 0x1f, 0x00,
0x01
]);
for (k in options.pids) {
// stream_type
result.push(options.pids[k]);
// reserved elementary_PID
result.push(0xe0 | (k & 0x1f00) >>> 8);
result.push(k & 0xff);
// reserved ES_info_length
result.push(0xf0);
result.push(0x00); // ES_info_length = 0
}
// CRC_32
result.push([0x00, 0x00, 0x00, 0x00]); // invalid CRC but we don't check it
return result;
},
makePat = function(options) {
var
result = [],
k;
// table_id
result.push(0x00);
// section_syntax_indicator '0' reserved section_length
result.push(0x80);
result.push(0x0d); // section_length for one program
// transport_stream_id
result.push(0x00);
result.push(0x00);
// reserved version_number current_next_indicator
result.push(0x01); // current_next_indicator is 1
// section_number
result.push(0x00);
// last_section_number
result.push(0x00);
for (k in options.programs) {
// program_number
result.push((k & 0xFF00) >>> 8);
result.push(k & 0x00FF);
// reserved program_map_pid
result.push((options.programs[k] & 0x1f00) >>> 8);
result.push(options.programs[k] & 0xff);
}
return result;
},
makePsi = function(options) {
var result = [];
// pointer_field
if (options.payloadUnitStartIndicator) {
result.push(0x00);
}
if (options.programs) {
return result.concat(makePat(options));
}
return result.concat(makePmt(options));
},
makePacket = function(options) {
var
result = [],
settings = extend({
payloadUnitStartIndicator: true,
pid: 0x00
}, options);
// header
// sync_byte
result.push(0x47);
// transport_error_indicator payload_unit_start_indicator transport_priority PID
result.push((settings.pid & 0x1f) << 8 | 0x40);
result.push(settings.pid & 0xff);
// transport_scrambling_control adaptation_field_control continuity_counter
result.push(0x10);
result = result.concat(makePsi(settings));
// ensure the resulting packet is the correct size
result.length = window.videojs.hls.SegmentParser.MP2T_PACKET_LENGTH;
return result;
},
h264Type = window.videojs.hls.SegmentParser.STREAM_TYPES.h264,
adtsType = window.videojs.hls.SegmentParser.STREAM_TYPES.adts;
parser.parseSegmentBinaryData(new Uint8Array(makePacket({
programs: {
0x01: [0x01]
}
}).concat(makePacket({
pid: 0x01,
pids: {
0x02: h264Type, // h264 video
0x03: adtsType // adts audio
}
}))));
strictEqual(parser.stream.pmtPid, 0x01, 'PMT PID is 1');
strictEqual(parser.stream.programMapTable[h264Type], 0x02, 'video is PID 2');
strictEqual(parser.stream.programMapTable[adtsType], 0x03, 'audio is PID 3');
});
test('parses the first bipbop segment', function() {
parser.parseSegmentBinaryData(window.bcSegment);
ok(parser.tagsAvailable(), 'tags are available');
});
testAudioTag = function(tag) {
var
byte = tag.bytes[11],
format = (byte & 0xF0) >>> 4,
soundRate = byte & 0x03,
soundSize = (byte & 0x2) >>> 1,
soundType = byte & 0x1,
aacPacketType = tag.bytes[12];
equal(10, format, 'the audio format is aac');
equal(3, soundRate, 'the sound rate is 44kHhz');
equal(1, soundSize, 'the sound size is 16-bit samples');
equal(1, soundType, 'the sound type is stereo');
ok(aacPacketType === 0 || aacPacketType === 1, 'aac packets should have a valid type');
};
testVideoTag = function (tag) {
var
byte = tag.bytes[11],
frameType = (byte & 0xF0) >>> 4,
codecId = byte & 0x0F,
packetType = tag.bytes[12],
compositionTime = (tag.view.getInt32(13) & 0xFFFFFF00) >> 8;
// payload starts at tag.bytes[16]
// XXX: I'm not sure that frame types 3-5 are invalid
ok(frameType === 1 || frameType === 2,
'the frame type should be valid');
equal(7, codecId, 'the codec ID is AVC for h264');
ok(packetType <= 2 && packetType >= 0, 'the packet type is within [0, 2]');
if (packetType !== 1) {
equal(0,
compositionTime,
'the composition time is zero for non-NALU packets');
}
// TODO: the rest of the bytes are an NLU unit
if (packetType === 0) {
// AVC decoder configuration record
} else {
// NAL units
testNalUnit(tag.bytes.subarray(16));
}
};
testNalUnit = function(bytes) {
var
nalHeader = bytes[0];
// unitType = nalHeader & 0x1F;
equal(0, (nalHeader & 0x80) >>> 7, 'the first bit is always 0');
// equal(90, (nalHeader & 0x60) >>> 5, 'the NAL reference indicator is something');
// ok(unitType > 0, 'NAL unit type ' + unitType + ' is greater than 0');
// ok(unitType < 22 , 'NAL unit type ' + unitType + ' is less than 22');
};
asciiFromBytes = function(bytes) {
var
string = [],
i = bytes.byteLength;
while (i--) {
string[i] = String.fromCharCode(bytes[i]);
}
return string.join('');
};
testScriptString = function(tag, offset, expected) {
var
type = tag.bytes[offset],
stringLength = tag.view.getUint16(offset + 1),
string;
equal(2, type, 'the script element is of string type');
equal(stringLength, expected.length, 'the script string length is correct');
string = asciiFromBytes(tag.bytes.subarray(offset + 3,
offset + 3 + stringLength));
equal(expected, string, 'the string value is "' + expected + '"');
};
testScriptEcmaArray = function(tag, start) {
var
numItems = tag.view.getUint32(start),
i = numItems,
offset = start + 4,
length,
type;
while (i--) {
length = tag.view.getUint16(offset);
// advance offset to the property value
offset += 2 + length;
type = tag.bytes[offset];
ok(type === 1 || type === 0,
'the ecma array property value type is number or boolean');
offset++;
if (type) {
// boolean
ok(tag.bytes[offset] === 0 || tag.bytes[offset] === 1,
'the script boolean value is 0 or 1');
offset++;
} else {
// number
ok(!isNaN(tag.view.getFloat64(offset)), 'the value is not NaN');
offset += 8;
}
}
equal(tag.bytes[offset], 0, 'the property array terminator is valid');
equal(tag.bytes[offset + 1], 0, 'the property array terminator is valid');
equal(tag.bytes[offset + 2], 9, 'the property array terminator is valid');
};
testScriptTag = function(tag) {
testScriptString(tag, 11, 'onMetaData');
// the onMetaData object is stored as an 'ecma array', an array with non-
// integer indices (i.e. a dictionary or hash-map).
equal(8, tag.bytes[24], 'onMetaData is of ecma array type');
testScriptEcmaArray(tag, 25);
};
test('the flv tags are well-formed', function() {
var
byte,
tag,
type,
currentPts = 0,
lastTime = 0;
parser.parseSegmentBinaryData(window.bcSegment);
while (parser.tagsAvailable()) {
tag = parser.getNextTag();
type = tag.bytes[0];
ok(tag.pts >= currentPts, 'presentation time stamps are increasing');
currentPts = tag.pts;
// generic flv headers
ok(type === 8 || type === 9 || type === 18,
'the type field specifies audio, video or script');
byte = (tag.view.getUint32(1) & 0xFFFFFF00) >>> 8;
equal(tag.bytes.byteLength - 11 - 4, byte, 'the size field is correct');
byte = tag.view.getUint32(5) & 0xFFFFFF00;
ok(byte >= lastTime, 'the timestamp for the tag is greater than zero');
lastTime = byte;
// tag type-specific headers
({
8: testAudioTag,
9: testVideoTag,
18: testScriptTag
})[type](tag);
// previous tag size
equal(tag.bytes.byteLength - 4,
tag.view.getUint32(tag.bytes.byteLength - 4),
'the size of the previous tag is correct');
}
});
})(window);
(function(window) {
/*
======== A Handy Little QUnit Reference ========
http://api.qunitjs.com/
Test methods:
module(name, {[setup][ ,teardown]})
test(name, callback)
expect(numberOfAssertions)
stop(increment)
start(decrement)
Test assertions:
ok(value, [message])
equal(actual, expected, [message])
notEqual(actual, expected, [message])
deepEqual(actual, expected, [message])
notDeepEqual(actual, expected, [message])
strictEqual(actual, expected, [message])
notStrictEqual(actual, expected, [message])
throws(block, [expected], [message])
*/
var
manifestController,
segmentController,
m3u8parser,
parser,
expectedHeader = [
0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00,
0x09, 0x00, 0x00, 0x00, 0x00
],
testAudioTag,
testVideoTag,
testScriptTag,
asciiFromBytes,
testScriptString,
testScriptEcmaArray,
testNalUnit;
module('environment');
test('is sane', function() {
expect(1);
ok(true);
});
module('segment parser', {
setup: function() {
parser = new window.videojs.hls.SegmentParser();
}
});
test('creates an flv header', function() {
var header = Array.prototype.slice.call(parser.getFlvHeader());
ok(header, 'the header is truthy');
equal(9 + 4, header.length, 'the header length is correct');
equal(header[0], 'F'.charCodeAt(0), 'the first character is "F"');
equal(header[1], 'L'.charCodeAt(0), 'the second character is "L"');
equal(header[2], 'V'.charCodeAt(0), 'the third character is "V"');
deepEqual(expectedHeader, header, 'the rest of the header is correct');
});
test('parses the first bipbop segment', function() {
parser.parseSegmentBinaryData(window.bcSegment);
ok(parser.tagsAvailable(), 'tags are available');
});
testAudioTag = function(tag) {
var
byte = tag.bytes[11],
format = (byte & 0xF0) >>> 4,
soundRate = byte & 0x03,
soundSize = (byte & 0x2) >>> 1,
soundType = byte & 0x1,
aacPacketType = tag.bytes[12];
equal(10, format, 'the audio format is aac');
equal(3, soundRate, 'the sound rate is 44kHhz');
equal(1, soundSize, 'the sound size is 16-bit samples');
equal(1, soundType, 'the sound type is stereo');
ok(aacPacketType === 0 || aacPacketType === 1, 'aac packets should have a valid type');
};
testVideoTag = function (tag) {
var
byte = tag.bytes[11],
frameType = (byte & 0xF0) >>> 4,
codecId = byte & 0x0F,
packetType = tag.bytes[12],
compositionTime = (tag.view.getInt32(13) & 0xFFFFFF00) >> 8;
// payload starts at tag.bytes[16]
// XXX: I'm not sure that frame types 3-5 are invalid
ok(frameType === 1 || frameType === 2,
'the frame type should be valid');
equal(7, codecId, 'the codec ID is AVC for h264');
ok(packetType <= 2 && packetType >= 0, 'the packet type is within [0, 2]');
if (packetType !== 1) {
equal(0,
compositionTime,
'the composition time is zero for non-NALU packets');
}
// TODO: the rest of the bytes are an NLU unit
if (packetType === 0) {
// AVC decoder configuration record
} else {
// NAL units
testNalUnit(tag.bytes.subarray(16));
}
};
testNalUnit = function(bytes) {
var
nalHeader = bytes[0];
// unitType = nalHeader & 0x1F;
equal(0, (nalHeader & 0x80) >>> 7, 'the first bit is always 0');
// equal(90, (nalHeader & 0x60) >>> 5, 'the NAL reference indicator is something');
// ok(unitType > 0, 'NAL unit type ' + unitType + ' is greater than 0');
// ok(unitType < 22 , 'NAL unit type ' + unitType + ' is less than 22');
};
asciiFromBytes = function(bytes) {
var
string = [],
i = bytes.byteLength;
while (i--) {
string[i] = String.fromCharCode(bytes[i]);
}
return string.join('');
};
testScriptString = function(tag, offset, expected) {
var
type = tag.bytes[offset],
stringLength = tag.view.getUint16(offset + 1),
string;
equal(2, type, 'the script element is of string type');
equal(stringLength, expected.length, 'the script string length is correct');
string = asciiFromBytes(tag.bytes.subarray(offset + 3,
offset + 3 + stringLength));
equal(expected, string, 'the string value is "' + expected + '"');
};
testScriptEcmaArray = function(tag, start) {
var
numItems = tag.view.getUint32(start),
i = numItems,
offset = start + 4,
length,
type;
while (i--) {
length = tag.view.getUint16(offset);
// advance offset to the property value
offset += 2 + length;
type = tag.bytes[offset];
ok(type === 1 || type === 0,
'the ecma array property value type is number or boolean');
offset++;
if (type) {
// boolean
ok(tag.bytes[offset] === 0 || tag.bytes[offset] === 1,
'the script boolean value is 0 or 1');
offset++;
} else {
// number
ok(!isNaN(tag.view.getFloat64(offset)), 'the value is not NaN');
offset += 8;
}
}
equal(tag.bytes[offset], 0, 'the property array terminator is valid');
equal(tag.bytes[offset + 1], 0, 'the property array terminator is valid');
equal(tag.bytes[offset + 2], 9, 'the property array terminator is valid');
};
testScriptTag = function(tag) {
testScriptString(tag, 11, 'onMetaData');
// the onMetaData object is stored as an 'ecma array', an array with non-
// integer indices (i.e. a dictionary or hash-map).
equal(8, tag.bytes[24], 'onMetaData is of ecma array type');
testScriptEcmaArray(tag, 25);
};
test('the flv tags are well-formed', function() {
var
tag,
byte,
type,
lastTime = 0;
parser.parseSegmentBinaryData(window.bcSegment);
while (parser.tagsAvailable()) {
tag = parser.getNextTag();
type = tag.bytes[0];
// generic flv headers
ok(type === 8 || type === 9 || type === 18,
'the type field specifies audio, video or script');
byte = (tag.view.getUint32(1) & 0xFFFFFF00) >>> 8;
equal(tag.bytes.byteLength - 11 - 4, byte, 'the size field is correct');
byte = tag.view.getUint32(5) & 0xFFFFFF00;
ok(byte >= lastTime, 'the timestamp for the tag is greater than zero');
lastTime = byte;
// tag type-specific headers
({
8: testAudioTag,
9: testVideoTag,
18: testScriptTag
})[type](tag);
// previous tag size
equal(tag.bytes.byteLength - 4,
tag.view.getUint32(tag.bytes.byteLength - 4),
'the size of the previous tag is correct');
}
});
/*
M3U8 Test Suite
*/
module('m3u8 parser', {
setup: function() {
m3u8parser = new window.videojs.hls.M3U8Parser();
}
});
test('should create my parser', function() {
ok(m3u8parser !== undefined);
});
test('should successfully parse manifest data', function() {
var parsedData = m3u8parser.parse(window.playlistData);
ok(parsedData);
});
test('test for expected results', function() {
var data = m3u8parser.parse(window.playlistData);
notEqual(data, null, 'data is not NULL');
equal(data.invalidReasons.length, 0, 'data has 0 invalid reasons');
equal(data.hasValidM3UTag, true, 'data has valid EXTM3U');
equal(data.targetDuration, 10, 'data has correct TARGET DURATION');
equal(data.allowCache, "NO", 'acceptable ALLOW CACHE');
equal(data.isPlaylist, false, 'data is parsed as a PLAYLIST as expected');
equal(data.playlistType, "VOD", 'acceptable PLAYLIST TYPE');
equal(data.mediaItems.length, 16, 'acceptable mediaItem count');
equal(data.mediaSequence, 0, 'MEDIA SEQUENCE is correct');
equal(data.totalDuration, -1, "ZEN TOTAL DURATION is unknown as expected");
equal(data.hasEndTag, true, 'should have ENDLIST tag');
});
module('brightcove playlist', {
setup: function() {
m3u8parser = new window.videojs.hls.M3U8Parser();
}
});
test('should parse a brightcove manifest data', function() {
var data = m3u8parser.parse(window.brightcove_playlist_data);
ok(data);
equal(data.playlistItems.length, 4, 'Has correct rendition count');
equal(data.isPlaylist, true, 'data is parsed as a PLAYLIST as expected');
equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct');
equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct');
equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct');
equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct');
}
);
module('manifest controller', {
setup: function() {
manifestController = new window.videojs.hls.ManifestController();
this.vjsget = window.videojs.get;
window.videojs.get = function(url, success) {
success(window.brightcove_playlist_data);
};
},
teardown: function() {
window.videojs.get = this.vjsget;
}
});
test('should create', function() {
ok(manifestController);
});
test('should return a parsed object', function() {
var data = manifestController.parseManifest(window.brightcove_playlist_data);
ok(data);
equal(data.playlistItems.length, 4, 'Has correct rendition count');
equal(data.playlistItems[0].bandwidth, 240000, 'First rendition index bandwidth is correct');
equal(data.playlistItems[0]["program-id"], 1, 'First rendition index program-id is correct');
equal(data.playlistItems[0].resolution.width, 396, 'First rendition index resolution width is correct');
equal(data.playlistItems[0].resolution.height, 224, 'First rendition index resolution height is correct');
});
test('should get a manifest from hermes', function() {
manifestController.loadManifest('http://example.com/16x9-master.m3u8',
function(responseData) {
ok(responseData);
},
function() {
ok(false, 'does not error');
},
function() {});
});
module('segment controller', {
setup: function() {
segmentController = new window.videojs.hls.SegmentController();
this.vjsget = window.videojs.get;
window.videojs.get = function(url, success) {
success(window.bcSegment);
};
},
teardown: function() {
window.videojs.get = this.vjsget;
}
});
test('bandwidth calulation test', function() {
var
multiSecondData = segmentController.calculateThroughput(10000, 1000, 2000),
subSecondData = segmentController.calculateThroughput(10000, 1000, 1500);
equal(multiSecondData, 80000, 'MULTI-Second bits per second calculation');
equal(subSecondData, 160000, 'SUB-Second bits per second calculation');
});
})(this);
......@@ -8,10 +8,11 @@
<script src="../libs/qunit/qunit.js"></script>
<!-- video.js -->
<script src="../node_modules/video.js/video.dev.js"></script>
<script src="../node_modules/video.js/dist/video-js/video.js"></script>
<script src="../node_modules/videojs-contrib-media-sources/videojs-media-sources.js"></script>
<!-- HLS plugin -->
<script src="../src/video-js-hls.js"></script>
<script src="../src/videojs-hls.js"></script>
<script src="../src/flv-tag.js"></script>
<script src="../src/exp-golomb.js"></script>
<script src="../src/h264-stream.js"></script>
......@@ -19,23 +20,30 @@
<script src="../src/segment-parser.js"></script>
<!-- M3U8 -->
<script src="../src/m3u8/m3u8.js"></script>
<script src="../src/m3u8/m3u8-tag-types.js"></script>
<script src="../src/stream.js"></script>
<script src="../src/m3u8/m3u8-parser.js"></script>
<script src="../src/manifest-controller.js"></script>
<!-- M3U8 TEST DATA -->
<script src="manifest/playlistM3U8data.js"></script>
<script src="manifest/brightcove_playlist_m3u8.js"></script>
<script src="../tmp/manifests.js"></script>
<script src="../tmp/expected.js"></script>
<!-- M3U8 -->
<!-- SEGMENT -->
<script src="tsSegment-bc.js"></script>
<script src="../src/segment-controller.js"></script>
<script src="../src/bin-utils.js"></script>
<script src="video-js-hls_test.js"></script>
<!-- Test cases -->
<script>
module('environment');
test('is sane', function() {
expect(1);
ok(true);
});
</script>
<script src="videojs-hls_test.js"></script>
<script src="segment-parser.js"></script>
<script src="exp-golomb_test.js"></script>
<script src="flv-tag_test.js"></script>
<script src="m3u8_test.js"></script>
</head>
<body>
<div id="qunit"></div>
......
(function(window, videojs, undefined) {
/*
======== A Handy Little QUnit Reference ========
http://api.qunitjs.com/
Test methods:
module(name, {[setup][ ,teardown]})
test(name, callback)
expect(numberOfAssertions)
stop(increment)
start(decrement)
Test assertions:
ok(value, [message])
equal(actual, expected, [message])
notEqual(actual, expected, [message])
deepEqual(actual, expected, [message])
notDeepEqual(actual, expected, [message])
strictEqual(actual, expected, [message])
notStrictEqual(actual, expected, [message])
throws(block, [expected], [message])
*/
var
player,
oldFlashSupported,
oldXhr,
oldSourceBuffer,
oldSupportsNativeHls,
xhrUrls;
module('HLS', {
setup: function() {
// mock out Flash feature for phantomjs
oldFlashSupported = videojs.Flash.isSupported;
videojs.Flash.isSupported = function() {
return true;
};
oldSourceBuffer = window.videojs.SourceBuffer;
window.videojs.SourceBuffer = function() {
this.appendBuffer = function() {};
};
// force native HLS to be ignored
oldSupportsNativeHls = videojs.hls.supportsNativeHls;
videojs.hls.supportsNativeHls = false;
// create the test player
var video = document.createElement('video');
document.querySelector('#qunit-fixture').appendChild(video);
player = videojs(video, {
flash: {
swf: '../node_modules/video.js/dist/video-js/video-js.swf'
},
techOrder: ['flash']
});
player.buffered = function() {
return videojs.createTimeRange(0, 0);
};
// make XHR synchronous
oldXhr = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
this.open = function(method, url) {
xhrUrls.push(url);
};
this.send = function() {
// if the request URL looks like one of the test manifests, grab the
// contents off the global object
var manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(xhrUrls.slice(-1)[0]);
if (manifestName) {
manifestName = manifestName[1];
}
this.responseText = window.manifests[manifestName || xhrUrls.slice(-1)[0]];
this.response = new Uint8Array([1]).buffer;
this.readyState = 4;
this.onreadystatechange();
};
};
xhrUrls = [];
},
teardown: function() {
videojs.Flash.isSupported = oldFlashSupported;
videojs.hls.supportsNativeHls = oldSupportsNativeHls;
window.videojs.SourceBuffer = oldSourceBuffer;
window.XMLHttpRequest = oldXhr;
}
});
test('loads the specified manifest URL on init', function() {
var loadedmanifest = false, loadedmetadata = false;
player.on('loadedmanifest', function() {
loadedmanifest = true;
});
player.on('loadedmetadata', function() {
loadedmetadata = true;
});
player.hls('manifest/playlist.m3u8');
strictEqual(player.hls.readyState(), 0, 'the readyState is HAVE_NOTHING');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
ok(loadedmanifest, 'loadedmanifest fires');
ok(loadedmetadata, 'loadedmetadata fires');
ok(player.hls.master, 'a master is inferred');
ok(player.hls.media, 'the manifest is available');
ok(player.hls.media.segments, 'the segment entries are parsed');
strictEqual(player.hls.master.playlists[0],
player.hls.media,
'the playlist is selected');
strictEqual(player.hls.readyState(), 1, 'the readyState is HAVE_METADATA');
});
test('sets the duration if one is available on the playlist', function() {
var calls = 0;
player.duration = function(value) {
if (value === undefined) {
return 0;
}
calls++;
};
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(1, calls, 'duration is set');
});
test('calculates the duration if needed', function() {
var durations = [];
player.duration = function(duration) {
if (duration === undefined) {
return 0;
}
durations.push(duration);
};
player.hls('manifest/liveMissingSegmentDuration.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(durations.length, 1, 'duration is set');
strictEqual(durations[0], 6.64 + (2 * 8), 'duration is calculated');
});
test('starts downloading a segment on loadedmetadata', function() {
player.hls('manifest/media.m3u8');
player.buffered = function() {
return videojs.createTimeRange(0, 0);
};
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(xhrUrls[1],
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/00001.ts',
'the first segment is requested');
});
test('recognizes absolute URIs and requests them unmodified', function() {
player.hls('manifest/absoluteUris.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(xhrUrls[1],
'http://example.com/00001.ts',
'the first segment is requested');
});
test('recognizes domain-relative URLs', function() {
player.hls('manifest/domainUris.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(xhrUrls[1],
window.location.origin + '/00001.ts',
'the first segment is requested');
});
test('re-initializes the plugin for each source', function() {
var firstInit, secondInit;
player.hls('manifest/master.m3u8');
firstInit = player.hls;
player.hls('manifest/master.m3u8');
secondInit = player.hls;
notStrictEqual(firstInit, secondInit, 'the plugin object is replaced');
});
test('triggers an error when a master playlist request errors', function() {
var
status = 0,
error;
window.XMLHttpRequest = function() {
this.open = function() {};
this.send = function() {
this.readyState = 4;
this.status = status;
this.onreadystatechange();
};
};
player.on('error', function() {
error = player.hls.error;
});
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
ok(error, 'an error is triggered');
strictEqual(2, error.code, 'a network error is triggered');
});
test('downloads media playlists after loading the master', function() {
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(xhrUrls[0], 'manifest/master.m3u8', 'master playlist requested');
strictEqual(xhrUrls[1],
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/media.m3u8',
'media playlist requested');
strictEqual(xhrUrls[2],
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/00001.ts',
'first segment requested');
});
test('timeupdates do not check to fill the buffer until a media playlist is ready', function() {
var urls = [];
window.XMLHttpRequest = function() {
this.open = function(method, url) {
urls.push(url);
};
this.send = function() {};
};
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
player.trigger('timeupdate');
strictEqual(1, urls.length, 'one request was made');
strictEqual('manifest/media.m3u8', urls[0], 'media playlist requested');
});
test('calculates the bandwidth after downloading a segment', function() {
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
ok(player.hls.bandwidth, 'bandwidth is calculated');
ok(player.hls.bandwidth > 0,
'bandwidth is positive: ' + player.hls.bandwidth);
ok(player.hls.segmentXhrTime >= 0,
'saves segment request time: ' + player.hls.segmentXhrTime + 's');
});
test('selects a playlist after segment downloads', function() {
var calls = 0;
player.hls('manifest/master.m3u8');
player.hls.selectPlaylist = function() {
calls++;
return player.hls.master.playlists[0];
};
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(calls, 1, 'selects after the initial segment');
player.currentTime = function() {
return 1;
};
player.buffered = function() {
return videojs.createTimeRange(0, 2);
};
player.trigger('timeupdate');
strictEqual(calls, 2, 'selects after additional segments');
});
test('moves to the next segment if there is a network error', function() {
var mediaIndex;
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// fail the next segment request
window.XMLHttpRequest = function() {
this.open = function() {};
this.send = function() {
this.readyState = 4;
this.status = 400;
this.onreadystatechange();
};
};
mediaIndex = player.hls.mediaIndex;
player.trigger('timeupdate');
strictEqual(mediaIndex + 1, player.hls.mediaIndex, 'media index is incremented');
});
test('updates the duration after switching playlists', function() {
var
calls = 0,
selectedPlaylist = false;
player.hls('manifest/master.m3u8');
player.hls.selectPlaylist = function() {
selectedPlaylist = true;
return player.hls.master.playlists[1];
};
player.duration = function(duration) {
if (duration === undefined) {
return 0;
}
// only track calls that occur after the playlist has been switched
if (player.hls.media === player.hls.master.playlists[1]) {
calls++;
}
};
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
ok(selectedPlaylist, 'selected playlist');
strictEqual(calls, 1, 'updates the duration');
});
test('downloads additional playlists if required', function() {
var
called = false,
playlist = {
uri: 'media3.m3u8'
};
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// before an m3u8 is downloaded, no segments are available
player.hls.selectPlaylist = function() {
if (!called) {
called = true;
return playlist;
}
playlist.segments = [];
return playlist;
};
xhrUrls = [];
// the playlist selection is revisited after a new segment is downloaded
player.currentTime = function() {
return 1;
};
player.trigger('timeupdate');
strictEqual(2, xhrUrls.length, 'requests were made');
strictEqual(xhrUrls[1],
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/' +
playlist.uri,
'made playlist request');
strictEqual(playlist, player.hls.media, 'a new playlists was selected');
ok(player.hls.media.segments, 'segments are now available');
});
test('selects a playlist below the current bandwidth', function() {
var playlist;
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// the default playlist has a really high bitrate
player.hls.master.playlists[0].attributes.BANDWIDTH = 9e10;
// playlist 1 has a very low bitrate
player.hls.master.playlists[1].attributes.BANDWIDTH = 1;
// but the detected client bandwidth is really low
player.hls.bandwidth = 10;
playlist = player.hls.selectPlaylist();
strictEqual(playlist,
player.hls.master.playlists[1],
'the low bitrate stream is selected');
});
test('raises the minimum bitrate for a stream proportionially', function() {
var playlist;
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// the default playlist's bandwidth + 10% is equal to the current bandwidth
player.hls.master.playlists[0].attributes.BANDWIDTH = 10;
player.hls.bandwidth = 11;
// 9.9 * 1.1 < 11
player.hls.master.playlists[1].attributes.BANDWIDTH = 9.9;
playlist = player.hls.selectPlaylist();
strictEqual(playlist,
player.hls.master.playlists[1],
'a lower bitrate stream is selected');
});
test('uses the lowest bitrate if no other is suitable', function() {
var playlist;
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// the lowest bitrate playlist is much greater than 1b/s
player.hls.bandwidth = 1;
playlist = player.hls.selectPlaylist();
// playlist 1 has the lowest advertised bitrate
strictEqual(playlist,
player.hls.master.playlists[1],
'the lowest bitrate stream is selected');
});
test('selects the correct rendition by player dimensions', function() {
var playlist;
player.hls('manifest/master.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
player.width(640);
player.height(360);
player.hls.bandwidth = 3000000;
playlist = player.hls.selectPlaylist();
deepEqual(playlist.attributes.RESOLUTION, {width:396,height:224},'should return the correct resolution by player dimensions');
equal(playlist.attributes.BANDWIDTH, 440000, 'should have the expected bandwidth in case of multiple');
player.width(1920);
player.height(1080);
player.hls.bandwidth = 3000000;
playlist = player.hls.selectPlaylist();
deepEqual(playlist.attributes.RESOLUTION, {width:960,height:540},'should return the correct resolution by player dimensions');
equal(playlist.attributes.BANDWIDTH, 1928000, 'should have the expected bandwidth in case of multiple');
});
test('does not download the next segment if the buffer is full', function() {
player.hls('manifest/media.m3u8');
player.currentTime = function() {
return 15;
};
player.buffered = function() {
return videojs.createTimeRange(0, 20);
};
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
player.trigger('timeupdate');
strictEqual(xhrUrls.length, 1, 'no segment request was made');
});
test('downloads the next segment if the buffer is getting low', function() {
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(xhrUrls.length, 2, 'did not make a request');
player.currentTime = function() {
return 15;
};
player.buffered = function() {
return videojs.createTimeRange(0, 19.999);
};
player.trigger('timeupdate');
strictEqual(xhrUrls.length, 3, 'made a request');
strictEqual(xhrUrls[2],
window.location.origin +
window.location.pathname.split('/').slice(0, -1).join('/') +
'/manifest/00002.ts',
'made segment request');
});
test('stops downloading segments at the end of the playlist', function() {
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
xhrUrls = [];
player.hls.mediaIndex = 4;
player.trigger('timeupdate');
strictEqual(xhrUrls.length, 0, 'no request is made');
});
test('only makes one segment request at a time', function() {
var openedXhrs = 0;
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
// mock out a long-running XHR
window.XMLHttpRequest = function() {
this.send = function() {};
this.open = function() {
openedXhrs++;
};
};
player.trigger('timeupdate');
strictEqual(1, openedXhrs, 'one XHR is made');
player.trigger('timeupdate');
strictEqual(1, openedXhrs, 'only one XHR is made');
});
test('uses the src attribute if no options are provided and it ends in ".m3u8"', function() {
var url = 'http://example.com/services/mobile/streaming/index/master.m3u8?videoId=1824650741001';
player.el().querySelector('.vjs-tech').src = url;
player.hls();
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
strictEqual(url, xhrUrls[0], 'currentSrc is used');
});
test('ignores src attribute if it doesn\'t have the "m3u8" extension', function() {
var tech = player.el().querySelector('.vjs-tech');
tech.src = 'basdfasdfasdfliel//.m3u9';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
tech.src = '';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
tech.src = 'http://example.com/movie.mp4?q=why.m3u8';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
tech.src = 'http://example.m3u8/movie.mp4';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
tech.src = '//example.com/movie.mp4#http://tricky.com/master.m3u8';
player.hls();
ok(!(player.currentSrc() in videojs.mediaSources), 'no media source is created');
strictEqual(xhrUrls.length, 0, 'no request is made');
});
test('activates if the first playable source is HLS', function() {
var video;
document.querySelector('#qunit-fixture').innerHTML =
'<video controls>' +
'<source type="slartibartfast$%" src="movie.slarti">' +
'<source type="application/x-mpegURL" src="movie.m3u8">' +
'<source type="video/mp4" src="movie.mp4">' +
'</video>';
video = document.querySelector('#qunit-fixture video');
player = videojs(video, {
flash: {
swf: '../node_modules/video.js/dist/video-js/video-js.swf'
},
techOrder: ['flash']
});
player.hls();
ok(player.currentSrc() in videojs.mediaSources, 'media source created');
});
test('cancels outstanding XHRs when seeking', function() {
var
aborted = false,
opened = 0;
player.hls('manifest/media.m3u8');
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
player.hls.media = {
segments: [{
uri: '0.ts',
duration: 10
}, {
uri: '1.ts',
duration: 10
}]
};
// XHR requests will never complete
window.XMLHttpRequest = function() {
this.open = function() {
opened++;
};
this.send = function() {};
this.abort = function() {
aborted = true;
this.readyState = 4;
this.status = 0;
this.onreadystatechange();
};
};
// trigger a segment download request
player.trigger('timeupdate');
opened = 0;
// attempt to seek while the download is in progress
player.trigger('seeking');
ok(aborted, 'XHR aborted');
strictEqual(1, opened, 'opened new XHR');
});
test('playlist 404 should trigger MEDIA_ERR_NETWORK', function() {
var errorTriggered = false;
window.XMLHttpRequest = function() {
this.open = function(method, url) {
xhrUrls.push(url);
};
this.send = function() {
this.readyState = 4;
this.status = 404;
this.onreadystatechange();
};
};
player.hls('manifest/media.m3u8');
player.on('error', function() {
errorTriggered = true;
});
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
equal(true, errorTriggered, 'Missing Playlist error event should trigger');
equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
ok(player.hls.error.message, 'Player error type should inform user correctly');
});
test('segment 404 should trigger MEDIA_ERR_NETWORK', function () {
player.hls('manifest/media.m3u8');
player.on('loadedmanifest', function () {
window.XMLHttpRequest = function () {
this.open = function (method, url) {
xhrUrls.push(url);
};
this.send = function () {
this.readyState = 4;
this.status = 404;
this.onreadystatechange();
};
};
});
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
ok(player.hls.error.message, 'an error message is available');
equal(2, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK');
});
test('segment 500 should trigger MEDIA_ERR_ABORTED', function () {
player.hls('manifest/media.m3u8');
player.on('loadedmanifest', function () {
window.XMLHttpRequest = function () {
this.open = function (method, url) {
xhrUrls.push(url);
};
this.send = function () {
this.readyState = 4;
this.status = 500;
this.onreadystatechange();
};
};
});
videojs.mediaSources[player.currentSrc()].trigger({
type: 'sourceopen'
});
ok(player.hls.error.message, 'an error message is available');
equal(4, player.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED');
});
test('has no effect if native HLS is available', function() {
videojs.hls.supportsNativeHls = true;
player.hls('manifest/master.m3u8');
ok(!(player.currentSrc() in videojs.mediaSources),
'no media source was opened');
});
})(window, window.videojs);