Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: middleware #3788

Merged
merged 66 commits into from
Jan 19, 2017
Merged

feat: middleware #3788

merged 66 commits into from
Jan 19, 2017

Conversation

gkatsev
Copy link
Member

@gkatsev gkatsev commented Nov 16, 2016

Description

This is the initial PR for middleware which hopes to solve the problems from #3734.
Middleware are objects with two special functions setSource and setTech. On top of these special methods, it can have getters and setters. Currently, the only allowed ones are currentTime, duration, and setCurrentTime. In the future it can be extended to allow more. Also, methods like play could probably be treated as setters and events originating from the video element can be treated as getters and events triggered on the player could be treated as setters. But that's more in the future.

The core of middleware is that video types are basically a routing tree. So, you can "use" a middleware on a particular type but you can also "use" a tech on a particular type using special syntax since techs need a bit extra handling.

// example HLS usage
videojs.use('application/x-mpegurl', require('contrib-hls'));
videojs.use('application/vnd.apple.mpegurl', require('contrib-hls'));

// example tech usage
videojs.use('video/mp4', 'videojs/html5');
videojs.use('application/x-mpegurl', 'videojs/html5');

The contrib-hls object could look something like this:

module.exports = {
  setSource(srcObj, next) {
    // next expects an error as a first argument to let the middleware know whether you can handle the current srcObj
    // you can also modify this srcObj and return something else instead which will continue down the middleware tree
    next(false, srcObj);
  },

  setTech(tech) {
    // this gets the tech that ends up getting chosen to back everything. Store it if you need it.
    // the tech still acts as the final source of truth for everything
  },

  currentTime(previousCt) {
    // getters get the value from further up the chain and should return a transformed value, if necessary.
    // In a simple case, the middleware would get the value from the tech and would expect to return a value for the player to use
    return previousCt / 2;
  },

  setCurrentTime(newCt) {
    // setters get the value from further up the chain and should return a transformed value, if necessary.
   // In a simple case, the middleware would get the value from the player and would expect to return a value for the tech to use
  return newCt;
  }
};

I have a simple live example here: http://plnkr.co/edit/Ap9Fvg21lpYW3Rnlk9ty?p=preview

@boushley
Copy link
Contributor

I haven't dug into the code yet, but from the description it seems odd that for setSource and setCurrentTime follow different patterns.

What is the purpose of the error argument passed to next in setSource?

@@ -20,9 +20,15 @@
"build": "grunt dist",
"change": "grunt chg-add",
"clean": "grunt clean",
"grunt": "grunt",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of this will get removed before it's ready. Makes working with this locally a bit easier.
It'll probably get pulled into a separate PR.

}

middlewareGet(method) {
return this.middleware_.reduceRight(this.middlewareIterator(method), undefined);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Finally, I can use reduce and reduceRight!

*/
if (typeof define === 'function' && define.amd) {
define('videojs', [], () => videojs);
module.exports = videojs;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will get pulled into a separate PR.

module.exports = videojs;
}
videojs.use = middlewareUse;
videojs.use('video/mp4', 'videojs/html5');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How to register the techs for native functionality is still something I'm a bit unsure about.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely something we need to think about. Because as it stands you can never add a middleware that handles setSource for 'video/mp4' since there is a tech registered as the first item always.

Copy link
Contributor

@boushley boushley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great start. There are a couple things we need to work through though, like techs abruptly ending the middleware chain as talked about in slack.

Also, we might want to see if we can unify this syntax in some way so that setSource is more like a generic setter. Although to do that you would have to make the setCurrentTime setter async instead of sync which would likely have some bigger impacts.

});

