diff --git a/src/js/player.js b/src/js/player.js index 75bd217cf2..4bde35f1e8 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -25,6 +25,7 @@ import mergeOptions from './utils/merge-options.js'; import textTrackConverter from './tracks/text-track-list-converter.js'; import ModalDialog from './modal-dialog'; import Tech from './tech/tech.js'; +import * as middleware from './tech/middleware.js'; import {ALL as TRACK_TYPES} from './tracks/track-types'; // The following imports are used only to ensure that the corresponding modules @@ -369,6 +370,8 @@ class Player extends Component { this.options_.playerOptions = playerOptionsCopy; + this.middleware_ = []; + this.initChildren(); // Set isAudio based on whether or not an audio tag was used @@ -420,6 +423,8 @@ class Player extends Component { this.on('fullscreenchange', this.handleFullscreenChange_); this.on('stageclick', this.handleStageClick_); + + this.changingSrc_ = false; } /** @@ -834,16 +839,8 @@ class Player extends Component { techOptions.tag = this.tag; } - if (source) { - this.currentType_ = source.type; - - if (source.src === this.cache_.src && this.cache_.currentTime > 0) { - techOptions.startTime = this.cache_.currentTime; - } - - this.cache_.sources = null; - this.cache_.source = source; - this.cache_.src = source.src; + if (source && source.src === this.cache_.src && this.cache_.currentTime > 0) { + techOptions.startTime = this.cache_.currentTime; } // Initialize tech instance @@ -1508,13 +1505,12 @@ class Player extends Component { */ techCall_(method, arg) { // If it's not ready yet, call method when it is - if (this.tech_ && !this.tech_.isReady_) { - this.tech_.ready(function() { - this[method](arg); - }, true); - // Otherwise call method now - } else { + this.ready(function() { + if (method in middleware.allowedSetters) { + return middleware.set(this.middleware_, this.tech_, method, arg); + } + try { if (this.tech_) { this.tech_[method](arg); @@ -1523,7 +1519,7 @@ class Player extends Component { log(e); throw e; } - } + }); } /** @@ -1540,6 +1536,10 @@ class Player extends Component { techGet_(method) { if (this.tech_ && this.tech_.isReady_) { + if (method in middleware.allowedGetters) { + return middleware.get(this.middleware_, this.tech_, method); + } + // Flash likes to die and reload when you hide or reposition it. // In these cases the object methods go away and we get errors. // When that happens we'll catch the errors and inform tech that it's not ready any more. @@ -1572,21 +1572,26 @@ class Player extends Component { * return undefined. */ play() { + if (this.changingSrc_) { + this.ready(function() { + this.techCall_('play'); + }); + // Only calls the tech's play if we already have a src loaded - if (this.src() || this.currentSrc()) { + } else if (this.src() || this.currentSrc()) { return this.techGet_('play'); - } - - this.ready(function() { - this.tech_.one('loadstart', function() { - const retval = this.play(); + } else { + this.ready(function() { + this.tech_.one('loadstart', function() { + const retval = this.play(); - // silence errors (unhandled promise from play) - if (retval !== undefined && typeof retval.then === 'function') { - retval.then(null, (e) => {}); - } + // silence errors (unhandled promise from play) + if (retval !== undefined && typeof retval.then === 'function') { + retval.then(null, (e) => {}); + } + }); }); - }); + } } /** @@ -2181,66 +2186,96 @@ class Player extends Component { */ src(source) { if (source === undefined) { - return this.techGet_('src'); + return this.cache_.src; } - let currentTech = Tech.getTech(this.techName_); + this.changingSrc_ = true; - // Support old behavior of techs being registered as components. - // Remove once that deprecated behavior is removed. - if (!currentTech) { - currentTech = Component.getComponent(this.techName_); - } + let src = source; - // case: Array of source objects to choose from and pick the best to play if (Array.isArray(source)) { - this.sourceList_(source); - - // case: URL String (http://myvideo...) + this.cache_.sources = source; + src = source[0]; } else if (typeof source === 'string') { - // create a source object from the string - this.src({ src: source }); - - // case: Source object { src: '', type: '' ... } - } else if (source instanceof Object) { - // check if the source has a type and the loaded tech cannot play the source - // if there's no type we'll just try the current tech - if (source.type && !currentTech.canPlaySource(source, this.options_[this.techName_.toLowerCase()])) { - // create a source list with the current source and send through - // the tech loop to check for a compatible technology - this.sourceList_([source]); - } else { - this.cache_.sources = null; - this.cache_.source = source; - this.cache_.src = source.src; - - this.currentType_ = source.type || ''; - - // wait until the tech is ready to set the source - this.ready(function() { - - // The setSource tech method was added with source handlers - // so older techs won't support it - // We need to check the direct prototype for the case where subclasses - // of the tech do not support source handlers - if (currentTech.prototype.hasOwnProperty('setSource')) { - this.techCall_('setSource', source); - } else { - this.techCall_('src', source.src); - } + src = { + src: source + }; - if (this.options_.preload === 'auto') { - this.load(); - } + this.cache_.sources = [src]; + } - if (this.options_.autoplay) { - this.play(); - } + this.cache_.source = src; + + this.currentType_ = src.type; + + middleware.setSource(Fn.bind(this, this.setTimeout), src, (src_, mws) => { + this.middleware_ = mws; + + const err = this.src_(src_); + + if (err) { + if (Array.isArray(source) && source.length > 1) { + return this.src(source.slice(1)); + } - // Set the source synchronously if possible (#2326) - }, true); + // We need to wrap this in a timeout to give folks a chance to add error event handlers + this.setTimeout(function() { + this.error({ code: 4, message: this.localize(this.options_.notSupportedMessage) }); + }, 0); + + // we could not find an appropriate tech, but let's still notify the delegate that this is it + // this needs a better comment about why this is needed + this.triggerReady(); + + return; } + + this.changingSrc_ = false; + this.cache_.src = src_.src; + middleware.setTech(mws, this.tech_); + }); + } + + src_(source) { + const sourceTech = this.selectSource([source]); + + if (!sourceTech) { + return true; } + + if (sourceTech.tech !== this.techName_) { + this.changingSrc_ = true; + + // load this technology with the chosen source + this.loadTech_(sourceTech.tech, sourceTech.source); + return false; + } + + // wait until the tech is ready to set the source + this.ready(function() { + + // The setSource tech method was added with source handlers + // so older techs won't support it + // We need to check the direct prototype for the case where subclasses + // of the tech do not support source handlers + if (this.tech_.constructor.prototype.hasOwnProperty('setSource')) { + this.techCall_('setSource', source); + } else { + this.techCall_('src', source.src); + } + + if (this.options_.preload === 'auto') { + this.load(); + } + + if (this.options_.autoplay) { + this.play(); + } + + // Set the source synchronously if possible (#2326) + }, true); + + return false; } /** @@ -2335,7 +2370,7 @@ class Player extends Component { * The current source */ currentSrc() { - return this.techGet_('currentSrc') || this.cache_.src || ''; + return this.cache_.source && this.cache_.source.src || ''; } /** diff --git a/src/js/tech/html5.js b/src/js/tech/html5.js index a70600a5da..0163637c28 100644 --- a/src/js/tech/html5.js +++ b/src/js/tech/html5.js @@ -719,6 +719,29 @@ Html5.isSupported = function() { return !!(Html5.TEST_VID && Html5.TEST_VID.canPlayType); }; +/** + * Check if the tech can support the given type + * + * @param {string} type + * The mimetype to check + * @return {string} 'probably', 'maybe', or '' (empty string) + */ +Html5.canPlayType = function(type) { + return Html5.TEST_VID.canPlayType(type); +}; + +/** + * Check if the tech can support the given source + * @param {Object} srcObj + * The source object + * @param {Object} options + * The options passed to the tech + * @return {string} 'probably', 'maybe', or '' (empty string) + */ +Html5.canPlaySource = function(srcObj, options) { + return Html5.canPlayType(srcObj.type); +}; + /** * Check if the volume can be changed in this browser/device. * Volume cannot be changed in a lot of mobile devices. diff --git a/src/js/tech/middleware.js b/src/js/tech/middleware.js new file mode 100644 index 0000000000..a1f9c73d38 --- /dev/null +++ b/src/js/tech/middleware.js @@ -0,0 +1,85 @@ +import { assign } from '../utils/obj.js'; + +const middlewares = {}; + +export function use(type, middleware) { + middlewares[type] = middlewares[type] || []; + middlewares[type].push(middleware); +} + +export function getMiddleware(type) { + if (type) { + return middlewares[type]; + } + + return middlewares; +} + +export function setSource(setTimeout, src, next) { + setTimeout(() => setSourceHelper(src, middlewares[src.type], next), 1); +} + +export function setTech(middleware, tech) { + middleware.forEach((mw) => mw.setTech && mw.setTech(tech)); +} + +export function get(middleware, tech, method) { + return middleware.reduceRight(middlewareIterator(method), tech[method]()); +} + +export function set(middleware, tech, method, arg) { + return tech[method](middleware.reduce(middlewareIterator(method), arg)); +} + +export const allowedGetters = { + currentTime: 1, + duration: 1 +}; + +export const allowedSetters = { + setCurrentTime: 1 +}; + +function middlewareIterator(method) { + return (value, mw) => { + if (mw[method]) { + return mw[method](value); + } + + return value; + }; +} + +function setSourceHelper(src = {}, middleware = [], next, acc = []) { + const [mw, ...mwrest] = middleware; + + // if mw is a string, then we're at a fork in the road + if (typeof mw === 'string') { + setSourceHelper(src, middlewares[mw], next, acc); + + // if we have an mw, call its setSource method + } else if (mw) { + mw.setSource(assign({}, src), function(err, _src) { + + // something happened, try the next middleware on the current level + // make sure to use the old src + if (err) { + return setSourceHelper(src, mwrest, next, acc); + } + + // we've succeeded, now we need to go deeper + acc.push(mw); + + // if it's the same time, continue does the current chain + // otherwise, we want to go down the new chain + setSourceHelper(_src, + src.type === _src.type ? mwrest : middlewares[_src.type], + next, + acc); + }); + } else if (mwrest.length) { + setSourceHelper(src, mwrest, next, acc); + } else { + next(src, acc); + } +} diff --git a/src/js/tech/tech.js b/src/js/tech/tech.js index 618249f8ef..1384b0a2ad 100644 --- a/src/js/tech/tech.js +++ b/src/js/tech/tech.js @@ -11,6 +11,7 @@ import { bufferedPercent } from '../utils/buffer.js'; import MediaError from '../media-error.js'; import window from 'global/window'; import document from 'global/document'; +import * as middleware from './middleware.js'; import {isPlain} from '../utils/obj'; import * as TRACK_TYPES from '../tracks/track-types'; @@ -729,6 +730,32 @@ class Tech extends Component { return ''; } + /** + * Check if the type is supported by this tech. + * + * The base tech does not support any type, but source handlers might + * overwrite this. + * + * @param {string} type + * The media type to check + * @return {string} Returns the native video element's response + */ + static canPlayType() { + return ''; + } + + /** + * Check if the tech can support the given source + * @param {Object} srcObj + * The source object + * @param {Object} options + * The options passed to the tech + * @return {string} 'probably', 'maybe', or '' (empty string) + */ + static canPlaySource(srcObj, options) { + return Tech.canPlayType(srcObj.type); + } + /* * Return whether the argument is a Tech or not. * Can be passed either a Class like `Html5` or a instance like `player.tech_` @@ -765,6 +792,15 @@ class Tech extends Component { throw new Error(`Tech ${name} must be a Tech`); } + if (!Tech.canPlayType) { + throw new Error('Techs must have a static canPlayType method on them'); + } + if (!Tech.canPlaySource) { + throw new Error('Techs must have a static canPlaySource method on them'); + } + + middleware.use('*', {name, tech}); + Tech.techs_[name] = tech; return tech; } diff --git a/src/js/video.js b/src/js/video.js index 9fb3d25d00..db914035c1 100644 --- a/src/js/video.js +++ b/src/js/video.js @@ -30,6 +30,7 @@ import xhr from 'xhr'; // Include the built-in techs import Tech from './tech/tech.js'; +import { use as middlewareUse } from './tech/middleware.js'; // HTML5 Element Shim for IE8 if (typeof HTMLVideoElement === 'undefined' && Dom.isReal()) { @@ -298,6 +299,8 @@ videojs.getTech = Tech.getTech; */ videojs.registerTech = Tech.registerTech; +videojs.use = middlewareUse; + /** * A suite of browser and device tests from {@link browser}. * diff --git a/test/unit/player.test.js b/test/unit/player.test.js index 6f900b2dbc..4b21e5181d 100644 --- a/test/unit/player.test.js +++ b/test/unit/player.test.js @@ -7,12 +7,12 @@ import * as browser from '../../src/js/utils/browser.js'; import log from '../../src/js/utils/log.js'; import MediaError from '../../src/js/media-error.js'; import Html5 from '../../src/js/tech/html5.js'; +import Tech from '../../src/js/tech/tech.js'; import TestHelpers from './test-helpers.js'; import document from 'global/document'; import sinon from 'sinon'; import window from 'global/window'; -import Tech from '../../src/js/tech/tech.js'; -import TechFaker from './tech/tech-faker.js'; +import * as middleware from '../../src/js/tech/middleware.js'; QUnit.module('Player', { beforeEach() { @@ -283,6 +283,9 @@ QUnit.test('should asynchronously fire error events during source selection', fu assert.ok(player.error().code === 4, 'Source could not be played error thrown'); }); + // The first one is for player initialization + // The second one is the setTimeout for triggering the error + this.clock.tick(1); this.clock.tick(1); player.dispose(); @@ -597,49 +600,6 @@ QUnit.test('make sure that controls listeners do not get added too many times', player.dispose(); }); -QUnit.test('should select the proper tech based on the the sourceOrder option', function(assert) { - const fixture = document.getElementById('qunit-fixture'); - const html = - ''; - - // Extend TechFaker to create a tech that plays the only mime-type that TechFaker - // will not play - class PlaysUnsupported extends TechFaker { - constructor(options, handleReady) { - super(options, handleReady); - } - // Support ONLY "video/unsupported-format" - static isSupported() { - return true; - } - static canPlayType(type) { - return (type === 'video/unsupported-format' ? 'maybe' : ''); - } - static canPlaySource(srcObj) { - return srcObj.type === 'video/unsupported-format'; - } - } - Tech.registerTech('PlaysUnsupported', PlaysUnsupported); - - fixture.innerHTML += html; - let tag = document.getElementById('example_1'); - - let player = new Player(tag, { techOrder: ['techFaker', 'playsUnsupported'], sourceOrder: true }); - - assert.equal(player.techName_, 'PlaysUnsupported', 'selected the PlaysUnsupported tech when sourceOrder is truthy'); - player.dispose(); - - fixture.innerHTML += html; - tag = document.getElementById('example_1'); - - player = new Player(tag, { techOrder: ['techFaker', 'playsUnsupported']}); - assert.equal(player.techName_, 'TechFaker', 'selected the TechFaker tech when sourceOrder is falsey'); - player.dispose(); -}); - QUnit.test('should register players with generated ids', function(assert) { const fixture = document.getElementById('qunit-fixture'); @@ -974,11 +934,17 @@ QUnit.test('should clear pending errors on disposal', function(assert) { const player = TestHelpers.makePlayer(); + clock.tick(1); + player.src({ src: 'http://example.com/movie.unsupported-format', type: 'video/unsupported-format' }); + + clock.tick(1); + player.dispose(); + try { clock.tick(5000); } catch (e) { @@ -1072,6 +1038,13 @@ if (window.Promise) { const player = TestHelpers.makePlayer({}); const done = assert.async(); + player.src({ + src: 'http://example.com/video.mp4', + type: 'video/mp4' + }); + + this.clock.tick(1); + player.tech_.play = () => window.Promise.resolve('foo'); const p = player.play(); @@ -1089,6 +1062,13 @@ if (window.Promise) { QUnit.test('play promise should resolve to native value if returned', function(assert) { const player = TestHelpers.makePlayer({}); + player.src({ + src: 'http://example.com/video.mp4', + type: 'video/mp4' + }); + + this.clock.tick(1); + player.tech_.play = () => 'foo'; const p = player.play(); @@ -1426,6 +1406,140 @@ QUnit.test('should not allow to register custom player when any player has been videojs.registerComponent('Player', Player); }); +QUnit.test('techGet runs through middleware if allowedGetter', function(assert) { + let cts = 0; + let durs = 0; + let ps = 0; + + videojs.use('video/foo', { + currentTime() { + cts++; + }, + duration() { + durs++; + }, + paused() { + ps++; + } + }); + + const tag = TestHelpers.makeTag(); + const player = videojs(tag, { + techOrder: ['techFaker'] + }); + + player.middleware_ = middleware.getMiddleware('video/foo'); + + player.techGet_('currentTime'); + player.techGet_('duration'); + player.techGet_('paused'); + + assert.equal(cts, 1, 'currentTime is allowed'); + assert.equal(durs, 1, 'duration is allowed'); + assert.equal(ps, 0, 'paused is not allowed'); + + middleware.getMiddleware('video/foo').pop(); + player.dispose(); +}); + +QUnit.test('techCall runs through middleware if allowedSetter', function(assert) { + let cts = 0; + let vols = 0; + + videojs.use('video/foo', { + setCurrentTime(ct) { + cts++; + return ct; + }, + setVolume() { + vols++; + } + }); + + const tag = TestHelpers.makeTag(); + const player = videojs(tag, { + techOrder: ['techFaker'] + }); + + player.middleware_ = middleware.getMiddleware('video/foo'); + + this.clock.tick(1); + + player.techCall_('setCurrentTime', 10); + player.techCall_('setVolume', 0.5); + + this.clock.tick(1); + + assert.equal(cts, 1, 'setCurrentTime is allowed'); + assert.equal(vols, 0, 'setVolume is not allowed'); + + middleware.getMiddleware('video/foo').pop(); + player.dispose(); +}); + +QUnit.test('src selects tech based on middleware', function(assert) { + class FooTech extends Html5 {} + class BarTech extends Html5 {} + + FooTech.isSupported = () => true; + FooTech.canPlayType = (type) => type === 'video/mp4'; + FooTech.canPlaySource = (src) => FooTech.canPlayType(src.type); + + BarTech.isSupported = () => true; + BarTech.canPlayType = (type) => type === 'video/flv'; + BarTech.canPlaySource = (src) => BarTech.canPlayType(src.type); + + videojs.registerTech('FooTech', FooTech); + videojs.registerTech('BarTech', BarTech); + + videojs.use('video/foo', { + setSource(src, next) { + next(null, { + src: 'http://example.com/video.mp4', + type: 'video/mp4' + }); + } + }); + + videojs.use('video/bar', { + setSource(src, next) { + next(null, { + src: 'http://example.com/video.flv', + type: 'video/flv' + }); + } + }); + + const tag = TestHelpers.makeTag(); + const player = videojs(tag, { + techOrder: ['fooTech', 'barTech'] + }); + + player.src({ + src: 'foo', + type: 'video/foo' + }); + + this.clock.tick(1); + + assert.equal(player.techName_, 'FooTech', 'the FooTech (html5) tech is chosen'); + + player.src({ + src: 'bar', + type: 'video/bar' + }); + + this.clock.tick(1); + + assert.equal(player.techName_, 'BarTech', 'the BarTech (Flash) tech is chosen'); + + middleware.getMiddleware('video/foo').pop(); + middleware.getMiddleware('video/bar').pop(); + player.dispose(); + delete Tech.techs_.FooTech; + delete Tech.techs_.BarTech; +}); + QUnit.test('options: plugins', function(assert) { const optionsSpy = sinon.spy(); diff --git a/test/unit/tech/middleware.test.js b/test/unit/tech/middleware.test.js new file mode 100644 index 0000000000..47c0c0a37f --- /dev/null +++ b/test/unit/tech/middleware.test.js @@ -0,0 +1,280 @@ +/* eslint-env qunit */ +import * as middleware from '../../../src/js/tech/middleware.js'; +import sinon from 'sinon'; +import window from 'global/window'; + +QUnit.module('Middleware', { + beforeEach(assert) { + this.clock = sinon.useFakeTimers(); + }, + afterEach(assert) { + this.clock.restore(); + } +}); + +QUnit.test('middleware can be added with the use method', function(assert) { + const myMw = {}; + + middleware.use('foo', myMw); + + assert.equal(middleware.getMiddleware('foo').pop(), myMw, 'we are able to add middleware'); +}); + +QUnit.test('middleware get iterates through the middleware array the right order', function(assert) { + const cts = []; + const durs = []; + const foos = []; + const mws = [ + { + currentTime(ct) { + cts.push(ct); + return ct * 2; + }, + duration(dur) { + durs.push(dur); + return dur + 2; + }, + foo(f) { + foos.push(f); + return f / 2; + } + }, + { + currentTime(ct) { + cts.push(ct); + return ct + 2; + }, + duration(dur) { + durs.push(dur); + return dur / 2; + }, + foo(f) { + foos.push(f); + return f + 3; + } + } + ]; + const tech = { + currentTime(ct) { + return 5; + }, + duration(dur) { + return 5; + }, + foo(f) { + return 5; + } + }; + + const ct = middleware.get(mws, tech, 'currentTime'); + const dur = middleware.get(mws, tech, 'duration'); + const foo = middleware.get(mws, tech, 'foo'); + + const assertion = (actual, expected, actualArr, expectedArr, type) => { + assert.equal(actual, expected, `our middleware chain return currectly for ${type}`); + assert.deepEqual(actualArr, expectedArr, `we got called in the correct order for ${type}`); + }; + + assertion(ct, 14, cts, [5, 7], 'currentTime'); + assertion(dur, 4.5, durs, [5, 2.5], 'duration'); + assertion(foo, 4, foos, [5, 8], 'foo'); +}); + +QUnit.test('middleware set iterates through the middleware array the right order', function(assert) { + const cts = []; + const durs = []; + const foos = []; + const mws = [ + { + currentTime(ct) { + cts.push(ct); + return ct * 2; + }, + duration(dur) { + durs.push(dur); + return dur + 2; + }, + foo(f) { + foos.push(f); + return f / 2; + } + }, + { + currentTime(ct) { + cts.push(ct); + return ct + 2; + }, + duration(dur) { + durs.push(dur); + return dur / 2; + }, + foo(f) { + foos.push(f); + return f + 3; + } + } + ]; + const tech = { + currentTime(ct) { + cts.push(ct); + return ct / 2; + }, + duration(dur) { + durs.push(dur); + return dur; + }, + foo(f) { + foos.push(f); + return f; + } + }; + + const ct = middleware.set(mws, tech, 'currentTime', 10); + const dur = middleware.set(mws, tech, 'duration', 10); + const foo = middleware.set(mws, tech, 'foo', 10); + + const assertion = (actual, expected, actualArr, expectedArr, type) => { + assert.equal(actual, expected, `our middleware chain return currectly for ${type}`); + assert.deepEqual(actualArr, expectedArr, `we got called in the correct order for ${type}`); + }; + + assertion(ct, 11, cts, [10, 20, 22], 'currentTime'); + assertion(dur, 6, durs, [10, 12, 6], 'duration'); + assertion(foo, 8, foos, [10, 5, 8], 'foo'); +}); + +QUnit.test('setSource is run asynchronously', function(assert) { + let src; + let acc; + + middleware.setSource(window.setTimeout, { src: 'foo', type: 'video/foo' }, function(_src, _acc) { + src = _src; + acc = _acc; + }); + + assert.equal(src, undefined, 'no src was returned yet'); + assert.equal(acc, undefined, 'no accumulator was returned yet'); + + this.clock.tick(1); + + assert.deepEqual(src, {src: 'foo', type: 'video/foo'}, 'we got the same source back'); + assert.equal(acc.length, 0, 'we did not accumulate any middleware since there were none'); +}); + +QUnit.test('setSource selects a source based on the middleware given', function(assert) { + let src; + let acc; + const mw = { + setSource(_src, next) { + next(null, { + src: 'http://example.com/video.mp4', + type: 'video/mp4' + }); + } + }; + + middleware.use('video/foo', mw); + + middleware.setSource(window.setTimeout, {src: 'foo', type: 'video/foo'}, function(_src, _acc) { + src = _src; + acc = _acc; + }); + + this.clock.tick(1); + + assert.equal(src.type, 'video/mp4', 'we selected a new type of video/mp4'); + assert.equal(src.src, 'http://example.com/video.mp4', 'we selected a new src of video.mp4'); + assert.equal(acc.length, 1, 'we got one middleware'); + assert.equal(acc[0], mw, 'we chose the one middleware'); + + middleware.getMiddleware('video/foo').pop(); +}); + +QUnit.test('setSource can select multiple middleware from multiple types', function(assert) { + let src; + let acc; + const foomw = { + setSource(_src, next) { + next(null, { + src: 'bar', + type: 'video/bar' + }); + } + }; + const barmw = { + setSource(_src, next) { + next(null, { + src: 'http://example.com/video.mp4', + type: 'video/mp4' + }); + } + }; + + middleware.use('video/foo', foomw); + middleware.use('video/bar', barmw); + + middleware.setSource(window.setTimeout, {src: 'foo', type: 'video/foo'}, function(_src, _acc) { + src = _src; + acc = _acc; + }); + + this.clock.tick(1); + + assert.equal(src.type, 'video/mp4', 'we selected a new type of video/mp4'); + assert.equal(src.src, 'http://example.com/video.mp4', 'we selected a new src of video.mp4'); + assert.equal(acc.length, 2, 'we got two middleware'); + assert.equal(acc[0], foomw, 'foomw is the first middleware'); + assert.equal(acc[1], barmw, 'barmw is the first middleware'); + + middleware.getMiddleware('video/foo').pop(); + middleware.getMiddleware('video/bar').pop(); +}); + +QUnit.test('setSource will select all middleware of a given type, until src change', function(assert) { + let src; + let acc; + const foomw1 = { + setSource(_src, next) { + next(null, { + src: 'bar', + type: 'video/foo' + }); + } + }; + const foomw2 = { + setSource(_src, next) { + next(null, { + src: 'http://example.com/video.mp4', + type: 'video/mp4' + }); + } + }; + const foomw3 = { + setSource(_src, next) { + next(null, { + src: 'http://example.com/video.mp4', + type: 'video/mp4' + }); + } + }; + + middleware.use('video/foo', foomw1); + middleware.use('video/foo', foomw2); + middleware.use('video/foo', foomw3); + + middleware.setSource(window.setTimeout, {src: 'foo', type: 'video/foo'}, function(_src, _acc) { + src = _src; + acc = _acc; + }); + + this.clock.tick(1); + + assert.equal(src.type, 'video/mp4', 'we selected a new type of video/mp4'); + assert.equal(src.src, 'http://example.com/video.mp4', 'we selected a new src of video.mp4'); + assert.equal(acc.length, 2, 'we got two middleware'); + assert.equal(acc[0], foomw1, 'foomw is the first middleware'); + assert.equal(acc[1], foomw2, 'foomw is the first middleware'); + + middleware.getMiddleware('video/foo').pop(); + middleware.getMiddleware('video/foo').pop(); +}); diff --git a/test/unit/tech/tech-faker.js b/test/unit/tech/tech-faker.js index b6f47dd021..b11f9a1405 100644 --- a/test/unit/tech/tech-faker.js +++ b/test/unit/tech/tech-faker.js @@ -30,6 +30,8 @@ class TechFaker extends Tech { setControls(val) {} + setVolume(newVolume) {} + currentTime() { return 0; } diff --git a/test/unit/test-helpers.js b/test/unit/test-helpers.js index 1f9f73f693..e901f9f24f 100644 --- a/test/unit/test-helpers.js +++ b/test/unit/test-helpers.js @@ -21,7 +21,11 @@ const TestHelpers = { playerOptions = playerOptions || {}; playerOptions.techOrder = playerOptions.techOrder || ['techFaker']; - return new Player(videoTag, playerOptions); + const player = new Player(videoTag, playerOptions); + + player.middleware_ = [player.tech_]; + + return player; }, getComputedStyle(el, rule) { diff --git a/test/unit/tracks/text-tracks.test.js b/test/unit/tracks/text-tracks.test.js index aab476c026..b037f56ad0 100644 --- a/test/unit/tracks/text-tracks.test.js +++ b/test/unit/tracks/text-tracks.test.js @@ -419,7 +419,7 @@ QUnit.test('should return correct remote text track values', function(assert) { const tag = document.getElementById('example_1'); const player = TestHelpers.makePlayer({}, tag); - this.clock.tick(1); + this.clock.tick(10); assert.equal(player.remoteTextTracks().length, 1, 'add text track via html'); assert.equal(player.remoteTextTrackEls().length, 1, 'add html track element via html'); diff --git a/test/unit/video.test.js b/test/unit/video.test.js index 3f04622f01..5efb627499 100644 --- a/test/unit/video.test.js +++ b/test/unit/video.test.js @@ -3,8 +3,16 @@ import videojs from '../../src/js/video.js'; import * as Dom from '../../src/js/utils/dom.js'; import log from '../../src/js/utils/log.js'; import document from 'global/document'; - -QUnit.module('video.js'); +import sinon from 'sinon'; + +QUnit.module('video.js', { + beforeEach() { + this.clock = sinon.useFakeTimers(); + }, + afterEach() { + this.clock.restore(); + } +}); QUnit.test('should create a video tag and have access children in old IE', function(assert) { const fixture = document.getElementById('qunit-fixture'); @@ -276,6 +284,8 @@ QUnit.test('ingested player div should not create a new tag for movingMediaEleme techOrder: ['html5'] }); + this.clock.tick(1); + assert.equal(player.el(), playerDiv, 'we re-used the given div'); assert.equal(player.tech_.el(), vid, 'we re-used the video element'); assert.ok(player.hasClass('foo'), 'keeps any classes that were around previously'); @@ -311,6 +321,8 @@ QUnit.test('should create a new tag for movingMediaElementInDOM', function(asser techOrder: ['html5'] }); + this.clock.tick(1); + assert.notEqual(player.el(), playerDiv, 'we used a new div'); assert.notEqual(player.tech_.el(), vid, 'we a new video element');