aethercall
Version:
A scalable WebRTC video calling API built with Node.js and OpenVidu
1,022 lines (877 loc) âĸ 41.6 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AetherCall Test - Video Calling</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
overflow: hidden;
}
.header {
background: #2c3e50;
color: white;
padding: 20px;
text-align: center;
}
.header h1 {
margin-bottom: 10px;
font-size: 2rem;
}
.content {
padding: 30px;
}
.card {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
border: 1px solid #e9ecef;
}
.card h3 {
color: #2c3e50;
margin-bottom: 15px;
font-size: 1.2rem;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: 600;
color: #495057;
}
.form-group input {
width: 100%;
padding: 12px;
border: 2px solid #e9ecef;
border-radius: 6px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.button {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
margin-right: 10px;
margin-bottom: 10px;
transition: background 0.3s;
}
.button:hover {
background: #5a6fd8;
}
.button:disabled {
background: #6c757d;
cursor: not-allowed;
}
.button.success {
background: #28a745;
}
.button.success:hover {
background: #218838;
}
.button.danger {
background: #dc3545;
}
.button.danger:hover {
background: #c82333;
}
.status {
padding: 15px;
border-radius: 6px;
margin-bottom: 20px;
font-weight: 600;
}
.status.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.status.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.status.info {
background: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
.video-container {
margin-top: 20px;
}
.video-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.video-element {
background: #000;
border-radius: 8px;
overflow: hidden;
position: relative;
aspect-ratio: 16/9;
border: 2px solid transparent;
transition: border-color 0.3s;
}
.video-element.speaking {
border-color: #28a745;
box-shadow: 0 0 15px rgba(40, 167, 69, 0.5);
}
.video-element.screen-share {
border-color: #007bff;
}
.video-element video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-label {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.7);
color: white;
padding: 5px 10px;
border-radius: 4px;
font-size: 12px;
}
.stream-info {
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.7);
color: white;
padding: 5px 8px;
border-radius: 4px;
font-size: 11px;
display: flex;
gap: 5px;
}
.stream-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.stream-indicator.video-on { background: #28a745; }
.stream-indicator.video-off { background: #dc3545; }
.stream-indicator.audio-on { background: #007bff; }
.stream-indicator.audio-off { background: #6c757d; }
.controls {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 20px;
}
.room-info {
background: #e8f5e8;
border: 1px solid #c3e6cb;
border-radius: 6px;
padding: 15px;
margin-top: 15px;
}
.room-info h4 {
color: #155724;
margin-bottom: 10px;
}
.room-info p {
margin: 5px 0;
color: #155724;
}
.participant-count {
background: #fff3cd;
border: 1px solid #ffeaa7;
border-radius: 6px;
padding: 10px;
margin-top: 15px;
text-align: center;
font-weight: 600;
color: #856404;
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>đĨ AetherCall Video Test</h1>
<p>Test the AetherCall API with real video calling functionality</p>
</div>
<div class="content">
<!-- Status messages -->
<div id="status" class="hidden"></div>
<!-- API Connection -->
<div class="card">
<h3>đ API Connection</h3>
<div class="form-group">
<label for="apiUrl">AetherCall API URL:</label>
<input type="text" id="apiUrl" value="http://localhost:3000" />
</div>
<button id="testConnection" class="button">Test Connection</button>
<div id="connectionStatus"></div>
</div>
<!-- Room Management -->
<div class="card">
<h3>đ Room Management</h3>
<p style="color: #6c757d; font-size: 14px; margin-bottom: 15px;">
<strong>Note:</strong> Creating a room requires API authentication. Joining a room does not.
</p>
<div class="form-group">
<label for="roomCode">Room Code:</label>
<input type="text" id="roomCode" value="TEST123" placeholder="Enter room code" />
</div>
<div class="form-group">
<label for="displayName">Your Name:</label>
<input type="text" id="displayName" value="Test User" placeholder="Enter your name" />
</div>
<div class="controls">
<button id="createRoom" class="button">Create New Room</button>
<button id="joinRoom" class="button success">Join Room</button>
<button id="leaveRoom" class="button danger hidden">Leave Room</button>
</div>
<div id="roomInfo" class="hidden"></div>
</div>
<!-- Call Controls -->
<div id="callControls" class="card hidden">
<h3>đī¸ Call Controls</h3>
<div class="controls">
<button id="toggleVideo" class="button">đš Toggle Video</button>
<button id="toggleAudio" class="button">đ¤ Toggle Audio</button>
<button id="shareScreen" class="button">đĨī¸ Share Screen</button>
<button id="stopScreenShare" class="button danger hidden">âšī¸ Stop Screen Share</button>
<button id="enableAllAudio" class="button">đ Enable All Audio</button>
<button id="testAudio" class="button">đĩ Test Audio</button>
<button id="startRecording" class="button">đ´ Start Recording</button>
<button id="stopRecording" class="button danger hidden">âšī¸ Stop Recording</button>
</div>
<div id="participantCount" class="participant-count">Participants: 0</div>
</div>
<!-- Video Container -->
<div id="videoContainer" class="video-container hidden">
<h3>đē Video Streams</h3>
<div id="videoGrid" class="video-grid">
<!-- Videos will be added here -->
</div>
</div>
</div>
</div>
<!-- OpenVidu Browser SDK -->
<script src="https://github.com/OpenVidu/openvidu/releases/download/v2.29.0/openvidu-browser-2.29.0.min.js"></script>
<script>
// Global variables
let session = null;
let publisher = null;
let screenPublisher = null; // For screen sharing
let subscribers = [];
let currentRoom = null;
let currentSessionId = null; // Store the actual OpenVidu session ID
let recordingId = null;
let apiToken = null;
// DOM elements
const elements = {
status: document.getElementById('status'),
apiUrl: document.getElementById('apiUrl'),
testConnection: document.getElementById('testConnection'),
connectionStatus: document.getElementById('connectionStatus'),
roomCode: document.getElementById('roomCode'),
displayName: document.getElementById('displayName'),
createRoom: document.getElementById('createRoom'),
joinRoom: document.getElementById('joinRoom'),
leaveRoom: document.getElementById('leaveRoom'),
roomInfo: document.getElementById('roomInfo'),
callControls: document.getElementById('callControls'),
toggleVideo: document.getElementById('toggleVideo'),
toggleAudio: document.getElementById('toggleAudio'),
shareScreen: document.getElementById('shareScreen'),
stopScreenShare: document.getElementById('stopScreenShare'),
enableAllAudio: document.getElementById('enableAllAudio'),
testAudio: document.getElementById('testAudio'),
startRecording: document.getElementById('startRecording'),
stopRecording: document.getElementById('stopRecording'),
participantCount: document.getElementById('participantCount'),
videoContainer: document.getElementById('videoContainer'),
videoGrid: document.getElementById('videoGrid')
};
// Utility functions
function showStatus(message, type = 'info') {
elements.status.className = `status ${type}`;
elements.status.textContent = message;
elements.status.classList.remove('hidden');
setTimeout(() => {
elements.status.classList.add('hidden');
}, 5000);
}
function updateParticipantCount() {
const count = session ? session.streamManagers.length : 0;
elements.participantCount.textContent = `Participants: ${count}`;
}
// API functions
async function testApiConnection() {
try {
showStatus('Testing API connection...', 'info');
// Try health endpoint first, fallback to root if it doesn't exist
let response;
try {
response = await fetch(`${elements.apiUrl.value}/health`);
} catch (error) {
// If health endpoint fails, try root endpoint
response = await fetch(`${elements.apiUrl.value}/`);
}
if (response.ok) {
const data = await response.json();
const version = data.version || 'Unknown';
const uptime = data.uptime ? Math.floor(data.uptime) : 'Unknown';
elements.connectionStatus.innerHTML = `
<div class="status success">
â
Connected! Version: ${version}, Uptime: ${uptime}s
</div>
`;
showStatus('API connection successful!', 'success');
return true;
} else {
throw new Error(`HTTP ${response.status}`);
}
} catch (error) {
elements.connectionStatus.innerHTML = `
<div class="status error">
â Connection failed: ${error.message}
</div>
`;
showStatus(`Connection failed: ${error.message}`, 'error');
return false;
}
}
async function getApiToken() {
try {
const response = await fetch(`${elements.apiUrl.value}/api/auth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clientId: 'vanilla-js-test' })
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.status}`);
}
const data = await response.json();
apiToken = data.data.accessToken;
return apiToken;
} catch (error) {
showStatus(`Failed to get API token: ${error.message}`, 'error');
throw error;
}
}
async function createRoom() {
try {
showStatus('Creating new room...', 'info');
const token = await getApiToken();
const response = await fetch(`${elements.apiUrl.value}/api/auth/room`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
roomName: 'Vanilla JS Test Room',
maxParticipants: 10
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || errorData.message || `HTTP ${response.status}`);
}
const data = await response.json();
currentRoom = data.data;
elements.roomCode.value = data.data.roomCode;
elements.roomInfo.innerHTML = `
<div class="room-info">
<h4>â
Room Created Successfully!</h4>
<p><strong>Room Code:</strong> ${data.data.roomCode}</p>
<p><strong>Room ID:</strong> ${data.data.roomId}</p>
<p><strong>Max Participants:</strong> ${data.data.maxParticipants}</p>
<p><strong>Status:</strong> ${data.data.status}</p>
</div>
`;
elements.roomInfo.classList.remove('hidden');
showStatus(`Room created! Code: ${data.data.roomCode}`, 'success');
} catch (error) {
showStatus(`Failed to create room: ${error.message}`, 'error');
}
}
async function joinRoom() {
try {
if (!elements.roomCode.value || !elements.displayName.value) {
showStatus('Please enter room code and display name', 'error');
return;
}
showStatus('Joining room...', 'info');
const response = await fetch(`${elements.apiUrl.value}/api/connections/join-room`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
roomCode: elements.roomCode.value,
userId: `js-user-${Date.now()}`,
displayName: elements.displayName.value
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || errorData.message || `HTTP ${response.status}`);
}
const { data } = await response.json();
// Store the session ID for later use
currentSessionId = data.sessionId;
console.log('Join room successful:', {
sessionId: data.sessionId,
connectionId: data.connectionId,
roomCode: data.roomCode,
displayName: data.displayName
});
// Initialize OpenVidu session
const OV = new OpenVidu();
session = OV.initSession();
// Event handlers
session.on('streamCreated', (event) => {
const subscriber = session.subscribe(event.stream, undefined);
subscribers.push(subscriber);
// Safely parse stream data with error handling
let streamData = {};
try {
const connectionData = event.stream.connection.data;
if (connectionData && connectionData.trim()) {
streamData = JSON.parse(connectionData);
}
} catch (error) {
console.warn('Failed to parse connection data:', event.stream.connection.data, error);
// Fallback to empty object
streamData = {};
}
const isScreenShare = event.stream.typeOfVideo === 'SCREEN';
const displayName = streamData.displayName || 'Participant';
const label = isScreenShare ? `${displayName} (Screen)` : displayName;
addVideoElement(subscriber, label);
updateParticipantCount();
showStatus(`${isScreenShare ? 'Screen share' : 'Participant'} joined`, 'success');
});
session.on('streamDestroyed', (event) => {
subscribers = subscribers.filter(sub => sub.stream.streamId !== event.stream.streamId);
removeVideoElement(event.stream.streamId);
updateParticipantCount();
showStatus(`Participant left`, 'info');
});
// Connect to session
await session.connect(data.token, {
clientData: JSON.stringify({
userId: data.userId || `js-user-${Date.now()}`,
displayName: elements.displayName.value
})
});
// Create and publish local stream
publisher = await OV.initPublisherAsync(undefined, {
audioSource: undefined,
videoSource: undefined,
publishAudio: true,
publishVideo: true,
resolution: '640x480',
frameRate: 30,
insertMode: 'APPEND',
mirror: false
});
await session.publish(publisher);
addVideoElement(publisher, 'You (Local)');
// Update UI
elements.createRoom.disabled = true;
elements.joinRoom.disabled = true;
elements.leaveRoom.classList.remove('hidden');
elements.callControls.classList.remove('hidden');
elements.videoContainer.classList.remove('hidden');
updateParticipantCount();
showStatus(`Successfully joined room: ${elements.roomCode.value}`, 'success');
} catch (error) {
showStatus(`Failed to join room: ${error.message}`, 'error');
// Provide helpful hints for common errors
if (error.message.includes('Room not found')) {
showStatus('Room not found. Make sure the room code is correct and the room has been created.', 'error');
} else if (error.message.includes('inactive')) {
showStatus('Room is not active. The room may have ended or not started yet.', 'error');
}
console.error('Join room error:', error);
}
}
function addVideoElement(streamManager, label) {
const containerElement = document.createElement('div');
containerElement.className = 'video-element';
containerElement.id = `video-${streamManager.stream.streamId}`;
// Add class for screen share
if (streamManager.stream.typeOfVideo === 'SCREEN') {
containerElement.classList.add('screen-share');
}
// Create the actual video element that OpenVidu will use
const videoElement = document.createElement('video');
videoElement.autoplay = true;
videoElement.controls = false;
videoElement.muted = false;
videoElement.playsInline = true;
videoElement.style.width = '100%';
videoElement.style.height = '100%';
videoElement.style.objectFit = 'cover';
const labelElement = document.createElement('div');
labelElement.className = 'video-label';
labelElement.textContent = label;
// Add stream info indicators
const streamInfo = document.createElement('div');
streamInfo.className = 'stream-info';
const videoIndicator = document.createElement('span');
videoIndicator.className = `stream-indicator ${streamManager.stream.videoActive ? 'video-on' : 'video-off'}`;
videoIndicator.title = streamManager.stream.videoActive ? 'Video On' : 'Video Off';
const audioIndicator = document.createElement('span');
audioIndicator.className = `stream-indicator ${streamManager.stream.audioActive ? 'audio-on' : 'audio-off'}`;
audioIndicator.title = streamManager.stream.audioActive ? 'Audio On' : 'Audio Off';
streamInfo.appendChild(videoIndicator);
streamInfo.appendChild(audioIndicator);
// Assemble the container
containerElement.appendChild(videoElement);
containerElement.appendChild(labelElement);
containerElement.appendChild(streamInfo);
elements.videoGrid.appendChild(containerElement);
// Let OpenVidu attach the stream to our video element
streamManager.addVideoElement(videoElement);
console.log('Video container created for:', label, {
streamId: streamManager.stream.streamId,
videoElement: videoElement,
hasAudio: streamManager.stream.hasAudio,
audioActive: streamManager.stream.audioActive
});
// Handle video element events
videoElement.addEventListener('loadedmetadata', () => {
console.log('Video metadata loaded for:', label);
videoElement.play().then(() => {
console.log('Video/audio playing successfully for:', label);
showStatus(`Media playing for ${label}`, 'success');
}).catch((error) => {
console.warn('Autoplay prevented for', label, error);
showStatus(`Click on ${label}'s video to enable audio`, 'info');
// Add click handler to enable audio
const enableAudio = () => {
videoElement.play();
videoElement.removeEventListener('click', enableAudio);
showStatus(`Audio enabled for ${label}`, 'success');
};
videoElement.addEventListener('click', enableAudio);
});
});
videoElement.addEventListener('playing', () => {
console.log('Video started playing for:', label);
});
videoElement.addEventListener('error', (error) => {
console.error('Video error for', label, error);
});
// Listen for stream property changes
streamManager.on('streamPropertyChanged', (event) => {
console.log('Stream property changed:', event.changedProperty, event.newValue, 'for', label);
if (event.changedProperty === 'videoActive') {
videoIndicator.className = `stream-indicator ${event.newValue ? 'video-on' : 'video-off'}`;
videoIndicator.title = event.newValue ? 'Video On' : 'Video Off';
} else if (event.changedProperty === 'audioActive') {
audioIndicator.className = `stream-indicator ${event.newValue ? 'audio-on' : 'audio-off'}`;
audioIndicator.title = event.newValue ? 'Audio On' : 'Audio Off';
}
});
}
function removeVideoElement(streamId) {
const videoElement = document.getElementById(`video-${streamId}`);
if (videoElement) {
videoElement.remove();
}
}
function leaveRoom() {
if (session) {
session.disconnect();
session = null;
publisher = null;
screenPublisher = null;
subscribers = [];
currentRoom = null;
currentSessionId = null;
recordingId = null;
// Clear video grid
elements.videoGrid.innerHTML = '';
// Update UI
elements.createRoom.disabled = false;
elements.joinRoom.disabled = false;
elements.leaveRoom.classList.add('hidden');
elements.callControls.classList.add('hidden');
elements.videoContainer.classList.add('hidden');
elements.stopRecording.classList.add('hidden');
elements.startRecording.classList.remove('hidden');
elements.stopScreenShare.classList.add('hidden');
elements.shareScreen.classList.remove('hidden');
showStatus('Left the room', 'success');
}
}
function toggleVideo() {
if (publisher) {
const isVideoActive = publisher.stream.videoActive;
publisher.publishVideo(!isVideoActive);
elements.toggleVideo.textContent = isVideoActive ? 'đš Turn On Video' : 'đš Turn Off Video';
showStatus(`Video ${isVideoActive ? 'disabled' : 'enabled'}`, 'info');
}
}
function toggleAudio() {
if (publisher) {
const isAudioActive = publisher.stream.audioActive;
publisher.publishAudio(!isAudioActive);
elements.toggleAudio.textContent = isAudioActive ? 'đ¤ Unmute' : 'đ¤ Mute';
showStatus(`Audio ${isAudioActive ? 'muted' : 'unmuted'}`, 'info');
}
}
async function shareScreen() {
try {
if (!session) {
showStatus('No active session to share screen', 'error');
return;
}
showStatus('Starting screen share...', 'info');
// Option 1: Replace camera with screen share
// Unpublish camera stream first
if (publisher) {
await session.unpublish(publisher);
removeVideoElement(publisher.stream.streamId);
console.log('Unpublished camera stream for screen share');
}
const OV = new OpenVidu();
screenPublisher = await OV.initPublisherAsync(undefined, {
videoSource: 'screen',
publishAudio: true, // Include system audio if available
publishVideo: true,
mirror: false
});
await session.publish(screenPublisher);
addVideoElement(screenPublisher, 'You (Screen Share)');
// Update UI
elements.shareScreen.classList.add('hidden');
elements.stopScreenShare.classList.remove('hidden');
elements.toggleVideo.disabled = true; // Disable camera toggle during screen share
showStatus('Screen sharing started! (Camera paused)', 'success');
// Handle screen share end (when user stops via browser UI)
screenPublisher.on('accessDenied', () => {
showStatus('Screen share access denied', 'error');
stopScreenShare();
});
screenPublisher.on('streamDestroyed', () => {
console.log('Screen share stream destroyed');
stopScreenShare();
});
} catch (error) {
showStatus(`Failed to start screen share: ${error.message}`, 'error');
console.error('Screen share error:', error);
// If screen share fails, restore camera
if (publisher && !session.streamManagers.includes(publisher)) {
try {
await session.publish(publisher);
addVideoElement(publisher, 'You (Local)');
console.log('Restored camera stream after screen share failure');
} catch (restoreError) {
console.error('Failed to restore camera:', restoreError);
}
}
}
}
async function stopScreenShare() {
if (screenPublisher) {
try {
await session.unpublish(screenPublisher);
removeVideoElement(screenPublisher.stream.streamId);
screenPublisher = null;
// Restore camera stream
if (publisher) {
await session.publish(publisher);
addVideoElement(publisher, 'You (Local)');
console.log('Restored camera stream after stopping screen share');
}
// Update UI
elements.shareScreen.classList.remove('hidden');
elements.stopScreenShare.classList.add('hidden');
elements.toggleVideo.disabled = false; // Re-enable camera toggle
showStatus('Screen sharing stopped, camera restored', 'info');
} catch (error) {
console.error('Error stopping screen share:', error);
showStatus('Error stopping screen share', 'error');
}
}
}
function enableAllAudio() {
// Enable audio for all video elements
const videoElements = document.querySelectorAll('video');
let enabledCount = 0;
console.log('Found video elements:', videoElements.length);
videoElements.forEach((video, index) => {
console.log(`Video ${index}:`, {
src: video.src,
srcObject: video.srcObject,
muted: video.muted,
volume: video.volume,
paused: video.paused,
readyState: video.readyState
});
video.muted = false;
video.volume = 1.0;
// Try to play if paused or not playing
if (video.paused || video.readyState === 0) {
video.play().then(() => {
enabledCount++;
console.log(`Audio enabled for video ${index}`);
}).catch(error => {
console.warn(`Could not enable audio for video ${index}:`, error);
});
} else {
enabledCount++;
console.log(`Video ${index} already playing`);
}
});
showStatus(`Audio enabled for ${enabledCount}/${videoElements.length} video streams`, 'success');
console.log('Audio status check:', {
totalVideos: videoElements.length,
enabledCount: enabledCount,
subscribers: subscribers.length,
publisher: publisher ? 'exists' : 'none'
});
}
function testAudio() {
// Create a test audio beep
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.value = 800;
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.5);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.5);
showStatus('Audio test beep played - if you hear it, your audio is working', 'info');
// Also log current audio states
console.log('Audio debug info:', {
publisher: publisher ? {
audioActive: publisher.stream.audioActive,
hasAudio: publisher.stream.hasAudio
} : 'none',
subscribers: subscribers.map(sub => ({
audioActive: sub.stream.audioActive,
hasAudio: sub.stream.hasAudio,
connectionData: sub.stream.connection.data
})),
videoElements: Array.from(document.querySelectorAll('video')).map(v => ({
muted: v.muted,
volume: v.volume,
paused: v.paused
}))
});
}
async function startRecording() {
try {
if (!session || !currentSessionId) {
showStatus('No active session to record', 'error');
return;
}
showStatus('Starting recording...', 'info');
const token = await getApiToken();
const response = await fetch(`${elements.apiUrl.value}/api/recordings/start`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionId: currentSessionId,
name: `vanilla-js-recording-${Date.now()}`,
outputMode: 'COMPOSED',
recordingLayout: 'BEST_FIT'
})
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || errorData.message || `HTTP ${response.status}`);
}
const data = await response.json();
recordingId = data.data.recordingId;
elements.startRecording.classList.add('hidden');
elements.stopRecording.classList.remove('hidden');
showStatus(`Recording started! ID: ${recordingId}`, 'success');
} catch (error) {
showStatus(`Failed to start recording: ${error.message}`, 'error');
}
}
async function stopRecording() {
try {
if (!recordingId) {
showStatus('No active recording to stop', 'error');
return;
}
showStatus('Stopping recording...', 'info');
const token = await getApiToken();
const response = await fetch(`${elements.apiUrl.value}/api/recordings/stop/${recordingId}`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || errorData.message || `HTTP ${response.status}`);
}
const data = await response.json();
elements.startRecording.classList.remove('hidden');
elements.stopRecording.classList.add('hidden');
recordingId = null;
showStatus(`Recording stopped! Duration: ${data.data.duration}s`, 'success');
} catch (error) {
showStatus(`Failed to stop recording: ${error.message}`, 'error');
}
}
// Event listeners
elements.testConnection.addEventListener('click', testApiConnection);
elements.createRoom.addEventListener('click', createRoom);
elements.joinRoom.addEventListener('click', joinRoom);
elements.leaveRoom.addEventListener('click', leaveRoom);
elements.toggleVideo.addEventListener('click', toggleVideo);
elements.toggleAudio.addEventListener('click', toggleAudio);
elements.shareScreen.addEventListener('click', shareScreen);
elements.stopScreenShare.addEventListener('click', stopScreenShare);
elements.enableAllAudio.addEventListener('click', enableAllAudio);
elements.testAudio.addEventListener('click', testAudio);
elements.startRecording.addEventListener('click', startRecording);
elements.stopRecording.addEventListener('click', stopRecording);
// Initialize
document.addEventListener('DOMContentLoaded', () => {
testApiConnection();
});
</script>
</body>
</html>