UNPKG

mediasfu-angular

Version:

mediasfu-angular - Angular 17/18/19 WebRTC SDK for video conferencing, webinars, broadcasts, live streaming, chat, recording, whiteboard, and AI agents. Prebuilt rooms and fully custom UIs.

928 lines (917 loc) 4.17 MB
import * as i0 from '@angular/core'; import { Injectable, Input, Optional, Inject, Component, ViewChild, HostListener, Injector, EventEmitter, Output, ContentChildren, forwardRef, ViewEncapsulation, InjectionToken, TemplateRef, Directive } from '@angular/core'; import { joinConsumeRoom, producerClosed, newPipeProducer, getOverlayPosition as getOverlayPosition$1, translationRoomConfig, translationConfigUpdated, translationLanguageSet, translationSubscribed, translationUnsubscribed, translationProducerReady, translationProducerClosed, translationChannelsAvailable, translationMemberState, translationError, translationTranscript, translationSpeakerOutputChanged, controlMedia, isSubtitleExpired, autoAdjust, calculateRowsAndColumns, changeVids, checkGrid, checkPermission, checkScreenShare, closeAndResize, compareActiveNames, compareScreenStates, connectIps, connectLocalIps, connectRecvTransport, connectSendTransport, connectSendTransportAudio, connectSendTransportScreen, connectSendTransportVideo, createSendTransport, disconnectSendTransportAudio, disconnectSendTransportVideo, disconnectSendTransportScreen, dispStreams, generatePageContent, getEstimate, getPipedProducersAlt, getProducersPiped, getVideos, mixStreams, onScreenChanges, processConsumerTransports, processConsumerTransportsAudio, readjust, receiveAllPipedTransports, reorderStreams, rePort, requestScreenShare, resumePauseAudioStreams, resumePauseStreams, resumeSendTransportAudio, reUpdateInter, signalNewConsumerTransport, startShareScreen, stopShareScreen, streamSuccessAudio, streamSuccessAudioSwitch, streamSuccessScreen, switchUserAudio, clickVideo, switchUserVideo, switchUserVideoAlt, trigger, updateMiniCardsGrid, updateParticipantAudioDecibels, aParams, hParams, screenParams, vParams, launchBackground, launchBreakoutRooms, breakoutRoomUpdated, launchCoHost as launchCoHost$1, modifyCoHostSettings, launchDisplaySettings, modifyDisplaySettings, launchConfirmExit, confirmExit, launchMediaSettings, launchMenuModal, launchMessages, launchParticipants, messageParticipants, muteParticipants, removeParticipants, handleCreatePoll, handleEndPoll, handleVotePoll, launchPoll, pollUpdated, checkPauseState, checkResumeState, confirmRecording, launchRecording, recordPauseTimer, recordResumeTimer, recordStartTimer, recordUpdateTimer, startRecording, stopRecording, updateRecording, launchRequests, respondToRequests, launchSettings, modifySettings, clickAudio, clickChat, clickScreenShare, switchAudio, switchVideo, switchVideoAlt, joinRoomOnMediaSFU, formatNumber, getModalPosition as getModalPosition$1, sleep as sleep$1, validateAlphanumeric, launchWaiting, respondToWaiting, launchConfigureWhiteboard, createDeviceClient, joinRoomClient, updateRoomParametersClient, joinConRoom, joinRoom, checkMediasfuURL, joinLocalRoom, allMembers, allMembersRest, allWaitingRoomMembers, banParticipant, controlMediaHost, disconnect, disconnectUserSelf, getDomains, hostRequestResponse, meetingEnded, meetingStillThere, meetingTimeRemaining, participantRequested, personJoined, producerMediaClosed, producerMediaPaused, producerMediaResumed, reInitiateRecording, receiveMessage, recordingNotice, roomRecordParams, screenProducerId, startRecords, stoppedRecording, timeLeftRecording, updateConsumingDomains, updateMediaSettings, updatedCoHost, userWaiting, connectSocket, connectLocalSocket, disconnectSocket, checkLimitsAndMakeRequest, createRoomOnMediaSFU, SUPPORTED_LANGUAGE_CODES as SUPPORTED_LANGUAGE_CODES$1, isLanguageSupported as isLanguageSupported$1, normalizeLanguageCode as normalizeLanguageCode$1, getLanguageName as getLanguageName$1, getLanguageNativeName as getLanguageNativeName$1, getSupportedLanguages as getSupportedLanguages$1, getLanguageMetadata, addPanelist, removePanelist, focusPanelists, updateParticipantPermission, bulkUpdateParticipantPermissions, updatePermissionConfig, isSpeakerInMyBreakoutRoom, pauseOriginalProducer, resumeOriginalProducer, isConsumingTranslationForSpeaker, getActiveTranslationConsumers, findOriginalProducerForSpeaker, stopConsumingTranslation, syncTranslationStateAfterBreakoutChange, panelistsUpdated, panelistFocusChanged, panelistControlMedia, addedAsPanelist, removedFromPanelists, permissionUpdated, permissionConfigUpdated, receiveRoomMessages, createResponseJoinRoom as createResponseJoinRoom$1, createLiveSubtitle, SoundPlayer as SoundPlayer$1 } from 'mediasfu-shared'; export { createLiveSubtitle, isSubtitleExpired } from 'mediasfu-shared'; 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, faClock, faHome, faStar, faLock, faChevronLeft, faChevronRight, faChevronUp, faChevronDown, faCamera, faPhotoFilm, faSpinner, faCopy, faEye, faEyeSlash, faEnvelope, faBars, faPaperPlane, faReply, faQrcode, faClosedCaptioning, faHeadphones, faLanguage, faComment, faTrash, faDotCircle, faCircle, faCheck, faDesktop, faComments, faPencilAlt, faPaintBrush, faShapes, faEraser, faSearch, faSearchPlus, faSearchMinus, faFont, faTextHeight, faUndo, faRedo, faMousePointer, faUser, faHandPaper, faUpload, faCog, faExclamationTriangle, faSignOutAlt, faDisplay, faExpand, faGaugeHigh, faWaveSquare, faMessage, faBan, faCheckCircle, faArrowLeft, faMoon, faSun, faLink, faShareNodes, faRecordVinyl, faPlayCircle, faPauseCircle, faStopCircle, faUserPlus, faTools, faPoll, faUserFriends, faChalkboardTeacher, faSync, faPhone, faShareAlt, faChartBar, faUserTie, faMinus, faUserShield } from '@fortawesome/free-solid-svg-icons'; import { BehaviorSubject, combineLatest } from 'rxjs'; import * as SelfieSegmentationPackage from '@mediapipe/selfie_segmentation'; import * as i2$1 from '@angular/forms'; import { FormsModule, NG_VALUE_ACCESSOR, Validators, ReactiveFormsModule } from '@angular/forms'; import { faFacebook, faTwitter, faWhatsapp, faTelegram } from '@fortawesome/free-brands-svg-icons'; import { BehaviorSubject as BehaviorSubject$1 } from 'rxjs/internal/BehaviorSubject'; import * as i1$1 from 'ngx-cookie-service'; import { CookieService } from 'ngx-cookie-service'; import * as i3 from '@zxing/ngx-scanner'; import { ZXingScannerModule } from '@zxing/ngx-scanner'; import { Socket } from 'socket.io-client'; /** * @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 { async joinConsumeRoom({ remote_sock, apiToken, apiUserName, parameters, }) { return joinConsumeRoom({ remote_sock, apiToken, apiUserName, parameters, }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: JoinConsumeRoom, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: JoinConsumeRoom, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: JoinConsumeRoom, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * @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 { async producerClosed({ remoteProducerId, parameters, }) { return producerClosed({ remoteProducerId, parameters: parameters, }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ProducerClosed, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ProducerClosed, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ProducerClosed, 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 { async newPipeProducer({ producerId, islevel, nsock, parameters, }) { return newPipeProducer({ producerId, islevel, nsock, parameters, }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: NewPipeProducer, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: NewPipeProducer, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: NewPipeProducer, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * 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 = true; imageStyle = {}; imageLoadFailed = false; 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 ?? {}; } ngOnChanges(changes) { if (changes['imageSource']) { this.imageLoadFailed = false; } } getMergedCardStyles() { return { display: 'flex', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%', color: '#ffffff', fontFamily: "var(--ms-modern-font-family, 'Segoe UI', 'Aptos', 'Trebuchet MS', sans-serif)", overflow: 'hidden', background: 'transparent', ...this.customStyle, }; } getAvatarStyle() { return { width: 'min(140px, 82%)', height: 'min(140px, 82%)', maxWidth: '82%', maxHeight: '82%', display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative', overflow: 'hidden', borderRadius: this.roundedImage ? '50%' : '24px', background: this.hasRenderableImage ? 'rgba(15, 23, 42, 0.18)' : 'linear-gradient(135deg, rgba(79, 70, 229, 0.92) 0%, rgba(20, 184, 166, 0.88) 55%, rgba(245, 158, 11, 0.82) 100%)', border: '1px solid rgba(255, 255, 255, 0.22)', boxShadow: '0 18px 34px rgba(15, 23, 42, 0.18)', }; } getMergedImageStyles() { return { width: '100%', height: '100%', objectFit: 'cover', borderRadius: this.roundedImage ? '50%' : '24px', ...this.imageStyle, }; } getInitialsStyle() { return { textAlign: 'center', 'font-size': this.getDisplayFontSize() + 'px', fontWeight: '800', letterSpacing: this.resolvedInitials.length > 4 ? '0.02em' : '0.08em', textTransform: 'none', lineHeight: '1', color: '#ffffff', textShadow: '0 2px 10px rgba(15, 23, 42, 0.22)', }; } get resolvedInitials() { const trimmedLabel = (this.initials || '').trim(); if (!trimmedLabel) { return ''; } return trimmedLabel.length > 10 ? trimmedLabel.substring(0, 10) : trimmedLabel; } getDisplayFontSize() { if (this.resolvedInitials.length > 8) { return Math.min(this.fontSize, 12); } if (this.resolvedInitials.length > 5) { return Math.min(this.fontSize, 14); } return this.fontSize; } get hasRenderableImage() { return Boolean(this.imageSource) && !this.imageLoadFailed; } handleImageError() { this.imageLoadFailed = true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", 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.2.20", type: MiniCard, isStandalone: true, selector: "app-mini-card", inputs: { initials: "initials", fontSize: "fontSize", customStyle: "customStyle", imageSource: "imageSource", roundedImage: "roundedImage", imageStyle: "imageStyle" }, usesOnChanges: true, ngImport: i0, template: ` <div class="mini-card" [ngStyle]="getMergedCardStyles()"> <div class="mini-card__avatar" [ngStyle]="getAvatarStyle()"> <ng-container *ngIf="hasRenderableImage; else noImage"> <img [src]="imageSource" alt="Profile" class="mini-card__image" [ngStyle]="getMergedImageStyles()" (error)="handleImageError()" /> </ng-container> <ng-template #noImage> <div class="mini-card__initials" [ngStyle]="getInitialsStyle()"> {{ resolvedInitials }} </div> </ng-template> <span class="mini-card__gloss" aria-hidden="true"></span> </div> </div> `, isInline: true, styles: [":host{display:block;width:100%;height:100%}.mini-card{display:flex;justify-content:center;align-items:center;width:100%;height:100%;color:#fff;font-family:var(--ms-modern-font-family, \"Segoe UI\", \"Aptos\", \"Trebuchet MS\", sans-serif);overflow:hidden}.mini-card__avatar{display:flex;justify-content:center;align-items:center;position:relative}.mini-card__image{width:100%;height:100%;object-fit:cover}.mini-card__initials{position:relative;z-index:1;font-weight:800}.mini-card__gloss{position:absolute;inset:0;background:linear-gradient(135deg,rgba(255,255,255,.18) 0%,transparent 55%);pointer-events:none}\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.2.20", ngImport: i0, type: MiniCard, decorators: [{ type: Component, args: [{ selector: 'app-mini-card', imports: [CommonModule], template: ` <div class="mini-card" [ngStyle]="getMergedCardStyles()"> <div class="mini-card__avatar" [ngStyle]="getAvatarStyle()"> <ng-container *ngIf="hasRenderableImage; else noImage"> <img [src]="imageSource" alt="Profile" class="mini-card__image" [ngStyle]="getMergedImageStyles()" (error)="handleImageError()" /> </ng-container> <ng-template #noImage> <div class="mini-card__initials" [ngStyle]="getInitialsStyle()"> {{ resolvedInitials }} </div> </ng-template> <span class="mini-card__gloss" aria-hidden="true"></span> </div> </div> `, styles: [":host{display:block;width:100%;height:100%}.mini-card{display:flex;justify-content:center;align-items:center;width:100%;height:100%;color:#fff;font-family:var(--ms-modern-font-family, \"Segoe UI\", \"Aptos\", \"Trebuchet MS\", sans-serif);overflow:hidden}.mini-card__avatar{display:flex;justify-content:center;align-items:center;position:relative}.mini-card__image{width:100%;height:100%;object-fit:cover}.mini-card__initials{position:relative;z-index:1;font-weight:800}.mini-card__gloss{position:absolute;inset:0;background:linear-gradient(135deg,rgba(255,255,255,.18) 0%,transparent 55%);pointer-events:none}\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: this.backgroundColor || 'rgba(8, 17, 32, 0.92)', borderRadius: 'inherit', overflow: 'hidden', position: 'relative', }; } getVideoStyle() { const baseStyles = { width: '100%', height: '100%', maxWidth: '100%', maxHeight: '100%', objectFit: this.forceFullDisplay ? 'cover' : 'contain', backgroundColor: this.backgroundColor || 'transparent', display: 'block', }; if (this.doMirror) { baseStyles.transform = 'rotateY(180deg)'; } return baseStyles; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CardVideoDisplay, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", 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 class=\"videoContainer\" [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:radial-gradient(circle at top,#94a3b82e,#081120f5);border-radius:inherit;overflow:hidden}video{width:100%;height:100%;display:block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: CardVideoDisplay, decorators: [{ type: Component, args: [{ selector: 'app-card-video-display', imports: [CommonModule], template: "<div class=\"videoContainer\" [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:radial-gradient(circle at top,#94a3b82e,#081120f5);border-radius:inherit;overflow:hidden}video{width:100%;height:100%;display:block}\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 }] }] } }); class SubtitleOverlayComponent { subtitle = null; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SubtitleOverlayComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.20", type: SubtitleOverlayComponent, isStandalone: true, selector: "app-subtitle-overlay", inputs: { subtitle: "subtitle" }, ngImport: i0, template: ` <div *ngIf="subtitle" class="subtitle-overlay"> <div class="subtitle-overlay__shell"> <span class="subtitle-overlay__text">{{ subtitle }}</span> </div> </div> `, isInline: true, styles: [".subtitle-overlay{position:absolute;left:10px;right:10px;bottom:12px;display:flex;justify-content:center;z-index:3;pointer-events:none}.subtitle-overlay__shell{max-width:90%;padding:8px 12px;background:#0009;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.15);border-radius:12px}.subtitle-overlay__text{color:#fff;font-size:13px;font-weight:500;text-shadow:0 1px 3px rgba(0,0,0,.5);text-align:center;display:-webkit-box;overflow:hidden;-webkit-line-clamp:3;-webkit-box-orient:vertical}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: SubtitleOverlayComponent, decorators: [{ type: Component, args: [{ selector: 'app-subtitle-overlay', standalone: true, imports: [CommonModule], template: ` <div *ngIf="subtitle" class="subtitle-overlay"> <div class="subtitle-overlay__shell"> <span class="subtitle-overlay__text">{{ subtitle }}</span> </div> </div> `, styles: [".subtitle-overlay{position:absolute;left:10px;right:10px;bottom:12px;display:flex;justify-content:center;z-index:3;pointer-events:none}.subtitle-overlay__shell{max-width:90%;padding:8px 12px;background:#0009;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.15);border-radius:12px}.subtitle-overlay__text{color:#fff;font-size:13px;font-weight:500;text-shadow:0 1px 3px rgba(0,0,0,.5);text-align:center;display:-webkit-box;overflow:hidden;-webkit-line-clamp:3;-webkit-box-orient:vertical}\n"] }] }], propDecorators: { subtitle: [{ type: Input }] } }); /** * 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 }) { return getOverlayPosition$1({ position }); } class TranslationReceiveMethods { async translationRoomConfig(options) { return translationRoomConfig(options); } async translationConfigUpdated(options) { return translationConfigUpdated(options); } async translationLanguageSet(options) { return translationLanguageSet(options); } async translationSubscribed(options) { return translationSubscribed(options); } async translationUnsubscribed(options) { return translationUnsubscribed(options); } async translationProducerReady(options) { return translationProducerReady(options); } async translationProducerClosed(options) { return translationProducerClosed(options); } async translationChannelsAvailable(options) { return translationChannelsAvailable(options); } async translationMemberState(options) { return translationMemberState(options); } async translationError(options) { return translationError(options); } async translationTranscript(options) { return translationTranscript(options); } async translationSpeakerOutputChanged(options) { return translationSpeakerOutputChanged(options); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: TranslationReceiveMethods, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: TranslationReceiveMethods, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: TranslationReceiveMethods, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); /** * 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, }) { return 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.2.20", ngImport: i0, type: ControlMedia, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ControlMedia, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: ControlMedia, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); class LiveSubtitleService { liveSubtitlesSubject = new BehaviorSubject(new Map()); showSubtitlesOnCardsSubject = new BehaviorSubject(true); liveSubtitles$ = this.liveSubtitlesSubject.asObservable(); showSubtitlesOnCards$ = this.showSubtitlesOnCardsSubject.asObservable(); setLiveSubtitles(subtitles) { this.liveSubtitlesSubject.next(subtitles); } getLiveSubtitles() { return this.liveSubtitlesSubject.getValue(); } setShowSubtitlesOnCards(show) { this.showSubtitlesOnCardsSubject.next(show); } getShowSubtitlesOnCards() { return this.showSubtitlesOnCardsSubject.getValue(); } getSubtitleForSpeaker(speakerId, speakerName) { const subtitles = this.liveSubtitlesSubject.getValue(); if (speakerId && subtitles.has(speakerId)) { return subtitles.get(speakerId) || null; } if (speakerName && subtitles.has(speakerName)) { return subtitles.get(speakerName) || null; } return null; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: LiveSubtitleService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: LiveSubtitleService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.20", ngImport: i0, type: LiveSubtitleService, 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 - Type of event (used for internal logic). * @input {boolean} forceFullDisplay - Forces full display if true. * @input {MediaStream | null} videoStream - Stream of the video to be displayed. * @input {boolean} showControls - Determines if the controls are displayed. Default is true. * @input {boolean} showInfo - Determines if info (e.g., participant name) is shown. Default is true. * @input {HTMLElement | CustomComponent} videoInfoComponent - Custom component for video info display. * @input {HTMLElement | CustomComponent} videoControlsComponent - Custom component for video controls. * @input {'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'} controlsPosition - Position of controls overlay. * @input {'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'} infoPosition - Position of info overlay. * @input {Participant} participant - Participant data object. * @input {string} backgroundColor - Background color of the video card. * @input {AudioDecibels[]} audioDecibels - Audio decibel data for animating waveform. * @input {boolean} doMirror - If true, mirrors the video display. * @input {VideoCardParameters} parameters - Additional parameters including socket and alert configuration. * * @property {number[]} waveformAnimations - Array representing animation states for waveform bars. * @property {boolean} showWaveform - Flag to toggle waveform animation. Default is true. * @property {any} interval - Interval reference for audio decibel checks. * @property {IconDefinition} faMicrophone - FontAwesome icon for microphone. * @property {IconDefinition} faMicrophoneSlash - FontAwesome icon for muted microphone. * @property {IconDefinition} faVideo - FontAwesome icon for video. * @property {IconDefinition} faVideoSlash - FontAwesome icon for video off. * * @method ngOnInit - Lifecycle hook to initialize audio decibel interval check. * @method ngOnDestroy - Lifecycle hook to clear intervals. * @method animateWaveform - Starts audio waveform animation. * @method resetWaveform - Resets waveform animations. * @method getAnimationDuration - Returns animation duration for given bar index. * @method toggleAudio - Toggles participant's audio status. * @method toggleVideo - Toggles participant's video status. * @method renderControls - Renders the control buttons (audio and video) based on participant status. * @method getOverlayPosition - Returns overlay position styles based on the input position string. * @method isCustomComponent - Checks if a component is a custom component. * @method isFunctionComponent - Checks if a component is a function component. */ class VideoCard { controlMediaService; liveSubtitleService; customStyle = {}; name; barColor = 'red'; textColor = 'white'; imageSource; roundedImage = false; imageStyle = {}; remoteProducerId; eventType; forceFullDisplay; videoStream = null; showControls = true; showInfo = true; videoInfoComponent; videoControlsComponent; controlsPosition = 'topLeft'; infoPosition = 'topRight'; participant; backgroundColor; audioDecibels = []; doMirror; parameters; waveformAnimations = Array.from({ length: 5 }, () => 3); showWaveform = false; audioLevelInterval; waveformInterval; isDarkModeEnabled = false; faMicrophone = faMicrophone; faMicrophoneSlash = faMicrophoneSlash; faVideo = faVideo; faVideoSlash = faVideoSlash; constructor(controlMediaService, liveSubtitleService, injectedCustomStyle, injectedName, injectedBarColor, injectedTextColor, injectedImageSource, injectedRoundedImage, injectedImageStyle, injectedRemoteProducerId, injectedEventType, injectedForceFullDisplay, injectedVideoStream, injectedShowControls, injectedShowInfo, injectedVideoInfoComponent, injectedVideoControlsComponent, injectedControlsPosition, injectedInfoPosition, injectedParticipant, injectedBackgroundColor, injectedAudioDecibels, injectedDoMirror, injectedParameters) { this.controlMediaService = controlMediaService; this.liveSubtitleService = liveSubtitleService; this.customStyle = injectedCustomStyle || this.customStyle; this.name = injectedName || this.name; this.barColor = injectedBarColor || this.barColor; this.textColor = injectedTextColor || this.textColor; this.imageSource = injectedImageSource || this.imageSource; this.roundedImage = injectedRoundedImage || this.roundedImage; this.imageStyle = injectedImageStyle || this.imageStyle; this.remoteProducerId = injectedRemoteProducerId || this.remoteProducerId; this.eventType = injectedEventType || this.eventType; this.forceFullDisplay = injectedForceFullDisplay || this.forceFullDisplay; this.videoStream = injectedVideoStream || this.videoStream; this.showControls = injectedShowControls != null ? injectedShowControls : this.showControls; this.showInfo = injectedShowInfo != null ? injectedShowInfo : this.showInfo; this.videoInfoComponent = injectedVideoInfoComponent || this.videoInfoComponent; this.videoControlsComponent = injectedVideoControlsComponent || this.videoControlsComponent; this.controlsPosition = injectedControlsPosition || this.controlsPosition; this.infoPosition = injectedInfoPosition || this.infoPosition; this.participant = injectedParticipant || this.participant; this.backgroundColor = injectedBackgroundColor || this.backgroundColor; this.audioDecibels = injectedAudioDecibels || this.audioDecibels; this.doMirror = injectedDoMirror || this.doMirror; this.parameters = injectedParameters || this.parameters; } get liveSubtitleText() { if (!this.liveSubtitleService.getShowSubtitlesOnCards()) { return null; } const speakerId = this.participant?.id || ''; const speakerName = this.participant?.name || this.name || ''; const subtitle = this.liveSubtitleService.getSubtitleForSpeaker(speakerId, speakerName); if (!subtitle || isSubtitleExpired(subtitle)) { return null; } return subtitle.text; } ngOnInit() { this.syncThemeMode(this.parameters?.getUpdatedAllParams?.()); this.audioLevelInterval = setInterval(() => { const params = this.parameters.getUpdatedAllParams(); const { audioDecibels, participants } = params; this.syncThemeMode(params); const existingEntry = audioDecibels && audioDecibels.find((entry) => entry.name === this.name); const part