visit
Live Demo:
The Picture-in-picture API has been available in most browsers for quite some time. However, the main downside was that developers have very limited control over custom controls and the look and feel of it, as it only allows to use Video element as a PiP element. This forces developers to use canvas hacks for any custom UI.
Let's start by looking at the browser API before adding React integration. For proper integration, we need to support three main operations: opening a window, closing and detecting a closed event, and detecting the feature itself.
API for opening picture-in-picture windows is very similar to the one you can use for regular window.open()
.
However, there are some important differences:
const pipWindow = await documentPictureInPicture.requestWindow({
width: 500, // optional
heithg: 500, // optional
});
// You have full control over this PiP now
pipWindow.document.body.innerHTML = 'Hello from PiP';
NOTE: It is important to note that you can only open this window in response to use interaction (); otherwise, you will get this error
DOMException: Failed to execute 'requestWindow' on 'DocumentPictureInPicture': Document PiP requires user activation
You can listen to the pagehide
event to detect when the PiP window is closing. For example, in case a user decides to close the PiP window.
pipWindow.addEventListener("pagehide", (event) => {
// do something when pip is closed by the browser/user
});
You can also decide to close the window at any moment programmatically:
pipWindow.close();
if ('documentPictureInPicture' in window) {
// Feature supported
}
type PiPContextType = {
isSupported: boolean;
pipWindow: Window | null;
requestPipWindow: (width: number, height: number) => Promise<void>;
closePipWindow: () => void;
};
const PiPContext = createContext<PiPContextType | undefined>(undefined);
type PiPProviderProps = {
children: React.ReactNode;
};
export function PiPProvider({ children }: PiPProviderProps) {
// Detect if the feature is available.
const isSupported = "documentPictureInPicture" in window;
// Expose pipWindow that is currently active
const [pipWindow, setPipWindow] = useState<Window | null>(null);
// Close pipWidnow programmatically
const closePipWindow = useCallback(() => {
if (pipWindow != null) {
pipWindow.close();
setPipWindow(null);
}
}, [pipWindow]);
// Open new pipWindow
const requestPipWindow = useCallback(
async (width: number, height: number) => {
// We don't want to allow multiple requests.
if (pipWindow != null) {
return;
}
const pip = await window.documentPictureInPicture.requestWindow({
width,
height,
});
// Detect when window is closed by user
pip.addEventListener("pagehide", () => {
setPipWindow(null);
});
// It is important to copy all parent widnow styles. Otherwise, there would be no CSS available at all
// //developer.chrome.com/docs/web-platform/document-picture-in-picture/#copy-style-sheets-to-the-picture-in-picture-window
[...document.styleSheets].forEach((styleSheet) => {
try {
const cssRules = [...styleSheet.cssRules]
.map((rule) => rule.cssText)
.join("");
const style = document.createElement("style");
style.textContent = cssRules;
pip.document.head.appendChild(style);
} catch (e) {
const link = document.createElement("link");
if (styleSheet.href == null) {
return;
}
link.rel = "stylesheet";
link.type = styleSheet.type;
link.media = styleSheet.media.toString();
link.href = styleSheet.href;
pip.document.head.appendChild(link);
}
});
setPipWindow(pip);
},
[pipWindow]
);
const value = useMemo(() => {
{
return {
isSupported,
pipWindow,
requestPipWindow,
closePipWindow,
};
}
}, [closePipWindow, isSupported, pipWindow, requestPipWindow]);
return <PiPContext.Provider value={value}>{children}</PiPContext.Provider>;
}
export function usePiPWindow(): PiPContextType {
const context = useContext(PiPContext);
if (context === undefined) {
throw new Error("usePiPWindow must be used within a PiPContext");
}
return context;
}
Now, once we have access to pipWindow, we can render it using React API. Since pipWindow
is not part of our DOM tree that React manages, we need to use API to render it to different DOM elements.
Let's create a PiPWindow
component that we can use to render inside the newly created Document Picture-in-Picture window.
import { createPortal } from "react-dom";
type PiPWindowProps = {
pipWindow: Window;
children: React.ReactNode;
};
export default function PiPWindow({ pipWindow, children }: PiPWindowProps) {
return createPortal(children, pipWindow.document.body);
}
function Example() {
const { isSupported, requestPipWindow, pipWindow, closePipWindow } =
usePiPWindow();
const startPiP = useCallback(() => {
requestPipWindow(500, 500);
}, [requestPipWindow]);
const [count, setCount] = useState(0);
return (
<div>
{/* Make sure to have some fallback in case if API is not supported */}
{isSupported ? (
<>
<button onClick={pipWindow ? closePipWindow : startPiP}>
{pipWindow ? "Close PiP" : "Open PiP"}
</button>
{pipWindow && (
<PiPWindow pipWindow={pipWindow}>
<div
style={{
flex: 1,
textAlign: "center",
}}
>
<h3>Hello in PiP!</h3>
<button
onClick={() => {
setCount((count) => count + 1);
}}
>
Clicks count is {count}
</button>
</div>
</PiPWindow>
)}
</>
) : (
<div className="error">
Document Picture-in-Picture is not supported in this browser
</div>
)}
</div>
);
}
Live Demo: