215 lines
7.3 KiB
TypeScript
215 lines
7.3 KiB
TypeScript
import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { CallService, CallInfo } from '../../services/call.service';
|
|
import { UserService } from '../../services/user.service';
|
|
import { LoggerService } from '../../services/logger.service';
|
|
import { Subscription } from 'rxjs';
|
|
|
|
@Component({
|
|
selector: 'app-call',
|
|
standalone: true,
|
|
imports: [CommonModule],
|
|
templateUrl: './call.component.html',
|
|
styleUrl: './call.component.scss'
|
|
})
|
|
export class CallComponent implements OnInit, OnDestroy {
|
|
@ViewChild('localVideo') localVideoRef!: ElementRef<HTMLVideoElement>;
|
|
@ViewChild('remoteVideo') remoteVideoRef!: ElementRef<HTMLVideoElement>;
|
|
|
|
currentCall: CallInfo | null = null;
|
|
isRinging: boolean = false;
|
|
isCallActive: boolean = false;
|
|
isMuted: boolean = false;
|
|
isVideoEnabled: boolean = true;
|
|
isIncoming: boolean = false;
|
|
private subscriptions: Subscription[] = [];
|
|
private currentUser: any = null; // Cache current user to avoid repeated API calls
|
|
|
|
constructor(
|
|
public callService: CallService,
|
|
public userService: UserService,
|
|
private logger: LoggerService
|
|
) {}
|
|
|
|
async ngOnInit(): Promise<void> {
|
|
// Cache current user on init to avoid repeated calls
|
|
this.currentUser = await this.userService.getCurrentUser();
|
|
|
|
const callSub = this.callService.call$.subscribe(callInfo => {
|
|
this.handleCallUpdate(callInfo);
|
|
});
|
|
|
|
const signalSub = this.callService.signal$.subscribe(signal => {
|
|
if (signal.signalData?.remoteStreamReady && this.remoteVideoRef) {
|
|
const remoteStream = this.callService.getRemoteStream();
|
|
if (remoteStream && this.remoteVideoRef.nativeElement) {
|
|
this.remoteVideoRef.nativeElement.srcObject = remoteStream;
|
|
}
|
|
}
|
|
});
|
|
|
|
this.subscriptions.push(callSub, signalSub);
|
|
|
|
// Update video streams periodically
|
|
const interval = setInterval(() => {
|
|
this.updateVideoStreams();
|
|
}, 1000);
|
|
|
|
this.subscriptions.push(new Subscription(() => clearInterval(interval)));
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.subscriptions.forEach(sub => sub.unsubscribe());
|
|
}
|
|
|
|
private async handleCallUpdate(callInfo: CallInfo | null): Promise<void> {
|
|
this.logger.debug('Call component received call update:', callInfo);
|
|
|
|
// Handle null callInfo (when call is cleared)
|
|
if (!callInfo) {
|
|
this.logger.debug('Call info is null, clearing call state');
|
|
this.currentCall = null;
|
|
this.isRinging = false;
|
|
this.isCallActive = false;
|
|
if (this.localVideoRef?.nativeElement) {
|
|
this.localVideoRef.nativeElement.srcObject = null;
|
|
}
|
|
if (this.remoteVideoRef?.nativeElement) {
|
|
this.remoteVideoRef.nativeElement.srcObject = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.currentCall = callInfo;
|
|
|
|
// Ensure we have current user (use cache if available, fetch if not)
|
|
if (!this.currentUser) {
|
|
this.currentUser = await this.userService.getCurrentUser();
|
|
}
|
|
|
|
// Determine if this is an incoming call (we are the receiver) or outgoing call (we are the sender)
|
|
if (this.currentUser) {
|
|
const isSender = callInfo.senderId === this.currentUser.id;
|
|
const isReceiver = callInfo.receiverId === this.currentUser.id;
|
|
|
|
// It's incoming if we are the receiver (not the sender) and the call is ringing
|
|
// It's outgoing if we are the sender (not the receiver) and the call is ringing
|
|
this.isIncoming = isReceiver && !isSender && callInfo.callStatus === 'ringing';
|
|
|
|
this.logger.debug('Current user ID:', this.currentUser.id,
|
|
'Sender ID:', callInfo.senderId,
|
|
'Receiver ID:', callInfo.receiverId,
|
|
'Is sender:', isSender,
|
|
'Is receiver:', isReceiver,
|
|
'Is incoming:', this.isIncoming);
|
|
}
|
|
|
|
if (callInfo.callStatus === 'ringing') {
|
|
this.logger.debug('Call is ringing - showing UI');
|
|
this.isRinging = true;
|
|
this.isCallActive = false;
|
|
// Update UI immediately
|
|
this.updateVideoStreams();
|
|
} else if (callInfo.callStatus === 'accepted') {
|
|
this.logger.debug('Call accepted - starting active call');
|
|
this.isRinging = false;
|
|
this.isCallActive = true;
|
|
setTimeout(() => this.updateVideoStreams(), 500);
|
|
} else {
|
|
this.isRinging = false;
|
|
this.isCallActive = false;
|
|
if (callInfo.callStatus === 'ended' || callInfo.callStatus === 'rejected' || callInfo.callStatus === 'cancelled') {
|
|
this.logger.debug('Call terminated:', callInfo.callStatus);
|
|
this.currentCall = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private updateVideoStreams(): void {
|
|
if (this.localVideoRef?.nativeElement) {
|
|
const localStream = this.callService.getLocalStream();
|
|
if (localStream) {
|
|
this.localVideoRef.nativeElement.srcObject = localStream;
|
|
}
|
|
}
|
|
|
|
if (this.remoteVideoRef?.nativeElement && this.isCallActive) {
|
|
const remoteStream = this.callService.getRemoteStream();
|
|
if (remoteStream) {
|
|
this.remoteVideoRef.nativeElement.srcObject = remoteStream;
|
|
}
|
|
}
|
|
}
|
|
|
|
async answerCall(): Promise<void> {
|
|
await this.callService.answerCall();
|
|
this.isRinging = false;
|
|
this.isCallActive = true;
|
|
}
|
|
|
|
async rejectCall(): Promise<void> {
|
|
await this.callService.rejectCall();
|
|
this.isRinging = false;
|
|
this.currentCall = null;
|
|
}
|
|
|
|
async endCall(): Promise<void> {
|
|
// If call is still ringing and we're the caller, cancel the call
|
|
if (this.isRinging && !this.isIncoming && this.currentCall) {
|
|
await this.callService.cancelCall();
|
|
} else {
|
|
await this.callService.endCall();
|
|
}
|
|
this.isCallActive = false;
|
|
this.currentCall = null;
|
|
this.isRinging = false;
|
|
if (this.localVideoRef?.nativeElement) {
|
|
this.localVideoRef.nativeElement.srcObject = null;
|
|
}
|
|
if (this.remoteVideoRef?.nativeElement) {
|
|
this.remoteVideoRef.nativeElement.srcObject = null;
|
|
}
|
|
}
|
|
|
|
async toggleMute(): Promise<void> {
|
|
this.isMuted = !await this.callService.toggleMute();
|
|
}
|
|
|
|
async toggleVideo(): Promise<void> {
|
|
this.isVideoEnabled = !await this.callService.toggleVideo();
|
|
}
|
|
|
|
getOtherUserName(): string {
|
|
if (!this.currentCall) return '';
|
|
// Use cached currentUser if available, otherwise return based on call info
|
|
// If we're the sender (incoming=false), show receiver name
|
|
// If we're the receiver (incoming=true), show sender name
|
|
if (this.isIncoming && this.currentCall.senderName) {
|
|
return this.currentCall.senderName;
|
|
} else if (!this.isIncoming && this.currentCall.receiverName) {
|
|
return this.currentCall.receiverName;
|
|
}
|
|
return this.currentCall.senderName || this.currentCall.receiverName || 'Unknown';
|
|
}
|
|
|
|
getOtherUserAvatarUrl(): string | undefined {
|
|
if (!this.currentCall) return undefined;
|
|
// Return sender's avatar if we're the receiver (incoming call)
|
|
// Return receiver's avatar if we're the sender (outgoing call)
|
|
if (this.isIncoming) {
|
|
return this.currentCall.senderAvatarUrl;
|
|
}
|
|
// For outgoing calls, return the receiver's avatar
|
|
return this.currentCall.receiverAvatarUrl;
|
|
}
|
|
|
|
isVideoCall(): boolean {
|
|
return this.currentCall?.callType === 'video';
|
|
}
|
|
|
|
shouldShowCall(): boolean {
|
|
return this.currentCall !== null && (this.isRinging || this.isCallActive);
|
|
}
|
|
}
|
|
|