parth-meet / index.html
parthmax4's picture
Update index.html
d35ffe6 verified
Raw
History Blame Contribute Delete
22.2 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ParthMeet - Professional Video Meetings</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes slideIn {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.fade-in { animation: fadeIn 0.5s ease-out; }
.slide-in { animation: slideIn 0.3s ease-out; }
.pulse-animation { animation: pulse 2s infinite; }
.video-container {
position: relative;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
overflow: hidden;
aspect-ratio: 16/9;
}
video {
width: 100%;
height: 100%;
object-fit: cover;
}
.control-btn {
transition: all 0.2s ease;
}
.control-btn:hover {
transform: scale(1.1);
}
.control-btn:active {
transform: scale(0.95);
}
.gradient-bg {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.glass-effect {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- Home Screen -->
<div id="homeScreen" class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-4xl w-full fade-in">
<div class="text-center mb-12">
<h1 class="text-6xl font-bold text-gray-900 mb-4">
Parth<span class="gradient-bg bg-clip-text text-transparent">Meet</span>
</h1>
<p class="text-xl text-gray-600">Professional video meetings made simple- Developed by Parthmax(Saksham Pathak)</p>
</div>
<div class="grid md:grid-cols-2 gap-6">
<!-- Create Meeting Card -->
<div class="bg-white rounded-2xl shadow-lg p-8 hover:shadow-xl transition-shadow">
<div class="w-16 h-16 gradient-bg rounded-full flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-3">Create Meeting</h2>
<p class="text-gray-600 mb-6">Start a new video meeting instantly</p>
<input type="text" id="hostName" placeholder="Your name"
class="w-full px-4 py-3 border border-gray-300 rounded-lg mb-4 focus:ring-2 focus:ring-purple-500 focus:border-transparent outline-none">
<button onclick="createMeeting()"
class="w-full gradient-bg text-white py-3 rounded-lg font-semibold hover:opacity-90 transition-opacity">
Create Meeting
</button>
</div>
<!-- Join Meeting Card -->
<div class="bg-white rounded-2xl shadow-lg p-8 hover:shadow-xl transition-shadow">
<div class="w-16 h-16 bg-green-500 rounded-full flex items-center justify-center mb-6">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
</div>
<h2 class="text-2xl font-bold text-gray-900 mb-3">Join Meeting</h2>
<p class="text-gray-600 mb-6">Enter a meeting code to join</p>
<input type="text" id="participantName" placeholder="Your name"
class="w-full px-4 py-3 border border-gray-300 rounded-lg mb-3 focus:ring-2 focus:ring-green-500 focus:border-transparent outline-none">
<input type="text" id="meetingCode" placeholder="Meeting code"
class="w-full px-4 py-3 border border-gray-300 rounded-lg mb-4 focus:ring-2 focus:ring-green-500 focus:border-transparent outline-none">
<button onclick="joinMeeting()"
class="w-full bg-green-500 text-white py-3 rounded-lg font-semibold hover:bg-green-600 transition-colors">
Join Meeting
</button>
</div>
</div>
</div>
</div>
<!-- Meeting Screen -->
<div id="meetingScreen" class="hidden min-h-screen bg-gray-900 flex flex-col">
<!-- Header -->
<div class="bg-gray-800 border-b border-gray-700 px-6 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-4">
<h2 class="text-white font-semibold text-lg">ParthMeet</h2>
<div class="flex items-center gap-2 glass-effect px-3 py-1 rounded-full">
<div class="w-2 h-2 bg-green-500 rounded-full pulse-animation"></div>
<span class="text-gray-300 text-sm" id="meetingId">Meeting ID</span>
</div>
</div>
<button onclick="copyMeetingId()"
class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors text-sm">
Copy Meeting ID
</button>
</div>
</div>
<!-- Video Grid -->
<div class="flex-1 p-6 overflow-auto">
<div id="videoGrid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 max-w-7xl mx-auto">
<!-- Local Video -->
<div class="video-container relative group">
<video id="localVideo" autoplay muted playsinline></video>
<div class="absolute bottom-4 left-4 bg-black bg-opacity-50 px-3 py-1 rounded-lg">
<span class="text-white text-sm font-medium" id="localName">You</span>
</div>
<div id="localMutedIndicator" class="hidden absolute top-4 right-4 bg-red-500 rounded-full p-2">
<svg class="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clip-rule="evenodd"></path>
</svg>
</div>
</div>
</div>
</div>
<!-- Controls -->
<div class="bg-gray-800 border-t border-gray-700 px-6 py-6">
<div class="flex items-center justify-center gap-4">
<button id="micBtn" onclick="toggleMic()"
class="control-btn w-14 h-14 bg-gray-700 hover:bg-gray-600 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z"></path>
</svg>
</button>
<button id="videoBtn" onclick="toggleVideo()"
class="control-btn w-14 h-14 bg-gray-700 hover:bg-gray-600 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
</svg>
</button>
<button onclick="toggleScreenShare()"
class="control-btn w-14 h-14 bg-gray-700 hover:bg-gray-600 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>
</svg>
</button>
<button onclick="endMeeting()"
class="control-btn w-14 h-14 bg-red-600 hover:bg-red-700 rounded-full flex items-center justify-center">
<svg class="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
</div>
<script>
let localStream = null;
let screenStream = null;
let peerConnections = {};
let ws = null;
let currentMeetingId = null;
let participantId = null;
let isMicOn = true;
let isVideoOn = true;
let isScreenSharing = false;
const config = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
async function createMeeting() {
const hostName = document.getElementById('hostName').value.trim();
if (!hostName) {
alert('Please enter your name');
return;
}
try {
const response = await fetch('/api/meeting/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ host_name: hostName })
});
const data = await response.json();
if (data.success) {
currentMeetingId = data.meeting_id;
participantId = 'host_' + Date.now();
await initializeMeeting(hostName);
}
} catch (error) {
alert('Failed to create meeting: ' + error.message);
}
}
async function joinMeeting() {
const participantName = document.getElementById('participantName').value.trim();
const meetingCode = document.getElementById('meetingCode').value.trim();
if (!participantName || !meetingCode) {
alert('Please enter your name and meeting code');
return;
}
try {
const response = await fetch(`/api/meeting/${meetingCode}`);
const data = await response.json();
if (data.success) {
currentMeetingId = meetingCode;
participantId = 'participant_' + Date.now();
await initializeMeeting(participantName);
}
} catch (error) {
alert('Meeting not found or has ended');
}
}
async function initializeMeeting(userName) {
try {
localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
document.getElementById('localVideo').srcObject = localStream;
document.getElementById('localName').textContent = userName;
document.getElementById('meetingId').textContent = currentMeetingId;
document.getElementById('homeScreen').classList.add('hidden');
document.getElementById('meetingScreen').classList.remove('hidden');
connectWebSocket();
} catch (error) {
alert('Failed to access camera/microphone: ' + error.message);
}
}
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}/ws/${currentMeetingId}/${participantId}`);
ws.onmessage = async (event) => {
const message = JSON.parse(event.data);
switch (message.type) {
case 'participant_joined':
await handleParticipantJoined(message.data.participant_id);
break;
case 'participant_left':
handleParticipantLeft(message.data.participant_id);
break;
case 'offer':
await handleOffer(message);
break;
case 'answer':
await handleAnswer(message);
break;
case 'ice_candidate':
await handleIceCandidate(message);
break;
case 'meeting_ended':
alert(message.data.message);
window.location.reload();
break;
}
};
}
async function handleParticipantJoined(remoteId) {
if (remoteId === participantId) return;
const pc = new RTCPeerConnection(config);
peerConnections[remoteId] = pc;
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
pc.onicecandidate = (event) => {
if (event.candidate) {
ws.send(JSON.stringify({
type: 'ice_candidate',
candidate: event.candidate,
from: participantId,
target: remoteId
}));
}
};
pc.ontrack = (event) => {
addRemoteVideo(remoteId, event.streams[0]);
};
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
ws.send(JSON.stringify({
type: 'offer',
offer: offer,
from: participantId,
target: remoteId
}));
}
async function handleOffer(message) {
const remoteId = message.from;
const pc = new RTCPeerConnection(config);
peerConnections[remoteId] = pc;
localStream.getTracks().forEach(track => {
pc.addTrack(track, localStream);
});
pc.onicecandidate = (event) => {
if (event.candidate) {
ws.send(JSON.stringify({
type: 'ice_candidate',
candidate: event.candidate,
from: participantId,
target: remoteId
}));
}
};
pc.ontrack = (event) => {
addRemoteVideo(remoteId, event.streams[0]);
};
await pc.setRemoteDescription(new RTCSessionDescription(message.offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
ws.send(JSON.stringify({
type: 'answer',
answer: answer,
from: participantId,
target: remoteId
}));
}
async function handleAnswer(message) {
const pc = peerConnections[message.from];
if (pc) {
await pc.setRemoteDescription(new RTCSessionDescription(message.answer));
}
}
async function handleIceCandidate(message) {
const pc = peerConnections[message.from];
if (pc) {
await pc.addIceCandidate(new RTCIceCandidate(message.candidate));
}
}
function addRemoteVideo(remoteId, stream) {
let videoContainer = document.getElementById(`video_${remoteId}`);
if (!videoContainer) {
const grid = document.getElementById('videoGrid');
videoContainer = document.createElement('div');
videoContainer.id = `video_${remoteId}`;
videoContainer.className = 'video-container relative group slide-in';
videoContainer.innerHTML = `
<video autoplay playsinline></video>
<div class="absolute bottom-4 left-4 bg-black bg-opacity-50 px-3 py-1 rounded-lg">
<span class="text-white text-sm font-medium">Participant</span>
</div>
`;
grid.appendChild(videoContainer);
}
videoContainer.querySelector('video').srcObject = stream;
}
function handleParticipantLeft(remoteId) {
if (peerConnections[remoteId]) {
peerConnections[remoteId].close();
delete peerConnections[remoteId];
}
const videoContainer = document.getElementById(`video_${remoteId}`);
if (videoContainer) {
videoContainer.remove();
}
}
function toggleMic() {
isMicOn = !isMicOn;
localStream.getAudioTracks()[0].enabled = isMicOn;
const btn = document.getElementById('micBtn');
btn.classList.toggle('bg-red-600', !isMicOn);
btn.classList.toggle('bg-gray-700', isMicOn);
document.getElementById('localMutedIndicator').classList.toggle('hidden', isMicOn);
}
function toggleVideo() {
isVideoOn = !isVideoOn;
localStream.getVideoTracks()[0].enabled = isVideoOn;
const btn = document.getElementById('videoBtn');
btn.classList.toggle('bg-red-600', !isVideoOn);
btn.classList.toggle('bg-gray-700', isVideoOn);
}
async function toggleScreenShare() {
if (!isScreenSharing) {
try {
screenStream = await navigator.mediaDevices.getDisplayMedia({
video: true
});
const screenTrack = screenStream.getVideoTracks()[0];
Object.values(peerConnections).forEach(pc => {
const sender = pc.getSenders().find(s => s.track.kind === 'video');
if (sender) sender.replaceTrack(screenTrack);
});
document.getElementById('localVideo').srcObject = screenStream;
isScreenSharing = true;
screenTrack.onended = () => {
stopScreenShare();
};
} catch (error) {
console.error('Screen share error:', error);
}
} else {
stopScreenShare();
}
}
function stopScreenShare() {
if (screenStream) {
screenStream.getTracks().forEach(track => track.stop());
const videoTrack = localStream.getVideoTracks()[0];
Object.values(peerConnections).forEach(pc => {
const sender = pc.getSenders().find(s => s.track.kind === 'video');
if (sender) sender.replaceTrack(videoTrack);
});
document.getElementById('localVideo').srcObject = localStream;
isScreenSharing = false;
}
}
function copyMeetingId() {
navigator.clipboard.writeText(currentMeetingId);
const btn = event.target;
const originalText = btn.textContent;
btn.textContent = 'Copied!';
setTimeout(() => {
btn.textContent = originalText;
}, 2000);
}
async function endMeeting() {
if (confirm('Are you sure you want to leave the meeting?')) {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
if (screenStream) {
screenStream.getTracks().forEach(track => track.stop());
}
if (ws) {
ws.close();
}
Object.values(peerConnections).forEach(pc => pc.close());
window.location.reload();
}
}
window.addEventListener('beforeunload', () => {
if (localStream) {
localStream.getTracks().forEach(track => track.stop());
}
if (ws) {
ws.close();
}
});
</script>
</body>
</html>