Skip to content

Commit

Permalink
feat: playerresize event in all cases (#4864)
Browse files Browse the repository at this point in the history
Use ResizeObserver when available for better and more performant resizing information, otherwise, fall back to a throttled resize event on an iframe that's the size of the player.
Allows a video.js user to disable this by setting resizeManager: false as an option since the component will not be initialized.

Add a debounce util.

This reverts #4800 (e0ed0b5) because we end up getting two playerresize events with the dimension methods now.
  • Loading branch information
gkatsev authored Jan 30, 2018
1 parent 6a00577 commit 9ceb4e4
Show file tree
Hide file tree
Showing 17 changed files with 436 additions and 60 deletions.
38 changes: 37 additions & 1 deletion docs/guides/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ The architecture of the Video.js player is centered around components. The `Play
* [Specific Component Details](#specific-component-details)
* [Volume Panel](#volume-panel)
* [Text Track Settings](#text-track-settings)
* [Resize Manager](#resize-manager)

## What is a Component?

Expand Down Expand Up @@ -312,7 +313,8 @@ Player
│ ├── AudioTrackButton (hidden, unless there are relevant tracks)
│ └── FullscreenToggle
├── ErrorDisplay (hidden, until there is an error)
└── TextTrackSettings
├── TextTrackSettings
└── ResizeManager (hidden)
```

## Specific Component Details
Expand All @@ -338,3 +340,37 @@ let player = videojs('myplayer', {
The text track settings component is only available when using emulated text tracks.

[api]: http://docs.videojs.com/Component.html

### Resize Manager

This new component is in charge of triggering a `playerresize` event when the player size changed.
It uses the ResizeObserver if available or a polyfill was provided. It has no element when using the ResizeObserver.
If a ResizeObserver is not available, it will fallback to an iframe element and listen to its resize event via a debounced handler.

A ResizeObserver polyfill can be passed in like so:

```js
var player = videojs('myplayer', {
resizeManager: {
ResizeObserver: ResizeObserverPoylfill
}
});
```

To force using the iframe fallback, pass in `null` as the `ResizeObserver`:

```js
var player = videojs('myplayer', {
resizeManager: {
ResizeObserver: null
}
});
```

The ResizeManager can also just be disabled like so:

```js
var player = videojs('myplayer', {
resizeManager: false
});
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"docs:api": "jsdoc -c .jsdoc.json",
"postdocs:api": "node ./build/fix-api-docs.js",
"netlify": "babel-node ./build/netlify-docs.js",
"docs:lint": "remark -- './**/*.md'",
"docs:fix": "remark --output -- './**/*.md'",
"docs:lint": "remark -- './{,!(node_modules)/**/}!(CHANGELOG)*.md'",
"docs:fix": "remark --output -- './{,!(node_modules)/**/}!(CHANGELOG)*.md'",
"babel": "babel src/js -d es5",
"prepublish": "not-in-install && run-p build || in-install",
"publish": "node build/gh-release.js",
Expand Down
10 changes: 10 additions & 0 deletions src/css/video-js.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,13 @@
@import "components/captions-settings";

@import "print";

.vjs-resize-manager {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
visibility: hidden;
}
37 changes: 10 additions & 27 deletions src/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import './close-button.js';
import './control-bar/control-bar.js';
import './error-display.js';
import './tracks/text-track-settings.js';
import './resize-manager.js';

// Import Html5 tech, at least for disposing the original video tag.
import './tech/html5.js';
Expand Down Expand Up @@ -661,14 +662,11 @@ class Player extends Component {
* @param {number} [value]
* The value to set the `Player`'s width to.
*
* @param {boolean} [skipListeners]
* Skip the playerresize event trigger
*
* @return {number}
* The current width of the `Player` when getting.
*/
width(value, skipListeners) {
return this.dimension('width', value, skipListeners);
width(value) {
return this.dimension('width', value);
}

/**
Expand All @@ -678,21 +676,16 @@ class Player extends Component {
* @param {number} [value]
* The value to set the `Player`'s heigth to.
*
* @param {boolean} [skipListeners]
* Skip the playerresize event trigger
*
* @return {number}
* The current height of the `Player` when getting.
*/
height(value, skipListeners) {
return this.dimension('height', value, skipListeners);
height(value) {
return this.dimension('height', value);
}

/**
* A getter/setter for the `Player`'s width & height.
*
* @fires Player#playerresize
*
* @param {string} dimension
* This string can be:
* - 'width'
Expand All @@ -701,13 +694,10 @@ class Player extends Component {
* @param {number} [value]
* Value for dimension specified in the first argument.
*
* @param {boolean} [skipListeners]
* Skip the playerresize event trigger
*
* @return {number}
* The dimension arguments value when getting (width/height).
*/
dimension(dimension, value, skipListeners) {
dimension(dimension, value) {
const privDimension = dimension + '_';

if (value === undefined) {
Expand All @@ -730,17 +720,6 @@ class Player extends Component {

this[privDimension] = parsedVal;
this.updateStyleEl_();

// skipListeners allows us to avoid triggering the resize event when setting both width and height
if (this.isReady_ && !skipListeners) {
/**
* Triggered when the player is resized.
*
* @event Player#playerresize
* @type {EventTarget~Event}
*/
this.trigger('playerresize');
}
}

/**
Expand Down Expand Up @@ -3489,6 +3468,10 @@ Player.prototype.options_ = {
notSupportedMessage: 'No compatible source was found for this media.'
};

if (!browser.IS_IE8) {
Player.prototype.options_.children.push('resizeManager');
}

[
/**
* Returns whether or not the player is in the "ended" state.
Expand Down
121 changes: 121 additions & 0 deletions src/js/resize-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @file resize-manager.js
*/
import window from 'global/window';
import { debounce } from './utils/fn.js';
import * as Events from './utils/events.js';
import mergeOptions from './utils/merge-options.js';
import Component from './component.js';

/**
* A Resize Manager. It is in charge of triggering `playerresize` on the player in the right conditions.
*
* It'll either create an iframe and use a debounced resize handler on it or use the new {@link https://wicg.github.io/ResizeObserver/|ResizeObserver}.
*
* If the ResizeObserver is available natively, it will be used. A polyfill can be passed in as an option.
* If a `playerresize` event is not needed, the ResizeManager component can be removed from the player, see the example below.
* @example <caption>How to disable the resize manager</caption>
* const player = videojs('#vid', {
* resizeManager: false
* });
*
* @see {@link https://wicg.github.io/ResizeObserver/|ResizeObserver specification}
*
* @extends Component
*/
class ResizeManager extends Component {

/**
* Create the ResizeManager.
*
* @param {Object} player
* The `Player` that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of ResizeManager options.
*
* @param {Object} [options.ResizeObserver]
* A polyfill for ResizeObserver can be passed in here.
* If this is set to null it will ignore the native ResizeObserver and fall back to the iframe fallback.
*/
constructor(player, options) {
let RESIZE_OBSERVER_AVAILABLE = options.ResizeObserver || window.ResizeObserver;

// if `null` was passed, we want to disable the ResizeObserver
if (options.ResizeObserver === null) {
RESIZE_OBSERVER_AVAILABLE = false;
}

// Only create an element when ResizeObserver isn't available
const options_ = mergeOptions({createEl: !RESIZE_OBSERVER_AVAILABLE}, options);

super(player, options_);

this.ResizeObserver = options.ResizeObserver || window.ResizeObserver;
this.loadListener_ = null;
this.resizeObserver_ = null;
this.debouncedHandler_ = debounce(() => {
this.resizeHandler();
}, 100, false, player);

if (RESIZE_OBSERVER_AVAILABLE) {
this.resizeObserver_ = new this.ResizeObserver(this.debouncedHandler_);
this.resizeObserver_.observe(player.el());

} else {
this.loadListener_ = () => {
if (this.el_.contentWindow) {
Events.on(this.el_.contentWindow, 'resize', this.debouncedHandler_);
}
this.off('load', this.loadListener_);
};

this.on('load', this.loadListener_);
}
}

createEl() {
return super.createEl('iframe', {
className: 'vjs-resize-manager'
});
}

/**
* Called when a resize is triggered on the iframe or a resize is observed via the ResizeObserver
*
* @fires Player#playerresize
*/
resizeHandler() {
/**
* Called when the player size has changed
*
* @event Player#playerresize
* @type {EventTarget~Event}
*/
this.player_.trigger('playerresize');
}

dispose() {
if (this.resizeObserver_) {
this.resizeObserver_.unobserve(this.player_.el());
this.resizeObserver_.disconnect();
}

if (this.el_ && this.el_.contentWindow) {
Events.off(this.el_.contentWindow, 'resize', this.debouncedHandler_);
}

if (this.loadListener_) {
this.off('load', this.loadListener_);
}

this.ResizeObserver = null;
this.resizeObserver = null;
this.debouncedHandler_ = null;
this.loadListener_ = null;
}

}

Component.registerComponent('ResizeManager', ResizeManager);
export default ResizeManager;
52 changes: 52 additions & 0 deletions src/js/utils/fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* @module fn
*/
import { newGUID } from './guid.js';
import window from 'global/window';

/**
* Bind (a.k.a proxy or Context). A simple method for changing the context of a function
Expand Down Expand Up @@ -68,3 +69,54 @@ export const throttle = function(fn, wait) {

return throttled;
};

/**
* Creates a debounced function that delays invoking `func` until after `wait`
* milliseconds have elapsed since the last time the debounced function was
* invoked.
*
* Inspired by lodash and underscore implementations.
*
* @param {Function} func
* The function to wrap with debounce behavior.
*
* @param {number} wait
* The number of milliseconds to wait after the last invocation.
*
* @param {boolean} [immediate]
* Whether or not to invoke the function immediately upon creation.
*
* @param {Object} [context=window]
* The "context" in which the debounced function should debounce. For
* example, if this function should be tied to a Video.js player,
* the player can be passed here. Alternatively, defaults to the
* global `window` object.
*
* @return {Function}
* A debounced function.
*/
export const debounce = function(func, wait, immediate, context = window) {
let timeout;

/* eslint-disable consistent-this */
return function() {
const self = this;
const args = arguments;

let later = function() {
timeout = null;
later = null;
if (!immediate) {
func.apply(self, args);
}
};

if (!timeout && immediate) {
func.apply(self, args);
}

context.clearTimeout(timeout);
timeout = context.setTimeout(later, wait);
};
/* eslint-enable consistent-this */
};
4 changes: 4 additions & 0 deletions test/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ QUnit.test('should be able to access expected component API methods', function(a
assert.ok(comp.clearInterval, 'clearInterval exists');
assert.ok(comp.setTimeout, 'setTimeout exists');
assert.ok(comp.clearTimeout, 'clearTimeout exists');

comp.dispose();
});

QUnit.test('should be able to access expected MediaTech API methods', function(assert) {
Expand Down Expand Up @@ -289,4 +291,6 @@ QUnit.test('should extend Component', function(assert) {
const noMethods = new NoMethods({});

assert.ok(noMethods.on, 'should extend component with no methods or constructor');

myComponent.dispose();
});
3 changes: 2 additions & 1 deletion test/unit/button.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ QUnit.test('should localize its text', function(assert) {
assert.ok(el.nodeName.toLowerCase().match('button'));
assert.ok(el.innerHTML.match(/vjs-control-text"?>Juego/));
assert.equal(el.getAttribute('title'), 'Juego');
player.dispose();

testButton.dispose();
player.dispose();
});
Loading

0 comments on commit 9ceb4e4

Please sign in to comment.