Skip to content
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

Add built-in Picture-in-Picture button #6002

Merged
merged 9 commits into from
Jun 18, 2019
1 change: 1 addition & 0 deletions docs/guides/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ Player
│ ├── CaptionsButton (hidden, unless there are relevant tracks)
│ ├── AudioTrackButton (hidden, unless there are relevant tracks)
│ └── FullscreenToggle
│ └── PictureInPictureToggle
├── ErrorDisplay (hidden, until there is an error)
├── TextTrackSettings
└── ResizeManager (hidden)
Expand Down
1 change: 1 addition & 0 deletions docs/legacy-docs/guides/components.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ <h1 id="components">Components</h1>
SubtitlesButton (Hidden by default)
CaptionsButton (Hidden by default)
FullscreenToggle
PictureInPictureToggle
ErrorDisplay
TextTrackSettings
</code></pre><h2 id="progress-control">Progress Control</h2>
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"keycode": "^2.2.0",
"safe-json-parse": "4.0.0",
"tsml": "1.0.1",
"videojs-font": "3.1.1",
"videojs-font": "3.2.0",
"videojs-vtt.js": "0.14.1",
"xhr": "2.4.0"
},
Expand Down
12 changes: 12 additions & 0 deletions src/css/components/_picture-in-picture.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.video-js .vjs-picture-in-picture-control {
cursor: pointer;
@include flex(none);

& .vjs-icon-placeholder {
@extend .vjs-icon-picture-in-picture-enter;
}
}
// Switch to the exit icon when the player is in Picture-in-Picture
.video-js.vjs-picture-in-picture .vjs-picture-in-picture-control .vjs-icon-placeholder {
@extend .vjs-icon-picture-in-picture-exit;
}
1 change: 1 addition & 0 deletions src/css/video-js.scss
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
@import "components/play-pause";
@import "components/text-track";
@import "components/fullscreen";
@import "components/picture-in-picture";
@import "components/playback-rate";
@import "components/error";
@import "components/loading";
Expand Down
4 changes: 3 additions & 1 deletion src/js/control-bar/control-bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import './live-display.js';
import './seek-to-live.js';
import './progress-control/progress-control.js';
import './fullscreen-toggle.js';
import './picture-in-picture-toggle.js';
import './volume-panel.js';
import './text-track-controls/chapters-button.js';
import './text-track-controls/descriptions-button.js';
Expand Down Expand Up @@ -67,7 +68,8 @@ ControlBar.prototype.options_ = {
'descriptionsButton',
'subsCapsButton',
'audioTrackButton',
'fullscreenToggle'
'fullscreenToggle',
'pictureInPictureToggle'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd expect the fullscreen toggle to still be the right-most button, so, this would have to go above it.
I'm still a bit concerned on whether this can have issues, particularly in ad-plugins that often create their own control bar if it's included by default.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fullscreen toggle button is now the right-most button. I've updated screenshot as well.

]
};

Expand Down
93 changes: 93 additions & 0 deletions src/js/control-bar/picture-in-picture-toggle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @file picture-in-picture-toggle.js
*/
import Button from '../button.js';
import Component from '../component.js';
import document from 'global/document';

/**
* Toggle Picture-in-Picture mode
*
* @extends Button
*/
class PictureInPictureToggle extends Button {

/**
* Creates an instance of this class.
*
* @param {Player} player
* The `Player` that this class should be attached to.
*
* @param {Object} [options]
* The key/value store of player options.
*/
constructor(player, options) {
super(player, options);
this.on(player, 'pictureinpicturechange', this.handlePictureInPictureChange);

// TODO: Activate button on player loadedmetadata event.
// TODO: Deactivate button on player emptied event.
// TODO: Deactivate button if disablepictureinpicture attribute is present.
if (!document.pictureInPictureEnabled) {
this.disable();
}
}

/**
* Builds the default DOM `className`.
*
* @return {string}
* The DOM `className` for this object.
*/
buildCSSClass() {
return `vjs-picture-in-picture-control ${super.buildCSSClass()}`;
}

/**
* Handles pictureinpicturechange on the player and change control text accordingly.
*
* @param {EventTarget~Event} [event]
* The {@link Player#pictureinpicturechange} event that caused this function to be
* called.
*
* @listens Player#pictureinpicturechange
*/
handlePictureInPictureChange(event) {
if (this.player_.isInPictureInPicture()) {
this.controlText('Exit Picture-in-Picture');
} else {
this.controlText('Picture-in-Picture');
}
}

/**
* This gets called when an `PictureInPictureToggle` is "clicked". See
* {@link ClickableComponent} for more detailed information on what a click can be.
*
* @param {EventTarget~Event} [event]
* The `keydown`, `tap`, or `click` event that caused this function to be
* called.
*
* @listens tap
* @listens click
*/
handleClick(event) {
if (!this.player_.isInPictureInPicture()) {
this.player_.requestPictureInPicture();
} else {
this.player_.exitPictureInPicture();
}
}

}

