Plyr HLS 插件优化版:提升视频播放体验的完美解决方案


本帖最后由 宝宝丷 于 2024-7-24 14:01 编辑

Plyr HLS 插件优化版:提升视频播放体验的完美解决方案

主要功能和优势

  1. HLS 支持检测: 插件能够自动检测浏览器是否支持 HLS(HTTP Live Streaming),并根据支持情况提供相应的反馈。

  2. 视频质量级别切换: 用户可以通过简单的界面操作选择视频的不同质量级别,以适应不同的网络环境和设备性能。

  3. 错误处理和自动恢复: 实时监控视频播放过程中可能出现的各种错误,并提供自动恢复功能,确保播放的连续性和稳定性。

  4. 界面集成和交互优化: 将质量级别切换功能无缝集成到 Plyr 播放器的控制栏中,优化用户交互体验,使操作更加直观和便捷。

技术实现和开发细节

  • 基于 Hls.js: 插件利用 Hls.js 库实现对 HLS 格式视频的支持和管理。

  • JavaScript 和 DOM 操作: 使用现代 JavaScript 和 DOM API 实现质量级别切换按钮的创建和事件监听。

如何使用?

您可以通过以下步骤轻松集成 Plyr HLS 插件到您的网站或项目中:

  1. 引入 Plyr 播放器和依赖的 JavaScript 库。

  2. 初始化 Plyr 播放器,并配置插件选项以启用质量级别切换功能。

  3. 根据您的需求调整和定制插件的样式和行为。

探讨和反馈

我非常期待听到大家的反馈和建议!如果您有任何问题、意见或者想要分享您的使用经验,请在下方留言,让我们一起讨论和进步。

结语

希望 Plyr HLS 插件优化版能为大家提供一个更好的视频播放体验,同时也能在技术交流和学习上有所帮助。感谢大家的关注和支持!

技术实现和开发细节

前端HTML

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Plyr HLS 插件优化版</title>
    <link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/plyr/3.6.12/plyr.css">
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            background-color: #f8f9fa;
            margin: 0;
            padding: 0;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }

        .container {
            width: 80%;
            background-color: #ffffff;
            border-radius: 8px;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
            overflow: hidden;
            object-fit: cover;
        }

        .plyr__controls {
            background-color: rgba(0, 0, 0, 0.6);
        }

        .plyr__controls button {
            color: #ffffff !important;
        }

        .plyr__controls button:hover {
            background-color: rgba(255, 255, 255, 0.1);
        }

        .plyr__controls button[aria-expanded="true"]::after {
            color: #ffffff;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Plyr HLS 插件优化版</h1>
        <hr>
        <section>
            <video id="player" playsinline controls data-poster="https://lf3-static.bytednsdoc.com/obj/eden-cn/nupenuvpxnuvo/xgplayer_doc/poster.jpg">
                <source type="application/x-mpegURL" src="https://dora-doc.qiniu.com/004.m3u8" rel="external nofollow" >
            </video>
        </section>
        <hr>
    </div>

    <script src="https://cdn.bootcdn.net/ajax/libs/plyr/3.6.12/plyr.polyfilled.js" rel="external nofollow"  defer></script>
    <script src="https://cdn.bootcdn.net/ajax/libs/hls.js/1.1.5/hls.min.js" rel="external nofollow"  defer></script>
    <script src="https://www.52pojie.cn/plyr-hls-plugin.js?v=1.0" rel="external nofollow"  defer></script>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            const player = new Plyr('#player', {
                controls: [
                    'play', 'pause', 'progress', 'current-time', 'mute', 'volume', 'captions', 'settings', 'fullscreen'
                ],
                settings: ['captions', 'quality']
            });

            new PlyrHLSPlugin(player, { defaultQuality: '720p' });
        });
    </script>
</body>
</html>

依赖插件代码

class PlyrHLSPlugin {
    constructor(player, options = {}) {
        this.version = '1.0';
        this.player = player;
        this.options = Object.assign({
            defaultQuality: null,
            qualityMenuClass: 'plyr-quality-menu',
            qualityButtonClass: 'plyr-control plyr-control--quality',
            qualityMenuItemClass: 'plyr-menu-item',
            qualityMenuActiveClass: 'plyr-menu-item--active',
            errorMessageClass: 'plyr-error-message'
        }, options);
        this.hls = Hls.isSupported() ? new Hls() : null;
        this.qualityLevels = [];
        this.boundResizeHandler = this.updateMenuPosition.bind(this);

        this.init();
    }

