The Picture-in-Picture (PiP) feature allows you to continue watching video call content while browsing other web pages or using other applications. When Picture-in-Picture is enabled, the call interface will be displayed as a standalone floating window on top of the screen. Even when you switch to other browser tabs or applications, you can still view the call screen in real-time, effectively improving multitasking efficiency.
This document mainly introduces how to implement basic Web Picture-in-Picture audio and video call functionality by combining ZEGO Express SDK with the Web Picture-in-Picture API (Document Picture-in-Picture API).
Before implementing Web Picture-in-Picture audio and video call functionality, ensure that:
If you need to automatically trigger this feature when switching tabs, please perform the following operations:
Before using the Picture-in-Picture feature, you need to check whether the current browser environment supports the Document Picture-in-Picture API.
if ("documentPictureInPicture" in window) {
// Supports Picture-in-Picture feature
console.log("Current browser supports Document Picture-in-Picture API");
} else {
console.error("Current browser does not support Document Picture-in-Picture API");
}
Call the window.documentPictureInPicture.requestWindow() method to open a Picture-in-Picture window, specifying the window's width and height.
let pipWin = null;
async function openPictureInPicture() {
try {
// Open Picture-in-Picture window and set window size
pipWin = await window.documentPictureInPicture.requestWindow({
width: 360,
height: 500,
});
console.log("Picture-in-Picture window opened", pipWin);
// Continue with subsequent steps
} catch (error) {
console.error("Failed to open Picture-in-Picture window", error);
}
}
After opening the Picture-in-Picture window, you need to set its HTML structure and styles. You can set the window's head and body content by manipulating pipWin.document.
function setupPipWindow() {
// Set styles
pipWin.document.head.innerHTML = `
<style>
* {margin:0;padding:0;box-sizing: border-box; overflow: hidden;}
body { width: 100vw; height: 100vh }
.pip-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: #f5f5f5;
}
.video-wrap { flex: 1; display: flex; flex-direction: column; padding: 5px; }
.pip-video { flex: 1; margin: 5px 0; background: #000; }
.pip-controls {
padding: 10px;
display: flex;
justify-content: space-around;
background: #fff;
}
.pip-controls button {
padding: 8px 16px;
cursor: pointer;
}
</style>
`;
// Set page structure
pipWin.document.body.innerHTML = `
<div class="pip-container">
<div class="video-wrap">
<div class="pip-video" id="local-video"></div>
<div class="pip-video" id="remote-video"></div>
<div class="pip-video" id="screen-video"></div>
</div>
<div class="pip-controls">
<button id="toggle-mic">Toggle Mic</button>
<button id="toggle-camera">Toggle Camera</button>
<button id="hang-up">Hang Up</button>
</div>
</div>
`;
}
After creating streams with ZEGO Express SDK, you can render videos to specified elements in the Picture-in-Picture window using the playVideo method.
// Create local camera stream
localStream = await zg.createZegoStream({
camera: { video: { quality: 1 }, audio: true },
});
// Render local stream to Picture-in-Picture window
const pipLocalVideo = pipWin.document.getElementById("local-video");
localStream.playVideo(pipLocalVideo);
// Pull remote stream
remoteStream = await zg.startPlayingStream(remoteStreamID);
remoteView = zg.createRemoteStreamView(remoteStream);
// Render remote stream to Picture-in-Picture window
const pipRemoteVideo = pipWin.document.getElementById("remote-video");
remoteView.play(pipRemoteVideo);
// Create screen sharing stream
screenStream = await zg.createZegoStream({
screen: { video: true, audio: true },
});
// Get screen sharing content type (application window, browser tab, or entire screen)
screenShareType = screenStream.getScreenDisplaySurface();
// Render screen sharing stream to Picture-in-Picture window
const pipScreenVideo = pipWin.document.getElementById("screen-video");
screenStream.playVideo(pipScreenVideo);
Add event listeners to control buttons in the Picture-in-Picture window to implement microphone, camera toggle, and other functions.
function setupPipEventListeners() {
// Add click event for toggle microphone button
pipWin.document.getElementById("toggle-mic").addEventListener("click", () => {
// ...
});
// Add click event for toggle camera button
pipWin.document.getElementById("toggle-camera").addEventListener("click", () => {
// ...
});
// Add click event for hang up button
pipWin.document.getElementById("hang-up").addEventListener("click", () => {
// ...
});
}
When users manually close the Picture-in-Picture window, you need to listen to the pagehide event and handle it accordingly, such as rendering the video back to the main page window.
pipWin.addEventListener("pagehide", () => {
console.log("Picture-in-Picture window closed");
pipWin = null;
// Render video streams back to main page
// renderMainVideo();
});
If you need to programmatically close the Picture-in-Picture window in code, you can call the following methods:
Method 1: Close via window instance
if (pipWin) {
pipWin.close();
pipWin = null;
}
Method 2: Close via API
if (documentPictureInPicture.window) {
documentPictureInPicture.window.close();
}
State changes on the main page (such as microphone and camera toggles) are not automatically synchronized to the Picture-in-Picture window. You need to manually synchronize the states.
function updateCameraState(isVideoOff) {
const cameraText = isVideoOff ? "Turn on Camera" : "Turn off Camera";
// Synchronously update main page button state
const mainCameraButton = document.getElementById("toggle-camera");
if (mainCameraButton) {
mainCameraButton.textContent = cameraText;
}
// Synchronously update Picture-in-Picture window button state
if (pipWin) {
const pipCameraButton = pipWin.document.getElementById("toggle-camera");
if (pipCameraButton) {
pipCameraButton.textContent = cameraText;
}
}
}
The complete usage flow is as follows:
// 1. Check if browser supports Picture-in-Picture feature
if ("documentPictureInPicture" in window) {
// 2. Open Picture-in-Picture window
pipWin = await window.documentPictureInPicture.requestWindow({
width: 360,
height: 500,
});
// 3. Set window HTML structure and CSS styles
setupPipWindow();
// 4. Render video streams to Picture-in-Picture window
const pipLocalVideo = pipWin.document.getElementById("local-video");
localStream.playVideo(pipLocalVideo);
const pipRemoteVideo = pipWin.document.getElementById("remote-video");
remoteView.play(pipRemoteVideo);
// 5. Add event listeners for buttons in Picture-in-Picture window
setupPipEventListeners();
// 6. Listen for Picture-in-Picture window close event
pipWin.addEventListener("pagehide", () => {
pipWin = null;
// renderMainVideo();
});
}
