UNPKG

mediasfu-angular

Version:
1,069 lines (1,058 loc) 3.07 MB
import * as i0 from '@angular/core'; import { Injectable, Component, Optional, Inject, Input, ViewChild, HostListener, Injector, EventEmitter, Output, ContentChildren, ViewEncapsulation } from '@angular/core'; import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i2 from '@fortawesome/angular-fontawesome'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { faMicrophone, faMicrophoneSlash, faVideo, faVideoSlash, faTimes, faPen, faUsers, faPlus, faDoorOpen, faRandom, faHandPointer, faSave, faPlay, faSyncAlt, faStop, faStar, faLock, faCamera, faPhotoFilm, faSpinner, faCopy, faEnvelope, faBars, faPaperPlane, faReply, faQrcode, faComment, faTrash, faDotCircle, faCircle, faCheck, faDesktop, faComments, faChevronRight, faChevronLeft, faPencilAlt, faPaintBrush, faShapes, faEraser, faSearch, faSearchPlus, faSearchMinus, faFont, faTextHeight, faUndo, faRedo, faMousePointer, faHandPaper, faUpload, faBan, faRecordVinyl, faPlayCircle, faPauseCircle, faStopCircle, faCog, faClock, faUserPlus, faTools, faPoll, faUserFriends, faChalkboardTeacher, faSync, faPhone, faShareAlt, faChartBar } from '@fortawesome/free-solid-svg-icons'; import io, { Socket } from 'socket.io-client'; import * as i2$1 from 'mediasoup-client/lib/types'; import * as mediasoupClient from 'mediasoup-client'; import * as i1$1 from 'ngx-cookie-service'; import { CookieService } from 'ngx-cookie-service'; import { SelfieSegmentation } from '@mediapipe/selfie_segmentation'; import * as i2$2 from '@angular/forms'; import { FormsModule, Validators, ReactiveFormsModule } from '@angular/forms'; import { faFacebook, faWhatsapp, faTelegram } from '@fortawesome/free-brands-svg-icons'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import * as i3 from '@zxing/ngx-scanner'; import { ZXingScannerModule } from '@zxing/ngx-scanner'; import { BehaviorSubject as BehaviorSubject$1, combineLatest } from 'rxjs'; /** * Service to validate if a given string contains only alphanumeric characters. * * @class ValidateAlphanumeric * * @example * ```typescript * const validator = new ValidateAlphanumeric(); * validator.validateAlphanumeric({ str: 'abc123' }).then(isValid => console.log(isValid)); // true * validator.validateAlphanumeric({ str: 'abc 123!' }).then(isValid => console.log(isValid)); // false * ``` * * @param {ValidateAlphanumericOptions} options - Contains the string to validate. * @param {string} options.str - The input string that needs to be validated. * @returns {Promise<boolean>} - A promise resolving to `true` if the input string is alphanumeric, otherwise `false`. */ class ValidateAlphanumeric { /** * Validates if the given string contains only alphanumeric characters. * * @param {ValidateAlphanumericOptions} options - The options containing the string to validate. * @param {string} options.str - The string to be validated. * @returns {Promise<boolean>} - A promise that resolves to `true` if the string is alphanumeric, otherwise `false`. */ async validateAlphanumeric({ str }) { let code, i, len; for (i = 0, len = str.length; i < len; i++) { code = str.charCodeAt(i); if (!(code > 47 && code < 58) && // numeric (0-9) !(code > 64 && code < 91) && // upper alpha (A-Z) !(code > 96 && code < 123)) { // lower alpha (a-z) return false; } } return true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ValidateAlphanumeric, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ValidateAlphanumeric, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ValidateAlphanumeric, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * Joins a conference room with the provided options and performs validation checks. * * @param {JoinConRoomOptions} options - Contains: * - socket: Socket instance for communication. * - roomName: Name of the room to join. * - islevel: User level within the room. * - member: Member identifier. * - sec: Security token. * - apiUserName: API username for authentication. * * - **Validation**: * - Checks that `roomName`, `apiUserName`, and `member` are alphanumeric. * - Ensures `roomName` starts with 's' or 'p' and meets length requirements. * - Verifies `sec`, `islevel`, and `apiUserName` comply with length and format expectations. * * - **Response Handling**: * - Resolves to the server's response data upon a successful join. * - Rejects with specific reasons if the user is banned, suspended, or if the room host is not present. * * @returns {Promise<JoinConRoomResponse>} Resolves with the join response data, or rejects with error details. * @throws {Error} Throws validation errors or issues encountered while joining the room. * * @example * ```typescript * const joinOptions = { * socket: mySocket, * roomName: 'sMyRoom', * islevel: '1', * member: 'participant123', * sec: '64-character-long-secret-key-here...', * apiUserName: 'apiUser123', * }; * joinConRoom(joinOptions) * .then(response => console.log('Joined room:', response)) * .catch(error => console.error('Failed to join room:', error)); * ``` */ class JoinConRoom { validateAlphanumeric; constructor(validateAlphanumeric) { this.validateAlphanumeric = validateAlphanumeric; } /** * Joins a conference room using the provided options. * * @param {JoinConRoomOptions} options - The options for joining the conference room. * @param {Socket} options.socket - The socket instance to use for communication. * @param {string} options.roomName - The name of the room to join. * @param {string} options.islevel - The level of the user. * @param {string} options.member - The member identifier. * @param {string} options.sec - The security token. * @param {string} options.apiUserName - The API username. * @returns {Promise<JoinConRoomResponse>} A promise that resolves with the response of the join operation. * * @throws {Error} If any of the required parameters are missing or invalid. * @throws {Error} If the user is banned, suspended, or if the host has not joined the room yet. */ async joinConRoom({ socket, roomName, islevel, member, sec, apiUserName, }) { return new Promise((resolve, reject) => { // Validate inputs if (!(sec && roomName && islevel && apiUserName && member)) { const validationError = { success: false, rtpCapabilities: null, reason: 'Missing required parameters', }; reject(validationError); return; } // Validate alphanumeric for roomName, apiUserName, and member try { this.validateAlphanumeric.validateAlphanumeric({ str: roomName }); this.validateAlphanumeric.validateAlphanumeric({ str: apiUserName }); this.validateAlphanumeric.validateAlphanumeric({ str: member }); } catch (error) { const validationError = { success: false, rtpCapabilities: null, reason: 'Invalid roomName or apiUserName or member', }; reject(validationError); return; } // Validate roomName starts with 's' or 'p' if (!(roomName.startsWith('s') || roomName.startsWith('p'))) { const validationError = { success: false, rtpCapabilities: null, reason: 'Invalid roomName, must start with s or p', }; reject(validationError); return; } // Validate other conditions for sec, roomName, islevel, apiUserName if (!(sec.length === 64 && roomName.length >= 8 && islevel.length === 1 && apiUserName.length >= 6 && (islevel === '0' || islevel === '1' || islevel === '2'))) { const validationError = { success: false, rtpCapabilities: null, reason: 'Invalid roomName or islevel or apiUserName or secret', }; reject(validationError); return; } socket.emit('joinConRoom', { roomName, islevel, member, sec, apiUserName }, async (data) => { try { // Check if rtpCapabilities is null if (data.rtpCapabilities === null) { // Check if banned, suspended, or noAdmin if (data.banned) { throw new Error('User is banned.'); } if (data.suspended) { throw new Error('User is suspended.'); } if (data.noAdmin) { throw new Error('Host has not joined the room yet.'); } // Resolve with the data received from the 'joinConRoom' event resolve(data); } else { // Resolve with the data received from the 'joinConRoom' event resolve(data); } } catch (error) { // Handle errors during the joinConRoom process console.log('Error joining room:', error); reject(error); } }); }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JoinConRoom, deps: [{ token: ValidateAlphanumeric }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JoinConRoom, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JoinConRoom, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: ValidateAlphanumeric }] }); /** * @service JoinConsumeRoom * @description Service to join a media consumption room, setup device, and manage piped transports. * * @method joinConsumeRoom * Joins a consumption room by sending a request to the server and performing necessary device and transport setup. * * @param {JoinConsumeRoomOptions} options - Options for joining the consumption room. * @param {Socket} options.remote_sock - The remote socket used for joining the room. * @param {string} options.apiToken - API token for authentication. * @param {string} options.apiUserName - API username for authentication. * @param {JoinConsumeRoomParameters} options.parameters - Parameters required for the function. * * @returns {Promise<JoinConsumeRoomResponse>} A promise that resolves with the result of the join request. * * @example * ```typescript * const joinConsumeRoomResponse = await joinConsumeRoomService.joinConsumeRoom({ * remote_sock: mySocket, * apiToken: 'apiToken123', * apiUserName: 'myUser', * parameters: { * roomName: 'room1', * islevel: '2', * member: 'JohnDoe', * device: null, * updateDevice: updateDeviceFunction, * receiveAllPipedTransports: receiveAllPipedTransportsFunction, * createDeviceClient: createDeviceClientFunction, * } * }); * ``` */ class JoinConsumeRoom { JoinConRoomService; constructor(JoinConRoomService) { this.JoinConRoomService = JoinConRoomService; } /** * Joins a consumption room by sending a request to the server and handles the necessary setup. * @param {Object} options - The options object containing necessary variables. * @param {any} options.remote_sock - The remote socket information. * @param {string} options.apiToken - The API token for authentication. * @param {string} options.apiUserName - The API username for authentication. * @param {any} options.parameters - Additional parameters required for the function. * @returns {Promise<any>} - A promise that resolves with data related to the success of joining the room. */ joinConsumeRoom = async ({ remote_sock, apiToken, apiUserName, parameters, }) => { let { roomName, islevel, member, device, updateDevice, //Mediasfu functions receiveAllPipedTransports, createDeviceClient, } = parameters; try { // Join the consumption room const data = await this.JoinConRoomService.joinConRoom({ socket: remote_sock, roomName, islevel, member, sec: apiToken, apiUserName, }); if (data && data.success) { // Setup media device if not already set if (!device) { if (data.rtpCapabilities) { const device_ = await createDeviceClient({ rtpCapabilities: data.rtpCapabilities, }); if (device_) { updateDevice(device_); } } } // Receive all piped transports await receiveAllPipedTransports({ nsock: remote_sock, parameters }); } return data; } catch (error) { console.log('Error in joinConsumeRoom:', error); throw new Error('Failed to join the consumption room or set up necessary components.'); } }; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JoinConsumeRoom, deps: [{ token: JoinConRoom }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JoinConsumeRoom, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: JoinConsumeRoom, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: JoinConRoom }] }); /** * @service ProducerClosed * @description Service to manage the closing of a producer, including resizing video elements and updating consumer transports. * * @method producerClosed * Closes a specific producer by its ID, adjusts any associated video elements, and updates the list of consumer transports. * * @param {ProducerClosedOptions} options - Options for closing the specified producer. * @param {string} options.remoteProducerId - Unique ID for the producer to close. * @param {ProducerClosedParameters} options.parameters - Parameters to configure the producer closure and related updates. * * @returns {Promise<void>} A promise that resolves when the producer has been closed and relevant updates have been made. * * @example * ```typescript * await producerClosedService.producerClosed({ * remoteProducerId: 'producer123', * parameters: { * consumerTransports: currentTransports, * screenId: 'screen123', * updateConsumerTransports: updateTransportList, * closeAndResize: closeAndResizeFunction, * getUpdatedAllParams: getUpdatedParamsFunction, * } * }); * ``` */ class ProducerClosed { /** * Handles the closing of a producer and resizes video elements. * @param {Object} options - The options object containing necessary variables. * @param {string} options.remoteProducerId - The ID of the remote producer. * @param {any} options.parameters - Additional parameters required for the function. * @returns {Promise<void>} */ producerClosed = async ({ remoteProducerId, parameters, }) => { let { consumerTransports, screenId, updateConsumerTransports, // mediasfu functions closeAndResize, } = parameters; // Handle producer closed const producerToClose = consumerTransports.find((transportData) => transportData.producerId === remoteProducerId); if (!producerToClose) { return; } // Check if the ID of the producer to close is == screenId let kind = producerToClose.consumer.kind; if (producerToClose.producerId == screenId) { kind = 'screenshare'; } try { await producerToClose['consumerTransport'].close(); } catch (error) { console.error('Error closing consumerTransport:', error); } try { producerToClose.consumer.close(); } catch (error) { console.error('Error closing consumer:', error); } consumerTransports = consumerTransports.filter((transportData) => transportData.producerId !== remoteProducerId); updateConsumerTransports(consumerTransports); // Close and resize the videos await closeAndResize({ producerId: remoteProducerId, kind: kind, parameters: parameters }); }; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ProducerClosed, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ProducerClosed, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ProducerClosed, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * Signals the creation of a new consumer transport. * * @param {Object} options - The options for signaling a new consumer transport. * @param {string} options.remoteProducerId - The ID of the remote producer. * @param {boolean} options.islevel - Indicates the level of the consumer. * @param {any} options.nsock - The socket instance for communication. * @param {SignalNewConsumerTransportOptions} options.parameters - The parameters for the transport. * * @returns {Promise<string[] | void>} A promise that resolves to an array of consuming transports or void. * * @throws Will throw an error if the signaling process fails. * * @example * const options = { * remoteProducerId: 'producer-id', * islevel: true, * nsock: socketInstance, * parameters: { * device: mediaDevice, * consumingTransports: [], * lock_screen: false, * updateConsumingTransports: updateFunction, * connectRecvTransport: connectFunction, * reorderStreams: reorderFunction, * getUpdatedAllParams: getUpdatedParamsFunction, * }, * }; * * signalNewConsumerTransport(options) * .then(consumingTransports => { * console.log('Consuming Transports:', consumingTransports); * }) * .catch(error => { * console.error('Error signaling new consumer transport:', error); * }); */ class SignalNewConsumerTransport { signalNewConsumerTransport = async ({ remoteProducerId, islevel, nsock, parameters, }) => { try { let { device, consumingTransports, lock_screen, updateConsumingTransports, connectRecvTransport, reorderStreams, } = parameters; // Get updated parameters const updatedParams = parameters.getUpdatedAllParams(); device = updatedParams.device; consumingTransports = updatedParams.consumingTransports; // Check if already consuming if (consumingTransports.includes(remoteProducerId)) { return consumingTransports; } // Add remote producer ID to consumingTransports array consumingTransports.push(remoteProducerId); updateConsumingTransports(consumingTransports); // Emit createWebRtcTransport event to signal a new consumer nsock.emit('createWebRtcTransport', { consumer: true, islevel }, async ({ params }) => { if (params.error) { // Handle error return; } try { // Create a new receiving transport using the received parameters if (!device) { throw new Error('Device is not initialized'); } const consumerTransport = device.createRecvTransport({ ...params }); // Handle 'connect' event for the consumer transport consumerTransport.on('connect', async ({ dtlsParameters }, callback, errback) => { try { // Emit transport-recv-connect event to signal connection nsock.emit('transport-recv-connect', { dtlsParameters, serverConsumerTransportId: params.id, }); callback(); } catch (error) { errback(error); } }); // Listen for connection state change consumerTransport.on('connectionstatechange', async (state) => { switch (state) { case 'connecting': // Handle connecting state break; case 'connected': // Handle connected state break; case 'failed': // Handle failed state consumerTransport.close(); // Reorder streams based on conditions if (lock_screen) { await reorderStreams({ add: true, parameters }); } else { await reorderStreams({ add: false, parameters }); } break; default: break; } }); // Connect the receiving transport await connectRecvTransport({ consumerTransport, remoteProducerId, serverConsumerTransportId: params.id, nsock, parameters, }); } catch (error) { console.log(error, 'createRecvTransport error'); // Handle error return; } }); } catch (error) { console.log(error, 'signalNewConsumerTransport error'); // Handle error return; } }; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: SignalNewConsumerTransport, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: SignalNewConsumerTransport, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: SignalNewConsumerTransport, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * @service NewPipeProducer * @description Service to manage new pipe producer events, update state, and handle screen orientation for optimal experience. * * @method newPipeProducer * Handles the setup of a new pipe producer and manages user notifications or orientation changes as needed. * * @param {NewPipeProducerOptions} options - Options for setting up a new pipe producer. * @param {string} options.producerId - Unique ID for the new producer. * @param {string} options.islevel - Level designation for the producer. * @param {Socket} options.nsock - The socket used for communication. * @param {NewPipeProducerParameters} options.parameters - Parameters to configure the new pipe producer. * * @returns {Promise<void>} A promise that completes when the new pipe producer is set up. * * @example * ```typescript * await newPipeProducerService.newPipeProducer({ * producerId: 'producer123', * islevel: '2', * nsock: mySocket, * parameters: { * first_round: true, * shareScreenStarted: false, * shared: false, * landScaped: false, * showAlert: alertFunction, * isWideScreen: true, * updateFirst_round: updateFirstRoundFunction, * updateLandScaped: updateLandScapedFunction, * device: myDevice, * consumingTransports: [], * connectRecvTransport: connectRecvTransportFunction, * reorderStreams: reorderStreamsFunction, * getUpdatedAllParams: getUpdatedParamsFunction, * } * }); * ``` */ class NewPipeProducer { signalNewConsumerTransportService; constructor(signalNewConsumerTransportService) { this.signalNewConsumerTransportService = signalNewConsumerTransportService; } /** * Handles new pipe producer events and updates relevant states. * @param {Object} options - The options object containing necessary variables. * @param {string} options.producerId - The ID of the producer. * @param {string} options.islevel - The level of the producer. * @param {any} options.nsock - The socket object. * @param {any} options.parameters - Additional parameters required for the function. * @returns {Promise<void>} */ newPipeProducer = async ({ producerId, islevel, nsock, parameters, }) => { let { first_round, shareScreenStarted, shared, landScaped, showAlert, isWideScreen, updateFirst_round, updateLandScaped, } = parameters; try { // Perform signaling for new consumer transport await this.signalNewConsumerTransportService.signalNewConsumerTransport({ remoteProducerId: producerId, islevel: islevel, nsock: nsock, parameters: parameters, }); first_round = false; if (shareScreenStarted || shared) { if (!isWideScreen) { if (!landScaped) { if (showAlert) { showAlert({ message: 'Please rotate your device to landscape mode for better experience', type: 'success', duration: 3000, }); } landScaped = true; updateLandScaped(landScaped); } } first_round = true; updateFirst_round(first_round); } } catch (error) { console.error('Error in newPipeProducer:', error); throw new Error('Failed to handle new pipe producer event.'); } }; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: NewPipeProducer, deps: [{ token: SignalNewConsumerTransport }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: NewPipeProducer, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: NewPipeProducer, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: () => [{ type: SignalNewConsumerTransport }] }); /** * MiniCard component displays a customizable card with an image or initials. * * @component * @selector app-mini-card * @standalone true * @imports CommonModule * * @template * ```html * <div class="mini-card" [ngStyle]="getMergedCardStyles()"> * <div *ngIf="imageSource; else noImage" class="image-container"> * <img [src]="imageSource" alt="Profile" [ngStyle]="getMergedImageStyles()" /> * </div> * <ng-template #noImage> * <div class="initials" [ngStyle]="getInitialsStyle()">{{ initials }}</div> * </ng-template> * </div> * ``` * * @styleUrls ['./mini-card.component.css'] * * @inputs * - `initials` (string): Initials to display if no image is provided. * - `fontSize` (number): Font size for initials text, default is 14. * - `customStyle` (CSSStyleDeclaration): Custom styles for the card. * - `imageSource` (string): Source URL for the image. * - `roundedImage` (boolean): Whether the image should be rounded, default is false. * - `imageStyle` (CSSStyleDeclaration): Custom styles for the image. * * @constructor * - Optionally accepts injected values for each input property. * * @methods * - `getMergedCardStyles()`: Returns merged styles for the card. * - `getMergedImageStyles()`: Returns merged styles for the image. * - `getInitialsStyle()`: Returns styles for the initials text. * * @example * ```html * <app-mini-card initials="AB" fontSize="20" [roundedImage]="true" imageSource="/path/to/image.jpg"></app-mini-card> * ``` */ class MiniCard { initials; fontSize = 14; customStyle = {}; imageSource; roundedImage = false; imageStyle = {}; constructor(injectedInitials, injectedFontSize, injectedCustomStyle, injectedImageSource, injectedRoundedImage, injectedImageStyle) { this.initials = injectedInitials || this.initials || ''; this.fontSize = injectedFontSize || this.fontSize || 14; this.customStyle = injectedCustomStyle || this.customStyle || {}; this.imageSource = injectedImageSource || this.imageSource || ''; this.roundedImage = injectedRoundedImage || this.roundedImage || true; this.imageStyle = injectedImageStyle || this.imageStyle || {}; } getMergedCardStyles() { return { 'font-size': this.fontSize + 'px', display: 'flex', justifyContent: 'center', alignItems: 'center', borderRadius: '0', width: '100%', height: '100%', color: 'black', fontFamily: "'Nunito', sans-serif", overflow: 'hidden', border: '2px solid black', ...this.customStyle, }; } getMergedImageStyles() { return { width: '60%', height: '60%', objectFit: 'cover', ...(this.roundedImage ? { borderRadius: '50%' } : {}), ...this.imageStyle, }; } getInitialsStyle() { return { textAlign: 'center', 'font-size': this.fontSize + 'px', }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: MiniCard, deps: [{ token: 'initials', optional: true }, { token: 'fontSize', optional: true }, { token: 'customStyle', optional: true }, { token: 'imageSource', optional: true }, { token: 'roundedImage', optional: true }, { token: 'imageStyle', optional: true }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.0.1", type: MiniCard, isStandalone: true, selector: "app-mini-card", inputs: { initials: "initials", fontSize: "fontSize", customStyle: "customStyle", imageSource: "imageSource", roundedImage: "roundedImage", imageStyle: "imageStyle" }, ngImport: i0, template: ` <div class="mini-card" [ngStyle]="getMergedCardStyles()"> <div *ngIf="imageSource; else noImage" class="image-container"> <img [src]="imageSource" alt="Profile" [ngStyle]="getMergedImageStyles()" /> </div> <ng-template #noImage> <div class="initials" [ngStyle]="getInitialsStyle()">{{ initials }}</div> </ng-template> </div> `, isInline: true, styles: [".mini-card{display:flex;justify-content:center;align-items:center;border-radius:0;width:100%;height:100%;color:#000;font-family:Nunito,sans-serif;overflow:hidden;border:2px solid black}.image-container{display:flex;justify-content:center;align-items:center;width:100%;height:100%}.backgroundImage{width:60%;height:60%;object-fit:cover}.roundedImage{border-radius:50%}.initials{text-align:center;font-size:14px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: MiniCard, decorators: [{ type: Component, args: [{ selector: 'app-mini-card', imports: [CommonModule], template: ` <div class="mini-card" [ngStyle]="getMergedCardStyles()"> <div *ngIf="imageSource; else noImage" class="image-container"> <img [src]="imageSource" alt="Profile" [ngStyle]="getMergedImageStyles()" /> </div> <ng-template #noImage> <div class="initials" [ngStyle]="getInitialsStyle()">{{ initials }}</div> </ng-template> </div> `, styles: [".mini-card{display:flex;justify-content:center;align-items:center;border-radius:0;width:100%;height:100%;color:#000;font-family:Nunito,sans-serif;overflow:hidden;border:2px solid black}.image-container{display:flex;justify-content:center;align-items:center;width:100%;height:100%}.backgroundImage{width:60%;height:60%;object-fit:cover}.roundedImage{border-radius:50%}.initials{text-align:center;font-size:14px}\n"] }] }], ctorParameters: () => [{ type: undefined, decorators: [{ type: Optional }, { type: Inject, args: ['initials'] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: ['fontSize'] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: ['customStyle'] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: ['imageSource'] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: ['roundedImage'] }] }, { type: undefined, decorators: [{ type: Optional }, { type: Inject, args: ['imageStyle'] }] }], propDecorators: { initials: [{ type: Input }], fontSize: [{ type: Input }], customStyle: [{ type: Input }], imageSource: [{ type: Input }], roundedImage: [{ type: Input }], imageStyle: [{ type: Input }] } }); /** * CardVideoDisplay component displays a video stream with options for full display, mirroring, and background color customization. * * @selector app-card-video-display * @standalone true * @imports CommonModule * * @inputs * - `remoteProducerId` (string): Identifier for the remote producer. * - `eventType` (EventType): Type of event, such as 'webinar'. Default is 'webinar'. * - `forceFullDisplay` (boolean): Forces full video display if true. Default is false. * - `videoStream` (MediaStream | null): The media stream to display in the video element. * - `backgroundColor` (string): Background color for the video container. Default is 'transparent'. * - `doMirror` (boolean): Mirrors the video if true. Default is false. * * @methods * - `ngOnInit()`: Initializes the video stream and sets the container style on component load. * - `ngOnChanges(changes: SimpleChanges)`: Updates the video stream or container style when inputs change. * - `updateVideoStream()`: Assigns the video stream to the video element if it differs from the current stream. * - `setVideoContainerStyle()`: Sets the style of the video container based on the provided background color. * - `getBaseVideoContainerStyle()`: Returns base styles for the video container. * - `getVideoStyle()`: Returns styles for the video element, including optional mirroring and sizing. * * @example * ```html * <app-card-video-display * [remoteProducerId]="producerId" * [eventType]="'conference'" * [forceFullDisplay]="true" * [videoStream]="stream" * [backgroundColor]="'black'" * [doMirror]="true"> * </app-card-video-display> * ``` **/ class CardVideoDisplay { remoteProducerId = ''; eventType = 'webinar'; forceFullDisplay = false; videoStream = null; backgroundColor = 'transparent'; doMirror = false; videoElement; videoContainerStyle; ngOnInit() { this.updateVideoStream(); this.setVideoContainerStyle(); } ngOnChanges(changes) { if (changes['videoStream'] && this.videoStream) { const currentStream = changes['videoStream'].currentValue; const previousStream = changes['videoStream'].previousValue; if (!previousStream || currentStream.id !== previousStream.id || currentStream.active !== previousStream.active) { this.updateVideoStream(); } } if (changes['backgroundColor'] && changes['backgroundColor'].currentValue !== changes['backgroundColor'].previousValue) { this.setVideoContainerStyle(); } } updateVideoStream() { if (this.videoElement && this.videoStream) { const videoElement = this.videoElement.nativeElement; // Update the video element's srcObject only if it has changed if (videoElement.srcObject !== this.videoStream) { videoElement.srcObject = this.videoStream; } } } setVideoContainerStyle() { this.videoContainerStyle = { ...this.getBaseVideoContainerStyle(), backgroundColor: this.backgroundColor, }; } getBaseVideoContainerStyle() { return { display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%', backgroundColor: 'black', }; } getVideoStyle() { const baseStyles = { width: this.forceFullDisplay ? '100%' : 'auto', height: '100%', maxWidth: '100%', maxHeight: '100%', objectFit: this.forceFullDisplay ? 'cover' : 'contain', backgroundColor: this.backgroundColor, }; if (this.doMirror) { baseStyles.transform = 'rotateY(180deg)'; } return baseStyles; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: CardVideoDisplay, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.0.1", type: CardVideoDisplay, isStandalone: true, selector: "app-card-video-display", inputs: { remoteProducerId: "remoteProducerId", eventType: "eventType", forceFullDisplay: "forceFullDisplay", videoStream: "videoStream", backgroundColor: "backgroundColor", doMirror: "doMirror" }, viewQueries: [{ propertyName: "videoElement", first: true, predicate: ["videoElement"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: "<div [ngStyle]=\"videoContainerStyle\">\r\n <video #videoElement autoplay muted playsinline [ngStyle]=\"getVideoStyle()\"></video>\r\n</div>\r\n", styles: [".videoContainer{display:flex;justify-content:center;align-items:center;width:100%;height:100%;background-color:#000}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: CardVideoDisplay, decorators: [{ type: Component, args: [{ selector: 'app-card-video-display', imports: [CommonModule], template: "<div [ngStyle]=\"videoContainerStyle\">\r\n <video #videoElement autoplay muted playsinline [ngStyle]=\"getVideoStyle()\"></video>\r\n</div>\r\n", styles: [".videoContainer{display:flex;justify-content:center;align-items:center;width:100%;height:100%;background-color:#000}\n"] }] }], propDecorators: { remoteProducerId: [{ type: Input }], eventType: [{ type: Input }], forceFullDisplay: [{ type: Input }], videoStream: [{ type: Input }], backgroundColor: [{ type: Input }], doMirror: [{ type: Input }], videoElement: [{ type: ViewChild, args: ['videoElement', { static: true }] }] } }); /** * Gets the style for positioning an overlay based on the specified position. * * @param {GetOverlayPositionOptions} options - Contains the desired position for the overlay. * @param {string} options.position - The position for the overlay ('topLeft', 'topRight', 'bottomLeft', 'bottomRight'). * @returns {OverlayPositionStyle} - Style object defining the overlay position. * * @example * ```typescript * const topLeftPosition = getOverlayPosition({ position: 'topLeft' }); * // Output: { top: 0, left: 0 } * * const bottomRightPosition = getOverlayPosition({ position: 'bottomRight' }); * // Output: { bottom: 0, right: 0 } * ``` */ function getOverlayPosition({ position }) { switch (position) { case 'topLeft': return { top: 0, left: 0 }; case 'topRight': return { top: 0, right: 0 }; case 'bottomLeft': return { bottom: 0, left: 0 }; case 'bottomRight': return { bottom: 0, right: 0 }; default: return {}; } } /** * Controls the media of a participant in a media session if certain conditions are met. * * @param {ControlMediaOptions} options - The options for controlling media. * @param {string} options.participantId - The ID of the participant to control. * @param {string} options.participantName - The name of the participant to control. * @param {'audio' | 'video' | 'screenshare' | 'all'} options.type - The type of media to control. * @param {Socket} options.socket - The socket instance for communication. * @param {CoHostResponsibility[]} options.coHostResponsibility - List of co-host responsibilities. * @param {Participant[]} options.participants - List of participants in the session. * @param {string} options.member - The current member attempting to control media. * @param {string} options.islevel - The level of the current member. * @param {Function} [options.showAlert] - Optional function to show alerts. * @param {string} options.coHost - The co-host information. * @param {string} options.roomName - The name of the room. * * @returns {Promise<void>} A promise that resolves when the media control operation is complete. * * @throws Will log an error message if the operation fails. * * @example * ```typescript * const options = { * participantId: '12345', * participantName: 'John Doe', * type: 'audio', * socket: socketInstance, * coHostResponsibility: [{ name: 'media', value: true }], * participants: participantList, * member: 'currentMember', * islevel: '2', * showAlert: showAlertFunction, * coHost: 'coHostName', * roomName: 'Room A', * }; * * controlMediaService.controlMedia(options) * .then(() => { * console.log('Media control action completed'); * }) * .catch((error) => { * console.error('Error controlling media:', error); * }); * ``` */ class ControlMedia { /** * Controls the media of a participant in a media session if certain conditions are met. * * @param {Object} options - The options for controlling media. * @param {string} options.participantId - The ID of the participant to control. * @param {string} options.participantName - The name of the participant to control. * @param {string} options.type - The type of media to control. * @param {Socket} options.socket - The socket instance for communication. * @param {Array} options.coHostResponsibility - List of co-host responsibilities. * @param {Array} options.participants - List of participants in the session. * @param {string} options.member - The current member attempting to control media. * @param {string} options.islevel - The level of the current member. * @param {Function} [options.showAlert] - Optional function to show alerts. * @param {string} options.coHost - The co-host information. * @param {string} options.roomName - The name of the room. * * @returns {Promise<void>} A promise that resolves when the media control operation is complete. */ async controlMedia({ participantId, participantName, type, socket, coHostResponsibility, participants, member, islevel, showAlert, coHost, roomName, }) { try { // Destructure parameters let mediaValue = false; try { mediaValue = coHostResponsibility.find((item) => item.name === 'media')?.value ?? false; } catch { /* handle error */ } let participant = participants.find((obj) => obj.name === participantName); if (islevel === '2' || (coHost === member && mediaValue === true)) { // Check if the participant is not muted and is not a host if (participant && ((!participant.muted && participant.islevel !== '2' && type == 'audio') || (participant.islevel !== '2' && type == 'video' && participant['videoOn']))) { // Emit controlMedia event to the server socket.emit('controlMedia', { participantId, participantName, type, roomName }); } } else { // Display an alert if the participant is not allowed to mute other participants if (showAlert) { showAlert({ message: 'You are not allowed to control media for other participants.', type: 'danger', duration: 3000, }); } } } catch (error) { console.log('controlMedia error', error); // throw error; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ControlMedia, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ControlMedia, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.1", ngImport: i0, type: ControlMedia, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * VideoCard component represents a customizable video display card with participant controls for toggling audio and video. * It also animates an audio waveform if sound is detected in the participant's audio stream. * * @selector app-video-card * @standalone true * @imports [CommonModule, FontAwesomeModule, CardVideoDisplay] * * @example * ```html * <app-video-card * [name]="participant.name" * [videoStream]="videoStream" * [audioDecibels]="audioDecibels" * [participant]="participant" * [parameters]="videoCardParameters" * ></app-video-card> * ``` * * @input {Partial<CSSStyleDeclaration>} customStyle - Styles for the card container. * @input {string} name - Name of the participant displayed on the card. * @input {string} barColor - Color of the waveform bars. Default is 'red'. * @input {string} textColor - Color of the name text. Default is 'white'. * @input {string} imageSource - Source URL of the participant's image. * @input {boolean} roundedImage - Whether the image should have rounded corners. * @input {Partial<CSSStyleDeclaration>} imageStyle - Additional styles for the image. * @input {string} remoteProducerId - ID of the remote media producer. * @input {EventType} eventType - Ty