From 9cbf0cfea35fd7a1f111fa1798f2b54a7ad39430 Mon Sep 17 00:00:00 2001 From: Dan Ziv Date: Mon, 23 Nov 2020 10:02:15 +0200 Subject: [PATCH] feat(FEC-10632): handle player dimensions dynamically (#506) --- docs/configuration.md | 52 ++++++++++++- flow-typed/types/dimensions.js | 10 +++ src/player.js | 67 +++++++++++++++- test/src/dimensions.spec.js | 138 +++++++++++++++++++++++++++++++++ test/src/utils/test-utils.js | 3 +- 5 files changed, 266 insertions(+), 4 deletions(-) create mode 100644 flow-typed/types/dimensions.js create mode 100644 test/src/dimensions.spec.js diff --git a/docs/configuration.md b/docs/configuration.md index e8a469150..e6348db23 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -20,7 +20,8 @@ var player = playkit.core.loadPlayer(config); network: PKNetworkConfigObject, customLabels: PKCustomLabelsConfigObject, abr: PKAbrConfigObject, - drm: PKDrmConfigObject + drm: PKDrmConfigObject, + dimensions: PKDimensionsConfig } ``` @@ -1386,4 +1387,53 @@ var config = { ## +> ### config.dimensions +> +> ##### Type: `PKDimensionsConfig` +> +> ```js +> { +> width?: string | number; +> height?: string | number; +> ratio?: string; +> } +> ``` +> +> ##### Description: Dimensions configuration +> +> > ### config.dimensions.width +> > +> > ##### Type: `string | number` +> > +> > ##### Default: `''` +> > +> > ##### Description: The width of the player. +> > +> > If number was provided, the width will be calculated in pixels (`width: 640` equivalent to `width: '640px'`). +> > If string was provided, any valid css syntax can be passed, for example: `width: '100%'`, `width: 'auto'`, etc. +> > +> > ### config.dimensions.height +> > +> > ##### Type: `string | number` +> > +> > ##### Default: `''` +> > +> > ##### Description: The height of the player. +> > +> > If number was provided, the height will be calculated in pixels (`height: 360` equivalent to `width: '360px'`). +> > If string was provided, any valid css syntax can be passed, for example: `height: '100%'`, `height: 'auto'`, etc. +> > +> > ### config.dimensions.ratio +> > +> > ##### Type: `string` +> > +> > ##### Default: `''` +> > +> > ##### Description: Defines the aspect ratio of the player. +> > +> > The aspect ratio should be written in the form of `'width:height'`, for example: `'4:3'` (classic TV ratio). +> > If one of the `height` or `width` parameters is additionally provided in the configuration, the value of the other parameter not provided will be calculated accordingly to match the aspect ratio. If both were provided, the `height` value would be overridden. + +## + Now that we've learned about the different options available in the player configuration, let's see [how does the source selection logic works](./source-selection-logic.md). diff --git a/flow-typed/types/dimensions.js b/flow-typed/types/dimensions.js new file mode 100644 index 000000000..20d10fdad --- /dev/null +++ b/flow-typed/types/dimensions.js @@ -0,0 +1,10 @@ +declare type PKPlayerDimensions = { + width: number, + height: number +}; + +declare type PKDimensionsConfig = { + width?: number | string, + height?: number | string, + ratio?: string +}; diff --git a/src/player.js b/src/player.js index b6fe665f1..c3dc945bc 100644 --- a/src/player.js +++ b/src/player.js @@ -394,6 +394,12 @@ export default class Player extends FakeEventTarget { * @private */ _shouldLoadAfterAttach: boolean = false; + /** + * The aspect ratio of the player. + * @type {?string} + * @private + */ + _aspectRatio: ?string; /** * @param {Object} config - The configuration for the player instance. @@ -454,6 +460,7 @@ export default class Player extends FakeEventTarget { this._attachMedia(); this._handlePlaybackOptions(); this._posterManager.setSrc(this._config.sources.poster); + this._handleDimensions(); this._handlePreload(); this._handleAutoPlay(); Player._logger.debug('Change source ended'); @@ -881,12 +888,32 @@ export default class Player extends FakeEventTarget { return null; } + /** + * Sets the dimensions of the player. + * @param {PKDimensionsConfig} dimensions - the player dimensions config. + * @returns {void} + * @public + */ + set dimensions(dimensions?: PKDimensionsConfig) { + const targetElement = document.getElementById(this.config.targetId); + if (!dimensions || Utils.Object.isEmptyObject(dimensions)) { + this._aspectRatio = null; + targetElement.style.width = null; + targetElement.style.height = null; + } else { + const {height, width} = Utils.Object.mergeDeep(this.dimensions, dimensions); + targetElement.style.width = typeof width === 'number' ? `${width}px` : width; + targetElement.style.height = typeof height === 'number' ? `${height}px` : height; + this._calcRatio(targetElement, dimensions); + } + } + /** * Get the dimensions of the player. - * @returns {{width: number, height: number}} - The dimensions of the player. + * @returns {PKPlayerDimensions} - The dimensions of the player. * @public */ - get dimensions(): Object { + get dimensions(): PKPlayerDimensions { return { width: this._el.clientWidth, height: this._el.clientHeight @@ -1914,6 +1941,18 @@ export default class Player extends FakeEventTarget { } } + /** + * Handles and sets the initial dimensions configuration if such exists. + * @private + * @returns {void} + */ + _handleDimensions(): void { + const {dimensions} = this.config; + if (Utils.Object.isObject(dimensions) && !Utils.Object.isEmptyObject(dimensions)) { + this.dimensions = dimensions; + } + } + /** * Start/resume the engine playback. * @private @@ -2010,6 +2049,7 @@ export default class Player extends FakeEventTarget { this._pause(); } } + /** * Resets the state flags of the player. * @returns {void} @@ -2023,6 +2063,29 @@ export default class Player extends FakeEventTarget { this._firstPlaying = false; } + /** + * Calculates the aspect ratio of the player. + * @param {HTMLDivElement} targetElement - the player root element. + * @param {PKDimensionsConfig} dimensions - the player dimensions input. + * @returns {void} + * @public + */ + _calcRatio(targetElement: HTMLDivElement, dimensions: PKDimensionsConfig) { + if (typeof dimensions.ratio !== 'undefined') { + this._aspectRatio = dimensions.ratio; + } + if (this._aspectRatio) { + const [ratioWidth, ratioHeight] = this._aspectRatio.split(':').map(r => Number(r)); + if (dimensions.width || (!dimensions.width && !dimensions.height)) { + const height = (ratioHeight / ratioWidth) * targetElement.clientWidth; + targetElement.style.height = `${height}px`; + } else if (dimensions.height && !dimensions.width) { + const width = (ratioWidth / ratioHeight) * targetElement.clientHeight; + targetElement.style.width = `${width}px`; + } + } + } + /** * @returns {Object} - The default configuration of the player. * @private diff --git a/test/src/dimensions.spec.js b/test/src/dimensions.spec.js new file mode 100644 index 000000000..43b7cda5c --- /dev/null +++ b/test/src/dimensions.spec.js @@ -0,0 +1,138 @@ +import {createElement, getConfigStructure, removeElement, removeVideoElementsFromTestPage} from './utils/test-utils'; +import Player from '../../src/player'; +import {Object as PKObject} from '../../src/utils'; +import SourcesConfig from './configs/sources.json'; + +const targetId = 'player-placeholder_dimensions.spec'; +const sourcesConfig = PKObject.copyDeep(SourcesConfig); + +describe('Dimensions API ', function () { + const origConfig = getConfigStructure(targetId); + const playerContainer = createElement('DIV', targetId); + let player, config; + + before(() => { + origConfig.sources = sourcesConfig.MultipleSources; + }); + + const createPlayer = config => { + player = new Player(config); + playerContainer.appendChild(player.getView()); + }; + + beforeEach(() => { + config = PKObject.copyDeep(origConfig); + }); + + afterEach(() => { + player.destroy(); + }); + + after(() => { + removeVideoElementsFromTestPage(); + removeElement(targetId); + }); + + it('should calc height to match configure ratio', () => { + config.dimensions = { + ratio: '4:3', + width: 100 + }; + createPlayer(config); + player.dimensions.should.deep.equals({width: 100, height: 75}); + }); + + it('should calc width to match configure ratio', () => { + config.dimensions = { + ratio: '4:3', + height: 75 + }; + createPlayer(config); + player.dimensions.should.deep.equals({width: 100, height: 75}); + }); + + it('should calc width and override height to match configure ratio', () => { + config.dimensions = { + ratio: '4:3', + width: 100, + height: 100 + }; + createPlayer(config); + player.dimensions.should.deep.equals({width: 100, height: 75}); + }); + + it('should set the configure width and height', () => { + config.dimensions = { + width: 100, + height: 100 + }; + createPlayer(config); + player.dimensions.should.deep.equals({width: 100, height: 100}); + }); + + it('should dynamically change the width and height', () => { + config.dimensions = { + width: 100, + height: 100 + }; + createPlayer(config); + player.dimensions.should.deep.equals({width: 100, height: 100}); + player.dimensions = {height: 50, width: 70}; + player.dimensions.should.deep.equals({height: 50, width: 70}); + player.dimensions = {height: 60}; + player.dimensions.should.deep.equals({height: 60, width: 70}); + player.dimensions = {width: 200}; + player.dimensions.should.deep.equals({height: 60, width: 200}); + }); + + it('should reset width and height', done => { + config.dimensions = { + width: 100, + height: 100 + }; + createPlayer(config); + player.play(); + player.ready().then(() => { + player.dimensions.should.deep.equals({width: 100, height: 100}); + player.dimensions = {}; + player.dimensions.width.should.equals(window.innerWidth); + player.dimensions = {width: 90, height: 80}; + player.dimensions.should.deep.equals({width: 90, height: 80}); + player.dimensions = null; + player.dimensions.width.should.equals(window.innerWidth); + done(); + }); + }); + + it('should change ratio dynamically', () => { + config.dimensions = { + ratio: '4:3', + width: 100 + }; + createPlayer(config); + player.dimensions.should.deep.equals({width: 100, height: 75}); + player.dimensions = {ratio: '9:2'}; + player.dimensions.should.deep.equals({width: 100, height: 22}); + player.dimensions = {height: 100}; + player.dimensions.should.deep.equals({width: 450, height: 100}); + }); + + it('should reset ratio dynamically', () => { + config.dimensions = { + ratio: '4:3', + width: 100 + }; + createPlayer(config); + player.dimensions.should.deep.equals({width: 100, height: 75}); + player.dimensions = {ratio: ''}; + player.dimensions.should.deep.equals({width: 100, height: 75}); + player.dimensions = {height: 100}; + player.dimensions.should.deep.equals({width: 100, height: 100}); + player.dimensions = {ratio: '4:3'}; + player.dimensions.should.deep.equals({width: 100, height: 75}); + player.dimensions = {ratio: null}; + player.dimensions.should.deep.equals({width: 100, height: 75}); + player.dimensions = {height: 100}; + player.dimensions.should.deep.equals({width: 100, height: 100}); + }); +}); diff --git a/test/src/utils/test-utils.js b/test/src/utils/test-utils.js index 694ce71a4..13a19ac27 100644 --- a/test/src/utils/test-utils.js +++ b/test/src/utils/test-utils.js @@ -2,8 +2,9 @@ * Configuration structure of the player. * @returns {Object} - The configuration structure of the player. */ -function getConfigStructure() { +function getConfigStructure(targetId) { return { + targetId, sources: {}, playback: { enableCEA708Captions: true,