// something weird happened, try the next middleware on the current level
} else if (middleware.length > 1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would probably be good to at least log a warning that middleware for this src was undefined.


// we've succeeded, now we need to go deeper
acc.push(mw);
ssh(_src, middlewares[_src.type] || [], next, acc);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need some way in this case to prevent us from calling the same middleware over and over. If the middleware leaves the src.type unchanged and doesn't send along an error, we will keep calling the same middleware over and over.


// something happened, try the next middleware on the current level
if (err) {
return ssh(src, middleware.slice(1), next, acc);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of doing a return here which implies that we care about the return value, can we just put an else on the if statement and add the following two lines to it.

This will also make the code more consistent with other recursive calls in this function which don't return.


// we've reached a fork, so, we need go deeper
} else {
ssh(src, middlewares[mw] || [], next, acc);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this makes it possible to do something like videojs.use('video/foo', 'my-standard-middleware-stack') and when that "middleware" is hit it would then start processing the my-standard-middleware-stack set of middlewares, right? Seems interesting.

Although assuming that set of middleware eventually changes the type back to something standard we need to determine if we will ever call the same middleware twice.

const middlewares = {};

export function use(type, middleware) {
(middlewares[type] = middlewares[type] || []).push(middleware);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm too new to the project to know the general code style guidelines, but I find that lines like this while clever add an unnecessary obstacle to reading the code.

I feel that dividing this up into two lines makes it more idiomatic, and thus easier to read:

middlewares[type] = middlewares[type] || []
middlewares[type].push(middleware)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but my one lines...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:D

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're picking this nit, I generally prefer concat to || [].

middlewares[type] = [].concat(middlewares[type], middlware)

This is a little bit of nonsense here since we're ok with mutating the original array, so ¯_(ツ)_/¯

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I agree with @boushley, but @mmcc would be fine too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think that "Type" should be renamed to mimeType. I am also wondering if it would make more sense for middleware to come first in the arguments order. I want to use() hls for x type(s). I also thing that type should be able to be an array.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mmcc The only problem with concat in that case is that if middlewares[type] is undefined, you'll get an array like [undefined, middleware] which I think is probably undesirable.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went for the two line method.

}

middlewareSet(method, arg) {
return this.middleware_.reduce(this.middlewareIterator(method), arg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hooray for the right tool for the job! (reduce here and reduceRight below!)


middlewareGet(method) {
return this.middleware_.reduceRight(this.middlewareIterator(method), undefined);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like these three middleware functions might belong in the middleware.js file instead. Just be that they accept a list of middlewares to process that would be passed in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The methods should be private. The reason why they aren't in middleware.js and why setSource is special is because setSource traverses through our middleware tree and sets up the specific middleware to be used for the player at this time. Though, I guess it can be moved into middleware.js and then this.middleware_ could be passed in.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, I was assuming that this.middleware_ would be passed in.

The player.js file is big enough as it is. Seems like iterating over a list of middlewares should be in the middlewares file instead.

@gkatsev
Copy link
Member Author

gkatsev commented Nov 17, 2016

Should middleware be given a reference to the player?

@boushley
Copy link
Contributor

It seems like it could add some new capabilities. In fact to implement the use case I talked about before (unmute sets volume to above 0) you would need to handle the mute setter and be able to set volume on the player.

This could also cause problems if someone does something silly like tries to set currentTime on the player while in a middleware that is handling currentTime.

Copy link
Member

@mmcc mmcc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silly nitpicks aside, I really, really like this concept. A lot. Huge 👍

this.middleware_ = mws;
this.loadTech_(tech.name, src_);
middleware.setTech(mws, this.tech_);
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If tech.name is the only thing used here, we could destructure in the args. I don't feel strongly about this one, just something I've started doing a little more of lately.

middleware.setSource(src, ({ name }, src_, mws) => {
  this.middleware_ = mws;
  this.loadTech_(name, src_);
  middleware.setTech(mws, this.tech_);
});

const middlewares = {};

export function use(type, middleware) {
(middlewares[type] = middlewares[type] || []).push(middleware);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're picking this nit, I generally prefer concat to || [].

middlewares[type] = [].concat(middlewares[type], middlware)

This is a little bit of nonsense here since we're ok with mutating the original array, so ¯_(ツ)_/¯

}

export function setSource(src, next) {
setTimeout(()=>ssh(src, middlewares[src.type] || [], next, []), 1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought Standard required spaces here (i.e. () => foo())?

I don't feel crazy strong either way, but on line 16 there are spaces so we're not even being consistent within the same file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently not but yeah, I agree that there should be more whitespace. Originally added it in to verify that async works.

Copy link
Contributor

@brandonocasey brandonocasey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR could also really use a tech guide update, that would certainly make it more understandable for me.

"predev": "babel src/js -d es5",
"dev": "run-p babel browserify",
"dev-test": "run-p dev test-watch",
"babel": "babel -w src/js -d es5",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to keep all of these in here? Most of them seem useful, so I wouldn't mind it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to pull these out of this PR when getting ready to merge and make a separate PR out of it.

@@ -829,6 +834,8 @@ class Player extends Component {
'techId': `${this.id()}_${techName}_api`,
'videoTracks': this.videoTracks_,
'textTracks': this.textTracks_,
'remoteTextTracks': this.remoteTextTracks_,
'remoteTextTrackEls_': this.remoteTextTrackEls_,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a change that is basically the same as this in the Unify TextTrack API PR. Does this change need to be in this PR? (if so we just need to be aware that this will have to be merged first, and the rebase could be a bit messy.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately, this change does need to be in this PR, some of the tests rely on tracks and will fail without it because techs are now async.

@@ -920,6 +923,8 @@ class Player extends Component {
// Save the current text tracks so that we can reuse the same text tracks with the next tech
this.videoTracks_ = this.videoTracks();
this.textTracks_ = this.textTracks();
this.remoteTextTracks_ = this.remoteTextTracks();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the other comment with a new link to the unify texttrack api PR.

return this;
}

src_(source) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will need a jsdoc comment

if (this.tech_) {
return this.tech_.textTracks();
if (!this.tech_) {
this.textTracks_ = this.textTracks_ || new TextTrackList();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See Other comments on Unify TextTrack API PR

if (method in middleware.allowedSetters) {
return middleware.set(this.middleware_, this.tech_, method, arg);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems like we should check do this here, to prevent checking this.tech_ in two places below.

if (!this.tech_) {
  return
}  

Also it seems like this.tech_.ready() already checks if the tech is ready? Seems like we could merge a lot of this code together to make it much simpler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, we probably want to put the middleware section into one of the ifs.
It's possible that just doing .ready() works, in which case, I can see if it's worth trying to simplify it for this PR.

@@ -1562,6 +1572,10 @@ class Player extends Component {
techGet_(method) {
if (this.tech_ && this.tech_.isReady_) {

if (method in middleware.allowedGetters) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function could also use some similar cleanup, maybe we can do both in another PR, but its always bothered me that it seems so messy an nested.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. I'd prefer to do the cleanup in a separate PR but might do it if it helps make the middleware part more clear as well.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can't actually do this cleanup here because we need to return a value from this function and this.ready() doesn't guarantee a value, even if you're running it synchronously. However, I think I was able to simplify techCall.

const middlewares = {};

export function use(type, middleware) {
(middlewares[type] = middlewares[type] || []).push(middleware);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think that "Type" should be renamed to mimeType. I am also wondering if it would make more sense for middleware to come first in the arguments order. I want to use() hls for x type(s). I also thing that type should be able to be an array.

}

export function setSource(setTimeout, src, next) {
setTimeout(() => ssh(src, middlewares[src.type], next), 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will we get this far if a src does not have a type? Should we be checking for this somewhere?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be async?

setTimeout(() => ssh(src, middlewares[src.type], next), 1);
}

export function setTech(middleware, tech) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

middlewareList since it seems like we want it to be an array here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or middlewares.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind of tricky since middleware is both a singular and a plural noun.

Copy link
Member

@misteroneill misteroneill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. Some minor feedback/comments.


export default videojs;
videojs.use = middlewareUse;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There a reason this comes after module.exports?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope.

@@ -9,6 +9,9 @@
<body>
<div id="qunit"></div>
<script src="../node_modules/qunitjs/qunit/qunit.js"></script>
<script>
QUnit.config.reorder = false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. I think setting reorder to false is really used to mask bad cleanup in tests. I think @gkatsev may have cleaned that up in another PR as well.

@@ -283,6 +281,7 @@ QUnit.test('should asynchronously fire error events during source selection', fu
});

this.clock.tick(1);
this.clock.tick(1);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add an explanation as to why we need to tick 2ms instead of 1ms?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because the error is triggered async, so, we need one for it, but we need an additional one for the player creation itself.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a comment

@gkatsev
Copy link
Member Author

gkatsev commented Dec 28, 2016

I've moved the grunt and package.json changes into a separate PR: #3896 and thus I reverted the changes in this PR.

@gkatsev gkatsev changed the title [WIP] Middleware [RFC] Middleware Dec 28, 2016
@gkatsev
Copy link
Member Author

gkatsev commented Dec 28, 2016

@mmcc @boushley @misteroneill I've changed the PR from WIP to RFC. I think it's in decent shape now just needs closer inspection.

@brandonocasey
Copy link
Contributor

@gkatsev It would be a good idea to run the jsdoc linter over this code

setSourceHelper(src, mwrest, next, acc);
} else {
// const techs = [].concat(middlewares['*']);
// const tech = techs.filter((tech) => !!tech.tech.canPlayType(src.type))[0].name;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these commented lines just be removed?

@gkatsev gkatsev changed the title [RFC] Middleware feat: middleware Jan 19, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants