|
|
(function(window, videojs, undefined) { 'use strict'; /* ======== 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 Flash = videojs.getComponent('Flash'), oldFlash, player, clock, oldMediaSource, oldCreateUrl, oldSegmentParser, oldSourceBuffer, oldFlashSupported, oldNativeHlsSupport, oldDecrypt, oldGlobalOptions, requests, xhr,
nextId = 0,
// patch over some methods of the provided tech so it can be tested
// synchronously with sinon's fake timers
mockTech = function(tech) { if (tech.isMocked_) { // make this function idempotent because HTML and Flash based
// playback have very different lifecycles. For HTML, the tech
// is available on player creation. For Flash, the tech isn't
// ready until the source has been loaded and one tick has
// expired.
return; }
tech.isMocked_ = true;
tech.paused_ = !tech.autoplay(); tech.paused = function() { return tech.paused_; };
if (!tech.currentTime_) { tech.currentTime_ = tech.currentTime; } tech.currentTime = function() { return tech.time_ === undefined ? tech.currentTime_() : tech.time_; };
tech.setSrc = function(src) { tech.src_ = src; }; tech.src = function(src) { if (src !== undefined) { return tech.setSrc(src); } return tech.src_ === undefined ? tech.src : tech.src_; }; tech.currentSrc_ = tech.currentSrc; tech.currentSrc = function() { return tech.src_ === undefined ? tech.currentSrc_() : tech.src_; };
tech.play_ = tech.play; tech.play = function() { tech.play_(); tech.paused_ = false; tech.trigger('play'); }; tech.pause_ = tech.pause_; tech.pause = function() { tech.pause_(); tech.paused_ = true; tech.trigger('pause'); };
tech.setCurrentTime = function(time) { tech.time_ = time;
setTimeout(function() { tech.trigger('seeking'); setTimeout(function() { tech.trigger('seeked'); }, 1); }, 1); }; },
createPlayer = function(options) { var video, player; video = document.createElement('video'); video.className = 'video-js'; document.querySelector('#qunit-fixture').appendChild(video); player = videojs(video, options || { flash: { swf: '' } });
player.buffered = function() { return videojs.createTimeRange(0, 0); }; mockTech(player.tech_);
return player; }, openMediaSource = function(player) { // ensure the Flash tech is ready
player.tech_.triggerReady(); clock.tick(1); mockTech(player.tech_);
// simulate the sourceopen event
player.tech_.hls.mediaSource.readyState = 'open'; player.tech_.hls.mediaSource.dispatchEvent({ type: 'sourceopen', swfId: player.tech_.el().id });
// endOfStream triggers an exception if flash isn't available
player.tech_.hls.mediaSource.endOfStream = function(error) { this.error_ = error; }; }, standardXHRResponse = function(request) { if (!request.url) { return; }
var contentType = "application/json", // contents off the global object
manifestName = (/(?:.*\/)?(.*)\.m3u8/).exec(request.url);
if (manifestName) { manifestName = manifestName[1]; } else { manifestName = request.url; }
if (/\.m3u8?/.test(request.url)) { contentType = 'application/vnd.apple.mpegurl'; } else if (/\.ts/.test(request.url)) { contentType = 'video/MP2T'; }
request.response = new Uint8Array(16).buffer; request.respond(200, { 'Content-Type': contentType }, window.manifests[manifestName]); },
// a no-op MediaSource implementation to allow synchronous testing
MockMediaSource = videojs.extend(videojs.EventTarget, { constructor: function() {}, addSourceBuffer: function() { return new (videojs.extend(videojs.EventTarget, { constructor: function() {}, abort: function() {}, buffered: videojs.createTimeRange(), appendBuffer: function() {}, remove: function() {} }))(); }, endOfStream: function() {} }),
// do a shallow copy of the properties of source onto the target object
merge = function(target, source) { var name; for (name in source) { target[name] = source[name]; } },
// return an absolute version of a page-relative URL
absoluteUrl = function(relativeUrl) { return window.location.protocol + '//' + window.location.host + (window.location.pathname .split('/') .slice(0, -1) .concat(relativeUrl) .join('/')); };
MockMediaSource.open = function() {};
module('HLS', { beforeEach: function() { oldMediaSource = videojs.MediaSource; videojs.MediaSource = MockMediaSource; oldCreateUrl = videojs.URL.createObjectURL; videojs.URL.createObjectURL = function() { return 'blob:mock-vjs-object-url'; };
// mock out Flash features for phantomjs
oldFlash = videojs.mergeOptions({}, Flash); Flash.embed = function(swf, flashVars) { var el = document.createElement('div'); el.id = 'vjs_mock_flash_' + nextId++; el.className = 'vjs-tech vjs-mock-flash'; el.duration = Infinity; el.vjs_load = function() {}; el.vjs_getProperty = function(attr) { if (attr === 'buffered') { return [[0,0]]; } return el[attr]; }; el.vjs_setProperty = function(attr, value) { el[attr] = value; }; el.vjs_src = function() {}; el.vjs_play = function() {}; el.vjs_discontinuity = function() {};
if (flashVars.autoplay) { el.autoplay = true; } if (flashVars.preload) { el.preload = flashVars.preload; }
el.currentTime = 0;
return el; }; oldFlashSupported = Flash.isSupported; Flash.isSupported = function() { return true; };
oldSourceBuffer = window.videojs.SourceBuffer; window.videojs.SourceBuffer = function() { this.appendBuffer = function() {}; this.abort = function() {}; };
// store functionality that some tests need to mock
oldSegmentParser = videojs.Hls.SegmentParser; oldGlobalOptions = videojs.mergeOptions(videojs.options);
// force the HLS tech to run
oldNativeHlsSupport = videojs.Hls.supportsNativeHls; videojs.Hls.supportsNativeHls = false;
oldDecrypt = videojs.Hls.Decrypter; videojs.Hls.Decrypter = function() {};
// fake XHRs
xhr = sinon.useFakeXMLHttpRequest(); videojs.xhr.XMLHttpRequest = xhr; requests = []; xhr.onCreate = function(xhr) { requests.push(xhr); };
// fake timers
clock = sinon.useFakeTimers();
// create the test player
player = createPlayer(); },
afterEach: function() { videojs.MediaSource = oldMediaSource; videojs.URL.createObjectURL = oldCreateUrl;
merge(videojs.options, oldGlobalOptions); Flash.isSupported = oldFlashSupported; merge(Flash, oldFlash);
videojs.Hls.SegmentParser = oldSegmentParser; videojs.Hls.supportsNativeHls = oldNativeHlsSupport; videojs.Hls.Decrypter = oldDecrypt; videojs.SourceBuffer = oldSourceBuffer;
player.dispose(); xhr.restore(); videojs.xhr.XMLHttpRequest = window.XMLHttpRequest; clock.restore(); } });
test('starts playing if autoplay is specified', function() { var plays = 0; player.autoplay(true); player.src({ src: 'manifest/playlist.m3u8', type: 'application/vnd.apple.mpegurl' }); // REMOVEME workaround https://github.com/videojs/video.js/issues/2326
player.tech_.triggerReady(); clock.tick(1); // make sure play() is called *after* the media source opens
player.tech_.hls.play = function() { plays++; }; openMediaSource(player);
standardXHRResponse(requests[0]); strictEqual(1, plays, 'play was called'); });
test('autoplay seeks to the live point after playlist load', function() { var currentTime = 0; player.autoplay(true); player.on('seeking', function() { currentTime = player.currentTime(); }); player.src({ src: 'liveStart30sBefore.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests.shift()); clock.tick(1);
notEqual(currentTime, 0, 'seeked on autoplay'); });
test('autoplay seeks to the live point after media source open', function() { var currentTime = 0; player.autoplay(true); player.on('seeking', function() { currentTime = player.currentTime(); }); player.src({ src: 'liveStart30sBefore.m3u8', type: 'application/vnd.apple.mpegurl' }); player.tech_.triggerReady(); clock.tick(1); standardXHRResponse(requests.shift()); openMediaSource(player); clock.tick(1);
notEqual(currentTime, 0, 'seeked on autoplay'); });
test('duration is set when the source opens after the playlist is loaded', function() { player.src({ src: 'media.m3u8', type: 'application/vnd.apple.mpegurl' }); player.tech_.triggerReady(); clock.tick(1); standardXHRResponse(requests.shift()); openMediaSource(player);
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'] });
ok(player, 'created the player'); });
test('creates a PlaylistLoader on init', function() { player.src({ src: 'manifest/playlist.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.src({ src:'manifest/playlist.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
equal(requests[0].aborted, true, 'aborted previous src'); standardXHRResponse(requests[1]); ok(player.tech_.hls.playlists.master, 'set the master playlist'); ok(player.tech_.hls.playlists.media(), 'set the media playlist'); ok(player.tech_.hls.playlists.media().segments, 'the segment entries are parsed'); strictEqual(player.tech_.hls.playlists.master.playlists[0], player.tech_.hls.playlists.media(), 'the playlist is selected'); });
test('re-initializes the playlist loader when switching sources', function() { // source is set
player.src({ src:'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); // loader gets media playlist
standardXHRResponse(requests.shift()); // request a segment
standardXHRResponse(requests.shift()); // change the source
player.src({ src:'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); // maybe not needed if https://github.com/videojs/video.js/issues/2326 gets fixed
clock.tick(1); ok(!player.tech_.hls.playlists.media(), 'no media playlist'); equal(player.tech_.hls.playlists.state, 'HAVE_NOTHING', 'reset the playlist loader state'); equal(requests.length, 1, 'requested the new src');
// buffer check
player.tech_.hls.checkBuffer_(); equal(requests.length, 1, 'did not request a stale segment');
// sourceopen
openMediaSource(player);
equal(requests.length, 1, 'made one request'); ok(requests[0].url.indexOf('master.m3u8') >= 0, 'requested only the new playlist'); });
test('sets the duration if one is available on the playlist', function() { var events = 0; 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'); equal(events, 1, 'durationchange is fired'); });
test('estimates individual segment durations if needed', function() { var changes = 0; player.src({ src: 'http://example.com/manifest/missingExtinf.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.hls.mediaSource.duration = NaN; player.tech_.on('durationchange', function() { changes++; });
standardXHRResponse(requests[0]); strictEqual(player.tech_.hls.mediaSource.duration, player.tech_.hls.playlists.media().segments.length * 10, 'duration is updated'); strictEqual(changes, 1, 'one durationchange fired'); });
test('translates seekable by the starting time for live playlists', function() { var seekable; player.src({ src: 'media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:15\n' + '#EXTINF:10,\n' + '0.ts\n' + '#EXTINF:10,\n' + '1.ts\n' + '#EXTINF:10,\n' + '2.ts\n' + '#EXTINF:10,\n' + '3.ts\n');
seekable = player.seekable(); equal(seekable.length, 1, 'one seekable range'); equal(seekable.start(0), 0, 'the earliest possible position is at zero'); equal(seekable.end(0), 10, 'end is relative to the start'); });
test('starts downloading a segment on loadedmetadata', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); player.buffered = function() { return videojs.createTimeRange(0, 0); }; openMediaSource(player);
standardXHRResponse(requests[0]); standardXHRResponse(requests[1]); strictEqual(requests[1].url, absoluteUrl('manifest/media-00001.ts'), 'the first segment is requested'); });
test('finds the correct buffered region based on currentTime', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); player.tech_.buffered = function() { return videojs.createTimeRanges([[0, 5], [6, 12]]); }; openMediaSource(player);
standardXHRResponse(requests[0]); standardXHRResponse(requests[1]); player.currentTime(3); clock.tick(1); equal(player.tech_.hls.findCurrentBuffered_().end(0), 5, 'inside the first buffered region'); player.currentTime(6); clock.tick(1); equal(player.tech_.hls.findCurrentBuffered_().end(0), 12, 'inside the second buffered region'); });
test('recognizes absolute URIs and requests them unmodified', function() { player.src({ src: 'manifest/absoluteUris.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]); standardXHRResponse(requests[1]); strictEqual(requests[1].url, 'http://example.com/00001.ts', 'the first segment is requested'); });
test('recognizes domain-relative URLs', function() { player.src({ src: 'manifest/domainUris.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]); standardXHRResponse(requests[1]); strictEqual(requests[1].url, window.location.protocol + '//' + window.location.host + '/00001.ts', 'the first segment is requested'); });
test('re-initializes the handler for each source', function() { var firstPlaylists, secondPlaylists, firstMSE, secondMSE, aborts;
aborts = 0;
player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); 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++; };
player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); secondPlaylists = player.tech_.hls.playlists; secondMSE = player.tech_.hls.mediaSource;
equal(1, aborts, 'aborted the old source buffer'); ok(requests[0].aborted, 'aborted the old segment request'); notStrictEqual(firstPlaylists, secondPlaylists, 'the playlist object is not reused'); notStrictEqual(firstMSE, secondMSE, 'the media source object is not reused'); });
test('triggers an error when a master playlist request errors', function() { player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); requests.pop().respond(500);
equal(player.tech_.hls.mediaSource.error_, 'network', 'a network error is triggered'); });
test('downloads media playlists after loading the master', function() { player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
player.tech_.hls.bandwidth = 20e10; standardXHRResponse(requests[0]); standardXHRResponse(requests[1]); standardXHRResponse(requests[2]);
strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); strictEqual(requests[1].url, absoluteUrl('manifest/media3.m3u8'), 'media playlist requested'); strictEqual(requests[2].url, absoluteUrl('manifest/media3-00001.ts'), 'first segment requested'); });
test('upshifts if the initial bandwidth hint is high', function() { player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
player.tech_.hls.bandwidth = 10e20; standardXHRResponse(requests[0]); standardXHRResponse(requests[1]); standardXHRResponse(requests[2]);
strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); strictEqual(requests[1].url, absoluteUrl('manifest/media3.m3u8'), 'media playlist requested'); strictEqual(requests[2].url, absoluteUrl('manifest/media3-00001.ts'), 'first segment requested'); });
test('downshifts if the initial bandwidth hint is low', function() { player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
player.tech_.hls.bandwidth = 100; standardXHRResponse(requests[0]); standardXHRResponse(requests[1]); standardXHRResponse(requests[2]);
strictEqual(requests[0].url, 'manifest/master.m3u8', 'master playlist requested'); strictEqual(requests[1].url, absoluteUrl('manifest/media1.m3u8'), 'media playlist requested'); strictEqual(requests[2].url, absoluteUrl('manifest/media1-00001.ts'), 'first segment requested'); });
test('starts checking the buffer on init', function() { var player, fills = 0, drains = 0;
player = createPlayer(); player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
// wait long enough for the buffer check interval to expire and
// trigger fill/drainBuffer
player.tech_.hls.fillBuffer = function() { fills++; }; player.tech_.hls.drainBuffer = function() { drains++; }; clock.tick(500); equal(fills, 1, 'called fillBuffer'); equal(drains, 1, 'called drainBuffer');
player.dispose(); clock.tick(100 * 1000); equal(fills, 1, 'did not call fillBuffer again'); equal(drains, 1, 'did not call drainBuffer again'); });
test('buffer checks are noops until a media playlist is ready', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.hls.checkBuffer_();
strictEqual(1, requests.length, 'one request was made'); strictEqual(requests[0].url, 'manifest/media.m3u8', 'media playlist requested'); });
test('buffer checks are noops when only the master is ready', function() { player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests.shift()); standardXHRResponse(requests.shift()); // ignore any outstanding segment requests
requests.length = 0;
// load in a new playlist which will cause playlists.media() to be
// undefined while it is being fetched
player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
// respond with the master playlist but don't send the media playlist yet
standardXHRResponse(requests.shift()); // trigger fillBuffer()
player.tech_.hls.checkBuffer_();
strictEqual(1, requests.length, 'one request was made'); strictEqual(requests[0].url, absoluteUrl('manifest/media1.m3u8'), 'media playlist requested'); });
test('calculates the bandwidth after downloading a segment', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]);
// set the request time to be a bit earlier so our bandwidth calculations are not NaN
requests[1].requestTime = (new Date())-100;
standardXHRResponse(requests[1]);
ok(player.tech_.hls.bandwidth, 'bandwidth is calculated'); ok(player.tech_.hls.bandwidth > 0, 'bandwidth is positive: ' + player.tech_.hls.bandwidth); ok(player.tech_.hls.segmentXhrTime >= 0, 'saves segment request time: ' + player.tech_.hls.segmentXhrTime + 's'); });
test('fires a progress event after downloading a segment', function() { var progressCount = 0;
player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests.shift()); player.on('progress', function() { progressCount++; }); standardXHRResponse(requests.shift());
equal(progressCount, 1, 'fired a progress event'); });
test('selects a playlist after segment downloads', function() { var calls = 0; player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.hls.selectPlaylist = function() { calls++; return player.tech_.hls.playlists.master.playlists[0]; };
standardXHRResponse(requests[0]); // master
standardXHRResponse(requests[1]); // media
standardXHRResponse(requests[2]); // segment
strictEqual(calls, 2, 'selects after the initial segment'); player.currentTime = function() { return 1; }; player.buffered = function() { return videojs.createTimeRange(0, 2); }; player.tech_.hls.sourceBuffer.trigger('updateend'); player.tech_.hls.checkBuffer_();
standardXHRResponse(requests[3]);
strictEqual(calls, 3, 'selects after additional segments'); });
test('reports an error if a segment is unreachable', function() { player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
player.tech_.hls.bandwidth = 20000; standardXHRResponse(requests[0]); // master
standardXHRResponse(requests[1]); // media
requests[2].respond(400); // segment
strictEqual(player.tech_.hls.mediaSource.error_, 'network', 'network error is triggered'); });
test('updates the duration after switching playlists', function() { var selectedPlaylist = false; player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
player.tech_.hls.bandwidth = 1e20; standardXHRResponse(requests[0]); // master
standardXHRResponse(requests[1]); // media3
player.tech_.hls.selectPlaylist = function() { selectedPlaylist = true;
// this duration should be overwritten by the playlist change
player.tech_.hls.mediaSource.duration = -Infinity;
return player.tech_.hls.playlists.master.playlists[1]; };
standardXHRResponse(requests[2]); // segment 0
standardXHRResponse(requests[3]); // media1
ok(selectedPlaylist, 'selected playlist'); ok(player.tech_.hls.mediaSource.duration !== -Infinity, 'updates the duration'); });
test('downloads additional playlists if required', function() { var called = false, playlist = { uri: 'media3.m3u8' }; player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
player.tech_.hls.bandwidth = 20000; standardXHRResponse(requests[0]);
standardXHRResponse(requests[1]); // before an m3u8 is downloaded, no segments are available
player.tech_.hls.selectPlaylist = function() { if (!called) { called = true; return playlist; } playlist.segments = [1, 1, 1]; return playlist; };
// the playlist selection is revisited after a new segment is downloaded
player.trigger('timeupdate');
requests[2].bandwidth = 3000000; requests[2].response = new Uint8Array([0]); requests[2].respond(200, null, ''); standardXHRResponse(requests[3]);
strictEqual(4, requests.length, 'requests were made'); strictEqual(requests[3].url, absoluteUrl('manifest/' + playlist.uri), 'made playlist request'); strictEqual(playlist.uri, player.tech_.hls.playlists.media().uri, 'a new playlists was selected'); ok(player.tech_.hls.playlists.media().segments, 'segments are now available'); });
test('selects a playlist below the current bandwidth', function() { var playlist; player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]);
// the default playlist has a really high bitrate
player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 9e10; // playlist 1 has a very low bitrate
player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 1; // but the detected client bandwidth is really low
player.tech_.hls.bandwidth = 10;
playlist = player.tech_.hls.selectPlaylist(); strictEqual(playlist, player.tech_.hls.playlists.master.playlists[1], 'the low bitrate stream is selected'); });
test('allows initial bandwidth to be provided', function() { player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.hls.bandwidth = 500;
requests[0].bandwidth = 1; requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-PLAYLIST-TYPE:VOD\n' + '#EXT-X-TARGETDURATION:10\n'); equal(player.tech_.hls.bandwidth, 500, 'prefers user-specified intial bandwidth'); });
test('raises the minimum bitrate for a stream proportionially', function() { var playlist; player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]);
// the default playlist's bandwidth + 10% is equal to the current bandwidth
player.tech_.hls.playlists.master.playlists[0].attributes.BANDWIDTH = 10; player.tech_.hls.bandwidth = 11;
// 9.9 * 1.1 < 11
player.tech_.hls.playlists.master.playlists[1].attributes.BANDWIDTH = 9.9; playlist = player.tech_.hls.selectPlaylist();
strictEqual(playlist, player.tech_.hls.playlists.master.playlists[1], 'a lower bitrate stream is selected'); });
test('uses the lowest bitrate if no other is suitable', function() { var playlist; player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]);
// the lowest bitrate playlist is much greater than 1b/s
player.tech_.hls.bandwidth = 1; playlist = player.tech_.hls.selectPlaylist();
// playlist 1 has the lowest advertised bitrate
strictEqual(playlist, player.tech_.hls.playlists.master.playlists[1], 'the lowest bitrate stream is selected'); });
test('selects the correct rendition by player dimensions', function() { var playlist;
player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' });
openMediaSource(player);
standardXHRResponse(requests[0]);
player.width(640); player.height(360); player.tech_.hls.bandwidth = 3000000;
playlist = player.tech_.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');
player.width(1920); player.height(1080); player.tech_.hls.bandwidth = 3000000;
playlist = player.tech_.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');
player.width(396); player.height(224); playlist = player.tech_.hls.selectPlaylist();
deepEqual(playlist.attributes.RESOLUTION, { width:396, height:224 },'should return the correct resolution by player dimensions, if exact match'); equal(playlist.attributes.BANDWIDTH, 440000, 'should have the expected bandwidth in case of multiple, if exact match');
});
test('selects the highest bitrate playlist when the player dimensions are ' + 'larger than any of the variants', function() { var playlist; player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-STREAM-INF:BANDWIDTH=1000,RESOLUTION=2x1\n' + 'media.m3u8\n' + '#EXT-X-STREAM-INF:BANDWIDTH=1,RESOLUTION=1x1\n' + 'media1.m3u8\n'); // master
standardXHRResponse(requests.shift()); // media
player.tech_.hls.bandwidth = 1e10;
player.width(1024); player.height(768);
playlist = player.tech_.hls.selectPlaylist();
equal(playlist.attributes.BANDWIDTH, 1000, 'selected the highest bandwidth variant'); });
test('does not download the next segment if the buffer is full', function() { var currentTime = 15; player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); player.tech_.currentTime = function() { return currentTime; }; player.tech_.buffered = function() { return videojs.createTimeRange(0, currentTime + videojs.Hls.GOAL_BUFFER_LENGTH); }; openMediaSource(player);
standardXHRResponse(requests[0]);
player.trigger('timeupdate');
strictEqual(requests.length, 1, 'no segment request was made'); });
test('downloads the next segment if the buffer is getting low', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]); standardXHRResponse(requests[1]);
strictEqual(requests.length, 2, 'made two requests'); player.tech_.currentTime = function() { return 15; }; player.tech_.buffered = function() { return videojs.createTimeRange(0, 19.999); }; player.tech_.hls.sourceBuffer.trigger('updateend'); player.tech_.hls.checkBuffer_();
standardXHRResponse(requests[2]);
strictEqual(requests.length, 3, 'made a request'); strictEqual(requests[2].url, absoluteUrl('manifest/media-00002.ts'), 'made segment request'); });
test('buffers based on the correct TimeRange if multiple ranges exist', function() { var currentTime, buffered; player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
player.tech_.currentTime = function() { return currentTime; }; player.tech_.buffered = function() { return videojs.createTimeRange(buffered); }; currentTime = 8; buffered = [[0, 10], [20, 30]];
standardXHRResponse(requests[0]); standardXHRResponse(requests[1]);
strictEqual(requests.length, 2, 'made two requests'); strictEqual(requests[1].url, absoluteUrl('manifest/media-00002.ts'), 'made segment request');
currentTime = 22; player.tech_.hls.sourceBuffer.trigger('updateend'); player.tech_.hls.checkBuffer_(); strictEqual(requests.length, 3, 'made three requests'); strictEqual(requests[2].url, absoluteUrl('manifest/media-00003.ts'), 'made segment request'); });
test('stops downloading segments at the end of the playlist', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests[0]); requests = []; player.tech_.hls.mediaIndex = 4; player.trigger('timeupdate');
strictEqual(requests.length, 0, 'no request is made'); });
test('only makes one segment request at a time', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests.pop()); player.trigger('timeupdate');
strictEqual(1, requests.length, 'one XHR is made'); player.trigger('timeupdate'); strictEqual(1, requests.length, 'only one XHR is made'); });
test('only appends one segment at a time', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests.pop()); // media.m3u8
standardXHRResponse(requests.pop()); // segment 0
player.tech_.hls.checkBuffer_(); equal(requests.length, 0, 'did not request while updating'); });
test('waits to download new segments until the media playlist is stable', function() { player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests.shift()); // master
player.tech_.hls.bandwidth = 1; // make sure we stay on the lowest variant
standardXHRResponse(requests.shift()); // media1
// force a playlist switch
player.tech_.hls.playlists.media('media3.m3u8');
standardXHRResponse(requests.shift()); // segment 0
player.tech_.hls.sourceBuffer.trigger('updateend');
equal(requests.length, 1, 'only the playlist request outstanding'); player.tech_.hls.checkBuffer_(); equal(requests.length, 1, 'delays segment fetching');
standardXHRResponse(requests.shift()); // media3
player.tech_.hls.checkBuffer_(); equal(requests.length, 1, 'resumes segment fetching'); });
test('cancels outstanding XHRs when seeking', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests[0]); player.tech_.hls.media = { segments: [{ uri: '0.ts', duration: 10 }, { uri: '1.ts', duration: 10 }] };
// attempt to seek while the download is in progress
player.currentTime(7); clock.tick(1);
ok(requests[1].aborted, 'XHR aborted'); strictEqual(requests.length, 3, 'opened new XHR'); });
test('when outstanding XHRs are cancelled, they get aborted properly', function() { var readystatechanges = 0;
player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests[0]);
// trigger a segment download request
player.trigger('timeupdate');
player.tech_.hls.segmentXhr_.onreadystatechange = function() { readystatechanges++; };
// attempt to seek while the download is in progress
player.currentTime(12); clock.tick(1);
ok(requests[1].aborted, 'XHR aborted'); strictEqual(requests.length, 3, 'opened new XHR'); notEqual(player.tech_.hls.segmentXhr_.url, requests[1].url, 'a new segment is request that is not the aborted one'); strictEqual(readystatechanges, 0, 'onreadystatechange was not called'); });
test('segmentXhr is properly nulled out when dispose is called', function() { var readystatechanges = 0, oldDispose = Flash.prototype.dispose, player; Flash.prototype.dispose = function() {};
player = createPlayer(); player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests[0]);
// trigger a segment download request
player.trigger('timeupdate');
player.tech_.hls.segmentXhr_.onreadystatechange = function() { readystatechanges++; };
player.tech_.hls.dispose();
ok(requests[1].aborted, 'XHR aborted'); strictEqual(requests.length, 2, 'did not open a new XHR'); equal(player.tech_.hls.segmentXhr_, null, 'the segment xhr is nulled out'); strictEqual(readystatechanges, 0, 'onreadystatechange was not called');
Flash.prototype.dispose = oldDispose; });
test('does not modify the media index for in-buffer seeking', function() { var mediaIndex; player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); standardXHRResponse(requests.shift()); player.tech_.buffered = function() { return videojs.createTimeRange(0, 20); }; mediaIndex = player.tech_.hls.mediaIndex;
player.tech_.setCurrentTime(11); clock.tick(1); equal(player.tech_.hls.mediaIndex, mediaIndex, 'did not interrupt buffering'); equal(requests.length, 1, 'did not abort the outstanding request'); });
test('playlist 404 should end stream with a network error', function() { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); requests.pop().respond(404);
equal(player.tech_.hls.mediaSource.error_, 'network', 'set a network error'); });
test('segment 404 should trigger MEDIA_ERR_NETWORK', function () { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' });
openMediaSource(player);
standardXHRResponse(requests[0]); requests[1].respond(404); ok(player.tech_.hls.error.message, 'an error message is available'); equal(2, player.tech_.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_NETWORK'); });
test('segment 500 should trigger MEDIA_ERR_ABORTED', function () { player.src({ src: 'manifest/media.m3u8', type: 'application/vnd.apple.mpegurl' });
openMediaSource(player);
standardXHRResponse(requests[0]); requests[1].respond(500); ok(player.tech_.hls.error.message, 'an error message is available'); equal(4, player.tech_.hls.error.code, 'Player error code should be set to MediaError.MEDIA_ERR_ABORTED'); });
test('seeking in an empty playlist is a non-erroring noop', function() { var requestsLength;
player.src({ src: 'manifest/empty-live.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
requests.shift().respond(200, null, '#EXTM3U\n');
requestsLength = requests.length; player.tech_.setCurrentTime(183); clock.tick(1);
equal(requests.length, requestsLength, 'made no additional requests'); });
test('tech\'s duration reports Infinity for live playlists', function() { player.src({ src: 'http://example.com/manifest/missingEndlist.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]);
strictEqual(player.tech_.duration(), Infinity, 'duration on the tech is infinity');
notEqual(player.tech_.hls.mediaSource.duration, Infinity, 'duration on the mediaSource is not infinity'); });
test('live playlist starts three target durations before live', function() { var mediaPlaylist; player.src({ src: 'live.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:101\n' + '#EXTINF:10,\n' + '0.ts\n' + '#EXTINF:10,\n' + '1.ts\n' + '#EXTINF:10,\n' + '2.ts\n' + '#EXTINF:10,\n' + '3.ts\n' + '#EXTINF:10,\n' + '4.ts\n');
equal(requests.length, 0, 'no outstanding segment request');
player.tech_.paused = function() { return false; }; player.tech_.trigger('play'); clock.tick(1); mediaPlaylist = player.tech_.hls.playlists.media(); equal(player.currentTime(), player.tech_.hls.seekable().end(0), 'seeked to the seekable end');
equal(requests.length, 1, 'begins buffering'); });
test('live playlist starts with correct currentTime value', function() { player.src({ src: 'http://example.com/manifest/liveStart30sBefore.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]);
player.tech_.hls.playlists.trigger('loadedmetadata');
player.tech_.paused = function() { return false; }; player.tech_.trigger('play'); clock.tick(1);
strictEqual(player.currentTime(), videojs.Hls.Playlist.seekable(player.tech_.hls.playlists.media()).end(0), 'currentTime is updated at playback'); });
test('resets the time to a seekable position when resuming a live stream ' + 'after a long break', function() { var seekTarget; player.src({ src: 'live0.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:16\n' + '#EXTINF:10,\n' + '16.ts\n'); // mock out the player to simulate a live stream that has been
// playing for awhile
player.tech_.hls.seekable = function() { return videojs.createTimeRange(160, 170); }; player.tech_.setCurrentTime = function(time) { if (time !== undefined) { seekTarget = time; } }; player.tech_.played = function() { return videojs.createTimeRange(120, 170); }; player.tech_.trigger('playing');
player.tech_.trigger('play'); equal(seekTarget, player.seekable().start(0), 'seeked to the start of seekable'); player.tech_.trigger('seeked'); });
test('reloads out-of-date live playlists when switching variants', function() { player.src({ src: 'http://example.com/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
player.tech_.hls.master = { playlists: [{ mediaSequence: 15, segments: [1, 1, 1] }, { uri: 'http://example.com/variant-update.m3u8', mediaSequence: 0, segments: [1, 1] }] }; // playing segment 15 on playlist zero
player.tech_.hls.media = player.tech_.hls.master.playlists[0]; player.mediaIndex = 1; window.manifests['variant-update'] = '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:16\n' + '#EXTINF:10,\n' + '16.ts\n' + '#EXTINF:10,\n' + '17.ts\n';
// switch playlists
player.tech_.hls.selectPlaylist = function() { return player.tech_.hls.master.playlists[1]; }; // timeupdate downloads segment 16 then switches playlists
player.trigger('timeupdate');
strictEqual(player.mediaIndex, 1, 'mediaIndex points at the next segment'); });
test('if withCredentials global option is used, withCredentials is set on the XHR object', function() { player.dispose(); videojs.options.hls = { withCredentials: true }; player = createPlayer(); player.src({ src: 'http://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); ok(requests[0].withCredentials, 'with credentials should be set to true if that option is passed in'); });
test('if withCredentials src option is used, withCredentials is set on the XHR object', function() { player.dispose(); player = createPlayer(); player.src({ src: 'http://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl', withCredentials: true }); openMediaSource(player); ok(requests[0].withCredentials, 'with credentials should be set to true if that option is passed in'); });
test('src level credentials supersede the global options', function() { player.dispose(); player = createPlayer(); player.src({ src: 'http://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl', withCredentials: true }); openMediaSource(player); ok(requests[0].withCredentials, 'with credentials should be set to true if that option is passed in');
});
test('does not break if the playlist has no segments', function() { player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); try { openMediaSource(player); requests[0].respond(200, null, '#EXTM3U\n' + '#EXT-X-PLAYLIST-TYPE:VOD\n' + '#EXT-X-TARGETDURATION:10\n'); } catch(e) { ok(false, 'an error was thrown'); throw e; } ok(true, 'no error was thrown'); strictEqual(requests.length, 1, 'no requests for non-existent segments were queued'); });
test('aborts segment processing on seek', function() { var currentTime = 0; player.src({ src: 'discontinuity.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.currentTime = function() { return currentTime; }; player.tech_.buffered = function() { return videojs.createTimeRange(); };
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + '#EXTINF:10,0\n' + '1.ts\n' + '#EXT-X-DISCONTINUITY\n' + '#EXTINF:10,0\n' + '2.ts\n' + '#EXT-X-ENDLIST\n'); // media
standardXHRResponse(requests.shift()); // 1.ts
standardXHRResponse(requests.shift()); // key.php
ok(player.tech_.hls.pendingSegment_, 'decrypting the segment');
// seek back to the beginning
player.currentTime(0); clock.tick(1); ok(!player.tech_.hls.pendingSegment_, 'aborted processing'); });
test('calls mediaSource\'s timestampOffset on discontinuity', function() { var buffered = [[]]; player.src({ src: 'discontinuity.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.play(); player.tech_.buffered = function() { return videojs.createTimeRange(buffered); };
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXTINF:10,0\n' + '1.ts\n' + '#EXT-X-DISCONTINUITY\n' + '#EXTINF:10,0\n' + '2.ts\n' + '#EXT-X-ENDLIST\n'); player.tech_.hls.sourceBuffer.timestampOffset = 0; standardXHRResponse(requests.shift()); // 1.ts
equal(player.tech_.hls.sourceBuffer.timestampOffset, 0, 'timestampOffset starts at zero');
buffered = [[0, 10]]; player.tech_.hls.sourceBuffer.trigger('updateend'); standardXHRResponse(requests.shift()); // 2.ts
equal(player.tech_.hls.sourceBuffer.timestampOffset, 10, 'timestampOffset set after discontinuity'); });
test('sets timestampOffset when seeking with discontinuities', function() { var timeRange = videojs.createTimeRange(0, 10);
player.src({ src: 'discontinuity.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.play(); player.tech_.buffered = function() { return timeRange; }; player.tech_.seeking = function (){ return true; };
requests.pop().respond(200, null, '#EXTM3U\n' + '#EXTINF:10,0\n' + '1.ts\n' + '#EXTINF:10,0\n' + '2.ts\n' + '#EXT-X-DISCONTINUITY\n' + '#EXTINF:10,0\n' + '3.ts\n' + '#EXT-X-ENDLIST\n'); player.tech_.hls.sourceBuffer.timestampOffset = 0; player.currentTime(21); clock.tick(1); equal(requests.shift().aborted, true, 'aborted first request'); standardXHRResponse(requests.pop()); // 3.ts
clock.tick(1000); equal(player.tech_.hls.sourceBuffer.timestampOffset, 20, 'timestampOffset starts at zero'); });
test('can seek before the source buffer opens', function() { player.src({ src: 'media.m3u8', type: 'application/vnd.apple.mpegurl' }); player.tech_.triggerReady(); clock.tick(1); standardXHRResponse(requests.shift()); player.triggerReady();
player.currentTime(1); equal(player.currentTime(), 1, 'seeked'); });
QUnit.skip('sets the timestampOffset after seeking to discontinuity', function() { var bufferEnd; player.src({ src: 'discontinuity.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.buffered = function() { return videojs.createTimeRange(0, bufferEnd); };
requests.pop().respond(200, null, '#EXTM3U\n' + '#EXTINF:10,0\n' + '1.ts\n' + '#EXT-X-DISCONTINUITY\n' + '#EXTINF:10,0\n' + '2.ts\n' + '#EXT-X-ENDLIST\n'); standardXHRResponse(requests.pop()); // 1.ts
// seek to a discontinuity
player.tech_.setCurrentTime(10); bufferEnd = 9.9; clock.tick(1); standardXHRResponse(requests.pop()); // 1.ts, again
player.tech_.hls.checkBuffer_(); standardXHRResponse(requests.pop()); // 2.ts
equal(player.tech_.hls.sourceBuffer.timestampOffset, 9.9, 'set the timestamp offset'); });
test('tracks segment end times as they are buffered', function() { var bufferEnd = 0; player.src({ src: 'media.m3u8', type: 'application/x-mpegURL' }); openMediaSource(player);
// as new segments are downloaded, the buffer end is updated
player.tech_.buffered = function() { return videojs.createTimeRange(0, bufferEnd); }; requests.shift().respond(200, null, '#EXTM3U\n' + '#EXTINF:10,\n' + '0.ts\n' + '#EXTINF:10,\n' + '1.ts\n' + '#EXT-X-ENDLIST\n');
// 0.ts is shorter than advertised
standardXHRResponse(requests.shift()); equal(player.tech_.hls.mediaSource.duration, 20, 'original duration is from the m3u8');
bufferEnd = 9.5; player.tech_.hls.sourceBuffer.trigger('update'); player.tech_.hls.sourceBuffer.trigger('updateend'); equal(player.tech_.hls.mediaSource.duration, 10 + 9.5, 'updated duration'); });
QUnit.skip('seeking does not fail when targeted between segments', function() { var currentTime, segmentUrl; player.src({ src: 'media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
// mock out the currentTime callbacks
player.tech_.el().vjs_setProperty = function(property, value) { if (property === 'currentTime') { currentTime = value; } }; player.tech_.el().vjs_getProperty = function(property) { if (property === 'currentTime') { return currentTime; } };
standardXHRResponse(requests.shift()); // media
standardXHRResponse(requests.shift()); // segment 0
player.tech_.hls.checkBuffer_(); segmentUrl = requests[0].url; standardXHRResponse(requests.shift()); // segment 1
// seek to a time that is greater than the last tag in segment 0 but
// less than the first in segment 1
// FIXME: it's not possible to seek here without timestamp-based
// segment durations
player.tech_.setCurrentTime(9.4); clock.tick(1); equal(requests[0].url, segmentUrl, 'requested the later segment');
standardXHRResponse(requests.shift()); // segment 1
player.tech_.trigger('seeked'); equal(player.currentTime(), 9.5, 'seeked to the later time'); });
test('resets the switching algorithm if a request times out', function() { player.src({ src: 'master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.hls.bandwidth = 1e20;
standardXHRResponse(requests.shift()); // master
standardXHRResponse(requests.shift()); // media.m3u8
// simulate a segment timeout
requests[0].timedout = true; requests.shift().abort();
standardXHRResponse(requests.shift());
strictEqual(player.tech_.hls.playlists.media(), player.tech_.hls.playlists.master.playlists[1], 'reset to the lowest bitrate playlist'); });
test('disposes the playlist loader', function() { var disposes = 0, player, loaderDispose; player = createPlayer(); player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); loaderDispose = player.tech_.hls.playlists.dispose; player.tech_.hls.playlists.dispose = function() { disposes++; loaderDispose.call(player.tech_.hls.playlists); };
player.dispose(); strictEqual(disposes, 1, 'disposed playlist loader'); });
test('remove event handlers on dispose', function() { var player, unscoped = 0;
player = createPlayer(); player.on = function(owner) { if (typeof owner !== 'object') { unscoped++; } }; player.off = function(owner) { if (typeof owner !== 'object') { unscoped--; } }; player.src({ src: 'manifest/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
standardXHRResponse(requests[0]); standardXHRResponse(requests[1]);
player.dispose();
ok(unscoped <= 0, 'no unscoped handlers'); });
test('aborts the source buffer on disposal', function() { var aborts = 0, player; player = createPlayer(); player.src({ src: 'manifest/master.m3u8', 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++; };
player.dispose(); strictEqual(aborts, 1, 'aborted the source buffer'); });
test('the source handler supports HLS mime types', function() { ['html5', 'flash'].forEach(function(techName) { ok(videojs.HlsSourceHandler(techName).canHandleSource({ type: 'aPplicatiOn/x-MPegUrl' }), 'supports x-mpegurl'); ok(videojs.HlsSourceHandler(techName).canHandleSource({ type: 'aPplicatiOn/VnD.aPPle.MpEgUrL' }), 'supports vnd.apple.mpegurl');
ok(!(videojs.HlsSourceHandler(techName).canHandleSource({ type: 'video/mp4' }) instanceof videojs.Hls), 'does not support mp4'); ok(!(videojs.HlsSourceHandler(techName).canHandleSource({ type: 'video/x-flv' }) instanceof videojs.Hls), 'does not support flv'); }); });
test('fires loadstart manually if Flash is used', function() { var tech = new (videojs.extend(videojs.EventTarget, { buffered: function() { return videojs.createTimeRange(); }, currentTime: function() { return 0; }, el: function() { return {}; }, preload: function() { return 'auto'; }, src: function() {}, setTimeout: window.setTimeout }))(), loadstarts = 0; tech.on('loadstart', function() { loadstarts++; }); videojs.HlsSourceHandler('flash').handleSource({ src: 'movie.m3u8', type: 'application/x-mpegURL' }, tech);
equal(loadstarts, 0, 'loadstart is not synchronous'); clock.tick(1); equal(loadstarts, 1, 'fired loadstart'); });
test('has no effect if native HLS is available', function() { var player; videojs.Hls.supportsNativeHls = true; player = createPlayer(); player.src({ src: 'http://example.com/manifest/master.m3u8', type: 'application/x-mpegURL' });
ok(!player.tech_.hls, 'did not load hls tech'); player.dispose(); });
test('is not supported on browsers without typed arrays', function() { var oldArray = window.Uint8Array; window.Uint8Array = null; ok(!videojs.Hls.isSupported(), 'HLS is not supported');
// cleanup
window.Uint8Array = oldArray; });
test('tracks the bytes downloaded', function() { player.src({ src: 'http://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
strictEqual(player.tech_.hls.bytesReceived, 0, 'no bytes received');
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXTINF:10,\n' + '0.ts\n' + '#EXTINF:10,\n' + '1.ts\n' + '#EXT-X-ENDLIST\n'); // transmit some segment bytes
requests[0].response = new ArrayBuffer(17); requests.shift().respond(200, null, ''); player.tech_.hls.sourceBuffer.trigger('updateend');
strictEqual(player.tech_.hls.bytesReceived, 17, 'tracked bytes received');
player.tech_.hls.checkBuffer_();
// transmit some more
requests[0].response = new ArrayBuffer(5); requests.shift().respond(200, null, '');
strictEqual(player.tech_.hls.bytesReceived, 22, 'tracked more bytes'); });
test('re-emits mediachange events', function() { var mediaChanges = 0; player.on('mediachange', function() { mediaChanges++; });
player.src({ src: 'http://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
player.tech_.hls.playlists.trigger('mediachange'); strictEqual(mediaChanges, 1, 'fired mediachange'); });
test('can be disposed before finishing initialization', function() { var readyHandlers = []; player.ready = function(callback) { readyHandlers.push(callback); }; player.src({ src: 'http://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl' }); player.src({ src: 'http://example.com/media.mp4', type: 'video/mp4' }); ok(readyHandlers.length > 0, 'registered a ready handler'); try { while (readyHandlers.length) { readyHandlers.shift().call(player); openMediaSource(player); } ok(true, 'did not throw an exception'); } catch (e) { ok(false, 'threw an exception'); } });
test('calls ended() on the media source at the end of a playlist', function() { var endOfStreams = 0, buffered = [[]]; player.src({ src: 'http://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.buffered = function() { return videojs.createTimeRanges(buffered); }; player.tech_.hls.mediaSource.endOfStream = function() { endOfStreams++; }; // playlist response
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXTINF:10,\n' + '0.ts\n' + '#EXT-X-ENDLIST\n'); // segment response
requests[0].response = new ArrayBuffer(17); requests.shift().respond(200, null, ''); strictEqual(endOfStreams, 0, 'waits for the buffer update to finish');
buffered =[[0, 10]]; player.tech_.hls.sourceBuffer.trigger('updateend'); strictEqual(endOfStreams, 1, 'ended media source'); });
test('calling play() at the end of a video replays', function() { var seekTime = -1; player.src({ src: 'http://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.setCurrentTime = function(time) { if (time !== undefined) { seekTime = time; } return 0; }; requests.shift().respond(200, null, '#EXTM3U\n' + '#EXTINF:10,\n' + '0.ts\n' + '#EXT-X-ENDLIST\n'); standardXHRResponse(requests.shift()); player.tech_.ended = function() { return true; };
player.tech_.trigger('play'); equal(seekTime, 0, 'seeked to the beginning'); });
test('segments remain pending without a source buffer', function() { player.src({ src: 'https://example.com/encrypted-media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=52"\n' + '#EXTINF:10,\n' + 'http://media.example.com/fileSequence52-A.ts' + '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php?r=53"\n' + '#EXTINF:10,\n' + 'http://media.example.com/fileSequence53-B.ts\n' + '#EXT-X-ENDLIST\n');
player.tech_.hls.sourceBuffer = undefined;
standardXHRResponse(requests.shift()); // key
standardXHRResponse(requests.shift()); // segment
player.tech_.hls.checkBuffer_(); ok(player.tech_.hls.pendingSegment_, 'waiting for the source buffer'); });
test('keys are requested when an encrypted segment is loaded', function() { player.src({ src: 'https://example.com/encrypted.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.trigger('play'); standardXHRResponse(requests.shift()); // playlist
strictEqual(requests.length, 2, 'a key XHR is created'); strictEqual(requests[0].url, player.tech_.hls.playlists.media().segments[0].key.uri, 'key XHR is created with correct uri'); strictEqual(requests[1].url, player.tech_.hls.playlists.media().segments[0].uri, 'segment XHR is created with correct uri'); });
test('keys are resolved relative to the master playlist', function() { player.src({ src: 'video/master-encrypted.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=17\n' + 'playlist/playlist.m3u8\n' + '#EXT-X-ENDLIST\n'); requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-TARGETDURATION:15\n' + '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence1.ts\n' + '#EXT-X-ENDLIST\n'); equal(requests.length, 2, 'requested the key'); equal(requests[0].url, absoluteUrl('video/playlist/keys/key.php'), 'resolves multiple relative paths'); });
test('keys are resolved relative to their containing playlist', function() { player.src({ src: 'video/media-encrypted.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-TARGETDURATION:15\n' + '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence1.ts\n' + '#EXT-X-ENDLIST\n'); equal(requests.length, 2, 'requested a key'); equal(requests[0].url, absoluteUrl('video/keys/key.php'), 'resolves multiple relative paths'); });
test('a new key XHR is created when a the segment is requested', function() { player.src({ src: 'https://example.com/encrypted-media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-TARGETDURATION:15\n' + '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence1.ts\n' + '#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence2.ts\n' + '#EXT-X-ENDLIST\n'); standardXHRResponse(requests.shift()); // key 1
standardXHRResponse(requests.shift()); // segment 1
// "finish" decrypting segment 1
player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16); player.tech_.hls.checkBuffer_(); player.tech_.buffered = function() { return videojs.createTimeRange(0, 2.833); }; player.tech_.hls.sourceBuffer.trigger('updateend');
strictEqual(requests.length, 2, 'a key XHR is created'); strictEqual(requests[0].url, 'https://example.com/' + player.tech_.hls.playlists.media().segments[1].key.uri, 'a key XHR is created with the correct uri'); });
test('seeking should abort an outstanding key request and create a new one', function() { player.src({ src: 'https://example.com/encrypted.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-TARGETDURATION:15\n' + '#EXT-X-KEY:METHOD=AES-128,URI="keys/key.php"\n' + '#EXTINF:9,\n' + 'http://media.example.com/fileSequence1.ts\n' + '#EXT-X-KEY:METHOD=AES-128,URI="keys/key2.php"\n' + '#EXTINF:9,\n' + 'http://media.example.com/fileSequence2.ts\n' + '#EXT-X-ENDLIST\n'); standardXHRResponse(requests.pop()); // segment 1
player.currentTime(11); clock.tick(1); ok(requests[0].aborted, 'the key XHR should be aborted'); requests.shift(); // aborted key 1
equal(requests.length, 2, 'requested the new key'); equal(requests[0].url, 'https://example.com/' + player.tech_.hls.playlists.media().segments[1].key.uri, 'urls should match'); });
test('retries key requests once upon failure', function() { player.src({ src: 'https://example.com/encrypted.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.trigger('play');
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence52-A.ts\n' + '#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'); standardXHRResponse(requests.pop()); // segment
requests[0].respond(404); equal(requests.length, 2, 'create a new XHR for the same key'); equal(requests[1].url, requests[0].url, 'should be the same key');
requests[1].respond(404); equal(requests.length, 2, 'gives up after one retry'); });
test('errors if key requests fail more than once', function() { var bytes = [];
player.src({ src: 'https://example.com/encrypted-media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.trigger('play');
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence52-A.ts\n' + '#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.pop()); // segment 1
requests.shift().respond(404); // fail key
requests.shift().respond(404); // fail key, again
player.tech_.hls.checkBuffer_();
equal(player.tech_.hls.mediaSource.error_, 'network', 'triggered a network error'); });
test('the key is supplied to the decrypter in the correct format', function() { var keys = [];
player.src({ src: 'https://example.com/encrypted-media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.trigger('play');
requests.pop().respond(200, null, '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:5\n' + '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence52-A.ts\n' + '#EXTINF:15.0,\n' + 'http://media.example.com/fileSequence52-B.ts\n');
videojs.Hls.Decrypter = function(encrypted, key) { keys.push(key); };
standardXHRResponse(requests.pop()); // segment
requests[0].response = new Uint32Array([0,1,2,3]).buffer; requests[0].respond(200, null, ''); requests.shift(); // key
equal(keys.length, 1, 'only one Decrypter was constructed'); deepEqual(keys[0], new Uint32Array([0, 0x01000000, 0x02000000, 0x03000000]), 'passed the specified segment key');
}); test('supplies the media sequence of current segment as the IV by default, if no IV is specified', function() { var ivs = [];
player.src({ src: 'https://example.com/encrypted-media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.trigger('play');
requests.pop().respond(200, null, '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:5\n' + '#EXT-X-KEY:METHOD=AES-128,URI="htts://priv.example.com/key.php?r=52"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence52-A.ts\n' + '#EXTINF:15.0,\n' + 'http://media.example.com/fileSequence52-B.ts\n');
videojs.Hls.Decrypter = function(encrypted, key, iv) { ivs.push(iv); };
requests[0].response = new Uint32Array([0,0,0,0]).buffer; requests[0].respond(200, null, ''); requests.shift(); standardXHRResponse(requests.pop());
equal(ivs.length, 1, 'only one Decrypter was constructed'); deepEqual(ivs[0], new Uint32Array([0, 0, 0, 5]), 'the IV for the segment is the media sequence'); });
test('switching playlists with an outstanding key request does not stall playback', function() { var buffered = []; var media = '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:5\n' + '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence52-A.ts\n' + '#EXTINF:15.0,\n' + 'http://media.example.com/fileSequence52-B.ts\n'; player.src({ src: 'https://example.com/master.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.trigger('play');
player.tech_.hls.bandwidth = 1; player.tech_.buffered = function() { return videojs.createTimeRange(buffered); }; // master playlist
standardXHRResponse(requests.shift()); // media playlist
requests.shift().respond(200, null, media); // mock out media switching from this point on
player.tech_.hls.playlists.media = function() { return player.tech_.hls.playlists.master.playlists[1]; }; // first segment of the original media playlist
standardXHRResponse(requests.pop());
// "switch" media
player.tech_.hls.playlists.trigger('mediachange'); ok(!requests[0].aborted, 'did not abort the key request');
// "finish" decrypting segment 1
standardXHRResponse(requests.shift()); // key
player.tech_.hls.pendingSegment_.bytes = new Uint8Array(16); player.tech_.hls.checkBuffer_(); buffered = [[0, 2.833]]; player.tech_.hls.sourceBuffer.trigger('updateend'); player.tech_.hls.checkBuffer_();
equal(requests.length, 1, 'made a request'); equal(requests[0].url, 'http://media.example.com/fileSequence52-B.ts', 'requested the segment'); });
test('resolves relative key URLs against the playlist', function() { player.src({ src: 'https://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player);
requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:5\n' + '#EXT-X-KEY:METHOD=AES-128,URI="key.php?r=52"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence52-A.ts\n' + '#EXT-X-ENDLIST\n'); equal(requests[0].url, 'https://example.com/key.php?r=52', 'resolves the key URL'); });
test('treats invalid keys as a key request failure', function() { var bytes = []; player.src({ src: 'https://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.trigger('play'); requests.shift().respond(200, null, '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:5\n' + '#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52"\n' + '#EXTINF:2.833,\n' + 'http://media.example.com/fileSequence52-A.ts\n' + '#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.pop()); // keys should be 16 bytes long
requests[0].response = new Uint8Array(1).buffer; requests.shift().respond(200, null, '');
equal(requests[0].url, 'https://priv.example.com/key.php?r=52', 'retries the key');
// the retried response is invalid, too
requests[0].response = new Uint8Array(1); requests.shift().respond(200, null, ''); player.tech_.hls.checkBuffer_();
// two failed attempts is a network error
equal(player.tech_.hls.mediaSource.error_, 'network', 'triggered a network error'); });
test('live stream should not call endOfStream', function(){ player.src({ src: 'https://example.com/media.m3u8', type: 'application/vnd.apple.mpegurl' }); openMediaSource(player); player.tech_.trigger('play'); requests[0].respond(200, null, '#EXTM3U\n' + '#EXT-X-MEDIA-SEQUENCE:0\n' + '#EXTINF:1\n' + '0.ts\n' ); requests[1].response = window.bcSegment; requests[1].respond(200, null, ""); equal("open", player.tech_.hls.mediaSource.readyState, "media source should be in open state, not ended state for live stream after the last segment in m3u8 downloaded"); });
test('does not download segments if preload option set to none', function() { player.preload('none'); player.src({ src: 'master.m3u8', type: 'application/vnd.apple.mpegurl' });
openMediaSource(player); standardXHRResponse(requests.shift()); // master
standardXHRResponse(requests.shift()); // media
player.tech_.hls.checkBuffer_();
requests = requests.filter(function(request) { return !/m3u8$/.test(request.uri); }); equal(requests.length, 0, 'did not download any segments'); });
module('Buffer Inspection');
test('detects time range edges added by updates', function() { var edges;
edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]), videojs.createTimeRange([[0, 11]])); deepEqual(edges, [{ end: 11 }], 'detected a forward addition');
edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[5, 10]]), videojs.createTimeRange([[0, 10]])); deepEqual(edges, [{ start: 0 }], 'detected a backward addition');
edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[5, 10]]), videojs.createTimeRange([[0, 11]])); deepEqual(edges, [ { start: 0 }, { end: 11 } ], 'detected forward and backward additions');
edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]), videojs.createTimeRange([[0, 10]])); deepEqual(edges, [], 'detected no addition');
edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([]), videojs.createTimeRange([[0, 10]])); deepEqual(edges, [ { start: 0 }, { end: 10 } ], 'detected an initial addition');
edges = videojs.Hls.bufferedAdditions_(videojs.createTimeRange([[0, 10]]), videojs.createTimeRange([[0, 10], [20, 30]])); deepEqual(edges, [ { start: 20 }, { end: 30} ], 'detected a non-contiguous addition'); });
test('treats null buffered ranges as no addition', function() { var edges = videojs.Hls.bufferedAdditions_(null, videojs.createTimeRange([[0, 11]]));
equal(edges.length, 0, 'no additions'); });
})(window, window.videojs);
|