/**
* The text that should display over the `PictureInPictureToggle`s controls. Added for localization.
*
* @type {string}
* @private
*/
PictureInPictureToggle.prototype.controlText_ = 'Picture-in-Picture';

Component.registerComponent('PictureInPictureToggle', PictureInPictureToggle);
export default PictureInPictureToggle;
111 changes: 111 additions & 0 deletions src/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -1142,6 +1142,8 @@ class Player extends Component {
this.on(this.tech_, 'pause', this.handleTechPause_);
this.on(this.tech_, 'durationchange', this.handleTechDurationChange_);
this.on(this.tech_, 'fullscreenchange', this.handleTechFullscreenChange_);
this.on(this.tech_, 'enterpictureinpicture', this.handleTechEnterPictureInPicture_);
this.on(this.tech_, 'leavepictureinpicture', this.handleTechLeavePictureInPicture_);
this.on(this.tech_, 'error', this.handleTechError_);
this.on(this.tech_, 'loadedmetadata', this.updateStyleEl_);
this.on(this.tech_, 'posterchange', this.handleTechPosterChange_);
Expand Down Expand Up @@ -2038,6 +2040,61 @@ class Player extends Component {
this.trigger('fullscreenchange');
}

/**
* @private
*/
togglePictureInPictureClass_() {
if (this.isInPictureInPicture()) {
this.addClass('vjs-picture-in-picture');
} else {
this.removeClass('vjs-picture-in-picture');
}
}

/**
* Handle Tech Enter Picture-in-Picture.
*
* @param {EventTarget~Event} event
* the enterpictureinpicture event that triggered this function
*
* @private
* @listens Tech#enterpictureinpicture
* @fires Player#pictureinpicturechange
*/
handleTechEnterPictureInPicture_(event) {
this.isInPictureInPicture(true);

/**
* Fired when going in and out of Picture-in-Picture.
*
* @event Player#pictureinpicturechange
* @type {EventTarget~Event}
*/
this.trigger('pictureinpicturechange');
}

/**
* Handle Tech Leave Picture-in-Picture.
*
* @param {EventTarget~Event} event
* the leavepictureinpicture event that triggered this function
*
* @private
* @listens Tech#leavepictureinpicture
* @fires Player#pictureinpicturechange
*/
handleTechLeavePictureInPicture_(event) {
this.isInPictureInPicture(false);

/**
* Fired when going in and out of Picture-in-Picture.
*
* @event Player#pictureinpicturechange
* @type {EventTarget~Event}
*/
this.trigger('pictureinpicturechange');
}

/**
* Fires when an error occurred during the loading of an audio/video.
*
Expand Down Expand Up @@ -2796,6 +2853,60 @@ class Player extends Component {
this.trigger('exitFullWindow');
}

/**
* Check if the player is in Picture-in-Picture mode or tell the player that it
* is or is not in Picture-in-Picture mode.
*
* @param {boolean} [isPiP]
* Set the players current Picture-in-Picture state
*
* @return {boolean}
* - true if Picture-in-Picture is on and getting
* - false if Picture-in-Picture is off and getting
*/
isInPictureInPicture(isPiP) {
if (isPiP !== undefined) {
this.isInPictureInPicture_ = !!isPiP;
this.togglePictureInPictureClass_();
return;
}
return !!this.isInPictureInPicture_;
}

/**
* Create a floating video window always on top of other windows so that users may
* continue consuming media while they interact with other content sites, or
* applications on their device.
*
* @see [Spec]{@link https://wicg.github.io/picture-in-picture}
*
* @fires Player#pictureinpicturechange
*
* @return {Promise}
* A promise with a Picture-in-Picture window.
*/
requestPictureInPicture() {
if ('pictureInPictureEnabled' in document) {
return this.techGet_('requestPictureInPicture');
}
}

/**
* Exit Picture-in-Picture mode.
*
* @see [Spec]{@link https://wicg.github.io/picture-in-picture}
*
* @fires Player#pictureinpicturechange
*
* @return {Promise}
* A promise.
*/
exitPictureInPicture() {
if ('pictureInPictureEnabled' in document) {
return document.exitPictureInPicture();
}
}

/**
* This gets called when a `Player` gains focus via a `focus` event.
* Turns on listening for `keydown` events. When they happen it
Expand Down
14 changes: 14 additions & 0 deletions src/js/tech/html5.js
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,20 @@ class Html5 extends Tech {
this.el_.webkitExitFullScreen();
}

/**
* Create a floating video window always on top of other windows so that users may
* continue consuming media while they interact with other content sites, or
* applications on their device.
*
* @see [Spec]{@link https://wicg.github.io/picture-in-picture}
*
* @return {Promise}
* A promise with a Picture-in-Picture window.
*/
requestPictureInPicture() {
return this.el_.requestPictureInPicture();
}

/**
* A getter/setter for the `Html5` Tech's source object.
* > Note: Please use {@link Html5#setSource}
Expand Down
16 changes: 16 additions & 0 deletions src/js/tech/tech.js
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,22 @@ class Tech extends Component {
return {};
}

/**
* Create a floating video window always on top of other windows so that users may
* continue consuming media while they interact with other content sites, or
* applications on their device.
*
* @see [Spec]{@link https://wicg.github.io/picture-in-picture}
*
* @return {Promise}
* A promise with a Picture-in-Picture window.
*
* @abstract
*/
requestPictureInPicture() {
return Promise.reject();
}

/**
* A method to set a poster from a `Tech`.
*
Expand Down
3 changes: 3 additions & 0 deletions test/api/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ QUnit.test('should be able to access expected player API methods', function(asse
assert.ok(player.textTracks, 'textTracks exists');
assert.ok(player.requestFullscreen, 'requestFullscreen exists');
assert.ok(player.exitFullscreen, 'exitFullscreen exists');
assert.ok(player.requestPictureInPicture, 'requestPictureInPicture exists');
assert.ok(player.exitPictureInPicture, 'exitPictureInPicture exists');
assert.ok(player.playbackRate, 'playbackRate exists');
assert.ok(player.networkState, 'networkState exists');
assert.ok(player.readyState, 'readyState exists');
Expand Down Expand Up @@ -164,6 +166,7 @@ QUnit.test('should export useful components to the public', function(assert) {
assert.ok(videojs.getComponent('Button'), 'Button should be public');
assert.ok(videojs.getComponent('PlayToggle'), 'PlayToggle should be public');
assert.ok(videojs.getComponent('FullscreenToggle'), 'FullscreenToggle should be public');
assert.ok(videojs.getComponent('PictureInPictureToggle'), 'PictureInPictureToggle should be public');
assert.ok(videojs.getComponent('BigPlayButton'), 'BigPlayButton should be public');
assert.ok(videojs.getComponent('LoadingSpinner'), 'LoadingSpinner should be public');
assert.ok(videojs.getComponent('CurrentTimeDisplay'),
Expand Down
17 changes: 17 additions & 0 deletions test/unit/controls.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import PlayToggle from '../../src/js/control-bar/play-toggle.js';
import PlaybackRateMenuButton from '../../src/js/control-bar/playback-rate-menu/playback-rate-menu-button.js';
import Slider from '../../src/js/slider/slider.js';
import FullscreenToggle from '../../src/js/control-bar/fullscreen-toggle.js';
import PictureInPictureToggle from '../../src/js/control-bar/picture-in-picture-toggle.js';
import ControlBar from '../../src/js/control-bar/control-bar.js';
import TestHelpers from './test-helpers.js';
import document from 'global/document';
Expand Down Expand Up @@ -168,6 +169,22 @@ QUnit.test('Fullscreen control text should be correct when fullscreenchange is t
fullscreentoggle.dispose();
});

QUnit.test('Picture-in-Picture control text should be correct when pictureinpicturechange is triggered', function(assert) {
const player = TestHelpers.makePlayer();
const pictureInPictureToggle = new PictureInPictureToggle(player);

player.isInPictureInPicture(true);
player.trigger('pictureinpicturechange');
assert.equal(pictureInPictureToggle.controlText(), 'Exit Picture-in-Picture', 'Control Text is correct while switching to Picture-in-Picture mode');

player.isInPictureInPicture(false);
player.trigger('pictureinpicturechange');
assert.equal(pictureInPictureToggle.controlText(), 'Picture-in-Picture', 'Control Text is correct while switching back to normal mode');

player.dispose();
pictureInPictureToggle.dispose();
});

QUnit.test('Clicking MuteToggle when volume is above 0 should toggle muted property and not change volume', function(assert) {
const player = TestHelpers.makePlayer({ techOrder: ['html5'] });
const muteToggle = new MuteToggle(player);
Expand Down