241 lines
7.6 KiB
JavaScript
241 lines
7.6 KiB
JavaScript
/**
|
|
* StreamFlow - Video Player Component
|
|
* ArtPlayer.js integration with custom skin
|
|
*/
|
|
|
|
import Artplayer from 'artplayer';
|
|
|
|
// Player instance reference
|
|
let currentPlayer = null;
|
|
|
|
/**
|
|
* Format duration for display
|
|
* @param {number} seconds - Duration in seconds
|
|
* @returns {string} Formatted duration
|
|
*/
|
|
function formatDuration(seconds) {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const secs = Math.floor(seconds % 60);
|
|
|
|
if (hours > 0) {
|
|
return `${hours}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
|
}
|
|
return `${minutes}:${String(secs).padStart(2, '0')}`;
|
|
}
|
|
|
|
/**
|
|
* Initialize video player
|
|
* @param {HTMLElement} container - Container element
|
|
* @param {Object} options - Player options
|
|
* @returns {Artplayer} Player instance
|
|
*/
|
|
export function initPlayer(container, options = {}) {
|
|
// Destroy existing player if any
|
|
destroyPlayer();
|
|
|
|
const {
|
|
url,
|
|
poster,
|
|
title,
|
|
autoplay = false,
|
|
qualities = []
|
|
} = options;
|
|
|
|
// Build player config with enhanced buffering
|
|
const playerConfig = {
|
|
container,
|
|
url,
|
|
poster,
|
|
title,
|
|
volume: 0.7,
|
|
autoplay,
|
|
autoSize: false,
|
|
autoMini: true,
|
|
loop: false,
|
|
flip: true,
|
|
playbackRate: true,
|
|
aspectRatio: true,
|
|
screenshot: true,
|
|
setting: true,
|
|
hotkey: true,
|
|
pip: true,
|
|
mutex: true,
|
|
fullscreen: true,
|
|
fullscreenWeb: true,
|
|
miniProgressBar: true,
|
|
playsInline: true,
|
|
autoPlayback: true,
|
|
theme: '#f5c518', // Golden-yellow accent
|
|
lang: 'en',
|
|
moreVideoAttr: {
|
|
crossOrigin: 'anonymous',
|
|
preload: 'auto',
|
|
},
|
|
airplay: true,
|
|
// HLS custom configuration for better buffering
|
|
customType: {
|
|
m3u8: function playM3u8(video, url, art) {
|
|
if (Hls.isSupported()) {
|
|
if (art.hls) {
|
|
art.hls.destroy();
|
|
}
|
|
const hls = new Hls({
|
|
// Buffer configuration for faster start
|
|
maxBufferLength: 30, // Max buffer in seconds
|
|
maxMaxBufferLength: 60, // Max buffer ceiling
|
|
maxBufferSize: 60 * 1000 * 1000, // Max buffer size (60MB)
|
|
maxBufferHole: 0.5, // Max gap in buffer
|
|
lowLatencyMode: false, // Disable low latency for stability
|
|
startLevel: -1, // Auto select quality
|
|
// Faster loading
|
|
enableWorker: true,
|
|
startFragPrefetch: true, // Prefetch next fragment
|
|
testBandwidth: true
|
|
});
|
|
hls.loadSource(url);
|
|
hls.attachMedia(video);
|
|
art.hls = hls;
|
|
art.on('destroy', () => hls.destroy());
|
|
|
|
// Handle HLS errors
|
|
hls.on(Hls.Events.ERROR, (event, data) => {
|
|
if (data.fatal) {
|
|
switch (data.type) {
|
|
case Hls.ErrorTypes.NETWORK_ERROR:
|
|
console.warn('HLS network error, trying to recover...');
|
|
hls.startLoad();
|
|
break;
|
|
case Hls.ErrorTypes.MEDIA_ERROR:
|
|
console.warn('HLS media error, trying to recover...');
|
|
hls.recoverMediaError();
|
|
break;
|
|
default:
|
|
console.error('Fatal HLS error');
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
// Native HLS support (Safari)
|
|
video.src = url;
|
|
}
|
|
}
|
|
},
|
|
settings: [
|
|
{
|
|
html: 'Speed',
|
|
selector: [
|
|
{ html: '0.5x', value: 0.5 },
|
|
{ html: '0.75x', value: 0.75 },
|
|
{ html: 'Normal', value: 1, default: true },
|
|
{ html: '1.25x', value: 1.25 },
|
|
{ html: '1.5x', value: 1.5 },
|
|
{ html: '2x', value: 2 }
|
|
],
|
|
onSelect(item) {
|
|
if (currentPlayer) {
|
|
currentPlayer.playbackRate = item.value;
|
|
}
|
|
return item.html;
|
|
}
|
|
}
|
|
],
|
|
icons: {
|
|
loading: `<div class="loading__spinner"></div>`,
|
|
state: `<svg viewBox="0 0 24 24" fill="currentColor" width="64" height="64"><path d="M8 5v14l11-7z"/></svg>`
|
|
},
|
|
cssVar: {
|
|
'--art-theme': '#f5c518',
|
|
'--art-background-color': '#0f0f0f',
|
|
'--art-progress-color': '#f5c518',
|
|
'--art-control-background-color': 'rgba(0, 0, 0, 0.8)',
|
|
'--art-control-height': '48px',
|
|
'--art-bottom-gap': '12px'
|
|
}
|
|
};
|
|
|
|
// Only add quality if available (ArtPlayer requires array, not undefined)
|
|
if (qualities.length > 0) {
|
|
playerConfig.quality = qualities.map((q, i) => ({
|
|
default: i === 0,
|
|
html: q,
|
|
url: url
|
|
}));
|
|
}
|
|
|
|
// Initialize ArtPlayer
|
|
currentPlayer = new Artplayer(playerConfig);
|
|
|
|
// Event handling
|
|
currentPlayer.on('ready', () => {
|
|
console.log('Player ready');
|
|
if (currentPlayer.video) {
|
|
currentPlayer.video.preload = 'auto';
|
|
}
|
|
});
|
|
|
|
currentPlayer.on('video:waiting', () => {
|
|
console.log('Buffering...');
|
|
});
|
|
|
|
currentPlayer.on('video:canplay', () => {
|
|
console.log('Can play');
|
|
});
|
|
|
|
currentPlayer.on('error', (error) => {
|
|
console.error('Player error:', error);
|
|
});
|
|
|
|
return currentPlayer;
|
|
}
|
|
|
|
/**
|
|
* Destroy current player instance
|
|
*/
|
|
export function destroyPlayer() {
|
|
if (currentPlayer) {
|
|
currentPlayer.destroy();
|
|
currentPlayer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get current player instance
|
|
* @returns {Artplayer|null} Current player or null
|
|
*/
|
|
export function getPlayer() {
|
|
return currentPlayer;
|
|
}
|
|
|
|
/**
|
|
* Create a lazy-load placeholder with play button
|
|
* @param {Object} options - Placeholder options
|
|
* @returns {HTMLElement} Placeholder element
|
|
*/
|
|
export function createPlayerPlaceholder(options = {}) {
|
|
const { poster, onClick } = options;
|
|
|
|
const placeholder = document.createElement('div');
|
|
placeholder.className = 'player-skeleton';
|
|
|
|
if (poster) {
|
|
placeholder.style.backgroundImage = `url(${poster})`;
|
|
placeholder.style.backgroundSize = 'cover';
|
|
placeholder.style.backgroundPosition = 'center';
|
|
}
|
|
|
|
placeholder.innerHTML = `
|
|
<div class="player-skeleton__play">
|
|
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M8 5v14l11-7z"/>
|
|
</svg>
|
|
</div>
|
|
`;
|
|
|
|
if (onClick) {
|
|
placeholder.addEventListener('click', onClick);
|
|
}
|
|
|
|
return placeholder;
|
|
}
|