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

#【Code实战派】技术分享征文挑战赛#

在这里插入图片描述

🌷 古之立大事者,不惟有超世之才,亦必有坚忍不拔之志
🎐 个人CSND主页——Micro麦可乐的博客
🐥《Docker实操教程》专栏以最新的Centos版本为基础进行Docker实操教程,入门到实战
🌺《RabbitMQ》专栏19年编写主要介绍使用JAVA开发RabbitMQ的系列教程,从基础知识到项目实战
🌸《设计模式》专栏以实际的生活场景为案例进行讲解,让大家对设计模式有一个更清晰的理解
🌛《开源项目》本专栏主要介绍目前热门的开源项目,带大家快速了解并轻松上手使用
🍎 《前端技术》专栏以实战为主介绍日常开发中前端应用的一些功能以及技巧,均附有完整的代码示例
✨《开发技巧》本专栏包含了各种系统的设计原理以及注意事项,并分享一些日常开发的功能小技巧
💕《Jenkins实战》专栏主要介绍Jenkins+Docker的实战教程,让你快速掌握项目CI/CD,是2024年最新的实战教程
🌞《Spring Boot》专栏主要介绍我们日常工作项目中经常应用到的功能以及技巧,代码样例完整
👍《Spring Security》专栏中我们将逐步深入Spring Security的各个技术细节,带你从入门到精通,全面掌握这一安全技术
如果文章能够给大家带来一定的帮助!欢迎关注、评论互动~

1. 前言

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

开启画中画(小窗口)
在这里插入图片描述
收缩主窗口

在这里插入图片描述

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

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


2. 为什么要使用 Document PiP API

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

与传统 Picture-in-Picture 的区别
在这里插入图片描述

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

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


3. 完整代码案例

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

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 代码流程

  • 用户点击按钮 → 调用 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 能实现更强的可定制化,适用于自定义播放器场景。

如果你在实践过程中有任何疑问或更好的扩展思路,欢迎在评论区留言,最后希望大家 一键三连 给博主一点点鼓励!


前端技术专栏回顾:

01【前端技术】 ES6 介绍及常用语法说明
02【前端技术】标签页通讯localStorage、BroadcastChannel、SharedWorker的技术详解
03 前端请求乱序问题分析与AbortController、async/await、Promise.all等解决方案
04 前端开发中深拷贝的循环引用问题:从问题复现到完美解决
05 前端AJAX请求上传下载进度监控指南详解与完整代码示例
06 TypeScript 进阶指南 - 使用泛型与keyof约束参数
07 前端实现视频文件动画帧图片提取全攻略 - 附完整代码样例
08 前端函数防抖(Debounce)完整讲解 - 从原理、应用到完整实现
09 JavaScript异步编程 Async/Await 使用详解:从原理到最佳实践
10 前端图片裁剪上传全流程详解:从预览到上传的完整流程
11 前端大文件分片上传详解 - Spring Boot 后端接口实现
12 前端实现图片防盗链技术详解 - 原理分析与SpringBoot解决方案
13 前端拖拽排序实现详解:从原理到实践 - 附完整代码
14 前端Base64格式文件上传详解:原理、实现与最佳实践
15 一文看懂Proxy与Object.defineProperty深度解析 - JavaScript的拦截艺术
在这里插入图片描述

评论 79
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Micro麦可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值