跳到主要内容
极客日志极客日志
首页博客AI提示词GitHub精选代理工具
搜索
|注册
博客列表
HTML / CSS大前端

前端实现 Web 视频画中画功能 - 主窗口与小窗同步控制

Document Picture-in-Picture API 允许将网页内容嵌入独立小窗,突破传统 PiP 限制。本方案在 Chrome 116+ 环境下,通过 DOM 节点迁移与事件监听,实现主窗口与小窗的播放进度、音量及状态实时同步。代码涵盖完整样式布局与交互逻辑,支持窗口关闭后自动恢复,适用于自定义播放器场景。

lzdxwyh发布于 2026/4/7更新于 2026/5/1211 浏览
前端实现 Web 视频画中画功能 - 主窗口与小窗同步控制

演示效果

在浏览器的多任务处理场景中,用户往往需要在观看视频的同时进行其他操作。传统的 Picture-in-Picture API 仅支持将 <video> 元素本身移出页面,无法携带自定义控件或交互逻辑。随着 Chrome 116+ 对 Document Picture-in-Picture API 的支持,我们可以将整个文档内容移入独立小窗,并保留完整的 HTML 结构与 JavaScript 上下文。

本文将演示如何基于该 API 实现一个主页面与小窗的视频同步控制方案,包括播放/暂停、音量调节及进度跳转的双向实时同步。

为什么要使用 Document PiP API

相比传统方案,新的 API 具备以下优势:

  • 完整 HTML 支持:小窗内可包含按钮、进度条等任意交互元素
  • 无缝集成:与原始页面共享 JavaScript 上下文,便于状态管理
  • 尺寸灵活:可自定义小窗的宽高
  • 双向通信:主页面与小窗之间可实时同步数据

注意:目前该功能主要支持 Chrome 108+ 及以上版本,Firefox 和 Safari 尚未完全支持。

完整代码案例

代码中 1.mp4 为测试视频资源,实际使用时请替换为合法的视频源地址。

CSS 样式

