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 还在不断发展中,未来可能会有更多功能和改进。保持关注最新的规范更新和浏览器支持情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值