Skip to content

Commit

Permalink
feat: Add document picture-in-picture support (#8113)
Browse files Browse the repository at this point in the history
Co-authored-by: François Beaufort <[email protected]>
  • Loading branch information
mister-ben and beaufortfrancois authored Apr 4, 2023
1 parent 882f3af commit 0c72805
Show file tree
Hide file tree
Showing 13 changed files with 320 additions and 13 deletions.
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ <h2>Navigation</h2>
<li><a href="sandbox/quality-levels.html">QualityLevels Demo</a></li>
<li><a href="sandbox/autoplay-tests.html">Autoplay Tests</a></li>
<li><a href="sandbox/noUITitleAttributes.html">noUITitleAttributes Demo</a></li>
<li><a href="sandbox/docpip.html">Document Picture-In-Picture Demo</a></li>
<li><a href="sandbox/skip-buttons.html">Skip Buttons demo</a></li>
<li><a href="sandbox/debug.html">Videojs debug build test page</a></li>
</ul>
Expand Down
1 change: 1 addition & 0 deletions lang/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"Opacity": "Deckkraft",
"Text Background": "Texthintergrund",
"Caption Area Background": "Hintergrund des Untertitelbereichs",
"Playing in Picture-in-Picture": "Wird im Bild-im-Bild-Modus wiedergegeben",
"Skip forward {1} seconds": "{1} Sekunden vorwärts",
"Skip backward {1} seconds": "{1} Sekunden zurück"
}
Expand Down
1 change: 1 addition & 0 deletions lang/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"Opacity": "Opacity",
"Text Background": "Text Background",
"Caption Area Background": "Caption Area Background",
"Playing in Picture-in-Picture": "Playing in Picture-in-Picture",
"Skip backward {1} seconds": "Skip backward {1} seconds",
"Skip forward {1} seconds": "Skip forward {1} seconds"
}
52 changes: 52 additions & 0 deletions sandbox/docpip.html.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Video.js Sandbox</title>
<link href="../dist/video-js.css" rel="stylesheet" type="text/css">
<script src="../dist/video.js"></script>
<meta http-equiv="origin-trial" content="AruMDfzKHqbkAi4xRXZRAmpUv/hnpKsuR0VB+B6S7TGJOZBQv6ZQ0jaH6+EDW1tHjwYBlBAObmYinZ/aGtaLGwQAAACYeyJvcmlnaW4iOiJodHRwczovL2RlcGxveS1wcmV2aWV3LTgxMTMtLXZpZGVvanMtcHJldmlldy5uZXRsaWZ5LmFwcDo0NDMiLCJmZWF0dXJlIjoiRG9jdW1lbnRQaWN0dXJlSW5QaWN0dXJlQVBJIiwiZXhwaXJ5IjoxNjk0MTMxMTk5LCJpc1N1YmRvbWFpbiI6dHJ1ZX0=" />
</head>
<body>
<div style="background-color:#eee; border: 1px solid #777; padding: 10px; margin-bottom: 20px; font-size: .8em; line-height: 1.5em; font-family: Verdana, sans-serif;">
<p>You can use /sandbox/ for writing and testing your own code. Nothing in /sandbox/ will get checked into the repo, except files that end in .example (so don't edit or add those files). To get started run `npm start` and open the index.html</p>
<pre>npm start</pre>
<pre>open http://localhost:9999/sandbox/index.html</pre>
</div>

<p>Document Picture-in-Picture is available in Chrome version 111 onwards.</p>

<video-js
id="vid1"
controls
preload="auto"
width="640"
height="264"></video-js>
</video-js>

<script>
var vid = document.getElementById('vid1');
var player = videojs(vid, {
enableDocumentPictureInPicture: true
});
player.loadMedia({
artist: 'Disney',
album: 'Oceans',
title: 'Oceans',
description: 'Journey in to the depths of a wonderland filled with mystery, beauty and power. Oceans is a spectacular story, narrated by Pierce Brosnan, about remarkable creatures under the sea. It\'s an unprecedented look at the lives of these elusive deepwater creatures through their own eyes. Incredible state-of-the-art-underwater filmmaking will take your breath away as you migrate with whales, swim alongside a great white shark and race with dolphins at play.',
poster: 'https://vjs.zencdn.net/v/oceans.png',
src: [{
src: 'https://vjs.zencdn.net/v/oceans.mp4',
type: 'video/mp4',
}]
})

player.on(['enterpictureinpicture', 'leavepictureinpicture', 'disablepictureinpicturechanged'], e => {
console.log(e.type);
});
player.disablePictureInPicture(true);
player.log('window.player created', player);
</script>

</body>
</html>
3 changes: 2 additions & 1 deletion src/css/components/_fullscreen.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
}
}

