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

基于 Document PiP API 实现视频小窗及状态同步

Document Picture-in-Picture API 为前端视频小窗播放提供了原生支持,允许整个文档进入独立窗口。本方案演示了如何创建自定义画中画窗口,并通过事件监听实现主窗口与子窗口在播放、暂停、进度及音量上的实时双向同步。相比传统 PiP,该方式支持更丰富的交互控件,适用于需要灵活视频体验的 Web 应用场景。

晚风叙旧发布于 2026/3/24更新于 2026/7/625 浏览
基于 Document PiP API 实现视频小窗及状态同步

基于 Document PiP API 实现视频小窗及状态同步

提示:本文演示如何利用 Document Picture-in-Picture API 实现网页视频的小窗播放功能,并支持主窗口与画中画窗口之间的双向状态同步。

1. 前言

在视频观看场景中,用户往往需要在播放的同时处理其他任务。传统的画中画(PiP)功能通常仅支持视频元素本身,无法携带自定义控件或交互逻辑。随着 Chrome 116+ 对 Document Picture-in-Picture API 的支持,我们能够将整个文档内容移入独立的小窗口中,并在其中保留完整的 HTML 结构和 JavaScript 上下文。

本文将带你实现一个完整案例:在主页面点击按钮后开启小窗模式,并确保主窗口与小窗在播放、暂停、进度、音量等状态上实时同步。

视频小窗效果示意

2. 为什么使用 Document PiP API

相比传统的 <video> 标签自带的 PiP 功能,新的 Document PiP 具有以下优势:

  • 完整 HTML 支持:小窗内可包含按钮、进度条、字幕等任意 DOM 元素。
  • 无缝集成:主页面与小窗共享 JavaScript 上下文,便于通信。
  • 尺寸灵活:可自定义小窗的宽度和高度。
  • 双向通信:主窗口与小窗可实时同步状态,提升用户体验。

3. 完整代码案例

以下示例包含 CSS 样式、HTML 结构及核心 JavaScript 逻辑。请确保本地有一个名为 1.mp4 的视频文件,或修改代码中的 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"><span class="supported">Chrome 116+</span></div>
                    <div class="browser"><span class="unsupported">Firefox</span></div>
                    <div class="browser"><span class="unsupported">Edge</span></div>
                    <div class="browser"><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 状态同步机制

为了保证体验一致,我们需要监听关键事件:

  • timeupdate:同步播放进度,注意设置阈值防止死循环。
  • play / pause:同步播放状态。
  • volumechange:同步音量设置。
  • pagehide:检测小窗关闭,执行清理工作。

5. 结语

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

本方案相比传统 PiP 能实现更强的可定制化,适用于自定义播放器场景。如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言讨论。

目录

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

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

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

更多推荐文章

查看全部
  • 面试高频问题:线上问题解决经验整理
  • JavaScript 原生实现图片轮播图
  • 滑动窗口算法实战:最小和子数组与最长无重复子串
  • 程序员必知的 10 大代码共享与协作平台
  • Vue3 + Python 校园排球俱乐部信息管理系统
  • Qwen2.5-7B 高性能 Web 推理服务部署指南
  • 基于 Rokid AR 眼镜的会议纪要助手开发实录
  • JDK 23 新特性与 ZGC 强制启用机制解析
  • Maven 高级特性:继承、聚合与私服配置
  • Flutter for OpenHarmony 集成 dart_openai 实现 AIGC 功能
  • 使用 Higress 将 REST API 转换为 MCP Server 工具
  • Java 经典排序算法全解析
  • C++ 基础概念
  • WebLogic 中间件高危漏洞应急响应案例分析
  • Cursor 实战:从零开发 Web 背单词应用
  • 算法题解:牛客 NC221681 dd 爱框框
  • 腾讯云开发 Copilot 低代码开发体验与技术分析
  • AI 产品经理职业路径与核心能力解析
  • 未来三年职场涨薪趋势分析与应对建议
  • 谷歌与OpenAI合作利用搜索增强大模型知识时效性

相关免费在线工具

  • 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