From 3320cbae9188fa7e4ea82010a85adf21a09cac75 Mon Sep 17 00:00:00 2001 From: mateen777 Date: Mon, 21 Oct 2024 12:14:42 +0530 Subject: [PATCH] layout design for video call --- src/app/core/constants/apiRestRequest.ts | 38 +- src/app/core/services/call.service.ts | 2 +- src/app/core/services/http.service.ts | 10 +- .../core/services/mediasoup.service.spec.ts | 16 + src/app/core/services/mediasoup.service.ts | 1090 +++++++++++++++++ src/app/core/services/peerjs.service.ts | 2 +- src/app/core/services/socket.service.ts | 64 +- src/app/core/utils/videoGrid.ts | 102 ++ .../components/consumer/consumer.component.ts | 8 +- .../components/join/join.component.html | 45 +- .../components/join/join.component.ts | 268 +++- .../meeting-room/meeting-room.component.html | 237 ++++ .../meeting-room/meeting-room.component.scss | 122 ++ .../meeting-room.component.spec.ts | 28 + .../meeting-room/meeting-room.component.ts | 1039 ++++++++++++++++ .../modules/join-meeting/joinmeet.routes.ts | 5 +- .../user-video/user-video.component.html | 40 + .../user-video/user-video.component.scss | 135 ++ .../user-video/user-video.component.spec.ts | 21 + .../user-video/user-video.component.ts | 110 ++ src/assets/images/g-profile-pic.png | Bin 0 -> 37125 bytes src/index.html | 3 + src/styles.scss | 95 ++ 23 files changed, 3346 insertions(+), 134 deletions(-) create mode 100644 src/app/core/services/mediasoup.service.spec.ts create mode 100644 src/app/core/services/mediasoup.service.ts create mode 100644 src/app/core/utils/videoGrid.ts create mode 100644 src/app/modules/join-meeting/components/meeting-room/meeting-room.component.html create mode 100644 src/app/modules/join-meeting/components/meeting-room/meeting-room.component.scss create mode 100644 src/app/modules/join-meeting/components/meeting-room/meeting-room.component.spec.ts create mode 100644 src/app/modules/join-meeting/components/meeting-room/meeting-room.component.ts create mode 100644 src/app/shared/components/user-video/user-video.component.html create mode 100644 src/app/shared/components/user-video/user-video.component.scss create mode 100644 src/app/shared/components/user-video/user-video.component.spec.ts create mode 100644 src/app/shared/components/user-video/user-video.component.ts create mode 100644 src/assets/images/g-profile-pic.png diff --git a/src/app/core/constants/apiRestRequest.ts b/src/app/core/constants/apiRestRequest.ts index 911e469..e4e50fc 100644 --- a/src/app/core/constants/apiRestRequest.ts +++ b/src/app/core/constants/apiRestRequest.ts @@ -11,7 +11,39 @@ export enum AuthEndPoints { register = "/users" } -export enum Webrtc { - broadcast = '/broadcast', - consumer = '/consumer' +export enum SocketEvents { + // create a room + CREATE_ROOM = "createRoom", + // join in the room + JOIN_ROOM = "joinRoom", + // new Peer + NEW_PEER = "newPeer", + // get All Peers + GET_PEERS = "getAllPeers", + //get RouterRtpCapabilities of the room + GET_ROUTER_RTPCAPABILITIES = "getRouterRtpCapabilities", + //create createWebRtcTransport transport + CREATE_WEBRTC_TRANSPORT = "createWebRtcTransport", + //connect transport + CONNECT_TRANSPORT = "connectTransport", + //produce + PRODUCE = "produce", + //consume + CONSUME = "consume", + //close the producer of the room + PRODUCER_CLOSED = "producerClosed", + //pause the producer of the room + PAUSE_PRODUCER = "pauseProducer", + //resume the producer of the room + RESUME_PRODUCER = "resumeProducer", + //resume the consumer of the room + RESUME_CONSUMER = "resumeConsumer", + //get producers of the room + GET_PRODUCERS = "getProducers", + //remove the peer of the room for server side event + EXIT_ROOM = "exitRoom", + //remove the peer of the room for client side event + PEER_CLOSED = "peerClosed", + //new producers for room + NEW_PRODUCERS = "newProducers", } \ No newline at end of file diff --git a/src/app/core/services/call.service.ts b/src/app/core/services/call.service.ts index 4298820..2d630ee 100644 --- a/src/app/core/services/call.service.ts +++ b/src/app/core/services/call.service.ts @@ -32,7 +32,7 @@ export class CallService { } getSessionId(): Observable { - return this.http.requestCall('/sessions', ApiMethod.POST); + return this.http.requestCall('/sessions', ApiMethod.POST,{}); } getTokens(sessionId:any,nickname:any):any { diff --git a/src/app/core/services/http.service.ts b/src/app/core/services/http.service.ts index 5c7a3f5..43f4c26 100644 --- a/src/app/core/services/http.service.ts +++ b/src/app/core/services/http.service.ts @@ -11,27 +11,27 @@ export class HttpService { constructor(private http:HttpClient) { } - requestCall(apiEndpoint:any,method:ApiMethod,data?:any){ + requestCall(apiEndpoint:any,method:ApiMethod,body:any,options?:any){ let response:any; switch (method) { case ApiMethod.GET: - response = this.http.get(`${environment.url}${apiEndpoint}`).pipe( + response = this.http.get(`${environment.url}${apiEndpoint}`,options).pipe( catchError((err)=> this.handleError(err))) break; case ApiMethod.POST: - response = this.http.post(`${environment.url}${apiEndpoint}`,data).pipe( + response = this.http.post(`${environment.url}${apiEndpoint}`,body,options).pipe( catchError((err)=> this.handleError(err))) break; case ApiMethod.PUT: - response = this.http.put(`${environment.url}${apiEndpoint}`,data).pipe( + response = this.http.put(`${environment.url}${apiEndpoint}`,body,options).pipe( catchError((err)=> this.handleError(err))) break; case ApiMethod.DELETE: - response = this.http.delete(`${environment.url}${apiEndpoint}`).pipe( + response = this.http.delete(`${environment.url}${apiEndpoint}`,options).pipe( catchError((err)=> this.handleError(err))) break; default: diff --git a/src/app/core/services/mediasoup.service.spec.ts b/src/app/core/services/mediasoup.service.spec.ts new file mode 100644 index 0000000..8660d6d --- /dev/null +++ b/src/app/core/services/mediasoup.service.spec.ts @@ -0,0 +1,16 @@ +/* tslint:disable:no-unused-variable */ + +import { TestBed, async, inject } from '@angular/core/testing'; +import { MediasoupService } from './mediasoup.service'; + +describe('Service: Mediasoup', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [MediasoupService] + }); + }); + + it('should ...', inject([MediasoupService], (service: MediasoupService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/core/services/mediasoup.service.ts b/src/app/core/services/mediasoup.service.ts new file mode 100644 index 0000000..9c1d159 --- /dev/null +++ b/src/app/core/services/mediasoup.service.ts @@ -0,0 +1,1090 @@ +import { Injectable, OnInit, inject } from '@angular/core'; +import { + Device, + RtpCapabilities, + TransportOptions, + Transport, +} from 'mediasoup-client/lib/types'; +import * as mediasoupClient from 'mediasoup-client'; +import { SocketService } from './socket.service'; +import { BehaviorSubject, Subscription, lastValueFrom } from 'rxjs'; +import { SocketEvents } from '../constants/apiRestRequest'; + +const mediaType = { + audio: 'audioType', + audioTab: 'audioTab', + video: 'videoType', + camera: 'cameraType', + screen: 'screenType', + speaker: 'speakerType', +}; + +@Injectable({ + providedIn: 'root', +}) +export class MediasoupService implements OnInit { + triggerStartToProduce = new BehaviorSubject<{ send: boolean; rec: boolean }>({ + send: false, + rec: false, + }); + triggerAspectRation = new BehaviorSubject(false); + triggerHandRaise = new BehaviorSubject(false); + //user related + sharing_enable:boolean = false; + sharing_peer:any; + room_id: string = ''; + localVideo: any; + localAudio: any; + localScreen: any; + userName: string = ''; + isMicOn: boolean = true; + isLobby: boolean = false; + isVideoOn: boolean = true; + isScreenOn: boolean = false; + isVideoAllowed: boolean = false; + isAudioAllowed: boolean = false; + joinRoomWithScreen: boolean = false; + peers: any[] = []; + videoDevices: MediaDeviceInfo[] = []; + audioDevices: MediaDeviceInfo[] = []; + speakers: MediaDeviceInfo[] = []; + + selectedCamera: any; + selectedMic: any; + selectedSpeaker: any; + + //mediasoup retaled + device!: Device; + RouterRtpCapabilitiesData!: RtpCapabilities; + producerTransportData!: TransportOptions; + consumerTransportData!: TransportOptions; + producerTransport!: Transport; + consumerTransport!: Transport; + consumers = new Map(); + producers = new Map(); + + // Encodings + forceVP8 = false; // Force VP8 codec for webcam and screen sharing + forceVP9 = false; // Force VP9 codec for webcam and screen sharing + forceH264 = false; // Force H264 codec for webcam and screen sharing + enableWebcamLayers = true; // Enable simulcast or SVC for webcam + enableSharingLayers = true; // Enable simulcast or SVC for screen sharing + numSimulcastStreamsWebcam = 3; // Number of streams for simulcast in webcam + numSimulcastStreamsSharing = 1; // Number of streams for simulcast in screen sharing + webcamScalabilityMode = 'L3T3'; // Scalability Mode for webcam | 'L1T3' for VP8/H264 (in each simulcast encoding), 'L3T3_KEY' for VP9 + sharingScalabilityMode = 'L1T3'; // Scalability Mode for screen sharing | 'L1T3' for VP8/H264 (in each simulcast encoding), 'L3T3' for VP9 + + // just to test + consumersList: any[] = []; + + //service + socketService = inject(SocketService); + + ngOnInit(): void { + + + } + + async loadDevice() { + try { + this.device = new mediasoupClient.Device(); + await this.device.load({ + routerRtpCapabilities: this.RouterRtpCapabilitiesData, + }); + console.log(this.device.rtpCapabilities); + } catch (error: any) { + if (error.name === 'UnsupportedError') { + console.error('Browser not supported'); + } else { + console.error('Browser not supported: ', error); + } + } + + return this.device; + } + + async initTransports() { + //emit event to backend first to create webrtcTransport at backend side first. + this.socketService.emitSocketEvent(SocketEvents.CREATE_WEBRTC_TRANSPORT, { + forceTcp: false, + rtpCapabilities: this.device.rtpCapabilities, + }) + .subscribe({ + next: async ({ params }: any) => { + this.producerTransportData = params; + this.createSendTransport(); + }, + error: (error: any) => console.log(error), + }); + + this.socketService.emitSocketEvent(SocketEvents.CREATE_WEBRTC_TRANSPORT, { + forceTcp: false, + }) + .subscribe({ + next: ({ params }: any) => { + this.consumerTransportData = params; + this.createRecvTransport(); + }, + error: (error: any) => console.log(error), + }); + + this.triggerStartToProduce.subscribe( + async (res: { send: boolean; rec: boolean }) => { + if (res.send && res.rec) { + console.log(res); + await this.startLocalMedia(); + + // this.socketService.emitSocketEvent(SocketEvents.GET_PEERS,'').subscribe((res:any)=>{ + // if (res.status == "OK") { + // console.log(res,'getpeers evevnt') + // const peersMap = new Map(JSON.parse(res.peers)); + // const peers = Array.from(peersMap.values()); + // this.peers = peers.map((val:any)=>{return { id:val.id,peer_name:val.peer_name }}) + // console.log(this.peers,'oldwajd') + // } + // }) + + this.socketService.emitSocketEvent(SocketEvents.GET_PRODUCERS, '').subscribe( + (res) => console.log(res) + ); + + } + } + ); + + } + + // PRODUCER TRANSPORT + createSendTransport() { + this.producerTransport = this.device.createSendTransport( + this.producerTransportData + ); + + this.producerTransport.on('connect', async ({ dtlsParameters }: any, callback: () => void, errback: (err: any) => void) => { + + this.socketService.emitSocketEvent(SocketEvents.CONNECT_TRANSPORT, { + dtlsParameters, + transport_id: this.producerTransportData.id, + }) + .subscribe({ + next: (res: any) => { + console.log( + 'connected succefully in connect listen sendtransport' + ); + callback(); + }, + error: (err: any) => { + errback(err); + console.log(err); + }, + }); + } + ); + + this.producerTransport.on('produce',async ({ kind, appData, rtpParameters }: any,callback: any,errback: any) => { + console.log('Going to produce', { kind, appData, rtpParameters }); + + this.socketService.emitSocketEvent(SocketEvents.PRODUCE, { + producerTransportId: this.producerTransport.id, + kind, + appData, + rtpParameters, + }) + .subscribe({ + next: ({ producer_id }: any) => { + if (producer_id.error) { + errback(producer_id.error); + } else { + callback({ id: producer_id }); + } + }, + error: (err: any) => { + errback(err); + console.log(err); + }, + }); + } + ); + + this.producerTransport.on('connectionstatechange', (state: any) => { + console.log(state, 'state'); + switch (state) { + case 'connecting': + console.log('Producer Transport connecting...'); + break; + case 'connected': + console.log('Producer Transport connected', { + id: this.producerTransport.id, + }); + break; + case 'failed': + console.log('Producer Transport failed', { + id: this.producerTransport.id, + }); + this.producerTransport.close(); + // this.exit(true); + // this.refreshBrowser(); + break; + default: + break; + } + }); + + this.producerTransport.on('icegatheringstatechange', (state: any) => { + console.log('Producer icegatheringstatechange', state); + }); + + console.log('successfully created send transport'); + this.triggerStartToProduce.next({ send: true, rec: false }); + } + + // #################################################### + // CONSUMER TRANSPORT + // #################################################### + + createRecvTransport() { + this.consumerTransport = this.device.createRecvTransport( + this.consumerTransportData + ); + + this.consumerTransport.on('connect', async ({ dtlsParameters }: any, callback: any, errback: any) => { + + this.socketService.emitSocketEvent(SocketEvents.CONNECT_TRANSPORT, { + dtlsParameters, + transport_id: this.consumerTransport.id, + }) + .subscribe({ + next: (res: any) => { + callback(); + }, + error: (err: any) => { + errback(err); + console.log(err); + }, + }); + } + ); + + this.consumerTransport.on('connectionstatechange', (state: any) => { + switch (state) { + case 'connecting': + console.log('Consumer Transport connecting...'); + break; + case 'connected': + console.log('Consumer Transport connected', { + id: this.consumerTransport.id, + }); + break; + case 'failed': + console.warn('Consumer Transport failed', { + id: this.consumerTransport.id, + }); + this.consumerTransport.close(); + // this.exit(true); + // this.refreshBrowser(); + break; + default: + break; + } + }); + + this.consumerTransport.on('icegatheringstatechange', (state: any) => { + console.log('Consumer icegatheringstatechange', state); + }); + console.log('successfully created revc transport'); + this.triggerStartToProduce.next({ send: true, rec: true }); + } + + async startLocalMedia() { + console.log('Start local media'); + if (this.isAudioAllowed && this.isMicOn) { + console.log('Start audio media'); + const track = this.localAudio?.getAudioTracks()[0]; + await this.produce(mediaType.audio, null, false, false, track); + } else { + console.log('Audio is off'); + // this.updatePeerInfo(this.peer_name, this.peer_id, 'audio', false); + } + if (this.isVideoAllowed && this.isVideoOn) { + console.log('Start video media'); + const track = this.localVideo?.getVideoTracks()[0]; + await this.produce(mediaType.video, null, false, false, track); + } else { + console.log('Video is off'); + + // this.updatePeerInfo(this.peer_name, this.peer_id, 'video', false); + } + + if (this.joinRoomWithScreen) { + console.log('Start Screen media'); + await this.produce(mediaType.screen, null, false, true, this.localScreen); + } + } + + // #################################################### + // PRODUCER + // #################################################### + + async produce( + type: any, + deviceId = null, + swapCamera = false, + init = false, + track: any + ) { + let mediaConstraints = {}; + let audio = false; + let screen = false; + + if (!this.device.canProduce('video') && type == mediaType.video) { + console.log(this.device, 'device'); + return console.error('Cannot produce video'); + } + + if (!this.device.canProduce('audio') && type == mediaType.audio) { + console.log(this.device, 'device'); + return console.error('Cannot produce audio'); + } + + try { + const params: any = { + track, + appData: { + mediaType: type, + }, + }; + + if (mediaType.audio == type) { + console.log('AUDIO ENABLE OPUS'); + params.codecOptions = { + opusStereo: true, + opusDtx: true, + opusFec: true, + opusNack: true, + }; + } + + if (mediaType.audio != type && mediaType.screen != type) { + const { encodings, codec } = this.getWebCamEncoding(); + console.log('GET WEBCAM ENCODING', { + encodings: encodings, + codecs: codec, + }); + params.encodings = encodings; + // params.codecs = codec; + params.codecOptions = { + videoGoogleStartBitrate: 1000, + }; + } + + if (mediaType.audio != type && mediaType.screen == type) { + const { encodings, codec } = this.getScreenEncoding(); + console.log('GET SCREEN ENCODING', { + encodings: encodings, + codecs: codec, + }); + params.encodings = encodings; + params.codecs = codec; + params.codecOptions = { + videoGoogleStartBitrate: 1000, + }; + } + + const producer = await this.producerTransport.produce(params); + + if (!producer) { + throw new Error('Producer not found!'); + } + + console.log('PRODUCER', producer); + + this.producers.set(producer.id, { type: type, producer: producer }); + + // if screen sharing produce the tab audio + microphone + // if (screen && stream.getAudioTracks()[0]) { + // this.produceScreenAudio(stream); + // } + + producer.on('trackended', () => { + console.log('Producer track ended', { id: producer.id }); + this.closeProducer(producer.id); + }); + + producer.on('transportclose', () => { + console.log('Producer transport close', { id: producer.id }); + this.closeProducer(producer.id); + }); + + producer.on('@close', () => { + console.log('@CLose event:- Closing producer', { id: producer.id }); + + }); + + } catch (err: any) { + console.error('Produce error:', err); + + } + } + + + // CONSUMER + + async consume(producer_id: any, peer_name: any, peer_id: any, type: any,peerData?:any) { + try { + const { consumer, stream, kind }: any = await this.getConsumeStream( + producer_id, + peer_id, + type + ); + + console.log('CONSUMER MEDIA TYPE ----> ' + type); + console.log('CONSUMER', consumer); + + console.log(consumer.id,'consumer.id'); + + const data = await lastValueFrom( + this.socketService.emitSocketEvent(SocketEvents.RESUME_CONSUMER, { + consumer_id : consumer.id + }) + ); + console.log(data+' resumed consumer'); + + this.consumers.set(consumer.id, consumer); + + consumer.on('trackended', () => { + console.log('Consumer track end', { id: consumer.id }); + this.removeConsumer(consumer.id, consumer.kind); + }); + + consumer.on('transportclose', () => { + console.log('Consumer transport close', { id: consumer.id }); + this.removeConsumer(consumer.id, consumer.kind); + }); + + console.log(peerData,'peerData') + + this.handleConsumer(type, type == 'audioType' ? peerData.peer_audio ? stream : null : stream, peer_name, peer_id); + } catch (error) { + console.error('Error in consume', error); + } + } + + async getConsumeStream(producerId: any, peer_id: any, type: any) { + const { rtpCapabilities } = this.device; + let res; + // convert observable to promise + const data = await lastValueFrom( + this.socketService.emitSocketEvent(SocketEvents.CONSUME, { + rtpCapabilities, + consumerTransportId: this.consumerTransport.id, + producerId, + }) + ); + console.log('DATA', data); + const { id, kind, rtpParameters } = data; + const codecOptions = {}; + const streamId = + peer_id + (type == 'hvhv' ? '-screen-sharing' : '-mic-webcam'); + const consumer = await this.consumerTransport.consume({ + id, + producerId, + kind, + rtpParameters, + // codecOptions, + streamId, + appData : { peer_id , type } + }); + + const stream = new MediaStream(); + stream.addTrack(consumer.track); + + res = { + consumer, + stream, + kind, + }; + + return res; + } + + handleConsumer(type: string, stream: any, peer_name: any, peer_id: any) { + console.log('PEER-INFO', peer_id); + const index = this.peers.findIndex((obj) => obj.id === peer_id); + + // let track:{video:any,screen:any,audio:any} = {video:undefined,screen:undefined,audio:undefined}; + let track: any = {}; + switch (type) { + case mediaType.video: track.video = stream; + if (index !== -1) this.peers[index].video = stream; + break; + + case mediaType.screen: track.screen = stream; + if (index !== -1){ + this.sharing_peer = { + ...this.peers[index], + ...track, + video:null, + audio:null + } + this.sharing_enable = true; + } + + break; + + case mediaType.audio: track.audio = stream; + if (index !== -1) this.peers[index].audio = stream; + break; + + default: + break; + } + + + if (type != 'screenType') { + // If object with the given ID is found + if (index !== -1) { + // Modify the object at the found index with new data + // this.peers[index] = { ...this.peers[index], ...track }; + } else { + // this.consumersList.push({ + // id: peer_id, + // peer_name: peer_name, + // ...track, + // }); + } + } else { + if (index > -1) { + // this.peers.unshift({ + // ...this.peers[index], + // ...track, + // }); + // this.sharing_peer = { + // ...this.peers[index], + // ...track, + // video:null, + // audio:null + // } + // this.sharing_enable = true; + } + } + this.triggerAspectRation.next(true); + console.log( + this.peers, + 'peers', + 'myid', + this.socketService.socketId + ); + } + + removeConsumer(consumer_id: any, consumer_kind: any) { + + const consumer = this.consumers.get(consumer_id); + if (!consumer) return; + + const { appData } = consumer; + console.log('Remove consumer', { + consumer_id: consumer_id, + consumer_kind: consumer_kind, + },appData); + let peer = this.peers.find((val) => val.id === appData.peer_id); + + console.log(peer,'peer') + if ((consumer.kind == 'audio' || appData.type == 'audioType') && peer) { + this.stopTracks(peer.audio); + peer.audio = null; + + } + if ((consumer.kind == 'video' || appData.type == 'videoType') && peer) { + this.stopTracks(peer.video); + peer.video = null; + } + + if (appData.type == 'screenType') { + // this.stopTracks(peer.screen); + // peer.screen = null; + // this.peers.splice(0,1); + + this.stopTracks(this.sharing_peer.screen); + this.sharing_enable = false; + this.sharing_peer = null; + console.log(this.sharing_peer, 'sharing_peer'); + this.triggerAspectRation.next(true); + } + console.log(this.peers, 'peers'); + + consumer.close(); + this.consumers.delete(consumer_id); + } + + pauseConsumer(consumer_id: any, consumer_kind: any) { + + const consumer = this.consumers.get(consumer_id); + if (!consumer) return; + + const { appData } = consumer; + console.log('Pause consumer', { + consumer_id: consumer_id, + consumer_kind: consumer_kind, + },appData); + let peer = this.peers.find((val) => val.id === appData.peer_id); + + console.log(peer,'peer') + consumer.pause(); + + if ((consumer.kind == 'audio' || appData.type == 'audioType') && peer) { + this.stopTracks(peer.audio); + peer.audio = null; + } + if ((consumer.kind == 'video' || appData.type == 'videoType') && peer) { + this.stopTracks(peer.video); + peer.video = null; + } + + if (appData.type == 'screenType') { + // this.stopTracks(peer.screen); + // peer.screen = null; + // this.peers.splice(0,1); + + this.stopTracks(this.sharing_peer.screen); + this.sharing_enable = false; + this.sharing_peer = null; + this.triggerAspectRation.next(true); + } + console.log(this.peers, 'peers'); + + } + + resumeConsumer(consumer_id: any, consumer_kind: any) { + + const consumer = this.consumers.get(consumer_id); + if (!consumer) return; + + const { appData } = consumer; + console.log('Resume consumer', { + consumer_id: consumer_id, + consumer_kind: consumer_kind, + },appData); + let peer = this.peers.find((val) => val.id === appData.peer_id); + + console.log(peer,'peer') + consumer.resume(); + const stream = new MediaStream(); + stream.addTrack(consumer.track); + + console.log(consumer.track,'track',stream) + + if ((consumer.kind == 'audio' || appData.type == 'audioType') && peer) { + peer.audio = stream; + } + if ((consumer.kind == 'video' || appData.type == 'videoType') && peer) { + peer.video = stream; + } + + // if (appData.type == 'screenType') { + // peer.screen = stream; + // this.triggerAspectRation.next(true); + // } + console.log(this.peers, 'peers'); + + } + + handleNewProducers = async (data: any) => { + console.log('handleNewProducers', data); + if (data?.length > 0) { + console.log('SocketOn New producers', data); + + for (let { producer_id, peer_name, peer_id, type,peer_audio,peer_video } of data) { + if (peer_id != this.socketService.socketId) { + await this.consume(producer_id, peer_name, peer_id, type,{peer_audio,peer_video}); + } else { + switch (type) { + case mediaType.audio: + this.handleConsumer(type, this.localAudio, peer_name, peer_id); + break; + case mediaType.video: + this.handleConsumer(type, this.localVideo, peer_name, peer_id); + break; + case mediaType.screen: + this.handleConsumer(type, this.localScreen, peer_name, peer_id); + break; + default: + break; + } + + // this.consumersList.push({ + // id:peer_id, + // peer_name:peer_name, + // audio:this.localAudio, + // video:this.localVideo, + // screen:this.localScreen, + // }) + } + } + } + }; + + handleConsumerClosed = ({ consumer_id, consumer_kind}: any) => { + console.log('SocketOn Closing consumer', { consumer_id, consumer_kind }); + this.removeConsumer(consumer_id, consumer_kind); + }; + + getWebCamEncoding() { + let encodings; + let codec; + + console.log('WEBCAM ENCODING', { + forceVP8: this.forceVP8, + forceVP9: this.forceVP9, + forceH264: this.forceH264, + numSimulcastStreamsWebcam: this.numSimulcastStreamsWebcam, + enableWebcamLayers: this.enableWebcamLayers, + webcamScalabilityMode: this.webcamScalabilityMode, + }); + + if (this.forceVP8) { + codec = this.device.rtpCapabilities.codecs?.find( + (c) => c.mimeType.toLowerCase() === 'video/vp8' + ); + if (!codec) + throw new Error('Desired VP8 codec+configuration is not supported'); + } else if (this.forceH264) { + codec = this.device.rtpCapabilities.codecs?.find( + (c) => c.mimeType.toLowerCase() === 'video/h264' + ); + if (!codec) + throw new Error('Desired H264 codec+configuration is not supported'); + } else if (this.forceVP9) { + codec = this.device.rtpCapabilities.codecs?.find( + (c) => c.mimeType.toLowerCase() === 'video/vp9' + ); + if (!codec) + throw new Error('Desired VP9 codec+configuration is not supported'); + } + + if (this.enableWebcamLayers) { + console.log('WEBCAM SIMULCAST/SVC ENABLED'); + + const firstVideoCodec = this.device.rtpCapabilities.codecs?.find( + (c) => c.kind === 'video' + ); + console.log('WEBCAM ENCODING: first codec available', { + firstVideoCodec: firstVideoCodec, + }); + + // If VP9 is the only available video codec then use SVC. + if ( + (this.forceVP9 && codec) || + firstVideoCodec?.mimeType.toLowerCase() === 'video/vp9' + ) { + console.log('WEBCAM ENCODING: VP9 with SVC'); + encodings = [ + { + maxBitrate: 5000000, + scalabilityMode: this.webcamScalabilityMode || 'L3T3_KEY', + }, + ]; + } else { + console.log('WEBCAM ENCODING: VP8 or H264 with simulcast'); + encodings = [ + { + scaleResolutionDownBy: 1, + maxBitrate: 5000000, + scalabilityMode: this.webcamScalabilityMode || 'L1T3', + }, + ]; + if (this.numSimulcastStreamsWebcam > 1) { + encodings.unshift({ + scaleResolutionDownBy: 2, + maxBitrate: 1000000, + scalabilityMode: this.webcamScalabilityMode || 'L1T3', + }); + } + if (this.numSimulcastStreamsWebcam > 2) { + encodings.unshift({ + scaleResolutionDownBy: 4, + maxBitrate: 500000, + scalabilityMode: this.webcamScalabilityMode || 'L1T3', + }); + } + } + } + return { encodings, codec }; + } + + getScreenEncoding() { + let encodings; + let codec; + + console.log('SCREEN ENCODING', { + forceVP8: this.forceVP8, + forceVP9: this.forceVP9, + forceH264: this.forceH264, + numSimulcastStreamsSharing: this.numSimulcastStreamsSharing, + enableSharingLayers: this.enableSharingLayers, + sharingScalabilityMode: this.sharingScalabilityMode, + }); + + if (this.forceVP8) { + codec = this.device.rtpCapabilities.codecs?.find((c) => c.mimeType.toLowerCase() === 'video/vp8'); + if (!codec) throw new Error('Desired VP8 codec+configuration is not supported'); + } else if (this.forceH264) { + codec = this.device.rtpCapabilities.codecs?.find((c) => c.mimeType.toLowerCase() === 'video/h264'); + if (!codec) throw new Error('Desired H264 codec+configuration is not supported'); + } else if (this.forceVP9) { + codec = this.device.rtpCapabilities.codecs?.find((c) => c.mimeType.toLowerCase() === 'video/vp9'); + if (!codec) throw new Error('Desired VP9 codec+configuration is not supported'); + } + + if (this.enableSharingLayers) { + console.log('SCREEN SIMULCAST/SVC ENABLED'); + + const firstVideoCodec = this.device.rtpCapabilities.codecs?.find((c) => c.kind === 'video'); + console.log('SCREEN ENCODING: first codec available', { firstVideoCodec: firstVideoCodec }); + + // If VP9 is the only available video codec then use SVC. + if ((this.forceVP9 && codec) || firstVideoCodec?.mimeType.toLowerCase() === 'video/vp9') { + console.log('SCREEN ENCODING: VP9 with SVC'); + encodings = [ + { + maxBitrate: 5000000, + scalabilityMode: this.sharingScalabilityMode || 'L3T3', + dtx: true, + }, + ]; + } else { + console.log('SCREEN ENCODING: VP8 or H264 with simulcast.'); + encodings = [ + { + scaleResolutionDownBy: 1, + maxBitrate: 5000000, + scalabilityMode: this.sharingScalabilityMode || 'L1T3', + dtx: true, + }, + ]; + if (this.numSimulcastStreamsSharing > 1) { + encodings.unshift({ + scaleResolutionDownBy: 2, + maxBitrate: 1000000, + scalabilityMode: this.sharingScalabilityMode || 'L1T3', + dtx: true, + }); + } + if (this.numSimulcastStreamsSharing > 2) { + encodings.unshift({ + scaleResolutionDownBy: 4, + maxBitrate: 500000, + scalabilityMode: this.sharingScalabilityMode || 'L1T3', + dtx: true, + }); + } + } + } + return { encodings, codec }; +} + + + closeProducer(producerId: string) { + if (!this.producers.has(producerId)) { + return console.log('There is no producer'); + } + + const { type, producer }: any = this.producers.get(producerId); + + const data = { + peer_name: this.userName, + producer_id: producerId, + type: type, + status: false, + }; + console.log('peer_id',this.socketService.getSocket.ioSocket.id) + console.log('Close producer', data); + + producer.close(); + this.socketService.emitSocketEvent(SocketEvents.PRODUCER_CLOSED, data).subscribe((res:any)=> console.log(res)); + + let peer = this.peers.find((val) => val.id === this.socketService.getSocket.ioSocket.id); + + if (type == 'audioType') { + this.stopTracks(this.localAudio); + this.localAudio = null; + peer.audio = null; + } + if (type == 'videoType') { + this.stopTracks(this.localVideo); + this.localVideo = null; + peer.video = null; + } + if (type == 'screenType') { + // this.stopTracks(this.localScreen); + // this.localScreen = null; + // this.peers[0].screen = null; + // this.peers.splice(0,1); + + this.stopTracks(this.localScreen); + this.sharing_enable = false; + this.localScreen = null; + this.sharing_peer = null; + this.triggerAspectRation.next(true); + console.log(this.sharing_peer, 'sharing_peer'); + } + + this.producers.delete(producerId); + } + + getProducerIdByType(type: string): any { + for (const [id, data] of this.producers.entries()) { + if (data.type === type) { + return id; + } + } + return null; // Return null if no matching type is found + } + + getProducerByType(type: string): any { + for (const [id, data] of this.producers.entries()) { + if (data.type === type) { + return data.producer; + } + } + return null; // Return null if no matching type is found + } + + async stopTracks(stream: any) { + if(!stream) return; + stream.getTracks().forEach((track: any) => { + track.stop(); + }); + } + + async changeWebcam() + { + try{ + const track = this.localVideo?.getVideoTracks()[0]; + const producer = this.getProducerByType('videoType'); + + await producer.replaceTrack({ track }); + } + catch (error) + { + console.error('changeWebcam() | failed: %o', error); + } + } + + async muteMic() + { + const producer = this.getProducerByType('audioType'); + + producer.pause(); + + try + { + this.socketService.emitSocketEvent(SocketEvents.PAUSE_PRODUCER, { producer_id: producer.id }).subscribe({ + next: async (message: any) => { + console.log(message) + this.isMicOn = !this.isMicOn; + this.handleConsumer('audioType',null,this.userName,this.socketService.socketId); + }, + error: (error: any) => console.log(error), + }); + } + catch (error) + { + console.error('muteMic() | failed: %o', error); + } + } + + async unMuteMic() + { + const producer = this.getProducerByType('audioType'); + + // const track = this.localAudio?.getAudioTracks()[0]; + // await producer.replaceTrack({ track }); + producer.resume(); + + try + { + this.socketService.emitSocketEvent(SocketEvents.RESUME_PRODUCER, { producer_id: producer.id }).subscribe({ + next: async (message: any) => { + console.log(message) + this.isMicOn = !this.isMicOn; + this.handleConsumer('audioType',this.localAudio,this.userName,this.socketService.socketId); + }, + error: (error: any) => console.log(error), + }); + } + catch (error) + { + console.error('unmuteMic() | failed: %o', error); + } + } + + handlePeerClosed(peer_id:string){ + + const index = this.peers.findIndex((peer:any) => peer.id == peer_id); + console.log(index,'index') + if (index > -1) { + this.peers.splice(index,1); + console.log(this.peers,'peers') + console.log(this.consumers,'consumers'); + this.triggerAspectRation.next(true); + } + + } + + // getRandomLightColor() { + // const letters = '0123456789ABCDEF'; + // let color = '#'; + // for (let i = 0; i < 6; i++) { + // color += letters[Math.floor(Math.random() * 16)]; + // } + // return color; + // } + + clean = () => { + // this._isConnected = false; + return new Promise(async (resolve,reject)=>{ + + if (this.consumerTransport) this.consumerTransport.close(); + if (this.producerTransport) this.producerTransport.close(); + const socket = this.socketService.getSocket; + socket.removeListener('disconnect'); + socket.removeListener('newProducers'); + socket.removeListener('consumerClosed'); + socket.removeListener('newPeer'); + + this.room_id = ''; + if (this.localVideo) { + this.stopTracks(this.localVideo); + this.localVideo = null; + }else{ this.localVideo = null} + + if (this.localAudio) { + this.stopTracks(this.localAudio); + this.localAudio = null; + }else{ this.localAudio = null;} + + if (this.localScreen) { + this.stopTracks(this.localAudio); + this.localScreen = null; + }else{ this.localScreen = null;} + + + this.userName = ''; + this.isMicOn = false; + this.isVideoOn = false; + this.peers = []; + this.consumersList = []; + + // this.device = null; + // this.RouterRtpCapabilitiesData = null; + // this.producerTransportData = null; + // this.consumerTransportData = null; + // this.producerTransport = null; + // this.consumerTransport = null; + + console.log(this.peers); + resolve(true); + }) + + }; +} diff --git a/src/app/core/services/peerjs.service.ts b/src/app/core/services/peerjs.service.ts index 699fe4e..88fcdd5 100644 --- a/src/app/core/services/peerjs.service.ts +++ b/src/app/core/services/peerjs.service.ts @@ -1,7 +1,7 @@ import { Injectable, OnInit, inject } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; import { HttpService } from './http.service'; -import { ApiMethod, Webrtc } from '../constants/apiRestRequest'; +import { ApiMethod } from '../constants/apiRestRequest'; import { Peer } from "peerjs"; import { Router } from '@angular/router'; diff --git a/src/app/core/services/socket.service.ts b/src/app/core/services/socket.service.ts index 8e5c414..11f616b 100644 --- a/src/app/core/services/socket.service.ts +++ b/src/app/core/services/socket.service.ts @@ -3,7 +3,9 @@ import { BehaviorSubject, Observable } from 'rxjs'; // import { io } from 'socket.io-client'; import { Socket } from 'ngx-socket-io'; import { HttpService } from './http.service'; -import { ApiMethod, Webrtc } from '../constants/apiRestRequest'; +import { ApiMethod } from '../constants/apiRestRequest'; +import { MediasoupService } from './mediasoup.service'; +import { ActivatedRoute } from '@angular/router'; @Injectable({ providedIn: 'root' @@ -26,7 +28,7 @@ export class SocketService implements OnInit{ // private socket:any; ngOnInit(): void { - console.log('initSocket started'); + console.log('initSocket started',this.socket); // this.initSocketConnection(); } @@ -52,17 +54,26 @@ export class SocketService implements OnInit{ return this.socket; } + get socketId(){ + return this.socket.ioSocket.id; + } + // this method is used to start connection/handhshake of socket with server connectSocket(message:any) { this.socket.emit('connect', message); } // this method is used to get response from server - connectEvent() { - return this.socket.fromEvent('connect'); - } - disconnectEvent() { - return this.socket.fromEvent('disconnect'); +// connectEvent() { +// return this.socket.fromEvent('connect'); +// } +// disconnectEvent() { +// return this.socket.fromEvent('disconnect'); +// } + + userJoinedEvent(roomId:string) { + const eventName = roomId + 'joinedinroom'; + return this.socket.fromEvent(eventName); } @@ -73,37 +84,22 @@ export class SocketService implements OnInit{ sendMessage(){ - this.socket.emit('check','dwadaw') - } - - // socket!:any; - - public joinRoom(payload:any) { - this.socket.emit('room:join', payload); + this.socket.emit('joinRoom','awad',(response:any) => { + console.log(response); + }) } - public confirmationFromRoomJoin = () => { - this.socket.on('room:join', (message:any) =>{ - this.message$.next(message); - }); - - return this.message$.asObservable(); - }; + fromEvent(eventName:string) { + return this.socket.fromEvent(eventName); + } - public userJoined = () => { - this.socket.on('user:joined', (user:any) =>{ - this.userJoined$.next(user); + emitSocketEvent(eventName: string, payload: any, extras?: any): Observable { + return new Observable((observer) => { + this.socket.emit(eventName, payload, (response: any) => { + observer.next(response); // Emit the response to observers + observer.complete(); // Complete the observable + }); }); - - return this.userJoined$.asObservable(); - }; - - broadcast(payload:any):Observable{ - return this.http.requestCall(Webrtc.broadcast,ApiMethod.POST,payload); - } - - consumer(payload:any):Observable{ - return this.http.requestCall(Webrtc.consumer,ApiMethod.POST,payload); } diff --git a/src/app/core/utils/videoGrid.ts b/src/app/core/utils/videoGrid.ts new file mode 100644 index 0000000..3e0d31d --- /dev/null +++ b/src/app/core/utils/videoGrid.ts @@ -0,0 +1,102 @@ + +// RESPONSIVE PARTICIPANTS VIEW + +import { ElementRef, QueryList } from "@angular/core"; + +let customRatio:boolean = true; + +// aspect 0 1 2 3 4 +let ratios:string[] = ['0:0', '4:3', '16:9', '1:1', '1:2']; +let aspect:number = 2; + +let ratio:number = getAspectRatio(); + +function getAspectRatio() { + customRatio = aspect == 0 ? true : false; + let ratio:string[] = ratios[aspect].split(':'); + return Number(ratio[1]) / Number(ratio[0]); +} + +function setAspectRatio(i:number) { + aspect = i; + ratio = getAspectRatio(); + // resizeVideoMedia(); +} + +function Area(Increment:number, Count:number, Width:number, Height:number, Margin = 10) { + ratio = customRatio ? 0.75 : ratio; + let i = 0; + let w = 0; + let h = Increment * ratio + Margin * 2; + while (i < Count) { + if (w + Increment > Width) { + w = 0; + h = h + Increment * ratio + Margin * 2; + } + w = w + Increment + Margin * 2; + i++; + } + if (h > Height) return false; + else return Increment; +} + +function resizeVideoMedia(videoContainer:ElementRef,videos:QueryList) { + let Margin = 4; + + let Width = videoContainer?.nativeElement.offsetWidth - Margin * 2; + let Height = videoContainer?.nativeElement.offsetHeight - Margin * 2; + let max = 0; + let optional = videos.length <= 2 ? 1 : 0; + // let isOneVideoElement = videos.length - optional == 1 ? true : false; + let isOneVideoElement = videos.length == 1; + + // full screen mode + let bigWidth = Width * 4; + if (isOneVideoElement) { + Width = Width - bigWidth; + } + + // loop (i recommend you optimize this) + let i = 1; + while (i < 5000) { + let w = Area(i, videos.length, Width, Height, Margin); + if (w === false) { + max = i - 1; + break; + } + i++; + } + + max = max - Margin * 2; + setWidth(videos, max, bigWidth, Margin, Height, isOneVideoElement); + // document.documentElement.style.setProperty('--vmi-wh', max / 3 + 'px'); +} + +function setWidth(videos:QueryList, width:number, bigWidth:number, margin:number, maxHeight:number, isOneVideoElement:boolean) { + ratio = customRatio ? 0.68 : ratio; + + videos.forEach((video: ElementRef, index: number) => { + const nativeElement = video.nativeElement; + + nativeElement.style.width = width + 'px'; + nativeElement.style.margin = margin + 'px'; + nativeElement.style.height = width * ratio + 'px'; + + if (isOneVideoElement) { + nativeElement.style.width = bigWidth + 'px'; + nativeElement.style.height = bigWidth * ratio + 'px'; + let camHeigh = nativeElement.style.height.substring(0, nativeElement.style.height.length - 2); + if (camHeigh >= maxHeight) nativeElement.style.height = maxHeight - 2 + 'px'; + } + }); +} + +// BREAKPOINTS + +const MOBILE_BREAKPOINT = 500; +const TABLET_BREAKPOINT = 580; +const DESKTOP_BREAKPOINT = 730; +const CUSTOM_BREAKPOINT = 680; + + +export { resizeVideoMedia, setAspectRatio }; \ No newline at end of file diff --git a/src/app/modules/join-meeting/components/consumer/consumer.component.ts b/src/app/modules/join-meeting/components/consumer/consumer.component.ts index 593dfe3..3701c31 100644 --- a/src/app/modules/join-meeting/components/consumer/consumer.component.ts +++ b/src/app/modules/join-meeting/components/consumer/consumer.component.ts @@ -98,11 +98,11 @@ async handleonnegotiationneeded(peer:any) { // body: JSON.stringify(payload), // })).json(); console.log(peer.localDescription,'localDescription'); - this.socketService.consumer(payload).subscribe((response:any)=>{ + // this.socketService.consumer(payload).subscribe((response:any)=>{ - console.log(response.sdp,'from server consumer'); - peer.setRemoteDescription(new RTCSessionDescription(response.sdp)); - }) + // console.log(response.sdp,'from server consumer'); + // peer.setRemoteDescription(new RTCSessionDescription(response.sdp)); + // }) } diff --git a/src/app/modules/join-meeting/components/join/join.component.html b/src/app/modules/join-meeting/components/join/join.component.html index 216e6c0..399087e 100644 --- a/src/app/modules/join-meeting/components/join/join.component.html +++ b/src/app/modules/join-meeting/components/join/join.component.html @@ -41,26 +41,26 @@

Meet People

more_vert
- -
- {{'Do you want people to see and hear you in the meeting?'}} - {{'Do you want people to see you in the meeting?'}} - {{'Do you want people to hear you in the meeting?'}} -
- -
@@ -107,7 +107,20 @@

Meet People

Ready to join?

-

No one else is here

+

No one else is here

+
+ + + + + +
diff --git a/src/app/modules/join-meeting/components/join/join.component.ts b/src/app/modules/join-meeting/components/join/join.component.ts index 58a3954..3233c9b 100644 --- a/src/app/modules/join-meeting/components/join/join.component.ts +++ b/src/app/modules/join-meeting/components/join/join.component.ts @@ -1,32 +1,33 @@ //Angular imports -import { AfterViewInit, Component, HostListener, OnInit, inject } from '@angular/core'; +import { AfterViewInit, Component, DestroyRef, HostListener, OnDestroy, OnInit, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; +import { SocketEvents } from '../../../../core/constants/apiRestRequest'; //rxjs imports -import { Subject, forkJoin, fromEvent, merge, of, timer } from 'rxjs'; -import { debounceTime, filter, switchMap, take, takeUntil, tap } from 'rxjs/operators'; +import { Subject, Subscription, forkJoin, of, } from 'rxjs'; +import { filter, switchMap, tap } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; // primeNg imports import { ButtonModule } from 'primeng/button'; import { TooltipModule } from 'primeng/tooltip'; import { DropdownModule } from 'primeng/dropdown'; import { DialogModule } from 'primeng/dialog'; +import { AvatarModule } from 'primeng/avatar'; +import { AvatarGroupModule } from 'primeng/avatargroup'; // Service imports import { SocketService } from 'src/app/core/services/socket.service'; -import { PeerjsService } from 'src/app/core/services/peerjs.service'; import { CallService } from 'src/app/core/services/call.service'; - -//delete below line -import { TokenModel, RecordingInfo, BroadcastingError, OpenViduAngularModule, BroadcastingService, ParticipantService, RecordingService } from 'openvidu-angular'; -import { FormsModule } from '@angular/forms'; import { NavigatorService } from 'src/app/core/services/navigator.service'; +import { MediasoupService } from 'src/app/core/services/mediasoup.service'; @Component({ selector: 'app-join', standalone: true, - imports: [CommonModule,FormsModule,ButtonModule,TooltipModule,DropdownModule,DialogModule], + imports: [CommonModule,FormsModule,ButtonModule,TooltipModule,DropdownModule,DialogModule,AvatarModule,AvatarGroupModule], templateUrl: './join.component.html', styleUrls: ['./join.component.scss'], host:{ @@ -34,19 +35,21 @@ import { NavigatorService } from 'src/app/core/services/navigator.service'; '(window:keydown.control.e)':'handleKeyPress($event)' } }) -export class JoinComponent implements OnInit,AfterViewInit{ +export class JoinComponent implements OnInit,AfterViewInit,OnDestroy { // @ViewChild('video',{static:true}) video!:HTMLVideoElement; private keyDownSubject = new Subject(); remoteSocketId:any = null; socketService = inject(SocketService); + mediasoupService = inject(MediasoupService); router = inject(Router); activatedRouter = inject(ActivatedRoute); callService = inject(CallService); nS = inject(NavigatorService); + destroyRef = inject(DestroyRef) - + room_id:string = ''; remoteVideo:any; remoteAudio:any; userName:string = ''; @@ -56,6 +59,7 @@ export class JoinComponent implements OnInit,AfterViewInit{ isAudioAllowed:boolean = false; askPermissionModal:boolean = false; permissionDeniedModal:boolean = false; + peerclosedSubscription!:Subscription; videoDevices: MediaDeviceInfo[] = []; audioDevices: MediaDeviceInfo[] = []; @@ -75,11 +79,22 @@ export class JoinComponent implements OnInit,AfterViewInit{ ]; + constructor(){ + this.mediasoupService.isLobby = true; + } + async ngOnInit() { + this.activatedRouter.params.subscribe((params:any)=>{ + if (params && params.id) { + this.room_id = params.id + this.mediasoupService.room_id = this.room_id; + } + }) this.activatedRouter.queryParams.subscribe((qparams:any)=>{ if (qparams && qparams.userName) { this.userName = qparams.userName + this.mediasoupService.userName = qparams.userName } }) @@ -96,15 +111,62 @@ export class JoinComponent implements OnInit,AfterViewInit{ this.toggleVideo(event); }) - this.socketService.connectEvent().subscribe((socket:any)=>{ + this.socketService.emitSocketEvent(SocketEvents.CREATE_ROOM,{ room_id :this.room_id,userName:this.userName}).subscribe({ + next: async (response:any)=> { + console.log(response); + const { isExist, message, room_info, status } = response; + + if (status == "OK") { + console.log(JSON.parse(room_info.peers),'room_info.peers'); + // const peersMap = new Map(JSON.parse(room_info.peers)); + // const peers = Array.from(peersMap.values()); + + // console.log(peers,'peers'); + + this.mediasoupService.peers = JSON.parse(room_info.peers); + console.log(message,this.mediasoupService.peers); + }else{ + console.log(message,room_info); + } + + this.getRtpCapabilities(); + }, + error:(error:any) => console.log(error) + }) + + this.socketService.fromEvent(SocketEvents.NEW_PEER).subscribe((res:any)=>{ + + const { peer } = res; + console.log(peer,':peer joined into room'); + this.mediasoupService.peers.push(peer); + this.mediasoupService.triggerAspectRation.next(true); + }) - console.log(socket,'connect in join comp') + this.peerclosedSubscription = this.socketService.fromEvent(SocketEvents.PEER_CLOSED + this.room_id).subscribe(({peer_id}:any)=>{ + const socketId = this.socketService.socketId; + console.log('wwwwwwwwwwwwwwwwwwwwwwwwww',peer_id) + let index = this.mediasoupService.peers.findIndex(val => val.id == peer_id); + if (index != -1) { + this.mediasoupService.peers.splice(index,1); + } }) // Call the function to check permissions await this.checkVideoAudioPermissions(); } + async getRtpCapabilities(){ + this.socketService.emitSocketEvent(SocketEvents.GET_ROUTER_RTPCAPABILITIES,'').subscribe({ + next:async (res:any)=> { + console.log(res,'RouterRtpCapabilities') + this.mediasoupService.RouterRtpCapabilitiesData = res; + + await this.mediasoupService.loadDevice(); + }, + error:(error:any) => console.log(error) + }); + } + async checkVideoAudioPermissions() { try { @@ -117,36 +179,51 @@ export class JoinComponent implements OnInit,AfterViewInit{ if (cameraPermissionStatus.state === 'granted' || microphonePermissionStatus.state === 'granted') { if (cameraPermissionStatus.state === 'granted') { - this.isVideoAllowed = true; - this.isVideoOn = true; + this.mediasoupService.isVideoAllowed = true; + this.mediasoupService.isVideoOn = true; this.nS.getVideoDevices().subscribe({ - next:(videoDevices:any)=> { this.videoDevices = videoDevices; this.selectedCamera = this.videoDevices[0]; this.initVideo() }, + next:(videoDevices:any)=> { + this.videoDevices = videoDevices; + this.selectedCamera = this.videoDevices[0]; + this.mediasoupService.videoDevices = videoDevices; + this.mediasoupService.selectedCamera = this.videoDevices[0]; + this.initVideo() + }, error:(error:any) => console.log(error) }) }else{ - this.isVideoAllowed = false; - this.isVideoOn = false; + this.mediasoupService.isVideoAllowed = false; + this.mediasoupService.isVideoOn = false; this.askPermissionModal = true; await this.askPermission(); } if (microphonePermissionStatus.state === 'granted') { - this.isAudioAllowed = true; - this.isMicOn = true; + this.mediasoupService.isAudioAllowed = true; + this.mediasoupService.isMicOn = true; this.nS.getAudioDevices().subscribe({ - next:(audios:any)=> { this.audioDevices = audios; this.selectedMic = this.audioDevices[0]; this.initAudio()}, + next:(audios:any)=> { + this.audioDevices = audios; + this.selectedMic = this.audioDevices[0]; + + this.mediasoupService.audioDevices = audios; + this.mediasoupService.selectedMic = this.audioDevices[0]; + this.initAudio()}, error:(error:any) => console.log(error) }) this.nS.getSpeakers().subscribe({ next:(speakers:any)=> { this.speakers = speakers; this.selectedSpeaker = this.speakers[0]; + + this.mediasoupService.speakers = speakers; + this.mediasoupService.selectedSpeaker = this.speakers[0]; }, error:(error:any) => console.log(error) }) }else{ - this.isAudioAllowed = false; - this.isMicOn = false; + this.mediasoupService.isAudioAllowed = false; + this.mediasoupService.isMicOn = false; this.askPermissionModal = true; await this.askPermission(); @@ -155,22 +232,30 @@ export class JoinComponent implements OnInit,AfterViewInit{ // await this.initVideo(); console.log('Camera and microphone permissions granted.'); }else if(cameraPermissionStatus.state === 'prompt' || microphonePermissionStatus.state === 'prompt'){ - cameraPermissionStatus.state === 'prompt' ? this.isVideoAllowed = false : this.isVideoAllowed = true; - microphonePermissionStatus.state === 'prompt' ? this.isAudioAllowed = false : this.isAudioAllowed = true; + cameraPermissionStatus.state === 'prompt' ? this.mediasoupService.isVideoAllowed = false : this.mediasoupService.isVideoAllowed = true; + microphonePermissionStatus.state === 'prompt' ? this.mediasoupService.isAudioAllowed = false : this.mediasoupService.isAudioAllowed = true; this.askPermissionModal = true; await this.askPermission(); - if (this.isVideoAllowed && this.isAudioAllowed) { + if (this.mediasoupService.isVideoAllowed && this.mediasoupService.isAudioAllowed) { forkJoin([this.nS.getVideoDevices(),this.nS.getAudioDevices(),this.nS.getSpeakers()]).subscribe({ next:([videoDevices,audios,speakers]:any)=> { this.videoDevices = videoDevices; this.audioDevices = audios; - this.speakers = speakers; + this.speakers = speakers; + + this.mediasoupService.videoDevices = videoDevices; + this.mediasoupService.audioDevices = audios; + this.mediasoupService.speakers = speakers; this.selectedCamera = this.videoDevices[0]; this.selectedMic = this.audioDevices[0]; this.selectedSpeaker = this.speakers[0]; + + this.mediasoupService.selectedCamera = this.videoDevices[0]; + this.mediasoupService.selectedMic = this.audioDevices[0]; + this.mediasoupService.selectedSpeaker = this.speakers[0]; this.initVideo(); console.log(videoDevices,audios,speakers,'res') }, @@ -180,10 +265,10 @@ export class JoinComponent implements OnInit,AfterViewInit{ console.log('Camera and microphone permissions need to ask.'); } else { - this.isVideoAllowed = false; - this.isAudioAllowed = false; - this.isMicOn = false; - this.isVideoOn = false; + this.mediasoupService.isVideoAllowed = false; + this.mediasoupService.isAudioAllowed = false; + this.mediasoupService.isMicOn = false; + this.mediasoupService.isVideoOn = false; console.log('Camera and/or microphone permissions are denied.'); } } catch (error) { @@ -193,23 +278,23 @@ export class JoinComponent implements OnInit,AfterViewInit{ async askPermission(){ try { - if (!this.isVideoAllowed && this.isAudioAllowed) { + if (!this.mediasoupService.isVideoAllowed && this.mediasoupService.isAudioAllowed) { let stream = await getMedia({video:true}); - this.isVideoAllowed = true; + this.mediasoupService.isVideoAllowed = true; this.askPermissionModal = false; await this.stopTracks(stream); - }else if (!this.isAudioAllowed && this.isVideoAllowed) { + }else if (!this.mediasoupService.isAudioAllowed && this.mediasoupService.isVideoAllowed) { // await this.initAudio(); let stream = await getMedia({audio:true}); - this.isAudioAllowed = true; + this.mediasoupService.isAudioAllowed = true; this.askPermissionModal = false; await this.stopTracks(stream); }else{ // await this.initVideo(); // await this.initAudio(); let stream = await getMedia({video:true,audio:true}); - this.isVideoAllowed = true; - this.isAudioAllowed = true; + this.mediasoupService.isVideoAllowed = true; + this.mediasoupService.isAudioAllowed = true; this.askPermissionModal = false; await this.stopTracks(stream); } @@ -217,11 +302,11 @@ export class JoinComponent implements OnInit,AfterViewInit{ if (error instanceof DOMException) { this.askPermissionModal = false; if (error.message == 'Permission dismissed') { - if (!this.isVideoAllowed) { - this.isVideoOn = false; + if (!this.mediasoupService.isVideoAllowed) { + this.mediasoupService.isVideoOn = false; } - if (!this.isAudioAllowed) { - this.isMicOn = false; + if (!this.mediasoupService.isAudioAllowed) { + this.mediasoupService.isMicOn = false; } } if (error.message == 'Permission denied') { @@ -254,8 +339,8 @@ async stopTracks(stream:any) { async initVideo() { try { - if (this.remoteVideo) { - await this.stopTracks(this.remoteVideo); + if (this.mediasoupService.localVideo) { + await this.stopTracks(this.mediasoupService.localVideo); } const deviceId = this.selectedCamera.deviceId; const videoConstraints = { @@ -269,8 +354,8 @@ async stopTracks(stream:any) { }; const stream = await navigator.mediaDevices.getUserMedia(videoConstraints); - this.remoteVideo = stream; - console.log(this.remoteVideo,'remoteVideo'); + this.mediasoupService.localVideo = stream; + console.log(this.mediasoupService.localVideo,'remoteVideo'); } catch (error:any) { console.log(error.status,'uoyoyoyo'); console.log(typeof error,'type'); @@ -280,17 +365,23 @@ async stopTracks(stream:any) { async initAudio() { try { - if (this.remoteAudio) { - await this.stopTracks(this.remoteAudio); + if (this.mediasoupService.localAudio) { + await this.stopTracks(this.mediasoupService.localAudio); } const deviceId = this.selectedMic.deviceId; const audioConstraints = { - audio: true, + audio: { + autoGainControl: false, + echoCancellation: false, + noiseSuppression: true, + deviceId: deviceId, + }, + video: false, }; const stream = await navigator.mediaDevices.getUserMedia(audioConstraints); - - this.remoteAudio = stream; - console.log(this.remoteAudio,'remoteAudio'); + + this.mediasoupService.localAudio = stream; + console.log(this.mediasoupService.localAudio,'remoteAudio'); } catch (error:any) { console.log(error.status,'remoteAudio uoyoyoyo'); console.log(typeof error,'remoteAudio type'); @@ -299,16 +390,16 @@ async initAudio() { toggleMic(e:any){ e.preventDefault(); - if (this.isAudioAllowed) { - this.isMicOn = !this.isMicOn; - if (this.isMicOn) { - this.initAudio(); - } else { - if (this.remoteAudio) { - this.stopTracks(this.remoteAudio); - this.remoteAudio = null; - } - } + if (this.mediasoupService.isAudioAllowed) { + this.mediasoupService.isMicOn = !this.mediasoupService.isMicOn; + // if (this.mediasoupService.isMicOn) { + // this.initAudio(); + // } else { + // if (this.mediasoupService.localAudio) { + // this.stopTracks(this.mediasoupService.localAudio); + // this.mediasoupService.localAudio = null; + // } + // } } else { this.permissionDeniedModal = true; } @@ -323,15 +414,15 @@ handleKeyPress(event: KeyboardEvent): void { async toggleVideo(e:any){ e.preventDefault(); - if (this.isVideoAllowed) { - this.isVideoOn = !this.isVideoOn; + if (this.mediasoupService.isVideoAllowed) { + this.mediasoupService.isVideoOn = !this.mediasoupService.isVideoOn; console.log(e) - if (this.isVideoOn) { + if (this.mediasoupService.isVideoOn) { this.initVideo(); } else { - if (this.remoteVideo) { - this.stopTracks(this.remoteVideo); - this.remoteVideo = null; + if (this.mediasoupService.localVideo) { + this.stopTracks(this.mediasoupService.localVideo); + this.mediasoupService.localVideo = null; } } }else{ @@ -340,7 +431,48 @@ async toggleVideo(e:any){ } joinNow(){ - this.socketService.sendMessage() + const payload = { + room_id:this.room_id , userName:this.userName + } + this.socketService.emitSocketEvent(SocketEvents.JOIN_ROOM,payload).subscribe({ + next:({joined,isExist,peer}:any)=> { + if (joined) { + this.mediasoupService.peers.push(peer); + this.router.navigate(['join/room',this.room_id],{ + queryParams:{ userName:peer.peer_name } + }); + console.log('joined',joined,peer) + } + + if (!isExist) { + this.createRoom(); + } + }, + error:(error:any) => console.log(error) + }); } + createRoom(){ + this.socketService.emitSocketEvent(SocketEvents.CREATE_ROOM,{ room_id :this.room_id,userName:this.userName}).subscribe({ + next: async (response:any)=> { + console.log(response); + const { isExist, status, message, room_info} = response; + + if (status == 'OK') { + console.log(JSON.parse(room_info.peers),'room_info.peers'); + this.mediasoupService.peers = JSON.parse(room_info.peers); + console.log(message,this.mediasoupService.peers); + await this.getRtpCapabilities(); + this.joinNow(); + }else{ + console.log(message,room_info); + } + }, + error:(error:any) => console.log(error) + }) + } + + ngOnDestroy(): void { + if (this.peerclosedSubscription) this.peerclosedSubscription.unsubscribe() + } } diff --git a/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.html b/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.html new file mode 100644 index 0000000..401310f --- /dev/null +++ b/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.html @@ -0,0 +1,237 @@ + + + + + +
+ +
+ +
+ + +
+
+ {{mS.sharing_peer.peer_name}} +
+
+
+
+ + +
+
+
+ +
+ + +
+
+
+
+
+
+ + +
+ + +
+ + +
+
+
+
+
+ + + +
+ dawvdjhwvad +
+
+ + {{'you' | titlecase}} +
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ + + +
+
+ +
+
+ + {{ today | date:"h:mm a"}} + +
+ +
+ + + + + + + + + + +
+ + + +
+ + + + +
+ +
+
+ + + + + + + + + + + + + diff --git a/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.scss b/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.scss new file mode 100644 index 0000000..25bd130 --- /dev/null +++ b/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.scss @@ -0,0 +1,122 @@ +.emojies{ + background: #071426; + border-radius: 2.25rem; + display: flex; + // flex-direction: row-reverse; + flex-shrink: 1; + height: 2.5rem; + max-width: 100%; + min-width: 12.5rem; + outline: 1px solid transparent; + outline-offset: -1px; +} + +.main-container { + .user-container { + @apply grid-cols-1; + + // sm + @media screen and (min-width: 640px) { + @apply sm:grid-cols-2; + } + + // md + @media screen and (min-width: 768px) { + @apply md:grid-cols-3; + } + + // lg + @media screen and (min-width: 1024px) { + // Define styles for lg screen size here + } + + // xl + @media screen and (min-width: 1280px) { + // Define styles for xl screen size here + } + + // 2xl + @media screen and (min-width: 1536px) { + // Define styles for 2xl screen size here + } + } +} + + + +// .main-container{ + +// .user-container{ +// @apply grid-cols-1 +// } +// // sm +// @media screen and (min-width: 640px){ +// .main-container > .user-container { +// @apply sm:grid-cols-2 +// } +// } +// // md +// @media screen and (min-width: 768px){ +// .main-container > .user-container { +// @apply md:grid-cols-3 +// } +// } +// // lg +// @media screen and (min-width: 1024px){ + +// } +// // xl +// @media screen and (min-width: 1280px){ + +// } +// // 2xl +// @media screen and (min-width: 1536px){ + +// } +// } + +.users5{ + @apply w-[25rem] h-[16rem] +} +.users5Nav{ + @apply w-[19rem] h-[16rem] +} + +.users6{ + @apply w-[23rem] h-[16rem] +} +.users6Nav{ + @apply w-[20rem] h-[13rem] +} + +.reaction-display { + position: fixed; + bottom: 120px; + // left: 20%; + transform: translateX(-50%); + display: flex; + justify-content: center; + flex-direction: column; + animation: fadeOut 3s linear; + } + + .animated-emoji { + width: 40px; + height: 40px; + margin: 0 5px; + } + + @keyframes fadeOut { + 0% { + opacity: 1; + transform: translateY(0); + } + 80% { + opacity: 1; + // transform: translateY(-420px); + } + 100% { + opacity: 0; + transform: translateY(-420px); + } + } \ No newline at end of file diff --git a/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.spec.ts b/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.spec.ts new file mode 100644 index 0000000..e1a2775 --- /dev/null +++ b/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.spec.ts @@ -0,0 +1,28 @@ +/* tslint:disable:no-unused-variable */ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { MeetingRoomComponent } from './meeting-room.component'; + +describe('MeetingRoomComponent', () => { + let component: MeetingRoomComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MeetingRoomComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MeetingRoomComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.ts b/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.ts new file mode 100644 index 0000000..a508a1b --- /dev/null +++ b/src/app/modules/join-meeting/components/meeting-room/meeting-room.component.ts @@ -0,0 +1,1039 @@ +//Angular imports +import { AfterViewInit, Component, ElementRef, OnDestroy, OnInit, QueryList, ViewChild, ViewChildren, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { fromEvent, Subscription, timer } from 'rxjs'; +import { debounceTime, map, switchMap, take } from 'rxjs/operators'; +import { SocketEvents } from '../../../../core/constants/apiRestRequest'; + +// primeNg imports +import { ButtonModule } from 'primeng/button'; +import { TooltipModule } from 'primeng/tooltip'; +import { DropdownModule } from 'primeng/dropdown'; +import { DialogModule } from 'primeng/dialog'; +import { AvatarModule } from 'primeng/avatar'; +import { AvatarGroupModule } from 'primeng/avatargroup'; +import { Menu, MenuModule } from 'primeng/menu'; + +// service imports +import { MediasoupService } from 'src/app/core/services/mediasoup.service'; +import { NavigatorService } from 'src/app/core/services/navigator.service'; +import { SocketService } from 'src/app/core/services/socket.service'; +import { ConnectionPositionPair } from '@angular/cdk/overlay'; +import { UserVideoComponent } from 'src/app/shared/components/user-video/user-video.component'; + +// utility imports +import { setAspectRatio,resizeVideoMedia } from "../../../../core/utils/videoGrid"; + +@Component({ + selector: 'app-meeting-room', + standalone: true, + imports: [CommonModule,FormsModule,MenuModule,ButtonModule,TooltipModule,DropdownModule,DialogModule,AvatarModule,AvatarGroupModule,UserVideoComponent], + templateUrl: './meeting-room.component.html', + styleUrls: ['./meeting-room.component.scss'], + +}) +export class MeetingRoomComponent implements OnInit,AfterViewInit, OnDestroy { + + @ViewChild('videogridContainer') videoContainer!: ElementRef; + @ViewChildren('videoitem') videoGrid!: QueryList; + + socketService = inject(SocketService); + mS = inject(MediasoupService); + router = inject(Router); + activatedRouter = inject(ActivatedRoute); + nS = inject(NavigatorService); + + today: number = Date.now(); + open:boolean = false; + sideNavOpen:boolean = false; + isEmojiVisible:boolean = false; + isEmojiColorMenuActive:boolean = false; + resizeSubscription$!: Subscription; + emojiIndex: number = 0; + emojiColor: string = '#fec724'; + + items = [ + { + label: 'radio_button_checkedRecord Meeting', + escape: false, + disabled:true, + styleClass:'menu_item', + google_icon:'radio_checked', + command: () => { } + }, + { + label: 'dashboardChange Layout', + escape: false, + disabled:true, + styleClass:'menu_item', + google_icon:'dashboard', + command: () => { } + }, + { + label: 'fullscreenFull Screen', + escape: false, + styleClass:'menu_item', + google_icon:'fullscreen', + command: () => { } + }, + { + label: 'picture_in_pictureOpen picture-in-picture', + escape: false, + disabled:true, + styleClass:'menu_item', + google_icon:'picture_in_picture', + command: () => { } + }, + { + label: 'auto_awesomeApply Visual Effects', + escape: false, + disabled:true, + styleClass:'menu_item', + google_icon:'auto_awesome', + command: () => { } + }, + { + label: 'closed_captionTurn on captions', + escape: false, + disabled:true, + styleClass:'menu_item', + google_icon:'closed_caption', + command: () => { } + }, + { + label: 'settingsSettings', + escape: false, + styleClass:'menu_item', + google_icon:'settings', + command: () => { } + }, + ] + + emojicolorVarients = [ + { + label: '', + // label: '#fec724', + emoji_color: '#fec724', + escape: false, + styleClass:'emojicolor_item', + command: () => { this.setEmojiColor('#fec724',0); } + }, + { + label: '', + // label: '#e2c6a7', + emoji_color: '#e2c6a7', + escape: false, + styleClass:'emojicolor_item', + command: () => { this.setEmojiColor('#e2c6a7',1); } + }, + { + label: '', + // label: '#c7a786', + emoji_color: '#c7a786', + escape: false, + styleClass:'emojicolor_item', + command: () => { this.setEmojiColor('#c7a786',2); } + }, + { + label: '', + // label: '#a68063', + emoji_color: '#a68063', + escape: false, + styleClass:'emojicolor_item', + command: () => { this.setEmojiColor('#a68063',3); } + }, + { + label: '', + emoji_color: '#926241', + escape: false, + styleClass:'emojicolor_item', + command: () => { this.setEmojiColor('#926241',4); } + }, + { + label: '', + emoji_color: '#654c45', + escape: false, + styleClass:'emojicolor_item', + command: () => { this.setEmojiColor('#654c45',5); } + }, + + ] + + emojies:any[] = [ + { + emoji_name:'Sparkling Heart', + emoji: '💖', + animated_emoji : 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f496/512.gif', + }, + { + emoji_name:'Thumbs Up Sign', + emoji: '👍', + emoji_skintone:['👍','👍🏻','👍🏼','👍🏽','👍🏾','👍🏿'], + animated_emoji:[ + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44d/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44d_1f3fb/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44d_1f3fc/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44d_1f3fd/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44d_1f3fe/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44d_1f3ff/512.gif' + ], + }, + { + emoji_name:'Party Popper', + emoji: '🎉', + animated_emoji : 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f389/512.gif' + }, + { + emoji_name:'Clapping Hands', + emoji: '👏', + emoji_skintone:['👏','👏🏻','👏🏼','👏🏽','👏🏾','👏🏿'], + animated_emoji:[ + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44f/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44f_1f3fb/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44f_1f3fc/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44f_1f3fd/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44f_1f3fe/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44f_1f3ff/512.gif' + ] + + }, + { + emoji_name:'Face with Tears of Joy', + emoji: '😂', + animated_emoji:'https://fonts.gstatic.com/s/e/notoemoji/latest/1f602/512.gif' + }, + { + emoji_name:'Face with Open Mouth', + emoji: '😮', + animated_emoji: 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f62e/512.gif' + }, + { + emoji_name:'Crying Face', + emoji: '😢', + animated_emoji: 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f622/512.gif', + }, + { + emoji_name:'Thinking Face', + emoji: '🤔', + animated_emoji:'https://fonts.gstatic.com/s/e/notoemoji/latest/1f914/512.gif', + }, + { + emoji_name:'Thumbs Down', + emoji: '👎', + emoji_skintone:['👎','👎🏻','👎🏼','👎🏽','👎🏾','👎🏿'], + + animated_emoji:[ + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44e/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44e_1f3fb/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44e_1f3fc/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44e_1f3fd/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44e_1f3fe/512.gif', + 'https://fonts.gstatic.com/s/e/notoemoji/latest/1f44e_1f3ff/512.gif' + ] + }, + ] + + constructor(){ + this.mS.isLobby = false; + } + + peerById(index:any, peer:any){ + return peer.id; + } + + ngOnInit() { + + setInterval(() => { + this.today = Date.now(); + }, 1000); + + this.socketService.fromEvent('newProducers').subscribe((data:any)=>{ + console.log('newProducers',data); + this.mS.handleNewProducers(data); + }); + + this.socketService.fromEvent('consumerClosed').subscribe((data:any)=>{ + console.log('consumerClosed'); + this.mS.handleConsumerClosed(data); + }); + + this.socketService.fromEvent('consumerPaused').subscribe((data:any)=>{ + console.log('consumerPaused'); + const { consumer_id, consumer_kind} = data; + this.mS.pauseConsumer(consumer_id,consumer_kind); + }); + + this.socketService.fromEvent('consumerResumed').subscribe((data:any)=>{ + console.log('consumerResumed'); + const { consumer_id, consumer_kind} = data; + this.mS.resumeConsumer(consumer_id,consumer_kind); + }); + + this.socketService.fromEvent('peerClosed').subscribe(({peer_id}:any)=>{ + console.log('peerClosed',peer_id); + this.mS.handlePeerClosed(peer_id); + }); + + this.socketService.fromEvent(this.mS.room_id + 'Room').subscribe((user:any)=>{ + const socketId = this.socketService.socketId + console.log(user,':user joined into room'); + if (user.id != socketId) { + // this.joinedRoomUsers.push(user); + } + }) + + this.mS.triggerAspectRation.subscribe((res:boolean)=>{ + + if (res) { + console.log('triggerAspectRation',res,this.videoContainer); + setTimeout(() => { + if (this.videoContainer) this.handleAspectRatio(); + }, 0); + } + }) + + this.mS.initTransports(); + + this.resizeSubscription$ = fromEvent(window, 'resize').pipe(debounceTime(50)) + .subscribe(event => { + // Handle window resize event here + if (!this.mS.sharing_enable) { + console.log('Window resized:', event); + this.handleAspectRatio(); + } + }); + + } + + ngAfterViewInit(): void { + + this.handleAspectRatio(); + } + + setEmojiColor(color:string,emojiIndex:number){ + this.emojiColor = color; + this.emojiIndex = emojiIndex; + } + + openSideNav(){ + this.sideNavOpen = !this.sideNavOpen; + setTimeout(() => { + this.handleAspectRatio(); + }, 0); + } + + toggleEmoji(){ + this.isEmojiVisible = !this.isEmojiVisible; + setTimeout(() => { + this.handleAspectRatio(); + }, 100); + } + + reactions: any[] = []; + + sendReaction(emoji: any,index:number) { + const src = Array.isArray(emoji.animated_emoji) ? emoji.animated_emoji[this.emojiIndex] : emoji.animated_emoji; + + const emo = { ...emoji,src:src,left: this.getRandomLeftPosition()}; + this.reactions.push(emo); + setTimeout(() => { + this.reactions.shift(); + }, 2900); + } + + getRandomLeftPosition(): string { + const leftPosition = Math.random() * 20; // Generates a number between 0 and 20 + return `${leftPosition}%`; + } + + isHandle:boolean = false; + toggleHand(e:any){ + this.isHandle = !this.isHandle; + this.mS.triggerHandRaise.next(this.isHandle); + } + + show:boolean = false; + async toggleMic(e:any){ + e.preventDefault(); + if (!this.mS.isAudioAllowed){ this.show = true; return ;} + + const audioProducer = this.mS.getProducerByType('audioType'); + if (!audioProducer){ + + const track = this.mS.localAudio?.getAudioTracks()[0]; + await this.mS.produce('audioType', null, false, false, track); + this.mS.isMicOn = !this.mS.isMicOn; + this.mS.handleConsumer('audioType',this.mS.localAudio,this.mS.userName,this.socketService.socketId); + console.log('audio produced'); + + }else{ + if (!this.mS.isMicOn) { + setTimeout(() => { + this.mS.unMuteMic() + }, 100); + }else{ + this.mS.muteMic() + } + } + + } + + async initAudio() { + + try { + if (this.mS.localAudio) { + await this.stopTracks(this.mS.localAudio); + } + const deviceId = this.mS.selectedMic.deviceId; + const audioConstraints = { + audio: { + echoCancellation: true, + noiseSuppression: true, + deviceId: deviceId, + }, + // audio: true, + }; + const stream = await navigator.mediaDevices.getUserMedia(audioConstraints); + + this.mS.localAudio = stream; + console.log(this.mS.localAudio,'remoteAudio'); + } catch (error:any) { + console.log(error.status,'remoteAudio uoyoyoyo'); + console.log(typeof error,'remoteAudio type'); + } + } + + async toggleVideo(e:any){ + e.preventDefault(); + if (this.mS.isVideoAllowed) { + this.mS.isVideoOn = !this.mS.isVideoOn; + const producerId = this.mS.getProducerIdByType('videoType'); + + if (this.mS.isVideoOn && !producerId) { + await this.initVideo(); + + const track = this.mS.localVideo?.getVideoTracks()[0]; + + await this.mS.produce('videoType', null, false, false, track); + + console.log('meeting room'); + + this.mS.handleConsumer('videoType',this.mS.localVideo,this.mS.userName,this.socketService.socketId); + } else { + + this.mS.closeProducer(producerId) + } + }else{ + // this.permissionDeniedModal= true; + } + } + + async changeWebCamera(){ + + if (this.mS.isVideoAllowed && this.mS.isVideoOn) { + await this.initVideo(); + + this.mS.changeWebcam(); + }else{ + + } + } + + async initVideo() { + + try { + if (this.mS.localVideo) { + await this.stopTracks(this.mS.localVideo); + } + const deviceId = this.mS.selectedCamera.deviceId; + const videoConstraints = { + audio: false, + video: { + width: { ideal: 1280 }, + height: { ideal: 720 }, + deviceId: deviceId, + aspectRatio: 1.777, + }, + }; + const stream = await navigator.mediaDevices.getUserMedia(videoConstraints); + + this.mS.localVideo = stream; + console.log(this.mS.localVideo,'remoteVideo'); + } catch (error:any) { + console.log(error.status,'uoyoyoyo'); + console.log(typeof error,'type'); + } + } + + async toggleScreen(e:any){ + e.preventDefault(); + this.mS.isScreenOn = !this.mS.isScreenOn; + const producerId = this.mS.getProducerIdByType('screenType'); + + if (this.mS.isScreenOn && !producerId) { + await this.initScreen(); + + const track = this.mS.localScreen?.getVideoTracks()[0]; + + if (this.mS.localScreen && track) { + await this.mS.produce('screenType', null, false, false, track); + + console.log('meeting room screenType'); + + this.mS.handleConsumer('screenType',this.mS.localScreen,this.mS.userName,this.socketService.socketId); + } + } else { + + this.mS.closeProducer(producerId) + } + } + + async initScreen() { + + try { + if (this.mS.localScreen) { + await this.stopTracks(this.mS.localScreen); + } + const videoConstraints = { + audio : false, + video : + { + displaySurface : 'monitor', + logicalSurface : true, + cursor : true, + width : { max: 1920 }, + height : { max: 1080 }, + frameRate : { max: 30 } + } + }; + const stream = await navigator.mediaDevices.getDisplayMedia(videoConstraints); + + this.mS.localScreen = stream; + console.log(this.mS.localScreen,'remoteScreen'); + } catch (error:any) { + this.mS.isScreenOn = false; + this.mS.localScreen = null; + console.log(error.code,error.name,error.message,'uoyoyoyo'); + console.log(typeof error,'type'); + } + } + + async stopTracks(stream:any) { + stream.getTracks().forEach((track:any) => { + track.stop(); + }); + } + + handleAspectRatio() { + if (this.videoGrid.length > 1) { + this.adaptAspectRatio(this.videoGrid.length); + } else { + resizeVideoMedia(this.videoContainer,this.videoGrid); + } + } + + adaptAspectRatio(participantsCount:number) { + /* + ['0:0', '4:3', '16:9', '1:1', '1:2']; + */ + let desktop, + mobile = 1; + // desktop aspect ratio + switch (participantsCount) { + case 1: + case 2: + case 3: + case 4: + case 7: + case 9: + desktop = 2; // (16:9) + break; + case 5: + case 6: + case 10: + case 11: + desktop = 1; // (4:3) + break; + // case 2: + case 8: + desktop = 3; // (1:1) + break; + default: + desktop = 0; // (0:0) + } + // mobile aspect ratio + switch (participantsCount) { + case 3: + case 9: + case 10: + mobile = 2; // (16:9) + break; + case 2: + case 7: + case 8: + case 11: + mobile = 1; // (4:3) + break; + case 1: + case 4: + case 5: + case 6: + mobile = 3; // (1:1) + break; + default: + mobile = 3; // (1:1) + } + if (participantsCount > 11) { + desktop = 1; // (4:3) + mobile = 3; // (1:1) + } + + setAspectRatio(desktop); + resizeVideoMedia(this.videoContainer,this.videoGrid); + } + + leaveRoom(){ + + this.socketService.emitSocketEvent(SocketEvents.EXIT_ROOM,{}).subscribe({ + next:async (message: any) => { + console.log(message); + await this.mS.clean(); + this.router.navigate(['']) + }, + error: (error: any) => console.log(error), + complete: ()=> { } + }) + } + + ngOnDestroy(): void { + this.resizeSubscription$.unsubscribe(); + } + + + + + + + + + + + + + + + + + + + + + + + dummy_data:any[] = [ + { + "id": "940b032b-e7df-4323-994a-a49876409f5d", + "peer_name": "Nichole.Wintheiser1", + "avatar": "https://avatars.githubusercontent.com/u/58811466", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "4e3c381c-e72d-407a-8e00-78c2e548800d", + "peer_name": "Ally.Jakubowski5", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/186.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "e37ceaf1-9532-4b4c-a806-47f7b301808a", + "peer_name": "Joanne.Barrows", + "avatar": "https://avatars.githubusercontent.com/u/50040549", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "b519715c-bdb1-4f2a-8bec-26a43bdaaec2", + "peer_name": "Alford70", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/1187.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "ef94821e-5c3a-4236-8387-1e0c8d218213", + "peer_name": "Ova.Little48", + "avatar": "https://avatars.githubusercontent.com/u/45348419", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "8017adc7-15e8-4828-b6ac-207988527ac5", + "peer_name": "Tressie_Bartell", + "avatar": "https://avatars.githubusercontent.com/u/48299448", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "f3ea0a11-d7b4-47fc-b6b5-dbf01762420e", + "peer_name": "Alene.Pouros52", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/997.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "4d9ebd2b-cefe-4b1d-9e60-34f137e97542", + "peer_name": "Arianna62", + "avatar": "https://avatars.githubusercontent.com/u/68416506", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "f84561f3-ba5b-4353-9523-251d7b3b9089", + "peer_name": "Arden_DAmore", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/1104.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "e50769d4-2c49-4d0a-85bc-466f99bbc191", + "peer_name": "Harry44", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/91.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "477b73b9-9d33-4618-ab85-3f6604db42bd", + "peer_name": "Nia_Stokes", + "avatar": "https://avatars.githubusercontent.com/u/39166124", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "e99ad65f-4ad0-4bc9-b6b5-334041320553", + "peer_name": "Francesca25", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/467.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "e6e27ec0-e5b8-430e-aefb-985b6161586d", + "peer_name": "Libbie.Hegmann37", + "avatar": "https://avatars.githubusercontent.com/u/77700676", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "21c39c51-43d2-426f-8531-c103a9691462", + "peer_name": "Ashton.Barrows", + "avatar": "https://avatars.githubusercontent.com/u/57753872", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "8da6fedd-5bfd-4fff-b089-f98409bef3c8", + "peer_name": "Ariel87", + "avatar": "https://avatars.githubusercontent.com/u/10283029", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "6c42fe3c-7cb3-4551-b87b-4207e82044c5", + "peer_name": "Kelsie39", + "avatar": "https://avatars.githubusercontent.com/u/38908618", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "589e361c-3aeb-4eef-91bb-a044786d4b3e", + "peer_name": "Clovis_Feeney", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/1153.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "c2c9ab27-c96d-4d1d-8b8d-c8f7045396b2", + "peer_name": "Tanner_Morar", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/859.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "5ec5688f-900f-43ec-a8fb-a6b87f9de061", + "peer_name": "Nico1", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/250.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "d71195d6-1827-4fa5-bd4a-b0a9da35dd95", + "peer_name": "Vidal.Turner28", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/759.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "f66f92eb-ef6a-44a5-a17c-7100d5c54607", + "peer_name": "Jonathan.Howe20", + "avatar": "https://avatars.githubusercontent.com/u/32919419", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "efc89503-b1e2-4503-b58f-f5057c044d37", + "peer_name": "Norma.Walter84", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/45.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "c348bfd4-0d83-46b2-b10d-292f7a322c46", + "peer_name": "Claudie23", + "avatar": "https://avatars.githubusercontent.com/u/73662193", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "d40d1b46-cc48-4431-aa6a-98f431406e2d", + "peer_name": "Aglae_Ferry", + "avatar": "https://avatars.githubusercontent.com/u/40572602", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "c34c1555-a375-4b8b-9192-ab697779a96f", + "peer_name": "Humberto99", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/687.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "4006f6a0-6806-4a4a-8ece-ebeb6c95255e", + "peer_name": "Muhammad.Steuber", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/346.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "2724421c-60fe-48f6-9bdc-b0d3c67d8494", + "peer_name": "Korey_Ryan", + "avatar": "https://avatars.githubusercontent.com/u/56398892", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "6ae0782c-3704-45ca-9e5d-a3fb77b2c625", + "peer_name": "Brooklyn43", + "avatar": "https://avatars.githubusercontent.com/u/53213603", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "79663f1d-10e1-44fa-aca9-a129c4a8a3ee", + "peer_name": "Alva.Johnson", + "avatar": "https://avatars.githubusercontent.com/u/23065698", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "0e9d7f17-1bec-44da-9ff5-370c5d984c58", + "peer_name": "Henriette.Jacobi40", + "avatar": "https://avatars.githubusercontent.com/u/22014858", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "2e925328-d13e-4b1e-97d8-5d6540a999d9", + "peer_name": "Paolo_Pagac", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/163.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "1caa4511-8a44-4155-9399-96212f3fdd05", + "peer_name": "Jett.Trantow15", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/722.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "d78867b6-fd28-4181-833a-967e332cf619", + "peer_name": "Laurie26", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/117.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "52355398-97f6-4ed2-94a2-29c905f930c5", + "peer_name": "Haley_Deckow", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/61.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "77b8ec43-fd11-4b28-8f67-7d9f56ced209", + "peer_name": "Cecelia.Wuckert", + "avatar": "https://avatars.githubusercontent.com/u/22749070", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "9637b077-5fe8-427c-b0e0-3c160a51811f", + "peer_name": "Joy35", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/1064.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "b20c133f-fa3f-440d-9d70-88e06af442f8", + "peer_name": "Bennie75", + "avatar": "https://avatars.githubusercontent.com/u/16007471", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "5c001bd2-f355-4ed9-9044-58400d6699ed", + "peer_name": "Justyn9", + "avatar": "https://avatars.githubusercontent.com/u/55612278", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "37524dc7-daac-4b39-9df1-a52817b51ec0", + "peer_name": "Emely34", + "avatar": "https://avatars.githubusercontent.com/u/8659801", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "d4a96fc7-c471-484f-9d56-e1d2343656bb", + "peer_name": "Larissa_Doyle", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/173.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "3501f6ef-a38c-4a86-afdd-2485c5e200c3", + "peer_name": "Roma_Kozey", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/990.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "6a3aa73f-4b69-4565-94b4-ecf47a27fd51", + "peer_name": "Donna17", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/784.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "1a8c76de-1489-4979-bbcb-a2e647f0d0d4", + "peer_name": "Felicity.Hettinger49", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/592.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "eceecfc7-582f-4c1f-90e8-731662cbdab7", + "peer_name": "Nona.Turcotte", + "avatar": "https://avatars.githubusercontent.com/u/62540936", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "7612058e-b835-4785-bb84-e2618f986b40", + "peer_name": "Jennie.Jakubowski", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/593.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "a3ef3499-1f83-4c7a-8d63-5b588cb7e6c3", + "peer_name": "Verlie.Quigley", + "avatar": "https://avatars.githubusercontent.com/u/4100983", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "66cdf7eb-e798-49fb-80c3-2f3d740522ee", + "peer_name": "Mallie.Witting-Torphy76", + "avatar": "https://avatars.githubusercontent.com/u/70898735", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "3df3c3a1-8af4-44bd-9ae0-debcf948e59b", + "peer_name": "Teagan_Schiller37", + "avatar": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/1105.jpg", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "fee2b0f5-9c7a-40df-8809-d996bb538226", + "peer_name": "Reyes_Carter", + "avatar": "https://avatars.githubusercontent.com/u/63316500", + "video": "", + "audio": "", + "screen": "" + }, + { + "id": "939e502e-4e7b-4661-9d82-6408119d1d00", + "peer_name": "Lester.Pfeffer64", + "avatar": "https://avatars.githubusercontent.com/u/68857447", + "video": "", + "audio": "", + "screen": "" + } + ] + + users:any[] = this.dummy_data.slice(0,4); +} diff --git a/src/app/modules/join-meeting/joinmeet.routes.ts b/src/app/modules/join-meeting/joinmeet.routes.ts index 744788a..fa69c39 100644 --- a/src/app/modules/join-meeting/joinmeet.routes.ts +++ b/src/app/modules/join-meeting/joinmeet.routes.ts @@ -1,6 +1,7 @@ import { Routes } from '@angular/router'; import { JoinComponent } from './components/join/join.component'; import { ConsumerComponent } from './components/consumer/consumer.component'; +import { MeetingRoomComponent } from './components/meeting-room/meeting-room.component'; export const joinRoutes: Routes = [ { @@ -13,7 +14,7 @@ export const joinRoutes: Routes = [ component:JoinComponent }, { - path:'consumer', - component:ConsumerComponent + path:'room/:id', + component:MeetingRoomComponent }, ]; diff --git a/src/app/shared/components/user-video/user-video.component.html b/src/app/shared/components/user-video/user-video.component.html new file mode 100644 index 0000000..d563f23 --- /dev/null +++ b/src/app/shared/components/user-video/user-video.component.html @@ -0,0 +1,40 @@ +
+ + + + + +
+
+ mic_off +
+
+
+ +
+
+ +
+
+
+ front_hand +
+ {{userName}} +
+
+
+
+ +
+
+ + + +
+ {{ userName?.charAt(0) }} +
+
+
+
\ No newline at end of file diff --git a/src/app/shared/components/user-video/user-video.component.scss b/src/app/shared/components/user-video/user-video.component.scss new file mode 100644 index 0000000..d02c76f --- /dev/null +++ b/src/app/shared/components/user-video/user-video.component.scss @@ -0,0 +1,135 @@ +.font_size{ + font-size: clamp(1rem, 2vw + .75rem, 3rem); +} + +#audioVisualizer { + display: flex; + height: 1.625em; + width: 1.625em; + align-items: center; + background-color: rgba(26, 115, 232, 0.9); + border-radius: 50%; + color: #fff; + justify-content: center; + overflow: hidden; + gap: 2px; + } + + #left, + #right { + width: 0.25em; + min-height: 0.25em; + background-color: white; + border-radius: 12px; + max-height: 0.5em; + /* min-height: 1em; */ + } + + #middle { + width: 0.25em; + min-height: 0.25em; + background-color: white; + border-radius: 12px; + max-height: 1em; + /* margin-left: 2px; + margin-right: 2px; */ + } + + .hand-raise{ + border-radius: .75rem; + height: 1.5rem; + left: 0; + position: absolute; + // top: .075rem; + width: 100%; + background-color: #fff; + animation: expandPill 1s cubic-bezier(.4,0,.2,1); + } + + @keyframes expandPill { + 0% { + margin-left: .25rem; + transform: scale(0); + width: 1.5rem; + } + 36.7% { + margin-left: .25rem; + transform: scale(1.1); + width: 1.5rem; + } + 75% { + margin-left: .25rem; + transform: scale(1); + width: 1.5rem; + } + 100% { + margin-left: 0; + width: 100%; + } + } + + .hand{ + align-items: center; + color: #fff; + display: flex; + font-size: .875rem; + position: relative; + color: rgb(32, 33, 36); + animation: popUpIcon .75s cubic-bezier(.4,0,.2,1), rotateIcon .9s cubic-bezier(.4,0,.2,1) .75s; + transform-origin: bottom; + } + + @keyframes popUpIcon { + 0% { + transform: translateY(24px); + } + 22% { + transform: translateY(24px); + } + 48.93% { + transform: translateY(-5px); + } + 100% { + transform: translateY(0); + } + } + + @keyframes rotateIcon { + 20.33% { + transform: rotate(14deg); + } + 38.89% { + transform: rotate(-9deg); + } + 57.44% { + transform: rotate(14deg); + } + 74.11% { + transform: rotate(-9deg); + } + 88.89% { + transform: rotate(5deg); + } + 0% { + transform: rotate(0deg); + } + } + + .expand_name{ + margin-left: .5rem; + color: rgb(32, 33, 36); + text-shadow: none; + animation: expandName 1s cubic-bezier(.4,0,.2,1); + } + + @keyframes expandName { + 0% { + width: 0; + } + 75% { + width: 0; + } + 100% { + width: 100%; + } + } \ No newline at end of file diff --git a/src/app/shared/components/user-video/user-video.component.spec.ts b/src/app/shared/components/user-video/user-video.component.spec.ts new file mode 100644 index 0000000..42c8a8a --- /dev/null +++ b/src/app/shared/components/user-video/user-video.component.spec.ts @@ -0,0 +1,21 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UserVideoComponent } from './user-video.component'; + +describe('UserVideoComponent', () => { + let component: UserVideoComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [UserVideoComponent] + }); + fixture = TestBed.createComponent(UserVideoComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/components/user-video/user-video.component.ts b/src/app/shared/components/user-video/user-video.component.ts new file mode 100644 index 0000000..dd68b8c --- /dev/null +++ b/src/app/shared/components/user-video/user-video.component.ts @@ -0,0 +1,110 @@ +import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AvatarModule } from 'primeng/avatar'; +import { SocketService } from 'src/app/core/services/socket.service'; +import { MediasoupService } from 'src/app/core/services/mediasoup.service'; + +@Component({ + selector: 'app-user-video', + standalone: true, + imports: [CommonModule,AvatarModule], + templateUrl: './user-video.component.html', + styleUrls: ['./user-video.component.scss'] +}) +export class UserVideoComponent implements OnInit, OnChanges ,OnDestroy { + + + @ViewChild('audioVisualizer') audioVisualizer!: ElementRef; + @ViewChild('left') left!: ElementRef; + @ViewChild('middle') middle!: ElementRef; + @ViewChild('right') right!: ElementRef; + + @Input() id!:any; + @Input() userName!:any; + @Input() peer!:any; + @Input() videoStream!:MediaStream; + @Input() audioStream!:MediaStream; + @Input() screenStream!:MediaStream; + @Input() users!:any; + @Input() bg_color:string = ''; + + audioContext!: AudioContext; + analyserNode!: AnalyserNode; + sourceNode: any; + isHandRaised: boolean = false; + + socketService = inject(SocketService); + mS = inject(MediasoupService); + + ngOnChanges(changes: SimpleChanges): void { + + if (changes['audioStream']) { + this.audioStream ? this.initAudioContext() : this.stopAudio(); + } + } + + ngOnInit(): void { + this.mS.triggerHandRaise.subscribe((isHandRaised:boolean)=>{ + this.isHandRaised = isHandRaised; + }) + } + + getRandomLightColor() { + const letters = '0123456789ABCDEF'; + let color = '#'; + for (let i = 0; i < 6; i++) { + color += letters[Math.floor(Math.random() * 16)]; + } + return color; + } + + initAudioContext(): void { + this.audioContext = new AudioContext(); + this.analyserNode = this.audioContext.createAnalyser(); + this.analyserNode.fftSize = 512; + + try { + this.sourceNode = this.audioContext.createMediaStreamSource(this.audioStream); + this.sourceNode.connect(this.analyserNode); + // this.analyserNode.connect(this.audioContext.destination); + this.updateVisualizer(); + } catch (error) { + console.log(error); + } + } + + updateVisualizer(): void { + const dataArray = new Uint8Array(this.analyserNode.frequencyBinCount); + this.analyserNode.getByteFrequencyData(dataArray); + + // Calculate average volume + const volume = + dataArray.reduce((acc, val) => acc + val, 0) / dataArray.length; + + if (this.left && this.right && this.middle) { + const middleVolume = volume; + this.middle.nativeElement.style.height = `${middleVolume}px`; + + const leftVolume = volume * 0.5; // Adjust as needed + this.left.nativeElement.style.height = `${leftVolume}px`; + + const rightVolume = volume * 0.5; // Adjust as needed + this.right.nativeElement.style.height = `${rightVolume}px`; + } + + requestAnimationFrame(() => this.updateVisualizer()); + } + + stopAudio(): void { + // if (this.audioStream) { + // this.audioStream.getTracks().forEach((track) => track.stop()); + // } + if (this.audioContext && this.audioContext.state != 'closed') { + this.audioContext.close(); + } + } + + ngOnDestroy(): void { + this.stopAudio(); + } +} diff --git a/src/assets/images/g-profile-pic.png b/src/assets/images/g-profile-pic.png new file mode 100644 index 0000000000000000000000000000000000000000..9890cd2a9b9fd3ff90b58b4e400c18243c07ff38 GIT binary patch literal 37125 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 + + + diff --git a/src/styles.scss b/src/styles.scss index 8fbb7a5..8538a79 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -7,6 +7,12 @@ box-sizing: border-box; } +.noto-color-emoji-regular { + font-family: "Noto Color Emoji", sans-serif; + font-weight: 400; + font-style: normal; + } + body{ height: 100vh; margin: 0px; @@ -33,4 +39,93 @@ body{ .p-dialog-header{ padding: 8px; } +} + +.tooltip-padding{ + + .p-tooltip-arrow{ + display: none !important; + } + .p-tooltip-text{ + padding: 4px 8px; + } +} + +.menu_item{ + + .p-menuitem-link{ + padding: 0.75rem !important; + width: 100%; + + .p-menuitem-text{ + display: flex; + align-items: center; + width: 100%; + gap: 10px; + } + } +} + +.emojicolor_item{ + + .p-menuitem-link{ + padding: 0.5rem 0.25rem !important; + width: 100%; + justify-content: center; + // .p-menuitem-text{ + // display: flex; + // align-items: center; + // width: 100%; + // gap: 10px; + // } + } +} + +.sidenav{ + align-items: center; + border: 1px solid transparent; + border-radius: 8px; + box-sizing: border-box; + display: flex; + justify-content: center; + max-width: 100%; + overflow: hidden; + position: absolute; + right: 12px; + top: 12px; + bottom: 12px; + transform: none; + transition: transform .2s cubic-bezier(.4,0,.2,1), bottom .5s cubic-bezier(0.4,0,0.2,1); + width: 360px; + background-color: rebeccapurple; +} + +.close_sidenav{ + transform: translateX(calc(360px + 16px)); +} + +.emoji-container{ + align-items: center; + box-sizing: border-box; + display: flex; + justify-content: center; + max-width: 100%; + overflow: hidden; + position: absolute; + bottom: 80px; + left: 0; + // transform: none; + transition: transform .2s cubic-bezier(.4,0,.2,1), bottom .2s cubic-bezier(0.4,0,0.2,1), height .2s ease; + width: 100%; + height: 0px; + // background-color: rgba(127, 255, 212, 0.801); +} + +.close-emoji-container{ + height: 40px; +} +.close-emoji-container:hover{ + .emoji-btn{ + opacity: 1; + } } \ No newline at end of file