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

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

综述由AI生成演示了如何利用 Document Picture-in-Picture API 在前端实现视频画中画功能。通过创建独立小窗口,支持自定义控件与交互,解决了传统 PiP 无法带自定义控件的问题。核心在于主页面与小窗之间的双向通信,实现了播放、暂停、音量及进度的实时同步。该方案适用于视频会议、在线课程等需要浮动播放的场景,提升了多任务处理时的用户体验。

数字游民发布于 2026/4/11更新于 2026/5/2212 浏览
前端实现视频画中画功能 - 主窗口与小窗同步控制

演示效果

你是否注意到 B 站等视频平台中有一个画中画功能?点击后视频会展现为一个小窗,即使将主窗口缩小或切换到其他应用,小窗口依然保留在桌面上播放。

随着 Chrome 116+ 支持 Document Picture-in-Picture API,我们不仅能播放视频,还能将整个页面内容移入画中画小窗口,并在其中实现自定义控件、进度条等操作。本文将演示如何实现主页面和小窗的视频同步控制,例如暂停、音量调节、进度跳转保持一致。

为什么要使用 Document PiP API

在多任务处理场景下,用户常需要在观看视频的同时浏览信息或回复消息。传统的小窗模式(PiP)功能有限,无法携带自定义控件与交互逻辑。新的 Document Picture-in-Picture API 允许整个文档出现在独立的小窗口中,支持丰富交互。

与传统 PiP 的区别

区别对比

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

完整代码案例

代码中 1.mp4 为测试视频资源,请自行替换 video 的 src 属性。

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: ;
    : ;
}

 {
    : ;
    :  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;
    }
}

目录

  1. 为什么要使用 Document PiP API
  2. 完整代码案例
  3. CSS 样式
  4. HTML 结构
  5. 演示效果
  6. 案例核心原理与流程
  7. 代码流程
  8. 状态同步机制
  9. 关键事件处理
  10. 结语
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • Llama-Recipes 增量备份与快照技术详解
  • 本地部署 Flux.1 绘画工具实测:中低显存设备的高质量生成方案
  • VSCode 接入智谱 GLM-4 及自定义大模型配置指南
  • 闲置手机变复古掌机:天马 G 前端安装与原理分析
  • Python 三维网格处理库 Trimesh 详解
  • 基于 Termux 的 Android 平台 OpenClaw 部署:移动端 AI 助理
  • 为什么选择 Python:核心特性与优势解析
  • Spring Boot 微服务架构设计与实战
  • 滑动窗口算法进阶:最大连续 1 与最小操作数
  • 基于 VoxCPM-1.5 的红外相机智能语音驱赶系统实战
  • Linux 基于匿名管道实现简易进程池
  • Dify 工作流发布为 MCP Server 实践指南
  • Xinference v1.17.1 基于 GitHub Codespaces 的云端 WebUI 快速部署
  • 大模型与 AIGC 概述及基础知识
  • Agent、RAG 与 LangChain:AI 应用开发的三大核心支柱
  • Vue 警告 Duplicate keys detected 重复键报错排查与解决
  • 主流 AI 编程工具对比:TRAE、Qoder、Cursor 与 GitHub Copilot
  • BFS 解决拓扑排序
  • Dify 工作流发布为 MCP Server 实战指南
  • ToDesk ToClaw 评测:基于 OpenClaw 的零门槛 AI 桌面自动化

相关免费在线工具

  • 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

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%
/* 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
max-width
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.(, 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 演示图,查看效果。

演示 GIF

案例核心原理与流程

代码流程

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

状态同步机制

同步机制

关键事件处理

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

结语

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

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

addEventListener
'click'
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'