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

feat: Add document picture-in-picture support #8113

Merged
merged 21 commits into from
Apr 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Comment on lines +12 to +15
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Consider this temporary. This needs a better solution longer term that only modifies the last visible menu, but this situation could apply in situations other than PiP so deserves tackling separately to thi PR.


// 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.
mister-ben marked this conversation as resolved.
Show resolved Hide resolved
*/
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');
mister-ben marked this conversation as resolved.
Show resolved Hide resolved
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();
mister-ben marked this conversation as resolved.
Show resolved Hide resolved
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