JSMpeg全屏功能:HTML5 Fullscreen API的跨浏览器实现

JSMpeg全屏功能:HTML5 Fullscreen API的跨浏览器实现

【免费下载链接】jsmpeg MPEG1 Video Decoder in JavaScript 【免费下载链接】jsmpeg 项目地址: https://gitcode.com/gh_mirrors/js/jsmpeg

引言:你还在为视频播放器全屏适配烦恼吗?

在Web开发中,实现可靠的全屏功能一直是前端工程师的痛点。特别是在使用JSMpeg这类高性能视频解码库时,全屏控制不仅关乎用户体验,更涉及复杂的浏览器兼容性问题。本文将系统讲解如何基于HTML5 Fullscreen API为JSMpeg播放器实现跨浏览器全屏功能,解决从API封装到事件处理的全流程技术挑战。

读完本文你将获得:

  • 一套完整的JSMpeg全屏功能实现方案
  • 跨浏览器兼容性处理的实战经验
  • 全屏状态管理与视频渲染优化技巧
  • 完整可复用的代码示例与集成指南

全屏功能的技术现状与挑战

浏览器兼容性矩阵

特性ChromeFirefoxSafariEdgeIE
基础API支持✅ 15+✅ 10+✅ 5.1+✅ 12+
前缀要求webkitmozwebkit标准
键盘事件捕获⚠️有限支持
视频尺寸调整⚠️需手动处理
退出全屏事件

全屏实现的核心挑战

  1. API碎片化:不同浏览器对Fullscreen API的实现存在前缀差异
  2. 状态同步:播放器状态与全屏状态的双向同步
  3. 渲染适配:全屏模式下的视频分辨率与画布尺寸调整
  4. 用户体验:键盘快捷键处理与全屏切换动画
  5. 错误处理:全屏请求被拒绝的场景处理

JSMpeg架构与全屏功能设计

JSMpeg播放器核心组件

mermaid

全屏功能架构设计

mermaid

跨浏览器全屏API封装

标准化全屏方法

/**
 * 全屏API封装工具类
 */
const FullscreenHelper = {
    // 检测浏览器支持的全屏API方法
    getSupportedMethods() {
        const methods = {
            request: null,
            exit: null,
            element: null,
            changeEvent: null,
            errorEvent: null
        };
        
        if (document.documentElement.requestFullscreen) {
            methods.request = 'requestFullscreen';
            methods.exit = 'exitFullscreen';
            methods.element = 'fullscreenElement';
            methods.changeEvent = 'fullscreenchange';
            methods.errorEvent = 'fullscreenerror';
        } else if (document.documentElement.webkitRequestFullscreen) {
            methods.request = 'webkitRequestFullscreen';
            methods.exit = 'webkitExitFullscreen';
            methods.element = 'webkitFullscreenElement';
            methods.changeEvent = 'webkitfullscreenchange';
            methods.errorEvent = 'webkitfullscreenerror';
        } else if (document.documentElement.mozRequestFullScreen) {
            methods.request = 'mozRequestFullScreen';
            methods.exit = 'mozCancelFullScreen';
            methods.element = 'mozFullScreenElement';
            methods.changeEvent = 'mozfullscreenchange';
            methods.errorEvent = 'mozfullscreenerror';
        }
        
        return methods;
    },
    
    // 检查是否支持全屏API
    isSupported() {
        return !!this.getSupportedMethods().request;
    },
    
    // 请求全屏
    async request(element) {
        const methods = this.getSupportedMethods();
        if (!methods.request) return false;
        
        try {
            if (methods.request === 'webkitRequestFullscreen') {
                // Safari需要特殊处理
                await element[methods.request]();
            } else {
                await element[methods.request]();
            }
            return true;
        } catch (e) {
            console.error('全屏请求失败:', e);
            return false;
        }
    },
    
    // 退出全屏
    async exit() {
        const methods = this.getSupportedMethods();
        if (!methods.exit || !this.isFullscreen()) return false;
        
        try {
            await document[methods.exit]();
            return true;
        } catch (e) {
            console.error('退出全屏失败:', e);
            return false;
        }
    },
    
    // 切换全屏状态
    async toggle(element) {
        return this.isFullscreen() ? this.exit() : this.request(element);
    },
    
    // 检查是否处于全屏状态
    isFullscreen() {
        const methods = this.getSupportedMethods();
        if (!methods.element) return false;
        return !!document[methods.element];
    },
    
    // 添加全屏状态变化事件监听
    onFullscreenChange(callback) {
        const methods = this.getSupportedMethods();
        if (!methods.changeEvent) return;
        
        document.addEventListener(methods.changeEvent, callback);
        return () => {
            document.removeEventListener(methods.changeEvent, callback);
        };
    },
    
    // 添加全屏错误事件监听
    onFullscreenError(callback) {
        const methods = this.getSupportedMethods();
        if (!methods.errorEvent) return;
        
        document.addEventListener(methods.errorEvent, callback);
        return () => {
            document.removeEventListener(methods.errorEvent, callback);
        };
    }
};