*{
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    line-height: 1.6;
    color: #333;
    background: linear-gradient(135deg, #1a2a6c, #b21f1f, #1a2a6c);
    padding: 20px;
    min-height: 100vh;
}
.container {
    max-width: 1200px;
    :  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;
    : ;
}
 {
    : ;
}
 {
    : ;
}
(max-width: ) {
     {
        : column;
    }
}

目录

  1. 为什么要使用 Document PiP API
  2. 完整代码案例
  3. CSS 样式
  4. HTML 结构
  5. 演示效果
  6. 案例核心原理与流程
  7. 代码流程
  8. 状态同步机制
  9. 关键事件处理
  10. 结语
  • 💰 8折买阿里云服务器限时8折了解详情
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%
/* 16:9 Aspect Ratio */
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
900px
.content
flex-direction

HTML 结构

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>视频小窗模式演示</title>
    <link href="css/pip.css" rel="stylesheet" type="text/css"/>
</head>
<body>
<div class="container">
    <header>
        <h1>视频小窗模式演示</h1>
        <p class="subtitle">使用 Document Picture-in-Picture API 实现在其他内容上浮动播放视频</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>Document Picture-in-Picture API</h2>
            <div class="feature-list">
                <div class="feature">
                    <div class="feature-icon">1</div>
                    <div>
                        <h3>任意 HTML 内容</h3>
                        <p>可以在小窗中显示视频控件、字幕等任意 HTML 元素</p>
                    </div>
                </div>
                <div class="feature">
                    <div class="feature-icon">2</div>
                    <div>
                        <h3>保持播放状态</h3>
                        <p>进入小窗模式时视频持续播放,不中断观看体验</p>
                    </div>
                </div>
                <div class="feature">
                    <div class="feature-icon">3</div>
                    <div>
                        <h3>双向同步</h3>
                        <p>主页面和小窗中的视频状态实时同步</p>
                    </div>
                </div>
                <div class="feature">
                    <div class="feature-icon">4</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">
                        <svg width="24" height="24" fill="#4285F4" viewBox="0 0 24 24"><path d="M12 15.6l-3.9 2.3 1-4.3-3.2-2.9 4.3-.4L12 6.5l1.8 4.1 4.3.4-3.2 2.9 1 4.3z"/></svg>
                        <span class="supported">Chrome 108+</span>
                    </div>
                    <div class="browser">
                        <svg width="24" height="24" fill="#FF9500" viewBox="0 0 24 24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17.2 3H6.8l-5.2 9 5.2 9h10.4l5.2-9-5.2-9zm-1.15 16h-8.1l-4.04-7 4.04-7h8.09l4.04 7-4.03 7z"/></svg>
                        <span class="unsupported">Firefox</span>
                    </div>
                    <div class="browser">
                        <svg width="24" height="24" fill="#0078D7" viewBox="0 0 24 24"><path d="M0 0v24h24V0H0zm22 22H2V2h20v20z"/><path d="M12 12l-4 4 1.4 1.4 2.6-2.6 2.6 2.6 1.4-1.4z"/></svg>
                        <span class="unsupported">Edge</span>
                    </div>
                    <div class="browser">
                        <svg width="24" height="24" fill="#000000" viewBox="0 0 24 24"><path d="M18.7 4.3c-1.2-1.2-2.9-1.9-4.7-1.9H5C3.3 2.4 2 3.7 2 5.4v13.1c0 1.8 1.5 3.2 3.3 3.2H19c1.8 0 3.2-1.4 3.2-3.2V9c0-1.8-.7-3.5-1.9-4.7h-.6zM19 20.5H5.3c-.9 0-1.7-.7-1.7-1.7V5.4c0-.9.8-1.7 1.7-1.7h9c.9 0 1.7.7 1.7 1.7v3.9h3.9c.9 0 1.7.8 1.7 1.7v7.9c0 .9-.7 1.7-1.6 1.7z"/></svg>
                        <span class="unsupported">Safari</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.(, toggleFullscreen);
    closePipButton.(, closePictureInPicture);
    
    pipVideo. = mainVideo.;
    mainVideo. = ;
    pipVideo. = ;
}


 () {
    statusText. = message;
}


 () {
    pipStatus. = message;
}


  () {
     (!isPipSupported) ;
    
    
     (..) {
         ();
        ;
    }
    
     {
        
         pipWindow =  ..({
            : ,
            : ,
        });
        
        
        pipWindow.. = ;
        
        
         style = .();
        style. = ;
        pipWindow...(style);
        
        
        pipWindow...(pipVideo);
        
        
        pipVideo. = mainVideo.;
         (!mainVideo.) {
             pipVideo.();
        }  {
            pipVideo.();
        }
        
        
        pipWindow.(,  {
            
            pipContainer.(pipVideo);
            pipContainer.. = ;
            ();
        });
        
        
        pipContainer.. = ;
        ();
        ();
        
        
        mainVideo.(, syncVideoTime);
        pipVideo.(, syncVideoTime);
        
        mainVideo.(,  pipVideo.());
        mainVideo.(,  pipVideo.());
        pipVideo.(,  mainVideo.());
        pipVideo.(,  mainVideo.());
        
        
        mainVideo.(, syncVolume);
        pipVideo.(, syncVolume);
    }  (error) {
        ();
        .(error);
    }
}


 () {
    
     (.(mainVideo. - pipVideo.) > ) {
         ( === mainVideo) {
            pipVideo. = mainVideo.;
        }  {
            mainVideo. = pipVideo.;
        }
    }
}


 () {
     ( === mainVideo) {
        pipVideo. = mainVideo.;
        pipVideo. = mainVideo.;
    }  {
        mainVideo. = pipVideo.;
        mainVideo. = pipVideo.;
    }
}


  () {
     (..) {
        ...();
    }
    
    
    mainVideo.(, syncVideoTime);
    pipVideo.(, syncVideoTime);
    mainVideo.(, syncVolume);
    pipVideo.(, syncVolume);
    
    
    pipContainer.(pipVideo);
    pipContainer.. = ;
    ();
    ();
}


 () {
     (!.) {
         (mainVideo.) {
            mainVideo.();
        }   (mainVideo.) {
            mainVideo.();
        }   (mainVideo.) {
            mainVideo.();
        }
    }  {
         (.) {
            .();
        }   (.) {
            .();
        }   (.) {
            .();
        }
    }
}


.(, init);
</script>
</body>
</html>

演示效果

GIF 演示

案例核心原理与流程

代码流程

  1. 用户点击按钮 → 调用 documentPictureInPicture.requestWindow() 创建 PiP 窗口
  2. 将包含 <video> 的 DOM 节点移动到小窗口中
  3. 在两个窗口里同步播放、暂停、音量、当前时间等状态
  4. 窗口关闭时,通过监听 pagehide 或按钮将 DOM 恢复到主窗口

状态同步机制

同步机制

这里的关键在于避免死循环同步。我们在 syncVideoTime 函数中加入了阈值判断(0.5 秒),只有当时间差超过一定范围时才触发同步,防止频繁回调导致性能问题。

关键事件处理

  • timeupdate:同步播放进度,需处理防抖逻辑
  • play/pause:同步播放状态,确保两端动作一致
  • volumechange:同步音量设置
  • pagehide:检测小窗关闭,执行清理工作

结语

Document Picture-in-Picture API 为开发者提供了强大的工具来创建更灵活的视频观看体验。虽然目前浏览器支持有限(Chrome 116+),但随着标准的发展,相信它将成为视频播放页面的标配功能。

