import document from 'global/document'; import window from 'global/window'; import QUnit from 'qunit'; import sinon from 'sinon'; import videojs from 'video.js'; import muxjs from 'mux.js'; import FlashSourceBuffer from '../src/flash-source-buffer'; import FlashConstants from '../src/flash-constants'; // 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 */ // return the sequence of calls to append to the SWF const appendCalls = function(calls) { return calls.filter(function(call) { return call.callee && call.callee === 'vjs_appendChunkReady'; }); }; const getFlvHeader = function() { return new Uint8Array([1, 2, 3]); }; const makeFlvTag = function(pts, data) { return { pts, dts: pts, bytes: data }; }; let timers; let oldSTO; const fakeSTO = function() { oldSTO = window.setTimeout; timers = []; timers.run = function(num) { let timer; while (num--) { timer = this.pop(); if (timer) { timer(); } } }; timers.runAll = function() { while (this.length) { this.pop()(); } }; window.setTimeout = function(callback) { timers.push(callback); }; window.setTimeout.fake = true; }; const unfakeSTO = function() { timers = []; window.setTimeout = oldSTO; }; // Create a WebWorker-style message that signals the transmuxer is done const createDataMessage = function(data, audioData, metadata, captions) { let captionStreams = {}; if (captions) { captions.forEach((caption) => { captionStreams[caption.stream] = true; }); } return { data: { action: 'data', segment: { tags: { videoTags: data.map((tag) => { return makeFlvTag(tag.pts, tag.bytes); }), audioTags: audioData ? audioData.map((tag) => { return makeFlvTag(tag.pts, tag.bytes); }) : [] }, metadata, captions, captionStreams } } }; }; const doneMessage = { data: { action: 'done' } }; const postMessage_ = function(msg) { if (msg.action === 'push') { window.setTimeout(()=> { this.onmessage(createDataMessage([{ bytes: new Uint8Array(msg.data, msg.byteOffset, msg.byteLength), pts: 0 }])); }, 1); } else if (msg.action === 'flush') { window.setTimeout(() => { this.onmessage(doneMessage); }, 1); } }; QUnit.module('Flash MediaSource', { beforeEach(assert) { let swfObj; // Mock the environment's timers because certain things - particularly // player readiness - are asynchronous in video.js 5. this.clock = sinon.useFakeTimers(); this.fixture = document.getElementById('qunit-fixture'); this.video = document.createElement('video'); this.fixture.appendChild(this.video); this.player = videojs(this.video); this.oldMediaSource = window.MediaSource || window.WebKitMediaSource; window.MediaSource = null; window.WebKitMediaSource = null; this.Flash = videojs.getTech('Flash'); this.oldFlashSupport = this.Flash.isSupported; this.oldCanPlay = this.Flash.canPlaySource; this.Flash.canPlaySource = this.Flash.isSupported = function() { return true; }; this.oldFlashTransmuxerPostMessage = muxjs.flv.Transmuxer.postMessage; this.oldGetFlvHeader = muxjs.flv.getFlvHeader; muxjs.flv.getFlvHeader = getFlvHeader; this.swfCalls = []; this.mediaSource = new videojs.MediaSource(); this.player.src({ src: videojs.URL.createObjectURL(this.mediaSource), type: 'video/mp2t' }); // vjs6 takes 1 tick to set source async this.clock.tick(1); swfObj = document.createElement('fake-object'); swfObj.id = 'fake-swf-' + assert.test.testId; this.player.el().replaceChild(swfObj, this.player.tech_.el()); this.player.tech_.hls = new videojs.EventTarget(); this.player.tech_.el_ = swfObj; swfObj.tech = this.player.tech_; /* eslint-disable camelcase */ swfObj.vjs_abort = () => { this.swfCalls.push('abort'); }; swfObj.vjs_getProperty = (attr) => { if (attr === 'buffered') { return []; } else if (attr === 'currentTime') { return 0; // ignored for vjs6 } else if (attr === 'videoWidth') { return 0; } this.swfCalls.push({ attr }); }; swfObj.vjs_load = () => { this.swfCalls.push('load'); }; swfObj.vjs_setProperty = (attr, value) => { this.swfCalls.push({ attr, value }); }; swfObj.vjs_discontinuity = (attr, value) => { this.swfCalls.push({ attr, value }); }; swfObj.vjs_appendChunkReady = (method) => { window.setTimeout(() => { let chunk = window[method](); // only care about the segment data, not the flv header if (method.substr(0, 21) === 'vjs_flashEncodedData_') { let call = { callee: 'vjs_appendChunkReady', arguments: [window.atob(chunk).split('').map((c) => c.charCodeAt(0))] }; this.swfCalls.push(call); } }, 1); }; swfObj.vjs_adjustCurrentTime = (value) => { this.swfCalls.push({ call: 'adjustCurrentTime', value }); }; /* eslint-enable camelcase */ this.mediaSource.trigger({ type: 'sourceopen', swfId: swfObj.id }); fakeSTO(); }, afterEach() { window.MediaSource = this.oldMediaSource; window.WebKitMediaSource = window.MediaSource; this.Flash.isSupported = this.oldFlashSupport; this.Flash.canPlaySource = this.oldCanPlay; muxjs.flv.Transmuxer.postMessage = this.oldFlashTransmuxerPostMessage; muxjs.flv.getFlvHeader = this.oldGetFlvHeader; this.player.dispose(); this.clock.restore(); this.swfCalls = []; unfakeSTO(); } }); QUnit.test('raises an exception for unrecognized MIME types', function() { try { this.mediaSource.addSourceBuffer('video/garbage'); } catch (e) { QUnit.ok(e, 'an error was thrown'); return; } QUnit.ok(false, 'no error was thrown'); }); QUnit.test('creates FlashSourceBuffers for video/mp2t', function() { QUnit.ok(this.mediaSource.addSourceBuffer('video/mp2t') instanceof FlashSourceBuffer, 'create source buffer'); }); QUnit.test('creates FlashSourceBuffers for audio/mp2t', function() { QUnit.ok(this.mediaSource.addSourceBuffer('audio/mp2t') instanceof FlashSourceBuffer, 'create source buffer'); }); QUnit.test('waits for the next tick to append', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.transmuxer_.postMessage = postMessage_; QUnit.equal(this.swfCalls.length, 1, 'made one call on init'); QUnit.equal(this.swfCalls[0], 'load', 'called load'); sourceBuffer.appendBuffer(new Uint8Array([0, 1])); this.swfCalls = appendCalls(this.swfCalls); QUnit.strictEqual(this.swfCalls.length, 0, 'no appends were made'); }); QUnit.test('passes bytes to Flash', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.transmuxer_.postMessage = postMessage_; this.swfCalls.length = 0; sourceBuffer.appendBuffer(new Uint8Array([0, 1])); timers.runAll(); timers.runAll(); QUnit.ok(this.swfCalls.length, 'the SWF was called'); this.swfCalls = appendCalls(this.swfCalls); QUnit.strictEqual(this.swfCalls[0].callee, 'vjs_appendChunkReady', 'called vjs_appendChunkReady'); QUnit.deepEqual(this.swfCalls[0].arguments[0], [0, 1], 'passed the base64 encoded data'); }); QUnit.test('passes chunked bytes to Flash', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let oldChunkSize = FlashConstants.BYTES_PER_CHUNK; sourceBuffer.transmuxer_.postMessage = postMessage_; FlashConstants.BYTES_PER_CHUNK = 2; this.swfCalls.length = 0; sourceBuffer.appendBuffer(new Uint8Array([0, 1, 2, 3, 4])); timers.runAll(); QUnit.ok(this.swfCalls.length, 'the SWF was called'); this.swfCalls = appendCalls(this.swfCalls); QUnit.equal(this.swfCalls.length, 3, 'the SWF received 3 chunks'); QUnit.strictEqual(this.swfCalls[0].callee, 'vjs_appendChunkReady', 'called vjs_appendChunkReady'); QUnit.deepEqual(this.swfCalls[0].arguments[0], [0, 1], 'passed the base64 encoded data'); QUnit.deepEqual(this.swfCalls[1].arguments[0], [2, 3], 'passed the base64 encoded data'); QUnit.deepEqual(this.swfCalls[2].arguments[0], [4], 'passed the base64 encoded data'); FlashConstants.BYTES_PER_CHUNK = oldChunkSize; }); QUnit.test('clears the SWF on seeking', function() { let aborts = 0; this.mediaSource.addSourceBuffer('video/mp2t'); // track calls to abort() /* eslint-disable camelcase */ this.mediaSource.swfObj.vjs_abort = function() { aborts++; }; /* eslint-enable camelcase */ this.mediaSource.tech_.trigger('seeking'); QUnit.strictEqual(1, aborts, 'aborted pending buffer'); }); QUnit.test('drops tags before currentTime when seeking', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let i = 10; let currentTime; let tags_ = []; sourceBuffer.transmuxer_.postMessage = postMessage_; this.mediaSource.tech_.currentTime = function() { return currentTime; }; // push a tag into the buffer to establish the starting PTS value currentTime = 0; sourceBuffer.transmuxer_.onmessage(createDataMessage([{ pts: 19 * 1000, bytes: new Uint8Array(1) }])); timers.runAll(); sourceBuffer.appendBuffer(new Uint8Array(10)); timers.runAll(); // mock out a new segment of FLV tags, starting 10s after the // starting PTS value while (i--) { tags_.unshift( { pts: (i * 1000) + (29 * 1000), bytes: new Uint8Array([i]) } ); } let dataMessage = createDataMessage(tags_); // mock gop start at seek point dataMessage.data.segment.tags.videoTags[7].keyFrame = true; sourceBuffer.transmuxer_.onmessage(dataMessage); // seek to 7 seconds into the new swegment this.mediaSource.tech_.seeking = function() { return true; }; currentTime = 10 + 7; this.mediaSource.tech_.trigger('seeking'); sourceBuffer.appendBuffer(new Uint8Array(10)); this.swfCalls.length = 0; timers.runAll(); QUnit.deepEqual(this.swfCalls[0].arguments[0], [7, 8, 9], 'three tags are appended'); }); QUnit.test('drops audio and video (complete gops) tags before the buffered end always', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let endTime; let videoTags_ = []; let audioTags_ = []; sourceBuffer.transmuxer_.postMessage = postMessage_; this.mediaSource.tech_.buffered = function() { return videojs.createTimeRange([[0, endTime]]); }; // push a tag into the buffer to establish the starting PTS value endTime = 0; // mock buffering 17 seconds of data so flash source buffer internal end of buffer // tracking is accurate let i = 17; while (i--) { videoTags_.unshift({ pts: (i * 1000) + (19 * 1000), bytes: new Uint8Array(1) }); } i = 17; while (i--) { audioTags_.unshift({ pts: (i * 1000) + (19 * 1000), bytes: new Uint8Array(1) }); } let dataMessage = createDataMessage(videoTags_, audioTags_); sourceBuffer.transmuxer_.onmessage(dataMessage); timers.runAll(); sourceBuffer.appendBuffer(new Uint8Array(10)); timers.runAll(); i = 10; videoTags_ = []; audioTags_ = []; // mock out a new segment of FLV tags, starting 10s after the // starting PTS value while (i--) { videoTags_.unshift({ pts: (i * 1000) + (29 * 1000), bytes: new Uint8Array([i]) }); } i = 10; while (i--) { audioTags_.unshift({ pts: (i * 1000) + (29 * 1000), bytes: new Uint8Array([i + 100]) }); } dataMessage = createDataMessage(videoTags_, audioTags_); dataMessage.data.segment.tags.videoTags[0].keyFrame = true; dataMessage.data.segment.tags.videoTags[3].keyFrame = true; dataMessage.data.segment.tags.videoTags[6].keyFrame = true; dataMessage.data.segment.tags.videoTags[8].keyFrame = true; sourceBuffer.transmuxer_.onmessage(dataMessage); endTime = 10 + 7; sourceBuffer.appendBuffer(new Uint8Array(10)); this.swfCalls.length = 0; timers.runAll(); // end of buffer is 17 seconds // frames 0-6 for video have pts values less than 17 seconds // since frame 6 is a key frame, it should still be appended to preserve the entire gop // so we should have appeneded frames 6 - 9 // frames 100-106 for audio have pts values less than 17 seconds // but since we appended an extra video frame, we should also append audio frames // to fill in the gap in audio. This means we should be appending audio frames // 106, 107, 108, 109 // Append order is 6, 7, 107, 8, 108, 9, 109 since we order tags based on dts value QUnit.deepEqual(this.swfCalls[0].arguments[0], [6, 106, 7, 107, 8, 108, 9, 109], 'audio and video tags properly dropped'); }); QUnit.test('seeking into the middle of a GOP adjusts currentTime to the start of the GOP', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let i = 10; let currentTime; let tags_ = []; sourceBuffer.transmuxer_.postMessage = postMessage_; this.mediaSource.tech_.currentTime = function() { return currentTime; }; // push a tag into the buffer to establish the starting PTS value currentTime = 0; let dataMessage = createDataMessage([{ pts: 19 * 1000, bytes: new Uint8Array(1) }]); sourceBuffer.transmuxer_.onmessage(dataMessage); timers.runAll(); sourceBuffer.appendBuffer(new Uint8Array(10)); timers.runAll(); // mock out a new segment of FLV tags, starting 10s after the // starting PTS value while (i--) { tags_.unshift( { pts: (i * 1000) + (29 * 1000), bytes: new Uint8Array([i]) } ); } dataMessage = createDataMessage(tags_); // mock the GOP structure dataMessage.data.segment.tags.videoTags[0].keyFrame = true; dataMessage.data.segment.tags.videoTags[3].keyFrame = true; dataMessage.data.segment.tags.videoTags[5].keyFrame = true; dataMessage.data.segment.tags.videoTags[8].keyFrame = true; sourceBuffer.transmuxer_.onmessage(dataMessage); // seek to 7 seconds into the new swegment this.mediaSource.tech_.seeking = function() { return true; }; currentTime = 10 + 7; this.mediaSource.tech_.trigger('seeking'); sourceBuffer.appendBuffer(new Uint8Array(10)); this.swfCalls.length = 0; timers.runAll(); QUnit.deepEqual(this.swfCalls[0], { call: 'adjustCurrentTime', value: 15 }); QUnit.deepEqual(this.swfCalls[1].arguments[0], [5, 6, 7, 8, 9], '5 tags are appended'); }); QUnit.test('GOP trimming accounts for metadata tags prepended to key frames by mux.js', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let i = 10; let currentTime; let tags_ = []; sourceBuffer.transmuxer_.postMessage = postMessage_; this.mediaSource.tech_.currentTime = function() { return currentTime; }; // push a tag into the buffer to establish the starting PTS value currentTime = 0; let dataMessage = createDataMessage([{ pts: 19 * 1000, bytes: new Uint8Array(1) }]); sourceBuffer.transmuxer_.onmessage(dataMessage); timers.runAll(); sourceBuffer.appendBuffer(new Uint8Array(10)); timers.runAll(); // mock out a new segment of FLV tags, starting 10s after the // starting PTS value while (i--) { tags_.unshift( { pts: (i * 1000) + (29 * 1000), bytes: new Uint8Array([i]) } ); } // add in the metadata tags tags_.splice(8, 0, { pts: tags_[8].pts, bytes: new Uint8Array([8]) }, { pts: tags_[8].pts, bytes: new Uint8Array([8]) }); tags_.splice(5, 0, { pts: tags_[5].pts, bytes: new Uint8Array([5]) }, { pts: tags_[5].pts, bytes: new Uint8Array([5]) }); tags_.splice(0, 0, { pts: tags_[0].pts, bytes: new Uint8Array([0]) }, { pts: tags_[0].pts, bytes: new Uint8Array([0]) }); dataMessage = createDataMessage(tags_); // mock the GOP structure + metadata tags // if we see a metadata tag, that means the next tag will also be a metadata tag with // keyFrame true and the tag after that will be the keyFrame // e.g. // { keyFrame: false, metaDataTag: true}, // { keyFrame: true, metaDataTag: true}, // { keyFrame: true, metaDataTag: false} dataMessage.data.segment.tags.videoTags[0].metaDataTag = true; dataMessage.data.segment.tags.videoTags[1].metaDataTag = true; dataMessage.data.segment.tags.videoTags[1].keyFrame = true; dataMessage.data.segment.tags.videoTags[2].keyFrame = true; // no metadata tags in front of this key to test the case where mux.js does not prepend // the metadata tags dataMessage.data.segment.tags.videoTags[5].keyFrame = true; dataMessage.data.segment.tags.videoTags[7].metaDataTag = true; dataMessage.data.segment.tags.videoTags[8].metaDataTag = true; dataMessage.data.segment.tags.videoTags[8].keyFrame = true; dataMessage.data.segment.tags.videoTags[9].keyFrame = true; dataMessage.data.segment.tags.videoTags[12].metaDataTag = true; dataMessage.data.segment.tags.videoTags[13].metaDataTag = true; dataMessage.data.segment.tags.videoTags[13].keyFrame = true; dataMessage.data.segment.tags.videoTags[14].keyFrame = true; sourceBuffer.transmuxer_.onmessage(dataMessage); // seek to 7 seconds into the new swegment this.mediaSource.tech_.seeking = function() { return true; }; currentTime = 10 + 7; this.mediaSource.tech_.trigger('seeking'); sourceBuffer.appendBuffer(new Uint8Array(10)); this.swfCalls.length = 0; timers.runAll(); QUnit.deepEqual(this.swfCalls[0], { call: 'adjustCurrentTime', value: 15 }); QUnit.deepEqual(this.swfCalls[1].arguments[0], [5, 5, 5, 6, 7, 8, 8, 8, 9], '10 tags are appended, 4 of which are metadata tags'); }); QUnit.test('drops all tags if target pts append time does not fall within segment', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let i = 10; let currentTime; let tags_ = []; this.mediaSource.tech_.currentTime = function() { return currentTime; }; sourceBuffer.transmuxer_.postMessage = postMessage_; // push a tag into the buffer to establish the starting PTS value currentTime = 0; let dataMessage = createDataMessage([{ pts: 19 * 1000, bytes: new Uint8Array(1) }]); sourceBuffer.transmuxer_.onmessage(dataMessage); timers.runAll(); sourceBuffer.appendBuffer(new Uint8Array(10)); timers.runAll(); // mock out a new segment of FLV tags, starting 10s after the // starting PTS value while (i--) { tags_.unshift( { pts: (i * 1000) + (19 * 1000), bytes: new Uint8Array([i]) } ); } dataMessage = createDataMessage(tags_); // mock the GOP structure dataMessage.data.segment.tags.videoTags[0].keyFrame = true; dataMessage.data.segment.tags.videoTags[3].keyFrame = true; dataMessage.data.segment.tags.videoTags[5].keyFrame = true; dataMessage.data.segment.tags.videoTags[8].keyFrame = true; sourceBuffer.transmuxer_.onmessage(dataMessage); // seek to 7 seconds into the new swegment this.mediaSource.tech_.seeking = function() { return true; }; currentTime = 10 + 7; this.mediaSource.tech_.trigger('seeking'); sourceBuffer.appendBuffer(new Uint8Array(10)); this.swfCalls.length = 0; timers.runAll(); QUnit.equal(this.swfCalls.length, 0, 'dropped all tags and made no swf calls'); }); QUnit.test('seek targeting accounts for changing timestampOffsets', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let i = 10; let tags_ = []; let currentTime; this.mediaSource.tech_.currentTime = function() { return currentTime; }; sourceBuffer.transmuxer_.postMessage = postMessage_; let dataMessage = createDataMessage([{ pts: 19 * 1000, bytes: new Uint8Array(1) }]); // push a tag into the buffer to establish the starting PTS value currentTime = 0; sourceBuffer.transmuxer_.onmessage(dataMessage); timers.runAll(); // to seek across a discontinuity: // 1. set the timestamp offset to the media timeline position for // the start of the segment // 2. set currentTime to the desired media timeline position sourceBuffer.timestampOffset = 22; currentTime = sourceBuffer.timestampOffset + 3.5; this.mediaSource.tech_.seeking = function() { return true; }; // the new segment FLV tags are at disjoint PTS positions while (i--) { tags_.unshift({ // (101 * 1000) !== the old PTS offset pts: (i * 1000) + (101 * 1000), bytes: new Uint8Array([i + sourceBuffer.timestampOffset]) }); } dataMessage = createDataMessage(tags_); // mock gop start at seek point dataMessage.data.segment.tags.videoTags[3].keyFrame = true; sourceBuffer.transmuxer_.onmessage(dataMessage); this.mediaSource.tech_.trigger('seeking'); this.swfCalls.length = 0; timers.runAll(); QUnit.equal(this.swfCalls[0].value, 25, 'adjusted current time'); QUnit.deepEqual(this.swfCalls[1].arguments[0], [25, 26, 27, 28, 29, 30, 31], 'filtered the appended tags'); }); QUnit.test('calling endOfStream sets mediaSource readyState to ended', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.transmuxer_.postMessage = postMessage_; /* eslint-disable camelcase */ this.mediaSource.swfObj.vjs_endOfStream = () => { this.swfCalls.push('endOfStream'); }; /* eslint-enable camelcase */ sourceBuffer.addEventListener('updateend', () => { this.mediaSource.endOfStream(); }); this.swfCalls.length = 0; sourceBuffer.appendBuffer(new Uint8Array([0, 1])); timers.runAll(); QUnit.strictEqual(sourceBuffer.mediaSource_.readyState, 'ended', 'readyState is \'ended\''); QUnit.strictEqual(this.swfCalls.length, 2, 'made two calls to swf'); QUnit.deepEqual(this.swfCalls.shift().arguments[0], [0, 1], 'contains the data'); QUnit.ok(this.swfCalls.shift().indexOf('endOfStream') === 0, 'the second call should be for the updateend'); QUnit.strictEqual(timers.length, 0, 'no more appends are scheduled'); }); QUnit.test('opens the stream on sourceBuffer.appendBuffer after endOfStream', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let foo = () => { this.mediaSource.endOfStream(); sourceBuffer.removeEventListener('updateend', foo); }; sourceBuffer.transmuxer_.postMessage = postMessage_; /* eslint-disable camelcase */ this.mediaSource.swfObj.vjs_endOfStream = () => { this.swfCalls.push('endOfStream'); }; /* eslint-enable camelcase */ sourceBuffer.addEventListener('updateend', foo); this.swfCalls.length = 0; sourceBuffer.appendBuffer(new Uint8Array([0, 1])); timers.runAll(); QUnit.strictEqual(this.swfCalls.length, 2, 'made two calls to swf'); QUnit.deepEqual(this.swfCalls.shift().arguments[0], [0, 1], 'contains the data'); QUnit.equal(this.swfCalls.shift(), 'endOfStream', 'the second call should be for the updateend'); sourceBuffer.appendBuffer(new Uint8Array([2, 3])); // remove previous video pts save because mock appends don't have actual timing data sourceBuffer.videoBufferEnd_ = NaN; timers.runAll(); QUnit.strictEqual(this.swfCalls.length, 1, 'made one more append'); QUnit.deepEqual(this.swfCalls.shift().arguments[0], [2, 3], 'contains the third and fourth bytes'); QUnit.strictEqual( sourceBuffer.mediaSource_.readyState, 'open', 'The streams should be open if more bytes are appended to an "ended" stream' ); QUnit.strictEqual(timers.length, 0, 'no more appends are scheduled'); }); QUnit.test('abort() clears any buffered input', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.transmuxer_.postMessage = postMessage_; this.swfCalls.length = 0; sourceBuffer.appendBuffer(new Uint8Array([0])); sourceBuffer.abort(); timers.pop()(); QUnit.strictEqual(this.swfCalls.length, 1, 'called the swf'); QUnit.strictEqual(this.swfCalls[0], 'abort', 'invoked abort'); }); // requestAnimationFrame is heavily throttled or unscheduled when // the browser tab running contrib-media-sources is in a background // tab. If that happens, video data can continuously build up in // memory and cause the tab or browser to crash. QUnit.test('does not use requestAnimationFrame', function() { let oldRFA = window.requestAnimationFrame; let requests = 0; let sourceBuffer; window.requestAnimationFrame = function() { requests++; }; sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.transmuxer_.postMessage = postMessage_; sourceBuffer.appendBuffer(new Uint8Array([0, 1, 2, 3])); while (timers.length) { timers.pop()(); } QUnit.equal(requests, 0, 'no calls to requestAnimationFrame were made'); window.requestAnimationFrame = oldRFA; }); QUnit.test('updating is true while an append is in progress', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let ended = false; sourceBuffer.transmuxer_.postMessage = postMessage_; sourceBuffer.addEventListener('updateend', function() { ended = true; }); sourceBuffer.appendBuffer(new Uint8Array([0, 1])); QUnit.equal(sourceBuffer.updating, true, 'updating is set'); while (!ended) { timers.pop()(); } QUnit.equal(sourceBuffer.updating, false, 'updating is unset'); }); QUnit.test('throws an error if append is called while updating', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.appendBuffer(new Uint8Array([0, 1])); sourceBuffer.transmuxer_.postMessage = postMessage_; QUnit.throws(function() { sourceBuffer.appendBuffer(new Uint8Array([0, 1])); }, function(e) { return e.name === 'InvalidStateError' && e.code === window.DOMException.INVALID_STATE_ERR; }, 'threw an InvalidStateError'); }); QUnit.test('stops updating if abort is called', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let updateEnds = 0; sourceBuffer.transmuxer_.postMessage = postMessage_; sourceBuffer.addEventListener('updateend', function() { updateEnds++; }); sourceBuffer.appendBuffer(new Uint8Array([0, 1])); sourceBuffer.abort(); QUnit.equal(sourceBuffer.updating, false, 'no longer updating'); QUnit.equal(updateEnds, 1, 'triggered updateend'); }); QUnit.test('forwards duration overrides to the SWF', function() { /* eslint-disable no-unused-vars */ let ignored = this.mediaSource.duration; /* eslint-enable no-unused-vars */ QUnit.deepEqual(this.swfCalls[1], { attr: 'duration' }, 'requests duration from the SWF'); this.mediaSource.duration = 101.3; // Setting a duration results in two calls to the swf // Ignore the first call (this.swfCalls[2]) as it was just to get the // current duration QUnit.deepEqual(this.swfCalls[3], { attr: 'duration', value: 101.3 }, 'set the duration override'); }); QUnit.test('returns NaN for duration before the SWF is ready', function() { this.mediaSource.swfObj = null; QUnit.ok(isNaN(this.mediaSource.duration), 'duration is NaN'); }); QUnit.test('calculates the base PTS for the media', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let tags_ = []; sourceBuffer.transmuxer_.postMessage = postMessage_; // seek to 15 seconds this.player.tech_.seeking = function() { return true; }; this.player.tech_.currentTime = function() { return 15; }; // FLV tags for this segment start at 10 seconds in the media // timeline tags_.push( // zero in the media timeline is PTS 3 { pts: (10 + 3) * 1000, bytes: new Uint8Array([10]) }, { pts: (15 + 3) * 1000, bytes: new Uint8Array([15]) } ); let dataMessage = createDataMessage(tags_); // mock gop start at seek point dataMessage.data.segment.tags.videoTags[1].keyFrame = true; sourceBuffer.transmuxer_.onmessage(dataMessage); // let the source buffer know the segment start time sourceBuffer.timestampOffset = 10; this.swfCalls.length = 0; timers.runAll(); QUnit.equal(this.swfCalls.length, 1, 'made a SWF call'); QUnit.deepEqual(this.swfCalls[0].arguments[0], [15], 'dropped the early tag'); }); QUnit.test('remove fires update events', function() { let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); let events = []; sourceBuffer.transmuxer_.postMessage = postMessage_; sourceBuffer.on(['update', 'updateend'], function(event) { events.push(event.type); }); sourceBuffer.remove(0, 1); QUnit.deepEqual(events, ['update', 'updateend'], 'fired update events'); QUnit.equal(sourceBuffer.updating, false, 'finished updating'); }); QUnit.test('passes endOfStream network errors to the tech', function() { this.mediaSource.readyState = 'ended'; this.mediaSource.endOfStream('network'); QUnit.equal(this.player.tech_.error().code, 2, 'set a network error'); }); QUnit.test('passes endOfStream decode errors to the tech', function() { this.mediaSource.readyState = 'ended'; this.mediaSource.endOfStream('decode'); QUnit.equal(this.player.tech_.error().code, 3, 'set a decode error'); }); QUnit.test('has addSeekableRange()', function() { QUnit.ok(this.mediaSource.addSeekableRange_, 'has addSeekableRange_'); }); QUnit.test('fires loadedmetadata after first segment append', function() { let loadedmetadataCount = 0; this.mediaSource.tech_.on('loadedmetadata', () => loadedmetadataCount++); let sourceBuffer = this.mediaSource.addSourceBuffer('video/mp2t'); sourceBuffer.transmuxer_.postMessage = postMessage_; QUnit.equal(loadedmetadataCount, 0, 'loadedmetadata not called on buffer creation'); sourceBuffer.appendBuffer(new Uint8Array([0, 1])); QUnit.equal(loadedmetadataCount, 0, 'loadedmetadata not called on segment append'); timers.runAll(); QUnit.equal(loadedmetadataCount, 1, 'loadedmetadata fires after first append'); sourceBuffer.appendBuffer(new Uint8Array([0, 1])); timers.runAll(); QUnit.equal(loadedmetadataCount, 1, 'loadedmetadata does not fire after second append'); });