Plyr HLS 插件优化版:提升视频播放体验的完美解决方案
主要功能和优势
-
HLS 支持检测: 插件能够自动检测浏览器是否支持 HLS(HTTP Live Streaming),并根据支持情况提供相应的反馈。
-
视频质量级别切换: 用户可以通过简单的界面操作选择视频的不同质量级别,以适应不同的网络环境和设备性能。
-
错误处理和自动恢复: 实时监控视频播放过程中可能出现的各种错误,并提供自动恢复功能,确保播放的连续性和稳定性。
-
界面集成和交互优化: 将质量级别切换功能无缝集成到 Plyr 播放器的控制栏中,优化用户交互体验,使操作更加直观和便捷。
技术实现和开发细节
-
基于 Hls.js: 插件利用 Hls.js 库实现对 HLS 格式视频的支持和管理。
-
JavaScript 和 DOM 操作: 使用现代 JavaScript 和 DOM API 实现质量级别切换按钮的创建和事件监听。
如何使用?
您可以通过以下步骤轻松集成 Plyr HLS 插件到您的网站或项目中:
-
引入 Plyr 播放器和依赖的 JavaScript 库。
-
初始化 Plyr 播放器,并配置插件选项以启用质量级别切换功能。
-
根据您的需求调整和定制插件的样式和行为。
探讨和反馈
我非常期待听到大家的反馈和建议!如果您有任何问题、意见或者想要分享您的使用经验,请在下方留言,让我们一起讨论和进步。
结语
希望 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;
}