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 Play middleware to avoid calling play on tech #337

Merged
merged 108 commits into from
Apr 19, 2018

Conversation

ldayananda
Copy link
Contributor

@ldayananda ldayananda commented Feb 28, 2018

Limitations: can't be used on mobile platforms and is only available for use on later versions of videojs.

  • Implements a terminating play middleware.
  • Removes special adplaying handling
  • Reworks the "fast track" cases where there is no preroll ad to first advance to Preroll state and contentResuming mode
  • Simplify logic to determine whether a manual play is required after resuming to content
  • Further modularizes cancelContentPlay and removes 1ms timeout

Todos:

  • write integration tests for play middleware
  • finalize mocking for unit tests for play middleware
  • more negative test cases in unit tests

@@ -315,6 +325,7 @@ const contribAdsPlugin = function(options) {
};

player.ads._state = new BeforePreroll(player);
player.ads._state.init(player);
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah interesting, was this causing an issue or was it just something you noticed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Mostly something I noticed, I wanted to set terminate at the earliest point possible in BeforePreroll but felt putting it in the constructor wasn't right

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup makes total sense.

@@ -30,7 +33,10 @@ export default class BeforePreroll extends ContentState {
player.ads.debug('Received play event (BeforePreroll)');

// Don't start content playback yet
cancelContentPlay(player);
if (!isMiddlewareMediatorSupported()) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe you already planned on doing this, but the next step is to move this logic into cancelContentPlay.js. That is, always cancelContentPlay on play if !isMiddlewareMediatorSupported. Then all the states have to do is set _shouldBlockPlay.

@@ -18,6 +18,7 @@ by integrations.
* adTimeoutTimeout has been removed. It was not part of the documented interface, but make note if your integration inspected it.
* There is no longer a snapshot object while checking for postrolls. Now a snapshot is only taken when a postroll ad break actually begins.
* The `contentplayback` event (removed in [4.0.0](https://github.com/videojs/videojs-contrib-ads/blob/cc664517aa0d07398decc0aa5d41974330efc4e4/CHANGELOG.md#400), re-added as deprecated in [4.1.1](https://github.com/videojs/videojs-contrib-ads/blob/cc664517aa0d07398decc0aa5d41974330efc4e4/CHANGELOG.md#411)), has been removed. Use the `playing` event instead.
* The `adplaying` behavior is an implementation detail and has changed in this update. The `adplaying` event is no longer guaranteed to happen once per ad break. It is not intended to be used to detect the beginning of an ad break. The `ads-pod-started` event should be used instead. The `ads-ad-started` event can be used to detect the start of an individual ad in an ad break. There will be multiple `ads-ad-started` events corresponding to each ad in the ad break.
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

// Don't use cancelContentPlay while playMiddleware is in use
return;
} else if (player.ads._shouldBlockPlay === false) {
// Don't block play if explictly set to not block
Copy link
Contributor

Choose a reason for hiding this comment

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

To make this comment more specific, maybe "Only block play if the ad plugin is in a state where play should be blocked. This currently means BeforePreroll and Preroll."

// another cancellation is already in flight, so do nothing
return;
}

player.ads.debug('Using cancelContentPlay to block content playback');
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

},
callPlay() {
// Block play calls while waiting for an ad
if (obj.isMiddlewareMediatorSupported() &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Is the check to isMiddlewareMediatorSupported needed?

Copy link
Contributor

Choose a reason for hiding this comment

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

I ask because it's checked before setting up the middleware in plugin.js

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 is mostly defensive. Although it is checked before registering the the middleware, if in case that changes we want to make sure we don't use both the playMiddleware and cancelContentPlay at the same time.

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 in this case it doesn't have much defensive value but could lead to confusion about how the code works. That's subjective though so not a big deal.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at this again, it looks like it's impossible for this to return false because the middleware is only set up on the same condition. Sorry to waffle but I think we should remove it unless there is a way for it to return true because a reader will assume it does something.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So here's my thinking behind the "defensive code" reasoning:

  • once a middleware is registered, it will always run(since we're using the MIME type agnostic '*')
  • there's a number of video.js versions that don't have Middleware Mediator support (since it was added relatively recently)
  • since the middleware is registered in plugin.js, I'm worried that the reason why we check isMiddlewareMediatorSupported would be lost over time and we'll wind up doing something that causes a runtime exception.

Maybe I'm just worrying about nothing though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Once middleware is registered it will always run, and conditions for it being registered will never change, so it won't need to be checked again.

The video.js versions that don't have mediator support are handled within the check before registering.

The last concern is definitely a valid one, but I think it is best addressed by commenting the code to add the reason. Basically it sounds like you're writing a workaround for a hypothetical bug, but really, you're making it harder to identify the hypothetical bug when if and when it happens.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe the last concern can be addressed by doing two things:

  • adding more comments to explain why we check for support first in plugin.js
  • check if videojs.middleware exists before trying to return the middleware.TERMINATOR here, to avoid a runtime exception breaking the whole player.

Also, this check is also done in initCancelContentPlay, do you feel the same about that?

Copy link
Contributor

Choose a reason for hiding this comment

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

"adding more comments to explain why we check for support first in plugin.js" 👍

"check if videojs.middleware exists before trying to return the middleware.TERMINATOR" That doesn't seem much different. If middleware isn't supported, we would not have gotten that far.

"Also, this check is also done in initCancelContentPlay, do you feel the same about that?" I do, yeah. It's performing a check for an unrelated feature. The logic should be hoisted to the first place that can handle it and form a bottleneck rather than spreading checks throughout various modules.

play(terminated, value) {
if (terminated) {
player.ads.debug('Play event was terminated.');
player.trigger('play');
Copy link
Contributor

Choose a reason for hiding this comment

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

I would suggest a comment explaining why we trigger play after play was terminated.

src/plugin.js Outdated
@@ -9,6 +9,7 @@ import redispatch from './redispatch.js';
import initializeContentupdate from './contentupdate.js';
import adMacroReplacement from './macros.js';
import cueTextTracks from './cueTextTracks.js';
import pm from './playMiddleware.js';
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest spelling it out as import playMiddleware from './playMiddleware.js'; to follow the project convention and make the code more readable.

src/plugin.js Outdated
@@ -120,6 +126,14 @@ const contribAdsPlugin = function(options) {
player.ads._pausedOnContentupdate = false;
});

// Keep track of whether a play event has happened
player.on('play', () => {
videojs.log('*$*', '_playRequested about to be set');
Copy link
Contributor

Choose a reason for hiding this comment

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

Some debug messages to remove still


if (player.ads.inAdBreak()) {
prefixEvent(player, 'ad', event);
} else if (player.ads.isContentResuming() || resumingAfterNoPreroll) {
} else if (player.ads.isContentResuming() ||
Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest a comment for this line

* Reset resumingAfterNoPreroll once the content actually starts playing
*/
onPlaying(player) {
this.resumingAfterNoPreroll = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe we transition to Content Mode after a playing event, so would this be the second playing event? Not sure that resumingAfterNoPreroll needs to be part of ContentPlayback and that the ad states could handle it.

Copy link
Contributor

Choose a reason for hiding this comment

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

In that case, would it work to set this.resumingAfterNoPreroll = false; in ContentPlayback.init?

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. If there is nopreroll, adtimeout e.c.t in either BeforePreroll or Preroll states, we immediately transition to ContentPlayback. This is why the resumingAfterNoPreroll flag needs to be passed into this state. Also, this is why playing will happen after the transition to ContentPlayback.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I think this further indicates an issue with how we're handling nopreroll. Normally we transition to ContentPlayback after playing, but in this case we don't. As a result, preroll logic is creeping into ContentPlayback.

What do you think of setting resumingAfterNoPreroll within Preroll, and transitioning to Preroll on nopreroll (heh) with resumingAfterNoPreroll and contentResuming both set to true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

so Preroll -> Preroll?

Honestly I think this would be a big change and wouldn't fit in the context of this PR. I think making resumingAfterNoPreroll "first-class" should be done separately, and really we shouldn't have the special resumingAfterNoPreroll flag and instead go into contentResuming until playing.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with avoiding the special resumingAfterNoPreroll flag and staying in Preroll until playing. The state transition would be BeforePreroll -> Preroll if nopreroll happened earlier. If nopreroll happened during Preroll it would stay in that state and set contentResuming to true.

The reason I think it fits into this PR is that you're creating technical debt (the preroll handling in ContentPlayback) to work around the problem. Rather than introduce new technical debt with the intent to remove it later, it's preferable to not introduce it at all.

I'm 100% down to assist in making this change.

@@ -26,8 +26,13 @@ export default class Preroll extends AdState {
player.trigger('adtimeout');
}, timeout);

if (!this.inAdBreak() && !this.isContentResuming()) {
player.ads._shouldBlockPlay = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

In init you would never be in ad break or content resuming, so this check isn't needed.

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 is mostly defensive since the flag is used for both cancelContentPlay and playMiddleware and the timing of when it is set is important.

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 would also like to avoid retesting it currently

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Todo: remove conditional check

@@ -86,6 +86,13 @@ export default class State {
return false;
}

/*
* Overriden by BeforePrerollState, PrerollState and ContentPlaybackState.
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't need State in the names

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, I was following the convention elsewhere in this file

Copy link
Contributor

Choose a reason for hiding this comment

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

It's probably my bad, I renamed them at one point during development. I think we should use the current file names.

Copy link
Contributor Author

@ldayananda ldayananda left a comment

Choose a reason for hiding this comment

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

Review comments below:

if (terminated) {
player.ads.debug('Play call to Tech was terminated.');
// Trigger play event to ensure that event order remains the same
// as with cancelContentPlay.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Change this to refer to the "intent to play" rather than refer to cancelContentPlay

src/plugin.js Outdated
@@ -9,6 +9,8 @@ import redispatch from './redispatch.js';
import initializeContentupdate from './contentupdate.js';
import adMacroReplacement from './macros.js';
import cueTextTracks from './cueTextTracks.js';
import playMiddlewareFeature from './playMiddleware.js';
const { playMiddleware, isMiddlewareMediatorSupported } = playMiddlewareFeature;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

put this below all the imports

@@ -26,8 +26,13 @@ export default class Preroll extends AdState {
player.trigger('adtimeout');
}, timeout);

if (!this.inAdBreak() && !this.isContentResuming()) {
player.ads._shouldBlockPlay = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Todo: remove conditional check

*/
obj.isMiddlewareMediatorSupported = function() {

if (videojs.browser.IS_IOS || videojs.browser.IS_ANDROID) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe we could just call load() on mobile devices to fulfill the user gesture requirement?

if (pm.isMiddlewareMediatorSupported()) {
// Don't use cancelContentPlay while playMiddleware is in use
return;
} else if (player.ads._shouldBlockPlay === 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.

Make cancelContentPlay self-sufficient. Handle play events in this file.

Alternatives:

  • add another flag for "between init and starting preroll ad break"
  • have a 3 value flag "cancelContentPlay" "middleware" "don't block"

@@ -7,16 +8,36 @@ import {ContentState, Midroll, Postroll} from '../states.js';
*/
export default class ContentPlayback extends ContentState {

init(player) {
// Play the content if cancelContentPlay happened or we paused on 'contentupdate'
init(player, resumingAfterNoPreroll) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Future issue: change the 2nd arguments to an options object

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Created #355 for this

@@ -1,4 +1,5 @@
import {ContentState, Midroll, Postroll} from '../states.js';
// import pm from '../playMiddleware.js';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Remove!

this.afterLoadStart(() => {
this.transitionTo(ContentPlayback);
});
} else {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does this change seem right @incompl? This way both endLinearAdMode and resumeAfterNoPreroll will wait for playing to transitionTo ContentPlayback

Copy link
Contributor

Choose a reason for hiding this comment

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

adserror is deprecated and I think we should focus on removing reliance on it rather than fixing it.

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 not sure I really follow what you mean. I don't want to add any more fundamental changes to this PR, so I don't plan on removing the behavior of adserror here, just making it more clear what is going on.

Copy link
Contributor

Choose a reason for hiding this comment

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

Might as well change it to match everything else, there is a risk in changing it but it's probably not high.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool, I think we agree that this change makes sense then?

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@@ -214,7 +154,7 @@ QUnit.test('changing the src triggers "contentupdate"', function(assert) {
this.player.on('contentupdate', spy);

// set src and trigger synthetic 'loadstart'
this.player.src('http://media.w3.org/2010/05/sintel/trailer.mp4');
this.player.currentSrc = () => 'AH HA!!! I AM NOT A REAL SOURCE';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@incompl do we want to change this back?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's kind of nice that this demonstrates that a source isn't really being loaded, but we could also make the string less annoying.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hehe, I'm fine with leaving it

afterEach: _.flow(restoreVideojs, sharedHooks.afterEach)
});

QUnit.test('pauses to wait for prerolls when the plugin loads BEFORE play', function(assert) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These tests were moved from test.ads.js

@ldayananda ldayananda changed the title [WIP] feat: Add Play middleware to avoid calling play on tech feat: Add Play middleware to avoid calling play on tech Apr 12, 2018
import videojs from 'video.js';

const obj = {};
let videojsReference = videojs;
Copy link
Contributor

Choose a reason for hiding this comment

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

A comment for this line would be helpful. It's not obvious why this is necessary.

// and on desktop currently(as the user-gesture requirement on mobile
// will disallow calling play once play blocking is lifted)
// The middleware must also be registered outside of the plugin,
// to avoid a middleware factory being created for each player
Copy link
Contributor

Choose a reason for hiding this comment

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

Good comment

src/plugin.js Outdated
// The middleware must also be registered outside of the plugin,
// to avoid a middleware factory being created for each player
if (isMiddlewareMediatorSupported() && settings.debug) {
videojs.log('ADS:', 'Play middleware has been registered with videojs');
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be a little tidier to move this to where middleware is actually registered.

// Loading spinner from now until ad start or end of ad break.
player.addClass('vjs-ad-loading');

// If adserror, adscanceled, nopreroll or skipLinearAdMode already
// ocurred, resume to content immediately
if (shouldResumeToContent || player.ads.nopreroll_) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I realize it may be late in the game to mess with this, but is the player.ads.nopreroll_ part still necessary? The shouldResumeToContent stuff seems pretty airtight.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The main reason for it is that we set player.ads.nopreroll_ globally after listening for the nopreroll event in plugin.js. My logic was that it's still possible for there to be a nopreroll event roughly at the same time as play/one of the other negative scenarios and so we should check just in case. If we go down the route of removing nopreroll_ from the global state and making it part of the BeforePreroll & Preroll states only, I think we would be safe to remove it then

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah that sounds like the right direction to go in. Don't need to do it now.

@incompl
Copy link
Contributor

incompl commented Apr 19, 2018

LGTM, just some minor comments

@bcsdasneogi
Copy link

QA : LGTM

@ldayananda ldayananda merged commit 1482511 into master Apr 19, 2018
@misteroneill misteroneill deleted the play-middleware branch January 24, 2019 20:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants