From bf3eb45a37bc27c0857452423790ec29386eca6f Mon Sep 17 00:00:00 2001 From: ldayananda Date: Tue, 30 Jan 2018 11:30:42 -0500 Subject: [PATCH] feat: add mediator middleware type for play() (#4868) This will allow middleware to interact with calls to play() from the tech. This will require a method of indicating to middleware previously run that a middleware down the chain has terminated or stopped execution. * Adds middleware mediator method that runs middleware from the player to the tech and a second time back up to the player. This category was created because play is both a setter(changes the playback state) and a getter(gets a native play promise if available). This also has the ability to tell whether a middleware has terminated before reaching the tech. * Adds a middleware.TERMINATOR sentinel value that is available on the videojs object * Adds play to the allowedMediators * Adds paused to the allowedGetters * Adds a sandbox example of a play mediator middleware --- docs/guides/middleware.md | 80 ++++++++++++- sandbox/middleware-play.html.example | 161 +++++++++++++++++++++++++++ src/js/player.js | 6 + src/js/tech/middleware.js | 49 +++++++- src/js/utils/promise.js | 2 +- src/js/video.js | 33 +++++- test/unit/player.test.js | 10 +- test/unit/tech/middleware.test.js | 105 ++++++++++++++++- test/unit/tech/tech-faker.js | 3 + 9 files changed, 436 insertions(+), 13 deletions(-) create mode 100644 sandbox/middleware-play.html.example diff --git a/docs/guides/middleware.md b/docs/guides/middleware.md index 0e82d8aa50..f25635d488 100644 --- a/docs/guides/middleware.md +++ b/docs/guides/middleware.md @@ -5,14 +5,28 @@ Middleware is a Video.js feature that allows interaction with and modification o ## Table of Contents * [Understanding Middleware](#understanding-middleware) + * [Middleware Setters](#middleware-setters) + * [Middleware Getters](#middleware-getters) + * [Middleware Mediators](#middleware-mediators) + * [Termination and Mediators](#termination-and-mediators) * [Using Middleware](#using-middleware) + * [Terminating Mediator Methods](#terminating-mediator-methods) * [setSource](#setsource) ## Understanding Middleware -Middleware are functions that return an object with methods matching those on the `Tech`. There are currently a limited set of allowed methods that will be understood by middleware. These are: `buffered`, `currentTime`, `setCurrentTime`, `duration`, `seekable` and `played`. +Middleware are functions that return an object with methods matching those on the `Tech`. There are currently a limited set of allowed methods that will be understood by middleware. These are: `buffered`, `currentTime`, `setCurrentTime`, `duration`, `seekable`, `played`, `play`, `pause` and `paused`. -These allowed methods are split into two categories: `getters` and `setters`. Setters will be called on the `Player` first and run through middleware(from left to right) before calling the method, with its arguments, on the `Tech`. Getters are called on the `Tech` first and are run though middleware(from right to left) before returning the result to the `Player`. +These allowed methods are split into three categories: `getters`, `setters`, and `mediators`. + +### Middleware Setters +Setters will be called on the `Player` first and run through middleware (from left to right) before calling the method, with its arguments, on the `Tech`. + +### Middleware Getters +Getters are called on the `Tech` first and are run though middleware (from right to left) before returning the result to the `Player`. + +### Middleware Mediators +Mediators are called on the `Player` first, run through middleware (from left to right), then called on the `Tech`. The result is returned to the `Player` unchanged, while calling the middleware from right to left. For more information on mediators, check out the [mediator section](#termination-and-mediators). ``` +----------+ +----------+ @@ -24,6 +38,47 @@ These allowed methods are split into two categories: `getters` and `setters`. Se +----------+ +----------+ ``` +### Termination and Mediators + +Mediators are the third category of allowed methods. These are methods that not only change the state of the Tech, but also return some value back to the Player. Currently, these are `play` and `pause`. + +``` + mediate to tech + +-------------> + ++----------+ +----------+ +| | | | +| +-----call{method}-----> | +| Player | | Tech | +| <-------{method}-------+ | +| | | | ++----------+ +----------+ + + <---------------+ + mediate to player + +``` + +Mediators make a round trip: starting at the `Player`, mediating to the `Tech` and returning the result to the `Player` again. A `call{method}` method must be supplied by the middleware which is used when mediating to the `Tech`. On the way back to the `Player`, the `{method}` will be called instead, with 2 arguments: `terminated`, a Boolean indicating whether a middleware terminated during the mediation to the tech portion, and `value`, which is the value returned from the `Tech`. A barebones example of a middleware with Mediator methods is: + +``` +var myMiddleware = function(player) { + return { + callPlay: function() { + // mediating to the Tech + ... + }, + pause: function(terminated, value) { + // mediating back to the Player + ... + }, + ... + }; +}; +``` + +Middleware termination occurs when a middleware method decides to stop mediating to the Tech. We'll see more examples of this in the [next section](#terminating-mediator-methods). + ## Using Middleware Middleware are registered to a video MIME type, and will be run for any source with that type. @@ -56,6 +111,26 @@ var myMiddleware = function(player) { videojs.use('*', myMiddleware); ``` +### Terminating Mediator Methods + +Mediator methods can terminate, by doing the following: + +```javascript +var myMiddleware = function(player) { + return { + callPlay: function() { + // Terminate by returning the middleware terminator + return videojs.middleware.TERMINATOR; + }, + play: function(terminated, value) { + // the terminated argument should be true here. + }, + ... + }; +}; + +videojs.use('*', myMiddleware); +``` ## setSource @@ -71,4 +146,3 @@ videojs.use('*', function(player) { }; }); ``` - diff --git a/sandbox/middleware-play.html.example b/sandbox/middleware-play.html.example new file mode 100644 index 0000000000..240bfc7aca --- /dev/null +++ b/sandbox/middleware-play.html.example @@ -0,0 +1,161 @@ + + + + + Video.js Sandbox + + + + + + + + + + + + + + + + + + + + Terminate the play/pause middleware + + + + + + diff --git a/src/js/player.js b/src/js/player.js index f6d2552cd2..5f276a4e10 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -1632,6 +1632,9 @@ class Player extends Component { this.ready(function() { if (method in middleware.allowedSetters) { return middleware.set(this.middleware_, this.tech_, method, arg); + + } else if (method in middleware.allowedMediators) { + return middleware.mediate(this.middleware_, this.tech_, method, arg); } try { @@ -1663,6 +1666,9 @@ class Player extends Component { if (method in middleware.allowedGetters) { return middleware.get(this.middleware_, this.tech_, method); + + } else if (method in middleware.allowedMediators) { + return middleware.mediate(this.middleware_, this.tech_, method); } // Flash likes to die and reload when you hide or reposition it. diff --git a/src/js/tech/middleware.js b/src/js/tech/middleware.js index d6f0fa3c3f..05777ffbb6 100644 --- a/src/js/tech/middleware.js +++ b/src/js/tech/middleware.js @@ -1,7 +1,10 @@ import { assign } from '../utils/obj.js'; +import toTitleCase from '../utils/to-title-case.js'; const middlewares = {}; +export const TERMINATOR = {}; + export function use(type, middleware) { middlewares[type] = middlewares[type] || []; middlewares[type].push(middleware); @@ -23,28 +26,64 @@ export function setTech(middleware, tech) { middleware.forEach((mw) => mw.setTech && mw.setTech(tech)); } +/** + * Calls a getter on the tech first, through each middleware + * from right to left to the player. + */ export function get(middleware, tech, method) { return middleware.reduceRight(middlewareIterator(method), tech[method]()); } +/** + * Takes the argument given to the player and calls the setter method on each + * middlware from left to right to the tech. + */ export function set(middleware, tech, method, arg) { return tech[method](middleware.reduce(middlewareIterator(method), arg)); } +/** + * Takes the argument given to the player and calls the `call` version of the method + * on each middleware from left to right. + * Then, call the passed in method on the tech and return the result unchanged + * back to the player, through middleware, this time from right to left. + */ +export function mediate(middleware, tech, method, arg = null) { + const callMethod = 'call' + toTitleCase(method); + const middlewareValue = middleware.reduce(middlewareIterator(callMethod), arg); + const terminated = middlewareValue === TERMINATOR; + const returnValue = terminated ? null : tech[method](middlewareValue); + + executeRight(middleware, method, returnValue, terminated); + + return returnValue; +} + export const allowedGetters = { buffered: 1, currentTime: 1, duration: 1, seekable: 1, - played: 1 + played: 1, + paused: 1 }; export const allowedSetters = { setCurrentTime: 1 }; +export const allowedMediators = { + play: 1, + pause: 1 +}; + function middlewareIterator(method) { return (value, mw) => { + // if the previous middleware terminated, pass along the termination + if (value === TERMINATOR) { + return TERMINATOR; + } + if (mw[method]) { return mw[method](value); } @@ -53,6 +92,14 @@ function middlewareIterator(method) { }; } +function executeRight(mws, method, value, terminated) { + for (let i = mws.length - 1; i >= 0; i--) { + const mw = mws[i]; + + mw[method](terminated, value); + } +} + function setSourceHelper(src = {}, middleware = [], next, player, acc = [], lastRun = false) { const [mwFactory, ...mwrest] = middleware; diff --git a/src/js/utils/promise.js b/src/js/utils/promise.js index 03f8b1923b..186fd59785 100644 --- a/src/js/utils/promise.js +++ b/src/js/utils/promise.js @@ -9,7 +9,7 @@ * Whether or not the object is `Promise`-like. */ export function isPromise(value) { - return value !== undefined && typeof value.then === 'function'; + return value !== undefined && value !== null && typeof value.then === 'function'; } /** diff --git a/src/js/video.js b/src/js/video.js index be8a78d2a6..46ada599c2 100644 --- a/src/js/video.js +++ b/src/js/video.js @@ -31,7 +31,7 @@ import xhr from 'xhr'; // Include the built-in techs import Tech from './tech/tech.js'; -import { use as middlewareUse } from './tech/middleware.js'; +import { use as middlewareUse, TERMINATOR } from './tech/middleware.js'; // HTML5 Element Shim for IE8 if (typeof HTMLVideoElement === 'undefined' && Dom.isReal()) { @@ -340,8 +340,39 @@ videojs.getTech = Tech.getTech; */ videojs.registerTech = Tech.registerTech; +/** + * Register a middleware to a source type. + * + * @param {String} type A string representing a MIME type. + * @param {function(player):object} middleware A middleware factory that takes a player. + */ videojs.use = middlewareUse; +/** + * An object that can be returned by a middleware to signify + * that the middleware is being terminated. + * + * @type {object} + * @memberOf {videojs} + * @property {object} middleware.TERMINATOR + */ +// Object.defineProperty is not available in IE8 +if (!browser.IS_IE8 && Object.defineProperty) { + Object.defineProperty(videojs, 'middleware', { + value: {}, + writeable: false, + enumerable: true + }); + + Object.defineProperty(videojs.middleware, 'TERMINATOR', { + value: TERMINATOR, + writeable: false, + enumerable: true + }); +} else { + videojs.middleware = { TERMINATOR }; +} + /** * 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 31bd9d82b9..ef39c48d4b 100644 --- a/test/unit/player.test.js +++ b/test/unit/player.test.js @@ -1469,7 +1469,7 @@ QUnit.test('should not allow to register custom player when any player has been QUnit.test('techGet runs through middleware if allowedGetter', function(assert) { let cts = 0; let durs = 0; - let ps = 0; + let lps = 0; videojs.use('video/foo', () => ({ currentTime() { @@ -1478,8 +1478,8 @@ QUnit.test('techGet runs through middleware if allowedGetter', function(assert) duration() { durs++; }, - paused() { - ps++; + loop() { + lps++; } })); @@ -1496,11 +1496,11 @@ QUnit.test('techGet runs through middleware if allowedGetter', function(assert) player.techGet_('currentTime'); player.techGet_('duration'); - player.techGet_('paused'); + player.techGet_('loop'); assert.equal(cts, 1, 'currentTime is allowed'); assert.equal(durs, 1, 'duration is allowed'); - assert.equal(ps, 0, 'paused is not allowed'); + assert.equal(lps, 0, 'loop is not allowed'); middleware.getMiddleware('video/foo').pop(); player.dispose(); diff --git a/test/unit/tech/middleware.test.js b/test/unit/tech/middleware.test.js index 71faf4f550..50c6569767 100644 --- a/test/unit/tech/middleware.test.js +++ b/test/unit/tech/middleware.test.js @@ -21,7 +21,7 @@ QUnit.test('middleware can be added with the use method', function(assert) { assert.equal(middleware.getMiddleware('foo').pop(), mwFactory, 'we are able to add middleware'); }); -QUnit.test('middleware get iterates through the middleware array the right order', function(assert) { +QUnit.test('middleware get iterates through the middleware array in the right order', function(assert) { const cts = []; const durs = []; const foos = []; @@ -81,7 +81,7 @@ QUnit.test('middleware get iterates through the middleware array the right order assertion(foo, 4, foos, [5, 8], 'foo'); }); -QUnit.test('middleware set iterates through the middleware array the right order', function(assert) { +QUnit.test('middleware set iterates through the middleware array in the right order', function(assert) { const cts = []; const durs = []; const foos = []; @@ -144,6 +144,107 @@ QUnit.test('middleware set iterates through the middleware array the right order assertion(foo, 8, foos, [10, 5, 8], 'foo'); }); +QUnit.test('middleware mediate iterates through the middleware array twice', function(assert) { + let playsToTech = 0; + let playsToPlayer = 0; + let techPlays = 0; + let techPlay; + let pv1; + let pv2; + let pc1; + let pc2; + + const mws = [{ + callPlay() { + playsToTech++; + }, + play(cancelled, value) { + playsToPlayer++; + pv1 = value; + pc1 = cancelled; + } + }, { + callPlay() { + playsToTech++; + }, + play(cancelled, value) { + playsToPlayer++; + pv2 = value; + pc2 = cancelled; + } + }]; + const tech = { + play() { + techPlays++; + techPlay = {then: () => {}}; + + return techPlay; + } + }; + + const pp = middleware.mediate(mws, tech, 'play'); + + assert.equal(playsToTech, playsToPlayer, 'middleware got called the same number of times'); + assert.equal(playsToTech, 2, 'both middleware got called before the tech'); + assert.equal(techPlays, 1, 'the tech method only gets called once'); + assert.equal(playsToPlayer, 2, 'both middleware got called after the tech'); + + assert.deepEqual(pv1.then, techPlay.then, 'the value returned by the tech is passed through the middleware'); + assert.deepEqual(pv2, techPlay, 'the value returned by the tech is passed through the middleware'); + assert.deepEqual(pp, techPlay, 'the value returned to the player is the value returned from the tech'); + assert.equal(pc1, false, 'the play has not been cancelled in middleware 1'); + assert.equal(pc2, false, 'the play has not been cancelled in middleware 2'); +}); + +QUnit.test('middleware mediate allows and can detect cancellation', function(assert) { + let playsToTech = 0; + let playsToPlayer = 0; + let techPlays = 0; + let pv1; + let pv2; + let pc1; + let pc2; + + const mws = [{ + callPlay() { + playsToTech++; + }, + play(cancelled, value) { + playsToPlayer++; + pv1 = value; + pc1 = cancelled; + } + }, { + callPlay() { + playsToTech++; + return middleware.TERMINATOR; + }, + play(cancelled, value) { + playsToPlayer++; + pv2 = value; + pc2 = cancelled; + } + }]; + const tech = { + play() { + techPlays++; + return {then: () => {}}; + } + }; + + const pp = middleware.mediate(mws, tech, 'play'); + + assert.equal(playsToTech, 2, 'both middleware run until middleware terminates'); + assert.equal(techPlays, 0, 'the tech should not be called if a middleware terminates'); + assert.equal(playsToPlayer, 2, 'both middleware run after the tech'); + + assert.equal(pv1, null, 'null is returned through the middleware if a middleware terminated previously'); + assert.equal(pv2, null, 'null is returned through the middleware if a middleware terminated previously'); + assert.equal(pp, null, 'null is returned to the player if a middleware terminated previously'); + assert.equal(pc1, true, 'the play has been cancelled in middleware 1'); + assert.equal(pc2, true, 'the play has been cancelled in middleware 2'); +}); + QUnit.test('setSource is run asynchronously', function(assert) { let src; let acc; diff --git a/test/unit/tech/tech-faker.js b/test/unit/tech/tech-faker.js index 8346b120c8..1b0ca50a27 100644 --- a/test/unit/tech/tech-faker.js +++ b/test/unit/tech/tech-faker.js @@ -65,6 +65,9 @@ class TechFaker extends Tech { paused() { return true; } + loop() { + return false; + } play() { this.trigger('play'); }