当前位置: 首页 > news >正文

documentPictureInPicture API 教程

documentPictureInPicture API 教程

概述

documentPictureInPicture API 是一个强大的 Web API,允许开发者创建始终置顶的浮动窗口,用户可以在其中放置任意的 HTML 内容。与传统的视频画中画不同,这个 API 支持完整的 DOM 内容,包括交互式元素、表单、按钮等。

浏览器支持

目前该 API 主要在基于 Chromium 的浏览器中支持:

  • Chrome 116+
  • Edge 116+
  • Opera 102+

Firefox 和 Safari 暂时不支持此 API。

基本概念

什么是 documentPictureInPicture?

documentPictureInPicture 允许您:

  • 创建一个独立的浮动窗口
  • 在窗口中显示完整的 HTML 内容
  • 保持窗口始终置顶
  • 支持用户交互和 JavaScript 执行

主要用途

  • 媒体播放器控制面板
  • 聊天应用的浮动窗口
  • 实时数据监控面板
  • 工具栏和快捷操作面板
  • 视频会议的参与者列表

API 参考

检查浏览器支持

if ('documentPictureInPicture' in window) {console.log('documentPictureInPicture API 支持');
} else {console.log('documentPictureInPicture API 不支持');
}

主要方法和属性

documentPictureInPicture.requestWindow(options)

创建一个新的画中画窗口。

参数:

  • options
    

    (可选): 配置对象

    • width: 窗口宽度(像素)
    • height: 窗口高度(像素)

返回值: Promise,解析为 Window 对象

documentPictureInPicture.window

返回当前打开的画中画窗口对象,如果没有则返回 null

事件

enter 事件

当画中画窗口打开时触发。

documentPictureInPicture.addEventListener('enter', (event) => {console.log('画中画窗口已打开', event.window);
});

基础用法

1. 创建简单的画中画窗口

