openvidu-components: Added settings panel for configuring media devices

pull/739/head
csantosm 2022-06-16 14:01:07 +02:00
parent 3908dfc146
commit c8264fdb1b
58 changed files with 1029 additions and 371 deletions

View File

@ -2,7 +2,7 @@ import { Component, OnInit, Output, EventEmitter, ChangeDetectionStrategy, Chang
import { Subscription } from 'rxjs';
import { PanelType } from '../../../models/panel.model';
import { OpenViduAngularConfigService } from '../../../services/config/openvidu-angular.config.service';
import { PanelService } from '../../../services/panel/panel.service';
import { PanelEvent, PanelService } from '../../../services/panel/panel.service';
@Component({
selector: 'ov-activities-panel',
@ -114,8 +114,8 @@ export class ActivitiesPanelComponent implements OnInit {
private subscribeToPanelToggling() {
this.panelSubscription = this.panelService.panelOpenedObs.subscribe(
(ev: { opened: boolean; type?: PanelType | string; expand?: string }) => {
if (ev.type === PanelType.ACTIVITIES) {
(ev: PanelEvent) => {
if (ev.type === PanelType.ACTIVITIES && !!ev.expand) {
this.expandedPanel = ev.expand;
}
}

View File

@ -14,6 +14,11 @@
<ng-container *ngTemplateOutlet="backgroundEffectsPanelTemplate"></ng-container>
</ng-container>
<!-- Settings panel -->
<ng-container *ngIf="isSettingsPanelOpened">
<ng-container *ngTemplateOutlet="settingsPanelTemplate"></ng-container>
</ng-container>
<!-- Activities panel -->
<ng-container *ngIf="isActivitiesPanelOpened">
<ng-container *ngTemplateOutlet="activitiesPanelTemplate"></ng-container>

View File

@ -8,7 +8,7 @@ import {
ActivitiesPanelDirective
} from '../../directives/template/openvidu-angular.directive';
import { PanelType } from '../../models/panel.model';
import { PanelService } from '../../services/panel/panel.service';
import { PanelEvent, PanelService } from '../../services/panel/panel.service';
/**
*
@ -64,6 +64,11 @@ export class PanelComponent implements OnInit {
*/
@ContentChild('backgroundEffectsPanel', { read: TemplateRef }) backgroundEffectsPanelTemplate: TemplateRef<any>;
/**
* @ignore
*/
@ContentChild('settingsPanel', { read: TemplateRef }) settingsPanelTemplate: TemplateRef<any>;
/**
* @ignore
*/
@ -91,11 +96,22 @@ export class PanelComponent implements OnInit {
set externalBackgroundEffectsPanel(externalBackgroundEffectsPanel: BackgroundEffectsPanelDirective) {
// This directive will has value only when BACKGROUND EFFECTS PANEL component tagged with '*ovBackgroundEffectsPanel'
// is inside of the PANEL component tagged with '*ovPanel'
if (externalBackgroundEffectsPanel) {
this.backgroundEffectsPanelTemplate = externalBackgroundEffectsPanel.template;
}
// TODO: backgroundEffectsPanel does not provides customization
// if (externalBackgroundEffectsPanel) {
// this.backgroundEffectsPanelTemplate = externalBackgroundEffectsPanel.template;
// }
}
// TODO: settingsPanel does not provides customization
// @ContentChild(SettingsPanelDirective)
// set externalSettingsPanel(externalSettingsPanel: SettingsPanelDirective) {
// This directive will has value only when SETTINGS PANEL component tagged with '*ovSettingsPanel'
// is inside of the PANEL component tagged with '*ovPanel'
// if (externalSettingsPanel) {
// this.settingsPanelTemplate = externalSettingsPanel.template;
// }
// }
@ContentChild(ActivitiesPanelDirective)
set externalActivitiesPanel(externalActivitiesPanel: ActivitiesPanelDirective) {
// This directive will has value only when ACTIVITIES PANEL component tagged with '*ovActivitiesPanel'
@ -126,6 +142,7 @@ export class PanelComponent implements OnInit {
isParticipantsPanelOpened: boolean;
isChatPanelOpened: boolean;
isBackgroundEffectsPanelOpened: boolean;
isSettingsPanelOpened: boolean;
isActivitiesPanelOpened: boolean;
/**
@ -150,12 +167,11 @@ export class PanelComponent implements OnInit {
}
private subscribeToPanelToggling() {
this.panelSubscription = this.panelService.panelOpenedObs
.pipe(skip(1))
.subscribe((ev: { opened: boolean; type?: PanelType | string }) => {
this.panelSubscription = this.panelService.panelOpenedObs.pipe(skip(1)).subscribe((ev: PanelEvent) => {
this.isChatPanelOpened = ev.opened && ev.type === PanelType.CHAT;
this.isParticipantsPanelOpened = ev.opened && ev.type === PanelType.PARTICIPANTS;
this.isBackgroundEffectsPanelOpened = ev.opened && ev.type === PanelType.BACKGROUND_EFFECTS;
this.isSettingsPanelOpened = ev.opened && ev.type === PanelType.SETTINGS;
this.isActivitiesPanelOpened = ev.opened && ev.type === PanelType.ACTIVITIES;
this.isExternalPanelOpened = ev.opened && ev.type !== PanelType.PARTICIPANTS && ev.type !== PanelType.CHAT;
this.cd.markForCheck();

View File

@ -0,0 +1,29 @@
.settings-container {
display: flex;
padding: 10px;
}
.item-menu {
padding-right: 5px;
border-right: 1px solid var(--ov-secondary-color);
width: 200px;
}
.item-content {
padding: 16px;
flex-grow: 1;
}
.option {
border-radius: var(--ov-panel-radius);
}
.lang-container button {
width: 100%;
}
mat-list-option[aria-selected='true'] {
background: var(--ov-light-color);
}
::ng-deep .mat-list-item-content {
padding: 5px !important;
}

View File

@ -0,0 +1,43 @@
<div class="panel-container" id="background-effects-container" fxLayout="column" fxLayoutAlign="space-evenly none">
<div class="panel-header-container" fxFlex="55px" fxLayoutAlign="start center">
<h3 class="panel-title">{{ 'PANEL.SETTINGS.TITLE' | translate }}</h3>
<button class="panel-close-button" mat-icon-button matTooltip="{{ 'PANEL.CLOSE' | translate }}" (click)="close()">
<mat-icon>close</mat-icon>
</button>
</div>
<div class="settings-container" fxFlex="100%" fxLayoutAlign="space-evenly none">
<div class="item-menu">
<mat-selection-list #settings [multiple]="false">
<mat-list-option class="option" [selected]="true" value="general">
<mat-icon mat-list-icon>manage_accounts</mat-icon>
<div mat-line>{{ 'PANEL.SETTINGS.GENERAL' | translate }}</div>
</mat-list-option>
<mat-list-option class="option" value="video">
<mat-icon mat-list-icon>videocam</mat-icon>
<div mat-line>{{ 'PANEL.SETTINGS.VIDEO' | translate }}</div>
</mat-list-option>
<mat-list-option class="option" value="audio">
<mat-icon mat-list-icon>mic</mat-icon>
<div mat-line>{{ 'PANEL.SETTINGS.AUDIO' | translate }}</div>
</mat-list-option>
</mat-selection-list>
</div>
<div class="item-content">
<div *ngIf="settings.selectedOptions.selected[0]?.value === 'general'">
<ov-nickname-input></ov-nickname-input>
<mat-list>
<mat-list-item>
<mat-icon mat-list-icon>language</mat-icon>
<div mat-line>{{ 'PANEL.SETTINGS.LANGUAGE' | translate }}:</div>
<ov-lang-selector></ov-lang-selector>
</mat-list-item>
</mat-list>
</div>
<ov-video-devices-select *ngIf="settings.selectedOptions.selected[0]?.value === 'video'"></ov-video-devices-select>
<ov-audio-devices-select *ngIf="settings.selectedOptions.selected[0]?.value === 'audio'"></ov-audio-devices-select>
</div>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SettingsPanelComponent } from './settings-panel.component';
describe('SettingsPanelComponent', () => {
let component: SettingsPanelComponent;
let fixture: ComponentFixture<SettingsPanelComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SettingsPanelComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SettingsPanelComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Component, HostListener, OnInit } from '@angular/core';
import { MatOptionSelectionChange } from '@angular/material/core';
import { PanelType } from '../../../models/panel.model';
import { PanelService } from '../../../services/panel/panel.service';
@Component({
selector: 'ov-settings-panel',
templateUrl: './settings-panel.component.html',
styleUrls: ['../panel.component.css', './settings-panel.component.css']
})
export class SettingsPanelComponent implements OnInit {
selectedOption: string;
constructor(private panelService: PanelService) {}
ngOnInit() {}
close() {
this.panelService.togglePanel(PanelType.SETTINGS);
}
}

View File

@ -11,11 +11,6 @@
flex: 1 1 auto;
}
.lang-button {
background-color: var(--ov-logo-background-color);
color: var(--ov-text-color);
}
#branding-logo {
background-color: var(--ov-logo-background-color);
border-radius: var(--ov-panel-radius);
@ -81,31 +76,6 @@ hr {
margin-bottom: 0px !important;
}
#nickname-input-container,
.device-container-element {
display: flex;
}
#nickname-input-container button,
.device-container-element button {
margin: auto 10px auto auto;
}
#nickname-input-container button.mat-button-disabled {
color: #000000 !important;
}
#nickname-input-container mat-form-field,
.device-container-element mat-form-field {
width: 100%;
margin-top: 10px;
color: #000000;
}
#nickname-input-container mat-form-field {
color: #000000;
}
.mat-form-field-appearance-fill .mat-form-field-flex {
/* background-color: var(--ov-text-color); */
border-radius: var(--ov-video-radius);
@ -118,11 +88,6 @@ hr {
display: block !important;
}
#camera-button {
border-radius: var(--ov-buttons-radius);
/* background-color: var(--ov-secondary-color) !important; */
/* color: var(--ov-text-color) !important; */
}
.join-btn-container {
width: inherit;
@ -137,37 +102,6 @@ hr {
border-radius: var(--ov-video-radius);
}
.warn-btn {
color: var(--ov-text-color);
background-color: var(--ov-warn-color) !important;
}
.active-btn {
color: var(--ov-text-color);
background-color: var(--ov-tertiary-color) !important;
}
.media-btn {
margin: auto;
}
::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
padding: 1px !important;
}
::ng-deep .mat-input-element {
caret-color: #000000;
}
::ng-deep .mat-primary .mat-option.mat-selected:not(.mat-option-disabled) {
color: #000000;
}
::ng-deep .mat-form-field-label {
color: var(--ov-panel-text-color) !important;
}
::ng-deep .mat-form-field.mat-focused .mat-form-field-ripple {
background-color: var(--ov-panel-text-color) !important;
}
@media only screen and (max-width: 480px) {
.container,

View File

@ -3,15 +3,8 @@
<!-- <span>OpenVidu Call</span> -->
<span class="spacer"></span>
<button mat-flat-button [matMenuTriggerFor]="menu" class="lang-button" *ngIf="!isMinimal">
<span>{{langSelected?.name}}</span>
<mat-icon>expand_more</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let lang of languages" (click)="onLangSelected(lang.ISO)">
<span>{{lang.name}}</span>
</button>
</mat-menu>
<ov-lang-selector *ngIf="!isMinimal"></ov-lang-selector>
</mat-toolbar>
<div class="container" id="prejoin-container" fxLayout.gt-sm="row" fxLayout.lt-md="column">
@ -50,98 +43,19 @@
<div fxFlex.gt-sm="100%" fxFlex.lt-md="33%" fxLayoutAlign="center center" fxFlexFill class="nickname-container">
<h4 *ngIf="windowSize >= 960">{{ 'PREJOIN.NICKNAME_SECTION' | translate }}</h4>
<hr *ngIf="windowSize >= 960" />
<div id="nickname-input-container">
<button mat-icon-button disabled>
<mat-icon>person</mat-icon>
</button>
<mat-form-field appearance="standard">
<mat-label>{{ 'PREJOIN.NICKNAME' | translate }}</mat-label>
<input
matInput
type="text"
maxlength="20"
[(ngModel)]="nickname"
(change)="updateNickname()"
autocomplete="off"
/>
</mat-form-field>
</div>
<!-- <mat-button-toggle-group style="border-radius: 20px">
<button mat-icon-button class="media-btn">
<mat-icon matTooltip="Mute your audio">mic</mat-icon>
</button>
<mat-button-toggle class="split-button-1 drop-down-button" [matMenuTriggerFor]="dropdownMenuOne">
<mat-icon>arrow_drop_down</mat-icon>
</mat-button-toggle>
</mat-button-toggle-group>
<mat-menu #dropdownMenuOne="matMenu">
<button mat-menu-item>One</button>
<button mat-menu-item>Two</button>
<button mat-menu-item>Three</button>
</mat-menu> -->
<ov-nickname-input></ov-nickname-input>
</div>
<div fxFlex.gt-sm="100%" fxFlex.lt-md="33%" fxLayoutAlign="center center" fxFlexFill class="buttons-container">
<h4 *ngIf="windowSize >= 960">{{ 'PREJOIN.DEVICE_SECTION' | translate }}</h4>
<hr *ngIf="windowSize >= 960" />
<!-- Camera -->
<div class="device-container-element">
<button
mat-icon-button
id="camera-button"
[disabled]="!hasVideoDevices || videoMuteChanging"
[class.warn-btn]="isVideoMuted"
(click)="toggleCam()"
>
<mat-icon *ngIf="!isVideoMuted" matTooltip="{{ 'TOOLBAR.MUTE_VIDEO' | translate }}" id="videocam"
>videocam</mat-icon
>
<mat-icon *ngIf="isVideoMuted" matTooltip="{{ 'TOOLBAR.UNMUTE_VIDEO' | translate }}" id="videocam_off"
>videocam_off</mat-icon
>
</button>
<mat-form-field>
<mat-label>{{ 'PREJOIN.VIDEO_DEVICE' | translate }}</mat-label>
<mat-select
[disabled]="isVideoMuted || !hasVideoDevices"
[value]="cameraSelected?.device"
(selectionChange)="onCameraSelected($event)"
>
<mat-option *ngFor="let camera of cameras" [value]="camera.device">
{{ camera.label }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ov-video-devices-select></ov-video-devices-select>
<!-- Microphone -->
<div class="device-container-element">
<button
mat-icon-button
id="microhpone-button"
[disabled]="!hasAudioDevices"
[class.warn-btn]="isAudioMuted"
(click)="toggleMic()"
>
<mat-icon *ngIf="!isAudioMuted" matTooltip="{{ 'TOOLBAR.MUTE_AUDIO' | translate }}" id="mic">mic</mat-icon>
<mat-icon *ngIf="isAudioMuted" matTooltip="{{ 'TOOLBAR.UNMUTE_AUDIO' | translate }}" id="mic_off"
>mic_off</mat-icon
>
</button>
<mat-form-field>
<mat-label>{{ 'PREJOIN.AUDIO_DEVICE' | translate }}</mat-label>
<mat-select
[disabled]="isAudioMuted || !hasAudioDevices"
[value]="microphoneSelected?.device"
(selectionChange)="onMicrophoneSelected($event)"
>
<mat-option *ngFor="let microphone of microphones" [value]="microphone.device">
{{ microphone.label }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<ov-audio-devices-select></ov-audio-devices-select>
</div>
<div fxFlex.gt-sm="60%" fxLayout.lt-md="column" fxLayoutAlign="center center" fxFlexFill class="join-btn-container">

View File

@ -2,7 +2,6 @@ import { Component, HostListener, OnDestroy, OnInit, Output, EventEmitter, ViewC
import { MatMenuTrigger } from '@angular/material/menu';
import { MatSelect } from '@angular/material/select';
import { PublisherProperties } from 'openvidu-browser';
import { Subscription } from 'rxjs';
import { CustomDevice } from '../../models/device.model';
import { ILogger } from '../../models/logger.model';
@ -10,15 +9,10 @@ import { PanelType } from '../../models/panel.model';
import { ParticipantAbstractModel } from '../../models/participant.model';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
import { DeviceService } from '../../services/device/device.service';
import { LayoutService } from '../../services/layout/layout.service';
import { LoggerService } from '../../services/logger/logger.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { PanelService } from '../../services/panel/panel.service';
import { ParticipantService } from '../../services/participant/participant.service';
import { StorageService } from '../../services/storage/storage.service';
import { TranslateService } from '../../services/translate/translate.service';
import { VirtualBackgroundService } from '../../services/virtual-background/virtual-background.service';
/**
* @internal
@ -39,8 +33,6 @@ export class PreJoinComponent implements OnInit, OnDestroy, AfterViewInit {
@ViewChild(MatSelect) matSelect: MatSelect;
@Output() onJoinButtonClicked = new EventEmitter<any>();
languages: { name: string; ISO: string }[] = [];
langSelected: { name: string; ISO: string };
cameras: CustomDevice[];
microphones: CustomDevice[];
cameraSelected: CustomDevice;
@ -79,15 +71,10 @@ export class PreJoinComponent implements OnInit, OnDestroy, AfterViewInit {
constructor(
private layoutService: LayoutService,
private deviceSrv: DeviceService,
private loggerSrv: LoggerService,
private openviduService: OpenViduService,
private participantService: ParticipantService,
protected panelService: PanelService,
private libService: OpenViduAngularConfigService,
private storageSrv: StorageService,
private backgroundService: VirtualBackgroundService,
private translateService: TranslateService,
protected cdkSrv: CdkOverlayService
) {
this.log = this.loggerSrv.get('PreJoinComponent');
@ -96,20 +83,8 @@ export class PreJoinComponent implements OnInit, OnDestroy, AfterViewInit {
ngOnInit() {
this.subscribeToPrejoinDirectives();
this.subscribeToLocalParticipantEvents();
this.languages = this.translateService.getLanguagesInfo();
this.langSelected = this.translateService.getLangSelected();
this.windowSize = window.innerWidth;
this.hasVideoDevices = this.deviceSrv.hasVideoDeviceAvailable();
this.hasAudioDevices = this.deviceSrv.hasAudioDeviceAvailable();
this.microphones = this.deviceSrv.getMicrophones();
this.cameras = this.deviceSrv.getCameras();
this.cameraSelected = this.deviceSrv.getCameraSelected();
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
this.isVideoMuted = this.deviceSrv.isVideoMuted();
this.isAudioMuted = this.deviceSrv.isAudioMuted();
this.isLoading = false;
}
@ -133,79 +108,6 @@ export class PreJoinComponent implements OnInit, OnDestroy, AfterViewInit {
this.panelService.closePanel();
}
async onCameraSelected(event: any) {
const videoSource = event?.value;
// Is New deviceId different from the old one?
if (this.deviceSrv.needUpdateVideoTrack(videoSource)) {
const mirror = this.deviceSrv.cameraNeedsMirror(videoSource);
//TODO: Uncomment this when replaceTrack issue is fixed
// const pp: PublisherProperties = { videoSource, audioSource: false, mirror };
// await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
// TODO: Remove this when replaceTrack issue is fixed
const pp: PublisherProperties = { videoSource, audioSource: this.microphoneSelected.device, mirror };
// Reapply Virtual Background to new Publisher if necessary
const backgroundSelected = this.backgroundService.backgroundSelected.getValue();
if (this.backgroundService.isBackgroundApplied()) {
await this.backgroundService.removeBackground();
}
await this.openviduService.republishTrack(pp);
if (this.backgroundService.isBackgroundApplied()) {
await this.backgroundService.applyBackground(this.backgroundService.backgrounds.find((b) => b.id === backgroundSelected));
}
this.deviceSrv.setCameraSelected(videoSource);
this.cameraSelected = this.deviceSrv.getCameraSelected();
}
}
async onMicrophoneSelected(event: any) {
const audioSource = event?.value;
if (this.deviceSrv.needUpdateAudioTrack(audioSource)) {
//TODO: Uncomment this when replaceTrack issue is fixed
// const pp: PublisherProperties = { audioSource, videoSource: false };
// await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
// TODO: Remove this when replaceTrack issue is fixed
const mirror = this.deviceSrv.cameraNeedsMirror(this.cameraSelected.device);
const pp: PublisherProperties = { videoSource: this.cameraSelected.device, audioSource, mirror };
await this.openviduService.republishTrack(pp);
this.deviceSrv.setMicSelected(audioSource);
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
}
}
onLangSelected(lang: string) {
this.translateService.setLanguage(lang);
this.storageSrv.setLang(lang);
this.langSelected = this.translateService.getLangSelected();
}
async toggleCam() {
this.videoMuteChanging = true;
const publish = this.isVideoMuted;
await this.openviduService.publishVideo(publish);
this.isVideoMuted = !this.isVideoMuted;
this.storageSrv.setVideoMuted(this.isVideoMuted);
if (this.isVideoMuted && this.panelService.isExternalPanelOpened()) {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
}
this.videoMuteChanging = false;
}
toggleMic() {
const publish = this.isAudioMuted;
this.openviduService.publishAudio(publish);
this.isAudioMuted = !this.isAudioMuted;
this.storageSrv.setAudioMuted(this.isAudioMuted);
}
updateNickname() {
this.nickname = this.nickname === '' ? this.participantService.getMyNickname() : this.nickname;
this.participantService.setMyNickname(this.nickname);
this.storageSrv.setNickname(this.nickname);
}
joinSession() {
this.onJoinButtonClicked.emit();
this.panelService.closePanel();

View File

@ -24,6 +24,11 @@
z-index: 1;
}
.big {
width: 650px;
max-width: 100%;
}
.mat-drawer.mat-drawer-side {
z-index: 0 !important;
}
@ -44,7 +49,8 @@
background-color: var(--ov-primary-color);
}
#toolbar-container, #footer-container {
#toolbar-container,
#footer-container {
background-color: var(--ov-primary-color);
min-width: 400px !important;
width: 100%;
@ -63,8 +69,6 @@
min-width: 400px !important;
}
.reconnecting-container {
width: 100%;
height: 100%;
@ -74,10 +78,28 @@
position: absolute;
}
@media only screen and (max-width: 600px) {
#session-container {
width: 100%;
/* position: fixed; */
}
}
::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
padding: 1px !important;
}
::ng-deep .mat-input-element {
caret-color: #000000;
}
::ng-deep .mat-primary .mat-option.mat-selected:not(.mat-option-disabled) {
color: #000000;
}
::ng-deep .mat-form-field-label {
color: var(--ov-panel-text-color) !important;
}
::ng-deep .mat-form-field.mat-focused .mat-form-field-ripple {
background-color: var(--ov-panel-text-color) !important;
}

View File

@ -1,10 +1,11 @@
<div id="session-container">
<mat-sidenav-container #videoContainer class="sidenav-container">
<mat-sidenav-container #container #videoContainer class="sidenav-container">
<mat-sidenav
#sidenav
mode="{{ sidenavMode }}"
position="end"
class="sidenav-menu"
[ngClass]="{big: settingsPanelOpened}"
fixedInViewport="true"
fixedTopGap="0"
fixedBottomGap="0"

View File

@ -1,5 +1,6 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
@ -31,12 +32,12 @@ import { TokenService } from '../../services/token/token.service';
import { ActionService } from '../../services/action/action.service';
import { Signal } from '../../models/signal.model';
import { ParticipantService } from '../../services/participant/participant.service';
import { MatSidenav } from '@angular/material/sidenav';
import { MatDrawerContainer, MatSidenav } from '@angular/material/sidenav';
import { SidenavMode } from '../../models/layout.model';
import { LayoutService } from '../../services/layout/layout.service';
import { Subscription, skip } from 'rxjs';
import { PanelType } from '../../models/panel.model';
import { PanelService } from '../../services/panel/panel.service';
import { PanelEvent, PanelService } from '../../services/panel/panel.service';
import { RecordingService } from '../../services/recording/recording.service';
import { TranslateService } from '../../services/translate/translate.service';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
@ -66,6 +67,9 @@ export class SessionComponent implements OnInit {
sideMenu: MatSidenav;
sidenavMode: SidenavMode = SidenavMode.SIDE;
settingsPanelOpened: boolean;
drawer: MatDrawerContainer;
protected readonly SIDENAV_WIDTH_LIMIT_MODE = 790;
protected menuSubscription: Subscription;
@ -87,7 +91,8 @@ export class SessionComponent implements OnInit {
protected panelService: PanelService,
private recordingService: RecordingService,
private translateService: TranslateService,
private platformService: PlatformService
private platformService: PlatformService,
private cd: ChangeDetectorRef
) {
this.log = this.loggerSrv.get('SessionComponent');
}
@ -123,6 +128,22 @@ export class SessionComponent implements OnInit {
}, 0);
}
@ViewChild('container')
set container(container: MatDrawerContainer) {
setTimeout(() => {
if (container) {
this.drawer = container;
this.drawer._contentMarginChanges.subscribe(() => {
setTimeout(() => {
this.stopUpdateLayoutInterval();
this.layoutService.update();
this.drawer.autosize = false;
}, 250);
});
}
}, 0);
}
async ngOnInit() {
if (!this.usedInPrejoinPage) {
if (!this.tokenService.getScreenToken()) {
@ -170,24 +191,31 @@ export class SessionComponent implements OnInit {
protected subscribeToTogglingMenu() {
this.sideMenu.openedChange.subscribe(() => {
if (this.updateLayoutInterval) {
clearInterval(this.updateLayoutInterval);
}
this.stopUpdateLayoutInterval();
this.layoutService.update();
});
this.sideMenu.openedStart.subscribe(() => {
this.updateLayoutInterval = setInterval(() => this.layoutService.update(), 50);
this.startUpdateLayoutInterval();
});
this.sideMenu.closedStart.subscribe(() => {
this.updateLayoutInterval = setInterval(() => this.layoutService.update(), 50);
this.startUpdateLayoutInterval();
});
this.menuSubscription = this.panelService.panelOpenedObs
.pipe(skip(1))
.subscribe((ev: { opened: boolean; type?: PanelType | string }) => {
this.menuSubscription = this.panelService.panelOpenedObs.pipe(skip(1)).subscribe((ev: PanelEvent) => {
if (this.sideMenu) {
this.settingsPanelOpened = ev.opened && ev.type === PanelType.SETTINGS;
if (this.sideMenu.opened && ev.opened) {
if (ev.type === PanelType.SETTINGS || ev.oldType === PanelType.SETTINGS) {
// Switch from SETTINGS to another panel and vice versa.
// As the SETTINGS panel will be bigger than others, the sidenav container must be updated.
// Setting autosize to 'true' allows update it.
this.drawer.autosize = true;
this.startUpdateLayoutInterval();
}
}
ev.opened ? this.sideMenu.open() : this.sideMenu.close();
}
});
@ -324,4 +352,16 @@ export class SessionComponent implements OnInit {
this.recordingService.stopRecording(event);
});
}
private startUpdateLayoutInterval() {
this.updateLayoutInterval = setInterval(() => {
this.layoutService.update();
}, 50);
}
private stopUpdateLayoutInterval() {
if (this.updateLayoutInterval) {
clearInterval(this.updateLayoutInterval);
}
}
}

View File

@ -0,0 +1,17 @@
.device-container-element mat-form-field {
width: 100%;
margin-top: 10px;
color: #000000;
}
.device-container-element button {
margin: auto 10px auto auto;
}
.device-container-element {
display: flex;
}
.warn-btn {
color: var(--ov-text-color);
background-color: var(--ov-warn-color) !important;
}

View File

@ -0,0 +1,26 @@
<div class="device-container-element">
<button
mat-icon-button
id="microhpone-button"
[disabled]="!hasAudioDevices"
[class.warn-btn]="isAudioMuted"
(click)="toggleMic()"
>
<mat-icon *ngIf="!isAudioMuted" matTooltip="{{ 'TOOLBAR.MUTE_AUDIO' | translate }}" id="mic">mic</mat-icon>
<mat-icon *ngIf="isAudioMuted" matTooltip="{{ 'TOOLBAR.UNMUTE_AUDIO' | translate }}" id="mic_off"
>mic_off</mat-icon
>
</button>
<mat-form-field>
<mat-label>{{ 'PREJOIN.AUDIO_DEVICE' | translate }}</mat-label>
<mat-select
[disabled]="isAudioMuted || !hasAudioDevices"
[value]="microphoneSelected?.device"
(selectionChange)="onMicrophoneSelected($event)"
>
<mat-option *ngFor="let microphone of microphones" [value]="microphone.device">
{{ microphone.label }}
</mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { AudioDevicesComponent } from './audio-devices.component';
describe('AudioDevicesComponent', () => {
let component: AudioDevicesComponent;
let fixture: ComponentFixture<AudioDevicesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ AudioDevicesComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(AudioDevicesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,80 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { PublisherProperties } from 'openvidu-browser';
import { DeviceService } from '../../../services/device/device.service';
import { OpenViduService } from '../../../services/openvidu/openvidu.service';
import { StorageService } from '../../../services/storage/storage.service';
import { CustomDevice } from '../../../models/device.model';
import { ParticipantAbstractModel } from '../../../models/participant.model';
import { ParticipantService } from '../../../services/participant/participant.service';
import { Subscription } from 'rxjs';
/**
* @internal
*/
@Component({
selector: 'ov-audio-devices-select',
templateUrl: './audio-devices.component.html',
styleUrls: ['./audio-devices.component.css']
})
export class AudioDevicesComponent implements OnInit, OnDestroy {
hasAudioDevices: boolean;
isAudioMuted: boolean;
microphoneSelected: CustomDevice;
microphones: CustomDevice[] = [];
private localParticipantSubscription: Subscription;
constructor(
private openviduService: OpenViduService,
private deviceSrv: DeviceService,
private storageSrv: StorageService,
protected participantService: ParticipantService
) {}
ngOnInit(): void {
this.subscribeToParticipantMediaProperties();
this.hasAudioDevices = this.deviceSrv.hasAudioDeviceAvailable();
this.microphones = this.deviceSrv.getMicrophones();
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
this.isAudioMuted = this.deviceSrv.isAudioMuted();
if (this.openviduService.isSessionConnected()) {
this.isAudioMuted = !this.participantService.isMyAudioActive();
} else {
this.isAudioMuted = this.deviceSrv.isAudioMuted();
}
}
ngOnDestroy() {
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
}
toggleMic() {
const publish = this.isAudioMuted;
this.openviduService.publishAudio(publish);
}
async onMicrophoneSelected(event: any) {
const audioSource = event?.value;
if (this.deviceSrv.needUpdateAudioTrack(audioSource)) {
//TODO: Uncomment this when replaceTrack issue is fixed
// const pp: PublisherProperties = { audioSource, videoSource: false };
// await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
// TODO: Remove this when replaceTrack issue is fixed
const mirror = this.deviceSrv.cameraNeedsMirror(this.deviceSrv.getCameraSelected().device);
const pp: PublisherProperties = { videoSource: this.deviceSrv.getCameraSelected().device, audioSource, mirror };
await this.openviduService.republishTrack(pp);
this.deviceSrv.setMicSelected(audioSource);
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
}
}
private subscribeToParticipantMediaProperties() {
this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p: ParticipantAbstractModel) => {
if (p) {
this.isAudioMuted = !p.hasAudioActive();
this.storageSrv.setAudioMuted(this.isAudioMuted);
}
});
}
}

View File

@ -0,0 +1,4 @@
.lang-button {
background-color: var(--ov-logo-background-color);
color: var(--ov-text-color);
}

View File

@ -0,0 +1,9 @@
<button mat-flat-button [matMenuTriggerFor]="menu" class="lang-button">
<span>{{langSelected?.name}}</span>
<mat-icon>expand_more</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let lang of languages" (click)="onLangSelected(lang.ISO)">
<span>{{lang.name}}</span>
</button>
</mat-menu>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LangSelectorComponent } from './lang-selector.component';
describe('LangSelectorComponent', () => {
let component: LangSelectorComponent;
let fixture: ComponentFixture<LangSelectorComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ LangSelectorComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(LangSelectorComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,26 @@
import { Component, OnInit } from '@angular/core';
import { StorageService } from '../../../services/storage/storage.service';
import { TranslateService } from '../../../services/translate/translate.service';
@Component({
selector: 'ov-lang-selector',
templateUrl: './lang-selector.component.html',
styleUrls: ['./lang-selector.component.css']
})
export class LangSelectorComponent implements OnInit {
langSelected: { name: string; ISO: string };
languages: { name: string; ISO: string }[] = [];
constructor(private translateService: TranslateService, private storageSrv: StorageService) {}
ngOnInit(): void {
this.languages = this.translateService.getLanguagesInfo();
this.langSelected = this.translateService.getLangSelected();
}
onLangSelected(lang: string) {
this.translateService.setLanguage(lang);
this.storageSrv.setLang(lang);
this.langSelected = this.translateService.getLangSelected();
}
}

View File

@ -0,0 +1,21 @@
#nickname-input-container {
display: flex;
}
#nickname-input-container button {
margin: auto 10px auto auto;
}
#nickname-input-container button.mat-button-disabled {
color: #000000 !important;
}
#nickname-input-container mat-form-field {
width: 100%;
margin-top: 10px;
color: #000000;
}
#nickname-input-container mat-form-field {
color: #000000;
}

View File

@ -0,0 +1,16 @@
<div id="nickname-input-container">
<button mat-icon-button disabled>
<mat-icon>person</mat-icon>
</button>
<mat-form-field appearance="standard">
<mat-label>{{ 'PREJOIN.NICKNAME' | translate }}</mat-label>
<input
matInput
type="text"
maxlength="20"
[(ngModel)]="nickname"
(change)="updateNickname()"
autocomplete="off"
/>
</mat-form-field>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NicknameInputComponent } from './nickname-input.component';
describe('NicknameInputComponent', () => {
let component: NicknameInputComponent;
let fixture: ComponentFixture<NicknameInputComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ NicknameInputComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(NicknameInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,36 @@
import { Component, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { ParticipantAbstractModel } from '../../../models/participant.model';
import { ParticipantService } from '../../../services/participant/participant.service';
import { StorageService } from '../../../services/storage/storage.service';
@Component({
selector: 'ov-nickname-input',
templateUrl: './nickname-input.component.html',
styleUrls: ['./nickname-input.component.css']
})
export class NicknameInputComponent implements OnInit {
nickname: string;
localParticipantSubscription: Subscription;
constructor(private participantService: ParticipantService, private storageSrv: StorageService) {}
ngOnInit(): void {
this.subscribeToParticipantProperties();
this.nickname = this.participantService.getMyNickname();
}
updateNickname() {
this.nickname = this.nickname === '' ? this.participantService.getMyNickname() : this.nickname;
this.participantService.setMyNickname(this.nickname);
this.storageSrv.setNickname(this.nickname);
}
private subscribeToParticipantProperties() {
this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p: ParticipantAbstractModel) => {
if (p) {
this.nickname = p.getNickname();
}
});
}
}

View File

@ -0,0 +1,25 @@
#camera-button {
border-radius: var(--ov-buttons-radius);
/* background-color: var(--ov-secondary-color) !important; */
/* color: var(--ov-text-color) !important; */
}
.device-container-element mat-form-field {
width: 100%;
margin-top: 10px;
color: #000000;
}
.device-container-element button {
margin: auto 10px auto auto;
}
.device-container-element {
display: flex;
}
.warn-btn {
color: var(--ov-text-color);
background-color: var(--ov-warn-color) !important;
}

View File

@ -0,0 +1,24 @@
<div class="device-container-element">
<button
mat-icon-button
id="camera-button"
[disabled]="!hasVideoDevices || videoMuteChanging"
[class.warn-btn]="isVideoMuted"
(click)="toggleCam()"
>
<mat-icon *ngIf="!isVideoMuted" matTooltip="{{ 'TOOLBAR.MUTE_VIDEO' | translate }}" id="videocam">videocam</mat-icon>
<mat-icon *ngIf="isVideoMuted" matTooltip="{{ 'TOOLBAR.UNMUTE_VIDEO' | translate }}" id="videocam_off">videocam_off</mat-icon>
</button>
<mat-form-field>
<mat-label>{{ 'PREJOIN.VIDEO_DEVICE' | translate }}</mat-label>
<mat-select
[disabled]="isVideoMuted || !hasVideoDevices"
[value]="cameraSelected?.device"
(selectionChange)="onCameraSelected($event)"
>
<mat-option *ngFor="let camera of cameras" [value]="camera.device">
{{ camera.label }}
</mat-option>
</mat-select>
</mat-form-field>
</div>

View File

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VideoDevicesComponent } from './video-devices.component';
describe('VideoDevicesComponent', () => {
let component: VideoDevicesComponent;
let fixture: ComponentFixture<VideoDevicesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ VideoDevicesComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(VideoDevicesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,102 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { PublisherProperties } from 'openvidu-browser';
import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model';
import { PanelType } from '../../../models/panel.model';
import { ParticipantAbstractModel } from '../../../models/participant.model';
import { DeviceService } from '../../../services/device/device.service';
import { OpenViduService } from '../../../services/openvidu/openvidu.service';
import { PanelService } from '../../../services/panel/panel.service';
import { ParticipantService } from '../../../services/participant/participant.service';
import { StorageService } from '../../../services/storage/storage.service';
import { VirtualBackgroundService } from '../../../services/virtual-background/virtual-background.service';
/**
* @internal
*/
@Component({
selector: 'ov-video-devices-select',
templateUrl: './video-devices.component.html',
styleUrls: ['./video-devices.component.css']
})
export class VideoDevicesComponent implements OnInit, OnDestroy {
videoMuteChanging: boolean;
isVideoMuted: boolean;
cameraSelected: CustomDevice;
hasVideoDevices: boolean;
cameras: CustomDevice[];
localParticipantSubscription: Subscription;
constructor(
private openviduService: OpenViduService,
protected panelService: PanelService,
private storageSrv: StorageService,
private deviceSrv: DeviceService,
protected participantService: ParticipantService,
private backgroundService: VirtualBackgroundService
) {}
ngOnInit(): void {
this.subscribeToParticipantMediaProperties();
this.hasVideoDevices = this.deviceSrv.hasVideoDeviceAvailable();
this.cameras = this.deviceSrv.getCameras();
this.cameraSelected = this.deviceSrv.getCameraSelected();
if (this.openviduService.isSessionConnected()) {
this.isVideoMuted = !this.participantService.getLocalParticipant().isCameraVideoActive();
} else {
this.isVideoMuted = this.deviceSrv.isVideoMuted();
}
}
async ngOnDestroy() {
this.cameras = [];
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
}
async toggleCam() {
this.videoMuteChanging = true;
const publish = this.isVideoMuted;
await this.openviduService.publishVideo(publish);
this.storageSrv.setVideoMuted(this.isVideoMuted);
if (this.isVideoMuted && this.panelService.isExternalPanelOpened()) {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
}
this.videoMuteChanging = false;
}
async onCameraSelected(event: any) {
const videoSource = event?.value;
// Is New deviceId different from the old one?
if (this.deviceSrv.needUpdateVideoTrack(videoSource)) {
const mirror = this.deviceSrv.cameraNeedsMirror(videoSource);
//TODO: Uncomment this when replaceTrack issue is fixed
// const pp: PublisherProperties = { videoSource, audioSource: false, mirror };
// await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
// TODO: Remove this when replaceTrack issue is fixed
const pp: PublisherProperties = { videoSource, audioSource: this.deviceSrv.getMicrophoneSelected().device, mirror };
// Reapply Virtual Background to new Publisher if necessary
const backgroundSelected = this.backgroundService.backgroundSelected.getValue();
if (this.backgroundService.isBackgroundApplied()) {
await this.backgroundService.removeBackground();
}
await this.openviduService.republishTrack(pp);
if (this.backgroundService.isBackgroundApplied()) {
const bgSelected = this.backgroundService.backgrounds.find((b) => b.id === backgroundSelected);
if (bgSelected) {
await this.backgroundService.applyBackground(bgSelected);
}
}
this.deviceSrv.setCameraSelected(videoSource);
this.cameraSelected = this.deviceSrv.getCameraSelected();
}
}
protected subscribeToParticipantMediaProperties() {
this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p: ParticipantAbstractModel) => {
if (p) {
this.isVideoMuted = !p.isCameraVideoActive();
}
});
}
}

View File

@ -212,7 +212,7 @@ export class StreamComponent implements OnInit {
* @ignore
*/
toggleNicknameForm() {
if (this._stream.participant.local) {
if (this._stream?.participant?.local) {
this.toggleNickname = !this.toggleNickname;
}
}
@ -260,7 +260,7 @@ export class StreamComponent implements OnInit {
this.showAudioDetection = value;
// this.cd.markForCheck();
});
this.settingsButtonSub = this.libService.settingsButtonObs.subscribe((value: boolean) => {
this.settingsButtonSub = this.libService.streamSettingsButtonObs.subscribe((value: boolean) => {
this.showSettingsButton = value;
// this.cd.markForCheck();
});

View File

@ -124,6 +124,15 @@
.mat-icon-button[disabled] {
color: #fff;
}
.divider {
margin: 8px 0px;
}
::ng-deep .mat-menu-item {
/* margin-bottom: 10px; */
height: 40px;
line-height: 40px;
}
@media (max-width: 750px) {
#session-name {
display: none;
@ -143,10 +152,8 @@
}
}
::ng-deep .mat-menu-panel {
margin-bottom: 10px;
}
@keyframes blinker {
50% { opacity: 0.3; }
50% {
opacity: 0.3;
}
}

View File

@ -102,6 +102,14 @@
<mat-icon>auto_awesome</mat-icon>
<span>{{ 'TOOLBAR.BACKGROUND' | translate }}</span>
</button>
<mat-divider class="divider" *ngIf="!isMinimal && showSettingsButton"></mat-divider>
<!-- Settings button -->
<button *ngIf="!isMinimal && showSettingsButton" mat-menu-item id="settings-btn" (click)="toggleSettings()">
<mat-icon>settings</mat-icon>
<span>{{ 'TOOLBAR.SETTINGS' | translate }}</span>
</button>
</mat-menu>
<!-- External additional buttons -->

View File

@ -15,7 +15,7 @@ import {
import { first, skip, Subscription } from 'rxjs';
import { TokenService } from '../../services/token/token.service';
import { ChatService } from '../../services/chat/chat.service';
import { PanelService } from '../../services/panel/panel.service';
import { PanelEvent, PanelService } from '../../services/panel/panel.service';
import { DocumentService } from '../../services/document/document.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
@ -272,6 +272,11 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
showRecordingButton: boolean = true;
/**
* @ignore
*/
showSettingsButton: boolean = true;
/**
* @ignore
*/
@ -344,6 +349,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
private displayLogoSub: Subscription;
private displaySessionNameSub: Subscription;
private screenSizeSub: Subscription;
private settingsButtonSub: Subscription;
private currentWindowHeight = window.innerHeight;
/**
@ -432,6 +438,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
if (this.activitiesPanelButtonSub) this.activitiesPanelButtonSub.unsubscribe();
if (this.recordingSubscription) this.recordingSubscription.unsubscribe();
if (this.screenSizeSub) this.screenSizeSub.unsubscribe();
if (this.settingsButtonSub) this.settingsButtonSub.unsubscribe();
}
/**
@ -522,6 +529,13 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
}
/**
* @ignore
*/
toggleSettings() {
this.panelService.togglePanel(PanelType.SETTINGS);
}
/**
* @ignore
*/
@ -564,8 +578,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
});
}
protected subscribeToMenuToggling() {
this.panelTogglingSubscription = this.panelService.panelOpenedObs.subscribe(
(ev: { opened: boolean; type?: PanelType | string }) => {
this.panelTogglingSubscription = this.panelService.panelOpenedObs.subscribe((ev: PanelEvent) => {
this.isChatOpened = ev.opened && ev.type === PanelType.CHAT;
this.isParticipantsOpened = ev.opened && ev.type === PanelType.PARTICIPANTS;
this.isActivitiesOpened = ev.opened && ev.type === PanelType.ACTIVITIES;
@ -573,8 +586,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.unreadMessages = 0;
}
this.cd.markForCheck();
}
);
});
}
protected subscribeToChatMessages() {
@ -590,7 +602,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p: ParticipantAbstractModel) => {
if (p) {
this.isWebcamVideoActive = p.isCameraVideoActive();
this.isAudioActive = p.isCameraAudioActive() || p.isScreenAudioActive();
this.isAudioActive = p.hasAudioActive();
this.isScreenShareActive = p.isScreenActive();
this.isSessionCreator = p.getRole() === OpenViduRole.MODERATOR;
this.cd.markForCheck();
@ -627,11 +639,17 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
this.cd.markForCheck();
});
this.recordingButtonSub = this.libService.recordingButton.subscribe((value: boolean) => {
this.recordingButtonSub = this.libService.recordingButtonObs.subscribe((value: boolean) => {
this.showRecordingButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.settingsButtonSub = this.libService.toolbarSettingsButtonObs.subscribe((value: boolean) => {
this.showSettingsButton = value;
this.checkDisplayMoreOptions();
this.cd.markForCheck();
});
this.chatPanelButtonSub = this.libService.chatPanelButtonObs.subscribe((value: boolean) => {
this.showChatPanelButton = value;
this.cd.markForCheck();
@ -661,12 +679,12 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
private subscribeToScreenSize() {
this.screenSizeSub = this.documentService.screenSizeObs.subscribe((change: MediaChange[]) => {
console.log(change[0].mqAlias)
this.screenSize = change[0].mqAlias;
});
}
private checkDisplayMoreOptions() {
this.showMoreOptionsButton = this.showFullscreenButton || this.showBackgroundEffectsButton || this.showRecordingButton;
this.showMoreOptionsButton =
this.showFullscreenButton || this.showBackgroundEffectsButton || this.showRecordingButton || this.showSettingsButton;
}
}

View File

@ -75,6 +75,10 @@
<ov-background-effects-panel id="default-background-effects-panel"></ov-background-effects-panel>
</ng-template>
<ng-template #settingsPanel>
<ov-settings-panel id="default-settings-panel"></ov-settings-panel>
</ng-template>
<ng-template #activitiesPanel>
<ng-container *ngTemplateOutlet="openviduAngularActivitiesPanelTemplate"></ng-container>
</ng-template>

View File

@ -688,7 +688,7 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
// The devices are initialized without labels in Firefox.
// We need to force an update after publisher is allowed.
if (this.deviceSrv.areEmptyLabels()) {
await this.deviceSrv.forceUpdate();
await this.deviceSrv.initializeDevices();
if (this.deviceSrv.hasAudioDeviceAvailable()) {
const audioLabel = this.participantService.getMyCameraPublisher()?.stream?.getMediaStream()?.getAudioTracks()[0]?.label;
this.deviceSrv.setMicSelected(audioLabel);

View File

@ -19,7 +19,8 @@ import {
ToolbarDisplayLogoDirective,
ToolbarActivitiesPanelButtonDirective,
ToolbarBackgroundEffectsButtonDirective,
ToolbarRecordingButtonDirective
ToolbarRecordingButtonDirective,
ToolbarSettingsButtonDirective
} from './toolbar.directive';
import {
AudioMutedDirective,
@ -47,6 +48,7 @@ import {
ToolbarActivitiesPanelButtonDirective,
ToolbarDisplaySessionNameDirective,
ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamSettingsButtonDirective,
@ -75,6 +77,7 @@ import {
ToolbarActivitiesPanelButtonDirective,
ToolbarDisplaySessionNameDirective,
ToolbarDisplayLogoDirective,
ToolbarSettingsButtonDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamSettingsButtonDirective,

View File

@ -142,8 +142,8 @@ export class StreamSettingsButtonDirective implements AfterViewInit, OnDestroy {
}
update(value: boolean) {
if (this.libService.settingsButton.getValue() !== value) {
this.libService.settingsButton.next(value);
if (this.libService.streamSettingsButton.getValue() !== value) {
this.libService.streamSettingsButton.next(value);
}
}

View File

@ -242,6 +242,65 @@ export class ToolbarBackgroundEffectsButtonDirective implements AfterViewInit, O
}
}
/**
* The **settingsButton** directive allows show/hide the settings toolbar button.
*
* Default: `true`
*
* It can be used in the parent element {@link VideoconferenceComponent} specifying the name of the `toolbar` component:
*
* @example
* <ov-videoconference [toolbarSettingsButton]="false"></ov-videoconference>
*
* \
* And it also can be used in the {@link ToolbarComponent}.
* @example
* <ov-toolbar [settingsButton]="false"></ov-toolbar>
*/
@Directive({
selector: 'ov-videoconference[toolbarSettingsButton], ov-toolbar[settingsButton]'
})
export class ToolbarSettingsButtonDirective implements AfterViewInit, OnDestroy {
/**
* @ignore
*/
@Input() set toolbarSettingsButton(value: boolean) {
this.settingsValue = value;
this.update(this.settingsValue);
}
/**
* @ignore
*/
@Input() set settingsButton(value: boolean) {
this.settingsValue = value;
this.update(this.settingsValue);
}
private settingsValue: boolean = true;
/**
* @ignore
*/
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngAfterViewInit() {
this.update(this.settingsValue);
}
ngOnDestroy(): void {
this.clear();
}
private clear() {
this.settingsValue = true;
this.update(true);
}
private update(value: boolean) {
if (this.libService.toolbarSettingsButton.getValue() !== value) {
this.libService.toolbarSettingsButton.next(value);
}
}
}
/**
* The **leaveButton** directive allows show/hide the leave toolbar button.
*

View File

@ -259,7 +259,7 @@ export class PanelDirective {
* this.subscribeToPanelToggling();
* }
* subscribeToPanelToggling() {
* this.panelService.panelOpenedObs.subscribe((ev: { opened: boolean; type?: PanelType | string }) => {
* this.panelService.panelOpenedObs.subscribe((ev: PanelEvent) => {
* this.showExternalPanel = ev.opened && ev.type === 'my-panel';
* this.showExternalPanel2 = ev.opened && ev.type === 'my-panel2';
* });

View File

@ -37,6 +37,7 @@
"BACKGROUND":"背景效果",
"START_RECORDING": "开始录音",
"STOP_RECORDING": "停止录制",
"SETTINGS":"设置",
"LEAVE":"离开会议",
"PARTICIPANTS":"参与者",
"CHAT":"聊天",
@ -64,6 +65,13 @@
"CAMERA": "摄像头",
"SCREEN": "屏幕"
},
"SETTINGS": {
"TITLE": "设置",
"GENERAL": "一般的",
"VIDEO": "视频",
"AUDIO": "声音的",
"LANGUAGE": "语"
},
"BACKGROUND": {
"TITLE": "背景效果",
"BLURRED_SECTION": "没有效果和模糊的背景",

View File

@ -37,6 +37,7 @@
"BACKGROUND": "Hintergrund-Effekte",
"START_RECORDING": "Aufzeichnung starten",
"STOP_RECORDING": "Aufzeichnung stoppen",
"SETTINGS": "Einstellungen",
"LEAVE": "Die Sitzung verlassen",
"PARTICIPANTS": "Teilnehmer",
"CHAT": "Chat",
@ -64,6 +65,13 @@
"CAMERA": "KAMERA",
"SCREEN": "BILDSCHIRM"
},
"SETTINGS": {
"TITLE": "Einstellungen",
"GENERAL": "Allgemein",
"VIDEO": "Video",
"AUDIO": "Audio",
"LANGUAGE": "Sprache"
},
"BACKGROUND": {
"TITLE": "Hintergrund-Effekte",
"BLURRED_SECTION": "Keine Effekte und unscharfer Hintergrund",

View File

@ -38,6 +38,7 @@
"BACKGROUND": "Background effects",
"START_RECORDING": "Start recording",
"STOP_RECORDING": "Stop recording",
"SETTINGS": "Settings",
"LEAVE": "Leave the session",
"PARTICIPANTS": "Participants",
"CHAT": "Chat",
@ -65,6 +66,13 @@
"CAMERA": "CAMERA",
"SCREEN": "SCREEN"
},
"SETTINGS": {
"TITLE": "Settings",
"GENERAL": "General",
"VIDEO": "Video",
"AUDIO": "Audio",
"LANGUAGE": "Language"
},
"BACKGROUND": {
"TITLE": "Background effects",
"BLURRED_SECTION": "No effects and blurred background",

View File

@ -37,6 +37,7 @@
"BACKGROUND": "Efectos de fondo",
"START_RECORDING": "Iniciar grabación",
"STOP_RECORDING": "Detener grabación",
"SETTINGS": "Configuración",
"LEAVE": "Salir de la sesión",
"PARTICIPANTS": "Participantes",
"CHAT": "Chat",
@ -64,6 +65,13 @@
"CAMERA": "CÁMARA",
"SCREEN": "PANTALLA"
},
"SETTINGS": {
"TITLE": "Configuración",
"GENERAL": "General",
"VIDEO": "Video",
"AUDIO": "Audio",
"LANGUAGE": "Idioma"
},
"BACKGROUND": {
"TITLE": "Efectos de fondo",
"BLURRED_SECTION": "Sin efectos y fondo desenfocado",

View File

@ -37,6 +37,7 @@
"BACKGROUND": "Effets de fond",
"START_RECORDING": "démarrer l'enregistrement",
"STOP_RECORDING": "Arrêter l'enregistrement",
"SETTINGS": "Paramètres",
"LEAVE": "Quitter la session",
"PARTICIPANTS": "Participants",
"CHAT": "Chat",
@ -64,6 +65,13 @@
"CAMERA": "CAMÉRA",
"SCREEN": "ÉCRAN"
},
"SETTINGS": {
"TITLE": "Paramètres",
"GENERAL": "Général",
"VIDEO": "Vidéo",
"AUDIO": "l'audio",
"LANGUAGE": "Langue"
},
"BACKGROUND": {
"TITLE": "Effets de fond",
"BLURRED_SECTION": "Aucun effet et arrière-plan flou",

View File

@ -37,6 +37,7 @@
"BACKGROUND": "पृष्ठभूमि प्रभाव",
"START_RECORDING": "रिकॉर्डिंग प्रारंभ करें",
"STOP_RECORDING": "रिकॉर्डिंग रोकें",
"SETTINGS": "सेटिंग्स",
"LEAVE": "सत्र छोड़ें",
"PARTICIPANTS": "सदस्य",
"CHAT": "बातचीत",
@ -64,6 +65,13 @@
"CAMERA": "कैमरा",
"SCREEN": "स्क्रीन"
},
"SETTINGS": {
"TITLE": "सेटिंग्स",
"GENERAL": "सामान्य",
"VIDEO": "वीडियो",
"AUDIO": "ऑडियो",
"LANGUAGE": "भाषा"
},
"BACKGROUND": {
"TITLE": "पृष्ठभूमि प्रभाव",
"BLURRED_SECTION": "कोई प्रभाव नहीं है और पृष्ठभूमि धुंधली है",

View File

@ -37,6 +37,7 @@
"BACKGROUND": "Effetti di sfondo",
"START_RECORDING": "Avvia registrazione",
"STOP_RECORDING": "Interrompi registrazione",
"SETTINGS": "Impostazioni",
"LEAVE": "Abbandona la sessione",
"PARTICIPANTS": "Partecipanti",
"CHAT": "Chat",
@ -64,6 +65,13 @@
"CAMERA": "CAMERA",
"SCREEN": "SCREEN"
},
"SETTINGS": {
"TITLE": "Impostazioni",
"GENERAL": "Generale",
"VIDEO": "video",
"AUDIO": "Audio",
"LANGUAGE":"Lingua"
},
"BACKGROUND": {
"TITLE": "Effetti di sfondo",
"BLURRED_SECTION": "Nessun effetto e sfondo sfocato",

View File

@ -37,6 +37,7 @@
"BACKGROUND": "背景効果",
"START_RECORDING": "録画開始",
"STOP_RECORDING": "録画の停止",
"SETTINGS": "設定",
"LEAVE": "セッションを終了する",
"PARTICIPANTS": "参加者",
"CHAT": "チャット",
@ -64,6 +65,13 @@
"CAMERA": "カメラ",
"SCREEN": "スクリーン"
},
"SETTINGS": {
"TITLE": "設定",
"GENERAL": "全般的",
"VIDEO": "ビデオ",
"AUDIO": "オーディオ",
"LANGUAGE":"言語"
},
"BACKGROUND": {
"TITLE": "背景効果",
"BLURRED_SECTION": "エフェクトなし、ぼやけた背景",

View File

@ -37,6 +37,7 @@
"BACKGROUND": "Achtergrondeffecten",
"START_RECORDING": "Start opname",
"STOP_RECORDING": "Stop opname",
"SETTINGS": "Instellingen",
"LEAVE": "Verlaat de sessie",
"PARTICIPANTS": "Deelnemers",
"CHAT": "Chat",
@ -64,6 +65,13 @@
"CAMERA": "CAMERA",
"SCREEN": "SCHERM"
},
"SETTINGS": {
"TITLE": "Instellingen",
"GENERAL": "Algemeen",
"VIDEO": "Video",
"AUDIO": "Audio",
"LANGUAGE":"Taal"
},
"BACKGROUND": {
"TITLE": "Achtergrondeffecten",
"BLURRED_SECTION": "Geen effecten en onscherpe achtergrond",

View File

@ -37,6 +37,7 @@
"BACKGROUND": "Efeitos de fundo",
"START_RECORDING": "Iniciar_gravação",
"STOP_RECORDING": "Parar de gravar",
"SETTINGS": "Configurações",
"LEAVE": "Sair da sessão",
"PARTICIPANTS": "Participantes",
"CHAT": "Chat",
@ -64,6 +65,13 @@
"CAMERA": "CÂMERA",
"SCREEN": "TELA"
},
"SETTINGS": {
"TITLE": "Configurações",
"GENERAL": "Em geral",
"VIDEO": "Vídeo",
"AUDIO": "Áudio",
"LANGUAGE":"Linguagem"
},
"BACKGROUND": {
"TITLE": "Efeitos de fundo",
"BLURRED_SECTION": "Sem efeitos e fundo desfocado",

View File

@ -2,6 +2,7 @@ export enum PanelType {
CHAT = 'chat',
PARTICIPANTS = 'participants',
BACKGROUND_EFFECTS = 'background-effects',
ACTIVITIES = 'activities'
ACTIVITIES = 'activities',
SETTINGS = 'settings'
}

View File

@ -95,13 +95,28 @@ export abstract class ParticipantAbstractModel {
/**
* @internal
*/
public isCameraAudioActive(): boolean {
hasAudioActive(): boolean {
const cameraConnection = this.getCameraConnection();
if (cameraConnection) {
return cameraConnection.connected && cameraConnection.streamManager?.stream?.audioActive;
}
const screenConnection = this.getScreenConnection();
if (cameraConnection.connected) {
return this.isCameraAudioActive();
} else if (screenConnection.connected) {
return this.isScreenAudioActive();
}
return false;
}
/**
* @internal
*/
private isCameraAudioActive(): boolean {
const cameraConnection = this.getCameraConnection();
if (cameraConnection?.connected) {
return cameraConnection.streamManager?.stream?.audioActive;
}
return false;
}
/**
* @internal
@ -116,8 +131,8 @@ export abstract class ParticipantAbstractModel {
*/
isScreenAudioActive(): boolean {
const screenConnection = this.getScreenConnection();
if (screenConnection) {
return screenConnection?.connected && screenConnection?.streamManager?.stream?.audioActive;
if (screenConnection?.connected) {
return screenConnection?.streamManager?.stream?.audioActive;
}
return false;
}

View File

@ -48,11 +48,16 @@ import { AvatarProfileComponent } from './components/avatar-profile/avatar-profi
import { OpenViduAngularDirectiveModule } from './directives/template/openvidu-angular.directive.module';
import { ApiDirectiveModule } from './directives/api/api.directive.module';
import { BackgroundEffectsPanelComponent } from './components/panel/background-effects-panel/background-effects-panel.component';
import { SettingsPanelComponent } from './components/panel/settings-panel/settings-panel.component';
import { ActivitiesPanelComponent } from './components/panel/activities-panel/activities-panel.component';
import { RecordingActivityComponent } from './components/panel/activities-panel/recording-activity-panel/recording-activity.component';
import { AdminDashboardComponent } from './admin/dashboard/dashboard.component';
import { AdminLoginComponent } from './admin/login/login.component';
import { AppMaterialModule } from './openvidu-angular.material.module';
import { VideoDevicesComponent } from './components/settings/video-devices/video-devices.component';
import { AudioDevicesComponent } from './components/settings/audio-devices/audio-devices.component';
import { NicknameInputComponent } from './components/settings/nickname-input/nickname-input.component';
import { LangSelectorComponent } from './components/settings/lang-selector/lang-selector.component';
@NgModule({
declarations: [
@ -78,7 +83,12 @@ import { AppMaterialModule } from './openvidu-angular.material.module';
PanelComponent,
AvatarProfileComponent,
PreJoinComponent,
VideoDevicesComponent,
AudioDevicesComponent,
NicknameInputComponent,
LangSelectorComponent,
BackgroundEffectsPanelComponent,
SettingsPanelComponent,
ActivitiesPanelComponent,
RecordingActivityComponent,
AdminDashboardComponent,
@ -118,6 +128,7 @@ import { AppMaterialModule } from './openvidu-angular.material.module';
ParticipantsPanelComponent,
ParticipantPanelItemComponent,
BackgroundEffectsPanelComponent,
SettingsPanelComponent,
ActivitiesPanelComponent,
ChatPanelComponent,
SessionComponent,

View File

@ -28,6 +28,9 @@ export class OpenViduAngularConfigService {
fullscreenButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
fullscreenButtonObs: Observable<boolean>;
toolbarSettingsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
toolbarSettingsButtonObs: Observable<boolean>;
leaveButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
leaveButtonObs: Observable<boolean>;
@ -49,8 +52,8 @@ export class OpenViduAngularConfigService {
displayParticipantNameObs: Observable<boolean>;
displayAudioDetection = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayAudioDetectionObs: Observable<boolean>;
settingsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
settingsButtonObs: Observable<boolean>;
streamSettingsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
streamSettingsButtonObs: Observable<boolean>;
participantItemMuteButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
participantItemMuteButtonObs: Observable<boolean>;
backgroundEffectsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
@ -88,10 +91,11 @@ export class OpenViduAngularConfigService {
this.displaySessionNameObs = this.displaySessionName.asObservable();
this.displayLogoObs = this.displayLogo.asObservable();
this.recordingButtonObs = this.recordingButton.asObservable();
this.toolbarSettingsButtonObs = this.toolbarSettingsButton.asObservable();
//Stream observables
this.displayParticipantNameObs = this.displayParticipantName.asObservable();
this.displayAudioDetectionObs = this.displayAudioDetection.asObservable();
this.settingsButtonObs = this.settingsButton.asObservable();
this.streamSettingsButtonObs = this.streamSettingsButton.asObservable();
// Participant item observables
this.participantItemMuteButtonObs = this.participantItemMuteButton.asObservable();
// Recording activity observables
@ -107,7 +111,7 @@ export class OpenViduAngularConfigService {
return this.configuration;
}
isProduction(): boolean {
return this.configuration?.production;
return this.configuration?.production || false;
}
hasParticipantFactory(): boolean {

View File

@ -39,16 +39,15 @@ export class DeviceService {
private libSrv: OpenViduAngularConfigService
) {
this.log = this.loggerSrv.get('DevicesService');
}
async forceUpdate() {
this.cameras = [];
this.microphones = [];
await this.initializeDevices();
}
// async forceUpdate() {
// await this.initializeDevices();
// }
async initializeDevices() {
this.cameras = [];
this.microphones = [];
try {
this.OV = new OpenVidu();
// Forcing media permissions request.

View File

@ -76,6 +76,10 @@ export class OpenViduService {
this.ovEdition = edition;
}
isSessionConnected(): boolean {
return !!this.webcamSession.connection;
}
/**
* @internal
*/

View File

@ -4,6 +4,13 @@ import { ILogger } from '../../models/logger.model';
import { PanelType } from '../../models/panel.model';
import { LoggerService } from '../logger/logger.service';
export interface PanelEvent {
opened: boolean;
type?: PanelType | string;
expand?: string;
oldType?: PanelType | string;
}
@Injectable({
providedIn: 'root'
})
@ -11,14 +18,15 @@ export class PanelService {
/**
* Panel Observable which pushes the panel status in every update.
*/
panelOpenedObs: Observable<{ opened: boolean; type?: PanelType | string }>;
panelOpenedObs: Observable<PanelEvent>;
protected log: ILogger;
protected isChatOpened: boolean = false;
protected isParticipantsOpened: boolean = false;
protected isActivitiesOpened: boolean = false;
private isExternalOpened: boolean = false;
private externalType: string;
protected _panelOpened = <BehaviorSubject<{ opened: boolean; type?: PanelType | string, expand?: string }>>new BehaviorSubject({ opened: false });
protected _panelOpened = <BehaviorSubject<PanelEvent>>new BehaviorSubject({ opened: false });
private panelMap: Map<string, boolean> = new Map();
/**
* @internal
@ -26,6 +34,7 @@ export class PanelService {
constructor(protected loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('PanelService');
this.panelOpenedObs = this._panelOpened.asObservable();
Object.values(PanelType).forEach((panel) => this.panelMap.set(panel, false));
}
/**
@ -33,45 +42,43 @@ export class PanelService {
* If the type is differente, it will switch to the properly panel.
*/
togglePanel(type: PanelType | string, expand?: string) {
let nextOpenedValue: boolean = false;
if (this.panelMap.has(type)) {
this.log.d(`Toggling ${type} menu`);
let opened: boolean;
if (type === PanelType.CHAT) {
this.isChatOpened = !this.isChatOpened;
this.isParticipantsOpened = false;
this.isExternalOpened = false;
this.isActivitiesOpened = false
opened = this.isChatOpened;
} else if (type === PanelType.PARTICIPANTS) {
this.isParticipantsOpened = !this.isParticipantsOpened;
this.isChatOpened = false;
this.isExternalOpened = false;
this.isActivitiesOpened = false;
opened = this.isParticipantsOpened;
} else if (type === PanelType.ACTIVITIES) {
this.isActivitiesOpened = !this.isActivitiesOpened;
this.isChatOpened = false;
this.isExternalOpened = false;
this.isParticipantsOpened = false;
opened = this.isActivitiesOpened;
this.panelMap.forEach((opened: boolean, panel: string) => {
if (panel === type) {
// Toggle panel
this.panelMap.set(panel, !opened);
nextOpenedValue = !opened;
} else {
// Close others
this.panelMap.set(panel, false);
}
});
} else {
// Panel is external
this.log.d('Toggling external panel');
this.isChatOpened = false;
this.isParticipantsOpened = false;
this.isActivitiesOpened = false;
// Open when is close or is opened with another type
// Open when is closed or is opened with another type
this.isExternalOpened = !this.isExternalOpened || this.externalType !== type;
this.externalType = !this.isExternalOpened ? '' : type;
opened = this.isExternalOpened;
nextOpenedValue = this.isExternalOpened;
}
this._panelOpened.next({ opened, type, expand });
const oldType = this._panelOpened.getValue().type;
this._panelOpened.next({ opened: nextOpenedValue, type, expand, oldType });
}
/**
* @internal
*/
isPanelOpened(): boolean {
return this.isChatPanelOpened() || this.isParticipantsPanelOpened() || this.isActivitiesPanelOpened() || this.isExternalPanelOpened();
return (
this.isChatPanelOpened() || this.isParticipantsPanelOpened() || this.isActivitiesPanelOpened() || this.isExternalPanelOpened()
);
}
/**

View File

@ -196,7 +196,7 @@ export class ParticipantService {
}
isMyAudioActive(): boolean {
return this.localParticipant?.isCameraAudioActive() || this.localParticipant?.isScreenAudioActive();
return this.localParticipant?.hasAudioActive();
}
/**

View File

@ -26,6 +26,7 @@ export * from './lib/components/videoconference/videoconference.component';
export * from './lib/components/toolbar/toolbar.component';
export * from './lib/components/panel/panel.component';
export * from './lib/components/panel/chat-panel/chat-panel.component';
export * from './lib/components/panel/settings-panel/settings-panel.component';
export * from './lib/components/panel/background-effects-panel/background-effects-panel.component';
export * from './lib/components/panel/activities-panel/activities-panel.component';
export * from './lib/components/panel/participants-panel/participants-panel/participants-panel.component';