集成到JSMpeg播放器

// 修改Player类,添加全屏相关方法
Player.prototype.initFullscreen = function() {
    // 检查Fullscreen API支持情况
    this.fullscreenSupported = FullscreenHelper.isSupported();
    if (!this.fullscreenSupported) {
        console.warn('Fullscreen API is not supported in this browser');
        return;
    }
    
    // 获取播放器容器元素
    this.containerElement = this.options.container || this.renderer.canvas.parentElement;
    
    // 存储原始尺寸
    this.originalSize = {
        width: this.renderer.canvas.width,
        height: this.renderer.canvas.height
    };
    
    // 绑定全屏状态变化事件
    this.fullscreenChangeHandler = this.handleFullscreenChange.bind(this);
    this.removeFullscreenChangeListener = FullscreenHelper.onFullscreenChange(
        this.fullscreenChangeHandler
    );
    
    // 绑定全屏错误事件
    this.fullscreenErrorHandler = this.handleFullscreenError.bind(this);
    this.removeFullscreenErrorListener = FullscreenHelper.onFullscreenError(
        this.fullscreenErrorHandler
    );
    
    // 初始化全屏状态
    this.isFullscreen = false;
};

// 处理全屏状态变化
Player.prototype.handleFullscreenChange = function() {
    const wasFullscreen = this.isFullscreen;
    this.isFullscreen = FullscreenHelper.isFullscreen();
    
    if (this.isFullscreen) {
        // 进入全屏模式
        this.enterFullscreen();
        if (this.options.onFullscreenEnter) {
            this.options.onFullscreenEnter(this);
        }
    } else if (wasFullscreen) {
        // 退出全屏模式
        this.exitFullscreen();
        if (this.options.onFullscreenExit) {
            this.options.onFullscreenExit(this);
        }
    }
};

// 进入全屏模式时的处理
Player.prototype.enterFullscreen = function() {
    // 存储原始样式
    this.originalStyle = {
        width: this.renderer.canvas.style.width,
        height: this.renderer.canvas.style.height,
        maxWidth: this.renderer.canvas.style.maxWidth,
        maxHeight: this.renderer.canvas.style.maxHeight
    };
    
    // 获取屏幕尺寸
    const screenWidth = window.innerWidth;
    const screenHeight = window.innerHeight;
    
    // 调整播放器尺寸以适应全屏
    this.resizeRenderer(screenWidth, screenHeight);
    
    // 触发调整大小事件
    if (this.options.onResize) {
        this.options.onResize(screenWidth, screenHeight);
    }
};

// 退出全屏模式时的处理
Player.prototype.exitFullscreen = function() {
    // 恢复原始样式
    this.renderer.canvas.style.width = this.originalStyle.width;
    this.renderer.canvas.style.height = this.originalStyle.height;
    this.renderer.canvas.style.maxWidth = this.originalStyle.maxWidth;
    this.renderer.canvas.style.maxHeight = this.originalStyle.maxHeight;
    
    // 恢复原始尺寸
    this.resizeRenderer(this.originalSize.width, this.originalSize.height);
    
    // 触发调整大小事件
    if (this.options.onResize) {
        this.options.onResize(this.originalSize.width, this.originalSize.height);
    }
};

