Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ef19bb4

Browse files
committed
feat: add functionnal version of web rtc visio
1 parent 26f56be commit ef19bb4

File tree

9 files changed

+238
-37
lines changed

9 files changed

+238
-37
lines changed

front/src/app/features/supports/components/visio/visio.component.html

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,27 @@
55
<video #remote autoplay playsinline class="w-full rounded shadow border border-gray-300"></video>
66
</div>
77

8-
<button
8+
<div class="flex justify-between p-5">
9+
<button
910
(click)="startCall()"
1011
class="main-button"
11-
>
12-
Start Call
13-
</button>
12+
[disabled]="incomingCall"
13+
>
14+
Start Call
15+
</button>
16+
<button (click)="exitCall()" class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600">
17+
Exit call
18+
</button>
19+
@if(incomingCall) {
20+
<div>
21+
<p class="text-lg font-semibold text-gray-700">Incoming call...</p>
22+
<button class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600" (click)="acceptCall()">
23+
Accept Call
24+
</button>
25+
</div>
26+
}
27+
</div>
28+
29+
1430
</div>
1531

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { Component, ElementRef, ViewChild } from '@angular/core';
2-
import { AuthService } from '../../../../core/services/auth.service';
1+
import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
32
import { WebrtcService } from '../../services/webrtc.service';
43
import { SessionService } from '../../../../core/services/session.service';
54
import { ToastrService } from 'ngx-toastr';
5+
import { VisioSignalMessage } from '../../interfaces/visio-signal-message';
66

