yp / index.html
Ahmadraza76's picture
Create a full-featured watch party platform where users can watch YouTube videos together in real-time, with synchronized playback and a modern UI/UX. 🎯 Core Requirements: ✅ Real-time sync: Video playback must stay synced for all users in the party room (like Teleparty, Watch2Gether). ✅ YouTube support: Use yt-dlp (mu38 supported) in the backend to fetch direct video/audio stream URLs for smooth playback. ✅ High quality playback: Allow up to 1080p+ streaming with lowest latency using HTML5 video player. ✅ Custom room system: Room create/join via unique link Host has control (play, pause, seek) Guest syncs automatically ✅ Chat box: Real-time text chat for viewers in the room. ✅ User presence: Show online/offline users with colored indicators (e.g. green dot). ✅ Responsive UI: Should work perfectly on desktop and mobile browsers. ✅ No YouTube Ads during playback (since yt-dlp is used). ✅ Optionally allow private rooms (password protected). ⚙️ Tech Stack Suggestions: Frontend: React + Tailwind CSS (or Next.js) Backend: Node.js + Socket.IO (for real-time), Express Streaming: yt-dlp (mu38 version), ffmpeg (if needed for format conversion) Database: MongoDB or any fast NoSQL (for room metadata) ✨ Extra Features (if possible): Dark/light mode toggle Video queue/playlist support Host transfer option Emoji reactions in chat Watch history for users 🔐 The entire system must be self-hostable on a VPS, and capable of running without YouTube API key. - Initial Deployment
e0b94eb verified
Raw
History Blame Contribute Delete
48 kB
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SyncStream - Watch Videos Together</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/socket.io-client@4.7.2/dist/socket.io.min.js"></script>
<style>
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #1e293b;
}
::-webkit-scrollbar-thumb {
background: #475569;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #64748b;
}
/* Video player aspect ratio */
.video-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
height: 0;
overflow: hidden;
}
.video-container iframe,
.video-container video {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* Animation for new messages */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
.message-animate {
animation: fadeIn 0.3s ease-out;
}
/* Custom toggle switch */
.toggle-checkbox:checked {
right: 0;
border-color: #3b82f6;
}
.toggle-checkbox:checked + .toggle-label {
background-color: #3b82f6;
}
</style>
</head>
<body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-200">
<!-- Navigation -->
<nav class="bg-white dark:bg-gray-800 shadow-md py-4 px-6 flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fas fa-film text-blue-500 text-2xl"></i>
<h1 class="text-xl font-bold">SyncStream</h1>
</div>
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-moon dark:hidden"></i>
<i class="fas fa-sun hidden dark:inline"></i>
</button>
<button id="user-menu" class="flex items-center space-x-2">
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold">U</div>
<span class="hidden md:inline">User</span>
</button>
</div>
</nav>
<!-- Main Content -->
<div class="container mx-auto px-4 py-8">
<!-- Room Controls -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg mb-6 p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div class="flex items-center space-x-4">
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-link text-gray-400"></i>
</div>
<input type="text" id="room-link" class="pl-10 pr-4 py-2 border rounded-lg w-full md:w-64 bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600" value="https://syncstream.party/room/abc123" readonly>
</div>
<button id="copy-link" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
<i class="fas fa-copy mr-2"></i>Copy Link
</button>
</div>
<div class="flex items-center space-x-4">
<div class="flex items-center space-x-2">
<i class="fas fa-users text-gray-500 dark:text-gray-400"></i>
<span id="user-count" class="font-medium">1</span>
</div>
<div class="relative">
<button id="settings-toggle" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition">
<i class="fas fa-cog mr-2"></i>Settings
</button>
<div id="settings-dropdown" class="hidden absolute right-0 mt-2 w-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 z-10">
<div class="flex items-center justify-between mb-4">
<span>Dark Mode</span>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="dark-mode-toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="dark-mode-toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
<div class="flex items-center justify-between mb-4">
<span>Private Room</span>
<div class="relative inline-block w-10 mr-2 align-middle select-none">
<input type="checkbox" id="private-room-toggle" class="toggle-checkbox absolute block w-6 h-6 rounded-full bg-white border-4 appearance-none cursor-pointer"/>
<label for="private-room-toggle" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
</div>
</div>
<div id="password-field" class="hidden mb-4">
<label for="room-password" class="block text-sm font-medium mb-1">Room Password</label>
<input type="password" id="room-password" class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600">
</div>
<button id="transfer-host" class="w-full px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition mb-2">
<i class="fas fa-crown mr-2"></i>Transfer Host
</button>
<button id="leave-room" class="w-full px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition">
<i class="fas fa-sign-out-alt mr-2"></i>Leave Room
</button>
</div>
</div>
</div>
</div>
</div>
<div class="flex flex-col lg:flex-row gap-6">
<!-- Video Player Section -->
<div class="lg:w-2/3">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden">
<!-- Video Player -->
<div class="video-container">
<video id="video-player" class="w-full" controls></video>
</div>
<!-- Video Controls -->
<div class="p-4 bg-gray-50 dark:bg-gray-700">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-4">
<button id="play-pause" class="w-10 h-10 rounded-full bg-blue-500 text-white flex items-center justify-center hover:bg-blue-600 transition">
<i class="fas fa-play" id="play-icon"></i>
</button>
<div class="flex items-center space-x-2">
<button id="volume-btn" class="w-8 h-8 text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100">
<i class="fas fa-volume-up" id="volume-icon"></i>
</button>
<input type="range" id="volume-slider" min="0" max="1" step="0.01" value="1" class="w-20">
</div>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-gray-600 dark:text-gray-300" id="current-time">0:00</div>
<div class="flex items-center">
<input type="range" id="progress-bar" min="0" max="100" value="0" class="w-64 md:w-96">
</div>
<div class="text-sm text-gray-600 dark:text-gray-300" id="duration">0:00</div>
</div>
<div class="flex items-center space-x-2">
<button id="quality-btn" class="px-3 py-1 bg-gray-200 dark:bg-gray-600 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-500 transition">
<span id="quality-text">Auto</span>
<i class="fas fa-caret-down ml-1"></i>
</button>
<div id="quality-dropdown" class="hidden absolute mt-2 w-32 bg-white dark:bg-gray-800 rounded-lg shadow-lg py-1 z-10">
<a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" data-quality="auto">Auto</a>
<a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" data-quality="1080">1080p</a>
<a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" data-quality="720">720p</a>
<a href="#" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700" data-quality="480">480p</a>
</div>
<button id="fullscreen-btn" class="w-8 h-8 text-gray-600 dark:text-gray-300 hover:text-gray-800 dark:hover:text-gray-100">
<i class="fas fa-expand"></i>
</button>
</div>
</div>
<!-- Video Info -->
<div class="mb-4">
<h2 class="text-xl font-bold mb-1" id="video-title">No video selected</h2>
<p class="text-gray-600 dark:text-gray-300 text-sm" id="video-description">Add a video to start watching together</p>
</div>
<!-- Video URL Input -->
<div class="flex space-x-2">
<div class="flex-grow relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<i class="fas fa-link text-gray-400"></i>
</div>
<input type="text" id="video-url" class="pl-10 pr-4 py-2 border rounded-lg w-full bg-gray-50 dark:bg-gray-600 border-gray-300 dark:border-gray-500" placeholder="Paste YouTube URL here">
</div>
<button id="add-video" class="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
<i class="fas fa-plus mr-2"></i>Add
</button>
</div>
</div>
</div>
<!-- Playlist -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg mt-6 overflow-hidden">
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="font-semibold text-lg">
<i class="fas fa-list-ol mr-2 text-blue-500"></i>Playlist
</h3>
<button id="clear-playlist" class="text-sm text-red-500 hover:text-red-600">
<i class="fas fa-trash mr-1"></i>Clear All
</button>
</div>
<div class="divide-y divide-gray-200 dark:divide-gray-700 max-h-64 overflow-y-auto" id="playlist-container">
<div class="p-4 flex items-center hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
<div class="w-16 h-10 bg-gray-200 dark:bg-gray-600 rounded flex-shrink-0 overflow-hidden">
<img src="https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg" class="w-full h-full object-cover">
</div>
<div class="ml-3 flex-grow">
<h4 class="font-medium text-sm line-clamp-1">Never Gonna Give You Up</h4>
<p class="text-xs text-gray-500 dark:text-gray-400">Rick Astley • 3:32</p>
</div>
<button class="text-gray-400 hover:text-red-500">
<i class="fas fa-times"></i>
</button>
</div>
<!-- More playlist items will be added here dynamically -->
</div>
</div>
</div>
<!-- Chat Section -->
<div class="lg:w-1/3">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg overflow-hidden h-full flex flex-col">
<div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
<h3 class="font-semibold text-lg">
<i class="fas fa-comments mr-2 text-blue-500"></i>Chat
</h3>
<div class="flex items-center space-x-2">
<button id="emoji-picker-btn" class="w-8 h-8 text-gray-600 dark:text-gray-300 hover:text-blue-500">
<i class="far fa-smile"></i>
</button>
<div id="emoji-picker" class="hidden absolute right-0 mt-2 w-64 h-64 bg-white dark:bg-gray-800 rounded-lg shadow-lg p-4 z-10 overflow-y-auto">
<!-- Emoji categories and emojis will be added here -->
</div>
</div>
</div>
<!-- Chat Messages -->
<div class="flex-grow p-4 overflow-y-auto" id="chat-messages">
<div class="message mb-4 message-animate">
<div class="flex items-start">
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold mr-2">S</div>
<div>
<div class="flex items-center">
<span class="font-semibold mr-2">System</span>
<span class="text-xs text-gray-500 dark:text-gray-400">Just now</span>
</div>
<p class="text-sm mt-1">Welcome to the room! Start watching videos together with your friends.</p>
</div>
</div>
</div>
<!-- More chat messages will be added here dynamically -->
</div>
<!-- Chat Input -->
<div class="p-4 border-t border-gray-200 dark:border-gray-700">
<div class="flex space-x-2">
<input type="text" id="chat-input" class="flex-grow px-4 py-2 border rounded-lg bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600" placeholder="Type a message...">
<button id="send-message" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
</div>
<!-- Users Online -->
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg mt-6 overflow-hidden">
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
<h3 class="font-semibold text-lg">
<i class="fas fa-users mr-2 text-blue-500"></i>Users Online
</h3>
</div>
<div class="divide-y divide-gray-200 dark:divide-gray-700 max-h-64 overflow-y-auto" id="users-list">
<div class="p-3 flex items-center">
<div class="relative">
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-semibold mr-2">Y</div>
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-gray-800"></div>
</div>
<div class="ml-2">
<p class="font-medium">You (Host)</p>
<p class="text-xs text-gray-500 dark:text-gray-400">Watching</p>
</div>
</div>
<!-- More users will be added here dynamically -->
</div>
</div>
</div>
</div>
</div>
<!-- Modals -->
<!-- Create/Join Room Modal -->
<div id="room-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Join a Watch Party</h3>
<button id="close-room-modal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label for="room-id" class="block text-sm font-medium mb-1">Room ID</label>
<input type="text" id="room-id" class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600" placeholder="Enter room ID">
</div>
<div id="password-input" class="hidden">
<label for="join-password" class="block text-sm font-medium mb-1">Room Password</label>
<input type="password" id="join-password" class="w-full px-3 py-2 border rounded-lg bg-gray-50 dark:bg-gray-700 border-gray-300 dark:border-gray-600" placeholder="Enter password">
</div>
<div class="flex space-x-3 pt-2">
<button id="join-room" class="flex-grow px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">
Join Room
</button>
<button id="create-room" class="flex-grow px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition">
Create Room
</button>
</div>
</div>
</div>
</div>
<!-- Transfer Host Modal -->
<div id="transfer-host-modal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Transfer Host</h3>
<button id="close-transfer-modal" class="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<p class="text-gray-600 dark:text-gray-300">Select a user to transfer host privileges to:</p>
<div class="max-h-60 overflow-y-auto" id="transfer-users-list">
<!-- Users will be listed here -->
</div>
</div>
</div>
</div>
<script>
// DOM Elements
const videoPlayer = document.getElementById('video-player');
const playPauseBtn = document.getElementById('play-pause');
const playIcon = document.getElementById('play-icon');
const progressBar = document.getElementById('progress-bar');
const currentTimeDisplay = document.getElementById('current-time');
const durationDisplay = document.getElementById('duration');
const volumeBtn = document.getElementById('volume-btn');
const volumeIcon = document.getElementById('volume-icon');
const volumeSlider = document.getElementById('volume-slider');
const fullscreenBtn = document.getElementById('fullscreen-btn');
const qualityBtn = document.getElementById('quality-btn');
const qualityDropdown = document.getElementById('quality-dropdown');
const qualityText = document.getElementById('quality-text');
const videoUrlInput = document.getElementById('video-url');
const addVideoBtn = document.getElementById('add-video');
const videoTitle = document.getElementById('video-title');
const videoDescription = document.getElementById('video-description');
const playlistContainer = document.getElementById('playlist-container');
const clearPlaylistBtn = document.getElementById('clear-playlist');
const chatInput = document.getElementById('chat-input');
const sendMessageBtn = document.getElementById('send-message');
const chatMessages = document.getElementById('chat-messages');
const emojiPickerBtn = document.getElementById('emoji-picker-btn');
const emojiPicker = document.getElementById('emoji-picker');
const usersList = document.getElementById('users-list');
const userCount = document.getElementById('user-count');
const roomLink = document.getElementById('room-link');
const copyLinkBtn = document.getElementById('copy-link');
const settingsToggle = document.getElementById('settings-toggle');
const settingsDropdown = document.getElementById('settings-dropdown');
const darkModeToggle = document.getElementById('dark-mode-toggle');
const privateRoomToggle = document.getElementById('private-room-toggle');
const passwordField = document.getElementById('password-field');
const roomPassword = document.getElementById('room-password');
const transferHostBtn = document.getElementById('transfer-host');
const leaveRoomBtn = document.getElementById('leave-room');
const themeToggle = document.getElementById('theme-toggle');
const roomModal = document.getElementById('room-modal');
const closeRoomModal = document.getElementById('close-room-modal');
const joinRoomBtn = document.getElementById('join-room');
const createRoomBtn = document.getElementById('create-room');
const roomIdInput = document.getElementById('room-id');
const passwordInput = document.getElementById('password-input');
const joinPassword = document.getElementById('join-password');
const transferHostModal = document.getElementById('transfer-host-modal');
const closeTransferModal = document.getElementById('close-transfer-modal');
const transferUsersList = document.getElementById('transfer-users-list');
// State
let isHost = true;
let currentVideo = null;
let playlist = [];
let users = [];
let socket = null;
let roomId = 'abc123'; // This would be generated or from URL in a real app
let isPlaying = false;
let currentVolume = 1;
let isMuted = false;
let currentQuality = 'auto';
let isDarkMode = localStorage.getItem('darkMode') === 'true' ||
(!localStorage.getItem('darkMode') && window.matchMedia('(prefers-color-scheme: dark)').matches);
// Initialize
document.documentElement.classList.toggle('dark', isDarkMode);
darkModeToggle.checked = isDarkMode;
// Mock socket connection (in a real app, this would connect to your backend)
function connectSocket() {
console.log('Connecting to socket server...');
// In a real app: socket = io('https://your-backend-url');
// Mock socket behavior
setTimeout(() => {
console.log('Socket connected');
addSystemMessage('Connected to the room');
updateUserList([
{ id: 'user1', name: 'You', isHost: true, status: 'online', watching: true },
{ id: 'user2', name: 'Friend 1', isHost: false, status: 'online', watching: true },
{ id: 'user3', name: 'Friend 2', isHost: false, status: 'online', watching: false }
]);
}, 500);
}
// Event Listeners
themeToggle.addEventListener('click', toggleDarkMode);
darkModeToggle.addEventListener('change', toggleDarkMode);
privateRoomToggle.addEventListener('change', function() {
passwordField.classList.toggle('hidden', !this.checked);
});
settingsToggle.addEventListener('click', function() {
settingsDropdown.classList.toggle('hidden');
});
document.addEventListener('click', function(event) {
if (!settingsToggle.contains(event.target) && !settingsDropdown.contains(event.target)) {
settingsDropdown.classList.add('hidden');
}
if (!qualityBtn.contains(event.target) && !qualityDropdown.contains(event.target)) {
qualityDropdown.classList.add('hidden');
}
if (!emojiPickerBtn.contains(event.target) && !emojiPicker.contains(event.target)) {
emojiPicker.classList.add('hidden');
}
});
qualityBtn.addEventListener('click', function() {
qualityDropdown.classList.toggle('hidden');
});
document.querySelectorAll('#quality-dropdown a').forEach(item => {
item.addEventListener('click', function(e) {
e.preventDefault();
currentQuality = this.dataset.quality;
qualityText.textContent = currentQuality === 'auto' ? 'Auto' : `${currentQuality}p`;
qualityDropdown.classList.add('hidden');
// In a real app, this would change the video quality
console.log(`Changed quality to ${currentQuality}`);
});
});
playPauseBtn.addEventListener('click', togglePlayPause);
videoPlayer.addEventListener('play', function() {
isPlaying = true;
playIcon.className = 'fas fa-pause';
});
videoPlayer.addEventListener('pause', function() {
isPlaying = false;
playIcon.className = 'fas fa-play';
});
videoPlayer.addEventListener('timeupdate', updateProgressBar);
videoPlayer.addEventListener('durationchange', updateDuration);
videoPlayer.addEventListener('volumechange', updateVolumeUI);
progressBar.addEventListener('input', function() {
const seekTime = videoPlayer.duration * (this.value / 100);
videoPlayer.currentTime = seekTime;
// In a real app, this would sync with other users
if (isHost) {
// socket.emit('seek', { time: seekTime });
}
});
volumeBtn.addEventListener('click', toggleMute);
volumeSlider.addEventListener('input', function() {
videoPlayer.volume = this.value;
currentVolume = this.value;
updateVolumeUI();
});
fullscreenBtn.addEventListener('click', toggleFullscreen);
addVideoBtn.addEventListener('click', addVideoToPlaylist);
videoUrlInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') addVideoToPlaylist();
});
clearPlaylistBtn.addEventListener('click', clearPlaylist);
sendMessageBtn.addEventListener('click', sendMessage);
chatInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') sendMessage();
});
emojiPickerBtn.addEventListener('click', function() {
emojiPicker.classList.toggle('hidden');
// In a real app, this would load emoji categories and emojis
});
copyLinkBtn.addEventListener('click', copyRoomLink);
transferHostBtn.addEventListener('click', openTransferHostModal);
leaveRoomBtn.addEventListener('click', leaveRoom);
closeRoomModal.addEventListener('click', function() {
roomModal.classList.add('hidden');
});
joinRoomBtn.addEventListener('click', joinRoom);
createRoomBtn.addEventListener('click', createRoom);
closeTransferModal.addEventListener('click', function() {
transferHostModal.classList.add('hidden');
});
// Functions
function toggleDarkMode() {
isDarkMode = !isDarkMode;
document.documentElement.classList.toggle('dark', isDarkMode);
localStorage.setItem('darkMode', isDarkMode);
darkModeToggle.checked = isDarkMode;
}
function togglePlayPause() {
if (isPlaying) {
videoPlayer.pause();
// In a real app, this would sync with other users
if (isHost) {
// socket.emit('pause');
}
} else {
videoPlayer.play();
// In a real app, this would sync with other users
if (isHost) {
// socket.emit('play');
}
}
}
function updateProgressBar() {
const percent = (videoPlayer.currentTime / videoPlayer.duration) * 100;
progressBar.value = percent;
// Update time display
currentTimeDisplay.textContent = formatTime(videoPlayer.currentTime);
}
function updateDuration() {
durationDisplay.textContent = formatTime(videoPlayer.duration);
}
function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
}
function updateVolumeUI() {
if (videoPlayer.muted || videoPlayer.volume === 0) {
volumeIcon.className = 'fas fa-volume-mute';
isMuted = true;
} else {
isMuted = false;
if (videoPlayer.volume >= 0.5) {
volumeIcon.className = 'fas fa-volume-up';
} else if (videoPlayer.volume > 0) {
volumeIcon.className = 'fas fa-volume-down';
} else {
volumeIcon.className = 'fas fa-volume-off';
}
}
volumeSlider.value = videoPlayer.volume;
}
function toggleMute() {
videoPlayer.muted = !videoPlayer.muted;
updateVolumeUI();
}
function toggleFullscreen() {
if (!document.fullscreenElement) {
videoPlayer.requestFullscreen().catch(err => {
console.error(`Error attempting to enable fullscreen: ${err.message}`);
});
} else {
document.exitFullscreen();
}
}
function addVideoToPlaylist() {
const url = videoUrlInput.value.trim();
if (!url) return;
// In a real app, this would validate the URL and fetch video info from your backend
// For demo purposes, we'll mock this
const videoId = extractVideoId(url) || 'dQw4w9WgXcQ'; // Default to Rick Astley if invalid
const videoInfo = {
id: videoId,
title: "Sample Video Title",
duration: 183, // 3:03 in seconds
thumbnail: `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`,
url: url
};
playlist.push(videoInfo);
renderPlaylist();
if (!currentVideo) {
playVideo(videoInfo);
}
videoUrlInput.value = '';
// In a real app, this would sync with other users
if (isHost) {
// socket.emit('addToPlaylist', videoInfo);
}
}
function extractVideoId(url) {
// Simple URL parsing - in a real app you'd want more robust validation
const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/;
const match = url.match(regExp);
return (match && match[2].length === 11) ? match[2] : null;
}
function renderPlaylist() {
playlistContainer.innerHTML = '';
playlist.forEach((video, index) => {
const item = document.createElement('div');
item.className = 'p-4 flex items-center hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer';
if (currentVideo && currentVideo.id === video.id) {
item.classList.add('bg-blue-50', 'dark:bg-blue-900');
}
item.innerHTML = `
<div class="w-16 h-10 bg-gray-200 dark:bg-gray-600 rounded flex-shrink-0 overflow-hidden">
<img src="${video.thumbnail}" class="w-full h-full object-cover">
</div>
<div class="ml-3 flex-grow">
<h4 class="font-medium text-sm line-clamp-1">${video.title}</h4>
<p class="text-xs text-gray-500 dark:text-gray-400">${formatTime(video.duration)}</p>
</div>
<button class="text-gray-400 hover:text-red-500 remove-video" data-index="${index}">
<i class="fas fa-times"></i>
</button>
`;
item.addEventListener('click', () => {
playVideo(video);
});
playlistContainer.appendChild(item);
});
// Add event listeners to remove buttons
document.querySelectorAll('.remove-video').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const index = parseInt(this.dataset.index);
removeFromPlaylist(index);
});
});
}
function playVideo(video) {
currentVideo = video;
videoTitle.textContent = video.title;
videoDescription.textContent = `Added by You • ${formatTime(video.duration)}`;
// In a real app, this would use yt-dlp to get the direct stream URL
// For demo, we'll just show a placeholder
videoPlayer.src = '';
videoPlayer.poster = video.thumbnail;
renderPlaylist();
// In a real app, this would sync with other users
if (isHost) {
// socket.emit('playVideo', video);
}
}
function removeFromPlaylist(index) {
playlist.splice(index, 1);
renderPlaylist();
// In a real app, this would sync with other users
if (isHost) {
// socket.emit('removeFromPlaylist', index);
}
}
function clearPlaylist() {
playlist = [];
renderPlaylist();
currentVideo = null;
videoPlayer.src = '';
videoTitle.textContent = 'No video selected';
videoDescription.textContent = 'Add a video to start watching together';
// In a real app, this would sync with other users
if (isHost) {
// socket.emit('clearPlaylist');
}
}
function sendMessage() {
const message = chatInput.value.trim();
if (!message) return;
// In a real app, this would send via socket
// socket.emit('chatMessage', { text: message });
addChatMessage('You', message, new Date(), true);
chatInput.value = '';
}
function addChatMessage(sender, text, timestamp, isSelf = false) {
const messageDiv = document.createElement('div');
messageDiv.className = `message mb-4 message-animate ${isSelf ? 'self-message' : ''}`;
const timeString = formatChatTime(timestamp);
messageDiv.innerHTML = `
<div class="flex items-start ${isSelf ? 'flex-row-reverse' : ''}">
<div class="w-8 h-8 rounded-full ${isSelf ? 'bg-blue-500' : 'bg-purple-500'} flex items-center justify-center text-white font-semibold ${isSelf ? 'ml-2' : 'mr-2'}">
${sender.charAt(0).toUpperCase()}
</div>
<div class="${isSelf ? 'text-right' : ''}">
<div class="flex items-center ${isSelf ? 'justify-end' : ''}">
<span class="font-semibold mr-2">${isSelf ? 'You' : sender}</span>
<span class="text-xs text-gray-500 dark:text-gray-400">${timeString}</span>
</div>
<p class="text-sm mt-1 ${isSelf ? 'bg-blue-500 text-white' : 'bg-gray-100 dark:bg-gray-700'} rounded-lg px-3 py-2 inline-block">${text}</p>
</div>
</div>
`;
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function addSystemMessage(text) {
const messageDiv = document.createElement('div');
messageDiv.className = 'message mb-4 message-animate';
messageDiv.innerHTML = `
<div class="flex items-start">
<div class="w-8 h-8 rounded-full bg-gray-500 flex items-center justify-center text-white font-semibold mr-2">
<i class="fas fa-info-circle"></i>
</div>
<div>
<div class="flex items-center">
<span class="font-semibold mr-2">System</span>
<span class="text-xs text-gray-500 dark:text-gray-400">just now</span>
</div>
<p class="text-sm mt-1">${text}</p>
</div>
</div>
`;
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function formatChatTime(date) {
const now = new Date();
const diff = now - date;
if (diff < 60000) return 'just now';
if (diff < 3600000) return `${Math.floor(diff / 60000)} min ago`;
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function updateUserList(userList) {
users = userList;
usersList.innerHTML = '';
let onlineCount = 0;
userList.forEach(user => {
if (user.status === 'online') onlineCount++;
const userDiv = document.createElement('div');
userDiv.className = 'p-3 flex items-center';
userDiv.innerHTML = `
<div class="relative">
<div class="w-8 h-8 rounded-full ${user.isHost ? 'bg-yellow-500' : 'bg-green-500'} flex items-center justify-center text-white font-semibold mr-2">
${user.name.charAt(0).toUpperCase()}
</div>
<div class="absolute bottom-0 right-0 w-3 h-3 ${user.status === 'online' ? 'bg-green-500' : 'bg-gray-500'} rounded-full border-2 border-white dark:border-gray-800"></div>
</div>
<div class="ml-2">
<p class="font-medium">${user.name}${user.isHost ? ' (Host)' : ''}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">${user.watching ? 'Watching' : 'Idle'}</p>
</div>
`;
usersList.appendChild(userDiv);
});
userCount.textContent = onlineCount;
}
function copyRoomLink() {
roomLink.select();
document.execCommand('copy');
// Show feedback
const originalText = copyLinkBtn.innerHTML;
copyLinkBtn.innerHTML = '<i class="fas fa-check mr-2"></i>Copied!';
setTimeout(() => {
copyLinkBtn.innerHTML = originalText;
}, 2000);
}
function openTransferHostModal() {
transferUsersList.innerHTML = '';
// Filter out yourself and offline users
const eligibleUsers = users.filter(user =>
user.id !== 'user1' && user.status === 'online'
);
if (eligibleUsers.length === 0) {
transferUsersList.innerHTML = '<p class="text-gray-500 dark:text-gray-400 text-center py-4">No eligible users to transfer to</p>';
} else {
eligibleUsers.forEach(user => {
const userItem = document.createElement('div');
userItem.className = 'p-3 flex items-center hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer rounded-lg';
userItem.innerHTML = `
<div class="w-10 h-10 rounded-full bg-green-500 flex items-center justify-center text-white font-semibold mr-3">
${user.name.charAt(0).toUpperCase()}
</div>
<div>
<p class="font-medium">${user.name}</p>
</div>
`;
userItem.addEventListener('click', () => {
// In a real app, this would transfer host via socket
// socket.emit('transferHost', user.id);
addSystemMessage(`You transferred host privileges to ${user.name}`);
isHost = false;
transferHostModal.classList.add('hidden');
});
transferUsersList.appendChild(userItem);
});
}
transferHostModal.classList.remove('hidden');
}
function leaveRoom() {
// In a real app, this would disconnect from socket
addSystemMessage('You left the room');
// Redirect to home or show room modal
roomModal.classList.remove('hidden');
}
function joinRoom() {
const roomId = roomIdInput.value.trim();
if (!roomId) return;
const password = privateRoomToggle.checked ? joinPassword.value.trim() : null;
// In a real app, this would validate with the backend
addSystemMessage(`Joined room ${roomId}`);
roomModal.classList.add('hidden');
isHost = false;
}
function createRoom() {
// In a real app, this would create a room on the backend
const roomId = generateRoomId();
const isPrivate = privateRoomToggle.checked;
const password = isPrivate ? roomPassword.value.trim() : null;
addSystemMessage(`Created new room ${roomId}`);
roomModal.classList.add('hidden');
isHost = true;
roomLink.value = `https://syncstream.party/room/${roomId}`;
}
function generateRoomId() {
// Simple ID generator - in a real app you'd want something more robust
return Math.random().toString(36).substring(2, 8).toUpperCase();
}
// Initialize the app
connectSocket();
// For demo purposes, add some mock data
setTimeout(() => {
addChatMessage('Friend 1', 'Hey everyone! Ready to watch some videos?', new Date(Date.now() - 120000));
addChatMessage('Friend 2', 'Yeah! What should we watch first?', new Date(Date.now() - 60000));
// Add a sample video to playlist
const sampleVideo = {
id: 'dQw4w9WgXcQ',
title: "Never Gonna Give You Up",
duration: 212,
thumbnail: "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg",
url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
};
playlist.push(sampleVideo);
renderPlaylist();
}, 1000);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Ahmadraza76/yp" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>