跳到主要内容
前端实现视频画中画功能:主窗口与小窗同步控制 | 极客日志
JavaScript 大前端
前端实现视频画中画功能:主窗口与小窗同步控制 综述由AI生成 基于 Document Picture-in-Picture API 实现前端视频画中画功能,解决多任务场景下视频悬浮播放需求。方案涵盖主窗口与小窗的 DOM 迁移、样式注入及双向通信机制,重点实现了播放状态、音量、进度时间的实时同步逻辑。通过事件监听与阈值防抖优化,确保两端操作一致且性能稳定,适用于自定义播放器开发。
黑客帝国 发布于 2026/4/10 更新于 2026/5/23 16 浏览前端实现视频画中画功能:主窗口与小窗同步控制
在浏览网页时,我们常遇到需要一边看视频一边处理其他任务的情况。B 站等平台的画中画(PiP)功能允许视频悬浮播放,而 Chrome 116+ 引入的 Document Picture-in-Picture API 更进一步,支持将整个文档内容移入小窗,并保留自定义控件和交互能力。
本文将演示如何基于该 API 实现一个完整的主页面与小窗同步控制方案,包括播放状态、音量及进度的实时同步。
为什么要使用 Document PiP API
传统的 Picture-in-Picture API 仅支持 <video> 元素的独立浮窗,无法携带自定义 UI。新的 Document Picture-in-Picture API 则允许整个 HTML 文档进入小窗模式,具备以下优势:
完整 HTML 支持 :小窗内可包含按钮、进度条等任意交互元素。
无缝集成 :主页面与小窗共享 JavaScript 上下文,便于通信。
尺寸灵活 :可根据需求自定义小窗宽高。
双向通信 :主窗口与小窗能实时同步状态。
这特别适用于视频会议、在线课程或需要深度定制控制器的播放器场景。
完整代码案例
为了便于测试,我将 CSS、HTML 和 JS 整合为一个单文件示例。你可以直接保存为 .html 文件并在支持的浏览器中打开。
注意 :代码中的 src="1.mp4" 需替换为你本地的视频资源路径。
<!DOCTYPE html >
<html lang ="zh-CN" >
<head >
<meta charset ="UTF-8" >
<meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
<title > 视频小窗模式演示</title >
<style >
* {
margin : 0 ;
padding : 0 ;
box-sizing : border-box;
}
body {
: , Tahoma, Geneva, Verdana, sans-serif;
: ;
: ;
: ( , , , );
: ;
: ;
}
{
: ;
: auto;
: ( , , , );
: ;
: ( , , , );
: hidden;
}
{
: (to right, , );
: white;
: ;
: center;
}
{
: ;
: ;
: ( , , , );
}
{
: ;
: ;
: ;
: auto;
}
{
: flex;
: ;
: ;
}
{
: ;
: ;
: ;
: hidden;
: ( , , , );
}
{
: relative;
: ;
: ;
}
{
: absolute;
: ;
: ;
: ;
: ;
: block;
}
{
: flex;
: ;
: ;
: ;
}
{
: ;
: white;
: none;
: ;
: ;
: pointer;
: ;
: all ease;
: flex;
: center;
: ;
}
{
: ;
: (- );
: ( , , , );
}
{
: ;
: not-allowed;
: none;
: none;
}
{
: ;
: white;
: ;
: ;
: ( , , , );
}
{
: ;
: ;
: ;
: solid ;
}
{
: ;
}
{
: flex;
: flex-start;
: ;
}
{
: ;
: white;
: ;
: ;
: ;
: flex;
: center;
: center;
: ;
: ;
}
{
: fixed;
: ;
: ;
: ;
: ;
: black;
: ;
: hidden;
: ( , , , );
: ;
: none;
}
{
: ;
: ;
: cover;
}
{
: absolute;
: ;
: ;
: ;
: flex;
: center;
: ;
: ;
: opacity ;
}
{
: ;
}
{
: ;
: ;
: ;
: ;
: monospace;
}
{
: ;
: ;
: ;
: ;
: solid ;
}
{
: flex;
: ;
: ;
: wrap;
}
{
: flex;
: center;
: ;
}
{ : ; }
{ : ; }
( : ) {
{ : column; }
}
视频小窗模式演示
使用 Document Picture-in-Picture API 实现在其他内容上浮动播放视频
开启小窗模式
全屏
Document Picture-in-Picture API
1
任意 HTML 内容
可以在小窗中显示视频控件、字幕等任意 HTML 元素
2
保持播放状态
进入小窗模式时视频持续播放,不中断观看体验
3
双向同步
主页面和小窗中的视频状态实时同步
4
自由调整
用户可以调整小窗位置和大小,适应不同需求
当前状态: 等待操作
小窗状态: 未激活
浏览器支持情况
Chrome 108+
Firefox
Edge
Safari
关闭
font-family
'Segoe UI'
line-height
1.6
color
#333
background
linear-gradient
135deg
#1a2a6c
#b21f1f
#1a2a6c
padding
20px
min-height
100vh
.container
max-width
1200px
margin
0
background-color
rgba
255
255
255
0.95
border-radius
15px
box-shadow
0
10px
30px
rgba
0
0
0
0.3
overflow
header
background
linear-gradient
#1a2a6c
#b21f1f
color
padding
25px
40px
text-align
h1
font-size
2.5rem
margin-bottom
10px
text-shadow
0
2px
4px
rgba
0
0
0
0.3
.subtitle
font-size
1.2rem
opacity
0.9
max-width
700px
margin
0
.content
display
padding
30px
gap
30px
.video-section
flex
3
background
#f8f9fa
border-radius
10px
overflow
box-shadow
0
5px
15px
rgba
0
0
0
0.1
.video-container
position
padding-top
56.25%
background
#000
video
position
top
0
left
0
width
100%
height
100%
display
.video-controls
display
padding
15px
gap
10px
background
#e9ecef
button
background
#1a2a6c
color
border
padding
10px
20px
border-radius
5px
cursor
font-weight
600
transition
0.3s
display
align-items
gap
8px
button
:hover
background
#0d1a4d
transform
translateY
2px
box-shadow
0
4px
8px
rgba
0
0
0
0.2
button
:disabled
background
#6c757d
cursor
transform
box-shadow
.info-section
flex
2
background
padding
25px
border-radius
10px
box-shadow
0
5px
15px
rgba
0
0
0
0.05
h2
color
#1a2a6c
margin-bottom
20px
padding-bottom
10px
border-bottom
2px
#e9ecef
.feature-list
margin
20px
0
.feature
display
align-items
margin-bottom
15px
.feature-icon
background
#1a2a6c
color
width
30px
height
30px
border-radius
50%
display
align-items
justify-content
margin-right
15px
flex-shrink
0
.pip-window
position
bottom
20px
right
20px
width
300px
height
200px
background
border-radius
10px
overflow
box-shadow
0
10px
25px
rgba
0
0
0
0.4
z-index
1000
display
.pip-window
video
width
100%
height
100%
object-fit
.pip-controls
position
bottom
10px
left
0
right
0
display
justify-content
gap
10px
opacity
0
transition
0.3s
.pip-window
:hover
.pip-controls
opacity
1
.status
padding
15px
background
#e9ecef
border-radius
8px
margin-top
20px
font-family
.browser-support
margin-top
30px
padding
20px
background
#fff8e1
border-radius
8px
border-left
4px
#ffc107
.support-list
display
gap
15px
margin-top
15px
flex-wrap
.browser
display
align-items
gap
8px
.supported
color
#28a745
.unsupported
color
#dc3545
@media
max-width
900px
.content
flex-direction
</style >
</head >
<body >
<div class ="container" >
<header >
<h1 >
</h1 >
<p class ="subtitle" >
</p >
</header >
<div class ="content" >
<div class ="video-section" >
<div class ="video-container" >
<video id ="mainVideo" src ="1.mp4" controls playsinline >
</video >
</div >
<div class ="video-controls" >
<button id ="pipButton" title ="开启小窗模式" >
<svg width ="20" height ="20" fill ="currentColor" viewBox ="0 0 16 16" >
<path d ="M0 3.5A1.5 1.5 0 0 1 1.5 2h13A1.5 1.5 0 0 1 16 3.5v9a1.5 1.5 0 0 1-1.5 1.5h-13A1.5 1.5 0 0 1 0 12.5v-9zM1.5 3a.5.5 0 0 0-.5.5v9a.5.5 0 0 0 .5.5h13a.5.5 0 0 0 .5-.5v-9a.5.5 0 0 0-.5-.5h-13z" />
<path d ="M8 8.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 .5.5v3a.5.5 0 0 1-.5.5h-5a.5.5 0 0 1-.5-.5v-3z" />
</svg >
</button >
<button id ="fullscreenButton" >
<svg width ="20" height ="20" fill ="currentColor" viewBox ="0 0 16 16" >
<path d ="M1.5 1a.5.5 0 0 0-.5.5v4a.5.5 0 0 1-1 0v-4A1.5 1.5 0 0 1 1.5 0h4a.5.5 0 0 1 0 1h-4zM10 .5a.5.5 0 0 1 .5-.5h4A1.5 1.5 0 0 1 16 1.5v4a.5.5 0 0 1-1 0v-4a.5.5 0 0 0-.5-.5h-4a.5.5 0 0 1-.5-.5zM.5 10a.5.5 0 0 1 .5.5v4a.5.5 0 0 0 .5.5h4a.5.5 0 0 1 0 1h-4A1.5 1.5 0 0 1 0 14.5v-4a.5.5 0 0 1 .5-.5zm15 0a.5.5 0 0 1 .5.5v4a1.5 1.5 0 0 1-1.5 1.5h-4a.5.5 0 0 1 0-1h4a.5.5 0 0 0 .5-.5v-4a.5.5 0 0 1 .5-.5z" />
</svg >
</button >
</div >
</div >
<div class ="info-section" >
<h2 >
</h2 >
<div class ="feature-list" >
<div class ="feature" >
<div class ="feature-icon" >
</div >
<div >
<h3 >
</h3 >
<p >
</p >
</div >
</div >
<div class ="feature" >
<div class ="feature-icon" >
</div >
<div >
<h3 >
</h3 >
<p >
</p >
</div >
</div >
<div class ="feature" >
<div class ="feature-icon" >
</div >
<div >
<h3 >
</h3 >
<p >
</p >
</div >
</div >
<div class ="feature" >
<div class ="feature-icon" >
</div >
<div >
<h3 >
</h3 >
<p >
</p >
</div >
</div >
</div >
<div class ="status" >
<p >
<span id ="statusText" >
</span >
</p >
<p >
<span id ="pipStatus" >
</span >
</p >
</div >
<div class ="browser-support" >
<h3 >
</h3 >
<div class ="support-list" >
<div class ="browser" >
<span class ="supported" >
</span >
</div >
<div class ="browser" >
<span class ="unsupported" >
</span >
</div >
<div class ="browser" >
<span class ="unsupported" >
</span >
</div >
<div class ="browser" >
<span class ="unsupported" >
</span >
</div >
</div >
</div >
</div >
</div >
</div >
<div id ="pipContainer" class ="pip-window" >
<video id ="pipVideo" src ="1.mp4" controls >
</video >
<div class ="pip-controls" >
<button id ="closePipButton" title ="关闭小窗" >
</button >
</div >
</div >
<script >
const mainVideo = document .getElementById ('mainVideo' );
const pipVideo = document .getElementById ('pipVideo' );
const pipButton = document .getElementById ('pipButton' );
const fullscreenButton = document .getElementById ('fullscreenButton' );
const closePipButton = document .getElementById ('closePipButton' );
const pipContainer = document .getElementById ('pipContainer' );
const statusText = document .getElementById ('statusText' );
const pipStatus = document .getElementById ('pipStatus' );
const isPipSupported = 'documentPictureInPicture' in window ;
function init ( ) {
updateStatus (isPipSupported ? "Document Picture-in-Picture API 可用" : "您的浏览器不支持 Document Picture-in-Picture API" );
pipButton.disabled = !isPipSupported;
pipButton.addEventListener ('click' , togglePictureInPicture);
fullscreenButton.addEventListener ('click' , toggleFullscreen);
closePipButton.addEventListener ('click' , closePictureInPicture);
pipVideo.src = mainVideo.src ;
mainVideo.muted = true ;
pipVideo.muted = true ;
}
function updateStatus (message ) {
statusText.textContent = message;
}
function updatePipStatus (message ) {
pipStatus.textContent = message;
}
async function togglePictureInPicture ( ) {
if (!isPipSupported) return ;
if (window .documentPictureInPicture .window ) {
await closePictureInPicture ();
return ;
}
try {
const pipWindow = await window .documentPictureInPicture .requestWindow ({
width : 400 ,
height : 300 ,
});
pipWindow.document .title = "视频小窗播放" ;
const style = document .createElement ('style' );
style.textContent = `
body { margin: 0; background: black; height: 100vh; overflow: hidden; }
video { width: 100%; height: 100%; object-fit: contain; }
` ;
pipWindow.document .head .appendChild (style);
pipWindow.document .body .appendChild (pipVideo);
pipVideo.currentTime = mainVideo.currentTime ;
if (!mainVideo.paused ) {
await pipVideo.play ();
} else {
pipVideo.pause ();
}
pipWindow.addEventListener ('pagehide' , () => {
pipContainer.appendChild (pipVideo);
pipContainer.style .display = 'none' ;
updatePipStatus ("已关闭" );
});
pipContainer.style .display = 'block' ;
updateStatus ("小窗模式已激活" );
updatePipStatus ("运行中" );
mainVideo.addEventListener ('timeupdate' , syncVideoTime);
pipVideo.addEventListener ('timeupdate' , syncVideoTime);
mainVideo.addEventListener ('play' , () => pipVideo.play ());
mainVideo.addEventListener ('pause' , () => pipVideo.pause ());
pipVideo.addEventListener ('play' , () => mainVideo.play ());
pipVideo.addEventListener ('pause' , () => mainVideo.pause ());
mainVideo.addEventListener ('volumechange' , syncVolume);
pipVideo.addEventListener ('volumechange' , syncVolume);
} catch (error) {
updateStatus (`错误:${error.message} ` );
console .error (error);
}
}
function syncVideoTime ( ) {
if (Math .abs (mainVideo.currentTime - pipVideo.currentTime ) > 0.5 ) {
if (this === mainVideo) {
pipVideo.currentTime = mainVideo.currentTime ;
} else {
mainVideo.currentTime = pipVideo.currentTime ;
}
}
}
function syncVolume ( ) {
if (this === mainVideo) {
pipVideo.volume = mainVideo.volume ;
pipVideo.muted = mainVideo.muted ;
} else {
mainVideo.volume = pipVideo.volume ;
mainVideo.muted = pipVideo.muted ;
}
}
async function closePictureInPicture ( ) {
if (window .documentPictureInPicture .window ) {
window .documentPictureInPicture .window .close ();
}
mainVideo.removeEventListener ('timeupdate' , syncVideoTime);
pipVideo.removeEventListener ('timeupdate' , syncVideoTime);
mainVideo.removeEventListener ('volumechange' , syncVolume);
pipVideo.removeEventListener ('volumechange' , syncVolume);
pipContainer.appendChild (pipVideo);
pipContainer.style .display = 'none' ;
updateStatus ("小窗模式已关闭" );
updatePipStatus ("未激活" );
}
function toggleFullscreen ( ) {
if (!document .fullscreenElement ) {
if (mainVideo.requestFullscreen ) mainVideo.requestFullscreen ();
else if (mainVideo.webkitRequestFullscreen ) mainVideo.webkitRequestFullscreen ();
else if (mainVideo.msRequestFullscreen ) mainVideo.msRequestFullscreen ();
} else {
if (document .exitFullscreen ) document .exitFullscreen ();
else if (document .webkitExitFullscreen ) document .webkitExitFullscreen ();
else if (document .msExitFullscreen ) document .msExitFullscreen ();
}
}
document .addEventListener ('DOMContentLoaded' , init);
</script >
</body >
</html >
案例核心原理与流程
1. 代码流程
创建窗口 :用户点击按钮后,调用 documentPictureInPicture.requestWindow() 创建独立的 PiP 窗口。
DOM 迁移 :将 <video> DOM 节点移动到小窗口中,同时注入必要的样式。
状态同步 :通过事件监听器,在两个窗口间同步播放、暂停、音量及当前时间。
清理恢复 :窗口关闭时,监听 pagehide 事件,将 DOM 恢复到主窗口并移除监听器。
2. 状态同步机制 为了避免频繁触发导致性能问题,我们在同步时间戳时加入了阈值判断(0.5 秒)。当主窗口或小窗的时间差超过此值时,才强制同步另一方的时间。对于播放/暂停和音量变化,则采用直接监听事件的方式,确保即时响应。
3. 关键事件处理
timeupdate :负责同步播放进度,防止两端进度不一致。
play/pause :确保一端操作后,另一端立即跟随状态。
volumechange :同步静音状态和音量大小。
pagehide :检测小窗被系统关闭或用户手动关闭时的清理逻辑。
结语 Document Picture-in-Picture API 为开发者提供了强大的工具来创建更灵活的视频观看体验。虽然目前主要支持 Chrome 116+ 版本,但随着标准的演进,它有望成为视频播放页面的标配功能。
本方案展示了如何利用该 API 创建自定义画中画窗口,并实现了主窗口与小窗之间播放、暂停、音量控制与关闭逻辑的完整同步。相比传统 PiP,它能提供更强的可定制化能力,适用于各种需要精细控制的播放器场景。
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online