.video-js.vjs-audio-only-mode .vjs-fullscreen-control {
.video-js.vjs-audio-only-mode .vjs-fullscreen-control,
.vjs-pip-window .vjs-fullscreen-control {
display: none;
}

Expand Down
28 changes: 24 additions & 4 deletions src/css/components/_layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,15 @@
display: none;
}

// Fullscreen Styles
body.vjs-full-window {
// Fullscreen and Document Picture-in-Picture Styles
body.vjs-full-window,
body.vjs-pip-window {
padding: 0;
margin: 0;
height: 100%;
}
.vjs-full-window .video-js.vjs-fullscreen {
.vjs-full-window .video-js.vjs-fullscreen,
body.vjs-pip-window .video-js {
position: fixed;
overflow: hidden;
z-index: 1000;
Expand All @@ -134,7 +136,8 @@ body.vjs-full-window {
bottom: 0;
right: 0;
}
.video-js.vjs-fullscreen:not(.vjs-ios-native-fs) {
.video-js.vjs-fullscreen:not(.vjs-ios-native-fs),
body.vjs-pip-window .video-js {
width: 100% !important;
height: 100% !important;
// Undo any aspect ratio padding for fluid layouts
Expand All @@ -145,6 +148,23 @@ body.vjs-full-window {
cursor: none;
}

.vjs-pip-container .vjs-pip-text {
position: absolute;
bottom: 10%;
font-size: 2em;
background-color: rgba(0, 0, 0, .7);
padding: .5em;
text-align: center;
width: 100%
}

.vjs-layout-tiny.vjs-pip-container .vjs-pip-text,
.vjs-layout-x-small.vjs-pip-container .vjs-pip-text,
.vjs-layout-small.vjs-pip-container .vjs-pip-text {
bottom: 0;
font-size: 1.4em;
}


// Hide disabled or unsupported controls.
.vjs-hidden { display: none !important; }
Expand Down
3 changes: 2 additions & 1 deletion src/css/components/_picture-in-picture.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
}
}

.video-js.vjs-audio-only-mode .vjs-picture-in-picture-control {
.video-js.vjs-audio-only-mode .vjs-picture-in-picture-control,
.vjs-pip-window .vjs-picture-in-picture-control {
display: none;
}

Expand Down
3 changes: 2 additions & 1 deletion src/css/components/_poster.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@

// Don't hide the poster if we're playing audio or when audio-poster-mode is true
.vjs-audio.vjs-has-started .vjs-poster,
.vjs-has-started.vjs-audio-poster-mode .vjs-poster {
.vjs-has-started.vjs-audio-poster-mode .vjs-poster,
.vjs-pip-container.vjs-has-started .vjs-poster {
display: block;
}

Expand Down
5 changes: 5 additions & 0 deletions src/css/components/menu/_menu-popup.scss
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
border-top-color: rgba($primary-background-color, $primary-background-transparency); // Same as ul background
}

.vjs-pip-window .vjs-menu-button-popup .vjs-menu {
left: unset;
right: 1em; // Extra offset for last menu button in pip window, as fullscreen button not present
}

// Button Pop-up Menu
.vjs-menu-button-popup .vjs-menu .vjs-menu-content {
@include background-color-with-alpha($primary-background-color, $primary-background-transparency);
Expand Down
15 changes: 12 additions & 3 deletions src/js/control-bar/picture-in-picture-toggle.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import Button from '../button.js';
import Component from '../component.js';
import document from 'global/document';
import window from 'global/window';

/**
* @typedef { import('./player').default } Player
Expand Down Expand Up @@ -63,11 +64,19 @@ class PictureInPictureToggle extends Button {
}

/**
* Enables or disables button based on document.pictureInPictureEnabled property value
* or on value returned by player.disablePictureInPicture() method.
* Enables or disables button based on availability of a Picture-In-Picture mode.
*
* Enabled if
* - `player.options().enableDocumentPictureInPicture` is true and
* window.documentPictureInPicture is available; or
* - `player.disablePictureInPicture()` is false and
* element.requestPictureInPicture is available
*/
handlePictureInPictureEnabledChange() {
if (document.pictureInPictureEnabled && this.player_.disablePictureInPicture() === false) {
if (
(document.pictureInPictureEnabled && this.player_.disablePictureInPicture() === false) ||
(this.player_.options_.enableDocumentPictureInPicture && 'documentPictureInPicture' in window)
) {
this.enable();
} else {
this.disable();
Expand Down
54 changes: 53 additions & 1 deletion src/js/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -3037,14 +3037,59 @@ class Player extends Component {
* 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}
* This can use document picture-in-picture or element picture in picture
*
* Set `enableDocumentPictureInPicture` to `true` to use docPiP on a supported browser
* Else set `disablePictureInPicture` to `false` to disable elPiP on a supported browser
*
*
* @see [Spec]{@link https://w3c.github.io/picture-in-picture/}
* @see [Spec]{@link https://wicg.github.io/document-picture-in-picture/}
*
* @fires Player#enterpictureinpicture
*
* @return {Promise}
* A promise with a Picture-in-Picture window.
*/
requestPictureInPicture() {
if (this.options_.enableDocumentPictureInPicture && window.documentPictureInPicture) {
const pipContainer = document.createElement(this.el().tagName);

pipContainer.classList = this.el().classList;
pipContainer.classList.add('vjs-pip-container');
if (this.posterImage) {
pipContainer.appendChild(this.posterImage.el().cloneNode(true));
}
if (this.titleBar) {
pipContainer.appendChild(this.titleBar.el().cloneNode(true));
}
pipContainer.appendChild(Dom.createEl('p', { className: 'vjs-pip-text' }, {}, this.localize('Playing in picture-in-picture')));

return window.documentPictureInPicture.requestWindow({
// The aspect ratio won't be correct, Chrome bug https://crbug.com/1407629
initialAspectRatio: this.videoWidth() / this.videoHeight(),
copyStyleSheets: true
}).then(pipWindow => {
this.el_.parentNode.insertBefore(pipContainer, this.el_);

pipWindow.document.body.append(this.el_);
pipWindow.document.body.classList.add('vjs-pip-window');

this.player_.isInPictureInPicture(true);
this.player_.trigger('enterpictureinpicture');

// Listen for the PiP closing event to move the video back.
pipWindow.addEventListener('unload', (event) => {
const pipVideo = event.target.querySelector('.video-js');

pipContainer.replaceWith(pipVideo);
this.player_.isInPictureInPicture(false);
this.player_.trigger('leavepictureinpicture');
});

return pipWindow;
});
}
if ('pictureInPictureEnabled' in document && this.disablePictureInPicture() === false) {
/**
* This event fires when the player enters picture in picture mode
Expand All @@ -3054,6 +3099,7 @@ class Player extends Component {
*/
return this.techGet_('requestPictureInPicture');
}
return Promise.reject('No PiP mode is available');
}

/**
Expand All @@ -3067,7 +3113,13 @@ class Player extends Component {
* A promise.
*/
exitPictureInPicture() {
if (window.documentPictureInPicture && window.documentPictureInPicture.window) {
// With documentPictureInPicture, Player#leavepictureinpicture is fired in the unload handler
window.documentPictureInPicture.window.close();
return Promise.resolve();
}
if ('pictureInPictureEnabled' in document) {

/**
* This event fires when the player leaves picture in picture mode
*
Expand Down
25 changes: 25 additions & 0 deletions test/unit/controls.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SeekBar from '../../src/js/control-bar/progress-control/seek-bar.js';
import RemainingTimeDisplay from '../../src/js/control-bar/time-controls/remaining-time-display.js';
import TestHelpers from './test-helpers.js';
import document from 'global/document';
import window from 'global/window';
import sinon from 'sinon';

QUnit.module('Controls', {
Expand Down Expand Up @@ -300,6 +301,30 @@ QUnit.test('Picture-in-Picture control is hidden when the source is audio', func
pictureInPictureToggle.dispose();
});

QUnit.test('Picture-in-Picture control is displayed if docPiP is enabled', function(assert) {
const player = TestHelpers.makePlayer({
disablePictureInPicture: true,
enableDocumentPictureInPicture: true
});
const pictureInPictureToggle = new PictureInPictureToggle(player);
const testPiPObj = {};

if (!window.documentPictureInPicture) {
window.documentPictureInPicture = testPiPObj;
}

player.src({src: 'example.mp4', type: 'video/mp4'});
player.trigger('loadedmetadata');

assert.notOk(pictureInPictureToggle.hasClass('vjs-hidden'), 'pictureInPictureToggle button is not hidden');

player.dispose();
pictureInPictureToggle.dispose();
if (window.documentPictureInPicture === testPiPObj) {
delete window.documentPictureInPicture;
}
});

QUnit.test('Fullscreen control text should be correct when fullscreenchange is triggered', function(assert) {
const player = TestHelpers.makePlayer({controlBar: false});
const fullscreentoggle = new FullscreenToggle(player);
Expand Down
Loading

0 comments on commit 0c72805

Please sign in to comment.