Skip to content

Commit

Permalink
AMP Video Players: use IntersectionObserver instead of viewportCallba…
Browse files Browse the repository at this point in the history
…ck + scroll detection. (#30647)

* amp-video: remove all traces of viewportCallback

* devAssert

* devAssert --> toWin
  • Loading branch information
samouri committed Oct 27, 2020
1 parent 75a06d6 commit 467a57a
Show file tree
Hide file tree
Showing 18 changed files with 40 additions and 136 deletions.
5 changes: 0 additions & 5 deletions extensions/amp-3q-player/0.1/amp-3q-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,6 @@ class Amp3QPlayer extends AMP.BaseElement {
return isLayoutSizeDefined(layout);
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

/** @override */
pauseCallback() {
if (this.iframe_) {
Expand Down
7 changes: 0 additions & 7 deletions extensions/amp-brightcove/0.1/amp-brightcove.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,6 @@ class AmpBrightcove extends AMP.BaseElement {
return isLayoutSizeDefined(layout);
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {
visible,
});
}

/** @override */
buildCallback() {
this.urlReplacements_ = Services.urlReplacementsForDoc(this.element);
Expand Down
5 changes: 0 additions & 5 deletions extensions/amp-dailymotion/0.1/amp-dailymotion.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,6 @@ class AmpDailymotion extends AMP.BaseElement {
return isLayoutSizeDefined(layout);
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

/** @override */
buildCallback() {
this.videoid_ = userAssert(
Expand Down
5 changes: 0 additions & 5 deletions extensions/amp-ima-video/0.1/amp-ima-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,6 @@ class AmpImaVideo extends AMP.BaseElement {
});
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

/** @override */
unlayoutCallback() {
if (this.iframe_) {
Expand Down
5 changes: 0 additions & 5 deletions extensions/amp-mowplayer/0.1/amp-mowplayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,6 @@ class AmpMowplayer extends AMP.BaseElement {
return isLayoutSizeDefined(layout);
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

/** @override */
buildCallback() {
this.mediaid_ = userAssert(
Expand Down
5 changes: 0 additions & 5 deletions extensions/amp-nexxtv-player/0.1/amp-nexxtv-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,6 @@ class AmpNexxtvPlayer extends AMP.BaseElement {
return assertAbsoluteHttpOrHttpsUrl(src);
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

/** @override */
layoutCallback() {
const iframe = createFrameFor(this, this.getVideoIframeSrc_());
Expand Down
5 changes: 0 additions & 5 deletions extensions/amp-ooyala-player/0.1/amp-ooyala-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,6 @@ class AmpOoyalaPlayer extends AMP.BaseElement {
return isLayoutSizeDefined(layout);
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

/** @override */
pauseCallback() {
// Only send pauseVideo command if the player is playing. Otherwise
Expand Down
1 change: 1 addition & 0 deletions extensions/amp-ooyala-player/0.1/test/test-amp-ooyala.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ describes.realWin(
opt_placeholder
) {
const player = doc.createElement('amp-ooyala-player');
player.setAttribute('layout', 'fixed');
if (embedCode) {
player.setAttribute('data-embedcode', embedCode);
}
Expand Down
7 changes: 0 additions & 7 deletions extensions/amp-powr-player/0.1/amp-powr-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,6 @@ class AmpPowrPlayer extends AMP.BaseElement {
return isLayoutSizeDefined(layout);
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {
visible,
});
}

/** @override */
buildCallback() {
this.urlReplacements_ = Services.urlReplacementsForDoc(this.element);
Expand Down
5 changes: 0 additions & 5 deletions extensions/amp-video/0.1/amp-video.js
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,6 @@ class AmpVideo extends AMP.BaseElement {
// while the video is playing).
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

/** @override */
layoutCallback() {
this.video_ = dev().assertElement(this.video_);
Expand Down
5 changes: 0 additions & 5 deletions extensions/amp-wistia-player/0.1/amp-wistia-player.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,6 @@ class AmpWistiaPlayer extends AMP.BaseElement {
return true;
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

/** @override */
pauseCallback() {
if (this.iframe_) {
Expand Down
5 changes: 0 additions & 5 deletions extensions/amp-youtube/0.1/amp-youtube.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,6 @@ class AmpYoutube extends AMP.BaseElement {
return 0.75;
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

/** @override */
buildCallback() {
this.videoid_ = this.getVideoId_();
Expand Down
2 changes: 1 addition & 1 deletion extensions/amp-youtube/0.1/test/test-amp-youtube.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ describes.realWin(
);
});

it('loads only sddefault when it exists if source is videoid', async () => {
it('loads only default when it exists if source is videoid', async () => {
const yt = await getYt({'data-videoid': EXAMPLE_VIDEOID}, true, function (
yt
) {
Expand Down
94 changes: 30 additions & 64 deletions src/service/video-manager-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {VideoSessionManager} from './video-session-manager';
import {VideoUtils, getInternalVideoElementFor} from '../utils/video';
import {clamp} from '../utils/math';
import {createCustomEvent, getData, listen, listenOnce} from '../event-helper';
import {createViewportObserver} from '../viewport-observer';
import {dev, devAssert, user, userAssert} from '../log';
import {dict, map} from '../utils/object';
import {getMode} from '../mode';
Expand Down Expand Up @@ -81,22 +82,19 @@ export class VideoManager {
installAutoplayStylesForDoc(this.ampdoc)
);

/** @private {!../service/viewport/viewport-interface.ViewportInterface} */
this.viewport_ = Services.viewportForDoc(this.ampdoc);

/** @private {?Array<!VideoEntry>} */
this.entries_ = null;

/** @private {IntersectionObserver} */
this.viewportObserver_ = null;

/**
* Keeps last found entry as a small optimization for multiple state calls
* during one task.
* @private {?VideoEntry}
*/
this.lastFoundEntry_ = null;

/** @private {boolean} */
this.scrollListenerInstalled_ = false;

/** @private @const */
this.timer_ = Services.timerFor(ampdoc.win);

Expand Down Expand Up @@ -124,6 +122,8 @@ export class VideoManager {
/** @override */
dispose() {
this.getAutoFullscreenManager_().dispose();
this.viewportObserver_.disconnect();
this.viewportObserver_ = null;

if (!this.entries_) {
return;
Expand Down Expand Up @@ -176,19 +176,37 @@ export class VideoManager {
}
}

// TODO(#30723): create unregister() for cleanup.
/** @param {!../video-interface.VideoInterface} video */
register(video) {
devAssert(video);
const videoBE = /** @type {!AMP.BaseElement} */ (video);

this.registerCommonActions_(video);

if (!video.supportsPlatform()) {
return;
}
if (!this.viewportObserver_) {
const viewportCallback = (
/** @type {!Array<!IntersectionObserverEntry>} */ records
) =>
records.forEach(({target, isIntersecting}) => {
this.getEntry_(target).updateVisibility(
/* isVisible */ isIntersecting
);
});
this.viewportObserver_ = createViewportObserver(
viewportCallback,
this.ampdoc.win,
/* threshold */ MIN_VISIBILITY_RATIO_FOR_AUTOPLAY
);
}
this.viewportObserver_.observe(videoBE.element);
listen(videoBE.element, VideoEvents.RELOAD, () => entry.videoLoaded());

this.entries_ = this.entries_ || [];
const entry = new VideoEntry(this, video);
this.maybeInstallVisibilityObserver_(entry);
this.entries_.push(entry);

const {element} = entry.video;
Expand Down Expand Up @@ -249,45 +267,6 @@ export class VideoManager {
}
}

/**
* Install the necessary listeners to be notified when a video becomes visible
* in the viewport.
*
* Visibility of a video is defined by being in the viewport AND having
* {@link MIN_VISIBILITY_RATIO_FOR_AUTOPLAY} of the video element visible.
*
* @param {VideoEntry} entry
* @private
*/
maybeInstallVisibilityObserver_(entry) {
const {element} = entry.video;

listen(element, VideoEvents.VISIBILITY, (details) => {
const data = getData(details);
if (data && data['visible'] == true) {
entry.updateVisibility(/* opt_forceVisible */ true);
} else {
entry.updateVisibility();
}
});

listen(element, VideoEvents.RELOAD, () => {
entry.videoLoaded();
});

// TODO(aghassemi, #6425): Use IntersectionObserver
if (!this.scrollListenerInstalled_) {
const scrollListener = () => {
for (let i = 0; i < this.entries_.length; i++) {
this.entries_[i].updateVisibility();
}
};
this.viewport_.onScroll(scrollListener);
this.viewport_.onChanged(scrollListener);
this.scrollListenerInstalled_ = true;
}
}

/**
* Returns the entry in the video manager corresponding to the video or
* element provided
Expand Down Expand Up @@ -622,7 +601,6 @@ class VideoEntry {
this.manager_.registerForAutoFullscreen(this);
}

this.updateVisibility();
if (this.hasAutoplay) {
this.autoplayVideoBuilt_();
}
Expand Down Expand Up @@ -719,7 +697,6 @@ class VideoEntry {

this.getAnalyticsPercentageTracker_().start();

this.updateVisibility();
if (this.isVisible_) {
// Handles the case when the video becomes visible before loading
this.loadedVideoVisibilityChanged_();
Expand Down Expand Up @@ -948,25 +925,14 @@ class VideoEntry {
}

/**
* Called by all possible events that might change the visibility of the video
* such as scrolling or {@link ../video-interface.VideoEvents#VISIBILITY}.
* @param {?boolean=} opt_forceVisible
* Called by an IntersectionObserver.
* @param {boolean} isVisible
* @package
*/
updateVisibility(opt_forceVisible) {
updateVisibility(isVisible) {
const wasVisible = this.isVisible_;

if (opt_forceVisible) {
this.isVisible_ = true;
} else {
const {element} = this.video;
const ratio = element.getIntersectionChangeEntry().intersectionRatio;
this.isVisible_ =
(!isFiniteNumber(ratio) ? 0 : ratio) >=
MIN_VISIBILITY_RATIO_FOR_AUTOPLAY;
}

if (this.isVisible_ != wasVisible) {
this.isVisible_ = isVisible;
if (isVisible != wasVisible) {
this.videoVisibilityChanged_();
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/viewport-observer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import {devAssert} from '../src/log';
import {getMode} from './mode';
import {isIframed} from './dom';
import {toWin} from '../src/types';

/**
* Returns an IntersectionObserver tracking the Viewport.
Expand Down Expand Up @@ -76,7 +77,7 @@ export function observeWithSharedInOb(element, viewportCallback) {
);
}

const win = element.ownerDocument.defaultView;
const win = toWin(element.ownerDocument.defaultView);
let viewportObserver = viewportObservers.get(win);
if (!viewportObserver) {
viewportObservers.set(
Expand All @@ -93,7 +94,7 @@ export function observeWithSharedInOb(element, viewportCallback) {
* @param {!Element} element
*/
export function unobserveWithSharedInOb(element) {
const win = element.ownerDocument.defaultView;
const win = toWin(element.ownerDocument.defaultView);
const viewportObserver = viewportObservers.get(win);
if (viewportObserver) {
viewportObserver.unobserve(element);
Expand Down
9 changes: 1 addition & 8 deletions test/integration/test-video-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -301,9 +301,7 @@ describe
},
};

win = {
document: doc,
};
win = {document: doc};

isLite = false;

Expand Down Expand Up @@ -439,11 +437,6 @@ function createFakeVideoPlayerClass(win) {
});
}

/** @override */
viewportCallback(visible) {
this.element.dispatchCustomEvent(VideoEvents.VISIBILITY, {visible});
}

// VideoInterface Implementation. See ../src/video-interface.VideoInterface

/**
Expand Down
4 changes: 2 additions & 2 deletions test/integration/test-video-players-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ export function runVideoPlayerIntegrationTests(
);
});

it('should not play when not in view port initially', () => {
it('should not play when initially outside viewport', () => {
return getVideoPlayer({outsideView: true, autoplay: true}).then((r) => {
const timer = Services.timerFor(r.video.implementation_.win);
const p = listenOncePromise(r.video, VideoEvents.PLAYING).then(() => {
Expand Down Expand Up @@ -551,7 +551,7 @@ export function runVideoPlayerIntegrationTests(

function getVideoPlayer(options) {
options = options || {};
const top = options.outsideView ? '100vh' : '0';
const top = options.outsideView ? '126vh' : '0';
let fixture;
return createFixtureIframe('test/fixtures/video-players.html', FRAME_HEIGHT)
.then((f) => {
Expand Down

0 comments on commit 467a57a

Please sign in to comment.