1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036 |
- 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');
- });
|