video call

This commit is contained in:
2026-03-11 08:05:54 +01:00
parent f0e2b60f43
commit ffdea4fe62
9 changed files with 644 additions and 412 deletions

View File

@@ -4,14 +4,14 @@ import { toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { PeerVideoModalComponent } from './peer-video-modal.component';
import { PeerCallModalComponent } from './peer-call-modal.component';
import { ChatSessionService } from './chat-session.service';
import { JsonFileViewerComponent } from './json-file-viewer.component';
import type { ChatEntry, ConnectionState, PeerSummary } from './models';
import type { CallMode, ChatEntry, ConnectionState, PeerSummary } from './models';
@Component({
selector: 'app-chat-page',
imports: [CommonModule, FormsModule, RouterLink, JsonFileViewerComponent, PeerVideoModalComponent],
imports: [CommonModule, FormsModule, RouterLink, JsonFileViewerComponent, PeerCallModalComponent],
templateUrl: './chat-page.component.html',
styleUrl: './chat-page.component.scss',
})
@@ -46,6 +46,7 @@ export class ChatPageComponent implements OnDestroy {
messageText = '';
readonly forwardingEntryId = signal<string | null>(null);
readonly callChoicePeerId = signal<string | null>(null);
readonly emojiPickerOpen = signal(false);
readonly isRecordingVoice = signal(false);
readonly isDictating = signal(false);
@@ -63,8 +64,19 @@ export class ChatPageComponent implements OnDestroy {
readonly peerId = computed(() => this.routeParamMap().get('peerId') ?? '');
readonly peer = computed(() => this.session.peers().find((item) => item.id === this.peerId()) ?? null);
readonly currentUser = computed(() => this.session.currentUser());
readonly incomingVoiceCallPeer = computed(() => {
const peerId = this.session.incomingVoiceCallPeerId();
readonly callModalPeerId = computed(() =>
this.session.activeVoiceCallPeerId()
?? this.session.incomingVoiceCallPeerId()
?? this.session.outgoingVoiceCallPeerId()
?? null,
);
readonly callModalPeer = computed(() => {
const peerId = this.callModalPeerId();
return peerId ? this.session.peers().find((peer) => peer.id === peerId) ?? null : null;
});
readonly callChoicePeer = computed(() => {
const peerId = this.callChoicePeerId();
return peerId ? this.session.peers().find((peer) => peer.id === peerId) ?? null : null;
});
@@ -73,13 +85,50 @@ export class ChatPageComponent implements OnDestroy {
.messages()
.filter((entry) => entry.peerId === this.peerId()),
);
readonly remoteVideoStream = computed(() => this.session.remoteVideoStreamForPeer(this.peerId()));
readonly remoteCallAudioStream = computed(() =>
this.session.remoteAudioStreamForPeer(this.session.activeVoiceCallPeerId() ?? ''),
);
readonly remoteVideoModalVisible = computed(
() => this.session.remoteVideoModalPeerId() === this.peerId() && !!this.remoteVideoStream(),
this.session.remoteAudioStreamForPeer(this.callModalPeerId() ?? ''),
);
readonly callModalMode = computed<CallMode>(() => this.session.callModeForPeer(this.callModalPeerId() ?? '') ?? 'video');
readonly localCallStream = computed(() => this.session.localCallStreamForPeer(this.callModalPeerId() ?? ''));
readonly remoteCallVideoStream = computed(() => this.session.remoteVideoStreamForPeer(this.callModalPeerId() ?? ''));
readonly callModalVisible = computed(() => !!this.callModalPeer());
readonly callModalState = computed<'incoming' | 'outgoing' | 'active'>(() => {
const peerId = this.callModalPeerId();
if (!peerId) {
return 'active';
}
if (this.session.incomingVoiceCallPeerId() === peerId) {
return 'incoming';
}
if (this.session.outgoingVoiceCallPeerId() === peerId) {
return 'outgoing';
}
return 'active';
});
readonly callModalStatusText = computed(() => {
const peer = this.callModalPeer();
if (!peer) {
return '';
}
switch (this.callModalState()) {
case 'incoming':
return `${peer.displayName} is calling you${this.callModalMode() === 'audio' ? ' with audio only.' : '.'}`;
case 'outgoing':
return this.callModalMode() === 'audio'
? 'Calling… your microphone is ready.'
: 'Calling… your camera and microphone are ready.';
default:
return this.callModalMode() === 'audio'
? 'Connected with live audio.'
: 'Connected with live video and audio.';
}
});
readonly selectedPeerVoiceCallState = computed<'idle' | 'incoming' | 'outgoing' | 'active'>(() => {
const peerId = this.peerId();
@@ -265,6 +314,29 @@ export class ChatPageComponent implements OnDestroy {
});
}
openCallChoice(peerId: string): void {
if (!peerId) {
return;
}
this.callChoicePeerId.set(peerId);
}
closeCallChoice(): void {
this.callChoicePeerId.set(null);
}
async startSelectedCall(mode: CallMode): Promise<void> {
const peerId = this.callChoicePeerId() ?? this.peerId();
if (!peerId) {
return;
}
this.callChoicePeerId.set(null);
await this.session.startVoiceCall(peerId, mode);
}
async sendFile(peerId: string, input: HTMLInputElement): Promise<void> {
const file = input.files?.item(0);
@@ -474,19 +546,6 @@ export class ChatPageComponent implements OnDestroy {
this.forwardingEntryId.set(null);
}
async toggleCameraStream(peerId: string): Promise<void> {
if (this.session.isStreamingCameraToPeer(peerId)) {
await this.session.stopCameraStream(peerId);
return;
}
await this.session.startCameraStream(peerId);
}
async startVoiceCall(peerId: string): Promise<void> {
await this.session.startVoiceCall(peerId);
}
async endVoiceCall(peerId: string): Promise<void> {
await this.session.endVoiceCall(peerId);
}
@@ -496,10 +555,6 @@ export class ChatPageComponent implements OnDestroy {
return;
}
if (peerId !== this.peerId()) {
await this.router.navigate(['/chat', peerId]);
}
await this.session.acceptVoiceCall(peerId);
}
@@ -561,22 +616,6 @@ export class ChatPageComponent implements OnDestroy {
return this.indicatorTone(this.webRtcState()) === 'offline';
}
isStreamingCameraToSelectedPeer(): boolean {
const peerId = this.peerId();
return !!peerId && this.session.isStreamingCameraToPeer(peerId);
}
closeRemoteVideoModal(): void {
const peerId = this.peerId();
if (!peerId) {
return;
}
this.session.dismissRemoteVideoModal(peerId);
}
async switchPeer(peerId: string): Promise<void> {
if (!peerId || peerId === this.peerId()) {
return;
@@ -585,6 +624,7 @@ export class ChatPageComponent implements OnDestroy {
await this.stopDictation(true);
this.stopVoiceRecording(true);
this.forwardingEntryId.set(null);
this.callChoicePeerId.set(null);
this.emojiPickerOpen.set(false);
this.session.selectPeer(peerId);
await this.router.navigate(['/chat', peerId]);