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:
SamiAhmed7777
2025-10-21 00:39:45 -07:00
commit 0b7e2d0a5b
6080 changed files with 1332936 additions and 0 deletions

View File

View File

@@ -0,0 +1,573 @@
{* Enhanced Video Player Template *}
<div class="video-player-container" id="video-player-{$video_key}">
<div class="video-wrapper">
<video
id="video-element-{$video_key}"
class="video-element"
poster="{$video_thumbnail}"
preload="metadata"
playsinline
webkit-playsinline>
<p class="video-error-message">
Your browser doesn't support HTML5 video.
<a href="{$video_download_url}">Download the video</a> instead.
</p>
</video>
{* Loading Spinner *}
<div class="video-loading" id="video-loading-{$video_key}">
<div class="spinner"></div>
<p>Loading video...</p>
</div>
{* Custom Controls *}
<div class="video-controls" id="video-controls-{$video_key}">
<div class="controls-row controls-bottom">
{* Progress Bar *}
<div class="progress-container">
<div class="progress-bar">
<div class="progress-buffer"></div>
<div class="progress-played"></div>
<div class="progress-handle"></div>
</div>
<div class="progress-tooltip"></div>
</div>
{* Control Buttons *}
<div class="controls-buttons">
<div class="controls-left">
<button class="control-btn play-pause-btn" title="Play/Pause">
<i class="icon-play"></i>
<i class="icon-pause" style="display: none;"></i>
</button>
<div class="volume-container">
<button class="control-btn volume-btn" title="Mute/Unmute">
<i class="icon-volume-up"></i>
<i class="icon-volume-off" style="display: none;"></i>
</button>
<div class="volume-slider">
<div class="volume-bar">
<div class="volume-fill"></div>
<div class="volume-handle"></div>
</div>
</div>
</div>
<div class="time-display">
<span class="current-time">0:00</span>
<span class="time-separator">/</span>
<span class="duration">0:00</span>
</div>
</div>
<div class="controls-right">
<div class="quality-container">
<button class="control-btn quality-btn" title="Quality">
<i class="icon-settings"></i>
<span class="quality-label">Auto</span>
</button>
<div class="quality-menu">
<div class="quality-option" data-quality="auto">Auto</div>
{foreach from=$video_qualities item=quality}
<div class="quality-option" data-quality="{$quality.format}">{$quality.label}</div>
{/foreach}
</div>
</div>
<div class="speed-container">
<button class="control-btn speed-btn" title="Playback Speed">
<i class="icon-speed"></i>
<span class="speed-label">1x</span>
</button>
<div class="speed-menu">
<div class="speed-option" data-speed="0.25">0.25x</div>
<div class="speed-option" data-speed="0.5">0.5x</div>
<div class="speed-option" data-speed="0.75">0.75x</div>
<div class="speed-option active" data-speed="1">1x</div>
<div class="speed-option" data-speed="1.25">1.25x</div>
<div class="speed-option" data-speed="1.5">1.5x</div>
<div class="speed-option" data-speed="2">2x</div>
</div>
</div>
<button class="control-btn pip-btn" title="Picture in Picture" style="display: none;">
<i class="icon-pip"></i>
</button>
<button class="control-btn fullscreen-btn" title="Fullscreen">
<i class="icon-fullscreen"></i>
<i class="icon-fullscreen-exit" style="display: none;"></i>
</button>
</div>
</div>
</div>
</div>
{* Big Play Button *}
<div class="big-play-button" id="big-play-{$video_key}">
<i class="icon-play-large"></i>
</div>
{* Error Message *}
<div class="video-error" id="video-error-{$video_key}" style="display: none;">
<div class="error-content">
<i class="icon-error"></i>
<h3>Video Error</h3>
<p class="error-message">Unable to load video. Please try again.</p>
<button class="retry-btn">Retry</button>
</div>
</div>
</div>
{* Video Information *}
<div class="video-info">
<h1 class="video-title">{$video_title}</h1>
<div class="video-meta">
<div class="video-stats">
<span class="view-count">{$video_views|number_format} views</span>
<span class="upload-date">{$video_date}</span>
</div>
<div class="video-actions">
<button class="action-btn like-btn {if $user_liked}active{/if}" data-action="like">
<i class="icon-thumbs-up"></i>
<span class="like-count">{$video_likes|default:0}</span>
</button>
<button class="action-btn dislike-btn {if $user_disliked}active{/if}" data-action="dislike">
<i class="icon-thumbs-down"></i>
<span class="dislike-count">{$video_dislikes|default:0}</span>
</button>
<button class="action-btn share-btn" data-action="share">
<i class="icon-share"></i>
<span>Share</span>
</button>
<button class="action-btn save-btn {if $video_saved}active{/if}" data-action="save">
<i class="icon-bookmark"></i>
<span>Save</span>
</button>
</div>
</div>
</div>
</div>
{* Video Player JavaScript *}
<script>
// Video player configuration
const videoConfig_{$video_key} = {
videoKey: '{$video_key}',
streamUrl: '{$stream_url}',
hlsSupported: {if $hls_supported}true{else}false{/if},
qualities: {$video_qualities|@json_encode},
autoplay: {if $autoplay}true{else}false{/if},
startTime: {$start_time|default:0},
userId: '{$user_id|default:""}',
csrfToken: '{$csrf_token}'
};
// Initialize player when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
new EasyStreamPlayer('video-player-{$video_key}', videoConfig_{$video_key});
});
</script>
<style>
.video-player-container {
position: relative;
width: 100%;
max-width: 1280px;
margin: 0 auto;
background: #000;
border-radius: 8px;
overflow: hidden;
}
.video-wrapper {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 aspect ratio */
background: #000;
}
.video-element {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: contain;
}
.video-loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #fff;
z-index: 10;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top: 3px solid #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.video-controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
padding: 20px 15px 15px;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 20;
}
.video-wrapper:hover .video-controls,
.video-controls.show {
opacity: 1;
}
.progress-container {
margin-bottom: 10px;
position: relative;
}
.progress-bar {
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
position: relative;
cursor: pointer;
}
.progress-buffer,
.progress-played {
height: 100%;
border-radius: 2px;
position: absolute;
top: 0;
left: 0;
}
.progress-buffer {
background: rgba(255, 255, 255, 0.5);
}
.progress-played {
background: #ff4444;
}
.progress-handle {
width: 12px;
height: 12px;
background: #ff4444;
border-radius: 50%;
position: absolute;
top: -4px;
left: 0;
transform: translateX(-50%);
opacity: 0;
transition: opacity 0.2s ease;
}
.progress-bar:hover .progress-handle {
opacity: 1;
}
.controls-buttons {
display: flex;
justify-content: space-between;
align-items: center;
}
.controls-left,
.controls-right {
display: flex;
align-items: center;
gap: 10px;
}
.control-btn {
background: none;
border: none;
color: #fff;
cursor: pointer;
padding: 8px;
border-radius: 4px;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
gap: 4px;
}
.control-btn:hover {
background: rgba(255, 255, 255, 0.1);
}
.volume-container,
.quality-container,
.speed-container {
position: relative;
}
.volume-slider {
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
padding: 10px;
border-radius: 4px;
margin-bottom: 5px;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
}
.volume-container:hover .volume-slider {
opacity: 1;
visibility: visible;
}
.volume-bar {
width: 4px;
height: 60px;
background: rgba(255, 255, 255, 0.3);
border-radius: 2px;
position: relative;
cursor: pointer;
}
.volume-fill {
background: #ff4444;
width: 100%;
border-radius: 2px;
position: absolute;
bottom: 0;
}
.quality-menu,
.speed-menu {
position: absolute;
bottom: 100%;
right: 0;
background: rgba(0, 0, 0, 0.9);
border-radius: 4px;
padding: 5px 0;
margin-bottom: 5px;
min-width: 80px;
opacity: 0;
visibility: hidden;
transition: all 0.2s ease;
}
.quality-container:hover .quality-menu,
.speed-container:hover .speed-menu {
opacity: 1;
visibility: visible;
}
.quality-option,
.speed-option {
padding: 8px 15px;
color: #fff;
cursor: pointer;
transition: background-color 0.2s ease;
}
.quality-option:hover,
.speed-option:hover,
.quality-option.active,
.speed-option.active {
background: rgba(255, 255, 255, 0.1);
}
.time-display {
color: #fff;
font-size: 14px;
font-family: monospace;
}
.big-play-button {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80px;
height: 80px;
background: rgba(255, 68, 68, 0.9);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
z-index: 15;
}
.big-play-button:hover {
background: rgba(255, 68, 68, 1);
transform: translate(-50%, -50%) scale(1.1);
}
.big-play-button i {
color: #fff;
font-size: 32px;
margin-left: 4px;
}
.video-error {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 30;
}
.error-content {
text-align: center;
color: #fff;
}
.error-content i {
font-size: 48px;
color: #ff4444;
margin-bottom: 15px;
}
.retry-btn {
background: #ff4444;
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-top: 15px;
}
.video-info {
padding: 20px;
background: #fff;
}
.video-title {
font-size: 24px;
font-weight: 600;
margin: 0 0 15px 0;
line-height: 1.3;
}
.video-meta {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.video-stats {
display: flex;
gap: 15px;
color: #666;
font-size: 14px;
}
.video-actions {
display: flex;
gap: 10px;
}
.action-btn {
display: flex;
align-items: center;
gap: 5px;
padding: 8px 12px;
background: none;
border: 1px solid #ddd;
border-radius: 20px;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
}
.action-btn:hover {
background: #f5f5f5;
}
.action-btn.active {
background: #ff4444;
color: #fff;
border-color: #ff4444;
}
/* Mobile Responsive */
@media (max-width: 768px) {
.video-controls {
padding: 15px 10px 10px;
}
.controls-buttons {
flex-direction: column;
gap: 10px;
}
.controls-left,
.controls-right {
justify-content: center;
}
.video-title {
font-size: 20px;
}
.video-meta {
flex-direction: column;
align-items: flex-start;
}
.video-actions {
flex-wrap: wrap;
}
.action-btn {
font-size: 12px;
padding: 6px 10px;
}
}
@media (max-width: 480px) {
.big-play-button {
width: 60px;
height: 60px;
}
.big-play-button i {
font-size: 24px;
}
.video-info {
padding: 15px;
}
.video-title {
font-size: 18px;
}
}
</style>

View 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;