diff --git a/docs/guides/options.md b/docs/guides/options.md index 3aecf6bac..9216f6cd3 100644 --- a/docs/guides/options.md +++ b/docs/guides/options.md @@ -29,6 +29,7 @@ * [languages](#languages) * [nativeControlsForTouch](#nativecontrolsfortouch) * [notSupportedMessage](#notsupportedmessage) + * [fullscreenOptions](#fullscreenoptions) * [playbackRates](#playbackrates) * [plugins](#plugins) * [responsive](#responsive) @@ -277,6 +278,13 @@ Explicitly set a default value for [the associated tech option](#nativecontrolsf Allows overriding the default message that is displayed when Video.js cannot play back a media source. +### `fullscreenOptions` + +> Type: `Object` +> Default: `{navigationUI: 'hide'}` + +See [The Fullscreen API Spec](https://fullscreen.spec.whatwg.org/#dictdef-fullscreenoptions) for more details. + ### `playbackRates` > Type: `Array` diff --git a/src/js/control-bar/fullscreen-toggle.js b/src/js/control-bar/fullscreen-toggle.js index 45987a78d..bf7dbf5d6 100644 --- a/src/js/control-bar/fullscreen-toggle.js +++ b/src/js/control-bar/fullscreen-toggle.js @@ -3,7 +3,6 @@ */ import Button from '../button.js'; import Component from '../component.js'; -import FullscreenApi from '../fullscreen-api.js'; import document from 'global/document'; /** @@ -26,7 +25,7 @@ class FullscreenToggle extends Button { super(player, options); this.on(player, 'fullscreenchange', this.handleFullscreenChange); - if (document[FullscreenApi.fullscreenEnabled] === false) { + if (document[player.fsApi_.fullscreenEnabled] === false) { this.disable(); } } diff --git a/src/js/fullscreen-api.js b/src/js/fullscreen-api.js index 2ae57c7e4..85c9a9949 100644 --- a/src/js/fullscreen-api.js +++ b/src/js/fullscreen-api.js @@ -12,7 +12,9 @@ import document from 'global/document'; * @see [Specification]{@link https://fullscreen.spec.whatwg.org} * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js} */ -const FullscreenApi = {}; +const FullscreenApi = { + prefixed: true +}; // browser API methods const apiMap = [ @@ -59,7 +61,6 @@ const apiMap = [ const specApi = apiMap[0]; let browserApi; -let prefixedAPI = false; // determine the supported set of functions for (let i = 0; i < apiMap.length; i++) { @@ -76,8 +77,7 @@ if (browserApi) { FullscreenApi[specApi[i]] = browserApi[i]; } - prefixedAPI = browserApi[0] === specApi[0]; + FullscreenApi.prefixed = browserApi[0] !== specApi[0]; } export default FullscreenApi; -export { prefixedAPI }; diff --git a/src/js/player.js b/src/js/player.js index 1fb8bc116..f5a901576 100644 --- a/src/js/player.js +++ b/src/js/player.js @@ -21,7 +21,7 @@ import toTitleCase, { titleCaseEquals } from './utils/to-title-case.js'; import { createTimeRange } from './utils/time-ranges.js'; import { bufferedPercent } from './utils/buffer.js'; import * as stylesheet from './utils/stylesheet.js'; -import FullscreenApi, {prefixedAPI as prefixedFS} from './fullscreen-api.js'; +import FullscreenApi from './fullscreen-api.js'; import MediaError from './media-error.js'; import safeParseTuple from 'safe-json-parse/tuple'; import {assign} from './utils/obj'; @@ -358,6 +358,9 @@ class Player extends Component { // create logger this.log = createLogger(this.id_); + // Hold our own reference to fullscreen api so it can be mocked in tests + this.fsApi_ = FullscreenApi; + // Tracks when a tech changes the poster this.isPosterFromTech_ = false; @@ -2007,10 +2010,10 @@ class Player extends Component { // If cancelling fullscreen, remove event listener. if (this.isFullscreen() === false) { - Events.off(document, fsApi.fullscreenchange, this.boundDocumentFullscreenChange_); + Events.off(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); } - if (!prefixedFS) { + if (this.fsApi_.prefixed) { /** * @event Player#fullscreenchange * @type {EventTarget~Event} @@ -2729,14 +2732,17 @@ class Player extends Component { * This includes most mobile devices (iOS, Android) and older versions of * Safari. * + * @param {Object} [fullscreenOptions] + * Override the player fullscreen options + * * @fires Player#fullscreenchange */ - requestFullscreen() { - const fsApi = FullscreenApi; + requestFullscreen(fullscreenOptions) { + let fsOptions; this.isFullscreen(true); - if (fsApi.requestFullscreen) { + if (this.fsApi_.requestFullscreen) { // the browser supports going fullscreen at the element level so we can // take the controls fullscreen as well as the video @@ -2745,10 +2751,17 @@ class Player extends Component { // when canceling fullscreen. Otherwise if there's multiple // players on a page, they would all be reacting to the same fullscreen // events - Events.on(document, fsApi.fullscreenchange, this.boundDocumentFullscreenChange_); + Events.on(document, this.fsApi_.fullscreenchange, this.boundDocumentFullscreenChange_); - silencePromise(this.el_[fsApi.requestFullscreen]()); + // only pass FullscreenOptions to requestFullscreen if it isn't prefixed + if (!this.fsApi_.prefixed) { + fsOptions = this.options_.fullscreenOptions; + if (fullscreenOptions !== undefined) { + fsOptions = fullscreenOptions; + } + } + silencePromise(this.el_[this.fsApi_.requestFullscreen](fsOptions)); } else if (this.tech_.supportsFullScreen()) { // we can't take the video.js controls fullscreen but we can go fullscreen // with native controls @@ -2771,13 +2784,11 @@ class Player extends Component { * @fires Player#fullscreenchange */ exitFullscreen() { - const fsApi = FullscreenApi; - this.isFullscreen(false); // Check for browser element fullscreen support - if (fsApi.requestFullscreen) { - silencePromise(document[fsApi.exitFullscreen]()); + if (this.fsApi_.requestFullscreen) { + silencePromise(document[this.fsApi_.exitFullscreen]()); } else if (this.tech_.supportsFullScreen()) { this.techCall_('exitFullScreen'); } else { @@ -2997,7 +3008,7 @@ class Player extends Component { const FSToggle = Component.getComponent('FullscreenToggle'); - if (document[FullscreenApi.fullscreenEnabled] !== false) { + if (document[this.fsApi_.fullscreenEnabled] !== false) { FSToggle.prototype.handleClick.call(this); } @@ -4679,6 +4690,10 @@ Player.prototype.options_ = { // Default message to show when a video cannot be played. notSupportedMessage: 'No compatible source was found for this media.', + fullscreenOptions: { + navigationUI: 'hide' + }, + breakpoints: {}, responsive: false }; diff --git a/test/unit/player-fullscreen.test.js b/test/unit/player-fullscreen.test.js new file mode 100644 index 000000000..f32e68108 --- /dev/null +++ b/test/unit/player-fullscreen.test.js @@ -0,0 +1,202 @@ +/* eslint-env qunit */ +import Player from '../../src/js/player.js'; +import TestHelpers from './test-helpers.js'; +import sinon from 'sinon'; +import window from 'global/window'; + +const FullscreenTestHelpers = { + makePlayer(prefixed, playerOptions, videoTag) { + const player = TestHelpers.makePlayer(playerOptions, videoTag); + + player.fsApi_ = { + prefixed, + requestFullscreen: 'vjsRequestFullscreen', + exitFullscreen: 'vjsExitFullscreen', + fullscreenElement: 'vjsFullscreenElement', + fullscreenEnabled: 'vjsFullscreenEnabled', + fullscreenchange: 'vjsfullscreenchange', + fullscreenerror: 'vjsfullscreenerror' + }; + + return player; + } +}; + +QUnit.module('Player Fullscreen', { + beforeEach(assert) { + this.clock = sinon.useFakeTimers(); + // reset players storage + for (const playerId in Player.players) { + if (Player.players[playerId] !== null) { + Player.players[playerId].dispose(); + } + delete Player.players[playerId]; + } + + window.Element.prototype.vjsRequestFullscreen = function() { + assert.ok(false, 'vjsRequestFullscreen should not be called'); + }; + window.Element.prototype.vjsExitFullscreen = function() { + assert.ok(false, 'vjsExitFullscreen should not be called'); + }; + window.Element.prototype.vjsFullscreenElement = function() { + assert.ok(false, 'vjsFullscreenElement should not be called'); + }; + window.Element.prototype.vjsFullscreenEnabled = function() { + assert.ok(false, 'vjsFullscreenEnabled should not be called'); + }; + window.Element.prototype.vjsfullscreenchange = function() { + assert.ok(false, 'vjsfullscreenchange should not be called'); + }; + window.Element.prototype.vjsfullscreenerror = function() { + assert.ok(false, 'vjsfullscreenerror should not be called'); + }; + }, + afterEach() { + this.clock.restore(); + + delete window.Element.prototype.vjsRequestFullscreen; + delete window.Element.prototype.vjsExitFullscreen; + delete window.Element.prototype.vjsFullscreenElement; + delete window.Element.prototype.vjsFullscreenEnabled; + delete window.Element.prototype.vjsfullscreenchange; + delete window.Element.prototype.vjsfullscreenerror; + } +}); + +QUnit.test('fullscreenOptions should not be passed from player options on prefixed api', function(assert) { + + const fullscreenOptions = { + navigationUI: 'test', + foo: 'bar' + }; + + const player = FullscreenTestHelpers.makePlayer(true, { + fullscreenOptions + }); + + let requestFullscreenCalled = false; + let fsOpts; + + window.Element.prototype.vjsRequestFullscreen = function(opts) { + requestFullscreenCalled = true; + fsOpts = opts; + }; + + player.requestFullscreen(); + + assert.ok(requestFullscreenCalled, 'vjsRequestFullscreen should be called'); + assert.strictEqual(fsOpts, undefined, 'fullscreenOptions should not be passed'); + + player.dispose(); +}); + +QUnit.test('fullscreenOptions should be passed from player options on unprefixed api', function(assert) { + + const fullscreenOptions = { + navigationUI: 'test', + foo: 'bar' + }; + + const player = FullscreenTestHelpers.makePlayer(false, { + fullscreenOptions + }); + + let requestFullscreenCalled = false; + let fsOpts; + + window.Element.prototype.vjsRequestFullscreen = function(opts) { + requestFullscreenCalled = true; + fsOpts = opts; + }; + + player.requestFullscreen(); + + assert.ok(requestFullscreenCalled, 'vjsRequestFullscreen should be called'); + assert.notStrictEqual(fsOpts, undefined, 'fullscreenOptions should be passed'); + assert.deepEqual(fsOpts, fullscreenOptions, 'fullscreenOptions should match player options'); + + player.dispose(); +}); + +QUnit.test('fullscreenOptions should not be passed from function arguments on prefixed api', function(assert) { + + const fullscreenOptions = { + navigationUI: 'test', + foo: 'bar' + }; + + const player = FullscreenTestHelpers.makePlayer(true); + + let requestFullscreenCalled = false; + let fsOpts; + + window.Element.prototype.vjsRequestFullscreen = function(opts) { + requestFullscreenCalled = true; + fsOpts = opts; + }; + + player.requestFullscreen(fullscreenOptions); + + assert.ok(requestFullscreenCalled, 'vjsRequestFullscreen should be called'); + assert.strictEqual(fsOpts, undefined, 'fullscreenOptions should not be passed'); + + player.dispose(); +}); + +QUnit.test('fullscreenOptions should be passed from function arguments on unprefixed api', function(assert) { + + const fullscreenOptions = { + navigationUI: 'test', + foo: 'bar' + }; + + const player = FullscreenTestHelpers.makePlayer(false); + + let requestFullscreenCalled = false; + let fsOpts; + + window.Element.prototype.vjsRequestFullscreen = function(opts) { + requestFullscreenCalled = true; + fsOpts = opts; + }; + + player.requestFullscreen(fullscreenOptions); + + assert.ok(requestFullscreenCalled, 'vjsRequestFullscreen should be called'); + assert.notStrictEqual(fsOpts, undefined, 'fullscreenOptions should be passed'); + assert.deepEqual(fsOpts, fullscreenOptions, 'fullscreenOptions should match function args'); + + player.dispose(); +}); + +QUnit.test('fullscreenOptions from function args should override player options', function(assert) { + + const fullscreenOptions = { + navigationUI: 'args', + baz: 'bar' + }; + + const player = FullscreenTestHelpers.makePlayer(false, { + fullscreenOptions: { + navigationUI: 'playeroptions', + foo: 'bar' + } + }); + + let requestFullscreenCalled = false; + let fsOpts; + + window.Element.prototype.vjsRequestFullscreen = function(opts) { + requestFullscreenCalled = true; + fsOpts = opts; + }; + + player.requestFullscreen(fullscreenOptions); + + assert.ok(requestFullscreenCalled, 'vjsRequestFullscreen should be called'); + assert.notStrictEqual(fsOpts, undefined, 'fullscreenOptions should be passed'); + assert.deepEqual(fsOpts, fullscreenOptions, 'fullscreenOptions should match function args'); + + player.dispose(); +});