Merge branch 'ov-components-refactor'

This commit is contained in:
Carlos Santos 2023-05-31 10:55:51 +02:00
commit 12a34ca7cb
27 changed files with 16781 additions and 1319 deletions

View File

@ -71,7 +71,7 @@ jobs:
name: openvidu-browser
path: openvidu-components-angular
- name: Run Browserless Chrome
run: docker run -d -p 3000:3000 --network host browserless/chrome:1.53-chrome-stable
run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run openvidu-server-kms
run: |
docker run -p 4443:4443 --rm -d \
@ -106,7 +106,7 @@ jobs:
name: openvidu-browser
path: openvidu-components-angular
- name: Run Browserless Chrome
run: docker run -d -p 3000:3000 --network host browserless/chrome:1.53-chrome-stable
run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Run openvidu-server-kms
run: |
docker run -p 4443:4443 --rm -d \
@ -140,7 +140,7 @@ jobs:
name: openvidu-browser
path: openvidu-components-angular
- name: Run Browserless Chrome
run: docker run -d -p 3000:3000 --network host browserless/chrome:1.53-chrome-stable
run: docker run -d -p 3000:3000 --network host browserless/chrome:1.57-chrome-stable
- name: Install openvidu-browser and dependencies
run: |
cd openvidu-components-angular

View File

@ -2,7 +2,7 @@ import { expect } from 'chai';
import { By, until, WebDriver, WebElement } from 'selenium-webdriver';
export class OpenViduComponentsPO {
private TIMEOUT = 30 * 1000;
private TIMEOUT = 10 * 1000;
private POLL_TIMEOUT = 1 * 1000;
constructor(private browser: WebDriver) {}
@ -16,7 +16,7 @@ export class OpenViduComponentsPO {
);
}
async getNumberOfElements(selector: string){
async getNumberOfElements(selector: string): Promise<number> {
return (await this.browser.findElements(By.css(selector))).length;
}

View File

@ -1,3 +1,5 @@
import monkeyPatchMediaDevices from './utils/media-devices.js';
var MINIMAL;
var LANG;
var CAPTIONS_LANG;
@ -29,6 +31,7 @@ var CAPTIONS_BUTTON;
var SINGLE_TOKEN;
var SESSION_NAME;
var FAKE_DEVICES;
var PARTICIPANT_NAME;
@ -43,6 +46,8 @@ $(document).ready(() => {
SINGLE_TOKEN = url.searchParams.get('singleToken') === null ? false : url.searchParams.get('singleToken') === 'true';
FAKE_DEVICES = url.searchParams.get('fakeDevices') === null ? false : url.searchParams.get('fakeDevices') === 'true';
// Directives
MINIMAL = url.searchParams.get('minimal') === null ? false : url.searchParams.get('minimal') === 'true';
LANG = url.searchParams.get('lang') || 'en';
@ -197,6 +202,11 @@ function appendElement(id) {
async function joinSession(sessionName, participantName) {
var webComponent = document.querySelector('openvidu-webcomponent');
var tokens;
if (FAKE_DEVICES) {
monkeyPatchMediaDevices();
}
if (SINGLE_TOKEN) {
tokens = await getToken(sessionName);
} else {

View File

@ -8,7 +8,10 @@
crossorigin="anonymous"
></script>
<script src="app.js"></script>
<script type="module" src="utils/filter-stream.js"></script>
<script type="module" src="utils/shader-renderer.js"></script>
<script type="module" src="utils/media-devices.js"></script>
<script type="module" src="app.js"></script>
<script src="openvidu-webcomponent-dev.js"></script>
<link rel="stylesheet" href="openvidu-webcomponent-dev.css" />

View File

@ -0,0 +1,30 @@
class FilterStream {
constructor(stream, label) {
const videoTrack = stream.getVideoTracks()[0];
const { width, height } = videoTrack.getSettings();
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const video = document.createElement('video');
video.srcObject = new MediaStream([videoTrack]);
video.play();
video.addEventListener('play', () => {
const loop = () => {
if (!video.paused && !video.ended) {
ctx.filter = 'grayscale(100%)';
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, video.videoWidth, video.videoHeight);
setTimeout(loop, 33);
}
};
loop();
});
this.outputStream = canvas.captureStream();
Object.defineProperty(this.outputStream.getVideoTracks()[0], 'label', {
writable: true,
value: label
});
}
}
export { FilterStream };

View File

@ -0,0 +1,62 @@
// Ideally we'd use an editor or import shaders directly from the API.
import { FilterStream } from './filter-stream.js';
export default function monkeyPatchMediaDevices() {
const enumerateDevicesFn = MediaDevices.prototype.enumerateDevices;
const getUserMediaFn = MediaDevices.prototype.getUserMedia;
const getDisplayMediaFn = MediaDevices.prototype.getDisplayMedia;
const fakeDevice = {
deviceId: 'virtual',
groupID: '',
kind: 'videoinput',
label: 'custom_fake_video_1'
};
MediaDevices.prototype.enumerateDevices = async function () {
const res = await enumerateDevicesFn.call(navigator.mediaDevices);
res.push(fakeDevice);
return res;
};
MediaDevices.prototype.getUserMedia = async function () {
const args = arguments[0];
const { deviceId, advanced, width, height } = args.video;
if (deviceId === 'virtual' || deviceId?.exact === 'virtual') {
const constraints = {
video: {
facingMode: args.facingMode,
advanced,
width,
height
},
audio: false
};
const res = await getUserMediaFn.call(navigator.mediaDevices, constraints);
if (res) {
const filter = new FilterStream(res, fakeDevice.label);
return filter.outputStream;
}
return res;
}
return getUserMediaFn.call(navigator.mediaDevices, ...arguments);
};
MediaDevices.prototype.getDisplayMedia = async function () {
const { video, audio } = arguments[0];
const screenVideoElement = document.getElementsByClassName("OT_video-element screen-type")[0];
const currentTrackLabel = screenVideoElement?.srcObject?.getVideoTracks()[0]?.label;
const res = await getDisplayMediaFn.call(navigator.mediaDevices, { video, audio });
if (res && currentTrackLabel && currentTrackLabel !== 'custom_fake_screen') {
const filter = new FilterStream(res, 'custom_fake_screen');
return filter.outputStream;
}
return res;
};
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -167,10 +167,10 @@ export class RecordingActivityComponent implements OnInit {
*/
deleteRecording(id: string) {
const succsessCallback = () => {
const succsessCallback = async () => {
this.onDeleteRecordingClicked.emit(id);
// Sending signal to all participants with the aim of updating their recordings list
this.openviduService.sendSignal(Signal.RECORDING_DELETED, this.openviduService.getRemoteConnections());
await this.openviduService.sendSignal(Signal.RECORDING_DELETED, this.openviduService.getRemoteConnections());
};
this.actionService.openDeleteRecordingDialog(succsessCallback.bind(this));
}

View File

@ -194,16 +194,18 @@ export class SessionComponent implements OnInit, OnDestroy {
this.cd.markForCheck();
}
ngOnDestroy() {
async ngOnDestroy() {
// Reconnecting session is received in Firefox
// To avoid 'Connection lost' message uses session.off()
this.session?.off('reconnecting');
this.participantService.clear();
this.session = null;
this.sessionScreen = null;
if (this.menuSubscription) this.menuSubscription.unsubscribe();
if (this.layoutWidthSubscription) this.layoutWidthSubscription.unsubscribe();
if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe();
if (!this.usedInPrejoinPage) {
this.session?.off('reconnecting');
await this.participantService.clear();
this.session = null;
this.sessionScreen = null;
if (this.menuSubscription) this.menuSubscription.unsubscribe();
if (this.layoutWidthSubscription) this.layoutWidthSubscription.unsubscribe();
if (this.captionLanguageSubscription) this.captionLanguageSubscription.unsubscribe();
}
}
leaveSession() {
@ -251,20 +253,29 @@ export class SessionComponent implements OnInit, OnDestroy {
private async connectToSession(): Promise<void> {
try {
const webcamToken = this.openviduService.getWebcamToken();
const screenToken = this.openviduService.getScreenToken();
const participant = this.participantService.getLocalParticipant();
const nickname = participant.getNickname();
const participantId = participant.id;
const screenPublisher = this.participantService.getMyScreenPublisher();
const cameraPublisher = this.participantService.getMyCameraPublisher();
if (this.participantService.haveICameraAndScreenActive()) {
await this.openviduService.connectSession(this.openviduService.getWebcamSession(), webcamToken);
await this.openviduService.connectSession(this.openviduService.getScreenSession(), screenToken);
await this.openviduService.publish(this.participantService.getMyCameraPublisher());
await this.openviduService.publish(this.participantService.getMyScreenPublisher());
} else if (this.participantService.isOnlyMyScreenActive()) {
await this.openviduService.connectSession(this.openviduService.getScreenSession(), screenToken);
await this.openviduService.publish(this.participantService.getMyScreenPublisher());
if (participant.hasCameraAndScreenActives()) {
const webcamSessionId = await this.openviduService.connectWebcamSession(participantId, nickname);
if (webcamSessionId) this.participantService.setMyCameraConnectionId(webcamSessionId);
const screenSessionId = await this.openviduService.connectScreenSession(participantId, nickname);
if (screenSessionId) this.participantService.setMyScreenConnectionId(screenSessionId);
await this.openviduService.publishCamera(cameraPublisher);
await this.openviduService.publishScreen(screenPublisher);
} else if (participant.hasOnlyScreenActive()) {
await this.openviduService.connectScreenSession(participantId, nickname);
await this.openviduService.publishScreen(screenPublisher);
} else {
await this.openviduService.connectSession(this.openviduService.getWebcamSession(), webcamToken);
await this.openviduService.publish(this.participantService.getMyCameraPublisher());
await this.openviduService.connectWebcamSession(participantId, nickname);
await this.openviduService.publishCamera(cameraPublisher);
}
} catch (error) {
// this._error.emit({ error: error.error, messgae: error.message, code: error.code, status: error.status });
@ -287,21 +298,21 @@ export class SessionComponent implements OnInit, OnDestroy {
}
private subscribeToConnectionCreatedAndDestroyed() {
this.session.on('connectionCreated', (event: ConnectionEvent) => {
this.session.on('connectionCreated', async (event: ConnectionEvent) => {
const connectionId = event.connection?.connectionId;
const nickname: string = this.participantService.getNicknameFromConnectionData(event.connection.data);
const connectionNickname: string = this.participantService.getNicknameFromConnectionData(event.connection.data);
const isRemoteConnection: boolean = !this.openviduService.isMyOwnConnection(connectionId);
const isCameraConnection: boolean = !nickname?.includes(`_${VideoType.SCREEN}`);
const isCameraConnection: boolean = !connectionNickname?.includes(`_${VideoType.SCREEN}`);
const nickname = this.participantService.getMyNickname();
const data = event.connection?.data;
if (isRemoteConnection && isCameraConnection) {
// Adding participant when connection is created and it's not screen
this.participantService.addRemoteConnection(connectionId, data, null);
//Sending nicnkanme signal to new participants
if (this.openviduService.needSendNicknameSignal()) {
const data = { clientData: this.participantService.getMyNickname() };
this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, [event.connection], data);
//Sending nicnkanme signal to new connection
if (this.openviduService.myNicknameHasBeenChanged()) {
await this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, [event.connection], { clientData: nickname });
}
}
});

View File

@ -3,7 +3,6 @@ import { PublisherProperties } from 'openvidu-browser';
import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model';
import { ParticipantAbstractModel } from '../../../models/participant.model';
import { VideoType } from '../../../models/video-type.model';
import { DeviceService } from '../../../services/device/device.service';
import { OpenViduService } from '../../../services/openvidu/openvidu.service';
import { ParticipantService } from '../../../services/participant/participant.service';
@ -58,7 +57,7 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
toggleMic() {
const publish = this.isAudioMuted;
this.openviduService.publishAudio(publish);
this.participantService.publishAudio(publish);
this.onAudioMutedClicked.emit(publish);
}
@ -66,7 +65,8 @@ export class AudioDevicesComponent implements OnInit, OnDestroy {
const audioSource = event?.value;
if (this.deviceSrv.needUpdateAudioTrack(audioSource)) {
const pp: PublisherProperties = { audioSource, videoSource: false };
await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
const publisher = this.participantService.getMyCameraPublisher();
await this.openviduService.replaceCameraTrack(publisher, pp);
this.deviceSrv.setMicSelected(audioSource);
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
}

View File

@ -23,16 +23,17 @@
videocam_off
</mat-icon>
</button>
<mat-form-field>
<mat-form-field id="video-devices-form">
<mat-label *ngIf="hasVideoDevices">{{ 'PREJOIN.VIDEO_DEVICE' | translate }}</mat-label>
<mat-label *ngIf="!hasVideoDevices">{{ 'PREJOIN.NO_VIDEO_DEVICE' | translate }}</mat-label>
<mat-select
[disabled]="isVideoMuted || !hasVideoDevices"
[value]="cameraSelected?.device"
[compareWith]="compareObjectDevices"
[value]="cameraSelected"
(click)="onDeviceSelectorClicked.emit()"
(selectionChange)="onCameraSelected($event)"
>
<mat-option *ngFor="let camera of cameras" [value]="camera.device">
<mat-option *ngFor="let camera of cameras" [value]="camera" id="option-{{camera.label}}">
{{ camera.label }}
</mat-option>
</mat-select>

View File

@ -4,7 +4,6 @@ import { Subscription } from 'rxjs';
import { CustomDevice } from '../../../models/device.model';
import { PanelType } from '../../../models/panel.model';
import { ParticipantAbstractModel } from '../../../models/participant.model';
import { VideoType } from '../../../models/video-type.model';
import { DeviceService } from '../../../services/device/device.service';
import { OpenViduService } from '../../../services/openvidu/openvidu.service';
import { PanelService } from '../../../services/panel/panel.service';
@ -21,8 +20,8 @@ import { VirtualBackgroundService } from '../../../services/virtual-background/v
styleUrls: ['./video-devices.component.css']
})
export class VideoDevicesComponent implements OnInit, OnDestroy {
@Output() onDeviceSelectorClicked = new EventEmitter<void>();
@Output() onVideoMutedClicked = new EventEmitter<boolean>();
@Output() onDeviceSelectorClicked = new EventEmitter<void>();
@Output() onVideoMutedClicked = new EventEmitter<boolean>();
videoMuteChanging: boolean;
isVideoMuted: boolean;
@ -47,9 +46,8 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
await this.deviceSrv.refreshDevices();
}
this.hasVideoDevices = this.deviceSrv.hasVideoDeviceAvailable();
if(this.hasVideoDevices){
if (this.hasVideoDevices) {
this.cameras = this.deviceSrv.getCameras();
this.cameraSelected = this.deviceSrv.getCameraSelected();
}
@ -67,7 +65,7 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
async toggleCam() {
this.videoMuteChanging = true;
const publish = this.isVideoMuted;
await this.openviduService.publishVideo(publish);
await this.participantService.publishVideo(publish);
if (this.isVideoMuted && this.panelService.isExternalPanelOpened()) {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
}
@ -76,19 +74,21 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
}
async onCameraSelected(event: any) {
const videoSource = event?.value;
const device: CustomDevice = event?.value;
// Is New deviceId different from the old one?
if (this.deviceSrv.needUpdateVideoTrack(videoSource)) {
const mirror = this.deviceSrv.cameraNeedsMirror(videoSource);
if (this.deviceSrv.needUpdateVideoTrack(device)) {
const mirror = this.deviceSrv.cameraNeedsMirror(device.device);
// Reapply Virtual Background to new Publisher if necessary
const backgroundSelected = this.backgroundService.backgroundSelected.getValue();
const isBackgroundApplied = this.backgroundService.isBackgroundApplied()
const isBackgroundApplied = this.backgroundService.isBackgroundApplied();
if (isBackgroundApplied) {
await this.backgroundService.removeBackground();
}
const pp: PublisherProperties = { videoSource, audioSource: false, mirror };
await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
const pp: PublisherProperties = { videoSource: device.device, audioSource: false, mirror };
const publisher = this.participantService.getMyCameraPublisher();
await this.openviduService.replaceCameraTrack(publisher, pp);
if (isBackgroundApplied) {
const bgSelected = this.backgroundService.backgrounds.find((b) => b.id === backgroundSelected);
@ -97,11 +97,19 @@ export class VideoDevicesComponent implements OnInit, OnDestroy {
}
}
this.deviceSrv.setCameraSelected(videoSource);
this.deviceSrv.setCameraSelected(device.device);
this.cameraSelected = this.deviceSrv.getCameraSelected();
}
}
/**
* @internal
* Compare two devices to check if they are the same. Used by the mat-select
*/
compareObjectDevices(o1: CustomDevice, o2: CustomDevice): boolean {
return o1.label === o2.label;
}
protected subscribeToParticipantMediaProperties() {
this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p: ParticipantAbstractModel) => {
if (p) {

View File

@ -1,9 +1,8 @@
/* Fixes layout bug. The OT_root is created with the entire layout width and it has a weird UX behaviour */
.no-size {
/* Fixes layout bug. The OT_root is created with the entire layout width and it has a weird UX behaviour */
.no-size {
height: 0px !important;
width: 0px !important;
}
}
.nickname {
padding: 0px;
@ -11,46 +10,44 @@
z-index: 999;
border-radius: var(--ov-video-radius);
color: var(--ov-text-color);
font-family: 'Roboto','RobotoDraft',Helvetica,Arial,sans-serif;
}
.nicknameContainer {
font-family: 'Roboto', 'RobotoDraft', Helvetica, Arial, sans-serif;
}
.nicknameContainer {
background-color: var(--ov-secondary-color);
padding: 5px;
color: var(--ov-text-color);
font-weight: bold;
border-radius: var(--ov-video-radius);
}
}
#nickname-input-container {
background-color: var(--ov-secondary-color);
#nickname-input-container {
background-color: var(--ov-secondary-color);
border-radius: var(--ov-video-radius);
}
}
#closeButton {
#closeButton {
position: absolute;
top: -3px;
right: 0;
z-index: 999;
}
}
#nicknameForm {
#nicknameForm {
padding: 10px;
}
}
#audio-wave-container {
#audio-wave-container {
position: absolute;
right: 0;
z-index: 999;
padding: 5px;
}
right: 0;
z-index: 2;
padding: 5px;
}
.fullscreen {
.fullscreen {
top: 40px;
}
}
video {
video {
-o-object-fit: cover;
object-fit: cover;
width: 100%;
@ -60,57 +57,61 @@
padding: 0;
border: 0;
font-size: 100%;
}
}
.status-icons, #settings-container {
.status-icons,
#settings-container {
position: absolute;
bottom: 0;
z-index: 9999;
text-align: center;
}
}
.status-icons {
.status-icons {
left: 0;
}
}
.status-icons button, #settings-container button {
.status-icons button,
#settings-container button {
color: var(--ov-text-color);
width: 26px;
height: 26px;
margin: 5px;
border-radius: var(--ov-buttons-radius);
}
}
.status-icons button {
.status-icons button {
background-color: var(--ov-warn-color);
}
}
.status-icons .mat-icon-button, #settings-container .mat-icon-button{
.status-icons .mat-icon-button,
#settings-container .mat-icon-button {
line-height: 0px;
}
}
.status-icons mat-icon, #settings-container mat-icon {
.status-icons mat-icon,
#settings-container mat-icon {
font-size: 18px;
}
}
#settings-container{
#settings-container {
right: 0;
}
}
#settings-container button {
#settings-container button {
background-color: var(--ov-secondary-color);
}
}
/* Contains the video element, used to fix video letter-boxing */
.OV_stream {
/* Contains the video element, used to fix video letter-boxing */
.OV_stream {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
background-color: transparent;
border-radius: var(--ov-video-radius);
}
}
input {
input {
caret-color: #ffffff !important;
}
}

View File

@ -1,7 +1,7 @@
<div
*ngIf="this._stream"
class="OV_stream"
[ngClass]="{'no-size': !showVideo}"
[ngClass]="{ 'no-size': !showVideo }"
[id]="'container-' + this._stream.streamManager?.stream?.streamId"
#streamContainer
>
@ -50,11 +50,18 @@
</div>
<div *ngIf="!isMinimal && showSettingsButton" id="settings-container" class="videoButtons">
<button mat-icon-button (click)="toggleVideoMenu($event)" matTooltip="{{ 'STREAM.SETTINGS' | translate }}" matTooltipPosition="above" aria-label="Video settings menu" id="stream-menu-btn">
<button
mat-icon-button
(click)="toggleVideoMenu($event)"
matTooltip="{{ 'STREAM.SETTINGS' | translate }}"
matTooltipPosition="above"
aria-label="Video settings menu"
id="video-settings-btn-{{this._stream.streamManager?.stream?.typeOfVideo}}"
>
<mat-icon>more_vert</mat-icon>
</button>
<span [matMenuTriggerFor]="menu"></span>
<mat-menu #menu="matMenu" yPosition="above" xPosition="before">
<mat-menu #menu="matMenu" yPosition="above" xPosition="before" class="video-settings-menu">
<button mat-menu-item id="videoZoomButton" (click)="toggleVideoEnlarged()">
<mat-icon>{{ this.videoSizeIcon }}</mat-icon>
<span *ngIf="videoSizeIcon === videoSizeIconEnum.NORMAL">{{ 'STREAM.ZOOM_OUT' | translate }}</span>
@ -70,7 +77,7 @@
<button
mat-menu-item
(click)="replaceScreenTrack()"
id="changeScreenButton"
id="replace-screen-button"
*ngIf="!this._stream.streamManager?.remote && this._stream.streamManager?.stream?.typeOfVideo === videoTypeEnum.SCREEN"
>
<mat-icon>picture_in_picture</mat-icon>

