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: add mediator middleware type for play() #4868

Merged
merged 39 commits into from
Jan 30, 2018

Conversation

ldayananda
Copy link
Contributor

@ldayananda ldayananda commented Jan 9, 2018

Description

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.

Specific Changes proposed

  • 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

Checklist

  • Feature implemented / Bug fixed
  • unit tests added
  • documentation added
  • guide added/updated
  • verified in browser
  • example created
  • Reviewed by Two Core Contributors

@@ -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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

player.play() seems to be done via techGet for the html5 tech, but it's conceivable that a future mediator will use techCall

Copy link
Member

Choose a reason for hiding this comment

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

maybe we should just make a techCallAndGet for stuff like play?

Copy link
Contributor Author

@ldayananda ldayananda Jan 11, 2018

Choose a reason for hiding this comment

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

That might make sense; I don't feel strongly about it one way or another.

Copy link
Member

Choose a reason for hiding this comment

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

Let's get everything else ready and then come back to this.

Copy link
Member

Choose a reason for hiding this comment

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

eh, I think we can just leave it like this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, doesn't feel like there's a pressing need to change this yet

@@ -2,6 +2,8 @@ import { assign } from '../utils/obj.js';

const middlewares = {};

export const TERMINATOR = 'TERMINATOR';
Copy link
Member

Choose a reason for hiding this comment

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

we should make this be an object instance rather than a string since objects only equal themselves. Then the string will also be a valid value.

Copy link
Contributor Author

@ldayananda ldayananda Jan 10, 2018

Choose a reason for hiding this comment

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

what do you mean by "the string will also be a valid value"?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Did you mean change this to export const TERMINATOR = () => { return 'TERMINATOR'; } and then here:

https://github.com/ldayananda/video.js/blob/52011c3bc175897e735ec5018159de222db0980c/src/js/video.js#L752

change it to be videojs.TERMINATOR = TERMINATOR() ?

Copy link
Member

Choose a reason for hiding this comment

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

nope, literally just TERMINATOR = {}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, you meant "the string will also be a valid value" to mean that an allowed method could return the string 'TERMINATOR'! 👍 will do

@ldayananda ldayananda changed the title [WIP] feat: Add play() to middleware feat: Add play() to middleware Jan 13, 2018
@ldayananda ldayananda added needs: LGTM Needs one or more additional approvals minor This PR can be added to a minor release. It should not be added to a patch release. labels Jan 13, 2018
src/js/video.js Outdated
@@ -749,5 +749,7 @@ videojs.dom = Dom;
*/
videojs.url = Url;

videojs.TERMINATOR = TERMINATOR;
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 have access to Object.defineProperty we should probably use it here to prevent overwriting this value. Others may think that's overkill, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems reasonable, I'll do that

src/js/video.js Outdated
@@ -749,7 +749,9 @@ videojs.dom = Dom;
*/
videojs.url = Url;

