Vhs
Vhs
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Enhanced Video Player with HLS Support</title>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/
all.min.css" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/video.js/8.5.2/video-
js.min.css" rel="stylesheet">
<script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-http-streaming/3.3.0/
videojs-http-streaming.min.js"></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/video.js/8.5.2/video.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-quality-
levels/4.0.0/videojs-contrib-quality-levels.min.js"></script>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
background-color: #000;
color: #fff;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.video-container {
position: relative;
width: 80%;
max-width: 800px;
background-color: transparent;
aspect-ratio: 16/9;
}
/* Fullscreen styles */
.video-container:fullscreen {
width: 100%;
height: 100%;
max-width: none;
display: flex;
justify-content: center;
align-items: center;
background: black;
aspect-ratio: auto;
}
.video-container:-webkit-full-screen {
width: 100%;
height: 100%;
max-width: none;
display: flex;
justify-content: center;
align-items: center;
background: black;
aspect-ratio: auto;
}
.video-container:-moz-full-screen {
width: 100%;
height: 100%;
max-width: none;
display: flex;
justify-content: center;
align-items: center;
background: black;
aspect-ratio: auto;
}
.video-js {
width: 100%;
height: 100%;
background-color: transparent;
}
.vjs-quality-selector {
display: none !important;
}
.controls {
position: absolute;
bottom: 0;
left: 0;
right: 0;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
box-sizing: border-box;
z-index: 2;
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.8) 0%,
rgba(0, 0, 0, 0.6) 0%,
rgba(0, 0, 0, 0) 100%
);
height: 100px;
padding-top: 60px;
}
.progress-container {
position: absolute;
bottom: 50px;
left: 15px;
right: 15px;
height: 20px;
cursor: pointer;
display: flex;
align-items: center;
z-index: 3;
}
.progress-bar {
position: relative;
width: 100%;
height: 3px;
background: #333;
border-radius: 2px;
}
.progress {
position: absolute;
top: 0.15px;
left: 0;
width: 0;
height: 3px;
background: #5a4bda;
border-radius: 2px;
transition: width 0.05s linear;
box-shadow:
0 0 6px 3px rgba(90, 75, 218, 0.7),
0 0 4px 1px rgba(90, 75, 218, 0.9);
}
.control-button {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 5px;
margin: 0 5px;
font-size: 1.2em;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.left-controls, .right-controls {
display: flex;
align-items: center;
gap: 10px;
}
.time-info {
position: absolute;
bottom: 70px;
left: 15px;
display: flex;
align-items: center;
gap: 10px;
font-size: 16px;
font-weight: bold;
z-index: 3;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.end-time {
position: absolute;
bottom: 70px;
right: 15px;
font-size: 16px;
color: white;
font-weight: bold;
z-index: 3;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.playback-speed {
background: white;
color: black;
padding: 1.75px 5px;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
}
.volume-control {
position: relative;
display: flex;
align-items: center;
}
.volume-slider {
position: absolute;
left: 40px;
width: 0;
height: 40px;
transition: width 0.3s ease, opacity 0.3s ease;
opacity: 0;
border-radius: 8px;
display: flex;
align-items: center;
padding: 0 10px;
pointer-events: none;
background: rgba(0, 0, 0, 0.8);
}
.volume-slider.active {
width: 120px;
opacity: 1;
pointer-events: all;
}
.volume-percentage {
position: absolute;
background: white;
color: black;
padding: 3px 5px;
border-radius: 5px;
font-size: 12px;
top: -15px;
transform: translateX(-50%);
pointer-events: none;
transition: left 0.1s ease;
}
.volume-percentage::after {
content: '';
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid white;
}
input[type="range"] {
-webkit-appearance: none;
width: 100px;
height: 4px;
background: rgba(255, 255, 255, 0.4);
border-radius: 2px;
position: relative;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 12px;
height: 12px;
background: white;
border-radius: 50%;
cursor: pointer;
position: relative;
z-index: 2;
}
.volume-track {
position: absolute;
left: 10px;
height: 4px;
background: white;
border-radius: 2px;
pointer-events: none;
}
.settings-menu {
position: absolute;
bottom: 50px;
right: 80px;
width: 330px;
display: none;
z-index: 3;
}
.menu-container {
background: #17171C;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
position: absolute;
bottom: 0;
left: 0;
right: 0;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 142px;
}
.menu-container.hidden {
pointer-events: none;
visibility: hidden;
}
.menu-content {
opacity: 1;
transition: opacity 0.2s ease;
padding: 16px 0;
height: 100%;
}
.menu-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
}
.label {
color: #d9d9da;
font-size: 16px;
font-weight: 500;
width: 136px;
}
.value-selector {
background: #23232a;
padding: 10px 12px;
border-radius: 6px;
display: flex;
align-items: center;
gap: 4px;
cursor: pointer;
}
.value-selector:hover {
background: #1E1E24;
}
.selected-value {
color: #d9d9da;
font-size: 14px;
font-weight: 500;
width: 70px;
}
.dropdown {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: #17171C;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
opacity: 0;
pointer-events: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
height: 0;
z-index: 3;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dropdown.active {
opacity: 1;
pointer-events: auto;
height: 300px;
}
.dropdown-content {
opacity: 0;
transition: opacity 0.2s ease;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
.dropdown.active .dropdown-content {
opacity: 1;
}
.dropdown-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: #17171C;
z-index: 2;
flex-shrink: 0;
position: relative;
}
.dropdown-header::after {
content: '';
position: absolute;
bottom: 0;
left: 16px;
right: 16px;
height: 1px;
background: #3A3A46;
}
.back-button {
padding: 4px;
border-radius: 4px;
cursor: pointer;
}
.back-button:hover {
background: #23232A;
}
.dropdown-title {
color: #d9d9da;
font-size: 18px;
font-weight: 600;
}
#speed-options, #quality-options {
overflow-y: auto;
padding: 4px 16px 16px;
flex-grow: 1;
}
.option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
margin-bottom: 4px;
cursor: pointer;
border-radius: 6px;
}
.option:hover {
background: #23232A;
}
.option.selected {
background: #332E56;
}
.option-label {
color: #d9d9da;
font-size: 16px;
font-weight: 500;
}
.radio {
width: 14px;
height: 14px;
border: 1px solid #5A5A6C;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.option.selected .radio {
width: 20px;
height: 20px;
border-color: #B2A9FF;
}
.radio-inner {
display: none;
width: 15px;
height: 15px;
background: #B2A9FF;
border: 1px solid #17171C;
border-radius: 50%;
}
.option.selected .radio-inner {
display: block;
}
</style>
</head>
<body>
<div class="video-container">
<video id="video" class="video-js vjs-default-skin" controls
preload="auto"></video>
<div class="time-info">
<span id="startTime">0:00</span>
<div class="playback-speed" id="playbackSpeed">1x</div>
</div>
<div class="end-time" id="endTime">0:00</div>
<div class="controls">
<div class="left-controls">
<button class="control-button" id="playPauseBtn">
<i class="fas fa-play"></i>
</button>
<button class="control-button" id="rewindBtn">
<i class="fas fa-backward"></i>
</button>
<button class="control-button" id="forwardBtn">
<i class="fas fa-forward"></i>
</button>
<div class="volume-control">
<button class="control-button" id="muteBtn">
<i class="fas fa-volume-up"></i>
</button>
<div class="volume-slider">
<div class="volume-percentage">100%</div>
<div class="volume-track"></div>
<input type="range" id="volumeSlider" min="0" max="1"
step="0.01" value="1">
</div>
</div>
</div>
<div class="right-controls">
<button class="control-button" id="settingsBtn">
<i class="fas fa-cog"></i>
</button>
<button class="control-button" id="fullscreenBtn">
<i class="fas fa-expand"></i>
</button>
</div>
</div>
<div class="settings-menu">
<div class="container" id="menu-container">
<div class="menu-container" id="main-menu">
<div class="menu-content">
<div class="menu-item">
<span class="label">Speed</span>
<div class="value-selector"
onclick="toggleDropdown('speed', this)">
<span class="selected-value" id="speed-value">1</span>
<svg width="20" height="20" viewBox="0 0 20 20"
fill="none">
<path d="M8 14L12 10L8 6L8 14Z" fill="#B3B3BC"/>
</svg>
</div>
</div>
<div class="menu-item">
<span class="label">Quality</span>
<div class="value-selector"
onclick="toggleDropdown('quality', this)">
<span class="selected-value"
id="quality-value">Auto</span>
<svg width="20" height="20" viewBox="0 0 20 20"
fill="none">
<path d="M8 14L12 10L8 6L8 14Z" fill="#B3B3BC"/>
</svg>
</div>
</div>
</div>
</div>
if (url.endsWith('.mp4')) {
type = 'video/mp4';
} else if (url.endsWith('.webm')) {
type = 'video/webm';
} else if (url.endsWith('.ogv')) {
type = 'video/ogg';
} else if (url.includes('.m3u8')) {
type = 'application/x-mpegURL'; // HLS
} else if (url.includes('.mpd')) {
type = 'application/dash+xml'; // DASH
} else if (url.startsWith('rtmp://')) {
type = 'rtmp/mp4'; // RTMP
} else {
alert('Unsupported format. Please enter a valid video URL.');
return;
}
function handleSeek(clientX) {
const rect = progressBar.getBoundingClientRect();
const offsetX = clientX - rect.left;
const clampedOffsetX = Math.max(0, Math.min(offsetX, rect.width));
const clickPercent = clampedOffsetX / rect.width;
if (isDvrMode) {
const seekableStart = player.liveTracker.seekableStart();
const seekableEnd = player.liveTracker.seekableEnd();
const seekableRange = seekableEnd - seekableStart;
if (seekableRange > 0) {
const newTime = seekableStart + (clickPercent * seekableRange);
player.currentTime(newTime);
progress.style.width = `${clickPercent * 100}%`; // Update UI
immediately
startTime.textContent = formatTime(newTime); // Update time display
}
} else {
const duration = getVideoDuration();
if (duration > 0) {
const newTime = clickPercent * duration;
player.currentTime(newTime);
progress.style.width = `${clickPercent * 100}%`; // Update UI
immediately
startTime.textContent = formatTime(newTime); // Update time display
}
}
}
progressContainer.addEventListener('touchend', () => {
isDragging = false;
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
// Add both mouse and touch event listeners for outside clicks
document.addEventListener('mousedown', handleOutsideClick);
document.addEventListener('touchstart', (e) => {
handleOutsideClick(e.touches[0]);
});
function updateQualityOptions() {
const qualityLevels = player.qualityLevels();
const qualityOptions = document.getElementById('quality-options');
qualityOptions.innerHTML = '';
function initializeDropdowns() {
const speedContainer = document.getElementById('speed-options');
speedContainer.innerHTML = '';
speedOptions.forEach(speed => {
speedContainer.appendChild(createOption('speed', speed));
});
updateQualityOptions();
}
if (activeDropdown) {
document.getElementById(`${activeDropdown}-
dropdown`).classList.remove('active');
}
function closeDropdown(type) {
const dropdown = document.getElementById(`${type}-dropdown`);
dropdown.classList.remove('active');
requestAnimationFrame(() => {
mainMenu.classList.remove('hidden');
});
activeDropdown = null;
}
closeDropdown(type);
}
function formatTime(seconds) {
if (isNaN(seconds) || seconds === null || seconds === undefined) {
return "0:00";
}
if (hours > 0) {
return `${hours}:${minutes.toString().padStart(2, '0')}:$
{remainingSeconds.toString().padStart(2, '0')}`;
} else {
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
}
}
function updateVolumeDisplay() {
const value = player.muted() ? 0 : player.volume();
const percentage = Math.round(value * 100);
volumePercentage.textContent = `${percentage}%`;
volumeTrack.style.width = `${percentage}px`;
volumePercentage.style.left = `${percentage + 10}px`;
}
function updateVolumeIcon() {
const value = player.volume();
if (player.muted() || value === 0) {
muteBtn.innerHTML = '<i class="fas fa-volume-mute"></i>';
} else if (value < 0.5) {
muteBtn.innerHTML = '<i class="fas fa-volume-down"></i>';
} else {
muteBtn.innerHTML = '<i class="fas fa-volume-up"></i>';
}
}
case 'ArrowLeft':
e.preventDefault();
const newTimeBack = Math.max(0, player.currentTime() - 10);
player.currentTime(newTimeBack);
startTime.textContent = formatTime(newTimeBack);
break;
case 'ArrowRight':
e.preventDefault();
const duration = getVideoDuration();
const newTimeForward = Math.min(duration, player.currentTime()
+ 10);
player.currentTime(newTimeForward);
startTime.textContent = formatTime(newTimeForward);
break;
case 'ArrowUp':
e.preventDefault();
const newVolUp = Math.min(1, player.volume() + 0.1);
player.volume(newVolUp);
player.muted(false);
volumeSlider.value = newVolUp;
updateVolumeDisplay();
updateVolumeIcon();
break;
case 'ArrowDown':
e.preventDefault();
const newVolDown = Math.max(0, player.volume() - 0.1);
player.volume(newVolDown);
volumeSlider.value = newVolDown;
updateVolumeDisplay();
updateVolumeIcon();
break;
case 'KeyM':
e.preventDefault();
player.muted(!player.muted());
updateVolumeDisplay();
updateVolumeIcon();
break;
case 'KeyF':
e.preventDefault();
if (!document.fullscreenElement) {
videoContainer.requestFullscreen();
fullscreenBtn.innerHTML = '<i class="fas
fa-compress"></i>';
} else {
document.exitFullscreen();
fullscreenBtn.innerHTML = '<i class="fas fa-expand"></i>';
}
break;
case 'Comma':
if (e.shiftKey) { // < key
e.preventDefault();
const currentSpeed = player.playbackRate();
const speedIndex =
speedOptions.indexOf(currentSpeed.toString());
if (speedIndex > 0) {
const newSpeed = speedOptions[speedIndex - 1];
selectOption('speed', newSpeed);
}
}
break;
case 'Period':
if (e.shiftKey) { // > key
e.preventDefault();
const currentSpeed = player.playbackRate();
const speedIndex =
speedOptions.indexOf(currentSpeed.toString());
if (speedIndex < speedOptions.length - 1) {
const newSpeed = speedOptions[speedIndex + 1];
selectOption('speed', newSpeed);
}
}
break;
}
}
});
});
player.on('dispose', () => {
if (timeUpdateInterval) {
clearInterval(timeUpdateInterval);
timeUpdateInterval = null;
}
});
player.on('loadedmetadata', () => {
isDvrMode = player.liveTracker && player.liveTracker.isLive() &&
player.liveTracker.seekableEnd() > 0;
if (isDvrMode) {
console.log("DVR mode detected");
// Start the end time update interval for DVR mode
startEndTimeUpdates();
}
});
player.on('loadedmetadata', () => {
const duration = getVideoDuration();
endTime.textContent = formatTime(duration);
setTimeout(updateQualityOptions, 1000);
});
player.on('durationchange', () => {
const duration = getVideoDuration();
endTime.textContent = formatTime(duration);
});
player.on('qualitylevels', () => {
updateQualityOptions();
});
function startEndTimeUpdates() {
// Clear any existing interval
if (endTimeUpdateInterval) {
clearInterval(endTimeUpdateInterval);
}
player.on('timeupdate', () => {
if (!isDragging) {
const currentTime = player.currentTime();
if (isDvrMode) {
// In DVR mode, calculate progress based on seekable range
const seekableStart = player.liveTracker.seekableStart();
const seekableEnd = player.liveTracker.seekableEnd();
const seekableRange = seekableEnd - seekableStart;
if (seekableRange > 0) {
const progressPercent = ((currentTime - seekableStart) /
seekableRange) * 100;
progress.style.width = `${Math.min(progressPercent, 100)}%`; //
Prevent overflow
}
startTime.textContent = formatTime(currentTime);
endTime.textContent = formatTime(seekableEnd);
} else {
// Normal VOD behavior
const duration = getVideoDuration();
if (duration > 0) {
const progressPercent = (currentTime / duration) * 100;
progress.style.width = `${Math.min(progressPercent, 100)}%`; //
Prevent overflow
startTime.textContent = formatTime(currentTime);
}
}
}
});
player.on('durationchange', () => {
if (isDvrMode) {
if (player.liveTracker) {
const seekableEnd = player.liveTracker.seekableEnd();
endTime.textContent = formatTime(seekableEnd);
}
} else {
const duration = getVideoDuration();
endTime.textContent = formatTime(duration);
}
});
rewindBtn.addEventListener('click', () => {
if (isDvrMode) {
const seekableStart = player.liveTracker.seekableStart();
const newTime = Math.max(seekableStart, player.currentTime() - 10);
player.currentTime(newTime);
} else {
const newTime = Math.max(0, player.currentTime() - 10);
player.currentTime(newTime);
}
});
forwardBtn.addEventListener('click', () => {
if (isDvrMode) {
const seekableEnd = player.liveTracker.seekableEnd();
const newTime = Math.min(seekableEnd, player.currentTime() + 10);
player.currentTime(newTime);
} else {
const duration = getVideoDuration();
const newTime = Math.min(duration, player.currentTime() + 10);
player.currentTime(newTime);
}
});
fullscreenBtn.addEventListener('click', () => {
if (!document.fullscreenElement) {
videoContainer.requestFullscreen();
fullscreenBtn.innerHTML = '<i class="fas fa-compress"></i>';
} else {
document.exitFullscreen();
fullscreenBtn.innerHTML = '<i class="fas fa-expand"></i>';
}
});
settingsBtn.addEventListener('click', () => {
settingsMenu.style.display = settingsMenu.style.display === 'block' ? 'none' :
'block';
});
player.on('pause', () => {
playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
});
</script>
</body>
</html>