跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
JavaScript大前端

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

利用 Chrome 116+ 支持的 Document Picture-in-Picture API,实现网页视频独立小窗播放。通过监听主窗口与小窗口的播放状态、进度及音量事件,完成实时双向同步。该方案突破传统 PiP 限制,支持自定义控件,适用于在线课程或会议场景。

神经兮兮发布于 2026/3/29更新于 2026/7/2941 浏览
前端实现视频画中画功能 - 主页面与小窗同步控制

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

演示效果

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志

1. 前言

不知道大家是否注意到,B 站等视频平台在播放时提供了一个 画中画 功能。当用户点击后,会展现一个小窗口播放视频。即使将主窗口缩小或切换到其他应用,小窗口依然保留在桌面上。

开启画中画(小窗口)

开启画中画

收缩主窗口

收缩主窗口

随着 Chrome 116+ 支持 Document Picture-in-Picture API 的出现,我们终于可以把整个页面内容(不仅仅是 <video>)移入画中画小窗口中,并在小窗中实现自定义控件、播放进度操作等功能。

本文将带大家实现一个主页面和小窗同步视频控制的功能,例如在主窗口暂停、小窗也同步暂停;调节音量、跳转进度也保持一致,提升用户体验。


2. 为什么要使用 Document PiP API

在当今多任务处理的时代,用户经常需要在观看视频的同时进行其他操作(如浏览信息、回复消息等)。小窗模式(画中画)解决了这一需求,让视频可以浮动在页面上方,同时用户可以自由浏览其他内容。

与传统 Picture-in-Picture 的区别

区别对比

  • 传统的画中画 API 功能有限,无法带自定义控件与交互
  • 新的 API 可以让整个文档出现在独立的小窗口中,支持丰富交互,如播放、暂停、音量、进度条等
  • 特别适用于视频会议、在线课程、弹幕播放器,以及需要自定义控制画中画的小应用

新 API 优势

  • 完整 HTML 支持:可包含按钮、进度条等交互元素
  • 无缝集成:与原始页面共享 JavaScript 上下文
  • 尺寸灵活:可自定义小窗尺寸
  • 双向通信:主页面与小窗实时同步

3. 完整代码案例

代码中的 1.mp4 为测试用的本地视频资源,大家可以自行获取相应资源源,修改 的 即可。

video
src