全屏状态管理与渲染适配

视频尺寸调整策略

// 调整渲染器尺寸
Player.prototype.resizeRenderer = function(width, height) {
    // 计算视频的宽高比
    const videoAspectRatio = this.originalSize.width / this.originalSize.height;
    
    // 根据目标尺寸和视频宽高比计算最佳显示尺寸
    let targetWidth, targetHeight;
    
    if (width / height > videoAspectRatio) {
        // 以高度为基准
        targetHeight = height;
        targetWidth = Math.round(height * videoAspectRatio);
    } else {
        // 以宽度为基准
        targetWidth = width;
        targetHeight = Math.round(width / videoAspectRatio);
    }
    
    // 调整渲染器尺寸
    this.renderer.resize(targetWidth, targetHeight);
    
    // 更新画布样式尺寸(用于居中显示)
    this.renderer.canvas.style.width = `${targetWidth}px`;
    this.renderer.canvas.style.height = `${targetHeight}px`;
    this.renderer.canvas.style.marginLeft = `${Math.round((width - targetWidth) / 2)}px`;
    this.renderer.canvas.style.marginTop = `${Math.round((height - targetHeight) / 2)}px`;
    
    // 存储当前尺寸
    this.currentSize = { width: targetWidth, height: targetHeight };
};

// 添加全屏控制方法
Player.prototype.requestFullscreen = function() {
    if (!this.fullscreenSupported) return false;
    
    // 请求全屏
    return FullscreenHelper.request(this.containerElement);
};

Player.prototype.exitFullscreen = function() {
    if (!this.fullscreenSupported || !this.isFullscreen) return false;
    
    // 退出全屏
    return FullscreenHelper.exit();
};

Player.prototype.toggleFullscreen = function() {
    if (!this.fullscreenSupported) return false;
    
    // 切换全屏状态
    return FullscreenHelper.toggle(this.containerElement);
};

渲染器适配实现

Canvas2D渲染器适配
// 修改Canvas2DRenderer的resize方法
CanvasRenderer.prototype.resize = function(width, height) {
    this.width = width | 0;
    this.height = height | 0;
    
    // 调整canvas元素尺寸
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    
    // 重新创建imageData对象
    this.imageData = this.context.getImageData(0, 0, this.width, this.height);
    
    // 清空画布
    JSMpeg.Fill(this.imageData.data, 255);
    
    // 如果处于全屏模式,调整样式
    if (this.player && this.player.isFullscreen) {
        this.canvas.style.display = 'block';
        this.canvas.style.position = 'absolute';
        this.canvas.style.top = '50%';
        this.canvas.style.left = '50%';
        this.canvas.style.transform = 'translate(-50%, -50%)';
    } else {
        // 恢复非全屏样式
        this.canvas.style.position = '';
        this.canvas.style.top = '';
        this.canvas.style.left = '';
        this.canvas.style.transform = '';
    }
};
WebGL渲染器适配
// 修改WebGLRenderer的resize方法
WebGLRenderer.prototype.resize = function(width, height) {
    this.width = width | 0;
    this.height = height | 0;
    
    // 调整canvas元素尺寸
    this.canvas.width = this.width;
    this.canvas.height = this.height;
    
    // 调整WebGL视口
    const gl = this.gl;
    gl.viewport(0, 0, this.width, this.height);
    
    // 重新计算编码宽度(16像素对齐)
    this.codedWidth = ((this.width + 15) >> 4) << 4;
    
    // 更新纹理尺寸
    if (this.textureY) {
        this.updateTextureSize(this.textureY, this.codedWidth, this.height);
    }
    if (this.textureCb || this.textureCr) {
        const chromaWidth = this.codedWidth >> 1;
        const chromaHeight = this.height >> 1;
        this.updateTextureSize(this.textureCb, chromaWidth, chromaHeight);
        this.updateTextureSize(this.textureCr, chromaWidth, chromaHeight);
    }
    
    // 全屏样式调整
    if (this.player && this.player.isFullscreen) {
        this.canvas.style.display = 'block';
        this.canvas.style.position = 'absolute';
        this.canvas.style.top = '50%';
        this.canvas.style.left = '50%';
        this.canvas.style.transform = 'translate(-50%, -50%)';
    } else {
        this.canvas.style.position = '';
        this.canvas.style.top = '';
        this.canvas.style.left = '';
        this.canvas.style.transform = '';
    }
};

