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
JavaScript
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