77
@Component({
88
selector: 'app-visio',
@@ -11,29 +11,50 @@ import { ToastrService } from 'ngx-toastr';
1111
templateUrl: './visio.component.html',
1212
styleUrl: './visio.component.scss'
1313
})
14-
export class VisioComponent {
14+
export class VisioComponent implements OnInit,OnDestroy {
1515
@Component({
1616
selector: 'app-visio',
1717
templateUrl: './visio.component.html',
1818
})
19-
@ViewChild('local') local!: ElementRef<HTMLVideoElement>;
20-
@ViewChild('remote') remote!: ElementRef<HTMLVideoElement>;
19+
@ViewChild('local') local!: ElementRef<HTMLVideoElement>;
20+
@ViewChild('remote') remote!: ElementRef<HTMLVideoElement>;
21+
@Input({required:true}) receiverId!: number;
2122

22-
constructor(private webrtc: WebrtcService, private auth: AuthService,private sessionService: SessionService,private toastr: ToastrService) {}
23+
incomingCall: VisioSignalMessage | null = null;
24+
25+
26+
constructor(private webrtc: WebrtcService,
27+
private sessionService: SessionService,
28+
private toastr: ToastrService) { }
2329

2430
ngOnInit(): void {
25-
//TODO: update session service with behavior sub
2631
const user = this.sessionService.getUser()!;
32+
this.webrtc.incomingOffer$.subscribe((signal) => {
33+
this.incomingCall = signal;
34+
});
2735
this.webrtc.listenToSignals(user.id);
36+
37+
}
38+
39+
ngOnDestroy(): void {
40+
this.webrtc.cleanUp();
2841
}
2942

3043
async startCall() {
3144
try {
32-
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
33-
45+
await this.webrtc.initPeer(this.receiverId, (remoteStream: MediaStream) => {
46+
if (this.remote?.nativeElement) {
47+
this.remote.nativeElement.srcObject = remoteStream;
48+
}
49+
});
50+
51+
const localStream = this.webrtc.getLocalStream();
3452
if (this.local?.nativeElement) {
35-
this.local.nativeElement.srcObject = stream;
53+
this.local.nativeElement.srcObject = localStream;
3654
}
55+
56+
await this.webrtc.createOffer(this.receiverId);
57+
3758
} catch (error: any) {
3859
if (error.name === 'NotAllowedError') {
3960
this.toastr.error("L'accès à la caméra ou au micro a été refusé.");
@@ -50,4 +71,27 @@ export class VisioComponent {
5071
}
5172
}
5273
}
74+
// TODO : need to add function for base code (init peer, getlocalstream and error toastr) //
75+
async acceptCall() {
76+
if (!this.incomingCall) return;
77+
78+
await this.webrtc.initPeer(this.incomingCall.senderId, (remoteStream) => {
79+
if (this.remote?.nativeElement) {
80+
this.remote.nativeElement.srcObject = remoteStream;
81+
}
82+
});
83+
84+
const localStream = this.webrtc.getLocalStream();
85+
if (this.local?.nativeElement) {
86+
this.local.nativeElement.srcObject = localStream;
87+
}
88+
89+
await this.webrtc.createAnswer(this.incomingCall);
90+
this.incomingCall = null;
91+
}
92+
93+
exitCall() {
94+
this.webrtc.close();
95+
}
96+
5397
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
<!-- User selection area -->
3+
<app-avaible-user-list #avaibleUser (selectedUser)="selectUser($event)"></app-avaible-user-list>
4+
5+
<!-- visio area -->
6+
<div class="relative flex flex-col">
7+
@if (userSelected(); as user) {
8+
<div class="flex justify-between items-center px-4 py-3 bg-gray-100 border-b">
9+
<span class="font-medium text-lg">{{ user.email }}</span>
10+
<button
11+
class="text-sm text-red-500 hover:underline"
12+
(click)="closeVisio()"
13+
>
14+
Close
15+
</button>
16+
</div>
17+
18+
<app-visio #visio class="flex-1 overflow-y-auto" [receiverId]="user.id"></app-visio>
19+
}
20+
21+
@else {
22+
<div class="flex items-center justify-center h-full text-gray-400 text-lg">
23+
Select a user to start visio
24+
</div>
25+
}
26+
</div>
27+

front/src/app/features/supports/container/visio-container/visio-container.component.scss

Whitespace-only changes.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { ComponentFixture, TestBed } from '@angular/core/testing';
2+
3+
import { VisioContainerComponent } from './visio-container.component';
4+
5+
describe('VisioContainerComponent', () => {
6+
let component: VisioContainerComponent;
7+
let fixture: ComponentFixture<VisioContainerComponent>;
8+
9+
beforeEach(async () => {
10+
await TestBed.configureTestingModule({
11+
imports: [VisioContainerComponent]
12+
})
13+
.compileComponents();
14+
15+
fixture = TestBed.createComponent(VisioContainerComponent);
16+
component = fixture.componentInstance;
17+
fixture.detectChanges();
18+
});
19+
20+
it('should create', () => {
21+
expect(component).toBeTruthy();
22+
});
23+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Component, signal, ViewChild } from '@angular/core';
2+
import { UserInfo } from '../../../../core/interfaces/user-info';
3+
import { SessionService } from '../../../../core/services/session.service';
4+
import { AvaibleUserListComponent } from '../../components/avaible-user-list/avaible-user-list.component';
5+
import { VisioComponent } from '../../components/visio/visio.component';
6+
7+
@Component({
8+
selector: 'app-visio-container',
9+
standalone: true,
10+
imports: [AvaibleUserListComponent,VisioComponent],
11+
templateUrl: './visio-container.component.html',
12+
styleUrl: './visio-container.component.scss'
13+
})
14+
export class VisioContainerComponent {
15+
@ViewChild('avaibleUser')
16+
avaibaleUserList!: AvaibleUserListComponent;
17+
@ViewChild('visio')
18+
visio!: VisioComponent;
19+
currentUserInfo = signal<UserInfo | null>(null);
20+
userSelected = signal<UserInfo | null>(null);
21+
22+
constructor(
23+
private sessionService: SessionService
24+
) {
25+
this.currentUserInfo.set(this.sessionService.getUser());
26+
}
27+
28+
selectUser(user: UserInfo) {
29+
this.userSelected.set(user);
30+
}
31+
32+
33+
closeVisio() {
34+
this.visio.exitCall();
35+
this.userSelected.set(null);
36+
this.avaibaleUserList.form.patchValue({userId:''});
37+
}
38+
}
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
<app-visio></app-visio>
1+
<div class="w-full max-w-6xl mx-auto bg-white shadow rounded-lg flex flex-col">
2+
<h2 class="text-2xl font-semibold text-center py-4 border-b border-gray-200 bg-gray-50">
3+
Support Visio
4+
</h2>
5+
<app-visio-container></app-visio-container>
6+
</div>

front/src/app/features/supports/pages/visio-conferencing-page/visio-conferencing-page.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Component } from '@angular/core';
2-
import { VisioComponent } from '../../components/visio/visio.component';
2+
import { VisioContainerComponent } from '../../container/visio-container/visio-container.component';
33

44
@Component({
55
selector: 'app-visio-conferencing-page',
66
standalone: true,
7-
imports: [VisioComponent],
7+
imports: [VisioContainerComponent],
88
templateUrl: './visio-conferencing-page.component.html',
99
styleUrl: './visio-conferencing-page.component.scss'
1010
})

front/src/app/features/supports/services/webrtc.service.ts

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,55 @@
11
import { Injectable } from "@angular/core";
2-
import { RxStomp } from "@stomp/rx-stomp";
3-
import { AuthService } from "../../../core/services/auth.service";
2+
import { IMessage, RxStomp } from "@stomp/rx-stomp";
43
import { SessionService } from "../../../core/services/session.service";
54
import { rxStompConfig } from "../config/rx-stomp.config";
65
import { VisioSignalMessage } from "../interfaces/visio-signal-message";
76
import { SignalingMessageTypeEnum } from "../enums/signaling-message-type.enum";
7+
import { Subject } from "rxjs";
88

99
@Injectable({ providedIn: 'root' })
1010
export class WebrtcService {
11-
private peer!: RTCPeerConnection;
11+
private peer!: RTCPeerConnection; // todo: for support multiple peers use an map with the user id for the key.
1212
private localStream!: MediaStream;
1313
private remoteStream!: MediaStream;
14-
1514
private rxStomp = new RxStomp();
1615

17-
constructor(private authService: AuthService, private sessionService: SessionService) {
16+
public incomingOffer$: Subject<VisioSignalMessage> = new Subject();
17+
18+
19+
constructor(private sessionService: SessionService) {
1820
this.rxStomp.configure(rxStompConfig);
1921
this.rxStomp.activate();
2022
}
2123

24+
cleanUp() : void {
25+
if (this.rxStomp.connected()) {
26+
this.rxStomp.deactivate();
27+
}
28+
}
29+
2230
async initPeer(receiverId: number, onRemoteStream: (stream: MediaStream) => void): Promise<void> {
23-
this.peer = new RTCPeerConnection();
31+
this.peer = new RTCPeerConnection({
32+
iceServers: [
33+
{ urls: 'stun:stun.l.google.com:19302' }
34+
]
35+
});
2436

2537
try {
38+
//Set local stream with the user media and add track to the peer connexion.
2639
this.localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
2740
this.localStream.getTracks().forEach(track => this.peer.addTrack(track, this.localStream));
2841

42+
// Listen track added by remote stream
2943
this.peer.ontrack = (event) => {
3044
this.remoteStream = event.streams[0];
3145
onRemoteStream(this.remoteStream);
3246
};
3347

48+
/**
49+
* Listen generated local ice candidate
50+
* for send to the remote peer (receiver)
51+
* by the signaling server.
52+
* */
3453
this.peer.onicecandidate = (event) => {
3554
if (event.candidate) {
3655
this.sendSignal({
@@ -41,15 +60,19 @@ export class WebrtcService {
4160
});
4261
}
4362
};
63+
64+
return Promise.resolve();
4465
} catch (error) {
45-
console.error("Error accessing local media:", error);
46-
throw new Error("Erreur lors de l'accès à la caméra ou au micro");
66+
return Promise.reject("Erreur lors de l'accès à la caméra ou au micro");
4767
}
4868
}
4969

5070
async createOffer(receiverId: number): Promise<void> {
51-
const offer = await this.peer.createOffer();
71+
//Create an SDP offer and set local description
72+
const offer: RTCSessionDescriptionInit = await this.peer.createOffer();
5273
await this.peer.setLocalDescription(offer);
74+
75+
// Send the offer to the signaling server for the receiver.
5376
this.sendSignal({
5477
type: SignalingMessageTypeEnum.OFFER,
5578
payload: JSON.stringify(offer),
@@ -58,19 +81,24 @@ export class WebrtcService {
5881
});
5982
}
6083

61-
async handleSignal(msg: any): Promise<void> {
84+
async createAnswer(signal: VisioSignalMessage): Promise<void> {
85+
// Remote origin is the payload
86+
await this.peer.setRemoteDescription(new RTCSessionDescription(JSON.parse(signal.payload)));
87+
const answer = await this.peer.createAnswer();
88+
await this.peer.setLocalDescription(answer);
89+
this.sendSignal({
90+
type: SignalingMessageTypeEnum.ANSWER,
91+
payload: JSON.stringify(answer),
92+
receiverId: signal.senderId,
93+
senderId: signal.receiverId
94+
});
95+
}
96+
97+
async handleSignalMessage(msg: IMessage): Promise<void> {
6298
const signal: VisioSignalMessage = JSON.parse(msg.body);
6399
switch (signal.type) {
64100
case SignalingMessageTypeEnum.OFFER:
65-
await this.peer.setRemoteDescription(new RTCSessionDescription(JSON.parse(signal.payload)));
66-
const answer = await this.peer.createAnswer();
67-
await this.peer.setLocalDescription(answer);
68-
this.sendSignal({
69-
type: SignalingMessageTypeEnum.ANSWER,
70-
payload: JSON.stringify(answer),
71-
receiverId: signal.senderId,
72-
senderId: this.sessionService.getUser()!.id
73-
});
101+
this.incomingOffer$.next(signal);
74102
break;
75103

76104
case SignalingMessageTypeEnum.ANSWER:
@@ -84,7 +112,27 @@ export class WebrtcService {
84112
}
85113

86114
listenToSignals(userId: number): void {
87-
this.rxStomp.watch(`/topic/visio/${userId}`).subscribe(msg => this.handleSignal(msg));
115+
this.rxStomp.watch(`/topic/visio/${userId}`).subscribe((msg: IMessage) => this.handleSignalMessage(msg));
116+
}
117+
118+
close(): void {
119+
// Stop all local tracks (caméra, micro)
120+
if (this.localStream) {
121+
this.localStream.getTracks().forEach(track => track.stop());
122+
this.localStream = undefined as any;
123+
}
124+
125+
// Stop all remote tracks
126+
if (this.remoteStream) {
127+
this.remoteStream.getTracks().forEach(track => track.stop());
128+
this.remoteStream = undefined as any;
129+
}
130+
131+
// Stock WebRTC connexion
132+
if (this.peer) {
133+
this.peer.close();
134+
this.peer = undefined as any;
135+
}
88136
}
89137

90138
private sendSignal(signal: VisioSignalMessage): void {

0 commit comments

Comments
 (0)