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

使用 Document Picture-in-Picture API 实现视频小窗同步控制

基于 Document Picture-in-Picture API 实现视频画中画功能的完整方案。通过创建独立小窗口并将视频 DOM 节点移入其中,实现了主页面与小窗之间的双向状态同步,包括播放暂停、音量调节及进度跳转。相比传统 PiP 仅支持视频标签的限制,此方案支持自定义控件与交互,适用于视频会议、在线课程等复杂场景。代码涵盖 CSS 布局、HTML 结构及 JS 逻辑,重点展示了事件监听与状态同步机制,需注意 Chrome 116+ 版本支持。

暖阳发布于 2026/3/27更新于 2026/9/1059 浏览
使用 Document Picture-in-Picture API 实现视频小窗同步控制

前端实现 B 站视频画中画功能 - 完整代码实现主页面和小窗同步视频控制功能

1. 前言

在视频播放场景中,用户往往需要在观看视频的同时进行其他操作。B 站等平台的画中画功能允许视频以小窗口形式悬浮在其他内容之上,即使主窗口被缩小或切换,小窗口依然保留在桌面上。

随着 Chrome 116+ 支持 Document Picture-in-Picture API,我们不再局限于传统的 <video> 标签画中画,而是可以将整个文档内容移入独立的小窗口中,并在其中实现自定义控件、播放进度操作等功能。

本文将演示如何结合主页面与小窗,实现视频状态的实时同步控制(如暂停、音量调节、进度跳转),提升多任务场景下的用户体验。

2. 为什么要使用 Document PiP API

在多任务处理时代,传统画中画 API 功能有限,无法携带自定义控件与交互。新的 Document Picture-in-Picture API 允许整个文档出现在独立的小窗口中,支持丰富交互,特别适用于视频会议、在线课程及需要自定义控制的小应用。

新 API 优势:

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

3. 完整代码案例

代码示例中 src="1.mp4" 为本地测试视频路径,实际使用时请替换为有效的视频资源地址。

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();
        } 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>

3.2 演示效果

演示效果 GIF

4. 案例核心原理与流程

4.1 代码流程
  1. 用户点击按钮 → 调用 documentPictureInPicture.requestWindow({ width, height }) 创建 PiP 窗口。
  2. 将包含 <video> 的 DOM 节点移动到小窗口中。
  3. 在两个窗口里同步播放、暂停、音量、当前时间等状态。
  4. 窗口关闭时,通过监听 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. 前端实现 B 站视频画中画功能 - 完整代码实现主页面和小窗同步视频控制功能
  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. 结语

更多推荐文章

查看全部
  • 人工智能大模型项目实战:从需求到落地的全流程指南
  • 大模型 RAG 应用中的两种高级检索模式:融合检索与递归检索
  • Linux 进程替换详解:从 fork 到 exec 的完整链路
  • 软件设计各个模块分析
  • Whisper Large-V3-Turbo 语音识别技术解析与落地
  • RabbitMQ 与 Spring-AMQP 事务及消息限流机制详解
  • macOS Safari 浏览器开启开发者模式指南
  • Linux 高频面试题与详细解析
  • Python 列表基础与常用操作详解
  • Java 主流快速开发框架横评:若依、芋道、JeecgBoot 与 Jeesite
  • AI 辅助钱包开发:智能生成合约交互与监控脚本
  • 多模态大模型(MM-LLM)架构与训练综述
  • mT5 中文-base 模型 WebUI 响应超时与 GPU OOM 优化指南
  • C++ 智能指针完全指南:从原理到实战
  • 前端视角的 API 设计最佳实践
  • MATLAB 实现基于强制导向函数法(PFA)的无人机三维路径规划
  • 基于腾讯云服务器部署 Openclaw 并接入飞书与 NVIDIA 模型
  • 基于 FPGA 的微波炉控制器设计
  • Neo4j Desktop 2.0 安装教程:自定义安装路径
  • JavaScript 数据类型详解:分类、判断方式与核心差异

相关免费在线工具

  • 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