videojs.TERMINATOR = TERMINATOR;
Object.defineProperty(videojs, 'TERMINATOR', {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@misteroneill does this seem right?

src/js/video.js Outdated
@@ -749,5 +749,9 @@ videojs.dom = Dom;
*/
videojs.url = Url;

Object.defineProperty(videojs, 'TERMINATOR', {
Copy link
Member

Choose a reason for hiding this comment

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

this will break in IE8 :)

Also, I think it's reasonable to make it enumerable and it might be nice to make writeable false be explicit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there an example somewhere else in the project of how to do this for IE8 as well? I tried to find one earlier and came up empty.

🆗 explicit writeable: false and enumerable: true sound good to me

Copy link
Member

Choose a reason for hiding this comment

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

something like

if (Object.defineProperty && !browser.IS_IE8) {
  Object.defineProperty(...);
} else {
  videojs.TERMINATOR = TERMINATOR;
}

src/js/video.js Outdated
@@ -749,5 +749,9 @@ videojs.dom = Dom;
*/
videojs.url = Url;

Object.defineProperty(videojs, 'TERMINATOR', {
Copy link
Member

Choose a reason for hiding this comment

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

should this be directly on videojs? Maybe namespaced videojs.middleware.TERMINATOR?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like the idea of it being namespaced to videojs.middleware.TERMINATOR

};

export const allowedSetters = {
setCurrentTime: 1
};

export const allowedMediators = {
play: 1
Copy link
Member

Choose a reason for hiding this comment

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

are we adding pause here as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I should. Thanks for catching that

for (let i = mws.length - 1; i >= 0; i--) {
const mw = mws[i];

mw[method](value, terminated);
Copy link
Member

Choose a reason for hiding this comment

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

can we swap the argument order so it's similar to a node-style callback?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea, we can do that.

@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

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

Let's get everything else ready and then come back to this.

@@ -59,7 +59,7 @@ class BigPlayButton extends Button {

const playFocus = () => playToggle.focus();

if (isPromise(playPromise)) {
if (playPromise && isPromise(playPromise)) {
Copy link
Member

Choose a reason for hiding this comment

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

isPromise should be doing a null check as well, no, this shouldn't be strictly necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did notice that isPromise was checking for undefined specifically. I thought that was for a specific reason, but maybe I was wrong?

Copy link
Member

Choose a reason for hiding this comment

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

if play() is called and no promise was returned, we end up with undefined. Though, we'll never go into this if statement if playPromise doesn't duck-type into a promise.
I'm ok with leaving it if you want, but it shouldn't be necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, while I was testing, if the play was terminated then I saw an error coming from this if statement. Perhaps the right way is to return something other than null in the cases where a middleware terminates.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see. Maybe we should just loosen the check in isPromise

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm fine with going either way - I'll check for null in isPromise for now

src/js/video.js Outdated
@@ -751,8 +751,8 @@ videojs.url = Url;

// Object.defineProperty is not available in IE8
if (!browser.IS_IE8 && Object.defineProperty) {
Object.defineProperty(videojs, 'middleware.TERMINATOR', {
value: TERMINATOR,
Object.defineProperty(videojs, 'middleware', {
Copy link
Member

Choose a reason for hiding this comment

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

you'd want defineProperty each step because now you could overwrite videojs.middleware.TERMINATOR = false

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point

@@ -0,0 +1,116 @@
# Middleware
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This will have to be rebased once the current middleware guide is merged

@gkatsev
Copy link
Member

gkatsev commented Jan 25, 2018

Looking at this version of the middleware guide (https://deploy-preview-4868--videojs-docs.netlify.com/tutorial-middleware.html), I realized we forgot to document the "special" setTech method. Also, looks like we say we have to return an object with these methods but an instance of a class or prototype or w/e will work too. But we can do this as part of a separate PR, probably.
Also, the package-lock has conflicts, yay! (sorry)

@@ -0,0 +1,184 @@
<!DOCTYPE html>
Copy link
Member

Choose a reason for hiding this comment

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

this file should be middleware-play.html.example

margin: 2em 1em;
}

.vjs-progress-control.vjs-control .vjs-play-progress.vjs-slider-bar.terminated {
Copy link
Member

Choose a reason for hiding this comment

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

why not look for the terminated class on the player instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean? I think I needed to be specific to get the right component

Copy link
Member

Choose a reason for hiding this comment

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

.terminated .vjs-process-control .vjs-play-progress

Then below (line 44), you can just do player.addClass('terminated') instead of going through the entire child heirarchy to get the proper element. Or something like that.

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

return value;
Copy link
Member

Choose a reason for hiding this comment

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

is this required?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I can remove it

var vid = document.getElementById("vid1");
var player = videojs(vid, {
controlBar: {
children: [
Copy link
Member

Choose a reason for hiding this comment

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

is there a reason for these?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

heh, copypasta.

},
// 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);
Copy link
Member

Choose a reason for hiding this comment

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

would be nice to have it .then and .catch in here.

src/js/video.js Outdated
@@ -749,5 +755,30 @@ videojs.dom = Dom;
*/
videojs.url = Url;

/**
Copy link
Member

Choose a reason for hiding this comment

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

can we put this block immediately after videojs.use so middleware related stuff are grouped together?

* [Using Middleware](#using-middleware)
* [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` and `pause`.
Copy link
Member

Choose a reason for hiding this comment

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

also paused?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch


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`. 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`.
Copy link
Member

Choose a reason for hiding this comment

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

We're missing a short description of mediators

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Think it's enough to link mediators to the next section "Termination and Mediators"?

Copy link
Member

Choose a reason for hiding this comment

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

Probably. Would be nice to get a one-sentence description, though, if we can.

@@ -56,6 +82,24 @@ var myMiddleware = function(player) {
videojs.use('*', myMiddleware);
```

Mediator methods can terminate, by doing the following:
Copy link
Member

Choose a reason for hiding this comment

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

would be good to make this a subsection so it can be linked to from above.


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`.

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`. For example: `callPlay`. 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`.
Copy link
Member

Choose a reason for hiding this comment

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

I think we should have simple examples here to better illustrate the text.

@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

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

eh, I think we can just leave it like this.

@ldayananda
Copy link
Contributor Author

@gkatsev I think I've now addressed all your comments. I agree that the corrections to the middleware guide can be done in a separate PR. And I removed the package-lock so we should be good now 😄

/**
* Calls a getter on the tech first, through each middleware
* from right to left to the player.
**/
Copy link
Member

Choose a reason for hiding this comment

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

nit: only one * for closing :)


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`. 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`.
Copy link
Member

Choose a reason for hiding this comment

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

Probably. Would be nice to get a one-sentence description, though, if we can.

src/js/video.js Outdated
enumerable: true
});
} else {
videojs.middleware.TERMINATOR = TERMINATOR;
Copy link
Member

Choose a reason for hiding this comment

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

this will fail because videojs.middleware isn't an object.
Should be videojs.middleware = {TERMINATOR}

@gkatsev
Copy link
Member

gkatsev commented Jan 26, 2018

Oh, the commit message should probably be more like feat: add mediator middleware type for play()

@ldayananda ldayananda changed the title feat: Add play() to middleware feat: add mediator middleware type for play() Jan 29, 2018

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`. 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`. Mediators are called on the `Player` first, run through middleware from left to right, then called on the `Tech` and the result is returned to the `Player` unchanged, while calling the middleware from right to left. For more information, check out the [mediator section](#termination-and-mediators).
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gkatsev Added a sentence for Mediators, but it's more confusing in my opinion than reading the full section

Copy link
Member

Choose a reason for hiding this comment

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

🤔 I think this is ok. Maybe @videojs/core-committers has any other thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

I think this paragraph should be split up. I propose breaking it into three distinct sections. Also, made some grammatical improvements.


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.

Copy link
Member

@gkatsev gkatsev left a comment

Choose a reason for hiding this comment

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

LGTM!


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`. 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`. Mediators are called on the `Player` first, run through middleware from left to right, then called on the `Tech` and the result is returned to the `Player` unchanged, while calling the middleware from right to left. For more information, check out the [mediator section](#termination-and-mediators).
Copy link
Member

Choose a reason for hiding this comment

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

I think this paragraph should be split up. I propose breaking it into three distinct sections. Also, made some grammatical improvements.


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.

@@ -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';
Copy link
Member

Choose a reason for hiding this comment

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

Mabye we should just do return Boolean(value) && typeof value.then === 'function';

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it's nice to be explicit here too 🤔

if (!browser.IS_IE8 && Object.defineProperty) {
Object.defineProperty(videojs, 'middleware', {
value: {},
writeable: 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 believe writable: false is the default, so this shouldn't be necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We talked about this elsewhere, but since having writeable: false is very explicit, I plan to keep it 😄

@gkatsev gkatsev merged commit bf3eb45 into videojs:master Jan 30, 2018
@ldayananda ldayananda deleted the play-middleware branch January 30, 2018 16:32
@gkatsev gkatsev removed the needs: LGTM Needs one or more additional approvals label Dec 24, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
minor This PR can be added to a minor release. It should not be added to a patch release.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants