openvidu-components: Imrpoved user settings

Fixed some bugs with replacing tracks and devices saved on storage
Minor refactoring
pull/707/head
csantosm 2022-02-21 17:33:23 +01:00
parent b437547501
commit f40093746f
12 changed files with 280 additions and 234 deletions

View File

@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { PublisherSpeakingEvent, StreamManager } from 'openvidu-browser'; import { PublisherSpeakingEvent, StreamManager } from 'openvidu-browser';
@ -7,19 +7,22 @@ import { PublisherSpeakingEvent, StreamManager } from 'openvidu-browser';
templateUrl: './audio-wave.component.html', templateUrl: './audio-wave.component.html',
styleUrls: ['./audio-wave.component.css'] styleUrls: ['./audio-wave.component.css']
}) })
export class AudioWaveComponent implements OnInit { export class AudioWaveComponent implements OnInit, OnDestroy {
isSpeaking: boolean = false; isSpeaking: boolean = false;
audioVolume: number = 0; audioVolume: number = 0;
private _streamManager: StreamManager;
@Input() @Input()
set streamManager(streamManager: StreamManager) { set streamManager(streamManager: StreamManager) {
this._streamManager = streamManager;
if(streamManager) { if(this._streamManager) {
streamManager.on('publisherStartSpeaking', (event: PublisherSpeakingEvent) => { this._streamManager.on('publisherStartSpeaking', (event: PublisherSpeakingEvent) => {
this.isSpeaking = true; this.isSpeaking = true;
}); });
streamManager.on('publisherStopSpeaking', (event: PublisherSpeakingEvent) => { this._streamManager.on('publisherStopSpeaking', (event: PublisherSpeakingEvent) => {
this.isSpeaking = false; this.isSpeaking = false;
}); });
@ -35,6 +38,12 @@ export class AudioWaveComponent implements OnInit {
} }
constructor() {} constructor() {}
ngOnDestroy(): void {
if(this._streamManager){
this._streamManager.off('publisherStartSpeaking');
this._streamManager.off('publisherStopSpeaking');
}
}
ngOnInit(): void {} ngOnInit(): void {}
} }

View File

@ -4,7 +4,7 @@
[id]="'container-' + this._stream.streamManager?.stream?.streamId" [id]="'container-' + this._stream.streamManager?.stream?.streamId"
#streamContainer #streamContainer
> >
<div class="nickname" [class.fullscreen]="isFullscreen"> <div class="nickname" [class.fullscreen]="isFullscreen" *ngIf="showNickname">
<div (click)="toggleNicknameForm()" class="nicknameContainer" selected *ngIf="!toggleNickname"> <div (click)="toggleNicknameForm()" class="nicknameContainer" selected *ngIf="!toggleNickname">
<span id="nickname">{{ this._stream.nickname }}</span> <span id="nickname">{{ this._stream.nickname }}</span>
<span *ngIf="_stream.local || (_stream.streamManager && !_stream.streamManager?.remote)"> (edit)</span> <span *ngIf="_stream.local || (_stream.streamManager && !_stream.streamManager?.remote)"> (edit)</span>
@ -31,7 +31,7 @@
<ng-content select="[network-quality]"></ng-content> <ng-content select="[network-quality]"></ng-content>
</div> </div>
<div id="audio-wave-container" *ngIf="_stream.type === 'CAMERA'"> <div id="audio-wave-container" *ngIf="showAudioDetection && _stream.type === 'CAMERA'">
<ov-audio-wave [streamManager]="_stream.streamManager"></ov-audio-wave> <ov-audio-wave [streamManager]="_stream.streamManager"></ov-audio-wave>
</div> </div>
@ -49,7 +49,7 @@
</button> </button>
</div> </div>
<div class="videoButtons"> <div class="videoButtons" *ngIf="showSettings">
<button mat-icon-button (click)="toggleVideoMenu($event)" matTooltip="Settings" aria-label="Video settings menu"> <button mat-icon-button (click)="toggleVideoMenu($event)" matTooltip="Settings" aria-label="Video settings menu">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>

View File

@ -34,6 +34,10 @@ export class StreamComponent implements OnInit {
@ViewChild(MatMenuTrigger) public menuTrigger: MatMenuTrigger; @ViewChild(MatMenuTrigger) public menuTrigger: MatMenuTrigger;
@ViewChild('menu') menu: MatMenuPanel; @ViewChild('menu') menu: MatMenuPanel;
@Input() showNickname: boolean = true;
@Input() showAudioDetection: boolean = true;
@Input() showSettings: boolean = true;
constructor( constructor(
protected documentService: DocumentService, protected documentService: DocumentService,
protected openviduService: OpenViduService, protected openviduService: OpenViduService,
@ -128,7 +132,7 @@ export class StreamComponent implements OnInit {
if (event && event.keyCode === 13 && this.nicknameFormControl.valid) { if (event && event.keyCode === 13 && this.nicknameFormControl.valid) {
const nickname = this.nicknameFormControl.value; const nickname = this.nicknameFormControl.value;
this.participantService.setNickname(this._stream.connectionId, nickname); this.participantService.setNickname(this._stream.connectionId, nickname);
this.storageService.set(Storage.USER_NICKNAME, nickname); this.storageService.setNickname(nickname);
this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, undefined, { clientData: nickname }); this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, undefined, { clientData: nickname });
this.toggleNicknameForm(); this.toggleNicknameForm();
} }
@ -141,7 +145,7 @@ export class StreamComponent implements OnInit {
publishAudio: !this.participantService.isMyCameraEnabled(), publishAudio: !this.participantService.isMyCameraEnabled(),
mirror: false mirror: false
}; };
await this.openviduService.replaceTrack(this.participantService.getMyScreenPublisher(), properties); await this.openviduService.replaceTrack(VideoType.SCREEN, properties);
} }
protected checkVideoEnlarged() { protected checkVideoEnlarged() {

View File

@ -44,17 +44,6 @@
<mat-icon *ngIf="isScreenShareEnabled" matTooltip="Disable screen share">screen_share</mat-icon> <mat-icon *ngIf="isScreenShareEnabled" matTooltip="Disable screen share">screen_share</mat-icon>
</button> </button>
<!-- Replace Screen track button -->
<!-- <button
id="navReplaceScreenButton"
mat-icon-button
*ngIf="isScreenShareEnabled"
(click)="replaceScreenTrack()"
[disabled]="isConnectionLost"
>
<mat-icon matTooltip="Replace screen track">picture_in_picture</mat-icon>
</button> -->
<!-- Fullscreen button --> <!-- Fullscreen button -->
<button mat-icon-button (click)="toggleFullscreen()" [disabled]="isConnectionLost" [class.active-btn]="isFullscreenEnabled"> <button mat-icon-button (click)="toggleFullscreen()" [disabled]="isConnectionLost" [class.active-btn]="isFullscreenEnabled">
<mat-icon *ngIf="isFullscreenEnabled" matTooltip="Exit Fullscreen">fullscreen_exit</mat-icon> <mat-icon *ngIf="isFullscreenEnabled" matTooltip="Exit Fullscreen">fullscreen_exit</mat-icon>

View File

@ -173,7 +173,7 @@ export class ToolbarComponent implements OnInit, OnDestroy {
publishAudio: hasAudio, publishAudio: hasAudio,
mirror: false mirror: false
}; };
const screenPublisher = this.openviduService.initPublisher(undefined, properties); const screenPublisher = await this.openviduService.initPublisher(undefined, properties);
screenPublisher.once('accessAllowed', async (event) => { screenPublisher.once('accessAllowed', async (event) => {
// Listen to event fired when native stop button is clicked // Listen to event fired when native stop button is clicked
@ -225,16 +225,6 @@ export class ToolbarComponent implements OnInit, OnDestroy {
} }
} }
async replaceScreenTrack() {
const properties: PublisherProperties = {
videoSource: ScreenType.SCREEN,
publishVideo: true,
publishAudio: !this.participantService.isMyCameraEnabled(),
mirror: false
};
await this.openviduService.replaceTrack(this.participantService.getMyScreenPublisher(), properties);
}
leaveSession() { leaveSession() {
this.log.d('Leaving session...'); this.log.d('Leaving session...');
this.openviduService.disconnect(); this.openviduService.disconnect();

View File

@ -48,17 +48,17 @@
(click)="toggleCam()" (click)="toggleCam()"
class="deviceButton" class="deviceButton"
id="configCardCameraButton" id="configCardCameraButton"
[class.warn-btn]="!isVideoActive" [class.warn-btn]="isVideoMuted"
> >
<mat-icon *ngIf="isVideoActive" matTooltip="Camera Enabled">videocam</mat-icon> <mat-icon *ngIf="!isVideoMuted" matTooltip="Camera Enabled">videocam</mat-icon>
<mat-icon *ngIf="!isVideoActive" matTooltip="Camera Disabled">videocam_off</mat-icon> <mat-icon *ngIf="isVideoMuted" matTooltip="Camera Disabled">videocam_off</mat-icon>
</button> </button>
</div> </div>
<div class="two" fxFlex="80" fxLayoutAlign="center center"> <div class="two" fxFlex="80" fxLayoutAlign="center center">
<mat-form-field class="alternate-theme"> <mat-form-field class="alternate-theme">
<mat-select <mat-select
placeholder="Camera Options" placeholder="Camera Options"
[ngModel]="isVideoActive && !!cameraSelected ? cameraSelected.device : 'None'" [ngModel]="!isVideoMuted && !!cameraSelected ? cameraSelected.device : 'None'"
(selectionChange)="onCameraSelected($event)" (selectionChange)="onCameraSelected($event)"
> >
<mat-option *ngFor="let camera of cameras" [value]="camera.device"> <mat-option *ngFor="let camera of cameras" [value]="camera.device">
@ -78,17 +78,17 @@
(click)="toggleMic()" (click)="toggleMic()"
class="deviceButton" class="deviceButton"
id="configCardMicrophoneButton" id="configCardMicrophoneButton"
[class.warn-btn]="!isAudioActive" [class.warn-btn]="isAudioMuted"
> >
<mat-icon *ngIf="isAudioActive" matTooltip="Microphone Enabled">mic</mat-icon> <mat-icon *ngIf="!isAudioMuted" matTooltip="Microphone Enabled">mic</mat-icon>
<mat-icon *ngIf="!isAudioActive" matTooltip="Microphone Disabled">mic_off</mat-icon> <mat-icon *ngIf="isAudioMuted" matTooltip="Microphone Disabled">mic_off</mat-icon>
</button> </button>
</div> </div>
<div class="two" fxFlex="80" fxLayoutAlign="center center"> <div class="two" fxFlex="80" fxLayoutAlign="center center">
<mat-form-field class="alternate-theme"> <mat-form-field class="alternate-theme">
<mat-select <mat-select
placeholder="Microphone Options" placeholder="Microphone Options"
[ngModel]="isAudioActive && microphoneSelected ? microphoneSelected.device : 'None'" [ngModel]="!isAudioMuted && microphoneSelected ? microphoneSelected.device : 'None'"
(selectionChange)="onMicrophoneSelected($event)" (selectionChange)="onMicrophoneSelected($event)"
> >
<mat-option *ngFor="let microphone of microphones" [value]="microphone.device"> <mat-option *ngFor="let microphone of microphones" [value]="microphone.device">

View File

@ -7,8 +7,7 @@ import { Publisher, PublisherProperties } from 'openvidu-browser';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { CustomDevice } from '../../models/device.model'; import { CustomDevice } from '../../models/device.model';
import { Storage } from '../../models/storage.model'; import { ScreenType, VideoType } from '../../models/video-type.model';
import { ScreenType } from '../../models/video-type.model';
import { NicknameMatcher } from '../../matchers/nickname.matcher'; import { NicknameMatcher } from '../../matchers/nickname.matcher';
@ -36,8 +35,8 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
microphones: CustomDevice[]; microphones: CustomDevice[];
cameraSelected: CustomDevice; cameraSelected: CustomDevice;
microphoneSelected: CustomDevice; microphoneSelected: CustomDevice;
isVideoActive = true; isVideoMuted: boolean;
isAudioActive = true; isAudioMuted: boolean;
screenShareEnabled: boolean; screenShareEnabled: boolean;
localParticipant: ParticipantAbstractModel; localParticipant: ParticipantAbstractModel;
columns: number; columns: number;
@ -55,7 +54,7 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
private actionService: ActionService, private actionService: ActionService,
private deviceSrv: DeviceService, private deviceSrv: DeviceService,
private loggerSrv: LoggerService, private loggerSrv: LoggerService,
private openViduopenviduService: OpenViduService, private openviduService: OpenViduService,
private participantService: ParticipantService, private participantService: ParticipantService,
private storageSrv: StorageService private storageSrv: StorageService
) { ) {
@ -68,10 +67,11 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
} }
async ngOnInit() { async ngOnInit() {
this.subscribeToLocalParticipantEvents();
this.openViduopenviduService.initialize();
await this.deviceSrv.initializeDevices(); await this.deviceSrv.initializeDevices();
const nickname = this.storageSrv.get(Storage.USER_NICKNAME) || this.generateRandomNickname();
this.subscribeToLocalParticipantEvents();
this.openviduService.initialize();
const nickname = this.storageSrv.getNickname() || this.generateRandomNickname();
this.nicknameFormControl.setValue(nickname); this.nicknameFormControl.setValue(nickname);
this.columns = window.innerWidth > 900 ? 2 : 1; this.columns = window.innerWidth > 900 ? 2 : 1;
this.setDevicesInfo(); this.setDevicesInfo();
@ -79,7 +79,6 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
await this.initwebcamPublisher(); await this.initwebcamPublisher();
} }
this.isLoading = false; this.isLoading = false;
} }
ngOnDestroy() { ngOnDestroy() {
@ -95,63 +94,57 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
async onCameraSelected(event: any) { async onCameraSelected(event: any) {
const videoSource = event?.value; const videoSource = event?.value;
if (!!videoSource) { // Is New deviceId different from the old one?
// Is New deviceId different from the old one? if (this.deviceSrv.needUpdateVideoTrack(videoSource)) {
if (this.deviceSrv.needUpdateVideoTrack(videoSource)) { const mirror = this.deviceSrv.cameraNeedsMirror(videoSource);
const mirror = this.deviceSrv.cameraNeedsMirror(videoSource); const pp: PublisherProperties = { videoSource, audioSource: false, mirror };
await this.openViduopenviduService.republishTrack(videoSource, null, mirror);
this.deviceSrv.setCameraSelected(videoSource);
this.cameraSelected = this.deviceSrv.getCameraSelected();
}
// Publish Webcam video
this.openViduopenviduService.publishVideo(this.participantService.getMyCameraPublisher(), true);
this.isVideoActive = true;
} else { await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
// Videosource is 'null' because of the user has selected 'None' or muted the camera this.cameraSelected = videoSource;
// Unpublish webcam this.deviceSrv.setCameraSelected(this.cameraSelected);
this.openViduopenviduService.publishVideo(this.participantService.getMyCameraPublisher(), false); }
//TODO: save 'None' device in storage if (this.isVideoMuted) {
// this.deviceSrv.setCameraSelected(videoSource); // Publish Webcam video
// this.cameraSelected = this.deviceSrv.getCameraSelected(); this.openviduService.publishVideo(this.participantService.getMyCameraPublisher(), true);
this.isVideoActive = false; this.isVideoMuted = false;
} }
} }
async onMicrophoneSelected(event: any) { async onMicrophoneSelected(event: any) {
const audioSource = event?.value; const audioSource = event?.value;
// Is New deviceId different than older?
if (!!audioSource) { if (this.deviceSrv.needUpdateAudioTrack(audioSource)) {
// Is New deviceId different than older? const pp: PublisherProperties = { audioSource, videoSource: false };
if (this.deviceSrv.needUpdateAudioTrack(audioSource)) { await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
const mirror = this.deviceSrv.cameraNeedsMirror(this.cameraSelected.device); this.microphoneSelected = audioSource;
await this.openViduopenviduService.republishTrack(null, audioSource, mirror); this.deviceSrv.setMicSelected(this.microphoneSelected);
this.deviceSrv.setMicSelected(audioSource); }
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected(); if (this.isAudioMuted) {
} // Enable microphone
// Publish microphone this.openviduService.publishAudio(this.participantService.getMyCameraPublisher(), true);
this.publishAudio(true); this.isAudioMuted = true;
this.isAudioActive = true;
return;
} }
// Unpublish microhpone
this.publishAudio(false);
this.isAudioActive = false;
} }
toggleCam() { toggleCam() {
this.isVideoActive = !this.isVideoActive;
this.openViduopenviduService.publishVideo(this.participantService.getMyCameraPublisher(), this.isVideoActive); const publish = this.isVideoMuted;
this.openviduService.publishVideo(this.participantService.getMyCameraPublisher(), publish);
if (this.participantService.areBothEnabled()) { if (this.participantService.areBothEnabled()) {
// Cam will not published, disable webcam with screensharing active
this.participantService.disableWebcamUser(); this.participantService.disableWebcamUser();
this.openViduopenviduService.publishAudio(this.participantService.getMyScreenPublisher(), this.isAudioActive); this.openviduService.publishAudio(this.participantService.getMyScreenPublisher(), publish);
} else if (this.participantService.isOnlyMyScreenEnabled()) { } else if (this.participantService.isOnlyMyScreenEnabled()) {
// Cam will be published, enable webcam
this.participantService.enableWebcamUser(); this.participantService.enableWebcamUser();
} }
this.isVideoMuted = !this.isVideoMuted;
this.storageSrv.setVideoMuted(this.isVideoMuted);
} }
toggleScreenShare() { async toggleScreenShare() {
// Disabling screenShare // Disabling screenShare
if (this.participantService.areBothEnabled()) { if (this.participantService.areBothEnabled()) {
this.participantService.disableScreenUser(); this.participantService.disableScreenUser();
@ -161,7 +154,7 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
// Enabling screenShare // Enabling screenShare
if (this.participantService.isOnlyMyCameraEnabled()) { if (this.participantService.isOnlyMyCameraEnabled()) {
const willThereBeWebcam = this.participantService.isMyCameraEnabled() && this.participantService.hasCameraVideoActive(); const willThereBeWebcam = this.participantService.isMyCameraEnabled() && this.participantService.hasCameraVideoActive();
const hasAudio = willThereBeWebcam ? false : this.hasAudioDevices && this.isAudioActive; const hasAudio = willThereBeWebcam ? false : this.hasAudioDevices && this.isAudioMuted;
const properties: PublisherProperties = { const properties: PublisherProperties = {
videoSource: ScreenType.SCREEN, videoSource: ScreenType.SCREEN,
audioSource: this.hasAudioDevices ? undefined : null, audioSource: this.hasAudioDevices ? undefined : null,
@ -169,7 +162,7 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
publishAudio: hasAudio, publishAudio: hasAudio,
mirror: false mirror: false
}; };
const screenPublisher = this.openViduopenviduService.initPublisher(undefined, properties); const screenPublisher = await this.openviduService.initPublisher(undefined, properties);
screenPublisher.on('accessAllowed', (event) => { screenPublisher.on('accessAllowed', (event) => {
screenPublisher.stream screenPublisher.stream
@ -199,8 +192,10 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
} }
toggleMic() { toggleMic() {
this.isAudioActive = !this.isAudioActive; const publish = this.isAudioMuted;
this.publishAudio(this.isAudioActive); this.openviduService.publishAudio(this.participantService.getMyCameraPublisher(), publish);
this.isAudioMuted = !this.isAudioMuted;
this.storageSrv.setAudioMuted(this.isAudioMuted);
} }
eventKeyPress(event) { eventKeyPress(event) {
@ -217,7 +212,8 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
if (this.nicknameFormControl.valid) { if (this.nicknameFormControl.valid) {
const nickname = this.nicknameFormControl.value; const nickname = this.nicknameFormControl.value;
this.participantService.setNickname(this.participantService.getMyCameraConnectionId(), nickname); this.participantService.setNickname(this.participantService.getMyCameraConnectionId(), nickname);
this.storageSrv.set(Storage.USER_NICKNAME, nickname); this.storageSrv.setNickname(nickname);
this.participantService.updateParticipantMediaStatus();
return this.onJoinClicked.emit(); return this.onJoinClicked.emit();
} }
this.scrollToBottom(); this.scrollToBottom();
@ -235,9 +231,8 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
this.cameraSelected = this.deviceSrv.getCameraSelected(); this.cameraSelected = this.deviceSrv.getCameraSelected();
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected(); this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
this.isVideoActive = this.hasVideoDevices && this.cameraSelected.label !== 'None'; this.isVideoMuted = this.deviceSrv.isVideoMuted();
this.isAudioActive = this.hasAudioDevices && this.microphoneSelected.label !== 'None'; this.isAudioMuted = this.deviceSrv.isAudioMuted();
} }
private scrollToBottom(): void { private scrollToBottom(): void {
@ -246,12 +241,6 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
} catch (err) {} } catch (err) {}
} }
private publishAudio(audio: boolean) {
this.participantService.isMyCameraEnabled()
? this.openViduopenviduService.publishAudio(this.participantService.getMyCameraPublisher(), audio)
: this.openViduopenviduService.publishAudio(this.participantService.getMyScreenPublisher(), audio);
}
private subscribeToLocalParticipantEvents() { private subscribeToLocalParticipantEvents() {
this.oVUsersSubscription = this.participantService.localParticipantObs.subscribe((p) => { this.oVUsersSubscription = this.participantService.localParticipantObs.subscribe((p) => {
this.localParticipant = p; this.localParticipant = p;
@ -262,9 +251,8 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
} }
private async initwebcamPublisher() { private async initwebcamPublisher() {
const publisher = await this.openViduopenviduService.initDefaultPublisher(undefined); const publisher = await this.openviduService.initDefaultPublisher(undefined);
if (publisher) { if (publisher) {
// this.handlePublisherSuccess(publisher); // this.handlePublisherSuccess(publisher);
this.handlePublisherError(publisher); this.handlePublisherError(publisher);
} }

View File

@ -1,5 +1,7 @@
export enum Storage{ export enum Storage{
USER_NICKNAME = 'openviduCallNickname', USER_NICKNAME = 'openviduCallNickname',
VIDEO_DEVICE = 'openviduCallVideoDevice', VIDEO_DEVICE = 'openviduCallVideoDevice',
AUDIO_DEVICE = 'openviduCallAudioDevice' AUDIO_DEVICE = 'openviduCallAudioDevice',
AUDIO_MUTED = 'openviduCallAudioMuted',
VIDEO_MUTED = 'openviduCallVideoMuted'
} }

View File

@ -3,7 +3,6 @@ import { Device, OpenVidu } from 'openvidu-browser';
import { CameraType, DeviceType, CustomDevice } from '../../models/device.model'; import { CameraType, DeviceType, CustomDevice } from '../../models/device.model';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { Storage } from '../../models/storage.model';
import { LoggerService } from '../logger/logger.service'; import { LoggerService } from '../logger/logger.service';
import { PlatformService } from '../platform/platform.service'; import { PlatformService } from '../platform/platform.service';
@ -23,6 +22,11 @@ export class DeviceService {
private videoDevicesDisabled: boolean; private videoDevicesDisabled: boolean;
private audioDevicesDisabled: boolean; private audioDevicesDisabled: boolean;
// Initialized with Storage.VIDEO_MUTED info saved on storage
private _isVideoMuted: boolean;
// Initialized with Storage.AUDIO_MUTED info saved on storage
private _isAudioMuted: boolean;
constructor(private loggerSrv: LoggerService, private platformSrv: PlatformService, private storageSrv: StorageService) { constructor(private loggerSrv: LoggerService, private platformSrv: PlatformService, private storageSrv: StorageService) {
this.log = this.loggerSrv.get('DevicesService'); this.log = this.loggerSrv.get('DevicesService');
this.OV = new OpenVidu(); this.OV = new OpenVidu();
@ -33,18 +37,21 @@ export class DeviceService {
} }
async initializeDevices() { async initializeDevices() {
// Requesting media permissions. Sometimes, browser doens't launch the media permissions modal. // Forcing media permissions request.
const mediaStream = await this.OV.getUserMedia({audioSource: undefined, videoSource: undefined }); // Sometimes, browser doens't launch the media permissions modal.
const mediaStream = await this.OV.getUserMedia({ audioSource: undefined, videoSource: undefined });
mediaStream?.getAudioTracks().forEach((track) => track.stop());
mediaStream?.getVideoTracks().forEach((track) => track.stop());
this.devices = await this.OV.getDevices(); this.devices = await this.OV.getDevices();
const customDevices = this.initializeCustomDevices(this.devices); const customDevices = this.initializeCustomDevices(this.devices);
this.cameras = customDevices.cameras; this.cameras = customDevices.cameras;
this.microphones = customDevices.microphones; this.microphones = customDevices.microphones;
mediaStream?.getAudioTracks().forEach((track) => track.stop()); this._isVideoMuted = this.storageSrv.isVideoMuted();
mediaStream?.getVideoTracks().forEach((track) => track.stop()); this._isAudioMuted = this.storageSrv.isAudioMuted();
this.log.d('Media devices',customDevices); this.log.d('Media devices', customDevices);
} }
private initializeCustomDevices(defaultVDevices: Device[]) { private initializeCustomDevices(defaultVDevices: Device[]) {
@ -52,8 +59,8 @@ export class DeviceService {
const defaultMicrophones = defaultVDevices.filter((device) => device.kind === DeviceType.AUDIO_INPUT); const defaultMicrophones = defaultVDevices.filter((device) => device.kind === DeviceType.AUDIO_INPUT);
const defaultCameras = defaultVDevices.filter((device) => device.kind === DeviceType.VIDEO_INPUT); const defaultCameras = defaultVDevices.filter((device) => device.kind === DeviceType.VIDEO_INPUT);
const customDevices: { cameras: CustomDevice[]; microphones: CustomDevice[] } = { const customDevices: { cameras: CustomDevice[]; microphones: CustomDevice[] } = {
cameras: [{ label: 'None', device: null, type: null }], cameras: [],
microphones: [{ label: 'None', device: null, type: null }] microphones: []
}; };
if (this.hasAudioDeviceAvailable) { if (this.hasAudioDeviceAvailable) {
@ -61,12 +68,12 @@ export class DeviceService {
customDevices.microphones.push({ label: device.label, device: device.deviceId }); customDevices.microphones.push({ label: device.label, device: device.deviceId });
}); });
// Settings microphone selected // Setting microphone selected
const storageMicrophone = this.getMicrophoneFromStogare(); const storageMicrophone = this.getMicrophoneFromStogare();
if (!!storageMicrophone) { if (!!storageMicrophone) {
this.microphoneSelected = storageMicrophone; this.microphoneSelected = storageMicrophone;
} else if (customDevices.microphones.length > 0) { } else if (customDevices.microphones.length > 0) {
this.microphoneSelected = customDevices.microphones.find((d) => d.label !== 'None'); this.microphoneSelected = customDevices.microphones[0];
} }
} }
@ -96,12 +103,20 @@ export class DeviceService {
if (!!storageCamera) { if (!!storageCamera) {
this.cameraSelected = storageCamera; this.cameraSelected = storageCamera;
} else if (customDevices.cameras.length > 0) { } else if (customDevices.cameras.length > 0) {
this.cameraSelected = customDevices.cameras.find((d) => d.label !== 'None'); this.cameraSelected = customDevices.cameras[0];
} }
} }
return customDevices; return customDevices;
} }
isVideoMuted(): boolean {
return this.hasVideoDeviceAvailable() && this._isVideoMuted;
}
isAudioMuted(): boolean {
return this.hasAudioDeviceAvailable() && this._isAudioMuted;
}
getCameraSelected(): CustomDevice { getCameraSelected(): CustomDevice {
return this.cameraSelected; return this.cameraSelected;
} }
@ -137,11 +152,11 @@ export class DeviceService {
} }
hasVideoDeviceAvailable(): boolean { hasVideoDeviceAvailable(): boolean {
return !this.videoDevicesDisabled && !this.cameras.every((device) => device.label === 'None'); return !this.videoDevicesDisabled && this.cameras.length > 0;
} }
hasAudioDeviceAvailable(): boolean { hasAudioDeviceAvailable(): boolean {
return !this.audioDevicesDisabled && !this.microphones.every((device) => device.label === 'None'); return !this.audioDevicesDisabled && this.microphones.length > 0;
} }
cameraNeedsMirror(deviceField: string): boolean { cameraNeedsMirror(deviceField: string): boolean {
@ -180,24 +195,24 @@ export class DeviceService {
} }
private getMicrophoneFromStogare(): CustomDevice { private getMicrophoneFromStogare(): CustomDevice {
let storageDevice = this.storageSrv.get(Storage.AUDIO_DEVICE); const storageDevice: CustomDevice = this.storageSrv.getAudioDevice();
if (!!storageDevice) { if (!!storageDevice && this.microphones.some((device) => device.device === storageDevice.device)) {
return storageDevice; return storageDevice;
} }
} }
private getCameraFromStorage() { private getCameraFromStorage() {
let storageDevice = this.storageSrv.get(Storage.VIDEO_DEVICE); const storageDevice: CustomDevice = this.storageSrv.getVideoDevice();
if (!!storageDevice) { if (!!storageDevice && this.cameras.some((device) => device.device === storageDevice.device)) {
return storageDevice; return storageDevice;
} }
} }
private saveCameraToStorage(cam: CustomDevice) { private saveCameraToStorage(cam: CustomDevice) {
this.storageSrv.set(Storage.VIDEO_DEVICE, cam); this.storageSrv.setVideoDevice(cam);
} }
private saveMicrophoneToStorage(mic: CustomDevice) { private saveMicrophoneToStorage(mic: CustomDevice) {
this.storageSrv.set(Storage.AUDIO_DEVICE, mic); this.storageSrv.setAudioDevice(mic);
} }
} }

View File

@ -22,8 +22,8 @@ export class OpenViduService {
protected screenSession: Session = null; protected screenSession: Session = null;
protected videoSource = undefined; protected videoSource = undefined;
protected audioSource = undefined; protected audioSource = undefined;
protected screenMediaStream: MediaStream = null; // protected screenMediaStream: MediaStream = null;
protected webcamMediaStream: MediaStream = null; // protected webcamMediaStream: MediaStream = null;
protected log: ILogger; protected log: ILogger;
constructor( constructor(
@ -49,6 +49,10 @@ export class OpenViduService {
} }
} }
getSession(): Session {
return this.getWebcamSession();
}
getWebcamSession(): Session { getWebcamSession(): Session {
return this.webcamSession; return this.webcamSession;
} }
@ -90,60 +94,44 @@ export class OpenViduService {
} }
} }
private disconnectSession(session: Session) {
if (session) {
if (session.sessionId === this.webcamSession?.sessionId) {
this.log.d('Disconnecting webcam session');
this.webcamSession?.disconnect();
this.webcamSession = null;
} else if (session.sessionId === this.screenSession?.sessionId) {
this.log.d('Disconnecting screen session');
this.screenSession?.disconnect();
this.screenSession = null;
}
}
}
disconnect() { disconnect() {
this.disconnectSession(this.webcamSession); this.disconnectSession(this.webcamSession);
this.disconnectSession(this.screenSession); this.disconnectSession(this.screenSession);
this.videoSource = undefined; this.videoSource = undefined;
this.audioSource = undefined; this.audioSource = undefined;
this.stopTracks(this.participantService.getMyCameraPublisher()?.stream?.getMediaStream()); // this.stopTracks(this.participantService.getMyCameraPublisher()?.stream?.getMediaStream());
this.stopTracks(this.participantService.getMyScreenPublisher()?.stream?.getMediaStream()); // this.stopTracks(this.participantService.getMyScreenPublisher()?.stream?.getMediaStream());
} }
/** /**
* Initialize a publisher checking devices saved on storage or if participant have devices available. * Initialize a publisher checking devices saved on storage or if participant have devices available.
*/ */
async initDefaultPublisher(targetElement: string | HTMLElement): Promise<Publisher> { async initDefaultPublisher(targetElement: string | HTMLElement): Promise<Publisher> {
await this.deviceService.initializeDevices();
const hasVideoDevices = this.deviceService.hasVideoDeviceAvailable(); const hasVideoDevices = this.deviceService.hasVideoDeviceAvailable();
const hasAudioDevices = this.deviceService.hasAudioDeviceAvailable(); const hasAudioDevices = this.deviceService.hasAudioDeviceAvailable();
const isVideoActive = hasVideoDevices && this.deviceService.getCameraSelected().label !== 'None'; const isVideoActive = !this.deviceService.isVideoMuted();
const isAudioActive = hasAudioDevices && this.deviceService.getMicrophoneSelected().label !== 'None'; const isAudioActive = !this.deviceService.isAudioMuted();
let videoSource = null; let videoSource = null;
let audioSource = null; let audioSource = null;
if(isVideoActive){ if (hasVideoDevices) {
// Video is active, assign the device selected // Video is active, assign the device selected
videoSource = this.deviceService.getCameraSelected().device videoSource = this.deviceService.getCameraSelected().device;
} else if(!isVideoActive && hasVideoDevices) { } else if (!isVideoActive && hasVideoDevices) {
// Video is muted, assign the default device // Video is muted, assign the default device
videoSource = undefined; // videoSource = undefined;
} }
if(isAudioActive){ if (hasAudioDevices) {
// Audio is active, assign the device selected // Audio is active, assign the device selected
audioSource = this.deviceService.getMicrophoneSelected().device audioSource = this.deviceService.getMicrophoneSelected().device;
} else if(!isAudioActive && hasAudioDevices) { } else if (!isAudioActive && hasAudioDevices) {
// Audio is muted, assign the default device // Audio is muted, assign the default device
audioSource = undefined; // audioSource = undefined;
} }
// const videoSource = publishVideo ? this.deviceService.getCameraSelected().device : false; // const videoSource = publishVideo ? this.deviceService.getCameraSelected().device : false;
// const audioSource = publishAudio ? this.deviceService.getMicrophoneSelected().device : false; // const audioSource = publishAudio ? this.deviceService.getMicrophoneSelected().device : false;
const mirror = this.deviceService.getCameraSelected() && this.deviceService.getCameraSelected().type === CameraType.FRONT; const mirror = this.deviceService.getCameraSelected() && this.deviceService.getCameraSelected().type === CameraType.FRONT;
@ -154,8 +142,8 @@ export class OpenViduService {
publishAudio: isAudioActive, publishAudio: isAudioActive,
mirror mirror
}; };
if (isVideoActive || isAudioActive) { if (hasVideoDevices || hasAudioDevices) {
const publisher = this.initPublisher(targetElement, properties); const publisher = await this.initPublisher(targetElement, properties);
this.participantService.setMyCameraPublisher(publisher); this.participantService.setMyCameraPublisher(publisher);
return publisher; return publisher;
} else { } else {
@ -163,10 +151,10 @@ export class OpenViduService {
} }
} }
initPublisher(targetElement: string | HTMLElement, properties: PublisherProperties): Publisher { async initPublisher(targetElement: string | HTMLElement, properties: PublisherProperties): Promise<Publisher> {
this.log.d('Initializing publisher with properties: ', properties); this.log.d('Initializing publisher with properties: ', properties);
const publisher = this.OV.initPublisher(targetElement, properties); const publisher = await this.OV.initPublisherAsync(targetElement, properties);
// this.participantService.setMyCameraPublisher(publisher); // this.participantService.setMyCameraPublisher(publisher);
publisher.once('streamPlaying', () => { publisher.once('streamPlaying', () => {
(<HTMLElement>publisher.videos[0].video).parentElement.classList.remove('custom-class'); (<HTMLElement>publisher.videos[0].video).parentElement.classList.remove('custom-class');
@ -174,21 +162,21 @@ export class OpenViduService {
return publisher; return publisher;
} }
//TODO: This method is used by replaceTrack. Check if it's neecessary //TODO: This method is used by republishTrack. Check if it's neecessary
private destroyPublisher(publisher: Publisher): void { // private destroyPublisher(publisher: Publisher): void {
if (!!publisher) { // if (!!publisher) {
// publisher.off('streamAudioVolumeChange'); // // publisher.off('streamAudioVolumeChange');
if (publisher.stream.getWebRtcPeer()) { // if (publisher.stream.getWebRtcPeer()) {
publisher.stream.disposeWebRtcPeer(); // publisher.stream.disposeWebRtcPeer();
} // }
publisher.stream.disposeMediaStream(); // publisher.stream.disposeMediaStream();
if (publisher.id === this.participantService.getMyCameraPublisher().id) { // if (publisher.id === this.participantService.getMyCameraPublisher().id) {
this.participantService.setMyCameraPublisher(publisher); // this.participantService.setMyCameraPublisher(publisher);
} else if (publisher.id === this.participantService.getMyScreenPublisher().id) { // } else if (publisher.id === this.participantService.getMyScreenPublisher().id) {
this.participantService.setMyScreenPublisher(publisher); // this.participantService.setMyScreenPublisher(publisher);
} // }
} // }
} // }
async publish(publisher: Publisher): Promise<void> { async publish(publisher: Publisher): Promise<void> {
if (!!publisher) { if (!!publisher) {
@ -221,7 +209,7 @@ export class OpenViduService {
if (!!publisher) { if (!!publisher) {
publisher.publishVideo(value); publisher.publishVideo(value);
// Send event to subscribers because of video has changed and view must update // Send event to subscribers because of video has changed and view must update
this.participantService.updateUsersStatus(); this.participantService.updateParticipantMediaStatus();
} }
} }
@ -229,44 +217,43 @@ export class OpenViduService {
if (!!publisher) { if (!!publisher) {
publisher.publishAudio(value); publisher.publishAudio(value);
// Send event to subscribers because of video has changed and view must update // Send event to subscribers because of video has changed and view must update
this.participantService.updateUsersStatus(); this.participantService.updateParticipantMediaStatus();
} }
} }
republishTrack(videoSource: string, audioSource: string, mirror: boolean = true): Promise<void> { // Used replaceTrack instead
return new Promise((resolve, reject) => { // republishTrack(videoSource: string, audioSource: string, mirror: boolean = true): Promise<void> {
if (!!videoSource) { // return new Promise(async (resolve, reject) => {
this.log.d('Replacing video track ' + videoSource); // if (!!videoSource) {
this.videoSource = videoSource; // this.log.d('Replacing video track ' + videoSource);
// this.stopVideoTracks(this.webcamUser.getStreamManager().stream.getMediaStream()); // this.videoSource = videoSource;
} // }
if (!!audioSource) { // if (!!audioSource) {
this.log.d('Replacing audio track ' + audioSource); // this.log.d('Replacing audio track ' + audioSource);
this.audioSource = audioSource; // this.audioSource = audioSource;
// this.stopAudioTracks(this.webcamUser.getStreamManager().stream.getMediaStream()); // }
} // this.destroyPublisher(this.participantService.getMyCameraPublisher());
this.destroyPublisher(this.participantService.getMyCameraPublisher()); // const properties: PublisherProperties = {
const properties: PublisherProperties = { // videoSource: this.videoSource,
videoSource: this.videoSource, // audioSource: this.audioSource,
audioSource: this.audioSource, // publishVideo: this.participantService.hasCameraVideoActive(),
publishVideo: this.participantService.hasCameraVideoActive(), // publishAudio: this.participantService.hasCameraAudioActive(),
publishAudio: this.participantService.hasCameraAudioActive(), // mirror
mirror // };
};
const publisher = this.initPublisher(undefined, properties); // const publisher = await this.initPublisher(undefined, properties);
this.participantService.setMyCameraPublisher(publisher); // this.participantService.setMyCameraPublisher(publisher);
publisher.once('streamPlaying', () => { // publisher.once('streamPlaying', () => {
this.participantService.setMyCameraPublisher(publisher); // this.participantService.setMyCameraPublisher(publisher);
resolve(); // resolve();
}); // });
publisher.once('accessDenied', () => { // publisher.once('accessDenied', () => {
reject(); // reject();
}); // });
}); // });
} // }
sendSignal(type: Signal, connections?: Connection[], data?: any): void { sendSignal(type: Signal, connections?: Connection[], data?: any): void {
const signalOptions: SignalOptions = { const signalOptions: SignalOptions = {
@ -282,28 +269,38 @@ export class OpenViduService {
} }
} }
async replaceTrack(currentPublisher: Publisher, newProperties: PublisherProperties) { async replaceTrack(videoType: VideoType, props: PublisherProperties) {
try { try {
if (!!currentPublisher) { this.log.d(`Replacing ${videoType} track`, props);
if (currentPublisher === this.participantService.getMyCameraPublisher()) { if (videoType === VideoType.CAMERA) {
// This branch has been disabled because echo problems replacing audio track. const isReplacingAudio = !!props.audioSource;
// this.stopAudioTracks(this.webcamMediaStream); const isReplacingVideo = !!props.videoSource;
// this.stopVideoTracks(this.webcamMediaStream);
// this.webcamMediaStream = await this.OV.getUserMedia(newProperties); if (this.platformService.isFirefox()) {
// const videoTrack: MediaStreamTrack = this.webcamMediaStream.getVideoTracks()[0]; // Firefox throw an exception trying to get a new MediaStreamTrack if the older one is not stopped
// const audioTrack: MediaStreamTrack = this.webcamMediaStream.getAudioTracks()[0]; // NotReadableError: Concurrent mic process limit. Stopping tracks before call to getUserMedia
// if(!!videoTrack){ if (isReplacingVideo) {
// await this.participantService.getMyCameraPublisher().replaceTrack(videoTrack); this.participantService.getMyCameraPublisher().stream.getMediaStream().getVideoTracks()[0].stop();
// } } else if (isReplacingAudio) {
// if(!!audioTrack) { this.participantService.getMyCameraPublisher().stream.getMediaStream().getAudioTracks()[0].stop();
// await this.participantService.getMyCameraPublisher().replaceTrack(audioTrack); }
// }
} else if (currentPublisher === this.participantService.getMyScreenPublisher()) {
const newScreenMediaStream = await this.OVScreen.getUserMedia(newProperties);
this.stopTracks(this.screenMediaStream);
await this.participantService.getMyScreenPublisher().replaceTrack(newScreenMediaStream.getVideoTracks()[0]);
this.screenMediaStream = newScreenMediaStream;
} }
const track = await this.OV.getUserMedia(props);
if (isReplacingAudio) {
// Replace audio track
const audioTrack: MediaStreamTrack = track.getAudioTracks()[0];
await this.participantService.getMyCameraPublisher().replaceTrack(audioTrack);
} else if (isReplacingVideo) {
// Replace video track
const videoTrack: MediaStreamTrack = track.getVideoTracks()[0];
await this.participantService.getMyCameraPublisher().replaceTrack(videoTrack);
}
} else if (videoType === VideoType.SCREEN) {
const newScreenMediaStream = await this.OVScreen.getUserMedia(props);
// this.stopTracks(this.screenMediaStream);
// this.screenMediaStream = newScreenMediaStream;
await this.participantService.getMyScreenPublisher().replaceTrack(newScreenMediaStream.getVideoTracks()[0]);
} }
} catch (error) { } catch (error) {
this.log.e('Error replacing track ', error); this.log.e('Error replacing track ', error);
@ -331,11 +328,25 @@ export class OpenViduService {
return remoteCameraConnections; return remoteCameraConnections;
} }
private stopTracks(mediaStream: MediaStream) { private disconnectSession(session: Session) {
if (mediaStream) { if (session) {
mediaStream?.getAudioTracks().forEach((track) => track.stop()); if (session.sessionId === this.webcamSession?.sessionId) {
mediaStream?.getVideoTracks().forEach((track) => track.stop()); this.log.d('Disconnecting webcam session');
// this.webcamMediaStream?.getAudioTracks().forEach((track) => track.stop()); this.webcamSession?.disconnect();
this.webcamSession = null;
} else if (session.sessionId === this.screenSession?.sessionId) {
this.log.d('Disconnecting screen session');
this.screenSession?.disconnect();
this.screenSession = null;
}
} }
} }
// private stopTracks(mediaStream: MediaStream) {
// if (mediaStream) {
// mediaStream?.getAudioTracks().forEach((track) => track.stop());
// mediaStream?.getVideoTracks().forEach((track) => track.stop());
// // this.webcamMediaStream?.getAudioTracks().forEach((track) => track.stop());
// }
// }
} }

View File

@ -113,7 +113,7 @@ export class ParticipantService {
this._screensharing.next(false); this._screensharing.next(false);
} }
updateUsersStatus() { updateParticipantMediaStatus() {
this._cameraVideoActive.next(this.localParticipant.isCameraVideoActive()); this._cameraVideoActive.next(this.localParticipant.isCameraVideoActive());
if (this.isMyCameraEnabled()) { if (this.isMyCameraEnabled()) {
this._cameraAudioActive.next(this.localParticipant.isCameraAudioActive()); this._cameraAudioActive.next(this.localParticipant.isCameraAudioActive());

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { LoggerService } from '../logger/logger.service'; import { LoggerService } from '../logger/logger.service';
import { Storage } from '../../models/storage.model';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
@ -13,12 +14,49 @@ export class StorageService {
this.log = this.loggerSrv.get('StorageService'); this.log = this.loggerSrv.get('StorageService');
} }
public set(key: string, item: any) { getNickname(): string {
return this.get(Storage.USER_NICKNAME);
}
setNickname(name: string){
this.set(Storage.USER_NICKNAME, name);
}
getVideoDevice() {
return this.get(Storage.VIDEO_DEVICE);
}
setVideoDevice(device: any) {
this.set(Storage.VIDEO_DEVICE, device);
}
getAudioDevice() {
return this.get(Storage.AUDIO_DEVICE);
}
setAudioDevice(device: any) {
this.set(Storage.AUDIO_DEVICE, device);
}
isVideoMuted(): boolean {
return this.get(Storage.VIDEO_MUTED) === 'true';
}
setVideoMuted(muted: boolean) {
this.set(Storage.VIDEO_MUTED, `${muted}`);
}
isAudioMuted(): boolean {
return this.get(Storage.AUDIO_MUTED) === 'true';
}
setAudioMuted(muted: boolean) {
this.set(Storage.AUDIO_MUTED, `${muted}`);
}
private set(key: string, item: any) {
const value = JSON.stringify({ item: item }); const value = JSON.stringify({ item: item });
// this.log.d('Storing on localStorage "' + key + '" with value "' + value + '"'); // this.log.d('Storing on localStorage "' + key + '" with value "' + value + '"');
this.storage.setItem(key, value); this.storage.setItem(key, value);
} }
public get(key: string): any { private get(key: string): any {
const value = JSON.parse(this.storage.getItem(key)); const value = JSON.parse(this.storage.getItem(key));
return value?.item ? value.item : null; return value?.item ? value.item : null;
} }