// 添加纹理尺寸更新方法
WebGLRenderer.prototype.updateTextureSize = function(texture, width, height) {
    const gl = this.gl;
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(
        gl.TEXTURE_2D, 0, gl.LUMINANCE, width, height, 0,
        gl.LUMINANCE, gl.UNSIGNED_BYTE, null
    );
};

用户交互与体验优化

全屏控制UI集成

<!-- 播放器控制界面HTML -->
<div class="jsmpeg-player-controls">
    <button class="play-pause-btn">播放/暂停</button>
    <div class="progress-bar"></div>
    <button class="fullscreen-btn">全屏</button>
</div>
// 为播放器添加UI控制集成
Player.prototype.setupControls = function() {
    if (!this.options.controls) return;
    
    // 创建控制栏元素
    this.controls = document.createElement('div');
    this.controls.className = 'jsmpeg-player-controls';
    this.controls.style.position = 'absolute';
    this.controls.style.bottom = '0';
    this.controls.style.left = '0';
    this.controls.style.right = '0';
    this.controls.style.backgroundColor = 'rgba(0, 0, 0, 0.5)';
    this.controls.style.padding = '10px';
    this.controls.style.color = 'white';
    
    // 添加全屏按钮
    if (this.fullscreenSupported) {
        this.fullscreenButton = document.createElement('button');
        this.fullscreenButton.className = 'fullscreen-btn';
        this.fullscreenButton.textContent = '全屏';
        this.fullscreenButton.style.marginLeft = 'auto';
        this.fullscreenButton.style.padding = '5px 10px';
        this.fullscreenButton.style.cursor = 'pointer';
        
        // 绑定点击事件
        this.fullscreenButton.addEventListener('click', () => {
            this.toggleFullscreen();
        });
        
        this.controls.appendChild(this.fullscreenButton);
    }
    
    // 将控制栏添加到容器
    this.containerElement.appendChild(this.controls);
    
    // 更新全屏按钮文本
    this.updateFullscreenButtonText();
};

// 更新全屏按钮文本
Player.prototype.updateFullscreenButtonText = function() {
    if (!this.fullscreenButton) return;
    
    this.fullscreenButton.textContent = this.isFullscreen ? '退出全屏' : '全屏';
};

键盘快捷键支持

// 添加键盘事件处理
Player.prototype.setupKeyboardControls = function() {
    if (!this.options.keyboardControls) return;
    
    this.keyboardHandler = this.handleKeyboardEvent.bind(this);
    document.addEventListener('keydown', this.keyboardHandler);
};

// 处理键盘事件
Player.prototype.handleKeyboardEvent = function(event) {
    // 如果有输入元素被聚焦,不处理键盘事件
    if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
        return;
    }
    
    switch (event.key) {
        case 'f':
        case 'F':
            // F键切换全屏
            this.toggleFullscreen();
            event.preventDefault();
            break;
        case 'Escape':
            // ESC键退出全屏
            if (this.isFullscreen) {
                this.exitFullscreen();
                event.preventDefault();
            }
            break;
        // 可以添加其他快捷键处理...
    }
};

错误处理与边界情况

全屏请求被拒绝的处理

// 修改全屏切换方法,添加错误处理
Player.prototype.toggleFullscreen = async function() {
    if (!this.fullscreenSupported) return false;
    
    try {
        const result = await FullscreenHelper.toggle(this.containerElement);
        
        // 更新按钮文本
        this.updateFullscreenButtonText();
        return result;
    } catch (error) {
        console.error('Failed to toggle fullscreen:', error);
        
        // 显示用户友好的错误消息
        if (this.options.onFullscreenError) {
            this.options.onFullscreenError(error);
        } else {
            this.showFullscreenErrorToast('全屏请求失败: ' + this.getFullscreenErrorText(error));
        }
        
        return false;
    }
};

