- 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
573 lines
14 KiB
Smarty
573 lines
14 KiB
Smarty
{* 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> |