3.1 可直接复用运行代码

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;margin: 0 auto;background-color:rgba(255, 255, 255, 0.95);border-radius: 15px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);overflow: hidden;}header{background:linear-gradient(to right, #1a2a6c, #b21f1f);color: white;padding: 25px 40px;text-align: center;}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 auto;}.content{display: flex;padding: 30px;gap: 30px;}.video-section{flex: 3;background: #f8f9fa;border-radius: 10px;overflow: hidden;box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);}.video-container{position: relative;padding-top: 56.25%;/* 16:9 Aspect Ratio */background: #000;}video{position: absolute;top: 0;left: 0;width: 100%;height: 100%;display: block;}.video-controls{display: flex;padding: 15px;gap: 10px;background: #e9ecef;}button{background: #1a2a6c;color: white;border: none;padding: 10px 20px;border-radius: 5px;cursor: pointer;font-weight: 600;transition: all 0.3s ease;display: flex;align-items: center;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: not-allowed;transform: none;box-shadow: none;}.info-section{flex: 2;background: white;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 solid #e9ecef;}.feature-list{margin: 20px 0;}.feature{display: flex;align-items: flex-start;margin-bottom: 15px;}.feature-icon{background: #1a2a6c;color: white;width: 30px;height: 30px;border-radius: 50%;display: flex;align-items: center;justify-content: center;margin-right: 15px;flex-shrink: 0;}.pip-window{position: fixed;bottom: 20px;right: 20px;width: 300px;height: 200px;background: black;border-radius: 10px;overflow: hidden;box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);z-index: 1000;display: none;}.pip-window video{width: 100%;height: 100%;object-fit: cover;}.pip-controls{position: absolute;bottom: 10px;left: 0;right: 0;display: flex;justify-content: center;gap: 10px;opacity: 0;transition: opacity 0.3s;}.pip-window:hover .pip-controls{opacity: 1;}.status{padding: 15px;background: #e9ecef;border-radius: 8px;margin-top: 20px;font-family: monospace;}.browser-support{margin-top: 30px;padding: 20px;background: #fff8e1;border-radius: 8px;border-left: 4px solid #ffc107;}.support-list{display: flex;gap: 15px;margin-top: 15px;flex-wrap: wrap;}.browser{display: flex;align-items: center;gap: 8px;}.supported{color: #28a745;}.unsupported{color: #dc3545;}@media(max-width: 900px){.content{flex-direction: column;}}

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.addEventListener('click', toggleFullscreen); closePipButton.addEventListener('click', closePictureInPicture);// 初始化视频源 pipVideo.src = mainVideo.src; mainVideo.muted = true; pipVideo.muted = true;}// 更新状态显示function updateStatus(message){ statusText.textContent = message;}// 更新 PIP 状态显示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();}elseif(mainVideo.webkitRequestFullscreen){ mainVideo.webkitRequestFullscreen();}elseif(mainVideo.msRequestFullscreen){ mainVideo.msRequestFullscreen();}}else{if(document.exitFullscreen){ document.exitFullscreen();}elseif(document.webkitExitFullscreen){ document.webkitExitFullscreen();}elseif(document.msExitFullscreen){ document.msExitFullscreen();}}}// 初始化应用 document.addEventListener('DOMContentLoaded', init);</script></body></html>

3.2 演示效果

小伙伴们可以根据以下 GIF 演示图,查看效果。

演示 GIF


4. 案例核心原理与流程

4.1 代码流程

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

4.2 状态同步机制

同步机制

4.3 关键事件处理

  • timeupdate:同步播放进度
  • play/pause:同步播放状态
  • volumechange:同步音量设置
  • pagehide:检测小窗关闭

5. 结语

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

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

如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言交流。

目录

  1. 前端实现视频画中画功能 - 主页面与小窗同步控制
  2. 1. 前言
  3. 2. 为什么要使用 Document PiP API
  4. 3. 完整代码案例
  5. 3.1 可直接复用运行代码
  6. 3.2 演示效果
  7. 4. 案例核心原理与流程
  8. 4.1 代码流程
  9. 4.2 状态同步机制
  10. 4.3 关键事件处理
  11. 5. 结语
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 从 “吹爆” 到 “冷静”:AIGC + 低代码为何难破企业级开发的硬骨头?
  • 搭建前端开发环境:安装 nvm、Node.js 与 pnpm 配置
  • Claude Code 核心功能与使用详解
  • 大模型微调核心:数据准备与 3 个实例详解
  • 2026 年字节跳动豆包大模型 2.0 全场景 AI 智能体平台解读
  • FPGA 时钟约束详解:create_clock 与 create_generated_clock
  • Coze 工作流实战:逻辑控制、数据处理与 AIGC 多媒体应用
  • 精易模块 Excel 操作实战:构建中小学生成绩管理系统
  • 基于 STM32F407 与 K230 的二维云台激光打靶系统设计
  • 基于 OpenClaw 与飞书搭建多 Agent AI 助理团队
  • GitHub Copilot 免费版与 Pro 版差异解析及选型建议
  • GitHub 教育认证通过后如何领取 Copilot Pro
  • 小红书 AlignRec: 多模态推荐系统的对齐与训练框架
  • 二级 Python 考试真题及参考代码合集(简单应用题部分)
  • VLSI 设计中的斯坦纳树算法演进:从 FLUTE 到现代布线
  • 15 个超级实用的 Python 技巧,提升开发效率
  • 大学生论文写作:AI 工具全流程实战指南
  • AI 幻觉深度解析:成因、风险与应对策略
  • OpenClaw AI 智能体部署与使用指南
  • AI Agent 记忆系统技术综述:核心挑战与主流方案

相关免费在线工具

  • 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