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

目录

  1. 1. 前言
  2. 2. 为什么要使用 Document PiP API
  3. 3. 完整代码案例
  4. 3.1 可直接复用运行代码
  5. CSS 代码
  6. HTML 代码
  7. 演示效果
  8. 4. 案例核心原理与流程
  9. 4.1 代码流程
  10. 4.2 状态同步机制
  11. 4.3 关键事件处理
  12. 5. 结语
  • 💰 8折买阿里云服务器限时8折了解详情
JavaScript大前端

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

如何使用 Document Picture-in-Picture API 在前端实现视频画中画功能。通过创建独立的小窗口播放视频,并实现主页面与小窗之间的播放状态、进度、音量等双向同步。提供了完整的 HTML、CSS 和 JavaScript 代码示例,适用于自定义播放器场景,提升了多任务处理下的视频观看体验。

松间照月发布于 2026/4/5更新于 2026/4/2010 浏览
前端实现视频画中画功能 - 主页面与小窗同步控制

在这里插入图片描述

1. 前言

不知道小伙伴是否发现 B 站的视频播放中,有一个功能 画中画 ,当用户点击会展现一个小窗播放,即使将主窗口缩起来,小窗口依然保留在外面电脑桌面上。

开启画中画(小窗口)

在这里插入图片描述

收缩主窗口

在这里插入图片描述

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

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


2. 为什么要使用 Document PiP API

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

与传统 Picture-in-Picture 的区别

在这里插入图片描述

  • 传统 <video> 的画中画 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"> 开启小窗模式  全屏 Document Picture-in-Picture API1任意 HTML 内容可以在小窗中显示视频控件、字幕等任意 HTML 元素2保持播放状态进入小窗模式时视频持续播放,不中断观看体验3双向同步主页面和小窗中的视频状态实时同步4自由调整用户可以调整小窗位置和大小,适应不同需求当前状态:等待操作小窗状态:未激活浏览器支持情况Chrome 108+FirefoxEdgeSafari关闭
演示效果

小伙伴们可以根据以下 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 能实现更强的可定制化,适用于自定义播放器场景。

  • 💰 8折买阿里云服务器限时8折购买
  • 🦞 5分钟部署阿里云小龙虾了解详情
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 心电信号(ECG)处理流程与核心算法详解
  • OpenClaw 多 Agent 对接飞书机器人架构与配置
  • Web3.0 开发实践
  • C++ 输入输出(IO)效率优化
  • AI 工具快速生成 PPT 与动漫风格漫画
  • 微信小程序健康菜谱分享网站:Python 技术架构与实现
  • MCP Server 实现 Excel 表格一键生成可视化图表 HTML 报告
  • OpenClaw 龙虾机器人 Windows 系统部署全攻略
  • OSCP 实战:破解 SSH 私钥的密码短语
  • 无人机安全测试工具 Drone Hacking Tool 使用指南
  • Moon VR Video Player 使用教程:支持 8K/12K 及多音轨字幕
  • Xcode 真机调试报错:Developer Disk Image 无法卸载或挂载
  • AI Copilot 在 VSCode 中的 7 大文档生成场景
  • 常见 AIGC 论文降重工具评测与对比
  • MySQL 到 KingbaseES 数据库迁移最佳实践指南
  • 系统架构与设计:空间、角色与开发流程
  • VSCode 关闭 Copilot 代码 AI 补全
  • Self-Attention 与 Multi-head Attention 核心原理及代码实现
  • 人工智能入门指南:零基础学习与实践
  • 使用 Web Unlocker API 获取亚马逊数据教程

相关免费在线工具

  • 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

<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">
<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">
</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">
</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">
</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">
</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;// 初始化页面functioninit(){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;}// 更新状态显示functionupdateStatus(message){ statusText.textContent = message;}// 更新 PIP 状态显示functionupdatePipStatus(message){ pipStatus.textContent = message;}// 切换小窗模式asyncfunctiontogglePictureInPicture(){if(!isPipSupported)return;// 如果小窗已打开,则关闭if(window.documentPictureInPicture.window){awaitclosePictureInPicture();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);}}// 同步视频播放时间functionsyncVideoTime(){// 避免循环同步if(Math.abs(mainVideo.currentTime - pipVideo.currentTime)>0.5){if(this=== mainVideo){ pipVideo.currentTime = mainVideo.currentTime;}else{ mainVideo.currentTime = pipVideo.currentTime;}}}// 同步音量functionsyncVolume(){if(this=== mainVideo){ pipVideo.volume = mainVideo.volume; pipVideo.muted = mainVideo.muted;}else{ mainVideo.volume = pipVideo.volume; mainVideo.muted = pipVideo.muted;}}// 关闭小窗模式asyncfunctionclosePictureInPicture(){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("未激活");}// 切换全屏模式functiontoggleFullscreen(){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>