9e8618df by David LaPalomento

Merge pull request #385 from dmlap/custom-codecs

Pass codecs along into source buffer creation
2 parents e8371f66 72ece371
language: node_js
sudo: false
node_js:
- '0.10'
- "stable"
install:
- npm install -g grunt-cli && npm install
notifications:
......@@ -11,6 +12,9 @@ notifications:
channels:
- "chat.freenode.net#videojs"
use_notice: true
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
env:
global:
- secure: dM7svnHPPu5IiUMeFWW5zg+iuWNpwt6SSDi3MmVvhSclNMRLesQoRB+7Qq5J/LiKhmjpv1/GlNVV0CTsHMRhZNwQ3fo38eEuTXv99aAflEITXwSEh/VntKViHbGFubn06EnVkJoH6MX3zJ6kbiwc2QdSQbywKzS6l6quUEpWpd0=
......
......@@ -357,7 +357,7 @@ module.exports = function(grunt) {
grunt.task.run(['karma:saucelabs']);
grunt.task.run(['connect:test', 'protractor:saucelabs']);
} else {
grunt.task.run(['karma:phantomjs']);
grunt.task.run(['karma:firefox']);
}
} else {
if (tasks.length === 0) {
......
......@@ -11,8 +11,6 @@
<!-- transmuxing -->
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/stream.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/mp4-generator.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/lib/transmuxer.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/flv-tag.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/exp-golomb.js"></script>
<script src="node_modules/videojs-contrib-media-sources/node_modules/mux.js/legacy/h264-extradata.js"></script>
......@@ -23,6 +21,9 @@
<!-- Media Sources plugin -->
<script src="node_modules/videojs-contrib-media-sources/src/videojs-media-sources.js"></script>
<script>
videojs.MediaSource.webWorkerURI = 'node_modules/videojs-contrib-media-sources/src/transmuxer_worker.js';
</script>
<!-- HLS plugin -->
<script src="src/videojs-hls.js"></script>
......@@ -39,12 +40,6 @@
<script src="src/bin-utils.js"></script>
<!-- example MPEG2-TS segments -->
<!-- bipbop -->
<!-- <script src="test/tsSegment.js"></script> -->
<!-- bunnies -->
<!--<script src="test/tsSegment-bc.js"></script>-->
<style>
body {
font-family: Arial, sans-serif;
......
......@@ -10,10 +10,10 @@ var
// a fudge factor to apply to advertised playlist bitrates to account for
// temporary flucations in client bandwidth
bandwidthVariance = 1.1,
Component = videojs.getComponent('Component'),
// the amount of time to wait between checking the state of the buffer
bufferCheckInterval = 500,
Component = videojs.getComponent('Component'),
keyXhr,
keyFailed,
......@@ -133,7 +133,7 @@ videojs.Hls.prototype.src = function(src) {
// We need to trigger this asynchronously to give others the chance
// to bind to the event when a source is set at player creation
setTimeout(function() {
this.setTimeout(function() {
this.tech_.trigger('loadstart');
}.bind(this), 1);
......@@ -180,6 +180,8 @@ videojs.Hls.prototype.src = function(src) {
});
}
this.setupSourceBuffer_();
selectedPlaylist = this.selectPlaylist();
oldBitrate = oldMediaPlaylist.attributes &&
oldMediaPlaylist.attributes.BANDWIDTH || 0;
......@@ -281,16 +283,7 @@ videojs.Hls.getMediaIndexForLive_ = function(selectedPlaylist) {
};
videojs.Hls.prototype.handleSourceOpen = function() {
this.sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t');
// transition the sourcebuffer to the ended state if we've hit the end of
// the playlist
this.sourceBuffer.addEventListener('updateend', function() {
if (this.duration() !== Infinity &&
this.mediaIndex === this.playlists.media().segments.length) {
this.mediaSource.endOfStream();
}
}.bind(this));
this.setupSourceBuffer_();
// if autoplay is enabled, begin playback. This is duplicative of
// code in video.js but is required because play() must be invoked
......@@ -303,6 +296,31 @@ videojs.Hls.prototype.handleSourceOpen = function() {
}
};
videojs.Hls.prototype.setupSourceBuffer_ = function() {
var media = this.playlists.media(), mimeType;
// wait until a media playlist is available and the Media Source is
// attached
if (!media || this.mediaSource.readyState !== 'open') {
return;
}
mimeType = 'video/mp2t';
if (media.attributes && media.attributes.CODECS) {
mimeType += '; codecs="' + media.attributes.CODECS + '"';
}
this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
// transition the sourcebuffer to the ended state if we've hit the end of
// the playlist
this.sourceBuffer.addEventListener('updateend', function() {
if (this.duration() !== Infinity &&
this.mediaIndex === this.playlists.media().segments.length) {
this.mediaSource.endOfStream();
}
}.bind(this));
};
// register event listeners to transform in-band metadata events into
// VTTCues on a text track
videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
......@@ -310,12 +328,6 @@ videojs.Hls.prototype.setupMetadataCueTranslation_ = function() {
metadataStream = this.segmentParser_.metadataStream,
textTrack;
// only expose metadata tracks to video.js versions that support
// dynamic text tracks (4.12+)
if (!this.tech_.addTextTrack) {
return;
}
// add a metadata cue whenever a metadata event is triggered during
// segment parsing
metadataStream.on('data', function(metadata) {
......@@ -585,6 +597,7 @@ videojs.Hls.prototype.dispose = function() {
}
this.resetSrc_();
Component.prototype.dispose.call(this);
};
/**
......
......@@ -207,7 +207,7 @@
'the title is parsed');
});
test('parses #EXTINF tags with carriage returns', function() {
var
var
manifest = '#EXTINF:13,Does anyone really use the title attribute?\r\n',
element;
parseStream.on('data', function(elem) {
......@@ -480,6 +480,16 @@
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');
manifest = '#EXT-X-STREAM-INF:CODECS="avc1.4d400d, mp4a.40.2"\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.CODECS,
'avc1.4d400d, mp4a.40.2',
'codecs are parsed');
});
test('parses #EXT-X-STREAM-INF with arbitrary attributes', function() {
var
......
......@@ -212,7 +212,8 @@ var
// return an absolute version of a page-relative URL
absoluteUrl = function(relativeUrl) {
return window.location.origin +
return window.location.protocol + '//' +
window.location.host +
(window.location.pathname
.split('/')
.slice(0, -1)
......@@ -239,6 +240,9 @@ module('HLS', {
el.className = 'vjs-tech vjs-mock-flash';
el.vjs_load = function() {};
el.vjs_getProperty = function(attr) {
if (attr === 'buffered') {
return [[0,0]];
}
return el[attr];
};
el.vjs_setProperty = function(attr, value) {
......@@ -383,6 +387,26 @@ test('duration is set when the source opens after the playlist is loaded', funct
equal(player.tech.hls.mediaSource.duration , 40, 'set the duration');
});
test('codecs are passed to the source buffer', function() {
var codecs = [];
player.src({
src: 'custom-codecs.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech.hls.mediaSource.addSourceBuffer = function(codec) {
codecs.push(codec);
};
requests.shift().respond(200, null,
'#EXTM3U\n' +
'#EXT-X-STREAM-INF:CODECS="video, audio"\n' +
'media.m3u8\n');
standardXHRResponse(requests.shift());
equal(codecs.length, 1, 'created a source buffer');
equal(codecs[0], 'video/mp2t; codecs="video, audio"', 'specified the codecs');
});
test('including HLS as a tech does not error', function() {
var player = createPlayer({
techOrder: ['hls', 'html5']
......@@ -450,14 +474,14 @@ test('re-initializes the playlist loader when switching sources', function() {
test('sets the duration if one is available on the playlist', function() {
var events = 0;
player.on('durationchange', function() {
events++;
});
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech.on('durationchange', function() {
events++;
});
standardXHRResponse(requests[0]);
equal(player.tech.hls.mediaSource.duration, 40, 'set the duration');
......@@ -549,7 +573,8 @@ test('recognizes domain-relative URLs', function() {
standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]);
strictEqual(requests[1].url,
window.location.origin + '/00001.ts',
window.location.protocol + '//' + window.location.host +
'/00001.ts',
'the first segment is requested');
});
......@@ -565,11 +590,11 @@ test('re-initializes the handler for each source', function() {
openMediaSource(player);
firstPlaylists = player.tech.hls.playlists;
firstMSE = player.tech.hls.mediaSource;
standardXHRResponse(requests.shift());
standardXHRResponse(requests.shift());
player.tech.hls.sourceBuffer.abort = function() {
aborts++;
};
standardXHRResponse(requests.shift());
standardXHRResponse(requests.shift());
player.src({
src: 'manifest/master.m3u8',
......@@ -1329,9 +1354,11 @@ test('when outstanding XHRs are cancelled, they get aborted properly', function(
test('segmentXhr is properly nulled out when dispose is called', function() {
var
readystatechanges = 0,
oldDispose = Flash.prototype.dispose;
oldDispose = Flash.prototype.dispose,
player;
Flash.prototype.dispose = function() {};
player = createPlayer();
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
......@@ -2108,7 +2135,7 @@ test('does not break if the playlist has no segments', function() {
});
test('clears the segment buffer on seek', function() {
var currentTime, bufferEnd, oldCurrentTime;
var currentTime, oldCurrentTime;
player.src({
src: 'discontinuity.m3u8',
......@@ -2122,8 +2149,8 @@ test('clears the segment buffer on seek', function() {
}
return currentTime;
};
player.buffered = function() {
return videojs.createTimeRange(0, bufferEnd);
player.tech.buffered = function() {
return videojs.createTimeRange();
};
requests.pop().respond(200, null,
......@@ -2139,7 +2166,6 @@ test('clears the segment buffer on seek', function() {
// play to 6s to trigger the next segment request
currentTime = 6;
bufferEnd = 10;
clock.tick(6000);
standardXHRResponse(requests.pop()); // 2.ts
......@@ -2346,6 +2372,17 @@ test('aborts the source buffer on disposal', function() {
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.dispose();
ok(true, 'disposed before creating the source buffer');
requests.length = 0;
player = createPlayer();
player.src({
src: 'manifest/media.m3u8',
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
standardXHRResponse(requests.shift());
player.tech.hls.sourceBuffer.abort = function() {
aborts++;
};
......@@ -2708,9 +2745,6 @@ test('skip segments if key requests fail more than once', function() {
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech.hls.sourceBuffer.appendBuffer = function(chunk) {
bytes.push(chunk);
};
player.tech.trigger('play');
requests.shift().respond(200, null,
......@@ -2721,6 +2755,9 @@ test('skip segments if key requests fail more than once', function() {
'#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=53"\n' +
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence53-A.ts\n');
player.tech.hls.sourceBuffer.appendBuffer = function(chunk) {
bytes.push(chunk);
};
standardXHRResponse(requests.shift()); // segment 1
requests.shift().respond(404); // fail key
requests.shift().respond(404); // fail key, again
......@@ -2878,9 +2915,6 @@ test('treats invalid keys as a key request failure', function() {
type: 'application/vnd.apple.mpegurl'
});
openMediaSource(player);
player.tech.hls.sourceBuffer.appendBuffer = function(chunk) {
bytes.push(chunk);
};
player.tech.trigger('play');
requests.shift().respond(200, null,
'#EXTM3U\n' +
......@@ -2891,6 +2925,9 @@ test('treats invalid keys as a key request failure', function() {
'#EXT-X-KEY:METHOD=NONE\n' +
'#EXTINF:15.0,\n' +
'http://media.example.com/fileSequence52-B.ts\n');
player.tech.hls.sourceBuffer.appendBuffer = function(chunk) {
bytes.push(chunk);
};
// segment request
standardXHRResponse(requests.shift());
// keys should be 16 bytes long
......