<div class="video-conference-container">
<ng-container *ngIf="!isConnected">
<div class="join-room">
<h2>Video Conference</h2>
<ng-container *ngIf="!roomId">
<select [(ngModel)]="meetingType">
<option value="instant">Instant Meeting</option>
<option value="scheduled">Scheduled Meeting</option>
</select>
<input [(ngModel)]="roomId" placeholder="Enter Room ID" *ngIf="meetingType === 'instant'" />
<input [(ngModel)]="scheduledMeetingId" placeholder="Enter Scheduled Meeting ID" *ngIf="meetingType === 'scheduled'" />
<input [(ngModel)]="userName" placeholder="Enter Your Name" *ngIf="!isAuthenticated" />
<button (click)="joinRoom(false)" [disabled]="!canJoin()">Join Meeting</button>
<button (click)="createNewMeeting()" *ngIf="meetingType === 'instant'">Create New Instant Meeting</button>
<button (click)="scheduleMeeting()" *ngIf="meetingType === 'scheduled'">Schedule New Meeting</button>
</ng-container>
<ng-container *ngIf="roomId">
<p>Room ID: {{ roomId }}</p>
<input [(ngModel)]="userName" placeholder="Enter Your Name" *ngIf="!isAuthenticated" />
<button (click)="joinRoom(false)" [disabled]="!userName">Join Meeting</button>
</ng-container>
</div>
</ng-container>
<!-- <ng-container *ngIf="!isConnected">
<app-room-creation (roomJoined)="onRoomJoined($event)"></app-room-creation>
</ng-container> -->
<ng-container *ngIf="isConnected">
<div class="meeting-room" [class.fullscreen]="isFullscreen">
<ng-container >
<div class="video-grid">
<div class="video-container local-video"
[class.whiteboard-active]="localWhiteboardActive"
[class.screen-sharing]="isScreenSharing"
(click)="focusParticipant({
id: 'local',
stream: localStream!,
name: userName,
isWhiteboardActive: localWhiteboardActive,
isScreenSharing: isScreenSharing,
hasScreenSharePermission: hasScreenSharePermission,
hasWhiteboardPermission: hasWhiteboardPermission,
isAudioMuted: isAudioMuted,
isVideoOff: isVideoOff,
showCameraWithScreenShare: false,
isHandRaised: false,
isHost: isHost
})">
<ng-container *ngIf="!localWhiteboardActive && !isScreenSharing">
<video #localVideo autoplay muted [class.video-off]="isVideoOff"></video>
</ng-container>
<ng-container *ngIf="isScreenSharing">
<video #localVideo autoplay muted [srcObject]="screenStream"></video>
<div class="screen-share-indicator">Screen sharing active</div>
</ng-container>
<ng-container *ngIf="localWhiteboardActive">
<app-whiteboard [socket]="socket" [roomId]="roomId"></app-whiteboard>
</ng-container>
<div class="participant-name">
You ({{ userName }})
<ng-container *ngIf="isHandRaised">
<i class="material-icons hand-raised-icon">pan_tool</i>
</ng-container>
<ng-container *ngIf="isHost">(Host)</ng-container>
<ng-container *ngIf="isScreenSharing">(Screen)</ng-container>
<ng-container *ngIf="localWhiteboardActive">(Whiteboard)</ng-container>
</div>
<div class="audio-indicator" [class.muted]="isAudioMuted">
<i class="material-icons">{{ isAudioMuted ? 'mic_off' : 'mic' }}</i>
</div>
</div>
<ng-container *ngFor="let participant of participants; trackBy: trackByFn">
<div class="video-container remote-video"
[class.whiteboard-active]="participant.isWhiteboardActive"
[class.screen-sharing]="participant.isScreenSharing"
(click)="focusParticipant(participant)">
<ng-container *ngIf="!participant.isWhiteboardActive">
<video [srcObject]="participant.stream" autoplay [class.video-off]="participant.isVideoOff"></video>
</ng-container>
<ng-container *ngIf="participant.isWhiteboardActive">
<app-whiteboard [socket]="socket" [roomId]="roomId"></app-whiteboard>
</ng-container>
<ng-container *ngIf="participant.isScreenSharing && isHost && participant.showCameraWithScreenShare">
<div class="camera-overlay">
<video [srcObject]="participant.cameraStream" autoplay></video>
</div>
</ng-container>
<div class="participant-name">
{{ participant.name }}
<ng-container *ngIf="participant.isHandRaised">
<i class="material-icons hand-raised-icon">pan_tool</i>
</ng-container>
<ng-container *ngIf="participant.isScreenSharing">(Screen)</ng-container>
<ng-container *ngIf="participant.isWhiteboardActive">(Whiteboard)</ng-container>
</div>
<div class="audio-indicator" [class.muted]="participant.isAudioMuted">
<i class="material-icons">{{ participant.isAudioMuted ? 'mic_off' : 'mic' }}</i>
</div>
</div>
</ng-container>
</div>
</ng-container>
<ng-template #focusedView>
<div class="focused-view">
<div class="main-video">
<ng-container *ngIf="focusedParticipant">
<ng-container *ngIf="focusedParticipant.isWhiteboardActive">
<app-whiteboard [socket]="socket" [roomId]="roomId"></app-whiteboard>
</ng-container>
<ng-container *ngIf="!focusedParticipant.isWhiteboardActive">
<video [srcObject]="focusedParticipant.stream" autoplay [muted]="focusedParticipant.id === 'local'" [class.video-off]="focusedParticipant.isVideoOff"></video>
</ng-container>
<div class="participant-name">
{{ focusedParticipant.name }}
<ng-container *ngIf="focusedParticipant.isHost">(Host)</ng-container>
<ng-container *ngIf="focusedParticipant.isScreenSharing">(Screen)</ng-container>
<ng-container *ngIf="focusedParticipant.isWhiteboardActive">(Whiteboard)</ng-container>
</div>
<div class="audio-indicator" [class.muted]="focusedParticipant.isAudioMuted">
<i class="material-icons">{{ focusedParticipant.isAudioMuted ? 'mic_off' : 'mic' }}</i>
</div>
</ng-container>
</div>
<div class="thumbnail-strip">
<ng-container *ngIf="focusedParticipant?.id !== 'local' && localStream">
<div class="video-thumbnail local-video"
(click)="focusParticipant({
id: 'local',
stream: localStream,
name: userName,
isWhiteboardActive: localWhiteboardActive,
isScreenSharing: isScreenSharing,
hasScreenSharePermission: true,
hasWhiteboardPermission: true,
isAudioMuted: isAudioMuted,
isVideoOff: isVideoOff,
showCameraWithScreenShare: false,
isHandRaised: false,
isHost: isHost
})">
<video #localVideo autoplay muted [class.video-off]="isVideoOff"></video>
<div class="participant-name">You ({{ userName }})</div>
</div>
</ng-container>
<ng-container *ngFor="let participant of participants; trackBy: trackByFn">
<ng-container *ngIf="participant.id !== focusedParticipant?.id">
<div class="video-container remote-video"
[class.whiteboard-active]="participant.isWhiteboardActive"
[class.screen-sharing]="participant.isScreenSharing"
(click)="focusParticipant(participant)">
<div class="participant-name">{{ participant.name }}
<ng-container *ngIf="participant.isHost">(Host)</ng-container></div>
</div>
</ng-container>
</ng-container>
</div>
</div>
</ng-template>
<div class="controls-bar">
<div class="left-controls">
<button (click)="toggleMeetingDetails()" class="control-btn">
<i class="material-icons">info</i>
</button>
</div>
<div class="center-controls">
<button (click)="toggleAudio()" [class.active]="!isAudioMuted" class="control-btn">
<i class="material-icons">{{ isAudioMuted ? 'mic_off' : 'mic' }}</i>
</button>
<button (click)="toggleVideo()" [class.active]="!isVideoOff" class="control-btn">
<i class="material-icons">{{ isVideoOff ? 'videocam_off' : 'videocam' }}</i>
</button>
<button (click)="toggleRecording()" [class.active]="isRecording" class="control-btn">
<i class="material-icons">{{ isRecording ? 'stop' : 'fiber_manual_record' }}</i>
</button>
<button (click)="toggleScreenSharing()" [disabled]="!hasScreenSharePermission" [class.active]="isScreenSharing" class="control-btn">
<i class="material-icons">{{ isScreenSharing ? 'stop_screen_share' : 'present_to_all' }}</i>
</button>
<button (click)="toggleWhiteboard()" [disabled]="!hasWhiteboardPermission" [class.active]="localWhiteboardActive" class="control-btn">
<i class="material-icons">dashboard</i>
</button>
<!-- <button (click)="raiseHand()" class="control-btn">
<i class="material-icons">pan_tool</i>
</button> -->
<button (click)="leaveRoom()" class="leave-btn">
<i class="material-icons">call_end</i>
</button>
<button (click)="toggleFullscreen()" class="control-btn">
<i class="material-icons">{{ isFullscreen ? 'fullscreen_exit' : 'fullscreen' }}</i>
</button>
</div>
<div class="right-controls">
<button (click)="toggleChat()" [class.active]="showChat" class="control-btn">
<i class="material-icons">chat</i>
<span class="notification-badge" *ngIf="unreadMessagesCount > 0">
{{ unreadMessagesCount }}
</span>
</button>
<button (click)="toggleParticipantsList()" [class.active]="showParticipants" class="control-btn">
<i class="material-icons">people</i>
</button>
<button (click)="toggleMoreOptions()" class="control-btn">
<i class="material-icons">more_vert</i>
</button>
<button (click)="toggleHandRaise()" class="control-btn">
<i class="material-icons">{{ isHandRaised ? 'pan_tool' : 'pan_tool_outlined' }}</i>
</button>
</div>
</div>
<div class="side-panel" [class.show-panel]="showChat || showParticipants || showMeetingDetails">
<ng-container *ngIf="showChat">
<div class="chat-container">
<div class="panel-header">
<h3>Meeting chat</h3>
<button (click)="toggleChat()" class="close-panel">
<i class="material-icons">close</i>
</button>
</div>
<div class="chat-messages" #chatMessages>
<div class="chat-messages" #chatMessages>
<ng-container *ngFor="let message of messages; trackBy: trackByFn">
<div class="message" [class.own-message]="message.sender === userName" [class.private-message]="message.isPrivate">
<div class="message-sender">{{ message.sender }} {{ message.isPrivate ? '(Private)' : '' }}</div>
<div class="message-content">
{{ message.content }}
<ng-container *ngIf="message.sender === userName">
<span class="message-status">
<i class="material-icons">check</i>
</span>
</ng-container>
</div>
<div class="message-time">{{ message.time | date:'shortTime' }}</div>
</div>
</ng-container>
</div>
<div class="chat-input">
<select [(ngModel)]="chatRecipient">
<option value="everyone">Everyone</option>
<option value="host" *ngIf="!isHost">Host</option>
<ng-container *ngIf="isHost">
<option *ngFor="let participant of participants" [value]="participant.id">{{ participant.name }}</option>
</ng-container>
</select>
<input
[(ngModel)]="newMessage"
placeholder="Send a message"
(keyup.enter)="sendMessage()"
/>
<button (click)="sendMessage()" [disabled]="!newMessage.trim()">
<i class="material-icons">send</i>
</button>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="showParticipants">
<div class="participants-list">
<div class="panel-header">
<h3>Participants ({{ participants.length + 1 }})</h3>
<button (click)="toggleParticipantsList()" class="close-panel">
<i class="material-icons">close</i>
</button>
</div>
<div class="participant-item host">
<span>You ({{ userName }}) {{ isHost ? '(Host)' : '' }}</span>
<i class="material-icons">person</i>
</div>
<ng-container *ngFor="let participant of participants; trackBy: trackByFn">
<div class="participant-item">
<span>
{{ participant.name }}
<ng-container *ngIf="participant.isHost">(Host)</ng-container>
<ng-container *ngIf="participant.isHandRaised">
<i class="material-icons hand-raised-icon">pan_tool</i>
</ng-container>
</span>
<i class="material-icons">person</i>
<ng-container *ngIf="isHost">
<div class="participant-controls">
<button (click)="updateParticipantPermission(participant.id, 'screenShare', !participant.hasScreenSharePermission)">
{{ participant.hasScreenSharePermission ? 'Revoke Screen Share' : 'Allow Screen Share' }}
</button>
<button (click)="updateParticipantPermission(participant.id, 'whiteboard', !participant.hasWhiteboardPermission)">
{{ participant.hasWhiteboardPermission ? 'Revoke Whiteboard' : 'Allow Whiteboard' }}
</button>
<button (click)="toggleCameraWithScreenShare(participant.id)">
{{ participant.showCameraWithScreenShare ? 'Hide Camera' : 'Show Camera' }}
</button>
<div class="participant-controls">
<button (click)="changeHost(participant.id)">Make Host</button>
<button (click)="kickParticipant(participant.id)">Remove</button>
</div>
</div>
</ng-container>
</div>
</ng-container>
</div>
</ng-container>
<ng-container *ngIf="showMeetingDetails">
<div class="meeting-details">
<div class="panel-header">
<h3>Meeting Details</h3>
<button (click)="toggleMeetingDetails()" class="close-panel">
<i class="material-icons">close</i>
</button>
</div>
<div class="details-content">
<p><strong>Room ID:</strong> {{ roomId }}</p>
<p><strong>Meeting Link:</strong> {{ meetingLink }}</p>
<button (click)="copyMeetingLink()">Copy Link</button>
</div>
</div>
</ng-container>
</div>
</div>
</ng-container>
</div>
import { Component, OnInit, OnDestroy, ViewChild, ElementRef, ChangeDetectorRef, Inject, PLATFORM_ID, inject, ApplicationRef } from '@angular/core';
import { CommonModule, isPlatformBrowser } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { Socket, io } from 'socket.io-client';
import { WhiteboardComponent } from '../whiteboard/whiteboard.component';
import { first } from 'rxjs';
import { environment } from '../../../environments/environment';
import { AuthService } from '../../auth/Auth.Service';
interface Participant {
id: any;
stream: MediaStream;
cameraStream?: MediaStream;
name: string;
isWhiteboardActive: boolean;
isScreenSharing: boolean;
hasScreenSharePermission: boolean;
hasWhiteboardPermission: boolean;
isAudioMuted: boolean;
isVideoOff: boolean;
isHandRaised: boolean;
showCameraWithScreenShare: boolean;
isHost: boolean;
}
interface Message {
id: string;
sender: string;
content: string;
time: Date;
isPrivate: boolean;
recipient?: string;
}
@Component({
selector: 'app-video-conference-page',
standalone: true,
imports: [CommonModule, FormsModule, WhiteboardComponent],
templateUrl: './video-conference-page.component.html',
styleUrl: './video-conference-page.component.scss'
})
export class VideoConferenceComponent implements OnInit, OnDestroy {
@ViewChild('localVideo') localVideo!: ElementRef<HTMLVideoElement>;
@ViewChild('chatMessages') chatMessages!: ElementRef;
public socket: Socket;
private peerConnections: Map<string, RTCPeerConnection> = new Map();
localStream: MediaStream | null = null;
screenStream: MediaStream | null = null;
participants: Participant[] = [];
messages: Message[] = [];
newMessage = '';
roomId: string = '';
userName: string = '';
meetingLink: string = '';
isConnected: boolean = false;
isHost: boolean = false;
isScreenSharing: boolean = false;
localWhiteboardActive: boolean = false;
isRecording: boolean = false;
isAudioMuted: boolean = false;
isVideoOff: boolean = false;
showChat: boolean = false;
showParticipants: boolean = false;
showMeetingDetails: boolean = false;
isGridView: boolean = true;
isFullscreen: boolean = false;
focusedParticipant: Participant | null = null;
hasScreenSharePermission: boolean = false;
hasWhiteboardPermission: boolean = false;
activeView: 'camera' | 'screen' | 'whiteboard' = 'camera';
isHandRaised: boolean = false;
private mediaRecorder: MediaRecorder | null = null;
private recordedChunks: Blob[] = [];
chatRecipient: string = 'everyone';
userId:any
meetingType: 'instant' | 'scheduled' = 'instant';
scheduledMeetingId: string = '';
isAuthenticated: boolean = false;
private hostId: string = '';
unreadMessagesCount: number = 0;
private messageSound: HTMLAudioElement;
isScreenSharingAllowed: boolean = true;
isWhiteboardAllowed: boolean = true;
private userNames: { [key: string]: string } = {};
constructor(
private http: HttpClient,
private route: ActivatedRoute,
private router: Router,
private cdr: ChangeDetectorRef,
private authService: AuthService,
@Inject(PLATFORM_ID) private platformId: Object
) {
// this.socket = io('http://localhost:3000', { autoConnect: false });
this.socket = io(environment.apiUrl, { autoConnect: false });
inject(ApplicationRef).isStable.pipe(
first((isStable) => isStable))
.subscribe(() => { this.socket.connect() });
this.messageSound = new Audio('assets/sounds/message-notification.mp3');
}
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
this.checkAuthentication();
}
}
private checkAuthentication() {
this.authService.isLoggedIn().subscribe(
isLoggedIn => {
this.isAuthenticated = isLoggedIn;
if (isLoggedIn) {
const user = this.authService.currentUserValue;
if (user) {
this.userName = user.user.name;
this.userId = user.user.id;
}
this.initializeBrowserFeatures();
} else {
this.router.navigate(['/login'], { queryParams: { returnUrl: this.router.url } });
}
}
);
}
private initializeBrowserFeatures() {
this.setupSocketListeners();
this.route.params.subscribe(params => {
if (params['roomId']) {
this.roomId = params['roomId'];
this.joinRoom(false);
}
});
}
canJoin(): boolean {
if (this.meetingType === 'instant') {
return !!this.roomId && !!this.userName;
} else {
return !!this.scheduledMeetingId && !!this.userName;
}
}
createNewMeeting() {
this.generateRoomId();
this.joinRoom(true);
}
scheduleMeeting() {
// Implement logic to schedule a meeting
// This might involve sending a request to your backend to create a scheduled meeting
this.http.post<{ meetingId: string }>(`${environment.apiUrls}/meeting/schedule-meeting`, {
hostId: this.userId,
scheduledTime: new Date() // You might want to add a date picker in the UI
}).subscribe(
response => {
this.scheduledMeetingId = response.meetingId;
alert(`Meeting scheduled. ID: ${this.scheduledMeetingId}`);
},
error => {
console.error('Error scheduling meeting:', error);
alert('Failed to schedule meeting. Please try again.');
}
);
}
ngOnDestroy() {
this.leaveRoom();
if (this.socket) {
this.socket.disconnect();
}
}
private setupSocketListeners() {
debugger
this.socket.on('room-state', this.handleRoomState.bind(this));
this.socket.on('user-connected', this.handleUserConnected.bind(this));
this.socket.on('user-disconnected', this.handleUserDisconnected.bind(this));
this.socket.on('all-users', this.handleAllUsers.bind(this));
this.socket.on('receive-message', this.handleReceiveMessage.bind(this));
this.socket.on('offer', this.handleOffer.bind(this));
this.socket.on('answer', this.handleAnswer.bind(this));
this.socket.on('ice-candidate', this.handleIceCandidate.bind(this));
this.socket.on('screen-share-started', this.handleScreenShareStarted.bind(this));
this.socket.on('screen-share-stopped', this.handleScreenShareStopped.bind(this));
this.socket.on('whiteboard-toggle', this.handleWhiteboardToggle.bind(this));
this.socket.on('permission-updated', this.handlePermissionUpdated.bind(this));
this.socket.on('host-changed', this.handleHostChanged.bind(this));
this.socket.on('private-message', this.handlePrivateMessage.bind(this));
this.socket.on('camera-toggle', this.handleCameraToggle.bind(this));
this.socket.on('video-toggle', this.handleVideoToggle.bind(this));
this.socket.on('user-raised-hand', this.handleHandRaised.bind(this));
this.socket.on('user-lowered-hand', this.handleHandLowered.bind(this));
this.socket.on('host-assigned', ({ hostId }) => {
this.hostId = hostId;
this.isHost = this.socket.id === hostId;
this.participants.forEach(p => p.isHost = p.id === hostId);
this.cdr.detectChanges();
});
this.socket.on('participant-kicked', ({ participantId }) => {
if (participantId === this.socket.id) {
alert('You have been removed from the meeting by the host.');
this.leaveRoom();
} else {
this.removeParticipant(participantId);
}
});
this.socket.on('host-changed', ({ newHostId, newHostName }) => {
this.hostId = newHostId;
this.isHost = this.socket.id === newHostId;
this.participants.forEach(p => p.isHost = p.id === newHostId);
// Update the participant's name to show (Host)
const newHost = this.participants.find(p => p.id === newHostId);
if (newHost) {
newHost.name = `${newHost.name} (Host)`;
}
this.cdr.detectChanges();
});
this.socket.on('host-media-change', ({ mediaType, isActive }) => {
if (mediaType === 'screen') {
this.handleHostScreenShare(isActive);
} else if (mediaType === 'video') {
this.handleHostVideoChange(isActive);
}
});
this.socket.on('screen-share-denied', () => {
alert('Screen sharing is currently in use by another participant.');
});
this.socket.on('whiteboard-denied', () => {
alert('Whiteboard is currently in use by another participant.');
});
}
changeHost(newHostId: string) {
if (this.isHost) {
this.socket.emit('change-host', { roomId: this.roomId, newHostId });
}
}
kickParticipant(participantId: string) {
if (this.isHost) {
this.socket.emit('kick-participant', { roomId: this.roomId, participantId });
}
}
private handleHostScreenShare(isActive: boolean) {
const hostParticipant = this.participants.find(p => p.id === this.hostId);
if (hostParticipant) {
hostParticipant.isScreenSharing = isActive;
if (isActive) {
this.focusedParticipant = hostParticipant;
this.isGridView = false;
} else {
this.focusedParticipant = null;
this.isGridView = true;
}
this.cdr.detectChanges();
}
}
private handleHostVideoChange(isActive: boolean) {
const hostParticipant = this.participants.find(p => p.id === this.hostId);
if (hostParticipant) {
hostParticipant.isVideoOff = !isActive;
this.cdr.detectChanges();
}
}
private handleRoomState({ participants, hostId }: { participants: any[]; hostId: string }) {
this.hostId = hostId; // Store the host's socket ID
participants.forEach(participant => {
if (participant.id !== this.socket.id) {
this.connectToNewUser(participant.id, participant.userName, participant.isHost);
}
});
this.cdr.detectChanges();
}
private handleUserConnected({ userId, userName, isHost }: { userId: string; userName: string; isHost: boolean }) {
if (!userId || userId === this.socket.id) {
console.error('No userId received!');
return; // Early exit if no userId is received
}
// Add the new participant to the list
this.participants.push({
id: userId,
stream: new MediaStream(),
name: userName,
isWhiteboardActive: false,
isScreenSharing: false,
hasScreenSharePermission: true,
hasWhiteboardPermission: isHost,
isAudioMuted: false,
isVideoOff: false,
showCameraWithScreenShare: false,
isHandRaised: false,
isHost: false,
});
this.cdr.detectChanges();
}
private handleUserDisconnected(userId: string) {
this.removeParticipant(userId);
this.cdr.detectChanges();
}
private handleAllUsers(users: { id: string; name: string; isHost: boolean }[]) {
users.forEach(user => this.connectToNewUser(user.id, user.name, user.isHost));
this.cdr.detectChanges();
}
private handleVideoToggle({ userId, isVideoOff }: { userId: string; isVideoOff: boolean }) {
const participant = this.participants.find(p => p.id === userId);
if (participant) {
participant.isVideoOff = isVideoOff;
this.cdr.detectChanges();
}
}
private handleReceiveMessage(message: Message) {
if (!this.messages.some(m => m.id === message.id)) {
this.messages.push(message);
this.cdr.detectChanges();
this.scrollToBottom();
// Increment unread messages count if chat is not open
if (!this.showChat) {
this.unreadMessagesCount++;
this.messageSound.play();
}
}
}
private async handleOffer({ from, offer }: { from: string; offer: RTCSessionDescriptionInit }) {
const pc = this.createPeerConnection(from);
await pc.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
this.socket.emit('answer', { to: from, answer });
}
private async handleAnswer({ from, answer }: { from: string; answer: RTCSessionDescriptionInit }) {
const pc = this.peerConnections.get(from);
if (pc) {
await pc.setRemoteDescription(new RTCSessionDescription(answer));
}
}
private async handleIceCandidate({ from, candidate }: { from: string; candidate: RTCIceCandidateInit }) {
const pc = this.peerConnections.get(from);
if (pc) {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
}
}
private handleScreenShareStarted({ userId, cameraTrack }: { userId: string; cameraTrack?: MediaStreamTrack }) {
this.isScreenSharingAllowed = false;
const participant = this.participants.find(p => p.id === userId);
if (participant) {
participant.isScreenSharing = true;
// Only create a new MediaStream if cameraTrack is valid
if (cameraTrack && cameraTrack instanceof MediaStreamTrack) {
participant.cameraStream = new MediaStream([cameraTrack]); // Properly convert cameraTrack to MediaStream
}
this.focusedParticipant = participant; // Focus on the participant who started screen sharing
this.isGridView = false;
this.cdr.detectChanges();
}
}
private handleScreenShareStopped({ userId }: { userId: string }) {
this.isScreenSharingAllowed = true;
const participant = this.participants.find(p => p.id === userId);
if (participant) {
participant.isScreenSharing = false;
if (this.focusedParticipant && this.focusedParticipant.id === userId) {
this.focusedParticipant = null;
this.isGridView = true;
}
this.cdr.detectChanges();
}
}
private handleWhiteboardToggle({ userId, isActive }: { userId: string; isActive: boolean }) {
if (isActive) {
this.isWhiteboardAllowed = false;
} else {
this.isWhiteboardAllowed = true;
}
const participant = this.participants.find(p => p.id === userId);
if (participant) {
participant.isWhiteboardActive = isActive;
if (isActive) {
this.focusedParticipant = participant;
this.isGridView = false;
} else {
this.focusedParticipant = null;
this.isGridView = true;
}
this.cdr.detectChanges();
}
}
private handlePermissionUpdated({ userId, permission, value }: { userId: string; permission: 'screenShare' | 'whiteboard'; value: boolean }) {
const participant = this.participants.find(p => p.id === userId);
if (participant) {
if (permission === 'screenShare') {
participant.hasScreenSharePermission = value;
} else if (permission === 'whiteboard') {
participant.hasWhiteboardPermission = value;
}
this.cdr.detectChanges();
}
}
private handleHostChanged({ newHostId }: { newHostId: string }) {
this.hostId = newHostId;
this.isHost = newHostId === this.socket.id;
this.hasWhiteboardPermission = this.isHost;
this.participants.forEach(participant => {
participant.hasWhiteboardPermission = participant.id === newHostId;
});
this.cdr.detectChanges();
}
private handleHandRaised({ userId }: { userId: string }) {
const participant = this.participants.find(p => p.id === userId);
if (participant) {
participant.isHandRaised = true;
this.cdr.detectChanges();
}
}
toggleHandRaise() {
this.isHandRaised = !this.isHandRaised;
if (this.isHandRaised) {
this.socket.emit('raise-hand', { roomId: this.roomId, userId: this.socket.id });
} else {
this.socket.emit('lower-hand', { roomId: this.roomId, userId: this.socket.id });
}
}
private handleHandLowered({ userId }: { userId: string }) {
const participant = this.participants.find(p => p.id === userId);
if (participant) {
participant.isHandRaised = false;
this.cdr.detectChanges();
}
}
generateRoomId() {
this.roomId = Math.random().toString(36).substring(2, 15);
this.generateMeetingLink();
}
generateMeetingLink() {
this.meetingLink = `${window.location.origin}/join/${this.roomId}`;
}
copyMeetingLink() {
navigator.clipboard.writeText(this.meetingLink).then(() => {
alert('Meeting link copied to clipboard!');
});
}
promptForUserName(isHost: boolean) {
this.userName = prompt('Enter your name to join the meeting:') || '';
if (this.userName) {
localStorage.setItem('userName', this.userName);
if (isHost) {
this.joinRoom(true);
} else {
this.joinRoom(false);
}
} else {
this.router.navigate(['/']);
}
}
async joinRoom(isHost: boolean) {
if (!this.isAuthenticated) {
this.router.navigate(['/login'], { queryParams: { returnUrl: this.router.url } });
return;
}
if (!this.canJoin()) {
alert('Please enter all required information.');
return;
}
try {
this.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
this.updateLocalVideoStream();
if (this.meetingType === 'scheduled') {
// Verify scheduled meeting
this.http.get<{ isValid: boolean }>(`${environment.apiUrls}/meeting/verify-scheduled-meeting/${this.scheduledMeetingId}`).subscribe(
response => {
if (response.isValid) {
this.roomId = this.scheduledMeetingId;
this.connectToRoom(isHost);
} else {
alert('Invalid or expired scheduled meeting ID.');
}
},
error => {
console.error('Error verifying scheduled meeting:', error);
alert('Failed to verify scheduled meeting. Please try again.');
}
);
} else {
this.connectToRoom(isHost);
}
} catch (error) {
console.error('Error joining room:', error);
this.handleJoinRoomError(error);
}
}
private connectToRoom(isHost: boolean) {
this.socket.emit('join-room', { roomId: this.roomId, userName: this.userName, isHost: isHost });
this.isConnected = true;
this.isHost = isHost;
this.hasScreenSharePermission = true;
this.hasWhiteboardPermission = isHost;
this.generateMeetingLink();
this.cdr.detectChanges();
}
private updateLocalVideoStream() {
requestAnimationFrame(() => {
if (this.localVideo && this.localVideo.nativeElement) {
this.localVideo.nativeElement.srcObject = this.localStream;
this.cdr.detectChanges();
}
});
}
private handleJoinRoomError(error: any) {
if (error instanceof DOMException) {
switch (error.name) {
case 'NotAllowedError':
alert('Permission to access camera and microphone was denied.');
break;
case 'NotFoundError':
alert('No camera or microphone found.');
break;
case 'NotReadableError':
alert('Your camera or microphone is already in use by another application.');
break;
default:
alert(`An error occurred: ${error.message}`);
}
} else {
alert('An unexpected error occurred. Please try again.');
}
const joinWithChatOnly = confirm('Would you like to join the room with chat only?');
if (joinWithChatOnly) {
this.socket.emit('join-room', { roomId: this.roomId, userName: this.userName, chatOnly: true });
this.isConnected = true;
this.cdr.detectChanges();
}
}
leaveRoom() {
this.stopScreenSharing();
this.stopRecording();
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop());
}
this.peerConnections.forEach(pc => pc.close());
this.peerConnections.clear();
this.socket.emit('leave-room', { roomId: this.roomId, userName: this.userName });
this.reset();
}
private reset() {
this.localStream = null;
this.participants = [];
this.isConnected = false;
this.roomId = '';
this.messages = [];
this.cdr.detectChanges();
}
async connectToNewUser(userId: string, userName: string, isHost: boolean) {
const pc = this.createPeerConnection(userId);
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
this.socket.emit('offer', { to: userId, offer });
// Add participant to the list
this.participants.push({
id: userId,
stream: new MediaStream(),
name: userName,
isWhiteboardActive: false,
isScreenSharing: false,
hasScreenSharePermission: true,
hasWhiteboardPermission: isHost,
isAudioMuted: false,
isVideoOff: false,
showCameraWithScreenShare: false,
isHandRaised: false,
isHost: false,
});
}
createPeerConnection(userId: string): RTCPeerConnection {
console.log('Creating PeerConnection for user:', userId);
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
});
pc.onicecandidate = (event) => {
if (event.candidate) {
this.socket.emit('ice-candidate', { to: userId, candidate: event.candidate });
}
};
pc.ontrack = (event) => {
const participant = this.participants.find(p => p.id === userId);
if (participant) {
// Add the new track to the participant's stream
participant.stream.addTrack(event.track);
this.cdr.detectChanges();
}
};
if (this.localStream) {
// Add the local stream to the peer connection
this.localStream.getTracks().forEach(track => {
pc.addTrack(track, this.localStream!);
});
}
this.peerConnections.set(userId, pc);
return pc;
}
removeParticipant(userId: string) {
const index = this.participants.findIndex(p => p.id === userId);
if (index !== -1) {
this.participants.splice(index, 1);
}
const pc = this.peerConnections.get(userId);
if (pc) {
pc.close();
this.peerConnections.delete(userId);
}
}
sendMessage() {
if (this.newMessage.trim()) {
const message: Message = {
id: Date.now().toString(),
sender: this.userName,
content: this.newMessage.trim(),
time: new Date(),
isPrivate: this.chatRecipient !== 'everyone',
recipient: this.chatRecipient !== 'everyone' ? this.chatRecipient : undefined
};
if (message.isPrivate) {
// If the recipient is 'host', set the recipientId to host's socket ID
if (this.chatRecipient === 'host') {
message.recipient = this.hostId; // You'll need to store hostId
}
this.socket.emit('private-message', { roomId: this.roomId, message, recipientId: message.recipient });
} else {
this.socket.emit('send-message', { roomId: this.roomId, message });
}
this.messages.push(message);
this.newMessage = '';
this.scrollToBottom();
}
}
private handlePrivateMessage({ message }: { message: Message }) {
if (!this.messages.some(m => m.id === message.id)) {
this.messages.push(message);
this.cdr.detectChanges();
this.scrollToBottom();
// Increment unread messages count if chat is not open
if (!this.showChat) {
this.unreadMessagesCount++;
this.messageSound.play();
}
}
}
toggleCameraWithScreenShare(participantId: string) {
const participant = this.participants.find(p => p.id === participantId);
if (participant) {
participant.showCameraWithScreenShare = !participant.showCameraWithScreenShare;
this.socket.emit('camera-toggle', { roomId: this.roomId, userId: participantId, showCamera: participant.showCameraWithScreenShare });
this.cdr.detectChanges();
}
}
private handleCameraToggle({ userId, showCamera }: { userId: string; showCamera: boolean }) {
const participant = this.participants.find(p => p.id === userId);
if (participant) {
participant.showCameraWithScreenShare = showCamera;
this.cdr.detectChanges();
}
}
private scrollToBottom() {
setTimeout(() => {
const chatMessages = this.chatMessages.nativeElement;
chatMessages.scrollTop = chatMessages.scrollHeight;
}, 0);
}
toggleChat() {
this.showChat = !this.showChat;
this.showParticipants = false;
this.showMeetingDetails = false;
// Reset unread messages count when chat is opened
if (this.showChat) {
this.unreadMessagesCount = 0;
}
this.cdr.detectChanges();
}
toggleAudio() {
if (this.localStream) {
const audioTrack = this.localStream.getAudioTracks()[0];
if (audioTrack) {
audioTrack.enabled = !audioTrack.enabled;
this.isAudioMuted = !audioTrack.enabled;
this.socket.emit('audio-toggle', { roomId: this.roomId, userId: this.socket.id, isAudioMuted: this.isAudioMuted });
}
}
this.cdr.detectChanges();
}
async toggleVideo() {
if (this.localStream) {
this.isVideoOff = !this.isVideoOff;
if (this.isVideoOff) {
await this.stopVideoTrack();
} else {
await this.startVideoTrack();
}
await this.updateLocalVideoStream();
if (this.isHost) {
this.socket.emit('host-media-change', {
roomId: this.roomId,
mediaType: 'video',
isActive: !this.isVideoOff
});
}
}
this.cdr.detectChanges();
}
private async stopVideoTrack() {
console.log('Stopping video track...');
const videoTrack = this.localStream!.getVideoTracks()[0];
if (videoTrack) {
videoTrack.stop();
this.localStream!.removeTrack(videoTrack);
console.log('Video track stopped and removed');
}
}
private async startVideoTrack() {
try {
console.log('Starting new video track...');
const newStream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
}
});
const newVideoTrack = newStream.getVideoTracks()[0];
this.localStream!.addTrack(newVideoTrack);
await this.replaceVideoTrackInPeerConnections(newVideoTrack);
console.log('New video track started and added');
} catch (error) {
console.error('Error starting video track:', error);
}
}
private async replaceVideoTrackInPeerConnections(newTrack: MediaStreamTrack) {
console.log('Replacing video track in peer connections...');
for (const [peerId, pc] of this.peerConnections) {
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
if (sender) {
try {
await sender.replaceTrack(newTrack);
console.log(`Video track replaced for peer: ${peerId}`);
} catch (error) {
console.error(`Error replacing track for peer ${peerId}:`, error);
}
}
}
}
async toggleScreenSharing() {
debugger
if (!this.isScreenSharing) {
await this.startScreenSharing();
} else {
this.stopScreenSharing();
}
if (this.isHost) {
this.socket.emit('host-media-change', {
roomId: this.roomId,
mediaType: 'screen',
isActive: this.isScreenSharing
});
}
}
async startScreenSharing() {
try {
this.screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
if (this.screenStream && this.screenStream.getVideoTracks().length > 0) {
this.isScreenSharing = true;
const screenTrack = this.screenStream.getVideoTracks()[0];
this.replaceVideoTrack(screenTrack);
screenTrack.onended = () => {
this.stopScreenSharing();
};
// Keep the camera stream separate
const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });
this.socket.emit('screen-share-started', {
roomId: this.roomId,
userId: this.socket.id,
cameraTrack: cameraStream.getVideoTracks()[0]
});
this.focusedParticipant = {
id: this.socket.id,
stream: this.screenStream,
cameraStream: cameraStream,
name: this.userName + ' (Screen)',
isWhiteboardActive: false,
isScreenSharing: true,
hasScreenSharePermission: true,
hasWhiteboardPermission: true,
isAudioMuted: this.isAudioMuted,
isVideoOff: false,
showCameraWithScreenShare: true,
isHandRaised: false,
isHost: false,
};
this.isGridView = false;
this.cdr.detectChanges();
} else {
console.warn('No video tracks found in the screen sharing stream.');
this.stopScreenSharing();
}
} catch (error) {
console.error('Error sharing screen:', error);
this.stopScreenSharing();
}
}
async stopScreenSharing() {
if (this.screenStream) {
this.screenStream.getTracks().forEach(track => track.stop());
this.screenStream = null;
}
this.isScreenSharing = false;
// Restore the original video track
await this.restoreVideoTrack();
this.socket.emit('screen-share-stopped', { roomId: this.roomId, userId: this.socket.id });
this.focusedParticipant = null;
this.isGridView = true;
this.cdr.detectChanges();
}
// async restoreVideoTrack() {
// try {
// // Get a new video stream
// const newStream = await navigator.mediaDevices.getUserMedia({ video: true });
// const newVideoTrack = newStream.getVideoTracks()[0];
// // Replace the track in all peer connections
// this.replaceVideoTrack(newVideoTrack);
// // Update local video
// if (this.localVideo && this.localVideo.nativeElement) {
// this.localVideo.nativeElement.srcObject = new MediaStream([newVideoTrack]);
// }
// // Don't forget to stop the old video track if it exists
// if (this.localStream) {
// const oldVideoTrack = this.localStream.getVideoTracks()[0];
// if (oldVideoTrack) {
// oldVideoTrack.stop();
// }
// // Update the localStream with the new video track
// this.localStream = new MediaStream([
// ...this.localStream.getAudioTracks(),
// newVideoTrack
// ]);
// }
// } catch (error) {
// console.error('Error restoring video track:', error);
// }
// }
async restoreVideoTrack() {
if (isPlatformBrowser(this.platformId)) {
try {
// Get a new video stream
const newStream = await navigator.mediaDevices.getUserMedia({ video: true });
const newVideoTrack = newStream.getVideoTracks()[0];
// Replace the track in all peer connections
this.replaceVideoTrack(newVideoTrack);
// Update local video
if (this.localVideo && this.localVideo.nativeElement) {
this.localVideo.nativeElement.srcObject = new MediaStream([newVideoTrack]);
}
// Stop the old video track if it exists
if (this.localStream) {
const oldVideoTrack = this.localStream.getVideoTracks()[0];
if (oldVideoTrack) {
oldVideoTrack.stop();
}
// Update the localStream with the new video track
this.localStream = new MediaStream([
...this.localStream.getAudioTracks(),
newVideoTrack
]);
}
} catch (error) {
console.error('Error restoring video track:', error);
}
} else {
console.warn('restoreVideoTrack called on the server; skipping.');
}
}
replaceVideoTrack(newTrack: MediaStreamTrack | null) {
this.peerConnections.forEach(pc => {
const sender = pc.getSenders().find(s => s.track?.kind === 'video');
if (sender) {
sender.replaceTrack(newTrack);
}
});
if (this.localVideo && this.localVideo.nativeElement) {
if (newTrack) {
const stream = new MediaStream([newTrack]);
this.localVideo.nativeElement.srcObject = stream;
} else {
this.localVideo.nativeElement.srcObject = null;
}
}
}
focusParticipant(participant: Participant) {
if (this.isGridView) {
this.focusedParticipant = participant;
this.isGridView = false;
} else {
this.focusedParticipant = null;
this.isGridView = true;
}
this.cdr.detectChanges();
}
toggleFullscreen() {
if (!this.isFullscreen) {
const elem = document.documentElement;
if (elem.requestFullscreen) {
elem.requestFullscreen();
}
this.isFullscreen = true;
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
this.isFullscreen = false;
}
this.cdr.detectChanges();
}
toggleWhiteboard() {
if (this.isHost || this.hasWhiteboardPermission) {
this.localWhiteboardActive = !this.localWhiteboardActive;
if (this.localWhiteboardActive) {
this.activeView = 'whiteboard';
} else {
this.activeView = 'camera';
this.restoreVideoTrack();
}
this.socket.emit('whiteboard-toggle', { roomId: this.roomId, userId: this.socket.id, isActive: this.localWhiteboardActive });
this.cdr.detectChanges();
} else {
alert('You do not have permission to use the whiteboard.');
}
}
updateParticipantPermission(participantId: string, permission: 'screenShare' | 'whiteboard', value: boolean) {
if (this.isHost) {
this.socket.emit('update-permission', { roomId: this.roomId, userId: participantId, permission, value });
}
}
toggleRecording() {
if (!this.isRecording) {
this.startRecording();
} else {
this.stopRecording();
}
}
toggleParticipantsList() {
this.showParticipants = !this.showParticipants;
this.showChat = false;
this.showMeetingDetails = false;
this.cdr.detectChanges();
}
startRecording() {
this.recordedChunks = [];
const stream = new MediaStream();
if (this.localStream) {
this.localStream.getTracks().forEach(track => stream.addTrack(track));
}
this.participants.forEach(participant => {
participant.stream.getTracks().forEach(track => stream.addTrack(track));
});
this.mediaRecorder = new MediaRecorder(stream, { mimeType: 'video/webm' });
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.recordedChunks.push(event.data);
}
};
this.mediaRecorder.start();
this.isRecording = true;
this.socket.emit('recording-started', { roomId: this.roomId, userId: this.socket.id });
}
stopRecording() {
if (this.mediaRecorder) {
this.mediaRecorder.stop();
this.isRecording = false;
this.socket.emit('recording-stopped', { roomId: this.roomId, userId: this.socket.id });
setTimeout(() => {
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
this.uploadRecording(blob);
}, 100);
}
}
uploadRecording(blob: Blob) {
const formData = new FormData();
formData.append('file', blob, `recording_${Date.now()}.webm`);
formData.append('roomId', this.roomId);
this.http.post<{ id: string; name: string; date: Date }>('http://localhost:3000/api/recordings', formData)
.subscribe(
(response) => {
console.log('Recording uploaded successfully');
// Handle successful upload (e.g., show a notification)
},
(error) => {
console.error('Error uploading recording:', error);
// Handle upload error (e.g., show an error message)
}
);
}
toggleMeetingDetails() {
this.showMeetingDetails = !this.showMeetingDetails;
this.showChat = false;
this.showParticipants = false;
this.cdr.detectChanges();
}
raiseHand() {
this.socket.emit('raise-hand', { roomId: this.roomId, userId: this.socket.id, userName: this.userName });
}
lowerHand() {
this.socket.emit('lower-hand', { roomId: this.roomId, userId: this.socket.id });
}
toggleMoreOptions() {
// Implement the logic to show/hide more options
console.log('Toggle more options');
}
changeHostTo(newHostId: string) {
if (this.isHost) {
this.socket.emit('change-host', { roomId: this.roomId, newHostId });
}
}
trackByFn(index: number, item: any): any {
return item.id;
}
}
0 Comments