本方案演示了如何使用 window.documentPictureInPicture.requestWindow() API 创建一个自定义画中画窗口,并实现主窗口与小窗之间同步播放、暂停、音量控制与关闭逻辑。该方案相比传统 PiP 能实现更强的可定制化,适用于自定义播放器场景。

addEventListener
'click'
addEventListener
'click'
// 初始化视频源
src
src
muted
true
muted
true
// 更新状态显示
function
updateStatus
message
textContent
// 更新 PIP 状态显示
function
updatePipStatus
message
textContent
// 切换小窗模式
async
function
togglePictureInPicture
if
return
// 如果小窗已打开,则关闭
if
window
documentPictureInPicture
window
await
closePictureInPicture
return
try
// 打开小窗
const
await
window
documentPictureInPicture
requestWindow
width
400
height
300
// 设置小窗标题
document
title
"视频小窗播放"
// 添加样式
const
document
createElement
'style'
textContent
` body { margin: 0; background: black; height: 100vh; overflow: hidden; } video { width: 100%; height: 100%; object-fit: contain; } `
document
head
appendChild
// 添加视频元素到小窗
document
body
appendChild
// 同步播放状态
currentTime
currentTime
if
paused
await
play
else
pause
// 处理小窗关闭事件
addEventListener
'pagehide'
() =>
// 将视频元素移回主文档
appendChild
style
display
'none'
updatePipStatus
"已关闭"
// 显示小窗容器(用于样式)
style
display
'block'
updateStatus
"小窗模式已激活"
updatePipStatus
"运行中"
// 同步播放状态
addEventListener
'timeupdate'
addEventListener
'timeupdate'
addEventListener
'play'
() =>
play
addEventListener
'pause'
() =>
pause
addEventListener
'play'
() =>
play
addEventListener
'pause'
() =>
pause
// 同步音量
addEventListener
'volumechange'
addEventListener
'volumechange'
catch
updateStatus
`错误:${error.message}`
console
error
// 同步视频播放时间
function
syncVideoTime
// 避免循环同步
if
Math
abs
currentTime
currentTime
0.5
if
this
currentTime
currentTime
else
currentTime
currentTime
// 同步音量
function
syncVolume
if
this
volume
volume
muted
muted
else
volume
volume
muted
muted
// 关闭小窗模式
async
function
closePictureInPicture
if
window
documentPictureInPicture
window
window
documentPictureInPicture
window
close
// 移除事件监听器
removeEventListener
'timeupdate'
removeEventListener
'timeupdate'
removeEventListener
'volumechange'
removeEventListener
'volumechange'
// 将视频移回原始位置
appendChild
style
display
'none'
updateStatus
"小窗模式已关闭"
updatePipStatus
"未激活"
// 切换全屏模式
function
toggleFullscreen
if
document
fullscreenElement
if
requestFullscreen
requestFullscreen
else
if
webkitRequestFullscreen
webkitRequestFullscreen
else
if
msRequestFullscreen
msRequestFullscreen
else
if
document
exitFullscreen
document
exitFullscreen
else
if
document
webkitExitFullscreen
document
webkitExitFullscreen
else
if
document
msExitFullscreen
document
msExitFullscreen
// 初始化应用
document
addEventListener
'DOMContentLoaded'
  • GPT-5.5 超高智商模型1元抵1刀ChatGPT中转购买
  • 代充Chatgpt Plus/pro 帐号了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 手机浏览器 Console 日志同步至电脑调试指南
  • VSCode 本地部署 DeepSeek 模型实战指南
  • PyCharm 断点调试 GLM-4.6V-Flash-WEB Python 脚本
  • Python 常用数据结构:集合(Set)详解与实战
  • Ubuntu 部署 OpenClaw 智能体框架实战指南
  • 大模型 AI 产品经理成长攻略与核心学习路线
  • C++ 多线程同步之条件变量实战
  • MCP 工具速成:npx 与 uvx 全流程安装指南
  • Lostlife2.0 角色对话系统升级:基于 LLama-Factory 微调剧情模型
  • CKS 认证考试核心题型实战解析与避坑指南
  • AI 技术动态:Claude 记忆插件、LangChain DeepAgents 与具身智能进展
  • DeerFlow 2.0:字节开源的超级 Agent 框架
  • AnythingLLM 集成 Whisper 实战:构建高效语音转文本方案
  • MATLAB 实现基于 DQN-MLP 的无人机三维路径规划
  • AI 绘画与设计变现:90 天落地计划及提示词模板
  • AIGC 版权解析:生成内容归属、侵权认定与保护路径
  • 五大经典排序算法详解:插入、希尔、冒泡、选择与堆排序
  • 基于大模型辅助数据分析的开源项目 ezdata 架构解析
  • Java 网络编程:Socket 套接字基础与实现
  • Nginx 配置 HTTPS 实战:前端与后端接入指南

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online