// 获取全屏错误的用户友好文本
Player.prototype.getFullscreenErrorText = function(error) {
    switch (error.name) {
        case 'SecurityError':
            return '全屏请求被安全策略阻止,请在用户交互事件中触发全屏请求';
        case 'NotAllowedError':
            return '全屏请求被用户拒绝或浏览器策略阻止';
        case 'TypeError':
            return '无效的元素用于全屏请求';
        default:
            return '未知错误: ' + error.message;
    }
};

// 显示错误提示
Player.prototype.showFullscreenErrorToast = function(message) {
    // 创建提示元素
    const toast = document.createElement('div');
    toast.textContent = message;
    toast.style.position = 'absolute';
    toast.style.bottom = '50px';
    toast.style.left = '50%';
    toast.style.transform = 'translateX(-50%)';
    toast.style.backgroundColor = 'rgba(255, 0, 0, 0.8)';
    toast.style.color = 'white';
    toast.style.padding = '10px 20px';
    toast.style.borderRadius = '4px';
    toast.style.zIndex = '1000';
    
    // 添加到容器
    this.containerElement.appendChild(toast);
    
    // 3秒后移除
    setTimeout(() => {
        toast.remove();
    }, 3000);
};

多显示器与分辨率适配

// 增强resizeRenderer方法,支持多显示器和高DPI
Player.prototype.resizeRenderer = function(width, height) {
    // 处理高DPI显示
    const dpr = window.devicePixelRatio || 1;
    
    // 计算视频的宽高比
    const videoAspectRatio = this.originalSize.width / this.originalSize.height;
    
    // 根据目标尺寸和视频宽高比计算最佳显示尺寸
    let targetWidth, targetHeight;
    
    if (width / height > videoAspectRatio) {
        // 以高度为基准
        targetHeight = height;
        targetWidth = Math.round(height * videoAspectRatio);
    } else {
        // 以宽度为基准
        targetWidth = width;
        targetHeight = Math.round(width / videoAspectRatio);
    }
    
    // 调整canvas元素的CSS尺寸
    this.renderer.canvas.style.width = `${targetWidth}px`;
    this.renderer.canvas.style.height = `${targetHeight}px`;
    
    // 调整canvas元素的实际像素尺寸(考虑设备像素比)以避免模糊
    const actualWidth = Math.round(targetWidth * dpr);
    const actualHeight = Math.round(targetHeight * dpr);
    
    // 调整渲染器尺寸
    this.renderer.resize(actualWidth, actualHeight);
    
    // 居中显示
    this.renderer.canvas.style.marginLeft = `${Math.round((width - targetWidth) / dpr / 2)}px`;
    this.renderer.canvas.style.marginTop = `${Math.round((height - targetHeight) / dpr / 2)}px`;
    
    // 存储当前尺寸
    this.currentSize = { 
        width: targetWidth, 
        height: targetHeight,
        actualWidth,
        actualHeight,
        dpr
    };
};

完整集成与使用指南

播放器初始化与全屏功能启用

// 完整的JSMpeg播放器初始化示例
const canvas = document.getElementById('video-canvas');
const player = new JSMpeg.Player('video-stream.ts', {
    canvas: canvas,
    autoplay: true,
    loop: true,
    controls: true,          // 启用控制栏
    keyboardControls: true,  // 启用键盘控制
    container: document.getElementById('player-container'),
    onFullscreenEnter: () => {
        console.log('Entered fullscreen mode');
    },
    onFullscreenExit: () => {
        console.log('Exited fullscreen mode');
    },
    onFullscreenError: (error) => {
        alert('全屏操作失败: ' + error.message);
    }
});

// 检查全屏支持并初始化
if (player.fullscreenSupported) {
    console.log('Fullscreen is supported');
    
    // 提供API给外部调用
    window.playerFullscreenApi = {
        toggle: () => player.toggleFullscreen(),
        enter: () => player.requestFullscreen(),
        exit: () => player.exitFullscreen(),
        isFullscreen: () => player.isFullscreen
    };
}

响应式布局适配

/* 播放器容器样式 */
#player-container {
    position: relative;
    width: 100%;
    max-width: 1280px;
    margin: 0 auto;
    background-color: #000;
}

