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 0000000..9890cd2 Binary files /dev/null and b/src/assets/images/g-profile-pic.png differ diff --git a/src/index.html b/src/index.html index 4c1e0c6..0f82914 100644 --- a/src/index.html +++ b/src/index.html @@ -6,6 +6,9 @@ + + + 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