feat: Add comprehensive documentation suite and reorganize project structure
- Created complete documentation in docs/ directory - Added PROJECT_OVERVIEW.md with feature highlights and getting started guide - Added ARCHITECTURE.md with system design and technical details - Added SECURITY.md with comprehensive security implementation guide - Added DEVELOPMENT.md with development workflows and best practices - Added DEPLOYMENT.md with production deployment instructions - Added API.md with complete REST API documentation - Added CONTRIBUTING.md with contribution guidelines - Added CHANGELOG.md with version history and migration notes - Reorganized all documentation files into docs/ directory for better organization - Updated README.md with proper documentation links and quick navigation - Enhanced project structure with professional documentation standards
This commit is contained in:
756
f_templates/frontend/tpl_videoplayer/videoplayer.js
Normal file
756
f_templates/frontend/tpl_videoplayer/videoplayer.js
Normal file
@@ -0,0 +1,756 @@
|
||||
/**
|
||||
* EasyStream Video Player
|
||||
* Advanced HTML5 video player with HLS support
|
||||
*/
|
||||
class EasyStreamPlayer {
|
||||
constructor(containerId, config) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.config = config;
|
||||
this.video = null;
|
||||
this.hls = null;
|
||||
this.currentQuality = 'auto';
|
||||
this.isFullscreen = false;
|
||||
this.controlsTimeout = null;
|
||||
this.progressUpdateInterval = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.video = this.container.querySelector('.video-element');
|
||||
this.setupEventListeners();
|
||||
this.loadVideo();
|
||||
this.setupProgressTracking();
|
||||
|
||||
// Load HLS.js if needed
|
||||
if (this.config.hlsSupported && !window.Hls) {
|
||||
this.loadHLSLibrary();
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const controls = this.container.querySelector('.video-controls');
|
||||
const bigPlayBtn = this.container.querySelector('.big-play-button');
|
||||
|
||||
// Video events
|
||||
this.video.addEventListener('loadstart', () => this.showLoading());
|
||||
this.video.addEventListener('canplay', () => this.hideLoading());
|
||||
this.video.addEventListener('play', () => this.onPlay());
|
||||
this.video.addEventListener('pause', () => this.onPause());
|
||||
this.video.addEventListener('timeupdate', () => this.updateProgress());
|
||||
this.video.addEventListener('durationchange', () => this.updateDuration());
|
||||
this.video.addEventListener('progress', () => this.updateBuffer());
|
||||
this.video.addEventListener('error', (e) => this.onError(e));
|
||||
this.video.addEventListener('ended', () => this.onEnded());
|
||||
|
||||
// Control events
|
||||
this.setupControlEvents();
|
||||
|
||||
// Big play button
|
||||
bigPlayBtn.addEventListener('click', () => this.togglePlay());
|
||||
|
||||
// Container hover for controls
|
||||
this.container.addEventListener('mouseenter', () => this.showControls());
|
||||
this.container.addEventListener('mouseleave', () => this.hideControlsDelayed());
|
||||
this.container.addEventListener('mousemove', () => this.showControls());
|
||||
|
||||
// Touch events for mobile
|
||||
this.setupTouchEvents();
|
||||
|
||||
// Keyboard shortcuts
|
||||
this.setupKeyboardShortcuts();
|
||||
|
||||
// Fullscreen events
|
||||
document.addEventListener('fullscreenchange', () => this.onFullscreenChange());
|
||||
document.addEventListener('webkitfullscreenchange', () => this.onFullscreenChange());
|
||||
document.addEventListener('mozfullscreenchange', () => this.onFullscreenChange());
|
||||
document.addEventListener('MSFullscreenChange', () => this.onFullscreenChange());
|
||||
}
|
||||
|
||||
setupControlEvents() {
|
||||
const controls = this.container.querySelector('.video-controls');
|
||||
|
||||
// Play/Pause button
|
||||
const playPauseBtn = controls.querySelector('.play-pause-btn');
|
||||
playPauseBtn.addEventListener('click', () => this.togglePlay());
|
||||
|
||||
// Progress bar
|
||||
const progressBar = controls.querySelector('.progress-bar');
|
||||
progressBar.addEventListener('click', (e) => this.seekToPosition(e));
|
||||
progressBar.addEventListener('mousedown', (e) => this.startSeeking(e));
|
||||
|
||||
// Volume controls
|
||||
const volumeBtn = controls.querySelector('.volume-btn');
|
||||
const volumeBar = controls.querySelector('.volume-bar');
|
||||
volumeBtn.addEventListener('click', () => this.toggleMute());
|
||||
volumeBar.addEventListener('click', (e) => this.setVolume(e));
|
||||
|
||||
// Quality selector
|
||||
const qualityOptions = controls.querySelectorAll('.quality-option');
|
||||
qualityOptions.forEach(option => {
|
||||
option.addEventListener('click', () => this.changeQuality(option.dataset.quality));
|
||||
});
|
||||
|
||||
// Speed selector
|
||||
const speedOptions = controls.querySelectorAll('.speed-option');
|
||||
speedOptions.forEach(option => {
|
||||
option.addEventListener('click', () => this.changeSpeed(parseFloat(option.dataset.speed)));
|
||||
});
|
||||
|
||||
// Fullscreen button
|
||||
const fullscreenBtn = controls.querySelector('.fullscreen-btn');
|
||||
fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());
|
||||
|
||||
// Picture-in-Picture button
|
||||
const pipBtn = controls.querySelector('.pip-btn');
|
||||
if (pipBtn && 'pictureInPictureEnabled' in document) {
|
||||
pipBtn.style.display = 'block';
|
||||
pipBtn.addEventListener('click', () => this.togglePictureInPicture());
|
||||
}
|
||||
}
|
||||
|
||||
setupTouchEvents() {
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let touchStartTime = 0;
|
||||
|
||||
this.container.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
touchStartY = e.touches[0].clientY;
|
||||
touchStartTime = this.video.currentTime;
|
||||
});
|
||||
|
||||
this.container.addEventListener('touchmove', (e) => {
|
||||
e.preventDefault();
|
||||
const touchX = e.touches[0].clientX;
|
||||
const touchY = e.touches[0].clientY;
|
||||
const deltaX = touchX - touchStartX;
|
||||
const deltaY = touchY - touchStartY;
|
||||
|
||||
// Horizontal swipe for seeking
|
||||
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > 50) {
|
||||
const seekAmount = (deltaX / this.container.offsetWidth) * this.video.duration;
|
||||
const newTime = Math.max(0, Math.min(this.video.duration, touchStartTime + seekAmount));
|
||||
this.video.currentTime = newTime;
|
||||
}
|
||||
|
||||
// Vertical swipe for volume (left side) or brightness (right side)
|
||||
if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > 50) {
|
||||
if (touchStartX < this.container.offsetWidth / 2) {
|
||||
// Left side - volume
|
||||
const volumeChange = -deltaY / this.container.offsetHeight;
|
||||
const newVolume = Math.max(0, Math.min(1, this.video.volume + volumeChange));
|
||||
this.video.volume = newVolume;
|
||||
this.updateVolumeDisplay();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.container.addEventListener('touchend', () => {
|
||||
this.showControls();
|
||||
this.hideControlsDelayed();
|
||||
});
|
||||
}
|
||||
|
||||
setupKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (!this.isPlayerFocused()) return;
|
||||
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
e.preventDefault();
|
||||
this.togglePlay();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
this.seek(-10);
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
this.seek(10);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.changeVolume(0.1);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.changeVolume(-0.1);
|
||||
break;
|
||||
case 'KeyM':
|
||||
e.preventDefault();
|
||||
this.toggleMute();
|
||||
break;
|
||||
case 'KeyF':
|
||||
e.preventDefault();
|
||||
this.toggleFullscreen();
|
||||
break;
|
||||
case 'Escape':
|
||||
if (this.isFullscreen) {
|
||||
this.exitFullscreen();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadVideo() {
|
||||
this.showLoading();
|
||||
|
||||
// Get stream information
|
||||
fetch(`/stream.php?v=${this.config.videoKey}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
this.setupVideoSources(data);
|
||||
|
||||
// Set start time if specified
|
||||
if (this.config.startTime > 0) {
|
||||
this.video.currentTime = this.config.startTime;
|
||||
}
|
||||
|
||||
// Autoplay if enabled
|
||||
if (this.config.autoplay) {
|
||||
this.play();
|
||||
}
|
||||
} else {
|
||||
this.showError(data.error || 'Failed to load video');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading video:', error);
|
||||
this.showError('Failed to load video');
|
||||
});
|
||||
}
|
||||
|
||||
setupVideoSources(streamData) {
|
||||
// Clear existing sources
|
||||
this.video.innerHTML = '';
|
||||
|
||||
if (streamData.streams.hls && this.supportsHLS()) {
|
||||
// Use HLS for adaptive streaming
|
||||
this.setupHLS(streamData.streams.hls.url);
|
||||
} else if (streamData.streams.progressive) {
|
||||
// Use progressive download
|
||||
this.setupProgressive(streamData.streams.progressive);
|
||||
} else {
|
||||
this.showError('No compatible video format available');
|
||||
return;
|
||||
}
|
||||
|
||||
// Update quality options
|
||||
this.updateQualityOptions(streamData.streams);
|
||||
}
|
||||
|
||||
setupHLS(hlsUrl) {
|
||||
if (window.Hls && Hls.isSupported()) {
|
||||
this.hls = new Hls({
|
||||
enableWorker: true,
|
||||
lowLatencyMode: false,
|
||||
backBufferLength: 90
|
||||
});
|
||||
|
||||
this.hls.loadSource(hlsUrl);
|
||||
this.hls.attachMedia(this.video);
|
||||
|
||||
this.hls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
console.log('HLS manifest loaded');
|
||||
this.hideLoading();
|
||||
});
|
||||
|
||||
this.hls.on(Hls.Events.ERROR, (event, data) => {
|
||||
console.error('HLS error:', data);
|
||||
if (data.fatal) {
|
||||
this.showError('Video streaming error');
|
||||
}
|
||||
});
|
||||
} else if (this.video.canPlayType('application/vnd.apple.mpegurl')) {
|
||||
// Native HLS support (Safari)
|
||||
this.video.src = hlsUrl;
|
||||
} else {
|
||||
this.showError('HLS not supported');
|
||||
}
|
||||
}
|
||||
|
||||
setupProgressive(progressiveStreams) {
|
||||
// Sort qualities by resolution (highest first)
|
||||
const sortedQualities = Object.entries(progressiveStreams)
|
||||
.sort((a, b) => this.getResolutionValue(b[0]) - this.getResolutionValue(a[0]));
|
||||
|
||||
// Add source elements
|
||||
sortedQualities.forEach(([quality, stream]) => {
|
||||
const source = document.createElement('source');
|
||||
source.src = stream.url;
|
||||
source.type = stream.type;
|
||||
source.setAttribute('data-quality', quality);
|
||||
this.video.appendChild(source);
|
||||
});
|
||||
|
||||
// Set default quality
|
||||
if (sortedQualities.length > 0) {
|
||||
this.video.src = sortedQualities[0][1].url;
|
||||
this.currentQuality = sortedQualities[0][0];
|
||||
}
|
||||
}
|
||||
|
||||
loadHLSLibrary() {
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://cdn.jsdelivr.net/npm/hls.js@latest';
|
||||
script.onload = () => {
|
||||
console.log('HLS.js loaded');
|
||||
this.loadVideo();
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
supportsHLS() {
|
||||
return window.Hls && Hls.isSupported() ||
|
||||
this.video.canPlayType('application/vnd.apple.mpegurl');
|
||||
}
|
||||
|
||||
togglePlay() {
|
||||
if (this.video.paused) {
|
||||
this.play();
|
||||
} else {
|
||||
this.pause();
|
||||
}
|
||||
}
|
||||
|
||||
play() {
|
||||
const playPromise = this.video.play();
|
||||
|
||||
if (playPromise !== undefined) {
|
||||
playPromise
|
||||
.then(() => {
|
||||
console.log('Video started playing');
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error playing video:', error);
|
||||
this.showError('Unable to play video');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pause() {
|
||||
this.video.pause();
|
||||
}
|
||||
|
||||
seek(seconds) {
|
||||
const newTime = Math.max(0, Math.min(this.video.duration, this.video.currentTime + seconds));
|
||||
this.video.currentTime = newTime;
|
||||
}
|
||||
|
||||
seekToPosition(e) {
|
||||
const progressBar = e.currentTarget;
|
||||
const rect = progressBar.getBoundingClientRect();
|
||||
const position = (e.clientX - rect.left) / rect.width;
|
||||
const newTime = position * this.video.duration;
|
||||
this.video.currentTime = newTime;
|
||||
}
|
||||
|
||||
changeVolume(delta) {
|
||||
const newVolume = Math.max(0, Math.min(1, this.video.volume + delta));
|
||||
this.video.volume = newVolume;
|
||||
this.updateVolumeDisplay();
|
||||
}
|
||||
|
||||
toggleMute() {
|
||||
this.video.muted = !this.video.muted;
|
||||
this.updateVolumeDisplay();
|
||||
}
|
||||
|
||||
setVolume(e) {
|
||||
const volumeBar = e.currentTarget;
|
||||
const rect = volumeBar.getBoundingClientRect();
|
||||
const position = 1 - ((e.clientY - rect.top) / rect.height);
|
||||
const newVolume = Math.max(0, Math.min(1, position));
|
||||
this.video.volume = newVolume;
|
||||
this.video.muted = false;
|
||||
this.updateVolumeDisplay();
|
||||
}
|
||||
|
||||
changeQuality(quality) {
|
||||
if (quality === this.currentQuality) return;
|
||||
|
||||
const currentTime = this.video.currentTime;
|
||||
const wasPlaying = !this.video.paused;
|
||||
|
||||
if (quality === 'auto' && this.hls) {
|
||||
this.hls.currentLevel = -1; // Auto quality
|
||||
} else if (this.hls) {
|
||||
// Find HLS level for quality
|
||||
const levels = this.hls.levels;
|
||||
const targetLevel = levels.findIndex(level =>
|
||||
level.height === this.getResolutionValue(quality)
|
||||
);
|
||||
if (targetLevel !== -1) {
|
||||
this.hls.currentLevel = targetLevel;
|
||||
}
|
||||
} else {
|
||||
// Progressive quality change
|
||||
const sources = this.video.querySelectorAll('source');
|
||||
const targetSource = Array.from(sources).find(source =>
|
||||
source.getAttribute('data-quality') === quality
|
||||
);
|
||||
if (targetSource) {
|
||||
this.video.src = targetSource.src;
|
||||
this.video.currentTime = currentTime;
|
||||
if (wasPlaying) {
|
||||
this.video.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.currentQuality = quality;
|
||||
this.updateQualityDisplay();
|
||||
}
|
||||
|
||||
changeSpeed(speed) {
|
||||
this.video.playbackRate = speed;
|
||||
this.updateSpeedDisplay();
|
||||
}
|
||||
|
||||
toggleFullscreen() {
|
||||
if (this.isFullscreen) {
|
||||
this.exitFullscreen();
|
||||
} else {
|
||||
this.enterFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
enterFullscreen() {
|
||||
const element = this.container;
|
||||
|
||||
if (element.requestFullscreen) {
|
||||
element.requestFullscreen();
|
||||
} else if (element.webkitRequestFullscreen) {
|
||||
element.webkitRequestFullscreen();
|
||||
} else if (element.mozRequestFullScreen) {
|
||||
element.mozRequestFullScreen();
|
||||
} else if (element.msRequestFullscreen) {
|
||||
element.msRequestFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
exitFullscreen() {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen();
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
} else if (document.mozCancelFullScreen) {
|
||||
document.mozCancelFullScreen();
|
||||
} else if (document.msExitFullscreen) {
|
||||
document.msExitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
togglePictureInPicture() {
|
||||
if (document.pictureInPictureElement) {
|
||||
document.exitPictureInPicture();
|
||||
} else {
|
||||
this.video.requestPictureInPicture();
|
||||
}
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
onPlay() {
|
||||
this.updatePlayPauseButton();
|
||||
this.hideBigPlayButton();
|
||||
this.startProgressTracking();
|
||||
}
|
||||
|
||||
onPause() {
|
||||
this.updatePlayPauseButton();
|
||||
this.showBigPlayButton();
|
||||
this.stopProgressTracking();
|
||||
}
|
||||
|
||||
onEnded() {
|
||||
this.showBigPlayButton();
|
||||
this.stopProgressTracking();
|
||||
this.updateProgress();
|
||||
}
|
||||
|
||||
onError(e) {
|
||||
console.error('Video error:', e);
|
||||
this.showError('Video playback error');
|
||||
}
|
||||
|
||||
onFullscreenChange() {
|
||||
this.isFullscreen = !!(document.fullscreenElement ||
|
||||
document.webkitFullscreenElement ||
|
||||
document.mozFullScreenElement ||
|
||||
document.msFullscreenElement);
|
||||
this.updateFullscreenButton();
|
||||
}
|
||||
|
||||
// UI Updates
|
||||
updatePlayPauseButton() {
|
||||
const playIcon = this.container.querySelector('.play-pause-btn .icon-play');
|
||||
const pauseIcon = this.container.querySelector('.play-pause-btn .icon-pause');
|
||||
|
||||
if (this.video.paused) {
|
||||
playIcon.style.display = 'inline';
|
||||
pauseIcon.style.display = 'none';
|
||||
} else {
|
||||
playIcon.style.display = 'none';
|
||||
pauseIcon.style.display = 'inline';
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress() {
|
||||
if (!this.video.duration) return;
|
||||
|
||||
const progress = (this.video.currentTime / this.video.duration) * 100;
|
||||
const progressPlayed = this.container.querySelector('.progress-played');
|
||||
const progressHandle = this.container.querySelector('.progress-handle');
|
||||
|
||||
progressPlayed.style.width = progress + '%';
|
||||
progressHandle.style.left = progress + '%';
|
||||
|
||||
this.updateTimeDisplay();
|
||||
}
|
||||
|
||||
updateBuffer() {
|
||||
if (!this.video.duration || !this.video.buffered.length) return;
|
||||
|
||||
const buffered = this.video.buffered.end(this.video.buffered.length - 1);
|
||||
const progress = (buffered / this.video.duration) * 100;
|
||||
const progressBuffer = this.container.querySelector('.progress-buffer');
|
||||
|
||||
progressBuffer.style.width = progress + '%';
|
||||
}
|
||||
|
||||
updateDuration() {
|
||||
this.updateTimeDisplay();
|
||||
}
|
||||
|
||||
updateTimeDisplay() {
|
||||
const currentTimeEl = this.container.querySelector('.current-time');
|
||||
const durationEl = this.container.querySelector('.duration');
|
||||
|
||||
currentTimeEl.textContent = this.formatTime(this.video.currentTime);
|
||||
durationEl.textContent = this.formatTime(this.video.duration || 0);
|
||||
}
|
||||
|
||||
updateVolumeDisplay() {
|
||||
const volumeFill = this.container.querySelector('.volume-fill');
|
||||
const volumeIcon = this.container.querySelector('.volume-btn i');
|
||||
|
||||
const volume = this.video.muted ? 0 : this.video.volume;
|
||||
volumeFill.style.height = (volume * 100) + '%';
|
||||
|
||||
// Update volume icon
|
||||
if (this.video.muted || volume === 0) {
|
||||
volumeIcon.className = 'icon-volume-off';
|
||||
} else {
|
||||
volumeIcon.className = 'icon-volume-up';
|
||||
}
|
||||
}
|
||||
|
||||
updateQualityDisplay() {
|
||||
const qualityLabel = this.container.querySelector('.quality-label');
|
||||
const qualityOptions = this.container.querySelectorAll('.quality-option');
|
||||
|
||||
qualityLabel.textContent = this.currentQuality === 'auto' ? 'Auto' : this.currentQuality;
|
||||
|
||||
qualityOptions.forEach(option => {
|
||||
option.classList.toggle('active', option.dataset.quality === this.currentQuality);
|
||||
});
|
||||
}
|
||||
|
||||
updateSpeedDisplay() {
|
||||
const speedLabel = this.container.querySelector('.speed-label');
|
||||
const speedOptions = this.container.querySelectorAll('.speed-option');
|
||||
|
||||
speedLabel.textContent = this.video.playbackRate + 'x';
|
||||
|
||||
speedOptions.forEach(option => {
|
||||
option.classList.toggle('active',
|
||||
parseFloat(option.dataset.speed) === this.video.playbackRate);
|
||||
});
|
||||
}
|
||||
|
||||
updateFullscreenButton() {
|
||||
const fullscreenIcon = this.container.querySelector('.fullscreen-btn .icon-fullscreen');
|
||||
const exitFullscreenIcon = this.container.querySelector('.fullscreen-btn .icon-fullscreen-exit');
|
||||
|
||||
if (this.isFullscreen) {
|
||||
fullscreenIcon.style.display = 'none';
|
||||
exitFullscreenIcon.style.display = 'inline';
|
||||
} else {
|
||||
fullscreenIcon.style.display = 'inline';
|
||||
exitFullscreenIcon.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Controls visibility
|
||||
showControls() {
|
||||
const controls = this.container.querySelector('.video-controls');
|
||||
controls.classList.add('show');
|
||||
|
||||
if (this.controlsTimeout) {
|
||||
clearTimeout(this.controlsTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
hideControlsDelayed() {
|
||||
if (this.controlsTimeout) {
|
||||
clearTimeout(this.controlsTimeout);
|
||||
}
|
||||
|
||||
this.controlsTimeout = setTimeout(() => {
|
||||
const controls = this.container.querySelector('.video-controls');
|
||||
controls.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
showBigPlayButton() {
|
||||
const bigPlayBtn = this.container.querySelector('.big-play-button');
|
||||
bigPlayBtn.style.display = 'flex';
|
||||
}
|
||||
|
||||
hideBigPlayButton() {
|
||||
const bigPlayBtn = this.container.querySelector('.big-play-button');
|
||||
bigPlayBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
const loading = this.container.querySelector('.video-loading');
|
||||
loading.style.display = 'block';
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
const loading = this.container.querySelector('.video-loading');
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const error = this.container.querySelector('.video-error');
|
||||
const errorMessage = error.querySelector('.error-message');
|
||||
|
||||
errorMessage.textContent = message;
|
||||
error.style.display = 'flex';
|
||||
|
||||
// Retry button
|
||||
const retryBtn = error.querySelector('.retry-btn');
|
||||
retryBtn.onclick = () => {
|
||||
error.style.display = 'none';
|
||||
this.loadVideo();
|
||||
};
|
||||
}
|
||||
|
||||
// Progress tracking for analytics
|
||||
setupProgressTracking() {
|
||||
if (!this.config.userId) return;
|
||||
|
||||
// Track progress every 10 seconds
|
||||
this.progressUpdateInterval = setInterval(() => {
|
||||
this.updateWatchProgress();
|
||||
}, 10000);
|
||||
|
||||
// Track on pause/seek
|
||||
this.video.addEventListener('pause', () => this.updateWatchProgress());
|
||||
this.video.addEventListener('seeked', () => this.updateWatchProgress());
|
||||
}
|
||||
|
||||
startProgressTracking() {
|
||||
if (this.progressUpdateInterval) return;
|
||||
this.setupProgressTracking();
|
||||
}
|
||||
|
||||
stopProgressTracking() {
|
||||
if (this.progressUpdateInterval) {
|
||||
clearInterval(this.progressUpdateInterval);
|
||||
this.progressUpdateInterval = null;
|
||||
}
|
||||
this.updateWatchProgress();
|
||||
}
|
||||
|
||||
updateWatchProgress() {
|
||||
if (!this.config.userId || !this.video.duration) return;
|
||||
|
||||
const data = {
|
||||
video_key: this.config.videoKey,
|
||||
current_time: this.video.currentTime,
|
||||
duration: this.video.duration,
|
||||
csrf_token: this.config.csrfToken
|
||||
};
|
||||
|
||||
fetch('/api/video/progress', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}).catch(error => {
|
||||
console.error('Error updating progress:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// Utility methods
|
||||
formatTime(seconds) {
|
||||
if (!seconds || isNaN(seconds)) return '0:00';
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
getResolutionValue(quality) {
|
||||
const resolutions = {
|
||||
'1080p': 1080,
|
||||
'720p': 720,
|
||||
'480p': 480,
|
||||
'360p': 360,
|
||||
'240p': 240
|
||||
};
|
||||
return resolutions[quality] || 0;
|
||||
}
|
||||
|
||||
isPlayerFocused() {
|
||||
return this.container.contains(document.activeElement) ||
|
||||
document.activeElement === document.body;
|
||||
}
|
||||
|
||||
updateQualityOptions(streams) {
|
||||
const qualityMenu = this.container.querySelector('.quality-menu');
|
||||
|
||||
// Clear existing options except auto
|
||||
const autoOption = qualityMenu.querySelector('[data-quality="auto"]');
|
||||
qualityMenu.innerHTML = '';
|
||||
qualityMenu.appendChild(autoOption);
|
||||
|
||||
// Add available qualities
|
||||
if (streams.progressive) {
|
||||
Object.keys(streams.progressive).forEach(quality => {
|
||||
const option = document.createElement('div');
|
||||
option.className = 'quality-option';
|
||||
option.dataset.quality = quality;
|
||||
option.textContent = this.getQualityLabel(quality);
|
||||
option.addEventListener('click', () => this.changeQuality(quality));
|
||||
qualityMenu.appendChild(option);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getQualityLabel(quality) {
|
||||
const labels = {
|
||||
'1080p': '1080p HD',
|
||||
'720p': '720p HD',
|
||||
'480p': '480p',
|
||||
'360p': '360p',
|
||||
'240p': '240p'
|
||||
};
|
||||
return labels[quality] || quality;
|
||||
}
|
||||
}
|
||||
|
||||
// Export for use in other scripts
|
||||
window.EasyStreamPlayer = EasyStreamPlayer;
|
||||
Reference in New Issue
Block a user