/* 视频画布样式 */
#video-canvas {
    width: 100%;
    height: auto;
    display: block;
}

/* 控制栏样式 */
.jsmpeg-player-controls {
    display: flex;
    align-items: center;
    opacity: 0;
    transition: opacity 0.3s ease;
}

/* 鼠标悬停时显示控制栏 */
#player-container:hover .jsmpeg-player-controls {
    opacity: 1;
}

/* 全屏状态样式 */
#player-container:-webkit-full-screen {
    background-color: #000;
}

#player-container:-moz-full-screen {
    background-color: #000;
}

#player-container:fullscreen {
    background-color: #000;
}

性能优化与最佳实践

全屏模式下的渲染优化

// WebGL渲染器性能优化
WebGLRenderer.prototype.optimizeForFullscreen = function(enabled) {
    const gl = this.gl;
    
    if (enabled) {
        // 全屏模式下的优化
        gl.disable(gl.BLEND);
        gl.enable(gl.CULL_FACE);
        this.textureUpdateStrategy = 'subimage';  // 使用更高效的纹理更新方式
        
        // 如果支持,启用各向异性过滤
        if (gl.getExtension('EXT_texture_filter_anisotropic')) {
            const maxAnisotropy = gl.getParameter(
                gl.getExtension('EXT_texture_filter_anisotropic').MAX_TEXTURE_MAX_ANISOTROPY_EXT
            );
            gl.texParameterf(gl.TEXTURE_2D, 
                gl.getExtension('EXT_texture_filter_anisotropic').TEXTURE_MAX_ANISOTROPY_EXT, 
                maxAnisotropy
            );
        }
    } else {
        // 恢复默认设置
        gl.enable(gl.BLEND);
        gl.disable(gl.CULL_FACE);
        this.textureUpdateStrategy = 'image';  // 标准纹理更新方式
    }
};

全屏功能测试矩阵

测试场景测试方法预期结果
基本全屏切换点击全屏按钮成功进入/退出全屏模式
键盘快捷键按F键和ESC键F键切换全屏,ESC键退出全屏
多显示器在不同显示器间切换全屏状态正确适应不同显示器分辨率
窗口大小调整非全屏时调整窗口大小视频保持正确比例并居中显示
移动设备在移动浏览器中测试支持全屏并正确旋转适应
权限拒绝模拟全屏请求被拒绝显示友好错误提示
播放中切换播放视频时切换全屏视频播放不中断,音频继续

总结与未来展望

全屏功能实现要点回顾

  1. API封装:通过统一接口封装不同浏览器的Fullscreen API实现
  2. 状态管理:维护播放器状态与全屏状态的同步
  3. 渲染适配:处理不同渲染器在全屏模式下的尺寸调整
  4. 用户体验:提供直观的控制界面与键盘快捷键
  5. 错误处理:优雅处理全屏请求失败场景
  6. 性能优化:针对全屏模式进行渲染性能优化

未来功能扩展方向

  1. 画中画模式:结合Picture-in-Picture API实现多任务观看体验
  2. 自定义全屏样式:允许用户自定义全屏模式下的控制界面
  3. 多显示器支持:增强对多显示器系统的检测与适配
  4. HDR支持:在全屏模式下提供HDR内容的渲染支持
  5. VR模式:探索将全屏功能扩展到VR设备的可能性

通过本文介绍的方案,我们实现了一套完整的JSMpeg全屏功能解决方案,解决了跨浏览器兼容性、渲染适配和用户体验等核心问题。这套方案不仅可以直接应用于JSMpeg播放器,其设计思路也可以为其他Web视频播放器的全屏功能实现提供参考。

希望本文能够帮助开发者更好地理解和实现Web视频播放器的全屏功能,为用户提供更加沉浸和流畅的视频观看体验。如果你有任何问题或改进建议,欢迎在评论区留言讨论。

相关资源推荐

【免费下载链接】jsmpeg MPEG1 Video Decoder in JavaScript 【免费下载链接】jsmpeg 项目地址: https://gitcode.com/gh_mirrors/js/jsmpeg

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

抵扣说明:

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

余额充值