async function openPictureInPicture() {try {// 检查浏览器支持if (!('documentPictureInPicture' in window)) {throw new Error('不支持 documentPictureInPicture API');}// 创建画中画窗口const pipWindow = await documentPictureInPicture.requestWindow({width: 400,height: 300});// 在窗口中添加内容pipWindow.document.body.innerHTML = `<div style="font-family: Arial, sans-serif; padding: 20px;"><h2>画中画窗口</h2><p>这是一个浮动的画中画窗口!</p><button onclick="window.close()">关闭窗口</button></div>`;console.log('画中画窗口已创建');} catch (error) {console.error('创建画中画窗口失败:', error);}
}

2. 复制现有元素到画中画窗口

async function moveElementToPiP(elementId) {try {const element = document.getElementById(elementId);if (!element) {throw new Error('未找到指定元素');}const pipWindow = await documentPictureInPicture.requestWindow({width: 500,height: 400});// 复制元素的样式const styles = Array.from(document.styleSheets).map(styleSheet => {try {return Array.from(styleSheet.cssRules).map(rule => rule.cssText).join('\n');} catch (e) {return '';}}).join('\n');// 创建样式标签const styleElement = pipWindow.document.createElement('style');styleElement.textContent = styles;pipWindow.document.head.appendChild(styleElement);// 移动元素到画中画窗口pipWindow.document.body.appendChild(element);// 监听窗口关闭事件,将元素移回原位置pipWindow.addEventListener('beforeunload', () => {document.body.appendChild(element);});} catch (error) {console.error('移动元素到画中画失败:', error);}
}

高级用法

1. 创建媒体播放器控制面板

class MediaControllerPiP {constructor(videoElement) {this.video = videoElement;this.pipWindow = null;}async openController() {try {this.pipWindow = await documentPictureInPicture.requestWindow({width: 400,height: 200});this.setupControllerUI();this.bindEvents();} catch (error) {console.error('打开控制面板失败:', error);}}setupControllerUI() {this.pipWindow.document.head.innerHTML = `<style>body {font-family: Arial, sans-serif;margin: 0;padding: 20px;background: #1a1a1a;color: white;}.controls {display: flex;align-items: center;gap: 10px;margin-bottom: 15px;}button {padding: 8px 16px;border: none;border-radius: 4px;background: #333;color: white;cursor: pointer;}button:hover {background: #555;}.progress {width: 100%;margin-top: 10px;}.time-display {font-size: 14px;margin-top: 10px;}</style>`;this.pipWindow.document.body.innerHTML = `<div class="media-controller"><h3>媒体控制面板</h3><div class="controls"><button id="playPause">播放/暂停</button><button id="stop">停止</button><button id="mute">静音</button></div><input type="range" id="progress" class="progress" min="0" max="100" value="0"><input type="range" id="volume" min="0" max="1" step="0.1" value="1"><div class="time-display" id="timeDisplay">00:00 / 00:00</div></div>`;}bindEvents() {const playPauseBtn = this.pipWindow.document.getElementById('playPause');const stopBtn = this.pipWindow.document.getElementById('stop');const muteBtn = this.pipWindow.document.getElementById('mute');const progressBar = this.pipWindow.document.getElementById('progress');const volumeBar = this.pipWindow.document.getElementById('volume');playPauseBtn.addEventListener('click', () => {if (this.video.paused) {this.video.play();} else {this.video.pause();}});stopBtn.addEventListener('click', () => {this.video.pause();this.video.currentTime = 0;});muteBtn.addEventListener('click', () => {this.video.muted = !this.video.muted;});progressBar.addEventListener('input', (e) => {const time = (e.target.value / 100) * this.video.duration;this.video.currentTime = time;});volumeBar.addEventListener('input', (e) => {this.video.volume = e.target.value;});// 更新进度显示this.video.addEventListener('timeupdate', () => {this.updateProgress();});}updateProgress() {if (!this.pipWindow) return;const progress = (this.video.currentTime / this.video.duration) * 100;const progressBar = this.pipWindow.document.getElementById('progress');const timeDisplay = this.pipWindow.document.getElementById('timeDisplay');if (progressBar) {progressBar.value = progress;}if (timeDisplay) {const current = this.formatTime(this.video.currentTime);const total = this.formatTime(this.video.duration);timeDisplay.textContent = `${current} / ${total}`;}}formatTime(seconds) {const mins = Math.floor(seconds / 60);const secs = Math.floor(seconds % 60);return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;}
}// 使用示例
// const video = document.getElementById('myVideo');
// const controller = new MediaControllerPiP(video);
// controller.openController();

2. 数据监控仪表板

class MonitoringDashboard {constructor() {this.pipWindow = null;this.updateInterval = null;}async open() {try {this.pipWindow = await documentPictureInPicture.requestWindow({width: 350,height: 250});this.setupDashboard();this.startMonitoring();// 窗口关闭时清理定时器this.pipWindow.addEventListener('beforeunload', () => {this.stopMonitoring();});} catch (error) {console.error('打开监控面板失败:', error);}}setupDashboard() {this.pipWindow.document.head.innerHTML = `<style>body {font-family: Arial, sans-serif;margin: 0;padding: 15px;background: #0f0f0f;color: #00ff00;font-size: 12px;}.metric {display: flex;justify-content: space-between;margin-bottom: 8px;padding: 5px;background: #1a1a1a;border-radius: 3px;}.value {font-weight: bold;}.status-good { color: #00ff00; }.status-warning { color: #ffaa00; }.status-error { color: #ff0000; }</style>`;this.pipWindow.document.body.innerHTML = `<div class="dashboard"><h3>系统监控</h3><div class="metric"><span>CPU 使用率:</span><span class="value" id="cpu">--</span></div><div class="metric"><span>内存使用:</span><span class="value" id="memory">--</span></div><div class="metric"><span>网络状态:</span><span class="value" id="network">--</span></div><div class="metric"><span>活跃连接:</span><span class="value" id="connections">--</span></div><div class="metric"><span>最后更新:</span><span class="value" id="lastUpdate">--</span></div></div>`;}startMonitoring() {this.updateMetrics();this.updateInterval = setInterval(() => {this.updateMetrics();}, 2000);}stopMonitoring() {if (this.updateInterval) {clearInterval(this.updateInterval);this.updateInterval = null;}}async updateMetrics() {if (!this.pipWindow) return;// 模拟获取系统指标const metrics = await this.getSystemMetrics();const cpuElement = this.pipWindow.document.getElementById('cpu');const memoryElement = this.pipWindow.document.getElementById('memory');const networkElement = this.pipWindow.document.getElementById('network');const connectionsElement = this.pipWindow.document.getElementById('connections');const lastUpdateElement = this.pipWindow.document.getElementById('lastUpdate');if (cpuElement) {cpuElement.textContent = `${metrics.cpu}%`;cpuElement.className = `value ${this.getStatusClass(metrics.cpu, 80, 90)}`;}if (memoryElement) {memoryElement.textContent = `${metrics.memory}%`;memoryElement.className = `value ${this.getStatusClass(metrics.memory, 75, 85)}`;}if (networkElement) {networkElement.textContent = metrics.networkStatus;networkElement.className = `value status-${metrics.networkStatus === '在线' ? 'good' : 'error'}`;}if (connectionsElement) {connectionsElement.textContent = metrics.connections;connectionsElement.className = 'value status-good';}if (lastUpdateElement) {lastUpdateElement.textContent = new Date().toLocaleTimeString();}}async getSystemMetrics() {// 模拟系统指标数据return {cpu: Math.floor(Math.random() * 100),memory: Math.floor(Math.random() * 100),networkStatus: Math.random() > 0.1 ? '在线' : '离线',connections: Math.floor(Math.random() * 50) + 10};}getStatusClass(value, warningThreshold, errorThreshold) {if (value >= errorThreshold) return 'status-error';if (value >= warningThreshold) return 'status-warning';return 'status-good';}
}// 使用示例
// const dashboard = new MonitoringDashboard();
// dashboard.open();

最佳实践

1. 样式处理

function copyStylesToPipWindow(pipWindow) {// 复制所有样式表Array.from(document.styleSheets).forEach(styleSheet => {try {const cssRules = Array.from(styleSheet.cssRules);const style = pipWindow.document.createElement('style');style.textContent = cssRules.map(rule => rule.cssText).join('\n');pipWindow.document.head.appendChild(style);} catch (e) {// 处理跨域样式表console.warn('无法复制样式表:', e);}});// 复制内联样式const linkElements = document.querySelectorAll('link[rel="stylesheet"]');linkElements.forEach(link => {const newLink = pipWindow.document.createElement('link');newLink.rel = 'stylesheet';newLink.href = link.href;pipWindow.document.head.appendChild(newLink);});
}

2. 响应式设计

function createResponsivePipWindow(content, minWidth = 300, minHeight = 200) {return documentPictureInPicture.requestWindow({width: Math.max(minWidth, window.innerWidth * 0.3),height: Math.max(minHeight, window.innerHeight * 0.4)}).then(pipWindow => {// 添加响应式样式const style = pipWindow.document.createElement('style');style.textContent = `body {margin: 0;padding: 10px;box-sizing: border-box;overflow: auto;}@media (max-width: 400px) {body { padding: 5px; font-size: 14px; }}`;pipWindow.document.head.appendChild(style);pipWindow.document.body.innerHTML = content;return pipWindow;});
}

3. 错误处理和降级方案

class PictureInPictureManager {static async openWithFallback(content, options = {}) {try {// 检查 API 支持if (!('documentPictureInPicture' in window)) {throw new Error('API_NOT_SUPPORTED');}// 检查是否已有窗口打开if (documentPictureInPicture.window) {documentPictureInPicture.window.close();}const pipWindow = await documentPictureInPicture.requestWindow(options);return pipWindow;} catch (error) {console.warn('画中画失败,使用降级方案:', error);return this.createModalFallback(content);}}static createModalFallback(content) {// 创建模态框作为降级方案const modal = document.createElement('div');modal.style.cssText = `position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);background: white;border: 1px solid #ccc;box-shadow: 0 4px 6px rgba(0,0,0,0.1);z-index: 10000;max-width: 90vw;max-height: 90vh;overflow: auto;`;modal.innerHTML = content;document.body.appendChild(modal);// 返回类似 Window 的对象return {document: { body: modal },close: () => modal.remove(),addEventListener: (event, handler) => {if (event === 'beforeunload') {// 在模态框被移除时触发const observer = new MutationObserver((mutations) => {mutations.forEach((mutation) => {if (mutation.type === 'childList' && !document.body.contains(modal)) {handler();observer.disconnect();}});});observer.observe(document.body, { childList: true });}}};}
}

注意事项和限制

1. 安全限制

  • 只能在用户手势(如点击)触发的情况下调用
  • 同时只能有一个画中画窗口
  • 不能直接访问父窗口的 DOM

2. 性能考虑

  • 画中画窗口会消耗额外的系统资源
  • 避免在窗口中放置过于复杂的内容
  • 及时清理事件监听器和定时器

3. 用户体验

  • 提供清晰的打开/关闭画中画的控制
  • 确保画中画内容在小窗口中仍然可用
  • 考虑用户可能调整窗口大小

调试技巧

1. 检查窗口状态

function debugPipWindow() {const pipWindow = documentPictureInPicture.window;if (pipWindow) {console.log('画中画窗口状态:');console.log('- 宽度:', pipWindow.innerWidth);console.log('- 高度:', pipWindow.innerHeight);console.log('- 是否关闭:', pipWindow.closed);console.log('- 文档标题:', pipWindow.document.title);} else {console.log('没有活跃的画中画窗口');}
}

2. 事件监听

// 监听所有画中画相关事件
documentPictureInPicture.addEventListener('enter', (event) => {console.log('画中画窗口打开:', event.window);event.window.addEventListener('resize', () => {console.log('窗口大小改变:', event.window.innerWidth, 'x', event.window.innerHeight);});event.window.addEventListener('beforeunload', () => {console.log('画中画窗口即将关闭');});
});

总结

documentPictureInPicture API 为 Web 应用提供了创建浮动窗口的强大能力。通过合理使用这个 API,可以显著提升用户体验,特别是在需要多任务处理或实时监控的场景中。

记住要:

  • 始终检查浏览器支持
  • 提供适当的降级方案
  • 注意性能和用户体验
  • 正确处理窗口生命周期
  • 遵循 Web 标准和最佳实践

这个 API 还在不断发展中,未来可能会有更多功能和改进。保持关注最新的规范更新和浏览器支持情况。

http://www.dtcms.com/a/308543.html

相关文章:

  • IK 字段级别词典的升级之路
  • 14day-ai入门-人工智能基础学习-OpenCV-图像预处理4
  • 2683. 相邻值的按位异或
  • GXHT30温湿度传感器可兼容SHT30
  • NMOS防反接电路分析
  • [特殊字符] 数字孪生 + 数据可视化:实战经验分享,让物理世界数据 “会说话”
  • ubuntu18.04 部署nfs服务
  • 第15届蓝桥杯C++青少组中级组选拔赛(STEMA)2024年3月10日真题
  • Java与MySQL AES加密解密实战指南
  • pytest vs unittest: 区别与优缺点比较
  • #C语言——学习攻略:深挖指针路线(五)--回调函数,qsort函数,qsort函数的模拟实现
  • ACOSRAR改进连续蚁群算法用于优化复杂环境下无人机路径规划,Matlab代码实现
  • 中烟创新参编的《软件和信息技术服务行业企业环境社会治理信息披露指南》标准正式发布
  • 树形DP-核心基础
  • 《质光相济:Three.js中3D视觉的底层交互逻辑》
  • 直击WAIC | 百度袁佛玉:加速具身智能技术及产品研发,助力场景应用多样化落地
  • 虚幻基础:模型穿模
  • 产品型号:PCD231B101产品类型:信号隔离转换模块
  • Redis学习14-认识哨兵机制
  • cesium视锥体
  • 【C#】基于SharpCompress实现压缩包解压功能
  • TDengine 中 TDgp 中添加算法模型(预测分析)
  • Spring Security之初体验
  • 智慧社区项目开发(四)——前后端登录认证相关功能实现解析
  • QT Word模板 + QuaZIP + LibreOffice,跨平台方案实现导出.docx文件后再转为.pdf文件
  • 安全月报 | 傲盾DDoS攻击防御2025年7月简报
  • 功能强大编辑器
  • [Agent开发平台] 可观测性(追踪与指标) | 依赖注入模式 | Wire声明式配置
  • 量子安全:微算法科技(MLGO)基于比特币的非对称共识链算法引领数字经济未来
  • Linux安装AnythingLLM