View File

@ -231,12 +231,12 @@ export class StreamComponent implements OnInit {
/**
* @ignore
*/
updateNickname(event) {
async updateNickname(event) {
if (event?.keyCode === 13 || event?.type === 'focusout') {
if (!!this.nickname) {
this.participantService.setMyNickname(this.nickname);
this.storageService.setNickname(this.nickname);
this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, undefined, { clientData: this.nickname });
await this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, undefined, { clientData: this.nickname });
}
this.toggleNicknameForm();
}
@ -252,7 +252,8 @@ export class StreamComponent implements OnInit {
publishAudio: !this.participantService.isMyCameraActive(),
mirror: false
};
await this.openviduService.replaceTrack(VideoType.SCREEN, properties);
const publisher = this.participantService.getMyScreenPublisher();
await this.openviduService.replaceScreenTrack(publisher, properties);
}
private checkVideoEnlarged() {

View File

@ -500,7 +500,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
async toggleMicrophone() {
this.onMicrophoneButtonClicked.emit();
try {
await this.openviduService.publishAudio(!this.isAudioActive);
this.participantService.publishAudio(!this.isAudioActive);
} catch (error) {
this.log.e('There was an error toggling microphone:', error.code, error.message);
this.actionService.openDialog(
@ -521,7 +521,7 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
if (this.panelService.isExternalPanelOpened() && !publishVideo) {
this.panelService.togglePanel(PanelType.BACKGROUND_EFFECTS);
}
await this.openviduService.publishVideo(publishVideo);
await this.participantService.publishVideo(publishVideo);
} catch (error) {
this.log.e('There was an error toggling camera:', error.code, error.message);
this.actionService.openDialog(this.translateService.translate('ERRORS.TOGGLE_CAMERA'), error?.error || error?.message || error);

View File

@ -601,7 +601,14 @@ export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewIni
await this.handlePublisherError(e);
resolve();
});
publisher.once('accessAllowed', () => resolve());
publisher.once('accessAllowed', () => {
this.participantService.setMyCameraPublisher(publisher);
this.participantService.updateLocalParticipant();
resolve();
});
} else {
this.participantService.setMyCameraPublisher(undefined);
this.participantService.updateLocalParticipant();
}
} catch (error) {
this.actionService.openDialog(error.name.replace(/_/g, ' '), error.message, true);

View File

@ -25,7 +25,7 @@ import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
* publishVideo = true;
* publishAudio = true;
*
* constructor(private httpClient: HttpClient, private openviduService: OpenViduService) { }
* constructor(private httpClient: HttpClient, private participantService: ParticipantService) { }
*
* async ngOnInit() {
* this.tokens = {
@ -36,18 +36,18 @@ import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
*
* toggleVideo() {
* this.publishVideo = !this.publishVideo;
* this.openviduService.publishVideo(this.publishVideo);
* this.participantService.publishVideo(this.publishVideo);
* }
*
* toggleAudio() {
* this.publishAudio = !this.publishAudio;
* this.openviduService.publishAudio(this.publishAudio);
* this.participantService.publishAudio(this.publishAudio);
* }
*
* async getToken(): Promise<string> {
* // Returns an OpeVidu token
* }
*
*
* }
* ```
*
@ -88,13 +88,12 @@ export class ToolbarDirective {
sessionId = "panel-directive-example";
tokens!: TokenModel;
* sessionId = 'toolbar-additionalbtn-directive-example';
* tokens!: TokenModel;
*
* constructor(
* private httpClient: HttpClient,
* private openviduService: OpenViduService,
* private participantService: ParticipantService
* ) { }
*
@ -107,18 +106,18 @@ export class ToolbarDirective {
*
* toggleVideo() {
* const publishVideo = !this.participantService.isMyVideoActive();
* this.openviduService.publishVideo(publishVideo);
* this.participantService.publishVideo(publishVideo);
* }
*
* toggleAudio() {
* const publishAudio = !this.participantService.isMyAudioActive();
* this.openviduService.publishAudio(publishAudio);
* this.participantService.publishAudio(publishAudio);
* }
*
* async getToken(): Promise<string> {
* // Returns an OpeVidu token
* }
*
*
* }
* ```
*/
@ -149,7 +148,7 @@ export class ToolbarAdditionalButtonsDirective {
*
* ```javascript
* export class ToolbarAdditionalPanelButtonsDirectiveComponent {
*
*
* sessionId = "toolbar-additionalPanelbtn";
* tokens!: TokenModel;
*
@ -169,7 +168,7 @@ export class ToolbarAdditionalButtonsDirective {
* async getToken(): Promise<string> {
* // Returns an OpeVidu token
* }
*
*
* }
* ```
*/
@ -206,7 +205,7 @@ export class ToolbarAdditionalPanelButtonsDirective {
*
* ```javascript
* export class PanelDirectiveComponent {
*
*
* sessionId = "panel-directive-example";
* tokens!: TokenModel;
*
@ -222,7 +221,7 @@ export class ToolbarAdditionalPanelButtonsDirective {
* async getToken(): Promise<string> {
* // Returns an OpeVidu token
* }
*
*
* }
* ```
*/
@ -275,7 +274,7 @@ export class PanelDirective {
*
* ```javascript
* export class AdditionalPanelsDirectiveComponent implements OnInit {
*
*
* sessionId = "toolbar-additionalbtn-directive-example";
* tokens!: TokenModel;
*
@ -445,7 +444,7 @@ export class BackgroundEffectsPanelDirective {
*
* ```javascript
* export class AppComponent implements OnInit {
*
*
* sessionId = "activities-panel-directive-example";
* tokens!: TokenModel;
*
@ -585,7 +584,7 @@ export class ParticipantsPanelDirective {
* async getToken(): Promise<string> {
* // Returns an OpeVidu token
* }
*
*
* }
* ```
*
@ -684,10 +683,10 @@ export class ParticipantPanelItemElementsDirective {
*
* We need to get the participants in our Session, so we use the {@link ParticipantService} to subscribe to the required Observables.
* We'll get the local participant and the remote participants to display their streams in our custom session layout.
*
*
* ```javascript
* export class LayoutDirectiveComponent implements OnInit, OnDestroy {
*
*
* sessionId = 'layout-directive-example';
* tokens!: TokenModel;
*
@ -723,7 +722,7 @@ export class ParticipantPanelItemElementsDirective {
* async getToken(): Promise<string> {
* // Returns an OpeVidu token
* }
*
*
* }
* ```
*/

View File

@ -21,7 +21,7 @@ export interface StreamModel {
/**
* The streamManager object from openvidu-browser library.{@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/StreamManager.html}
*/
streamManager: StreamManager;
streamManager: StreamManager | undefined;
/**
* Whether the stream is enlarged or not
*/
@ -29,7 +29,7 @@ export interface StreamModel {
/**
* Unique identifier of the stream
*/
connectionId: string;
connectionId: string | undefined;
/**
* The participant object
*/
@ -68,7 +68,7 @@ export abstract class ParticipantAbstractModel {
isMutedForcibly: boolean;
constructor(props: ParticipantProperties, model?: StreamModel) {
this.id = props.id ? props.id : Math.random().toString(32).replace('.','_');
this.id = props.id || Math.random().toString(32).replace('.','_');
this.local = props.local;
this.nickname = props.nickname;
this.colorProfile = !!props.colorProfile ? props.colorProfile : `hsl(${Math.random() * 360}, 100%, 80%)`;
@ -76,9 +76,9 @@ export abstract class ParticipantAbstractModel {
let streamModel: StreamModel = {
connected: model ? model.connected : true,
type: model ? model.type : VideoType.CAMERA,
streamManager: model ? model.streamManager : null,
streamManager: model?.streamManager,
videoEnlarged: model ? model.videoEnlarged : false,
connectionId: model ? model.connectionId : null,
connectionId: model?.connectionId,
participant: this
};
this.streams.set(streamModel.type, streamModel);
@ -113,7 +113,7 @@ export abstract class ParticipantAbstractModel {
private isCameraAudioActive(): boolean {
const cameraConnection = this.getCameraConnection();
if (cameraConnection?.connected) {
return cameraConnection.streamManager?.stream?.audioActive;
return cameraConnection.streamManager?.stream?.audioActive || false;
}
return false;
}
@ -132,7 +132,7 @@ export abstract class ParticipantAbstractModel {
isScreenAudioActive(): boolean {
const screenConnection = this.getScreenConnection();
if (screenConnection?.connected) {
return screenConnection?.streamManager?.stream?.audioActive;
return screenConnection?.streamManager?.stream?.audioActive || false;
}
return false;
}
@ -160,13 +160,14 @@ export abstract class ParticipantAbstractModel {
/**
* @internal
* @returns The participant active connection types
*/
getConnectionTypesActive(): VideoType[] {
let connType = [];
if (this.isCameraActive()) connType.push(VideoType.CAMERA);
if (this.isScreenActive()) connType.push(VideoType.SCREEN);
getActiveConnectionTypes(): VideoType[] {
const activeTypes: VideoType[] = [];
if (this.isCameraActive()) activeTypes.push(VideoType.CAMERA);
if (this.isScreenActive()) activeTypes.push(VideoType.SCREEN);
return connType;
return activeTypes;
}
/**
@ -218,7 +219,6 @@ export abstract class ParticipantAbstractModel {
*/
isLocal(): boolean {
return this.local;
// return Array.from(this.streams.values()).every((conn) => conn.local);
}
/**
@ -238,7 +238,7 @@ export abstract class ParticipantAbstractModel {
/**
* @internal
*/
setCameraPublisher(publisher: Publisher) {
setCameraPublisher(publisher: Publisher | undefined) {
const cameraConnection = this.getCameraConnection();
if (cameraConnection) cameraConnection.streamManager = publisher;
}
@ -307,6 +307,30 @@ export abstract class ParticipantAbstractModel {
if (screenConnection) screenConnection.connected = false;
}
/**
* @internal
* @returns true if both camera and screen are active
*/
hasCameraAndScreenActives(): boolean {
return this.isCameraActive() && this.isScreenActive();
}
/**
* @internal
* @returns true if only screen is active
*/
hasOnlyScreenActive(): boolean {
return this.isScreenActive() && !this.isCameraActive();
}
/**
* @internal
* @returns true if only camera is active
*/
hasOnlyCameraActive(): boolean {
return this.isCameraActive() && !this.isScreenActive();
}
/**
* @internal
*/

View File

@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
import { StreamModel, ParticipantAbstractModel } from '../models/participant.model';
import { ParticipantAbstractModel, StreamModel } from '../models/participant.model';
import { TranslateService } from '../services/translate/translate.service';
@Pipe({ name: 'streams' })
@ -10,11 +10,8 @@ export class ParticipantStreamsPipe implements PipeTransform {
let streams: StreamModel[] = [];
if(participants && Object.keys(participants).length > 0){
if (Array.isArray(participants)) {
participants.forEach((p) => {
streams = streams.concat(p.getAvailableConnections());
});
streams = participants.map(p => p.getAvailableConnections()).flat();
} else {
streams = participants.getAvailableConnections();
}
}
@ -30,15 +27,11 @@ export class StreamTypesEnabledPipe implements PipeTransform {
constructor(private translateService: TranslateService) {}
transform(participant: ParticipantAbstractModel): string {
let result = '';
let activeStreams = participant?.getConnectionTypesActive().toString();
const activeStreamsArr: string[] = activeStreams.split(',');
activeStreamsArr.forEach((type, index) => {
result += this.translateService.translate(`PANEL.PARTICIPANTS.${type}`)
if(activeStreamsArr.length > 0 && index < activeStreamsArr.length - 1){
result += ', ';
}
});
return `(${result})`;
const activeStreams = participant?.getActiveConnectionTypes() ?? [];
const streamNames = activeStreams.map(streamType => this.translateService.translate(`PANEL.PARTICIPANTS.${streamType}`));
const streamsString = streamNames.join(', ');
return `(${streamsString})`;
}
}

View File

@ -64,7 +64,7 @@ export class ChatService {
});
}
sendMessage(message: string) {
async sendMessage(message: string) {
message = message.replace(/ +(?= )/g, '');
if (message !== '' && message !== ' ') {
const data = {
@ -72,7 +72,7 @@ export class ChatService {
nickname: this.participantService.getMyNickname()
};
this.openviduService.sendSignal(Signal.CHAT, undefined, data);
await this.openviduService.sendSignal(Signal.CHAT, undefined, data);
}
}

View File

@ -177,8 +177,8 @@ export class DeviceService {
return this.microphoneSelected;
}
setCameraSelected(deviceField: any) {
this.cameraSelected = this.getCameraByDeviceField(deviceField);
setCameraSelected(deviceId: any) {
this.cameraSelected = this.getCameraByDeviceField(deviceId);
this.saveCameraToStorage(this.cameraSelected);
}
@ -187,8 +187,8 @@ export class DeviceService {
this.saveMicrophoneToStorage(this.microphoneSelected);
}
needUpdateVideoTrack(newVideoSource: string): boolean {
return this.cameraSelected?.device !== newVideoSource;
needUpdateVideoTrack(newDevice: CustomDevice): boolean {
return this.cameraSelected?.device !== newDevice.device || this.cameraSelected?.label !== newDevice.label;
}
needUpdateAudioTrack(newAudioSource: string): boolean {

View File

@ -16,7 +16,7 @@ export class PanelService {
private isExternalOpened: boolean = false;
private externalType: string;
protected _panelOpened = <BehaviorSubject<PanelEvent>>new BehaviorSubject({ opened: false });
private panelMap: Map<string, boolean> = new Map();
private panelTypes: string[] = Object.values(PanelType);
/**
* @internal
@ -24,7 +24,6 @@ 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,31 +32,22 @@ export class PanelService {
*/
togglePanel(type: PanelType | string, expand?: PanelSettingsOptions | string) {
let nextOpenedValue: boolean = false;
if (this.panelMap.has(type)) {
const oldType = this._panelOpened.getValue().type;
const oldOpened = this._panelOpened.getValue().opened;
if (this.panelTypes.includes(type)) {
this.log.d(`Toggling ${type} menu`);
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);
}
});
nextOpenedValue = oldType !== type ? true : !oldOpened;
} else {
// Panel is external
this.log.d('Toggling external panel');
// Close all panels
this.panelMap.forEach((_, panel: string) => this.panelMap.set(panel, false));
// Opening when external panel is closed or is opened with another type
this.isExternalOpened = !this.isExternalOpened || this.externalType !== type;
this.externalType = !this.isExternalOpened ? '' : type;
nextOpenedValue = this.isExternalOpened;
}
const oldType = this._panelOpened.getValue().type;
this._panelOpened.next({ opened: nextOpenedValue, type, expand, oldType });
}
@ -65,51 +55,54 @@ export class PanelService {
* @internal
*/
isPanelOpened(): boolean {
const anyOpened = Array.from(this.panelMap.values()).some((opened) => opened);
return anyOpened || this.isExternalPanelOpened();
return this._panelOpened.getValue().opened;
}
/**
* Closes the panel (if opened)
*/
closePanel(): void {
this.panelMap.forEach((_, panel: string) => this.panelMap.set(panel, false));
this._panelOpened.next({ opened: false });
this._panelOpened.next({ opened: false, type: undefined, expand: undefined, oldType: undefined });
}
/**
* Whether the chat panel is opened or not.
*/
isChatPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.CHAT);
const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.CHAT;
}
/**
* Whether the participants panel is opened or not.
*/
isParticipantsPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.PARTICIPANTS);
const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.PARTICIPANTS;
}
/**
* Whether the activities panel is opened or not.
*/
isActivitiesPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.ACTIVITIES);
const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.ACTIVITIES;
}
/**
* Whether the settings panel is opened or not.
*/
isSettingsPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.SETTINGS);
const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.SETTINGS;
}
/**
* Whether the background effects panel is opened or not.
*/
isBackgroundEffectsPanelOpened(): boolean {
return !!this.panelMap.get(PanelType.BACKGROUND_EFFECTS);
const panelState = this._panelOpened.getValue();
return panelState.opened && panelState.type === PanelType.BACKGROUND_EFFECTS;
}
isExternalPanelOpened(): boolean {

View File

@ -2,10 +2,18 @@ import { Injectable } from '@angular/core';
import { Publisher, Subscriber } from 'openvidu-browser';
import { BehaviorSubject, Observable } from 'rxjs';
import { ILogger } from '../../models/logger.model';
import { OpenViduRole, ParticipantAbstractModel, ParticipantModel, ParticipantProperties, StreamModel } from '../../models/participant.model';
import {
OpenViduRole,
ParticipantAbstractModel,
ParticipantModel,
ParticipantProperties,
StreamModel
} from '../../models/participant.model';
import { VideoType } from '../../models/video-type.model';
import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service';
import { DeviceService } from '../device/device.service';
import { LoggerService } from '../logger/logger.service';
import { OpenViduService } from '../openvidu/openvidu.service';
@Injectable({
providedIn: 'root'
@ -33,9 +41,13 @@ export class ParticipantService {
/**
* @internal
*/
constructor(protected openviduAngularConfigSrv: OpenViduAngularConfigService, protected loggerSrv: LoggerService) {
constructor(
protected openviduAngularConfigSrv: OpenViduAngularConfigService,
private openviduService: OpenViduService,
private deviceService: DeviceService,
protected loggerSrv: LoggerService
) {
this.log = this.loggerSrv.get('ParticipantService');
this.localParticipantObs = this._localParticipant.asObservable();
this.remoteParticipantsObs = this._remoteParticipants.asObservable();
}
@ -52,6 +64,129 @@ export class ParticipantService {
return this.localParticipant;
}
/**
* Publish or unpublish the local participant video stream (if available).
* It hides the camera stream (while muted) if screen is sharing.
* See openvidu-browser {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Publisher.html#publishVideo publishVideo}
*
*/
async publishVideo(publish: boolean): Promise<void> {
const publishAudio = this.isMyAudioActive();
const cameraPublisher = this.getMyCameraPublisher();
const screenPublisher = this.getMyScreenPublisher();
// Disabling webcam
if (this.localParticipant.hasCameraAndScreenActives()) {
await this.publishVideoAux(cameraPublisher, publish);
this.disableWebcamStream();
this.openviduService.unpublishCamera(cameraPublisher);
this.publishAudioAux(screenPublisher, publishAudio);
} else if (this.localParticipant.hasOnlyScreenActive()) {
// Enabling webcam
const hasAudio = this.hasScreenAudioActive();
const sessionId = await this.openviduService.connectWebcamSession(this.getMyNickname(), this.getLocalParticipant().id);
if (sessionId) this.setMyCameraConnectionId(sessionId);
await this.openviduService.publishCamera(cameraPublisher);
await this.publishVideoAux(cameraPublisher, true);
this.publishAudioAux(screenPublisher, false);
this.publishAudioAux(cameraPublisher, hasAudio);
this.enableWebcamStream();
} else {
// Muting/unmuting webcam
await this.publishVideoAux(cameraPublisher, publish);
}
this.updateLocalParticipant();
}
/**
* Publish or unpublish the local participant audio stream (if available).
* See openvidu-browser {@link https://docs.openvidu.io/en/stable/api/openvidu-browser/classes/Publisher.html#publishAudio publishAudio}.
*
*/
publishAudio(publish: boolean): void {
if (this.isMyCameraActive()) {
if (this.localParticipant.isScreenActive() && this.hasScreenAudioActive()) {
this.publishAudioAux(this.getMyScreenPublisher(), false);
}
this.publishAudioAux(this.getMyCameraPublisher(), publish);
} else {
this.publishAudioAux(this.getMyScreenPublisher(), publish);
}
this.updateLocalParticipant();
}
/**
* Share or unshare the local participant screen.
* Hide the camera stream (while muted) when screen is sharing.
*
*/
async toggleScreenshare() {
const screenPublisher = this.getMyScreenPublisher();
const cameraPublisher = this.getMyCameraPublisher();
const participantNickname = this.getMyNickname();
const participantId = this.getLocalParticipant().id;
if (this.localParticipant.hasCameraAndScreenActives()) {
// Disabling screenShare
this.disableScreenStream();
this.updateLocalParticipant();
this.openviduService.unpublishScreen(screenPublisher);
} else if (this.localParticipant.hasOnlyCameraActive()) {
// I only have the camera published
const willWebcamBePresent = this.isMyCameraActive() && this.isMyVideoActive();
const hasAudio = willWebcamBePresent ? false : this.isMyAudioActive();
const screenPublisher = await this.openviduService.initScreenPublisher(hasAudio);
screenPublisher.once('accessAllowed', async () => {
// Listen to event fired when native stop button is clicked
screenPublisher.stream
.getMediaStream()
.getVideoTracks()[0]
.addEventListener('ended', async () => {
this.log.d('Clicked native stop button. Stopping screen sharing');
await this.toggleScreenshare();
});
// Enabling screenShare
this.activeMyScreenShare(screenPublisher);
if (!this.openviduService.isScreenSessionConnected()) {
await this.openviduService.connectScreenSession(participantId, participantNickname);
}
await this.openviduService.publishScreen(screenPublisher);
if (!this.isMyVideoActive()) {
// Disabling webcam
this.disableWebcamStream();
this.updateLocalParticipant();
this.openviduService.unpublishCamera(cameraPublisher);
}
});
screenPublisher.once('accessDenied', (error: any) => {
return Promise.reject(error);
});
} else {
// I only have my screenshare active and I have no camera or it is muted
const hasAudio = this.hasScreenAudioActive();
// Enable webcam
if (!this.openviduService.isWebcamSessionConnected()) {
await this.openviduService.connectWebcamSession(participantId, participantNickname);
}
await this.openviduService.publishCamera(cameraPublisher);
this.publishAudioAux(cameraPublisher, hasAudio);
this.enableWebcamStream();
// Disabling screenshare
this.disableScreenStream();
this.updateLocalParticipant();
this.openviduService.unpublishScreen(screenPublisher);
}
}
/**
* @internal
*/
@ -62,7 +197,7 @@ export class ParticipantService {
/**
* @internal
*/
setMyCameraPublisher(publisher: Publisher) {
setMyCameraPublisher(publisher: Publisher | undefined) {
this.localParticipant.setCameraPublisher(publisher);
}
/**
@ -98,7 +233,6 @@ export class ParticipantService {
*/
enableWebcamStream() {
this.localParticipant.enableCamera();
this.updateLocalParticipant();
}
/**
@ -106,7 +240,6 @@ export class ParticipantService {
*/
disableWebcamStream() {
this.localParticipant.disableCamera();
this.updateLocalParticipant();
}
/**
@ -134,7 +267,6 @@ export class ParticipantService {
*/
disableScreenStream() {
this.localParticipant.disableScreen();
this.updateLocalParticipant();
}
/**
@ -180,7 +312,9 @@ export class ParticipantService {
/**
* @internal
*/
clear() {
async clear() {
await this.getMyCameraPublisher()?.stream?.disposeMediaStream();
await this.getMyScreenPublisher()?.stream?.disposeMediaStream();
this.disableScreenStream();
this.remoteParticipants = [];
this.updateRemoteParticipants();
@ -202,34 +336,6 @@ export class ParticipantService {
return this.localParticipant?.hasAudioActive();
}
/**
* @internal
*/
isMyScreenActive(): boolean {
return this.localParticipant.isScreenActive();
}
/**
* @internal
*/
isOnlyMyCameraActive(): boolean {
return this.isMyCameraActive() && !this.isMyScreenActive();
}
/**
* @internal
*/
isOnlyMyScreenActive(): boolean {
return this.isMyScreenActive() && !this.isMyCameraActive();
}
/**
* @internal
*/
haveICameraAndScreenActive(): boolean {
return this.isMyCameraActive() && this.isMyScreenActive();
}
/**
* @internal
*/
@ -241,7 +347,32 @@ export class ParticipantService {
* Force to update the local participant object and fire a new {@link localParticipantObs} Observable event.
*/
updateLocalParticipant() {
this._localParticipant.next(Object.assign(Object.create(this.localParticipant), this.localParticipant));
this._localParticipant.next(
Object.assign(Object.create(Object.getPrototypeOf(this.localParticipant)), { ...this.localParticipant })
);
}
private publishAudioAux(publisher: Publisher, value: boolean): void {
if (!!publisher) {
publisher.publishAudio(value);
}
}
/**
* @internal
*/
private async publishVideoAux(publisher: Publisher, publish: boolean): Promise<void> {
if (!!publisher) {
let resource: boolean | MediaStreamTrack = true;
if (publish) {
// Forcing restoration with a custom media stream (the older one instead the default)
const currentDeviceId = this.deviceService.getCameraSelected()?.device;
const mediaStream = await this.openviduService.createMediaStream({ videoSource: currentDeviceId, audioSource: false });
resource = mediaStream.getVideoTracks()[0];
}
await publisher.publishVideo(publish, resource);
}
}
/**