Skip to content

Commit

Permalink
feat: middleware (#3788)
Browse files Browse the repository at this point in the history
Add middleware support. Middleware can function as go-between between the player and the tech. For example, it can modify the duration that the tech returns to the player. In addition, middleware allow for supporting custom video sources and types.

Currently, middleware can only intercept timeline methods like duration, currentTime, and setCurrentTime.

For example,
```js
videojs.use('video/foo', {
  setSource(src, next) {
    next(null, {
      src: 'http://example.com/video.mp4',
      type: 'video/mp4'
    });
  }
});
```
Will allow you to set a source with type `video/foo` which will play back `video.mp4`.

This makes setting the source asynchronous, which aligns it with the spec a bit more. Methods like play can still be called synchronously on the player after setting the source and the player will play once the source has loaded.

`sourceOrder` option was removed as well and it will now always use source ordering.

BREAKING CHANGE: setting the source is now asynchronous. `sourceOrder` option removed and made the default.
  • Loading branch information
gkatsev committed Jan 19, 2017
1 parent b387437 commit 34aab3f
Show file tree
Hide file tree
Showing 11 changed files with 721 additions and 127 deletions.
191 changes: 113 additions & 78 deletions src/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -420,6 +423,8 @@ class Player extends Component {

this.on('fullscreenchange', this.handleFullscreenChange_);
this.on('stageclick', this.handleStageClick_);

this.changingSrc_ = false;
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -1523,7 +1519,7 @@ class Player extends Component {
log(e);
throw e;
}
}
});
}

/**
Expand All @@ -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.
Expand Down Expand Up @@ -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) => {});
}
});
});
});
}
}

/**
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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 || '';
}

/**
Expand Down
23 changes: 23 additions & 0 deletions src/js/tech/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
85 changes: 85 additions & 0 deletions src/js/tech/middleware.js
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 34aab3f

Please sign in to comment.