Skip to content

Commit

Permalink
feat: add mediator middleware type for play() (#4868)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
ldayananda authored and gkatsev committed Jan 30, 2018
1 parent a345971 commit bf3eb45
Show file tree
Hide file tree
Showing 9 changed files with 436 additions and 13 deletions.
80 changes: 77 additions & 3 deletions docs/guides/middleware.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

```
+----------+ +----------+
Expand All @@ -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.
Expand Down Expand Up @@ -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

Expand All @@ -71,4 +146,3 @@ videojs.use('*', function(player) {
};
});
```

161 changes: 161 additions & 0 deletions sandbox/middleware-play.html.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8" />
<title>Video.js Sandbox</title>

<!-- Add ES5 shim and sham for IE8 -->
<script src="../build/temp/ie8/videojs-ie8.js"></script>

<!-- Load the source files -->
<link href="../build/temp/video-js.css" rel="stylesheet" type="text/css">
<script src="../dist/video.js"></script>
<script src="../node_modules/videojs-flash/dist/videojs-flash.js"></script>

<!-- Set the location of the flash SWF -->
<script>
videojs.options.flash.swf = '../build/temp/video-js.swf';
</script>

<style>
.terminate-btn {
margin: 2em 1em;
}

.terminated .vjs-progress-control .vjs-play-progress {
background: red;
}
</style>
</head>
<body>

<video id="vid1" class="video-js" lang="en" controls poster="//d2zihajmogu5jn.cloudfront.net/elephantsdream/poster.png">
<source src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/ed_hd.mp4" type="video/mp4">
<source src="//d2zihajmogu5jn.cloudfront.net/elephantsdream/ed_hd.ogg" type="video/ogg">
</video>

<input id="stateToggle" type="checkbox" class="terminate-btn">
Terminate the play/pause middleware
</input>

<script>
var stateToggle = document.getElementById('stateToggle');

// Middleware 1
var m1 = function(player) {
return {
// Mediating play to the tech
callPlay: function() {
if (stateToggle.checked) {
console.log('Middleware 1: Play is set to terminate');

player.addClass('terminated');
return videojs.middleware.TERMINATOR;

} else {
console.log('Middleware 1: Play has been called');
player.removeClass('terminated');
}
},
// Mediating the results back to the player
play: function(cancelled, value) {
console.log('Middleware 1: play got from tech. What is the value passed?', value);

// Handle the promise if it is returned
if(value && value.then) {
value.then(() => {
console.log('Middleware 1: Promise resolved.')
})
.catch((err) => {
console.log('Middleware 1: Promise rejected.');
});
}

if (cancelled) {
console.log('Middleware 1: play has been cancelled prior to this middleware');
}
},
// Mediating to tech
callPause: function() {
if (stateToggle.checked) {
console.log('Middleware 1: Pause is set to terminate');

player.addClass('terminated');
return videojs.middleware.TERMINATOR;

} else {
console.log('Middleware 1: Pause has been called');
player.removeClass('terminated');
}
},
// Mediating the results back to the player
pause: function(cancelled, value) {
console.log('Middleware 1: pause got back from tech. What is the value passed?', value);

if (cancelled) {
console.log('Middleware 1: pause has been cancelled prior to this middleware');
}

return value;
},
// Required for middleware. Simply passes along the source
setSource: function(srcObj, next) {
next(null, srcObj);
}
};
};

// Middleware 2
var m2 = function(player) {
return {
callPlay: function() {
console.log('Middleware 2: play has been called');
},
play: function(cancelled, value) {
console.log('Middleware 2: got play from tech. What is the value passed?', value);

if (cancelled) {
console.log('Middleware 2: play has been cancelled prior to this middleware');
}

return value;
},
callPause: function() {
console.log('Middleware 2: pause has been called');
},
pause: function(cancelled, value) {
console.log('Middleware 2: got pause from tech. What is the value passed?', value);

if (cancelled) {
console.log('Middleware 2: pause has been cancelled prior to this middleware');
}

return value;
},
setSource: function(srcObj, next) {
next(null, srcObj);
}
};
}

videojs.use('*', m1);
videojs.use('*', m2);

// Initial set-up
var vid = document.getElementById("vid1");
var player = videojs(vid);

console.log('Calling play...');
player.setTimeout(() => {
player.play()
.then(() => {
console.log('The promise resolved, we are playing.');
},
(err) => {
console.log('The promise was rejected, we failed to play.');
});
}, 500);
</script>

</body>
</html>
6 changes: 6 additions & 0 deletions src/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down
49 changes: 48 additions & 1 deletion src/js/tech/middleware.js
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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);
}
Expand All @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion src/js/utils/promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}

/**
Expand Down
Loading

0 comments on commit bf3eb45

Please sign in to comment.