    init() {
        if (!this.hls) {
            this.showError('当前浏览器不支持 HLS,无法使用此插件的相关功能。');
            return;
        }

        const sourceElement = this.getSourceElement();
        if (!sourceElement) {
            this.showError('播放器未找到有效的 m3u8 源,请检查视频配置。');
            return;
        }

        this.hls.loadSource(sourceElement.src);
        this.hls.attachMedia(this.player.media);

        this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
            this.createQualitySwitcher();
            this.addQualitySwitcherToUI();
        });

        this.hls.on(Hls.Events.LEVEL_SWITCHED, (event, data) => {
            this.updateQualityMenu(data.level);
            this.showQualityChangeMessage(this.qualityLevels[data.level].label);
        });

        this.hls.on(Hls.Events.ERROR, (event, data) => {
            this.handleHlsError(data);
        });

        window.addEventListener('resize', this.boundResizeHandler);
        this.player.on('enterfullscreen', this.handleEnterFullscreen.bind(this));
        this.player.on('exitfullscreen', this.handleExitFullscreen.bind(this));
    }

    getSourceElement() {
        return this.player.media.querySelector('source[type="application/x-mpegURL"]');
    }

    createQualitySwitcher() {
        if (!this.hls.levels || this.hls.levels.length === 0) {
            this.showError('未获取到可用的质量级别,请检查视频源或网络情况。');
            return;
        }

        this.qualityLevels = this.hls.levels.map((level, index) => ({
            label: `${level.height}p`,
            width: level.width,
            height: level.height,
            bitrate: level.bitrate,
            index: index
        }));

        if (this.options.defaultQuality) {
            const defaultIndex = this.qualityLevels.findIndex(level => level.label === this.options.defaultQuality);
            if (defaultIndex !== -1) {
                setTimeout(() => {
                    this.hls.currentLevel = defaultIndex;
                }, 0);
            } else {
                console.warn(`未找到指定的默认质量级别: ${this.options.defaultQuality},将使用自动选择。`);
            }
        }
    }

    addQualitySwitcherToUI() {
        const qualityMenu = this.createQualityMenu();
        document.body.appendChild(qualityMenu);

        const qualityButton = this.createQualityButton();
        this.player.elements.controls.appendChild(qualityButton);

        qualityButton.addEventListener('click', (event) => {
            event.stopPropagation();
            this.toggleQualityMenu(qualityMenu);
            this.updateMenuPosition();
        });

        document.addEventListener('click', (event) => {
            if (!qualityMenu.contains(event.target) && !qualityButton.contains(event.target)) {
                this.hideQualityMenu(qualityMenu);
            }
        });

        this.qualityLevels.forEach((level, index) => {
            const button = this.createQualityMenuItem(level, index, qualityMenu);
            qualityMenu.appendChild(button);
        });

        this.updateQualityMenu(this.hls.currentLevel);
    }

    createQualityMenu() {
        const qualityMenu = document.createElement('div');
        qualityMenu.className = this.options.qualityMenuClass;
        qualityMenu.style.display = 'none';
        qualityMenu.style.backgroundColor = '#fff';
        qualityMenu.style.border = '1px solid #ccc';
        qualityMenu.style.borderRadius = '4px';
        qualityMenu.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1)';
        qualityMenu.style.position = 'absolute';
        qualityMenu.style.zIndex = '1000';
        return qualityMenu;
    }

    createQualityButton() {
        const qualityButton = document.createElement('button');
        qualityButton.className = this.options.qualityButtonClass;
        qualityButton.setAttribute('aria-label', '选择视频质量');
        qualityButton.style.backgroundColor = '#007bff';
        qualityButton.style.color = '#fff';
        qualityButton.style.border = 'none';
        qualityButton.style.borderRadius = '4px';
        qualityButton.style.cursor = 'pointer';
        qualityButton.style.transition = 'background-color 0.3s ease';
        qualityButton.innerHTML = '<svg role="img" aria-label="Quality" width="16" height="16"><use xlink:href=""></use></svg>';

        qualityButton.addEventListener('mouseenter', () => {
            qualityButton.style.backgroundColor = '#0056b3';
        });

        qualityButton.addEventListener('mouseleave', () => {
            qualityButton.style.backgroundColor = '#007bff';
        });

        return qualityButton;
    }

    createQualityMenuItem(level, index, qualityMenu) {
        const button = document.createElement('button');
        button.className = this.options.qualityMenuItemClass;
        button.textContent = `${level.label} (${this.formatBitrate(level.bitrate)})`;
        button.style.padding = '8px 12px';
        button.style.border = 'none';
        button.style.backgroundColor = 'transparent';
        button.style.color = '#333';
        button.style.cursor = 'pointer';
        button.style.transition = 'background-color 0.3s ease';

        button.addEventListener('mouseenter', () => {
            button.style.backgroundColor = '#f0f0f0';
        });

        button.addEventListener('mouseleave', () => {
            button.style.backgroundColor = 'transparent';
        });

        button.addEventListener('click', () => {
            this.hls.currentLevel = index;
            this.updateQualityMenu(index);
            this.hideQualityMenu(qualityMenu);
        });
        return button;
    }

    updateQualityMenu(currentIndex) {
        const qualityMenu = document.querySelector(`.${this.options.qualityMenuClass}`);
        if (!qualityMenu) return;

        requestAnimationFrame(() => {
            qualityMenu.querySelectorAll('button').forEach((button, index) => {
                button.classList.toggle(this.options.qualityMenuActiveClass, index === currentIndex);
            });
        });
    }

    updateMenuPosition() {
        const qualityMenu = document.querySelector(`.${this.options.qualityMenuClass}`);
        if (!qualityMenu) return;

        const rect = qualityMenu.getBoundingClientRect();
        qualityMenu.style.right = `${Math.max(10, window.innerWidth - rect.width - 20)}px`;
    }

    toggleQualityMenu(qualityMenu) {
        qualityMenu.style.display = qualityMenu.style.display === 'none' ? 'block' : 'none';
    }

    hideQualityMenu(qualityMenu) {
        qualityMenu.style.display = 'none';
    }

    handleHlsError(data) {
        let errorMessage = '';
        let errorType = '';
        switch (data.fatal) {
            case Hls.ErrorTypes.NETWORK_ERROR:
                errorMessage = '网络错误,请检查视频源或网络连接。';
                errorType = 'network';
                break;
            case Hls.ErrorTypes.MEDIA_ERROR:
                errorMessage = '媒体错误,尝试自动恢复。';
                errorType = 'media';
                this.hls.recoverMediaError();
                break;
            case Hls.ErrorTypes.OTHER_ERROR:
                errorMessage = '发生了其他错误,请检查播放器设置或视频源。';
                errorType = 'other';
                break;
            default:
                errorMessage = '未知错误。';
                errorType = 'unknown';
                break;
        }

        console.error('HLS Error:', errorMessage);
        this.showError(errorMessage, errorType);
    }

    showError(message, errorType) {
        const errorElement = document.createElement('div');
        errorElement.className = `${this.options.errorMessageClass} ${this.options.errorMessageClass}--${errorType}`;
        errorElement.textContent = message;
        errorElement.style.backgroundColor = '#dc3545';
        errorElement.style.color = '#fff';
        errorElement.style.padding = '8px 12px';
        errorElement.style.borderRadius = '4px';
        errorElement.style.margin = '8px';
        errorElement.style.display = 'inline-block';
        this.player.elements.controls.appendChild(errorElement);

        setTimeout(() => {
            errorElement.remove();
        }, 5000);
    }

    showQualityChangeMessage(qualityLabel) {
        const messageElement = document.createElement('div');
        messageElement.className = 'plyr-quality-change-message';
        messageElement.textContent = `已切换至 ${qualityLabel}`;
        messageElement.style.backgroundColor = '#28a745';
        messageElement.style.color = '#fff';
        messageElement.style.padding = '8px 12px';
        messageElement.style.borderRadius = '4px';
        messageElement.style.position = 'fixed';
        messageElement.style.bottom = '20px';
        messageElement.style.left = '50%';
        messageElement.style.transform = 'translateX(-50%)';
        document.body.appendChild(messageElement);

        setTimeout(() => {
            messageElement.remove();
        }, 3000);
    }

    formatBitrate(bitrate) {
        if (bitrate < 1000) {
            return `${bitrate} bps`;
        } else if (bitrate < 1000000) {
            return `${(bitrate / 1000).toFixed(1)} Kbps`;
        } else {
            return `${(bitrate / 1000000).toFixed(1)} Mbps`;
        }
    }

    handleEnterFullscreen() {
        const qualityButton = this.player.elements.controls.querySelector(`.${this.options.qualityButtonClass}`);
        if (qualityButton) {
            qualityButton.style.zIndex = '2000';
        }
    }

    handleExitFullscreen() {
        const qualityButton = this.player.elements.controls.querySelector(`.${this.options.qualityButtonClass}`);
        if (qualityButton) {
            qualityButton.style.zIndex = '';
        }
    }

    destroy() {
        if (this.hls) {
            this.hls.destroy();
        }
        window.removeEventListener('resize', this.boundResizeHandler);
        this.player.off('enterfullscreen', this.handleEnterFullscreen.bind(this));
        this.player.off('exitfullscreen', this.handleExitFullscreen.bind(this));
    }
}

// 兼容 CommonJS 和 ES 模块
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
    module.exports = PlyrHLSPlugin;
} else {
    window.PlyrHLSPlugin = PlyrHLSPlugin;
}



VCamera虚拟相机去除国区限制破解VIP

PVZ原版植物大战僵尸辅助例子

获取更多资讯请加入交流群


    协助本站SEO优化一下,谢谢!
    关键词不能为空
评 论
更换验证码