import document from 'global/document'; import window from 'global/window'; import QUnit from 'qunit'; import sinon from 'sinon'; import videojs from 'video.js'; import HtmlMediaSource from '../src/html-media-source'; import { gopsSafeToAlignWith, updateGopBuffer, removeGopBuffer } from '../src/virtual-source-buffer'; // we disable this because browserify needs to include these files // but the exports are not important /* eslint-disable no-unused-vars */ import {MediaSource, URL} from '../src/videojs-contrib-media-sources.js'; /* eslint-disable no-unused-vars */ QUnit.module('videojs-contrib-media-sources - HTML', { beforeEach() { this.fixture = document.getElementById('qunit-fixture'); this.video = document.createElement('video'); this.fixture.appendChild(this.video); this.source = document.createElement('source'); this.player = videojs(this.video); // add a fake source so that we can get this.player_ on sourceopen this.url = 'fake.ts'; this.source.src = this.url; this.video.appendChild(this.source); // Mock the environment's timers because certain things - particularly // player readiness - are asynchronous in video.js 5. this.clock = sinon.useFakeTimers(); this.oldMediaSource = window.MediaSource || window.WebKitMediaSource; window.MediaSource = videojs.extend(videojs.EventTarget, { constructor() { this.isNative = true; this.sourceBuffers = []; this.duration = NaN; }, addSourceBuffer(type) { let buffer = new (videojs.extend(videojs.EventTarget, { type, appendBuffer() {} }))(); this.sourceBuffers.push(buffer); return buffer; } }); window.MediaSource.isTypeSupported = function(mime) { return true; }; window.WebKitMediaSource = window.MediaSource; }, afterEach() { this.clock.restore(); this.player.dispose(); window.MediaSource = this.oldMediaSource; window.WebKitMediaSource = window.MediaSource; } }); QUnit.test('constructs a native MediaSource', function() { QUnit.ok( new videojs.MediaSource().nativeMediaSource_.isNative, 'constructed a MediaSource' ); }); const createDataMessage = function(type, typedArray, extraObject) { let message = { data: { action: 'data', segment: { type, data: typedArray.buffer, initSegment: { data: typedArray.buffer, byteOffset: typedArray.byteOffset, byteLength: typedArray.byteLength } }, byteOffset: typedArray.byteOffset, byteLength: typedArray.byteLength } }; return Object.keys(extraObject || {}).reduce(function(obj, key) { obj.data.segment[key] = extraObject[key]; return obj; }, message); }; // Create a WebWorker-style message that signals the transmuxer is done const doneMessage = { data: { action: 'done' } }; // send fake data to the transmuxer to trigger the creation of the // native source buffers const initializeNativeSourceBuffers = function(sourceBuffer) { // initialize an audio source buffer sourceBuffer.transmuxer_.onmessage(createDataMessage('audio', new Uint8Array(1))); // initialize a video source buffer sourceBuffer.transmuxer_.onmessage(createDataMessage('video', new Uint8Array(1))); // instruct the transmuxer to flush the "data" it has buffered so // far sourceBuffer.transmuxer_.onmessage(doneMessage); }; QUnit.test('creates mp4 source buffers for mp2t segments', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); initializeNativeSourceBuffers(sourceBuffer); QUnit.ok(mediaSource.videoBuffer_, 'created a video buffer'); QUnit.equal( mediaSource.videoBuffer_.type, 'video/mp4;codecs="avc1.4d400d"', 'video buffer has the default codec' ); QUnit.ok(mediaSource.audioBuffer_, 'created an audio buffer'); QUnit.equal( mediaSource.audioBuffer_.type, 'audio/mp4;codecs="mp4a.40.2"', 'audio buffer has the default codec' ); QUnit.equal(mediaSource.sourceBuffers.length, 1, 'created one virtual buffer'); QUnit.equal( mediaSource.sourceBuffers[0], sourceBuffer, 'returned the virtual buffer' ); QUnit.ok(sourceBuffer.transmuxer_, 'created a transmuxer'); }); QUnit.test( 'the terminate is called on the transmuxer when the media source is killed', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let terminates = 0; sourceBuffer.transmuxer_ = { terminate() { terminates++; } }; mediaSource.trigger('sourceclose'); QUnit.equal(terminates, 1, 'called terminate on transmux web worker'); }); QUnit.test('duration is faked when playing a live stream', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); mediaSource.duration = Infinity; mediaSource.nativeMediaSource_.duration = 100; QUnit.equal(mediaSource.nativeMediaSource_.duration, 100, 'native duration was not set to infinity'); QUnit.equal(mediaSource.duration, Infinity, 'the MediaSource wrapper pretends it has an infinite duration'); }); QUnit.test( 'duration uses the underlying MediaSource\'s duration when not live', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); mediaSource.duration = 100; mediaSource.nativeMediaSource_.duration = 120; QUnit.equal(mediaSource.duration, 120, 'the MediaSource wrapper returns the native duration'); }); QUnit.test('abort on the fake source buffer calls abort on the real ones', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let messages = []; let aborts = 0; initializeNativeSourceBuffers(sourceBuffer); sourceBuffer.transmuxer_.postMessage = function(message) { messages.push(message); }; sourceBuffer.bufferUpdating_ = true; sourceBuffer.videoBuffer_.abort = function() { aborts++; }; sourceBuffer.audioBuffer_.abort = function() { aborts++; }; sourceBuffer.abort(); QUnit.equal(aborts, 2, 'called abort on both'); QUnit.equal( sourceBuffer.bufferUpdating_, false, 'set updating to false' ); QUnit.equal(messages.length, 1, 'has one message'); QUnit.equal(messages[0].action, 'reset', 'reset called on transmuxer'); }); QUnit.test( 'calling remove deletes cues and invokes remove on any extant source buffers', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let removedCue = []; let removes = 0; initializeNativeSourceBuffers(sourceBuffer); sourceBuffer.inbandTextTracks_ = { CC1: { removeCue(cue) { removedCue.push(cue); this.cues.splice(this.cues.indexOf(cue), 1); }, cues: [ {startTime: 10, endTime: 20, text: 'delete me'}, {startTime: 0, endTime: 2, text: 'save me'} ] } }; mediaSource.videoBuffer_.remove = function(start, end) { if (start === 3 && end === 10) { removes++; } }; mediaSource.audioBuffer_.remove = function(start, end) { if (start === 3 && end === 10) { removes++; } }; sourceBuffer.remove(3, 10); QUnit.equal(removes, 2, 'called remove on both sourceBuffers'); QUnit.equal( sourceBuffer.inbandTextTracks_.CC1.cues.length, 1, 'one cue remains after remove' ); QUnit.equal( removedCue[0].text, 'delete me', 'the cue that overlapped the remove region was removed' ); }); QUnit.test( 'calling remove property handles absence of cues (null)', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); initializeNativeSourceBuffers(sourceBuffer); sourceBuffer.inbandTextTracks_ = { CC1: { cues: null } }; mediaSource.videoBuffer_.remove = function(start, end) { // pass }; mediaSource.audioBuffer_.remove = function(start, end) { // pass }; // this call should not raise an exception sourceBuffer.remove(3, 10); QUnit.equal( sourceBuffer.inbandTextTracks_.CC1.cues, null, 'cues are still null' ); }); QUnit.test('removing doesn\'t happen with audio disabled', function() { let mediaSource = new videojs.MediaSource(); let muxedBuffer = mediaSource.addSourceBuffer('video/mp2t'); // creating this audio buffer disables audio in the muxed one let audioBuffer = mediaSource.addSourceBuffer('audio/mp2t; codecs="mp4a.40.2"'); let removedCue = []; let removes = 0; initializeNativeSourceBuffers(muxedBuffer); muxedBuffer.inbandTextTracks_ = { CC1: { removeCue(cue) { removedCue.push(cue); this.cues.splice(this.cues.indexOf(cue), 1); }, cues: [ {startTime: 10, endTime: 20, text: 'delete me'}, {startTime: 0, endTime: 2, text: 'save me'} ] } }; mediaSource.videoBuffer_.remove = function(start, end) { if (start === 3 && end === 10) { removes++; } }; mediaSource.audioBuffer_.remove = function(start, end) { if (start === 3 && end === 10) { removes++; } }; muxedBuffer.remove(3, 10); QUnit.equal(removes, 1, 'called remove on only one source buffer'); QUnit.equal(muxedBuffer.inbandTextTracks_.CC1.cues.length, 1, 'one cue remains after remove'); QUnit.equal(removedCue[0].text, 'delete me', 'the cue that overlapped the remove region was removed'); }); QUnit.test('readyState delegates to the native implementation', function() { let mediaSource = new HtmlMediaSource(); QUnit.equal( mediaSource.readyState, mediaSource.nativeMediaSource_.readyState, 'readyStates are equal' ); mediaSource.nativeMediaSource_.readyState = 'nonsense stuff'; QUnit.equal( mediaSource.readyState, mediaSource.nativeMediaSource_.readyState, 'readyStates are equal' ); }); QUnit.test('addSeekableRange_ throws an error for media with known duration', function() { let mediaSource = new videojs.MediaSource(); mediaSource.duration = 100; QUnit.throws(function() { mediaSource.addSeekableRange_(0, 100); }, 'cannot add seekable range'); }); QUnit.test('addSeekableRange_ adds to the native MediaSource duration', function() { let mediaSource = new videojs.MediaSource(); mediaSource.duration = Infinity; mediaSource.addSeekableRange_(120, 240); QUnit.equal(mediaSource.nativeMediaSource_.duration, 240, 'set native duration'); QUnit.equal(mediaSource.duration, Infinity, 'emulated duration'); mediaSource.addSeekableRange_(120, 220); QUnit.equal(mediaSource.nativeMediaSource_.duration, 240, 'ignored the smaller range'); QUnit.equal(mediaSource.duration, Infinity, 'emulated duration'); }); QUnit.test('appendBuffer error triggers on the player', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let error = false; mediaSource.player_ = this.player; initializeNativeSourceBuffers(sourceBuffer); sourceBuffer.videoBuffer_.appendBuffer = () => { throw new Error(); }; this.player.on('error', () => error = true); // send fake data to the source buffer from the transmuxer to append to native buffer // initializeNativeSourceBuffers does the same thing to trigger the creation of // native source buffers. let fakeTransmuxerMessage = initializeNativeSourceBuffers; fakeTransmuxerMessage(sourceBuffer); this.clock.tick(1); QUnit.ok(error, 'error triggered on player'); }); QUnit.test('transmuxes mp2t segments', function() { let mp2tSegments = []; let mp4Segments = []; let data = new Uint8Array(1); let mediaSource; let sourceBuffer; mediaSource = new videojs.MediaSource(); sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.transmuxer_.postMessage = function(segment) { if (segment.action === 'push') { let buffer = new Uint8Array(segment.data, segment.byteOffset, segment.byteLength); mp2tSegments.push(buffer); } }; sourceBuffer.concatAndAppendSegments_ = function(segmentObj, destinationBuffer) { mp4Segments.push(segmentObj); }; sourceBuffer.appendBuffer(data); QUnit.equal(mp2tSegments.length, 1, 'transmuxed one segment'); QUnit.equal(mp2tSegments[0].length, 1, 'did not alter the segment'); QUnit.equal(mp2tSegments[0][0], data[0], 'did not alter the segment'); // an init segment sourceBuffer.transmuxer_.onmessage(createDataMessage('video', new Uint8Array(1))); // a media segment sourceBuffer.transmuxer_.onmessage(createDataMessage('audio', new Uint8Array(1))); // Segments are concatenated QUnit.equal( mp4Segments.length, 0, 'segments are not appended until after the `done` message' ); // send `done` message sourceBuffer.transmuxer_.onmessage(doneMessage); // Segments are concatenated QUnit.equal(mp4Segments.length, 2, 'appended the segments'); }); QUnit.test( 'handles typed-arrays that are subsets of their underlying buffer', function() { let mp2tSegments = []; let mp4Segments = []; let dataBuffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]); let data = dataBuffer.subarray(5, 7); let mediaSource; let sourceBuffer; mediaSource = new videojs.MediaSource(); sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.transmuxer_.postMessage = function(segment) { if (segment.action === 'push') { let buffer = new Uint8Array(segment.data, segment.byteOffset, segment.byteLength); mp2tSegments.push(buffer); } }; sourceBuffer.concatAndAppendSegments_ = function(segmentObj, destinationBuffer) { mp4Segments.push(segmentObj.segments[0]); }; sourceBuffer.appendBuffer(data); QUnit.equal(mp2tSegments.length, 1, 'emitted the fragment'); QUnit.equal( mp2tSegments[0].length, 2, 'correctly handled a typed-array that is a subset' ); QUnit.equal(mp2tSegments[0][0], 5, 'fragment contains the correct first byte'); QUnit.equal(mp2tSegments[0][1], 6, 'fragment contains the correct second byte'); // an init segment sourceBuffer.transmuxer_.onmessage(createDataMessage('video', data)); // Segments are concatenated QUnit.equal( mp4Segments.length, 0, 'segments are not appended until after the `done` message' ); // send `done` message sourceBuffer.transmuxer_.onmessage(doneMessage); // Segments are concatenated QUnit.equal(mp4Segments.length, 1, 'emitted the fragment'); QUnit.equal( mp4Segments[0].length, 2, 'correctly handled a typed-array that is a subset' ); QUnit.equal(mp4Segments[0][0], 5, 'fragment contains the correct first byte'); QUnit.equal(mp4Segments[0][1], 6, 'fragment contains the correct second byte'); }); QUnit.test( 'only appends audio init segment for first segment or on audio/media changes', function() { let mp4Segments = []; let initBuffer = new Uint8Array([0, 1]); let dataBuffer = new Uint8Array([2, 3]); let mediaSource; let sourceBuffer; mediaSource = new videojs.MediaSource(); sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.audioDisabled_ = false; mediaSource.player_ = this.player; mediaSource.url_ = this.url; mediaSource.trigger('sourceopen'); sourceBuffer.concatAndAppendSegments_ = function(segmentObj, destinationBuffer) { let segment = segmentObj.segments.reduce((seg, arr) => seg.concat(Array.from(arr)), []); mp4Segments.push(segment); }; QUnit.ok(sourceBuffer.appendAudioInitSegment_, 'will append init segment next'); // an init segment sourceBuffer.transmuxer_.onmessage(createDataMessage('audio', dataBuffer, { initSegment: { data: initBuffer.buffer, byteOffset: initBuffer.byteOffset, byteLength: initBuffer.byteLength } })); // Segments are concatenated QUnit.equal( mp4Segments.length, 0, 'segments are not appended until after the `done` message' ); // send `done` message sourceBuffer.transmuxer_.onmessage(doneMessage); // Segments are concatenated QUnit.equal(mp4Segments.length, 1, 'emitted the fragment'); // Contains init segment on first segment QUnit.equal(mp4Segments[0][0], 0, 'fragment contains the correct first byte'); QUnit.equal(mp4Segments[0][1], 1, 'fragment contains the correct second byte'); QUnit.equal(mp4Segments[0][2], 2, 'fragment contains the correct third byte'); QUnit.equal(mp4Segments[0][3], 3, 'fragment contains the correct fourth byte'); QUnit.ok(!sourceBuffer.appendAudioInitSegment_, 'will not append init segment next'); dataBuffer = new Uint8Array([4, 5]); sourceBuffer.transmuxer_.onmessage(createDataMessage('audio', dataBuffer, { initSegment: { data: initBuffer.buffer, byteOffset: initBuffer.byteOffset, byteLength: initBuffer.byteLength } })); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(mp4Segments.length, 2, 'emitted the fragment'); // does not contain init segment on next segment QUnit.equal(mp4Segments[1][0], 4, 'fragment contains the correct first byte'); QUnit.equal(mp4Segments[1][1], 5, 'fragment contains the correct second byte'); // audio track change this.player.audioTracks().trigger('change'); sourceBuffer.audioDisabled_ = false; QUnit.ok(sourceBuffer.appendAudioInitSegment_, 'audio change sets appendAudioInitSegment_'); dataBuffer = new Uint8Array([6, 7]); sourceBuffer.transmuxer_.onmessage(createDataMessage('audio', dataBuffer, { initSegment: { data: initBuffer.buffer, byteOffset: initBuffer.byteOffset, byteLength: initBuffer.byteLength } })); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(mp4Segments.length, 3, 'emitted the fragment'); // contains init segment after audio track change QUnit.equal(mp4Segments[2][0], 0, 'fragment contains the correct first byte'); QUnit.equal(mp4Segments[2][1], 1, 'fragment contains the correct second byte'); QUnit.equal(mp4Segments[2][2], 6, 'fragment contains the correct third byte'); QUnit.equal(mp4Segments[2][3], 7, 'fragment contains the correct fourth byte'); QUnit.ok(!sourceBuffer.appendAudioInitSegment_, 'will not append init segment next'); dataBuffer = new Uint8Array([8, 9]); sourceBuffer.transmuxer_.onmessage(createDataMessage('audio', dataBuffer, { initSegment: { data: initBuffer.buffer, byteOffset: initBuffer.byteOffset, byteLength: initBuffer.byteLength } })); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(mp4Segments.length, 4, 'emitted the fragment'); // does not contain init segment in next segment QUnit.equal(mp4Segments[3][0], 8, 'fragment contains the correct first byte'); QUnit.equal(mp4Segments[3][1], 9, 'fragment contains the correct second byte'); QUnit.ok(!sourceBuffer.appendAudioInitSegment_, 'will not append init segment next'); // rendition switch this.player.trigger('mediachange'); QUnit.ok(sourceBuffer.appendAudioInitSegment_, 'media change sets appendAudioInitSegment_'); dataBuffer = new Uint8Array([10, 11]); sourceBuffer.transmuxer_.onmessage(createDataMessage('audio', dataBuffer, { initSegment: { data: initBuffer.buffer, byteOffset: initBuffer.byteOffset, byteLength: initBuffer.byteLength } })); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(mp4Segments.length, 5, 'emitted the fragment'); // contains init segment after audio track change QUnit.equal(mp4Segments[4][0], 0, 'fragment contains the correct first byte'); QUnit.equal(mp4Segments[4][1], 1, 'fragment contains the correct second byte'); QUnit.equal(mp4Segments[4][2], 10, 'fragment contains the correct third byte'); QUnit.equal(mp4Segments[4][3], 11, 'fragment contains the correct fourth byte'); QUnit.ok(!sourceBuffer.appendAudioInitSegment_, 'will not append init segment next'); }); QUnit.test( 'appends video init segment for every segment', function() { let mp4Segments = []; let initBuffer = new Uint8Array([0, 1]); let dataBuffer = new Uint8Array([2, 3]); let mediaSource; let sourceBuffer; mediaSource = new videojs.MediaSource(); sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); mediaSource.player_ = this.player; mediaSource.url_ = this.url; mediaSource.trigger('sourceopen'); sourceBuffer.concatAndAppendSegments_ = function(segmentObj, destinationBuffer) { let segment = segmentObj.segments.reduce((seg, arr) => seg.concat(Array.from(arr)), []); mp4Segments.push(segment); }; // an init segment sourceBuffer.transmuxer_.onmessage(createDataMessage('video', dataBuffer, { initSegment: { data: initBuffer.buffer, byteOffset: initBuffer.byteOffset, byteLength: initBuffer.byteLength } })); // Segments are concatenated QUnit.equal( mp4Segments.length, 0, 'segments are not appended until after the `done` message' ); // send `done` message sourceBuffer.transmuxer_.onmessage(doneMessage); // Segments are concatenated QUnit.equal(mp4Segments.length, 1, 'emitted the fragment'); // Contains init segment on first segment QUnit.equal(mp4Segments[0][0], 0, 'fragment contains the correct first byte'); QUnit.equal(mp4Segments[0][1], 1, 'fragment contains the correct second byte'); QUnit.equal(mp4Segments[0][2], 2, 'fragment contains the correct third byte'); QUnit.equal(mp4Segments[0][3], 3, 'fragment contains the correct fourth byte'); dataBuffer = new Uint8Array([4, 5]); sourceBuffer.transmuxer_.onmessage(createDataMessage('video', dataBuffer, { initSegment: { data: initBuffer.buffer, byteOffset: initBuffer.byteOffset, byteLength: initBuffer.byteLength } })); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(mp4Segments.length, 2, 'emitted the fragment'); QUnit.equal(mp4Segments[1][0], 0, 'fragment contains the correct first byte'); QUnit.equal(mp4Segments[1][1], 1, 'fragment contains the correct second byte'); QUnit.equal(mp4Segments[1][2], 4, 'fragment contains the correct third byte'); QUnit.equal(mp4Segments[1][3], 5, 'fragment contains the correct fourth byte'); dataBuffer = new Uint8Array([6, 7]); sourceBuffer.transmuxer_.onmessage(createDataMessage('video', dataBuffer, { initSegment: { data: initBuffer.buffer, byteOffset: initBuffer.byteOffset, byteLength: initBuffer.byteLength } })); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(mp4Segments.length, 3, 'emitted the fragment'); // contains init segment after audio track change QUnit.equal(mp4Segments[2][0], 0, 'fragment contains the correct first byte'); QUnit.equal(mp4Segments[2][1], 1, 'fragment contains the correct second byte'); QUnit.equal(mp4Segments[2][2], 6, 'fragment contains the correct third byte'); QUnit.equal(mp4Segments[2][3], 7, 'fragment contains the correct fourth byte'); }); QUnit.test('handles empty codec string value', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t; codecs=""'); initializeNativeSourceBuffers(sourceBuffer); QUnit.ok(mediaSource.videoBuffer_, 'created a video buffer'); QUnit.equal( mediaSource.videoBuffer_.type, 'video/mp4;codecs="avc1.4d400d"', 'video buffer has the default codec' ); QUnit.ok(mediaSource.audioBuffer_, 'created an audio buffer'); QUnit.equal( mediaSource.audioBuffer_.type, 'audio/mp4;codecs="mp4a.40.2"', 'audio buffer has the default codec' ); QUnit.equal(mediaSource.sourceBuffers.length, 1, 'created one virtual buffer'); QUnit.equal( mediaSource.sourceBuffers[0], sourceBuffer, 'returned the virtual buffer' ); }); QUnit.test('can create an audio buffer by itself', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t; codecs="mp4a.40.2"'); initializeNativeSourceBuffers(sourceBuffer); QUnit.ok(!mediaSource.videoBuffer_, 'did not create a video buffer'); QUnit.ok(mediaSource.audioBuffer_, 'created an audio buffer'); QUnit.equal( mediaSource.audioBuffer_.type, 'audio/mp4;codecs="mp4a.40.2"', 'audio buffer has the default codec' ); QUnit.equal(mediaSource.sourceBuffers.length, 1, 'created one virtual buffer'); QUnit.equal( mediaSource.sourceBuffers[0], sourceBuffer, 'returned the virtual buffer' ); }); QUnit.test('can create an video buffer by itself', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t; codecs="avc1.4d400d"'); initializeNativeSourceBuffers(sourceBuffer); QUnit.ok(!mediaSource.audioBuffer_, 'did not create an audio buffer'); QUnit.ok(mediaSource.videoBuffer_, 'created an video buffer'); QUnit.equal( mediaSource.videoBuffer_.type, 'video/mp4;codecs="avc1.4d400d"', 'video buffer has the codec that was passed' ); QUnit.equal(mediaSource.sourceBuffers.length, 1, 'created one virtual buffer'); QUnit.equal( mediaSource.sourceBuffers[0], sourceBuffer, 'returned the virtual buffer' ); }); QUnit.test('handles invalid codec string', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t; codecs="nope"'); initializeNativeSourceBuffers(sourceBuffer); QUnit.ok(mediaSource.videoBuffer_, 'created a video buffer'); QUnit.equal( mediaSource.videoBuffer_.type, 'video/mp4;codecs="avc1.4d400d"', 'video buffer has the default codec' ); QUnit.ok(mediaSource.audioBuffer_, 'created an audio buffer'); QUnit.equal( mediaSource.audioBuffer_.type, 'audio/mp4;codecs="mp4a.40.2"', 'audio buffer has the default codec' ); QUnit.equal(mediaSource.sourceBuffers.length, 1, 'created one virtual buffer'); QUnit.equal( mediaSource.sourceBuffers[0], sourceBuffer, 'returned the virtual buffer' ); }); QUnit.test('handles codec strings in reverse order', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t; codecs="mp4a.40.5,avc1.64001f"'); initializeNativeSourceBuffers(sourceBuffer); QUnit.ok(mediaSource.videoBuffer_, 'created a video buffer'); QUnit.equal( mediaSource.videoBuffer_.type, 'video/mp4;codecs="avc1.64001f"', 'video buffer has the passed codec' ); QUnit.ok(mediaSource.audioBuffer_, 'created an audio buffer'); QUnit.equal( mediaSource.audioBuffer_.type, 'audio/mp4;codecs="mp4a.40.5"', 'audio buffer has the passed codec' ); QUnit.equal(mediaSource.sourceBuffers.length, 1, 'created one virtual buffer'); QUnit.equal( mediaSource.sourceBuffers[0], sourceBuffer, 'returned the virtual buffer' ); QUnit.ok(sourceBuffer.transmuxer_, 'created a transmuxer'); }); QUnit.test('forwards codec strings to native buffers when specified', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t; codecs="avc1.64001f,mp4a.40.5"'); initializeNativeSourceBuffers(sourceBuffer); QUnit.ok(mediaSource.videoBuffer_, 'created a video buffer'); QUnit.equal(mediaSource.videoBuffer_.type, 'video/mp4;codecs="avc1.64001f"', 'passed the video codec along'); QUnit.ok(mediaSource.audioBuffer_, 'created a video buffer'); QUnit.equal(mediaSource.audioBuffer_.type, 'audio/mp4;codecs="mp4a.40.5"', 'passed the audio codec along'); }); QUnit.test('parses old-school apple codec strings to the modern standard', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t; codecs="avc1.100.31,mp4a.40.5"'); initializeNativeSourceBuffers(sourceBuffer); QUnit.ok(mediaSource.videoBuffer_, 'created a video buffer'); QUnit.equal(mediaSource.videoBuffer_.type, 'video/mp4;codecs="avc1.64001f"', 'passed the video codec along'); QUnit.ok(mediaSource.audioBuffer_, 'created a video buffer'); QUnit.equal(mediaSource.audioBuffer_.type, 'audio/mp4;codecs="mp4a.40.5"', 'passed the audio codec along'); }); QUnit.test('specifies reasonable codecs if none are specified', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); initializeNativeSourceBuffers(sourceBuffer); QUnit.ok(mediaSource.videoBuffer_, 'created a video buffer'); QUnit.equal(mediaSource.videoBuffer_.type, 'video/mp4;codecs="avc1.4d400d"', 'passed the video codec along'); QUnit.ok(mediaSource.audioBuffer_, 'created a video buffer'); QUnit.equal(mediaSource.audioBuffer_.type, 'audio/mp4;codecs="mp4a.40.2"', 'passed the audio codec along'); }); QUnit.test('virtual buffers are updating if either native buffer is', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); initializeNativeSourceBuffers(sourceBuffer); mediaSource.videoBuffer_.updating = true; mediaSource.audioBuffer_.updating = false; QUnit.equal(sourceBuffer.updating, true, 'virtual buffer is updating'); mediaSource.audioBuffer_.updating = true; QUnit.equal(sourceBuffer.updating, true, 'virtual buffer is updating'); mediaSource.videoBuffer_.updating = false; QUnit.equal(sourceBuffer.updating, true, 'virtual buffer is updating'); mediaSource.audioBuffer_.updating = false; QUnit.equal(sourceBuffer.updating, false, 'virtual buffer is not updating'); }); QUnit.test( 'virtual buffers have a position buffered if both native buffers do', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); initializeNativeSourceBuffers(sourceBuffer); mediaSource.videoBuffer_.buffered = videojs.createTimeRanges([ [0, 10], [20, 30] ]); mediaSource.audioBuffer_.buffered = videojs.createTimeRanges([ [0, 7], [11, 15], [16, 40] ]); QUnit.equal(sourceBuffer.buffered.length, 2, 'two buffered ranges'); QUnit.equal(sourceBuffer.buffered.start(0), 0, 'first starts at zero'); QUnit.equal(sourceBuffer.buffered.end(0), 7, 'first ends at seven'); QUnit.equal(sourceBuffer.buffered.start(1), 20, 'second starts at twenty'); QUnit.equal(sourceBuffer.buffered.end(1), 30, 'second ends at 30'); }); QUnit.test('disabled audio does not affect buffered property', function() { let mediaSource = new videojs.MediaSource(); let muxedBuffer = mediaSource.addSourceBuffer('video/mp2t'); // creating a separate audio buffer disables audio on the muxed one let audioBuffer = mediaSource.addSourceBuffer('audio/mp2t; codecs="mp4a.40.2"'); initializeNativeSourceBuffers(muxedBuffer); mediaSource.videoBuffer_.buffered = videojs.createTimeRanges([[1, 10]]); mediaSource.audioBuffer_.buffered = videojs.createTimeRanges([[2, 11]]); QUnit.equal(audioBuffer.buffered.length, 1, 'one buffered range'); QUnit.equal(audioBuffer.buffered.start(0), 2, 'starts at two'); QUnit.equal(audioBuffer.buffered.end(0), 11, 'ends at eleven'); QUnit.equal(muxedBuffer.buffered.length, 1, 'one buffered range'); QUnit.equal(muxedBuffer.buffered.start(0), 1, 'starts at one'); QUnit.equal(muxedBuffer.buffered.end(0), 10, 'ends at ten'); }); QUnit.test('sets transmuxer baseMediaDecodeTime on appends', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let resets = []; sourceBuffer.transmuxer_.postMessage = function(message) { if (message.action === 'setTimestampOffset') { resets.push(message.timestampOffset); } }; sourceBuffer.timestampOffset = 42; QUnit.equal( resets.length, 1, 'reset called' ); QUnit.equal( resets[0], 42, 'set the baseMediaDecodeTime based on timestampOffset' ); }); QUnit.test('aggregates source buffer update events', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let updates = 0; let updateends = 0; let updatestarts = 0; initializeNativeSourceBuffers(sourceBuffer); mediaSource.player_ = this.player; sourceBuffer.addEventListener('updatestart', function() { updatestarts++; }); sourceBuffer.addEventListener('update', function() { updates++; }); sourceBuffer.addEventListener('updateend', function() { updateends++; }); QUnit.equal(updatestarts, 0, 'no updatestarts before a `done` message is received'); QUnit.equal(updates, 0, 'no updates before a `done` message is received'); QUnit.equal(updateends, 0, 'no updateends before a `done` message is received'); // the video buffer begins updating first: sourceBuffer.videoBuffer_.updating = true; sourceBuffer.audioBuffer_.updating = false; sourceBuffer.videoBuffer_.trigger('updatestart'); QUnit.equal(updatestarts, 1, 'aggregated updatestart'); sourceBuffer.audioBuffer_.updating = true; sourceBuffer.audioBuffer_.trigger('updatestart'); QUnit.equal(updatestarts, 1, 'aggregated updatestart'); // the audio buffer finishes first: sourceBuffer.audioBuffer_.updating = false; sourceBuffer.videoBuffer_.updating = true; sourceBuffer.audioBuffer_.trigger('update'); QUnit.equal(updates, 0, 'waited for the second update'); sourceBuffer.videoBuffer_.updating = false; sourceBuffer.videoBuffer_.trigger('update'); QUnit.equal(updates, 1, 'aggregated update'); // audio finishes first: sourceBuffer.videoBuffer_.updating = true; sourceBuffer.audioBuffer_.updating = false; sourceBuffer.audioBuffer_.trigger('updateend'); QUnit.equal(updateends, 0, 'waited for the second updateend'); sourceBuffer.videoBuffer_.updating = false; sourceBuffer.videoBuffer_.trigger('updateend'); QUnit.equal(updateends, 1, 'aggregated updateend'); }); QUnit.test('translates caption events into WebVTT cues', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let types = []; let hls608 = 0; mediaSource.player_ = { addRemoteTextTrack(options) { types.push(options.kind); return { track: { kind: options.kind, label: options.label, cues: [], addCue(cue) { this.cues.push(cue); } } }; }, textTracks() { return { getTrackById() {} }; }, remoteTextTracks() { }, tech_: new videojs.EventTarget() }; mediaSource.player_.tech_.on('usage', (event) => { if (event.name === 'hls-608') { hls608++; } }); sourceBuffer.timestampOffset = 10; sourceBuffer.transmuxer_.onmessage(createDataMessage('video', new Uint8Array(1), { captions: [{ startTime: 1, endTime: 3, text: 'This is an in-band caption in CC1', stream: 'CC1' }], captionStreams: {CC1: true} })); sourceBuffer.transmuxer_.onmessage(doneMessage); let cues = sourceBuffer.inbandTextTracks_.CC1.cues; QUnit.equal(hls608, 1, 'one hls-608 event was triggered'); QUnit.equal(types.length, 1, 'created one text track'); QUnit.equal(types[0], 'captions', 'the type was captions'); QUnit.equal(cues.length, 1, 'created one cue'); QUnit.equal(cues[0].text, 'This is an in-band caption in CC1', 'included the text'); QUnit.equal(cues[0].startTime, 11, 'started at eleven'); QUnit.equal(cues[0].endTime, 13, 'ended at thirteen'); }); QUnit.test('captions use existing tracks with id equal to CC#', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let addTrackCalled = 0; let tracks = { CC1: { kind: 'captions', label: 'CC1', id: 'CC1', cues: [], addCue(cue) { this.cues.push(cue); } }, CC2: { kind: 'captions', label: 'CC2', id: 'CC2', cues: [], addCue(cue) { this.cues.push(cue); } } }; mediaSource.player_ = { addRemoteTextTrack(options) { addTrackCalled++; }, textTracks() { return { getTrackById(id) { return tracks[id]; } }; }, remoteTextTracks() { }, tech_: new videojs.EventTarget() }; sourceBuffer.timestampOffset = 10; sourceBuffer.transmuxer_.onmessage(createDataMessage('video', new Uint8Array(1), { captions: [{ stream: 'CC1', startTime: 1, endTime: 3, text: 'This is an in-band caption in CC1' }, { stream: 'CC2', startTime: 1, endTime: 3, text: 'This is an in-band caption in CC2' }], captionStreams: {CC1: true, CC2: true} })); sourceBuffer.transmuxer_.onmessage(doneMessage); let cues = sourceBuffer.inbandTextTracks_.CC1.cues; QUnit.equal(addTrackCalled, 0, 'no tracks were created'); QUnit.equal(tracks.CC1.cues.length, 1, 'CC1 contains 1 cue'); QUnit.equal(tracks.CC2.cues.length, 1, 'CC2 contains 1 cue'); QUnit.equal(tracks.CC1.cues[0].text, 'This is an in-band caption in CC1', 'CC1 contains the right cue'); QUnit.equal(tracks.CC2.cues[0].text, 'This is an in-band caption in CC2', 'CC2 contains the right cue'); }); QUnit.test('translates metadata events into WebVTT cues', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); mediaSource.duration = Infinity; mediaSource.nativeMediaSource_.duration = 60; let types = []; let metadata = [{ cueTime: 2, frames: [{ url: 'This is a url tag' }, { value: 'This is a text tag' }] }, { cueTime: 12, frames: [{ data: 'This is a priv tag' }] }]; metadata.dispatchType = 0x10; mediaSource.player_ = { addRemoteTextTrack(options) { types.push(options.kind); return { track: { kind: options.kind, label: options.label, cues: [], addCue(cue) { this.cues.push(cue); } } }; }, remoteTextTracks() { } }; sourceBuffer.timestampOffset = 10; sourceBuffer.transmuxer_.onmessage(createDataMessage('video', new Uint8Array(1), { metadata })); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal( sourceBuffer.metadataTrack_.inBandMetadataTrackDispatchType, 16, 'in-band metadata track dispatch type correctly set' ); let cues = sourceBuffer.metadataTrack_.cues; QUnit.equal(types.length, 1, 'created one text track'); QUnit.equal(types[0], 'metadata', 'the type was metadata'); QUnit.equal(cues.length, 3, 'created three cues'); QUnit.equal(cues[0].text, 'This is a url tag', 'included the text'); QUnit.equal(cues[0].startTime, 12, 'started at twelve'); QUnit.equal(cues[0].endTime, 22, 'ended at StartTime of next cue(22)'); QUnit.equal(cues[1].text, 'This is a text tag', 'included the text'); QUnit.equal(cues[1].startTime, 12, 'started at twelve'); QUnit.equal(cues[1].endTime, 22, 'ended at the startTime of next cue(22)'); QUnit.equal(cues[2].text, 'This is a priv tag', 'included the text'); QUnit.equal(cues[2].startTime, 22, 'started at twenty two'); QUnit.equal(cues[2].endTime, Number.MAX_VALUE, 'ended at the maximum value'); mediaSource.duration = 100; mediaSource.trigger('sourceended'); QUnit.equal(cues[2].endTime, mediaSource.duration, 'sourceended is fired'); }); QUnit.test('does not wrap mp4 source buffers', function() { let mediaSource = new videojs.MediaSource(); mediaSource.addSourceBuffer('video/mp4;codecs=avc1.4d400d'); mediaSource.addSourceBuffer('audio/mp4;codecs=mp4a.40.2'); QUnit.equal( mediaSource.sourceBuffers.length, mediaSource.nativeMediaSource_.sourceBuffers.length, 'did not need virtual buffers' ); QUnit.equal(mediaSource.sourceBuffers.length, 2, 'created native buffers'); }); QUnit.test('can get activeSourceBuffers', function() { let mediaSource = new videojs.MediaSource(); // although activeSourceBuffers should technically be a SourceBufferList, we are // returning it as an array, and users may expect it to behave as such QUnit.ok(Array.isArray(mediaSource.activeSourceBuffers)); }); QUnit.test('active source buffers are updated on each buffer\'s updateend', function() { let mediaSource = new videojs.MediaSource(); let updateCallCount = 0; let sourceBuffer; mediaSource.updateActiveSourceBuffers_ = () => { updateCallCount++; }; sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); mediaSource.player_ = this.player; mediaSource.url_ = this.url; mediaSource.trigger('sourceopen'); QUnit.equal(updateCallCount, 0, 'active source buffers not updated on adding source buffer'); mediaSource.player_.audioTracks().trigger('addtrack'); QUnit.equal(updateCallCount, 1, 'active source buffers updated after addtrack'); sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); QUnit.equal(updateCallCount, 1, 'active source buffers not updated on adding second source buffer'); mediaSource.player_.audioTracks().trigger('removetrack'); QUnit.equal(updateCallCount, 2, 'active source buffers updated after removetrack'); mediaSource.player_.audioTracks().trigger('change'); QUnit.equal(updateCallCount, 3, 'active source buffers updated after change'); }); QUnit.test('combined buffer is the only active buffer when main track enabled', function() { let mediaSource = new videojs.MediaSource(); let sourceBufferAudio; let sourceBufferCombined; let audioTracks = [{ enabled: true, kind: 'main', label: 'main' }, { enabled: false, kind: 'alternative', label: 'English (UK)' }]; this.player.audioTracks = () => audioTracks; mediaSource.player_ = this.player; sourceBufferCombined = mediaSource.addSourceBuffer('video/m2pt'); sourceBufferCombined.videoCodec_ = true; sourceBufferCombined.audioCodec_ = true; sourceBufferAudio = mediaSource.addSourceBuffer('video/m2pt'); sourceBufferAudio.videoCodec_ = false; sourceBufferAudio.audioCodec_ = true; mediaSource.updateActiveSourceBuffers_(); QUnit.equal(mediaSource.activeSourceBuffers.length, 1, 'active source buffers starts with one source buffer'); QUnit.equal(mediaSource.activeSourceBuffers[0], sourceBufferCombined, 'active source buffers starts with combined source buffer'); }); QUnit.test('combined & audio buffers are active when alternative track enabled', function() { let mediaSource = new videojs.MediaSource(); let sourceBufferAudio; let sourceBufferCombined; let audioTracks = [{ enabled: false, kind: 'main', label: 'main' }, { enabled: true, kind: 'alternative', label: 'English (UK)' }]; this.player.audioTracks = () => audioTracks; mediaSource.player_ = this.player; sourceBufferCombined = mediaSource.addSourceBuffer('video/m2pt'); sourceBufferCombined.videoCodec_ = true; sourceBufferCombined.audioCodec_ = true; sourceBufferAudio = mediaSource.addSourceBuffer('video/m2pt'); sourceBufferAudio.videoCodec_ = false; sourceBufferAudio.audioCodec_ = true; mediaSource.updateActiveSourceBuffers_(); QUnit.equal(mediaSource.activeSourceBuffers.length, 2, 'active source buffers includes both source buffers'); // maintains same order as source buffers were created QUnit.equal(mediaSource.activeSourceBuffers[0], sourceBufferCombined, 'active source buffers starts with combined source buffer'); QUnit.equal(mediaSource.activeSourceBuffers[1], sourceBufferAudio, 'active source buffers ends with audio source buffer'); }); QUnit.test('video only & audio only buffers are always active', function() { let mediaSource = new videojs.MediaSource(); let sourceBufferAudio; let sourceBufferCombined; let audioTracks = [{ enabled: false, kind: 'main', label: 'main' }, { enabled: true, kind: 'alternative', label: 'English (UK)' }]; this.player.audioTracks = () => audioTracks; mediaSource.player_ = this.player; sourceBufferCombined = mediaSource.addSourceBuffer('video/m2pt'); sourceBufferCombined.videoCodec_ = true; sourceBufferCombined.audioCodec_ = false; sourceBufferAudio = mediaSource.addSourceBuffer('video/m2pt'); sourceBufferAudio.videoCodec_ = false; sourceBufferAudio.audioCodec_ = true; mediaSource.updateActiveSourceBuffers_(); QUnit.equal(mediaSource.activeSourceBuffers.length, 2, 'active source buffers includes both source buffers'); // maintains same order as source buffers were created QUnit.equal(mediaSource.activeSourceBuffers[0], sourceBufferCombined, 'active source buffers starts with combined source buffer'); QUnit.equal(mediaSource.activeSourceBuffers[1], sourceBufferAudio, 'active source buffers ends with audio source buffer'); audioTracks[0].enabled = true; audioTracks[1].enabled = false; mediaSource.updateActiveSourceBuffers_(); QUnit.equal(mediaSource.activeSourceBuffers.length, 2, 'active source buffers includes both source buffers'); // maintains same order as source buffers were created QUnit.equal(mediaSource.activeSourceBuffers[0], sourceBufferCombined, 'active source buffers starts with combined source buffer'); QUnit.equal(mediaSource.activeSourceBuffers[1], sourceBufferAudio, 'active source buffers ends with audio source buffer'); }); QUnit.test('Single buffer always active. Audio disabled depends on audio codec', function() { let mediaSource = new videojs.MediaSource(); let audioTracks = [{ enabled: true, kind: 'main', label: 'main' }]; this.player.audioTracks = () => audioTracks; mediaSource.player_ = this.player; let sourceBuffer = mediaSource.addSourceBuffer('video/m2pt'); // video only sourceBuffer.videoCodec_ = true; sourceBuffer.audioCodec_ = false; mediaSource.updateActiveSourceBuffers_(); QUnit.equal(mediaSource.activeSourceBuffers.length, 1, 'sourceBuffer is active'); QUnit.ok(mediaSource.activeSourceBuffers[0].audioDisabled_, 'audio is disabled on video only active sourceBuffer'); // audio only sourceBuffer.videoCodec_ = false; sourceBuffer.audioCodec_ = true; mediaSource.updateActiveSourceBuffers_(); QUnit.equal(mediaSource.activeSourceBuffers.length, 1, 'sourceBuffer is active'); QUnit.notOk(mediaSource.activeSourceBuffers[0].audioDisabled_, 'audio not disabled on audio only active sourceBuffer'); }); QUnit.test('video segments with info trigger videooinfo event', function() { let data = new Uint8Array(1); let infoEvents = []; let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let info = {width: 100}; let newinfo = {width: 225}; mediaSource.on('videoinfo', (e) => infoEvents.push(e)); // send an audio segment with info, then send done sourceBuffer.transmuxer_.onmessage(createDataMessage('video', data, {info})); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(infoEvents.length, 1, 'video info should trigger'); QUnit.deepEqual(infoEvents[0].info, info, 'video info = muxed info'); // send an audio segment with info, then send done sourceBuffer.transmuxer_.onmessage(createDataMessage('video', data, {info: newinfo})); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(infoEvents.length, 2, 'video info should trigger'); QUnit.deepEqual(infoEvents[1].info, newinfo, 'video info = muxed info'); }); QUnit.test('audio segments with info trigger audioinfo event', function() { let data = new Uint8Array(1); let infoEvents = []; let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t'); let info = {width: 100}; let newinfo = {width: 225}; mediaSource.on('audioinfo', (e) => infoEvents.push(e)); // send an audio segment with info, then send done sourceBuffer.transmuxer_.onmessage(createDataMessage('audio', data, {info})); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(infoEvents.length, 1, 'audio info should trigger'); QUnit.deepEqual(infoEvents[0].info, info, 'audio info = muxed info'); // send an audio segment with info, then send done sourceBuffer.transmuxer_.onmessage(createDataMessage('audio', data, {info: newinfo})); sourceBuffer.transmuxer_.onmessage(doneMessage); QUnit.equal(infoEvents.length, 2, 'audio info should trigger'); QUnit.deepEqual(infoEvents[1].info, newinfo, 'audio info = muxed info'); }); QUnit.test('creates native SourceBuffers immediately if a second ' + 'VirtualSourceBuffer is created', function() { let mediaSource = new videojs.MediaSource(); let sourceBuffer = mediaSource.addSourceBuffer('video/mp2t; codecs="avc1.64001f,mp4a.40.5"'); let sourceBuffer2 = mediaSource.addSourceBuffer('video/mp2t; codecs="mp4a.40.5"'); QUnit.ok(mediaSource.videoBuffer_, 'created a video buffer'); QUnit.equal( mediaSource.videoBuffer_.type, 'video/mp4;codecs="avc1.64001f"', 'video buffer has the specified codec' ); QUnit.ok(mediaSource.audioBuffer_, 'created an audio buffer'); QUnit.equal( mediaSource.audioBuffer_.type, 'audio/mp4;codecs="mp4a.40.5"', 'audio buffer has the specified codec' ); QUnit.equal(mediaSource.sourceBuffers.length, 2, 'created two virtual buffers'); QUnit.equal( mediaSource.sourceBuffers[0], sourceBuffer, 'returned the virtual buffer'); QUnit.equal( mediaSource.sourceBuffers[1], sourceBuffer2, 'returned the virtual buffer'); QUnit.equal( sourceBuffer.audioDisabled_, true, 'first source buffer\'s audio is automatically disabled'); QUnit.ok( sourceBuffer2.audioBuffer_, 'second source buffer has an audio source buffer'); }); QUnit.module('VirtualSourceBuffer - Isolated Functions'); QUnit.test('gopsSafeToAlignWith returns correct list', function() { // gopsSafeToAlignWith uses a 3 second safetyNet so that gops very close to the playhead // are not considered safe to append to const safetyNet = 3; const pts = (time) => Math.ceil(time * 90000); let mapping = 0; let currentTime = 0; let buffer = []; let player; let actual; let expected; expected = []; actual = gopsSafeToAlignWith(buffer, player, mapping); QUnit.deepEqual(actual, expected, 'empty array when player is undefined'); player = { currentTime: () => currentTime }; actual = gopsSafeToAlignWith(buffer, player, mapping); QUnit.deepEqual(actual, expected, 'empty array when buffer is empty'); buffer = expected = [ { pts: pts(currentTime + safetyNet + 1) }, { pts: pts(currentTime + safetyNet + 2) }, { pts: pts(currentTime + safetyNet + 3) } ]; actual = gopsSafeToAlignWith(buffer, player, mapping); QUnit.deepEqual(actual, expected, 'entire buffer considered safe when all gops come after currentTime + safetyNet'); buffer = [ { pts: pts(currentTime + safetyNet) }, { pts: pts(currentTime + safetyNet + 1) }, { pts: pts(currentTime + safetyNet + 2) } ]; expected = [ { pts: pts(currentTime + safetyNet + 1) }, { pts: pts(currentTime + safetyNet + 2) } ]; actual = gopsSafeToAlignWith(buffer, player, mapping); QUnit.deepEqual(actual, expected, 'safetyNet comparison is not inclusive'); currentTime = 10; mapping = -5; buffer = [ { pts: pts(currentTime - mapping + safetyNet - 2) }, { pts: pts(currentTime - mapping + safetyNet - 1) }, { pts: pts(currentTime - mapping + safetyNet) }, { pts: pts(currentTime - mapping + safetyNet + 1) }, { pts: pts(currentTime - mapping + safetyNet + 2) } ]; expected = [ { pts: pts(currentTime - mapping + safetyNet + 1) }, { pts: pts(currentTime - mapping + safetyNet + 2) } ]; actual = gopsSafeToAlignWith(buffer, player, mapping); QUnit.deepEqual(actual, expected, 'uses mapping to shift currentTime'); currentTime = 20; expected = []; actual = gopsSafeToAlignWith(buffer, player, mapping); QUnit.deepEqual(actual, expected, 'empty array when no gops in buffer come after currentTime'); }); QUnit.test('updateGopBuffer correctly processes new gop information', function() { let buffer = []; let gops = []; let replace = true; let actual; let expected; buffer = expected = [{ pts: 100 }, { pts: 200 }]; actual = updateGopBuffer(buffer, gops, replace); QUnit.deepEqual(actual, expected, 'returns buffer when no new gops'); gops = expected = [{ pts: 300 }, { pts: 400 }]; actual = updateGopBuffer(buffer, gops, replace); QUnit.deepEqual(actual, expected, 'returns only new gops when replace is true'); replace = false; buffer = []; gops = [{ pts: 100 }]; expected = [{ pts: 100 }]; actual = updateGopBuffer(buffer, gops, replace); QUnit.deepEqual(actual, expected, 'appends new gops to empty buffer'); buffer = [{ pts: 100 }, { pts: 200 }]; gops = [{ pts: 300 }, { pts: 400 }]; expected = [{ pts: 100 }, { pts: 200 }, { pts: 300 }, { pts: 400 }]; actual = updateGopBuffer(buffer, gops, replace); QUnit.deepEqual(actual, expected, 'appends new gops at end of buffer when no overlap'); buffer = [{ pts: 100 }, { pts: 200 }, { pts: 300 }, { pts: 400 }]; gops = [{ pts: 250 }, { pts: 300 }, { pts: 350 }]; expected = [{ pts: 100 }, { pts: 200 }, { pts: 250 }, { pts: 300 }, { pts: 350 }]; actual = updateGopBuffer(buffer, gops, replace); QUnit.deepEqual(actual, expected, 'slices buffer at point of overlap and appends new gops'); buffer = [{ pts: 100 }, { pts: 200 }, { pts: 300 }, { pts: 400 }]; gops = [{ pts: 200 }, { pts: 300 }, { pts: 350 }]; expected = [{ pts: 100 }, { pts: 200 }, { pts: 300 }, { pts: 350 }]; actual = updateGopBuffer(buffer, gops, replace); QUnit.deepEqual(actual, expected, 'overlap slice is inclusive'); buffer = [{ pts: 300 }, { pts: 400 }, { pts: 500 }, { pts: 600 }]; gops = [{ pts: 100 }, { pts: 200 }, { pts: 250 }]; expected = [{ pts: 100 }, { pts: 200 }, { pts: 250 }]; actual = updateGopBuffer(buffer, gops, replace); QUnit.deepEqual(actual, expected, 'completely replaces buffer with new gops when all gops come before buffer'); }); QUnit.test('removeGopBuffer correctly removes range from buffer', function() { const pts = (time) => Math.ceil(time * 90000); let buffer = []; let start = 0; let end = 0; let mapping = -5; let actual; let expected; expected = []; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'returns empty array when buffer empty'); start = 0; end = 8; buffer = expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'no removal when remove range comes before start of buffer'); start = 22; end = 30; buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'removes last gop when remove range is after end of buffer'); start = 0; end = 10; buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; expected = [ { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'clamps start range to begining of buffer'); start = 0; end = 12; buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; expected = [ { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'clamps start range to begining of buffer'); start = 0; end = 14; buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; expected = [ { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'clamps start range to begining of buffer'); start = 15; end = 30; buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'clamps end range to end of buffer'); start = 17; end = 30; buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'clamps end range to end of buffer'); start = 20; end = 30; buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'clamps end range to end of buffer'); buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; start = 12; end = 15; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'removes gops that remove range intersects with'); buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; start = 12; end = 14; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'removes gops that remove range intersects with'); buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; start = 13; end = 14; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'removes gops that remove range intersects with'); buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; start = 13; end = 15; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'removes gops that remove range intersects with'); buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; start = 12; end = 17; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'removes gops that remove range intersects with'); buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; start = 13; end = 16; expected = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'removes gops that remove range intersects with'); start = 10; end = 20; buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; expected = []; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'removes entire buffer when buffer inside remove range'); start = 0; end = 30; buffer = [ { pts: pts(10 - mapping) }, { pts: pts(11 - mapping) }, { pts: pts(12 - mapping) }, { pts: pts(15 - mapping) }, { pts: pts(18 - mapping) }, { pts: pts(20 - mapping) } ]; expected = []; actual = removeGopBuffer(buffer, start, end, mapping); QUnit.deepEqual(actual, expected, 'removes entire buffer when buffer inside remove range'); });