-
Notifications
You must be signed in to change notification settings - Fork 256
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
Conversation
@@ -315,6 +325,7 @@ const contribAdsPlugin = function(options) { | |||
}; | |||
|
|||
player.ads._state = new BeforePreroll(player); | |||
player.ads._state.init(player); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup makes total sense.
src/states/BeforePreroll.js
Outdated
@@ -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()) { |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/cancelContentPlay.js
Outdated
// 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 |
There was a problem hiding this comment.
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."
src/cancelContentPlay.js
Outdated
// another cancellation is already in flight, so do nothing | ||
return; | ||
} | ||
|
||
player.ads.debug('Using cancelContentPlay to block content playback'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
src/playMiddleware.js
Outdated
}, | ||
callPlay() { | ||
// Block play calls while waiting for an ad | ||
if (obj.isMiddlewareMediatorSupported() && |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/playMiddleware.js
Outdated
play(terminated, value) { | ||
if (terminated) { | ||
player.ads.debug('Play event was terminated.'); | ||
player.trigger('play'); |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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
src/redispatch.js
Outdated
|
||
if (player.ads.inAdBreak()) { | ||
prefixEvent(player, 'ad', event); | ||
} else if (player.ads.isContentResuming() || resumingAfterNoPreroll) { | ||
} else if (player.ads.isContentResuming() || |
There was a problem hiding this comment.
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
src/states/ContentPlayback.js
Outdated
* Reset resumingAfterNoPreroll once the content actually starts playing | ||
*/ | ||
onPlaying(player) { | ||
this.resumingAfterNoPreroll = false; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
src/states/Preroll.js
Outdated
@@ -26,8 +26,13 @@ export default class Preroll extends AdState { | |||
player.trigger('adtimeout'); | |||
}, timeout); | |||
|
|||
if (!this.inAdBreak() && !this.isContentResuming()) { | |||
player.ads._shouldBlockPlay = true; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Todo: remove conditional check
src/states/abstract/State.js
Outdated
@@ -86,6 +86,13 @@ export default class State { | |||
return false; | |||
} | |||
|
|||
/* | |||
* Overriden by BeforePrerollState, PrerollState and ContentPlaybackState. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review comments below:
src/playMiddleware.js
Outdated
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. |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
src/states/Preroll.js
Outdated
@@ -26,8 +26,13 @@ export default class Preroll extends AdState { | |||
player.trigger('adtimeout'); | |||
}, timeout); | |||
|
|||
if (!this.inAdBreak() && !this.isContentResuming()) { | |||
player.ads._shouldBlockPlay = true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Todo: remove conditional check
src/playMiddleware.js
Outdated
*/ | ||
obj.isMiddlewareMediatorSupported = function() { | ||
|
||
if (videojs.browser.IS_IOS || videojs.browser.IS_ANDROID) { |
There was a problem hiding this comment.
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?
src/cancelContentPlay.js
Outdated
if (pm.isMiddlewareMediatorSupported()) { | ||
// Don't use cancelContentPlay while playMiddleware is in use | ||
return; | ||
} else if (player.ads._shouldBlockPlay === false) { |
There was a problem hiding this comment.
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"
src/states/ContentPlayback.js
Outdated
@@ -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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created #355 for this
src/states/ContentPlayback.js
Outdated
@@ -1,4 +1,5 @@ | |||
import {ContentState, Midroll, Postroll} from '../states.js'; | |||
// import pm from '../playMiddleware.js'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove!
… return the terminator if it is supported
…thod for testing/debugging purposes. More play middleware unit tests
…ported. Update unit tests. Lock down integration tests
this.afterLoadStart(() => { | ||
this.transitionTo(ContentPlayback); | ||
}); | ||
} else { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
src/playMiddleware.js
Outdated
import videojs from 'video.js'; | ||
|
||
const obj = {}; | ||
let videojsReference = videojs; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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_) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
LGTM, just some minor comments |
QA : LGTM |
Limitations: can't be used on mobile platforms and is only available for use on later versions of videojs.
adplaying
handlingTodos: