Merge branch 'ov_components'

pull/707/head
csantosm 2022-03-17 12:18:03 +01:00
commit dc6f38c368
116 changed files with 13764 additions and 6319 deletions

View File

@ -0,0 +1,32 @@
name: openvidu-angular E2E
on:
push:
paths:
- 'openvidu-components-angular/**'
pull_request:
branches:
- master
jobs:
webcomponent_e2e:
name: WebComponent E2E tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Launch Selenium Chromedriver
run: docker run --shm-size="2g" --network host selenium/standalone-chrome:latest
- uses: actions/setup-node@v2
with:
node-version: '16'
- name: Launch openvidu-server-kms
run: docker run -p 4443:4443 --rm -d -e OPENVIDU_SECRET=MY_SECRET openvidu/openvidu-server-kms:latest
- name: Install dependencies
run: |
npm install --prefix openvidu-components-angular && \
npm install --prefix openvidu-components-angular/webcomponent-test-e2e
- name: Build Webcomponent
run: npm run webcomponent:prepare-test-e2e --prefix openvidu-components-angular
- name: Start webapp
run: npm run serve --prefix openvidu-components-angular/webcomponent-test-e2e &
- name: Running E2E tests
run: npm run test-ci --prefix openvidu-components-angular/webcomponent-test-e2e

16
.gitignore vendored
View File

@ -26,10 +26,16 @@ nbactions.xml
*/.settings/*
*/.tscache/*
openvidu-components-angular/node_modules/
openvidu-components-angular/.angular/
**/node_modules/
**/.angular/
**/.vscode
openvidu-components-angular/dist/
openvidu-components-angular/coverage/**
openvidu-components-angular/openvidu-webcomponent/
openvidu-components-angular/openvidu-angular-doc/
openvidu-components-angular/webcomponent-test-e2e/dist/
openvidu-components-angular/webcomponent-test-e2e/web/openvidu-webcomponent-dev.css
openvidu-components-angular/webcomponent-test-e2e/web/openvidu-webcomponent-dev.js

View File

@ -5,12 +5,13 @@ const VERSION = require("./package.json").version;
module.exports.buildWebcomponent = async () => {
console.log("Building OpenVidu Web Component (" + VERSION + ")");
const tutorialWcPath = "../../openvidu-tutorials/openvidu-webcomponent/web";
const e2eWcPath = "../webcomponent-test-e2e/web";
const e2eWcPath = "./webcomponent-test-e2e/web";
try {
await buildElement();
await copyFiles(tutorialWcPath);
await copyFiles(e2eWcPath);
await renameWebComponentTestName(e2eWcPath);
console.log("OpenVidu Web Component (" + VERSION + ") built");
} catch (error) {
@ -20,14 +21,9 @@ module.exports.buildWebcomponent = async () => {
async function buildElement() {
const files = [
// "./dist/openvidu-call/runtime.js",
// "./dist/openvidu-call/polyfills.js",
// "./dist/openvidu-call/scripts.js",
// "./dist/openvidu-call/main.js",
"./dist/openvidu-webcomponent/runtime.js",
"./dist/openvidu-webcomponent/main.js",
"./dist/openvidu-webcomponent/polyfills.js",
// "./dist/openvidu-webcomponent/scripts.js",
"./dist/openvidu-webcomponent/polyfills.js"
];
try {
@ -50,6 +46,11 @@ async function buildElement() {
}
}
function renameWebComponentTestName(dir) {
fs.renameSync(`${dir}/openvidu-webcomponent-${VERSION}.js`, `${dir}/openvidu-webcomponent-dev.js`);
fs.renameSync(`${dir}/openvidu-webcomponent-${VERSION}.css`, `${dir}/openvidu-webcomponent-dev.css`);
}
async function copyFiles(destination) {
if (fs.existsSync(destination)) {
try {

File diff suppressed because it is too large Load Diff

View File

@ -9,6 +9,7 @@
"lib:copy": "cp dist/openvidu-angular/openvidu-angular-*.tgz ../openvidu-tutorials/openvidu-angular-components",
"lib:test": "ng test openvidu-angular --no-watch --code-coverage",
"webcomponent:build": "./node_modules/@angular/cli/bin/ng.js build openvidu-webcomponent --configuration production && node ./openvidu-webcomponent-build.js",
"webcomponent:prepare-test-e2e": "npm run lib:build && npm run webcomponent:build",
"bundle-report": "ng build openvidu-webcomponent --stats-json --configuration production && webpack-bundle-analyzer dist/openvidu-webcomponent/stats.json",
"lint": "ng lint",
"e2e": "ng e2e"
@ -27,14 +28,13 @@
"@angular/router": "13.0.0",
"autolinker": "3.14.3",
"buffer": "^6.0.3",
"ng-dynamic-component": "10.1.0",
"openvidu-browser": "2.21.0-beta1",
"openvidu-browser": "^2.21.0",
"rxjs": "7.4.0",
"tslib": "2.3.1",
"zone.js": "0.11.4"
},
"devDependencies": {
"@angular-devkit/build-angular": "13.0.1",
"@angular-devkit/build-angular": "13.2.5",
"@angular/cli": "13.0.1",
"@angular/compiler": "13.0.0",
"@angular/compiler-cli": "13.0.0",

View File

@ -7,10 +7,3 @@
# You can see what browsers were selected by your queries by running:
# npx browserslist
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major versions
last 2 iOS major versions
Firefox ESR

View File

@ -10,8 +10,7 @@
"@angular/flex-layout": "^13.0.0-beta.36",
"autolinker": "^3.14.3",
"buffer": "^6.0.3",
"openvidu-browser": "^2.20.0",
"ng-dynamic-component": "^10.1.0"
"openvidu-browser": "^2.21.0"
},
"dependencies": {
"tslib": "^2.3.0"

View File

@ -0,0 +1,64 @@
@keyframes normal {
0%{
height: 20%;
}
50%{
height: 40%;
}
100%{
height: 20%;
}
}
@keyframes loud {
0%{
height: 30%;
}
50%{
height: 80%;
}
100%{
height: 30%;
}
}
.audio-container{
background-color: var(--ov-tertiary-color);
padding: 5px;
max-width: 15px;
max-height: 15px;
height: 15px;
width: 15px;
border-radius: var(--ov-buttons-radius);
display: flex;
justify-content: space-between;
}
.stick{
margin: auto;
height: 80%;
width: 3px;
background: var(--ov-light-color);
border-radius: 8px;
}
.pause {
animation-play-state:paused;
height: 1px;
}
.play {
animation-duration: 1.2s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-play-state:running;
}
.normal{
animation-name: normal;
}
.loud{
animation-name: loud;
}

View File

@ -0,0 +1,5 @@
<div class="audio-container">
<div class="stick normal" [ngClass]="isSpeaking ? 'play' : 'pause'"></div>
<div class="stick loud" [ngClass]="isSpeaking ? 'play' : 'pause'"></div>
<div class="stick normal" [ngClass]="isSpeaking ? 'play' : 'pause'"></div>
</div>

View File

@ -0,0 +1,49 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { PublisherSpeakingEvent, StreamManager } from 'openvidu-browser';
@Component({
selector: 'ov-audio-wave',
templateUrl: './audio-wave.component.html',
styleUrls: ['./audio-wave.component.css']
})
export class AudioWaveComponent implements OnInit, OnDestroy {
isSpeaking: boolean = false;
audioVolume: number = 0;
private _streamManager: StreamManager;
@Input()
set streamManager(streamManager: StreamManager) {
this._streamManager = streamManager;
if(this._streamManager) {
this._streamManager.on('publisherStartSpeaking', (event: PublisherSpeakingEvent) => {
this.isSpeaking = true;
});
this._streamManager.on('publisherStopSpeaking', (event: PublisherSpeakingEvent) => {
this.isSpeaking = false;
});
// streamManager.on('streamAudioVolumeChange', (event: any) => {
// // The loudest sounds on your system will be at 0dB
// // and silence in webaudio is -100dB.
// this.audioVolume = 100 + event.value.newValue;
// console.log('Publisher audio volume change from ' + event.value.oldValue + ' to' + event.value.newValue);
// console.log('AUDIO VOLUME', this.audioVolume);
// });
}
}
constructor() {}
ngOnDestroy(): void {
if(this._streamManager){
this._streamManager.off('publisherStartSpeaking');
this._streamManager.off('publisherStopSpeaking');
}
}
ngOnInit(): void {}
}

View File

@ -0,0 +1,22 @@
.poster {
position: absolute;
display: inline-grid;
z-index: 1;
margin: auto;
bottom: 0;
right: 0;
left: 0;
top: 0;
height: 70px;
width: 70px;
border-radius: var(--ov-video-radius);
border: 2px solid var(--ov-light-color);
color: #000000;
}
#poster-text {
padding: 20px;
font-weight: bold;
font-size: 40px;
margin: auto;
}

View File

@ -1,20 +1,20 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ParticipantItemComponent } from './participant-item.component';
import { AvatarProfileComponent } from './avatar-profile.component';
describe('ParticipantItemComponent', () => {
let component: ParticipantItemComponent;
let fixture: ComponentFixture<ParticipantItemComponent>;
describe('AvatarProfileComponent', () => {
let component: AvatarProfileComponent;
let fixture: ComponentFixture<AvatarProfileComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ ParticipantItemComponent ]
declarations: [ AvatarProfileComponent ]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ParticipantItemComponent);
fixture = TestBed.createComponent(AvatarProfileComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -0,0 +1,22 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'ov-avatar-profile',
template: `
<div class="poster" [ngStyle]="{ 'background-color': color }">
<span id="poster-text">{{ letter }}</span>
</div>
`,
styleUrls: ['./avatar-profile.component.css']
})
export class AvatarProfileComponent {
letter: string;
@Input()
set name(nickname: string) {
this.letter = nickname[0];
}
@Input() color;
constructor() {}
}

View File

@ -3,14 +3,17 @@
}
.bounds {
position: absolute;
.layout {
position: relative;
left: 0;
right: 0;
top: 0;
bottom: 0;
min-width: 350px !important;
min-height: 100%;
width: inherit;
height: -webkit-fill-available;
height: -moz-available;
}
/*!
@ -19,9 +22,6 @@
* http://opensource.org/licenses/MIT
*/
.custom-class {
min-height: 0px !important;
}
/**
* OT Base styles
@ -35,9 +35,7 @@
padding: 0;
border: 0;
font-size: 100%;
font-family: 'Ubuntu', sans-serif;
vertical-align: baseline;
transition: all .1s linear;
}
.OT_dialog-centering {
@ -217,8 +215,11 @@
.OT_publisher,
.OT_subscriber {
position: relative;
min-width: 48px;
min-height: 48px;
min-width: 0px;
min-height: 0px;
margin: 3px;
transition-duration: 0.1s;
transition-timing-function: ease-in-out;
}
.OT_publisher .OT_video-element,
@ -227,20 +228,10 @@
position: absolute;
width: 100%;
height: 100%;
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}
/* Styles that are applied when the video element should be mirrored */
.OT_publisher.OT_mirrored .OT_video-element {
-webkit-transform: scale(-1, 1);
transform: scale(-1, 1);
-webkit-transform-origin: 50% 50%;
transform-origin: 50% 50%;
}
.OT_subscriber_error {
background-color: #000;
color: #fff;

View File

@ -1,22 +1,18 @@
<div id="layout" class="bounds">
<div id="layout" class="layout">
<div
class="OT_root OT_publisher"
id="localUser"
*ngFor="let connection of localParticipant | connections"
[ngClass]="{ OV_small: !connection.streamManager?.stream?.videoActive }"
*ngFor="let stream of localParticipant | streams"
[ngClass]="{ OV_big: stream.videoEnlarged }"
>
<ng-template #localStream [ngComponentOutlet]="_localStreamComponent" [ndcDynamicInputs]="{ participant: connection }">
</ng-template>
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: stream }"></ng-container>
</div>
<div
*ngFor="let connection of remoteParticipants | connections"
*ngFor="let stream of remoteParticipants | streams"
class="OT_root OT_publisher"
id="remote-participant"
[ngClass]="{ OV_small: !connection.streamManager?.stream?.videoActive }"
[ngClass]="{ OV_big: stream.videoEnlarged }"
>
<!-- Dynamic ov-stream component injected -->
<ng-template #remoteStream [ngComponentOutlet]="_remoteStreamComponent" [ndcDynamicInputs]="{ participant: connection }">
</ng-template>
<ng-container *ngTemplateOutlet="streamTemplate; context: { $implicit: stream }"></ng-container>
</div>
</div>

View File

@ -1,30 +1,48 @@
import { AfterViewInit, Component, OnDestroy, OnInit, Type, ViewChild, ViewContainerRef } from '@angular/core';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
OnDestroy,
OnInit,
TemplateRef
} from '@angular/core';
import { Subscription } from 'rxjs';
import { ParticipantService } from '../../services/participant/participant.service';
import { ParticipantAbstractModel } from '../../models/participant.model';
import { LayoutService } from '../../services/layout/layout.service';
import { LibraryComponents } from '../../config/lib.config';
import { LibraryConfigService } from '../../services/library-config/library-config.service';
import { StreamDirective } from '../../directives/template/openvidu-angular.directive';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
@Component({
selector: 'ov-layout',
templateUrl: './layout.component.html',
styleUrls: ['./layout.component.css']
styleUrls: ['./layout.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
_localStreamComponent: Type<any>;
_remoteStreamComponent: Type<any>;
@ContentChild('stream', { read: TemplateRef }) streamTemplate: TemplateRef<any>;
@ContentChild(StreamDirective)
set externalStream(externalStream: StreamDirective) {
// This directive will has value only when STREAM component tagget with '*ovStream' directive
// is inside of the layout component tagged with '*ovLayout' directive
if (externalStream) {
this.streamTemplate = externalStream.template;
}
}
localParticipant: ParticipantAbstractModel;
remoteParticipants: ParticipantAbstractModel[] = [];
protected localParticipantSubs: Subscription;
protected remoteParticipantsSubs: Subscription;
protected updateLayoutInterval: NodeJS.Timer;
constructor(
protected libraryConfigSrv: LibraryConfigService,
protected layoutService: LayoutService,
protected participantService: ParticipantService
protected participantService: ParticipantService,
private libService: OpenViduAngularConfigService,
private cd: ChangeDetectorRef
) {}
@ViewChild('localStream', { static: false, read: ViewContainerRef })
@ -56,24 +74,35 @@ export class LayoutComponent implements OnInit, OnDestroy, AfterViewInit {
this.subscribeToParticipants();
}
ngAfterViewInit() {}
ngAfterViewInit() {
let timeout: number = 0;
// if (this.libService.isWebcomponent()) {
// timeout = 0;
// }
this.layoutService.initialize(timeout);
this.layoutService.update(timeout);
}
ngOnDestroy() {
this.localParticipant = null;
this.remoteParticipants = [];
if (this.localParticipantSubs) this.localParticipantSubs.unsubscribe();
if (this.remoteParticipantsSubs) this.remoteParticipantsSubs.unsubscribe();
this.layoutService.clear();
}
protected subscribeToParticipants() {
this.localParticipantSubs = this.participantService.localParticipantObs.subscribe((p) => {
this.localParticipant = p;
this.layoutService.update();
this.cd.markForCheck();
});
this.remoteParticipantsSubs = this.participantService.remoteParticipantsObs.subscribe((participants) => {
this.remoteParticipants = [...participants];
this.remoteParticipants = participants;
this.layoutService.update();
this.cd.markForCheck();
});
}
}

View File

@ -1,9 +1,9 @@
#chat-container {
margin: 20px;
background-color: var(--ov-light-color);
border-radius: 5px;
height: -webkit-fill-available;
height: -moz-available;
border-radius: var(--ov-panel-radius);
max-height: calc(100% - 40px);
min-height: calc(100% - 40px);
}
.header-container {
@ -19,6 +19,7 @@
.header-container button {
margin-left: auto;
border-radius: var(--ov-buttons-radius);
}
.text-container{
@ -43,7 +44,7 @@
background-color: var(--ov-light-dark-color);
padding: 10px;
margin: 10px;
border-radius: 5px;
border-radius: var(--ov-panel-radius);
}
.input-container textarea {
@ -89,14 +90,17 @@
.msg-content {
position: relative;
border-radius: 5px;
border-radius: var(--ov-panel-radius);
padding: 8px;
color: #000000;
width: auto;
max-width: 95%;
font-size: 13px;
word-break: break-all;
}
#send-btn {
border-radius: var(--ov-buttons-radius);
}

View File

@ -6,7 +6,7 @@
</button>
</div>
<div class="text-container" fxFlex="30px">
<div class="text-container" fxFlex="20px">
<p class="text-info">Messages will be removed at the end of the session</p>
</div>

View File

@ -1,4 +1,4 @@
import { AfterViewInit, Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, HostListener, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { ChatMessage } from '../../../models/chat.model';
import { MenuType } from '../../../models/menu.model';
@ -8,7 +8,8 @@ import { SidenavMenuService } from '../../../services/sidenav-menu/sidenav-menu.
@Component({
selector: 'ov-chat-panel',
templateUrl: './chat-panel.component.html',
styleUrls: ['./chat-panel.component.css']
styleUrls: ['./chat-panel.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatPanelComponent implements OnInit, AfterViewInit {
@ViewChild('chatScroll') chatScroll: ElementRef;
@ -19,7 +20,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
private chatMessageSubscription: Subscription;
constructor(private chatService: ChatService, private menuService: SidenavMenuService) {}
constructor(private chatService: ChatService, private menuService: SidenavMenuService, private cd: ChangeDetectorRef) {}
@HostListener('document:keydown.escape', ['$event'])
onKeydownHandler(event: KeyboardEvent) {
@ -52,8 +53,10 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
}
sendMessage(): void {
this.chatService.sendMessage(this.message);
this.message = '';
if(!!this.message) {
this.chatService.sendMessage(this.message);
this.message = '';
}
}
scrollToBottom(): void {
@ -73,6 +76,7 @@ export class ChatPanelComponent implements OnInit, AfterViewInit {
this.messageList = messages;
if (this.menuService.isMenuOpened()) {
this.scrollToBottom();
this.cd.markForCheck();
}
});
}

View File

@ -1,5 +1,12 @@
<!-- CHAT panel -->
<ng-container *ngIf="isChatPanelOpened">
<ng-container *ngTemplateOutlet="chatPanelTemplate"></ng-container>
</ng-container>
<ng-container *ngIf="isChatPanelOpened" #chat></ng-container>
<!-- PARTICIPANTS panel -->
<ng-container *ngIf="isParticipantsPanelOpened">
<ng-container *ngTemplateOutlet="participantsPanelTemplate"></ng-container>
</ng-container>
<ng-container *ngIf="isParticipantsPanelOpened" #participants></ng-container>

View File

@ -1,43 +1,41 @@
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, OnInit, TemplateRef } from '@angular/core';
import { skip, Subscription } from 'rxjs';
import { LibraryComponents } from '../../config/lib.config';
import { ChatPanelDirective, ParticipantsPanelDirective } from '../../directives/template/openvidu-angular.directive';
import { MenuType } from '../../models/menu.model';
import { LibraryConfigService } from '../../services/library-config/library-config.service';
import { SidenavMenuService } from '../../services/sidenav-menu/sidenav-menu.service';
@Component({
selector: 'ov-panel',
templateUrl: './panel.component.html',
styleUrls: ['./panel.component.css']
styleUrls: ['./panel.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PanelComponent implements OnInit, OnDestroy {
export class PanelComponent implements OnInit {
@ContentChild('participantsPanel', { read: TemplateRef }) participantsPanelTemplate: TemplateRef<any>;
@ContentChild('chatPanel', { read: TemplateRef }) chatPanelTemplate: TemplateRef<any>;
@ContentChild(ParticipantsPanelDirective)
set externalParticipantPanel(externalParticipantsPanel: ParticipantsPanelDirective) {
// This directive will has value only when PARTICIPANTS PANEL component tagged with '*ovParticipantsPanel'
// is inside of the PANEL component tagged with '*ovPanel'
if (externalParticipantsPanel) {
this.participantsPanelTemplate = externalParticipantsPanel.template;
}
}
@ContentChild(ChatPanelDirective)
set externalChatPanel(externalChatPanel: ChatPanelDirective) {
// This directive will has value only when CHAT PANEL component tagged with '*ovChatPanel'
// is inside of the PANEL component tagged with '*ovPanel'
if (externalChatPanel) {
this.chatPanelTemplate = externalChatPanel.template;
}
}
isParticipantsPanelOpened: boolean;
isChatPanelOpened: boolean;
menuSubscription: Subscription;
@ViewChild('chat', { static: false, read: ViewContainerRef })
set chat(reference: ViewContainerRef) {
setTimeout(() => {
if (reference) {
const component = this.libraryConfigSrv.getDynamicComponent(LibraryComponents.CHAT_PANEL);
reference.clear();
reference.createComponent(component);
}
}, 0);
}
@ViewChild('participants', { static: false, read: ViewContainerRef })
set participants(reference: ViewContainerRef) {
setTimeout(() => {
if (reference) {
const component = this.libraryConfigSrv.getDynamicComponent(LibraryComponents.PARTICIPANTS_PANEL);
reference.clear();
reference.createComponent(component);
}
}, 0);
}
constructor(protected libraryConfigSrv: LibraryConfigService, protected menuService: SidenavMenuService) {}
constructor(protected menuService: SidenavMenuService, private cd: ChangeDetectorRef) {}
ngOnInit(): void {
this.subscribeToPanelToggling();
@ -46,6 +44,7 @@ export class PanelComponent implements OnInit, OnDestroy {
this.menuSubscription = this.menuService.menuOpenedObs.pipe(skip(1)).subscribe((ev: { opened: boolean; type?: MenuType }) => {
this.isChatPanelOpened = ev.opened && ev.type === MenuType.CHAT;
this.isParticipantsPanelOpened = ev.opened && ev.type === MenuType.PARTICIPANTS;
this.cd.markForCheck();
});
}

View File

@ -1,25 +0,0 @@
.participant-subtitle {
font-style: italic;
font-size: 11px !important;
}
.participant-nickname {
font-weight: bold !important;
}
.participant-action-buttons{
display: flex;
padding-top: 0px;
}
mat-list-item {
height: max-content !important;
padding-bottom: 10px !important;
}
mat-list {
padding: 3px;
}
.participant-avatar {
display: contents;
}

View File

@ -1,11 +0,0 @@
<mat-list>
<mat-list-item>
<mat-icon matListAvatar class="participant-avatar">person</mat-icon>
<h3 matLine class="participant-nickname">{{ name }}</h3>
<p matLine class="participant-subtitle">{{ connections }}</p>
<!-- <p matLine>
<span class="participant-subtitle"></span>
</p> -->
</mat-list-item>
<mat-divider *ngIf="showDividerLine"></mat-divider>
</mat-list>

View File

@ -1,16 +0,0 @@
import { Component, Input, OnInit } from '@angular/core';
@Component({
selector: 'ov-participant-item',
templateUrl: './participant-item.component.html',
styleUrls: ['./participant-item.component.css']
})
export class ParticipantItemComponent implements OnInit {
@Input() name: string;
@Input() connections: string;
@Input() showDividerLine: boolean;
constructor() {}
ngOnInit(): void {}
}

View File

@ -0,0 +1,28 @@
<mat-list>
<mat-list-item>
<mat-icon matListAvatar class="participant-avatar">person</mat-icon>
<h3 matLine class="participant-nickname">{{ _participant.nickname }}</h3>
<p matLine class="participant-subtitle">{{ _participant | streamTypesEnabled }}</p>
<!-- <p matLine>
<span class="participant-subtitle"></span>
</p> -->
<div class="participant-action-buttons">
<button
mat-icon-button
id="mute-btn"
*ngIf="!_participant.local && showMuteButton"
[class.warn-btn]="_participant.isMutedForcibly"
(click)="toggleMuteForcibly()"
>
<mat-icon *ngIf="!_participant.isMutedForcibly">volume_up</mat-icon>
<mat-icon *ngIf="_participant.isMutedForcibly">volume_off</mat-icon>
</button>
<!-- External item elements -->
<ng-container *ngIf="participantPanelItemElementsTemplate">
<ng-container *ngTemplateOutlet="participantPanelItemElementsTemplate"></ng-container>
</ng-container>
</div>
</mat-list-item>
</mat-list>

View File

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

View File

@ -0,0 +1,52 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Subscription } from 'rxjs';
import { ParticipantPanelItemElementsDirective } from '../../../../directives/template/openvidu-angular.directive';
import { ParticipantAbstractModel } from '../../../../models/participant.model';
import { OpenViduAngularConfigService } from '../../../../services/config/openvidu-angular.config.service';
@Component({
selector: 'ov-participant-panel-item',
templateUrl: './participant-panel-item.component.html',
styleUrls: ['./participant-panel-item.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParticipantPanelItemComponent implements OnInit, OnDestroy {
@ContentChild('participantPanelItemElements', { read: TemplateRef }) participantPanelItemElementsTemplate: TemplateRef<any>;
showMuteButton: boolean = true;
private muteButtonSub: Subscription;
@ContentChild(ParticipantPanelItemElementsDirective)
set externalItemElements(externalItemElements: ParticipantPanelItemElementsDirective) {
// This directive will has value only when ITEM ELEMENTS component tagget with '*ovParticipantPanelItemElements' directive
// is inside of the P PANEL ITEM component tagged with '*ovParticipantPanelItem' directive
if (externalItemElements) {
this.participantPanelItemElementsTemplate = externalItemElements.template;
}
}
@Input()
set participant(p: ParticipantAbstractModel) {
this._participant = p;
}
_participant: ParticipantAbstractModel;
constructor(private libService: OpenViduAngularConfigService, private cd: ChangeDetectorRef) {}
ngOnInit(): void {
this.subscribeToParticipantPanelItemDirectives();
}
ngOnDestroy(): void {
if (this.muteButtonSub) this.muteButtonSub.unsubscribe();
}
toggleMuteForcibly() {
this._participant.setMutedForcibly(!this._participant.isMutedForcibly);
}
private subscribeToParticipantPanelItemDirectives() {
this.muteButtonSub = this.libService.participantItemMuteButton.subscribe((value: boolean) => {
this.showMuteButton = value;
this.cd.markForCheck();
});
}
}

View File

@ -1,9 +1,9 @@
.participants-container {
margin: 20px;
background-color: var(--ov-light-color);
border-radius: 5px;
height: -webkit-fill-available;
height: -moz-available;
border-radius: var(--ov-panel-radius);
max-height: calc(100% - 40px);
min-height: calc(100% - 40px);
}
.header-container {
@ -19,6 +19,7 @@
.header-container button {
margin-left: auto;
border-radius: var(--ov-buttons-radius);
}
.local-participant-container, .remote-participants-container {
@ -26,6 +27,8 @@
}
.scrollable {
height: calc(100% - 60px);
max-height: calc(100% - 60px);
overflow: auto;
}

View File

@ -1,4 +1,4 @@
<div class="participants-container">
<div class="participants-container" id="participants-container">
<div class="header-container">
<h3>Participants</h3>
<button mat-icon-button matTooltip="Close" (click)="close()">
@ -8,26 +8,16 @@
<div class="scrollable">
<div class="local-participant-container">
<ov-participant-item
[name]="localParticipant | nickname"
[connections]="localParticipant | connectionsEnabled"
[showDividerLine]="true"
></ov-participant-item>
<div class="local-participant-container" *ngIf="localParticipant">
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: localParticipant }"></ng-container>
<mat-divider *ngIf="true"></mat-divider>
</div>
<!-- <div class="message-container">
<p *ngIf="remoteParticipants.length === 0" class="text-info">No remote participants connected</p>
<p *ngIf="remoteParticipants.length > 0" class="text-info">{{ remoteParticipants.length }} remote participants connected</p>
</div> -->
<div class="remote-participants-container" id="remote-participants-container" *ngIf="remoteParticipants.length > 0">
<div class="remote-participants-container" *ngIf="remoteParticipants.length > 0">
<ov-participant-item
*ngFor="let p of remoteParticipants; let lastItem = last"
[name]="p | nickname"
[connections]="p | connectionsEnabled"
[showDividerLine]="!lastItem"
></ov-participant-item>
<div *ngFor="let participant of this.remoteParticipants" id="remote-participant-item">
<ng-container *ngTemplateOutlet="participantPanelItemTemplate; context: { $implicit: participant }"></ng-container>
</div>
</div>
</div>

View File

@ -1,33 +1,67 @@
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
import { ParticipantAbstractModel, ParticipantModel } from '../../../../models/participant.model';
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ContentChild, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { ParticipantAbstractModel } from '../../../../models/participant.model';
import { ParticipantService } from '../../../../services/participant/participant.service';
import { SidenavMenuService } from '../../../../services/sidenav-menu/sidenav-menu.service';
import { SidenavMenuService } from '../../../..//services/sidenav-menu/sidenav-menu.service';
import { ParticipantPanelItemDirective } from '../../../../directives/template/openvidu-angular.directive';
import { Subscription } from 'rxjs';
@Component({
selector: 'ov-participants-panel',
templateUrl: './participants-panel.component.html',
styleUrls: ['./participants-panel.component.css']
styleUrls: ['./participants-panel.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ParticipantsPanelComponent implements OnInit {
export class ParticipantsPanelComponent implements OnInit, OnDestroy {
localParticipant: any;
remoteParticipants: ParticipantAbstractModel[] = [];
constructor(protected participantService: ParticipantService, protected menuService: SidenavMenuService, private cd: ChangeDetectorRef) {}
private localParticipantSubs: Subscription;
private remoteParticipantsSubs: Subscription;
@ContentChild('participantPanelItem', { read: TemplateRef }) participantPanelItemTemplate: TemplateRef<any>;
@ContentChild(ParticipantPanelItemDirective)
set externalParticipantPanelItem(externalParticipantPanelItem: ParticipantPanelItemDirective) {
// This directive will has value only when PARTICIPANT PANEL ITEM component tagged with '*ovParticipantPanelItem'
// is inside of the PARTICIPANTS PANEL component tagged with '*ovParticipantsPanel'
if (externalParticipantPanelItem) {
this.participantPanelItemTemplate = externalParticipantPanelItem.template;
}
}
constructor(
protected participantService: ParticipantService,
protected menuService: SidenavMenuService,
private cd: ChangeDetectorRef
) {
}
ngOnInit(): void {
this.participantService.localParticipantObs.subscribe((p: ParticipantModel) => {
this.localParticipantSubs = this.participantService.localParticipantObs.subscribe((p: ParticipantAbstractModel) => {
this.localParticipant = p;
// Mark for re-rendering using an impure pipe 'connectionsEnabled'
// Mark for re-rendering using an impure pipe 'streamsTypesEnabled'
this.cd.markForCheck();
});
this.participantService.remoteParticipantsObs.subscribe((p: ParticipantModel[]) => {
this.remoteParticipants = p;
// Mark for re-rendering using an impure pipe 'connectionsEnabled'
this.remoteParticipantsSubs = this.participantService.remoteParticipantsObs.subscribe((p: ParticipantAbstractModel[]) => {
// Workaround which forc the objects references update
// After one entirely day trying to make it works, this is the only way
const remoteParticipantsAux = [];
p.forEach((par) => {
remoteParticipantsAux.push(Object.create(par));
});
this.remoteParticipants = remoteParticipantsAux;
// Mark for re-rendering using an impure pipe 'streamsTypesEnabled'
this.cd.markForCheck();
});
}
ngOnDestroy() {
if (this.localParticipantSubs) this.localParticipantSubs.unsubscribe();
if (this.remoteParticipantsSubs) this.remoteParticipantsSubs.unsubscribe;
}
close() {
this.menuService.closeMenu();
}

View File

@ -0,0 +1,159 @@
.container {
height: 100%;
padding: 80px;
background-color: var(--ov-light-dark-color);
}
#layout-container {
display: block !important;
}
h4 {
margin-bottom: 1px;
font-weight: bold;
}
hr {
margin: 0;
}
ov-layout {
height: -webkit-fill-available;
height: -moz-available;
width: -webkit-fill-available;
width: -moz-available;
}
.media-panel {
background-color: var(--ov-light-dark-color);
}
.media-panel-container {
width: 100%;
padding: 20px;
padding-right: 0px;
}
.nickname-container {
/* padding: 10px; */
display: block !important;
margin-bottom: 0px !important;
}
#nickname-input-container, .device-container-element {
display: flex;
}
#nickname-input-container button, .device-container-element button {
margin: auto 10px auto auto;
}
#nickname-input-container button.mat-button-disabled {
color: #000000 !important;
}
#nickname-input-container mat-form-field, .device-container-element mat-form-field {
width: 100%;
margin-top: 10px;
color: var(--ov-primary-color);
}
#nickname-input-container mat-form-field {
color: var(--ov-primary-color);
}
.mat-form-field-appearance-fill .mat-form-field-flex {
background-color: var(--ov-light-color);
border-radius: var(--ov-video-radius);
}
.buttons-container{
border-radius: 5px;
padding: 10px 0px;
height: 100px;
display: block !important;
}
#camera-button {
border-radius: var(--ov-buttons-radius);
/* background-color: var(--ov-secondary-color) !important; */
/* color: var(--ov-light-color) !important; */
}
.join-btn-container {
width: inherit;
text-align: center;
}
#join-button {
width: 100%;
font-weight: bold;
color: var(--ov-light-color);
background-color: var(--ov-tertiary-color);
border-radius: var(--ov-video-radius);
}
.warn-btn {
color: var(--ov-light-color);
background-color: var(--ov-warn-color) !important;
}
.active-btn {
color: var(--ov-light-color);
background-color: var(--ov-tertiary-color) !important;
}
.media-btn {
margin: auto;
}
::ng-deep .mat-button-toggle-appearance-standard .mat-button-toggle-label-content {
padding: 1px !important;
}
::ng-deep .mat-input-element {
caret-color: #000000;
}
::ng-deep .mat-primary .mat-option.mat-selected:not(.mat-option-disabled) {
color: #000000;
}
@media only screen and (max-width: 480px) {
.container, .media-panel-container, .buttons-container {
padding: 0;
}
.nickname-container, .buttons-container, .join-btn-container {
width: 90% !important;
margin: auto;
}
.join-btn-container {
padding: 0px 10px;
}
.media-panel {
align-items: flex-start !important;
}
}
@media only screen and (min-width: 480px) and (max-width: 960px) {
.container, .media-panel-container, .buttons-container {
padding: 0;
}
.nickname-container, .buttons-container, .join-btn-container{
width: 80% !important;
min-width: 80% !important;
margin: auto;
}
.buttons-container, .media-panel-container {
padding-top: 0px;
max-width: 600px;
}
.media-panel {
align-items: flex-start !important;
}
}

View File

@ -0,0 +1,94 @@
<div class="container" id="prejoin-container" fxLayout.gt-sm="row" fxLayout.lt-md="column">
<div fxFlex.gt-sm="65%" fxFlex.lt-md="55%" fxLayoutAlign="center center" id="layout-container">
<ov-layout>
<ng-template #stream let-stream>
<ov-stream [stream]="stream" [displayParticipantName]="false" [settingsButton]="false"></ov-stream>
</ng-template>
</ov-layout>
</div>
<div fxFlex.gt-sm="35%" fxFlex.lt-md="45%" fxLayoutAlign="center center" class="media-panel" *ngIf="localParticipant">
<div fxLayout="column" fxLayoutGap="10px" class="media-panel-container">
<div fxLayout.gt-sm="column" fxLayout.lt-md="column" fxLayoutGap="10px" fxFlex="33%">
<div fxFlex.gt-sm="100%" fxFlex.lt-md="33%" fxLayoutAlign="center center" fxFlexFill class="nickname-container">
<h4 *ngIf="windowSize >= 960">Set your name</h4>
<hr *ngIf="windowSize >= 960"/>
<div id="nickname-input-container">
<button mat-icon-button disabled>
<mat-icon>person</mat-icon>
</button>
<mat-form-field appearance="standard">
<mat-label>Nickname</mat-label>
<input matInput type="text" maxlength="20" [(ngModel)]="nickname" (change)="updateNickname()" autocomplete="off" />
</mat-form-field>
</div>
<!-- <mat-button-toggle-group style="border-radius: 20px">
<button mat-icon-button class="media-btn">
<mat-icon matTooltip="Mute your audio">mic</mat-icon>
</button>
<mat-button-toggle class="split-button-1 drop-down-button" [matMenuTriggerFor]="dropdownMenuOne">
<mat-icon>arrow_drop_down</mat-icon>
</mat-button-toggle>
</mat-button-toggle-group>
<mat-menu #dropdownMenuOne="matMenu">
<button mat-menu-item>One</button>
<button mat-menu-item>Two</button>
<button mat-menu-item>Three</button>
</mat-menu> -->
</div>
<div fxFlex.gt-sm="100%" fxFlex.lt-md="33%" fxLayoutAlign="center center" fxFlexFill class="buttons-container">
<h4 *ngIf="windowSize >= 960">Choose your devices</h4>
<hr *ngIf="windowSize >= 960"/>
<!-- Camera -->
<div class="device-container-element">
<button
mat-icon-button
id="camera-button"
[disabled]="!hasVideoDevices"
[class.warn-btn]="isVideoMuted"
(click)="toggleCam()"
>
<mat-icon *ngIf="!isVideoMuted" matTooltip="Mute Camera" id="videocam">videocam</mat-icon>
<mat-icon *ngIf="isVideoMuted" matTooltip="Unmute Camera" id="videocam_off">videocam_off</mat-icon>
</button>
<mat-form-field>
<mat-label>Video devices</mat-label>
<mat-select [disabled]="isVideoMuted || !hasVideoDevices" [value]="cameraSelected?.device" (selectionChange)="onCameraSelected($event)">
<mat-option *ngFor="let camera of cameras" [value]="camera.device">
{{ camera.label }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- Microphone -->
<div class="device-container-element">
<button
mat-icon-button
id="microhpone-button"
[disabled]="!hasAudioDevices"
[class.warn-btn]="isAudioMuted"
(click)="toggleMic()"
>
<mat-icon *ngIf="!isAudioMuted" matTooltip="Mute Audio" id="mic">mic</mat-icon>
<mat-icon *ngIf="isAudioMuted" matTooltip="Unmute Audio" id="mic_off">mic_off</mat-icon>
</button>
<mat-form-field>
<mat-label>Audio devices</mat-label>
<mat-select [disabled]="isAudioMuted || !hasAudioDevices" [value]="microphoneSelected?.device" (selectionChange)="onMicrophoneSelected($event)">
<mat-option *ngFor="let microphone of microphones" [value]="microphone.device">
{{ microphone.label }}
</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>
<div fxFlex.gt-sm="60%" fxLayout.lt-md="column" fxLayoutAlign="center center" fxFlexFill class="join-btn-container">
<button mat-flat-button (click)="joinSession()" form="nicknameForm" id="join-button">Join session</button>
</div>
</div>
</div>
</div>
</div>

View File

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

View File

@ -0,0 +1,235 @@
import { Component, HostListener, OnDestroy, OnInit, Output, EventEmitter } from '@angular/core';
import { Publisher, PublisherProperties } from 'openvidu-browser';
import { OpenViduErrorName } from 'openvidu-browser/lib/OpenViduInternal/Enums/OpenViduError';
import { Subscription } from 'rxjs';
import { CustomDevice } from '../../models/device.model';
import { ILogger } from '../../models/logger.model';
import { ParticipantAbstractModel, ParticipantProperties } from '../../models/participant.model';
import { ActionService } from '../../services/action/action.service';
import { DeviceService } from '../../services/device/device.service';
import { LayoutService } from '../../services/layout/layout.service';
import { LoggerService } from '../../services/logger/logger.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { ParticipantService } from '../../services/participant/participant.service';
import { StorageService } from '../../services/storage/storage.service';
@Component({
selector: 'ov-pre-join',
templateUrl: './pre-join.component.html',
styleUrls: ['./pre-join.component.css']
})
export class PreJoinComponent implements OnInit, OnDestroy {
@Output() onJoinButtonClicked = new EventEmitter<any>();
cameras: CustomDevice[];
microphones: CustomDevice[];
cameraSelected: CustomDevice;
microphoneSelected: CustomDevice;
isVideoMuted: boolean;
isAudioMuted: boolean;
localParticipant: ParticipantAbstractModel;
windowSize: number;
hasVideoDevices: boolean;
hasAudioDevices: boolean;
isLoading = true;
nickname: string;
private log: ILogger;
private localParticipantSubscription: Subscription;
private screenShareStateSubscription: Subscription;
@HostListener('window:resize')
sizeChange() {
this.windowSize = window.innerWidth;
this.layoutService.update();
}
constructor(
private layoutService: LayoutService,
private deviceSrv: DeviceService,
private loggerSrv: LoggerService,
private openviduService: OpenViduService,
private participantService: ParticipantService,
private storageSrv: StorageService
) {
this.log = this.loggerSrv.get('PreJoinComponent');
}
ngOnInit() {
this.subscribeToLocalParticipantEvents();
this.windowSize = window.innerWidth;
this.hasVideoDevices = this.deviceSrv.hasVideoDeviceAvailable();
this.hasAudioDevices = this.deviceSrv.hasAudioDeviceAvailable();
this.microphones = this.deviceSrv.getMicrophones();
this.cameras = this.deviceSrv.getCameras();
this.cameraSelected = this.deviceSrv.getCameraSelected();
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
this.isVideoMuted = this.deviceSrv.isVideoMuted();
this.isAudioMuted = this.deviceSrv.isAudioMuted();
this.isLoading = false;
}
ngOnDestroy() {
if (this.localParticipantSubscription) {
this.localParticipantSubscription.unsubscribe();
}
if (this.screenShareStateSubscription) {
this.screenShareStateSubscription.unsubscribe();
}
this.deviceSrv.clear();
}
async onCameraSelected(event: any) {
const videoSource = event?.value;
// Is New deviceId different from the old one?
if (this.deviceSrv.needUpdateVideoTrack(videoSource)) {
const mirror = this.deviceSrv.cameraNeedsMirror(videoSource);
//TODO: Uncomment this when replaceTrack issue is fixed
// const pp: PublisherProperties = { videoSource, audioSource: false, mirror };
// await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
// TODO: Remove this when replaceTrack issue is fixed
const pp: PublisherProperties = { videoSource, audioSource: this.microphoneSelected.device, mirror };
await this.openviduService.republishTrack(pp);
this.cameraSelected = videoSource;
this.deviceSrv.setCameraSelected(this.cameraSelected);
}
if (this.isVideoMuted) {
// Publish Webcam video
this.openviduService.publishVideo(this.participantService.getMyCameraPublisher(), true);
this.isVideoMuted = false;
}
}
async onMicrophoneSelected(event: any) {
const audioSource = event?.value;
// Is New deviceId different than older?
if (this.deviceSrv.needUpdateAudioTrack(audioSource)) {
//TODO: Uncomment this when replaceTrack issue is fixed
// const pp: PublisherProperties = { audioSource, videoSource: false };
// await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
// TODO: Remove this when replaceTrack issue is fixed
const mirror = this.deviceSrv.cameraNeedsMirror(this.cameraSelected.device);
const pp: PublisherProperties = { videoSource: this.cameraSelected.device, audioSource, mirror };
await this.openviduService.republishTrack(pp);
this.microphoneSelected = audioSource;
this.deviceSrv.setMicSelected(this.microphoneSelected);
}
if (this.isAudioMuted) {
// Enable microphone
this.openviduService.publishAudio(this.participantService.getMyCameraPublisher(), true);
this.isAudioMuted = true;
}
}
toggleCam() {
const publish = this.isVideoMuted;
this.openviduService.publishVideo(this.participantService.getMyCameraPublisher(), publish);
if (this.participantService.haveICameraAndScreenActive()) {
// Cam will not published, disable webcam with screensharing active
this.participantService.disableWebcamUser();
this.openviduService.publishAudio(this.participantService.getMyScreenPublisher(), publish);
} else if (this.participantService.isOnlyMyScreenActive()) {
// Cam will be published, enable webcam
this.participantService.enableWebcamUser();
}
this.isVideoMuted = !this.isVideoMuted;
this.storageSrv.setVideoMuted(this.isVideoMuted);
}
// async toggleScreenShare() {
// // Disabling screenShare
// if (this.participantService.haveICameraAndScreenActive()) {
// this.participantService.disableScreenUser();
// return;
// }
// // Enabling screenShare
// if (this.participantService.isOnlyMyCameraActive()) {
// const willThereBeWebcam = this.participantService.isMyCameraActive() && this.participantService.hasCameraVideoActive();
// const hasAudio = willThereBeWebcam ? false : this.hasAudioDevices && this.isAudioMuted;
// const properties: PublisherProperties = {
// videoSource: ScreenType.SCREEN,
// audioSource: this.hasAudioDevices ? undefined : null,
// publishVideo: true,
// publishAudio: hasAudio,
// mirror: false
// };
// const screenPublisher = await this.openviduService.initPublisher(undefined, properties);
// screenPublisher.on('accessAllowed', (event) => {
// screenPublisher.stream
// .getMediaStream()
// .getVideoTracks()[0]
// .addEventListener('ended', () => {
// this.log.d('Clicked native stop button. Stopping screen sharing');
// this.toggleScreenShare();
// });
// this.participantService.activeMyScreenShare(screenPublisher);
// if (!this.participantService.hasCameraVideoActive()) {
// this.participantService.disableWebcamUser();
// }
// });
// screenPublisher.on('accessDenied', (error: any) => {
// if (error && error.name === 'SCREEN_SHARING_NOT_SUPPORTED') {
// this.actionService.openDialog('Error sharing screen', 'Your browser does not support screen sharing');
// }
// });
// return;
// }
// // Disabling screnShare and enabling webcam
// this.participantService.enableWebcamUser();
// this.participantService.disableScreenUser();
// }
toggleMic() {
const publish = this.isAudioMuted;
this.openviduService.publishAudio(this.participantService.getMyCameraPublisher(), publish);
this.isAudioMuted = !this.isAudioMuted;
this.storageSrv.setAudioMuted(this.isAudioMuted);
}
updateNickname() {
this.nickname = this.nickname === '' ? this.participantService.getMyNickname() : this.nickname;
this.participantService.setMyNickname(this.nickname);
this.storageSrv.setNickname(this.nickname);
}
joinSession() {
this.onJoinButtonClicked.emit();
}
private subscribeToLocalParticipantEvents() {
this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p) => {
this.localParticipant = p;
this.nickname = this.localParticipant.getNickname();
});
}
//? After test in Chrome and Firefox, the devices always have labels.
//? It's not longer needed
// private handlePublisherSuccess(publisher: Publisher) {
// publisher.once('accessAllowed', async () => {
// if (this.deviceSrv.areEmptyLabels()) {
// await this.deviceSrv.forceUpdate();
// if (this.hasAudioDevices) {
// const audioLabel = publisher?.stream?.getMediaStream()?.getAudioTracks()[0]?.label;
// this.deviceSrv.setMicSelected(audioLabel);
// }
// if (this.hasVideoDevices) {
// const videoLabel = publisher?.stream?.getMediaStream()?.getVideoTracks()[0]?.label;
// this.deviceSrv.setCameraSelected(videoLabel);
// }
// this.setDevicesInfo();
// }
// });
// }
}

View File

@ -6,8 +6,8 @@
.sidenav-container {
position: relative;
height: calc(100% - 80px);
min-height: calc(100% - 80px);
height: calc(100% - 70px);
min-height: calc(100% - 70px);
padding-top: 10px;
width: 100%;
overflow: hidden;
@ -17,7 +17,7 @@
display: flex;
align-items: center;
justify-content: center;
width: 360px;
width: 380px;
background-color: var(--ov-primary-color);
border-left: none;
position: absolute;
@ -30,6 +30,11 @@
min-height: -moz-available;
}
#layout-container {
height: inherit;
width: inherit;
}
.mat-drawer-container {
background-color: var(--ov-primary-color);
}
@ -38,7 +43,7 @@
background-color: var(--ov-primary-color);
min-width: 400px !important;
width: 100%;
height: 60px;
height: 70px;
}
#footer {

View File

@ -1,6 +1,5 @@
<div id="session-container">
<mat-sidenav-container class="sidenav-container">
<!-- OPENVIDU PANELS -->
<mat-sidenav
#sidenav
mode="{{ sidenavMode }}"
@ -10,15 +9,17 @@
fixedTopGap="0"
fixedBottomGap="0"
>
<ng-template #panel></ng-template>
<ng-container *ngTemplateOutlet="panelTemplate"></ng-container>
</mat-sidenav>
<!-- OPENVIDU LAYOUT -->
<mat-sidenav-content class="sidenav-main">
<!-- Dynamic layout injection -->
<ng-template #layout></ng-template>
<div id="layout-container">
<ng-container *ngTemplateOutlet="layoutTemplate"></ng-container>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
<ng-template #toolbar></ng-template>
<div id="footer-container">
<ng-container *ngTemplateOutlet="toolbarTemplate"></ng-container>
</div>
</div>

View File

@ -1,14 +1,13 @@
import {
ChangeDetectionStrategy,
Component,
ContentChild,
EventEmitter,
HostListener,
Input,
OnInit,
Output,
TemplateRef,
ViewChild,
ViewContainerRef
ViewChild
} from '@angular/core';
import { Subscriber, Session, StreamEvent, StreamPropertyChangedEvent, SessionDisconnectedEvent, ConnectionEvent } from 'openvidu-browser';
@ -17,7 +16,7 @@ import { ILogger } from '../../models/logger.model';
import { ChatService } from '../../services/chat/chat.service';
import { LoggerService } from '../../services/logger/logger.service';
import { WebrtcService } from '../../services/webrtc/webrtc.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { TokenService } from '../../services/token/token.service';
import { ActionService } from '../../services/action/action.service';
import { Signal } from '../../models/signal.model';
@ -37,11 +36,15 @@ import { LibraryComponents } from '../../config/lib.config';
@Component({
selector: 'ov-session',
templateUrl: './session.component.html',
styleUrls: ['./session.component.css']
styleUrls: ['./session.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SessionComponent implements OnInit {
@Input() tokens: { webcam: string; screen: string };
// @Output() _session = new EventEmitter<any>();
@ContentChild('toolbar', { read: TemplateRef }) toolbarTemplate: TemplateRef<any>;
@ContentChild('panel', { read: TemplateRef }) panelTemplate: TemplateRef<any>;
@ContentChild('layout', { read: TemplateRef }) layoutTemplate: TemplateRef<any>;
@Output() onSessionCreated = new EventEmitter<any>();
// @Output() _publisher = new EventEmitter<any>();
// @Output() _error = new EventEmitter<any>();
@ -63,14 +66,13 @@ export class SessionComponent implements OnInit {
constructor(
protected actionService: ActionService,
protected webrtcService: WebrtcService,
protected openviduService: OpenViduService,
protected participantService: ParticipantService,
protected loggerSrv: LoggerService,
protected chatService: ChatService,
protected tokenService: TokenService,
protected layoutService: LayoutService,
protected menuService: SidenavMenuService,
protected libraryConfigSrv: LibraryConfigService
protected menuService: SidenavMenuService
) {
this.log = this.loggerSrv.get('SessionComponent');
}
@ -130,12 +132,8 @@ export class SessionComponent implements OnInit {
}
async ngOnInit() {
if (this.webrtcService.getWebcamSession() === null) {
this.webrtcService.initialize();
await this.webrtcService.initDefaultPublisher(undefined);
}
this.session = this.webrtcService.getWebcamSession();
this.sessionScreen = this.webrtcService.getScreenSession();
this.session = this.openviduService.getWebcamSession();
this.sessionScreen = this.openviduService.getScreenSession();
this.subscribeToConnectionCreatedAndDestroyed();
this.subscribeToStreamCreated();
this.subscribeToStreamDestroyed();
@ -143,18 +141,14 @@ export class SessionComponent implements OnInit {
this.subscribeToNicknameChanged();
this.chatService.subscribeToChat();
this.subscribeToReconnection();
this.tokenService.setWebcamToken(this.tokens.webcam);
this.tokenService.setScreenToken(this.tokens.screen);
this.onSessionCreated.emit(this.session);
await this.connectToSession();
// Workaround, firefox does not have audio when publisher join with muted camera
// if (this.platformService.isFirefox() && !this.localUserService.hasCameraVideoActive()) {
// this.webrtcService.publishVideo(this.localUserService.getMyCameraPublisher(), true);
// this.webrtcService.publishVideo(this.localUserService.getMyCameraPublisher(), false);
// this.openviduService.publishVideo(this.localUserService.getMyCameraPublisher(), true);
// this.openviduService.publishVideo(this.localUserService.getMyCameraPublisher(), false);
// }
// this._session.emit(this.session);
}
ngOnDestroy() {
@ -164,14 +158,15 @@ export class SessionComponent implements OnInit {
this.participantService.clear();
this.session = null;
this.sessionScreen = null;
this.layoutService.clear();
this.isChatPanelOpened = false;
this.isParticipantsPanelOpened = false;
if (this.menuSubscription) this.menuSubscription.unsubscribe();
if (this.layoutWidthSubscription) this.layoutWidthSubscription.unsubscribe();
}
leaveSession() {
this.log.d('Leaving session...');
this.webrtcService.disconnect();
this.openviduService.disconnect();
}
protected subscribeToTogglingMenu() {
@ -203,17 +198,17 @@ export class SessionComponent implements OnInit {
private async connectToSession(): Promise<void> {
try {
if (this.participantService.areBothEnabled()) {
await this.webrtcService.connectSession(this.webrtcService.getWebcamSession(), this.tokenService.getWebcamToken());
await this.webrtcService.connectSession(this.webrtcService.getScreenSession(), this.tokenService.getScreenToken());
await this.webrtcService.publish(this.participantService.getMyCameraPublisher());
await this.webrtcService.publish(this.participantService.getMyScreenPublisher());
} else if (this.participantService.isOnlyMyScreenEnabled()) {
await this.webrtcService.connectSession(this.webrtcService.getScreenSession(), this.tokenService.getScreenToken());
await this.webrtcService.publish(this.participantService.getMyScreenPublisher());
if (this.participantService.haveICameraAndScreenActive()) {
await this.openviduService.connectSession(this.openviduService.getWebcamSession(), this.tokenService.getWebcamToken());
await this.openviduService.connectSession(this.openviduService.getScreenSession(), this.tokenService.getScreenToken());
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(), this.tokenService.getScreenToken());
await this.openviduService.publish(this.participantService.getMyScreenPublisher());
} else {
await this.webrtcService.connectSession(this.webrtcService.getWebcamSession(), this.tokenService.getWebcamToken());
await this.webrtcService.publish(this.participantService.getMyCameraPublisher());
await this.openviduService.connectSession(this.openviduService.getWebcamSession(), this.tokenService.getWebcamToken());
await this.openviduService.publish(this.participantService.getMyCameraPublisher());
}
} catch (error) {
// this._error.emit({ error: error.error, messgae: error.message, code: error.code, status: error.status });
@ -226,7 +221,7 @@ export class SessionComponent implements OnInit {
this.session.on('connectionCreated', (event: ConnectionEvent) => {
const connectionId = event.connection?.connectionId;
const nickname: string = this.participantService.getNicknameFromConnectionData(event.connection.data);
const isRemoteConnection: boolean = !this.webrtcService.isMyOwnConnection(connectionId);
const isRemoteConnection: boolean = !this.openviduService.isMyOwnConnection(connectionId);
const isCameraConnection: boolean = !nickname?.includes(`_${VideoType.SCREEN}`);
const data = event.connection?.data;
@ -235,16 +230,16 @@ export class SessionComponent implements OnInit {
this.participantService.addRemoteConnection(connectionId, data, null);
//Sending nicnkanme signal to new participants
if (this.webrtcService.needSendNicknameSignal()) {
const data = { clientData: this.participantService.getWebcamNickname() };
this.webrtcService.sendSignal(Signal.NICKNAME_CHANGED, [event.connection], data);
if (this.openviduService.needSendNicknameSignal()) {
const data = { clientData: this.participantService.getMyNickname() };
this.openviduService.sendSignal(Signal.NICKNAME_CHANGED, [event.connection], data);
}
}
});
this.session.on('connectionDestroyed', (event: ConnectionEvent) => {
const nickname: string = this.participantService.getNicknameFromConnectionData(event.connection.data);
const isRemoteConnection: boolean = !this.webrtcService.isMyOwnConnection(event.connection.connectionId);
const isRemoteConnection: boolean = !this.openviduService.isMyOwnConnection(event.connection.connectionId);
const isCameraConnection: boolean = !nickname?.includes(`_${VideoType.SCREEN}`);
// Deleting participant when connection is destroyed
if (isRemoteConnection && isCameraConnection) {
@ -258,7 +253,7 @@ export class SessionComponent implements OnInit {
const connectionId = event.stream?.connection?.connectionId;
const data = event.stream?.connection?.data;
const isRemoteConnection: boolean = !this.webrtcService.isMyOwnConnection(connectionId);
const isRemoteConnection: boolean = !this.openviduService.isMyOwnConnection(connectionId);
if (isRemoteConnection) {
const subscriber: Subscriber = this.session.subscribe(event.stream, undefined);
this.participantService.addRemoteConnection(connectionId, data, subscriber);
@ -271,30 +266,27 @@ export class SessionComponent implements OnInit {
this.session.on('streamDestroyed', (event: StreamEvent) => {
const connectionId = event.stream.connection.connectionId;
this.participantService.removeConnectionByConnectionId(connectionId);
// event.preventDefault();
});
}
private subscribeToStreamPropertyChange() {
// this.session.on('streamPropertyChanged', (event: StreamPropertyChangedEvent) => {
// const connectionId = event.stream.connection.connectionId;
// const isRemoteConnection: boolean = !this.webrtcService.isMyOwnConnection(connectionId);
// if (isRemoteConnection) {
// if (event.changedProperty === 'videoActive') {
// // this.participantService.updateUsers();
// }
// }
// });
this.session.on('streamPropertyChanged', (event: StreamPropertyChangedEvent) => {
const connectionId = event.stream.connection.connectionId;
const isRemoteConnection: boolean = !this.openviduService.isMyOwnConnection(connectionId);
if (isRemoteConnection) {
this.participantService.updateRemoteParticipants();
}
});
}
private subscribeToNicknameChanged() {
this.session.on(`signal:${Signal.NICKNAME_CHANGED}`, (event: any) => {
const connectionId = event.from.connectionId;
const isRemoteConnection: boolean = !this.webrtcService.isMyOwnConnection(connectionId);
const isRemoteConnection: boolean = !this.openviduService.isMyOwnConnection(connectionId);
if (isRemoteConnection) {
const nickname = this.participantService.getNicknameFromConnectionData(event.data);
this.participantService.setNickname(connectionId, nickname);
this.participantService.setRemoteNickname(connectionId, nickname);
}
});
}

View File

@ -1,8 +1,15 @@
/* 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;
position: absolute;
z-index: 999;
border-radius: 5px;
border-radius: var(--ov-panel-radius);
color: #ffffff;
font-family: 'Roboto','RobotoDraft',Helvetica,Arial,sans-serif;
}
@ -11,12 +18,13 @@
padding: 5px;
color: #ffffff;
font-weight: bold;
border-radius: 2px;
border-radius: var(--ov-panel-radius);
}
#dialogNickname {
#nickname-input-container {
background-color: #0000005e;
border-radius: 3px;
border-radius: var(--ov-panel-radius);
}
#closeButton {
@ -30,6 +38,13 @@
padding: 10px;
}
#audio-wave-container {
position: absolute;
right: 0;
z-index: 1;
padding: 5px;
}
.fullscreen {
top: 40px;
}
@ -51,7 +66,7 @@
font-size: 100%;
}
.status-icons, .videoButtons {
.status-icons, #settings-container {
position: absolute;
bottom: 0;
z-index: 10;
@ -62,30 +77,31 @@
left: 0;
}
.status-icons button, .videoButtons button {
.status-icons button, #settings-container button {
color: #ffffff;
width: 26px;
height: 26px;
margin: 5px;
border-radius: var(--ov-buttons-radius);
}
.status-icons button {
background-color: var(--ov-warn-color);
}
.status-icons .mat-icon-button, .videoButtons .mat-icon-button{
.status-icons .mat-icon-button, #settings-container .mat-icon-button{
line-height: 0px;
}
.status-icons mat-icon, .videoButtons mat-icon {
.status-icons mat-icon, #settings-container mat-icon {
font-size: 18px;
}
.videoButtons{
#settings-container{
right: 0;
}
.videoButtons button {
#settings-container button {
background-color: var(--ov-dark-color);
}
@ -96,7 +112,7 @@
position: absolute;
overflow: hidden;
background-color: #000000;
border-radius: 5px;
border-radius: var(--ov-video-radius);
}
mat-error,

View File

@ -1,77 +1,68 @@
<div
*ngIf="this._participant"
class="OT_widget-container"
[id]="'container-' + this._participant.streamManager?.stream?.streamId"
#streamComponent
*ngIf="this._stream"
class="OT_widget-container no-size"
[id]="'container-' + this._stream.streamManager?.stream?.streamId"
#streamContainer
>
<div class="nickname" [class.fullscreen]="isFullscreen">
<div *ngIf="!isMinimal && showNickname" id="nickname-container" class="nickname" [class.fullscreen]="isFullscreen">
<div (click)="toggleNicknameForm()" class="nicknameContainer" selected *ngIf="!toggleNickname">
<span id="nickname">{{ this._participant.nickname }}</span>
<span *ngIf="_participant.local || (_participant.streamManager && !_participant.streamManager?.remote)"> (edit)</span>
<span id="nickname" *ngIf="this._stream.type === 'CAMERA'">{{ this._stream.participant.nickname }}</span>
<span id="nickname" *ngIf="this._stream.type === 'SCREEN'">{{ this._stream.participant.nickname }}_SCREEN</span>
</div>
<div *ngIf="toggleNickname && !this._participant.streamManager?.remote" [class.fullscreen]="isFullscreen" id="dialogNickname">
<form id="nicknameForm">
<mat-form-field color="primary">
<input
matInput
#nicknameInput
placeholder="Nick: {{ this._participant.nickname }}"
[formControl]="nicknameFormControl"
[errorStateMatcher]="matcher"
(keypress)="eventKeyPress($event)"
autocomplete="off"
(focusout)="toggleNicknameForm()"
/>
<mat-error *ngIf="nicknameFormControl.hasError('required')"> Nickname is <strong>required</strong> </mat-error>
<mat-error *ngIf="nicknameFormControl.hasError('maxlength')"> Nickname is <strong>too long!</strong> </mat-error>
</mat-form-field>
</form>
<div *ngIf="toggleNickname && !this._stream.streamManager?.remote" [class.fullscreen]="isFullscreen" id="nickname-input-container">
<input
matInput
#nicknameInput
autocomplete="off"
maxlength="20"
[(ngModel)]="this.nickname"
(keypress)="updateNickname($event)"
(focusout)="updateNickname($event)"
/>
</div>
<!-- Custom network quality element -->
<ng-content select="[network-quality]"></ng-content>
</div>
<div *ngIf="!isMinimal && showAudioDetection && _stream.type === 'CAMERA'" id="audio-wave-container">
<ov-audio-wave [streamManager]="_stream.streamManager"></ov-audio-wave>
</div>
<ov-avatar-profile
*ngIf="!_stream.streamManager?.stream?.videoActive && _stream.type === 'CAMERA'"
[name]="_stream.participant.nickname"
[color]="_stream.participant.colorProfile"
></ov-avatar-profile>
<ov-video
(dblclick)="toggleVideoSize()"
[streamManager]="this._participant.streamManager"
[mutedSound]="mutedSound"
(toggleVideoSizeEvent)="toggleVideoSize($event)"
(dblclick)="toggleVideoEnlarged()"
[streamManager]="_stream.streamManager"
[mutedSound]="_stream?.participant?.isMutedForcibly"
></ov-video>
<div class="status-icons">
<!-- <div id="statusMic" *ngIf="!this._participant.streamManager?.stream?.audioActive">
<mat-icon>mic_off</mat-icon>
</div> -->
<button mat-icon-button id="statusMic" *ngIf="!this._participant.streamManager?.stream?.audioActive" disabled>
<button mat-icon-button id="statusMic" *ngIf="!this._stream.streamManager?.stream?.audioActive" disabled>
<mat-icon>mic_off</mat-icon>
</button>
</div>
<div class="videoButtons">
<!-- Custom hand notification -->
<ng-content select="[hand-notification]"></ng-content>
<div *ngIf="!isMinimal && showSettingsButton" id="settings-container" class="videoButtons">
<button mat-icon-button (click)="toggleVideoMenu($event)" matTooltip="Settings" aria-label="Video settings menu">
<mat-icon>more_vert</mat-icon>
</button>
<span [matMenuTriggerFor]="menu"></span>
<mat-menu #menu="matMenu" yPosition="above" xPosition="before">
<button mat-menu-item id="videoZoomButton" (click)="toggleVideoSize()">
<button mat-menu-item id="videoZoomButton" (click)="toggleVideoEnlarged()">
<mat-icon>{{ this.videoSizeIcon }}</mat-icon>
<span *ngIf="videoSizeIcon === videoSizeIconEnum.NORMAL">Zoom out</span>
<span *ngIf="videoSizeIcon === videoSizeIconEnum.BIG">Zoom in</span>
</button>
<button mat-menu-item id="volumeButton" *ngIf="this._participant.streamManager?.remote" (click)="toggleSound()">
<mat-icon *ngIf="!mutedSound">volume_up</mat-icon>
<span *ngIf="!mutedSound">Mute sound</span>
<button mat-menu-item id="volumeButton" *ngIf="!this._stream.local" (click)="toggleSound()">
<mat-icon *ngIf="!_stream.participant.isMutedForcibly">volume_up</mat-icon>
<span *ngIf="!_stream.participant.isMutedForcibly">Mute sound</span>
<mat-icon *ngIf="mutedSound">volume_off</mat-icon>
<span *ngIf="mutedSound">Unmute sound</span>
<mat-icon *ngIf="_stream.participant.isMutedForcibly">volume_off</mat-icon>
<span *ngIf="_stream.participant.isMutedForcibly">Unmute sound</span>
</button>
<!-- <button mat-menu-item *ngIf="this._participant.streamManager?.stream?.videoActive" id="fullscreenButton" (click)="toggleFullscreen()">
<!-- <button mat-menu-item *ngIf="this._stream.streamManager?.stream?.videoActive" id="fullscreenButton" (click)="toggleFullscreen()">
<mat-icon *ngIf="!isFullscreenEnabled">fullscreen</mat-icon>
<span *ngIf="!isFullscreenEnabled">Fullscreen</span>
@ -83,7 +74,7 @@
mat-menu-item
(click)="replaceScreenTrack()"
id="changeScreenButton"
*ngIf="!this._participant.streamManager?.remote && this._participant.streamManager?.stream?.typeOfVideo === videoTypeEnum.SCREEN"
*ngIf="!this._stream.streamManager?.remote && this._stream.streamManager?.stream?.typeOfVideo === videoTypeEnum.SCREEN"
>
<mat-icon>picture_in_picture</mat-icon>
<span>Replace screen</span>

View File

@ -1,20 +1,18 @@
import { Component, ElementRef, HostListener, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { Component, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { Subscription } from 'rxjs';
import { MatMenuPanel, MatMenuTrigger } from '@angular/material/menu';
import { NicknameMatcher } from '../../matchers/nickname.matcher';
import { VideoSizeIcon } from '../../models/icon.model';
import { ScreenType, VideoType } from '../../models/video-type.model';
import { Storage } from '../../models/storage.model';
import { DocumentService } from '../../services/document/document.service';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { WebrtcService } from '../../services/webrtc/webrtc.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { LayoutService } from '../../services/layout/layout.service';
import { StorageService } from '../../services/storage/storage.service';
import { Signal } from '../../models/signal.model';
import { LayoutClass } from '../../models/layout.model';
import { PublisherProperties } from 'openvidu-browser';
import { StreamModel } from '../../models/participant.model';
import { ParticipantService } from '../../services/participant/participant.service';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
@Component({
selector: 'ov-stream',
@ -22,41 +20,57 @@ import { ParticipantService } from '../../services/participant/participant.servi
styleUrls: ['./stream.component.css']
})
export class StreamComponent implements OnInit {
videoSizeIconEnum = VideoSizeIcon;
videoTypeEnum = VideoType;
videoSizeIcon: VideoSizeIcon = VideoSizeIcon.BIG;
mutedSound: boolean;
toggleNickname: boolean;
nicknameFormControl: FormControl;
matcher: NicknameMatcher;
_participant: StreamModel;
@ViewChild('streamComponent', { read: ViewContainerRef }) streamComponent: ViewContainerRef;
@ViewChild(MatMenuTrigger) public menuTrigger: MatMenuTrigger;
@ViewChild('menu') menu: MatMenuPanel;
videoSizeIconEnum = VideoSizeIcon;
videoTypeEnum = VideoType;
videoSizeIcon: VideoSizeIcon = VideoSizeIcon.BIG;
toggleNickname: boolean;
_stream: StreamModel;
nickname: string;
isMinimal: boolean = false;
showNickname: boolean = true;
showAudioDetection: boolean = true;
showSettingsButton: boolean = true;
private _streamContainer: ElementRef;
private minimalSub: Subscription;
private displayParticipantNameSub: Subscription;
private displayAudioDetectionSub: Subscription;
private settingsButtonSub: Subscription;
constructor(
protected documentService: DocumentService,
protected openViduWebRTCService: WebrtcService,
protected openviduService: OpenViduService,
protected layoutService: LayoutService,
protected participantService: ParticipantService,
protected storageService: StorageService,
protected cdkSrv: CdkOverlayService
protected cdkSrv: CdkOverlayService,
private libService: OpenViduAngularConfigService
) {}
// @HostListener('document:fullscreenchange', ['$event'])
// @HostListener('document:webkitfullscreenchange', ['$event'])
// @HostListener('document:mozfullscreenchange', ['$event'])
// @HostListener('document:MSFullscreenChange', ['$event'])
// onFullscreenHandler(event) {
// this.isFullscreenEnabled = !this.isFullscreenEnabled;
// }
@ViewChild('streamContainer', { static: false, read: ElementRef })
set streamContainer(streamContainer: ElementRef) {
setTimeout(() => {
if (streamContainer) {
this._streamContainer = streamContainer;
// Remove 'no-size' css class for showing the element in the view.
// This is a workaround for fixing a layout bug which provide a bad UX with each new elements created.
this.documentService.removeNoSizeElementClass(this._streamContainer.nativeElement);
}
}, 0);
}
@Input()
set participant(participant: StreamModel) {
this._participant = participant;
this.checkVideoSizeBigIcon(this._participant.videoEnlarged);
this.nicknameFormControl = new FormControl(this._participant.nickname, [Validators.maxLength(25), Validators.required]);
set stream(stream: StreamModel) {
this._stream = stream;
this.checkVideoEnlarged();
if (this._stream.participant) {
this.nickname = this._stream.participant.nickname;
}
}
@ViewChild('nicknameInput')
@ -67,28 +81,22 @@ export class StreamComponent implements OnInit {
}
ngOnInit() {
this.matcher = new NicknameMatcher();
this.subscribeToStreamDirectives();
}
ngOnDestroy() {
this.cdkSrv.setSelector('body');
if (this.settingsButtonSub) this.settingsButtonSub.unsubscribe();
if (this.displayAudioDetectionSub) this.displayAudioDetectionSub.unsubscribe();
if (this.displayParticipantNameSub) this.displayParticipantNameSub.unsubscribe();
}
toggleVideoSize(resetAll?) {
const element = this.documentService.getHTMLElementByClassName(this.streamComponent.element.nativeElement, LayoutClass.ROOT_ELEMENT);
if (!!resetAll) {
this.documentService.removeAllBigElementClass();
this.participantService.resetUsersZoom();
this.participantService.resetUsersZoom();
}
this.documentService.toggleBigElementClass(element);
if (!!this._participant.streamManager?.stream?.connection?.connectionId) {
if (this.openViduWebRTCService.isMyOwnConnection(this._participant.streamManager?.stream?.connection?.connectionId)) {
this.participantService.toggleZoom(this._participant.streamManager?.stream?.connection?.connectionId);
toggleVideoEnlarged() {
if (!!this._stream.streamManager?.stream?.connection?.connectionId) {
if (this.openviduService.isMyOwnConnection(this._stream.streamManager?.stream?.connection?.connectionId)) {
this.participantService.toggleMyVideoEnlarged(this._stream.streamManager?.stream?.connection?.connectionId);
} else {
this.participantService.toggleUserZoom(this._participant.streamManager?.stream?.connection?.connectionId);
this.participantService.toggleRemoteVideoEnlarged(this._stream.streamManager?.stream?.connection?.connectionId);
}
}
this.layoutService.update();
@ -99,26 +107,27 @@ export class StreamComponent implements OnInit {
this.menuTrigger.closeMenu();
return;
}
this.cdkSrv.setSelector('#container-' + this._participant.streamManager?.stream?.streamId);
this.cdkSrv.setSelector('#container-' + this._stream.streamManager?.stream?.streamId);
this.menuTrigger.openMenu();
}
toggleSound() {
this.mutedSound = !this.mutedSound;
this._stream.participant.setMutedForcibly(!this._stream.participant.isMutedForcibly);
}
toggleNicknameForm() {
if (this._participant.local) {
if (this._stream.participant.local) {
this.toggleNickname = !this.toggleNickname;
}
}
eventKeyPress(event) {
if (event && event.keyCode === 13 && this.nicknameFormControl.valid) {
const nickname = this.nicknameFormControl.value;
this.participantService.setNickname(this._participant.connectionId, nickname);
this.storageService.set(Storage.USER_NICKNAME, nickname);
this.openViduWebRTCService.sendSignal(Signal.NICKNAME_CHANGED, undefined, {clientData: nickname});
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 });
}
this.toggleNicknameForm();
}
}
@ -127,14 +136,32 @@ export class StreamComponent implements OnInit {
const properties: PublisherProperties = {
videoSource: ScreenType.SCREEN,
publishVideo: true,
publishAudio: !this.participantService.isMyCameraEnabled(),
publishAudio: !this.participantService.isMyCameraActive(),
mirror: false
};
await this.openViduWebRTCService.replaceTrack(this.participantService.getMyScreenPublisher(), properties);
await this.openviduService.replaceTrack(VideoType.SCREEN, properties);
}
protected checkVideoSizeBigIcon(videoEnlarged: boolean) {
this.videoSizeIcon = videoEnlarged ? VideoSizeIcon.NORMAL : VideoSizeIcon.BIG;
private checkVideoEnlarged() {
this.videoSizeIcon = this._stream.videoEnlarged ? VideoSizeIcon.NORMAL : VideoSizeIcon.BIG;
}
private subscribeToStreamDirectives() {
this.minimalSub = this.libService.minimalObs.subscribe((value: boolean) => {
this.isMinimal = value;
});
this.displayParticipantNameSub = this.libService.displayParticipantNameObs.subscribe((value: boolean) => {
this.showNickname = value;
// this.cd.markForCheck();
});
this.displayAudioDetectionSub = this.libService.displayAudioDetectionObs.subscribe((value: boolean) => {
this.showAudioDetection = value;
// this.cd.markForCheck();
});
this.settingsButtonSub = this.libService.settingsButtonObs.subscribe((value: boolean) => {
this.showSettingsButton = value;
// this.cd.markForCheck();
});
}
}

View File

@ -10,12 +10,19 @@
right: 0;
}
#img-container {
#info-container {
display: flex;
align-items: center;
}
#media-buttons-container {
max-height: 100% !important;
}
#media-buttons-container button, ::ng-deep #media-buttons-container>.ng-star-inserted {
#media-buttons-container > button,
::ng-deep #media-buttons-container > button,
#media-buttons-container:not(#media-buttons-container > button) button,
/* Applying css for external additional buttons*/
::ng-deep #media-buttons-container:not(#media-buttons-container > button) button {
width: 40px;
height: 40px;
background-color: var(--ov-secondary-color);
@ -26,7 +33,7 @@
background-color: var(--ov-warn-color) !important;
}
.active-btn {
.active-btn, ::ng-deep .active-btn {
background-color: var(--ov-tertiary-color) !important;
}
@ -34,15 +41,19 @@
font-size: 24px;
}
#company-logo {
#media-buttons-container button, #menu-buttons-container button {
border-radius: var(--ov-buttons-radius);
}
#branding-logo {
background-color: var(--ov-dark-color);
border-radius: 5px;
border-radius: var(--ov-panel-radius);
max-width: 35px;
max-height: 35px;
padding: 10px;
}
}
#session-title {
#session-name {
font-family: 'Ubuntu', sans-serif;
font-weight: bold;
font-size: 15px;
@ -56,7 +67,7 @@
position: absolute;
top: 12px;
right: 33px;
border-radius: 50%;
border-radius: var(--ov-buttons-radius);
background-color: #ffa600;
border: 1px solid #000;
z-index: 99999;
@ -64,7 +75,7 @@
#leave-btn {
background-color: var(--ov-warn-color) !important;
border-radius: 10px !important;
border-radius: var(--ov-leave-button-radius) !important;
width: 60px !important;
}
@ -86,13 +97,13 @@
color: #fff;
}
@media (max-width: 750px) {
#session-title {
#session-name {
display: none;
}
}
@media (max-width: 850px) {
#company-logo {
#branding-logo {
display: none;
}
}

View File

@ -1,73 +1,68 @@
<mat-toolbar id="toolbar" role="heading" fxLayout fxLayoutAlign="center" fxLayoutGap="10px">
<div fxFlex="20%" fxLayoutAlign="start center">
<div id="img-container">
<img id="company-logo" alt="OpenVidu Logo" [src]="logoUrl" />
<span id="session-title" *ngIf="session && session.sessionId">{{ session.sessionId }}</span>
<div id="info-container">
<img *ngIf="!isMinimal && showLogo" id="branding-logo" src="assets/images/logo.png" ovLogo />
<span id="session-name" *ngIf="!isMinimal && session && session.sessionId && showSessionName">{{ session.sessionId }}</span>
</div>
</div>
<div fxFlex="60%" fxFlexOrder="2" fxLayoutAlign="center center" id="media-buttons-container">
<!-- Microphone button -->
<button
id="navMicrophoneButton"
id="mic-btn"
mat-icon-button
(click)="toggleMicrophone()"
[disabled]="isConnectionLost"
*ngIf="hasAudioDevices"
[class.warn-btn]="!isWebcamAudioEnabled"
[disabled]="isConnectionLost || !hasAudioDevices"
[class.warn-btn]="!isWebcamAudioActive"
>
<mat-icon *ngIf="isWebcamAudioEnabled" matTooltip="Mute your audio">mic</mat-icon>
<mat-icon *ngIf="!isWebcamAudioEnabled" matTooltip="Unmute your audio">mic_off</mat-icon>
<mat-icon *ngIf="isWebcamAudioActive" matTooltip="Mute your audio" id="mic">mic</mat-icon>
<mat-icon *ngIf="!isWebcamAudioActive" matTooltip="Unmute your audio" id="mic_off">mic_off</mat-icon>
</button>
<!-- Camera button -->
<button
id="navCameraButton"
id="camera-btn"
mat-icon-button
(click)="toggleCamera()"
[disabled]="isConnectionLost"
*ngIf="hasVideoDevices"
[class.warn-btn]="!isWebcamVideoEnabled"
[disabled]="isConnectionLost || !hasVideoDevices"
[class.warn-btn]="!isWebcamVideoActive"
>
<mat-icon *ngIf="isWebcamVideoEnabled" matTooltip="Mute your cam">videocam</mat-icon>
<mat-icon *ngIf="!isWebcamVideoEnabled" matTooltip="Unmute your cam">videocam_off</mat-icon>
<mat-icon *ngIf="isWebcamVideoActive" matTooltip="Mute your cam" id="videocam">videocam</mat-icon>
<mat-icon *ngIf="!isWebcamVideoActive" matTooltip="Unmute your cam" id="videocam_off">videocam_off</mat-icon>
</button>
<!-- Screenshare button -->
<button
id="navScreenButton"
mat-icon-button
*ngIf="!isMinimal && showScreenshareButton"
id="screenshare-btn"
(click)="toggleScreenShare()"
[disabled]="isConnectionLost"
[class.active-btn]="isScreenShareEnabled"
[class.active-btn]="isScreenShareActive"
>
<mat-icon *ngIf="!isScreenShareEnabled" matTooltip="Enable screen share">screen_share</mat-icon>
<mat-icon *ngIf="isScreenShareEnabled" matTooltip="Disable screen share">screen_share</mat-icon>
<mat-icon *ngIf="!isScreenShareActive" matTooltip="Enable screen share">screen_share</mat-icon>
<mat-icon *ngIf="isScreenShareActive" matTooltip="Disable screen share">screen_share</mat-icon>
</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 -->
<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="Fullscreen">fullscreen</mat-icon>
<button
mat-icon-button
id="fullscreen-btn"
*ngIf="!isMinimal && showFullscreenButton"
(click)="toggleFullscreen()"
[disabled]="isConnectionLost"
[class.active-btn]="isFullscreenActive"
>
<mat-icon *ngIf="isFullscreenActive" matTooltip="Exit Fullscreen">fullscreen_exit</mat-icon>
<mat-icon *ngIf="!isFullscreenActive" matTooltip="Fullscreen">fullscreen</mat-icon>
</button>
<!-- Custom centered buttons -->
<ng-container *ngIf="centeredButtonsTemplate">
<ng-container *ngTemplateOutlet="centeredButtonsTemplate"></ng-container>
<!-- External additional buttons -->
<ng-container *ngIf="toolbarAdditionalButtonsTemplate">
<ng-container *ngTemplateOutlet="toolbarAdditionalButtonsTemplate"></ng-container>
</ng-container>
<!-- Leave session button -->
<button mat-icon-button (click)="leaveSession()" id="leave-btn">
<button mat-icon-button *ngIf="showLeaveButton" (click)="leaveSession()" id="leave-btn">
<mat-icon matTooltip="Leave the session">call_end</mat-icon>
</button>
</div>
@ -75,23 +70,22 @@
<!-- Default participants button -->
<button
mat-icon-button
id="participants-panel-btn"
*ngIf="!isMinimal && showParticipantsPanelButton"
matTooltip="Show articipants"
(click)="toggleMenu('participants')"
(click)="toggleParticipantsPanel()"
[disabled]="isConnectionLost"
[class.active-btn]="isParticipantsOpened"
>
<mat-icon>people</mat-icon>
</button>
<div #customMenuButton>
<ng-content select="[menu-button]"></ng-content>
</div>
<!-- Default chat button -->
<button
mat-icon-button
*ngIf="!customMenuButton.innerHTML"
(click)="toggleMenu('chat')"
id="chat-panel-btn"
*ngIf="!isMinimal && showChatPanelButton"
(click)="toggleChatPanel()"
[disabled]="isConnectionLost"
[class.active-btn]="isChatOpened"
>

View File

@ -1,5 +1,6 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
EventEmitter,
@ -7,8 +8,7 @@ import {
OnDestroy,
OnInit,
Output,
TemplateRef,
ViewChild
TemplateRef
} from '@angular/core';
import { skip, Subscription } from 'rxjs';
import { TokenService } from '../../services/token/token.service';
@ -16,7 +16,7 @@ import { ChatService } from '../../services/chat/chat.service';
import { SidenavMenuService } from '../../services/sidenav-menu/sidenav-menu.service';
import { DocumentService } from '../../services/document/document.service';
import { WebrtcService } from '../../services/webrtc/webrtc.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { LoggerService } from '../../services/logger/logger.service';
import { ILogger } from '../../models/logger.model';
import { ScreenType } from '../../models/video-type.model';
@ -25,140 +25,175 @@ import { ActionService } from '../../services/action/action.service';
import { DeviceService } from '../../services/device/device.service';
import { ChatMessage } from '../../models/chat.model';
import { ParticipantService } from '../../services/participant/participant.service';
import { LibraryConfigService } from '../../services/library-config/library-config.service';
import { MenuType } from '../../models/menu.model';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
import { ToolbarAdditionalButtonsDirective } from '../../directives/template/openvidu-angular.directive';
@Component({
selector: 'ov-toolbar',
templateUrl: './toolbar.component.html',
styleUrls: ['./toolbar.component.css']
styleUrls: ['./toolbar.component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ToolbarComponent implements OnInit, OnDestroy {
@ContentChild('centeredButtons', { read: TemplateRef }) centeredButtonsTemplate: TemplateRef<any>;
@ContentChild('toolbarAdditionalButtons', { read: TemplateRef }) toolbarAdditionalButtonsTemplate: TemplateRef<any>;
@Output() onMicClicked = new EventEmitter<any>();
@Output() onCamClicked = new EventEmitter<any>();
@Output() onScreenShareClicked = new EventEmitter<any>();
@Output() onLeaveSessionClicked = new EventEmitter<any>();
@Output() onChatClicked = new EventEmitter<any>();
@ContentChild(ToolbarAdditionalButtonsDirective)
set externalAdditionalButtons(externalAdditionalButtons: ToolbarAdditionalButtonsDirective) {
// This directive will has value only when ADDITIONAL BUTTONS component tagget with '*ovToolbarAdditionalButtons' directive
// is inside of the TOOLBAR component tagged with '*ovToolbar' directive
if (externalAdditionalButtons) {
this.toolbarAdditionalButtonsTemplate = externalAdditionalButtons.template;
}
}
@Output() onLeaveButtonClicked = new EventEmitter<any>();
@Output() onCameraButtonClicked = new EventEmitter<any>();
@Output() onMicrophoneButtonClicked = new EventEmitter<any>();
@Output() onFullscreenButtonClicked = new EventEmitter<any>();
@Output() onScreenshareButtonClicked = new EventEmitter<any>();
@Output() onParticipantsPanelButtonClicked = new EventEmitter<any>();
@Output() onChatPanelButtonClicked = new EventEmitter<any>();
session: Session;
unreadMessages: number = 0;
messageList: ChatMessage[] = [];
isScreenShareEnabled: boolean;
isWebcamVideoEnabled: boolean;
isWebcamAudioEnabled: boolean;
isScreenShareActive: boolean;
isWebcamVideoActive: boolean;
isWebcamAudioActive: boolean;
isConnectionLost: boolean;
hasVideoDevices: boolean;
hasAudioDevices: boolean;
isFullscreenEnabled: boolean = false;
isFullscreenActive: boolean = false;
isChatOpened: boolean = false;
isParticipantsOpened: boolean = false;
logoUrl = 'assets/images/openvidu_globe.png';
protected log: ILogger;
protected menuTogglingSubscription: Subscription;
protected chatMessagesSubscription: Subscription;
protected screenShareStateSubscription: Subscription;
protected webcamVideoStateSubscription: Subscription;
protected webcamAudioStateSubscription: Subscription;
isMinimal: boolean = false;
showScreenshareButton = true;
showFullscreenButton: boolean = true;
showLeaveButton: boolean = true;
showParticipantsPanelButton: boolean = true;
showChatPanelButton: boolean = true;
showLogo: boolean = true;
showSessionName: boolean = true;
private log: ILogger;
private minimalSub: Subscription;
private menuTogglingSubscription: Subscription;
private chatMessagesSubscription: Subscription;
private localParticipantSubscription: Subscription;
private screenshareButtonSub: Subscription;
private fullscreenButtonSub: Subscription;
private leaveButtonSub: Subscription;
private participantsPanelButtonSub: Subscription;
private chatPanelButtonSub: Subscription;
private displayLogoSub: Subscription;
private displaySessionNameSub: Subscription;
private currentWindowHeight = window.innerHeight;
@HostListener('window:resize', ['$event'])
sizeChange(event) {
if (this.currentWindowHeight >= window.innerHeight) {
// The user has exit the fullscreen mode
this.isFullscreenActive = false;
this.currentWindowHeight = window.innerHeight;
}
}
@HostListener('document:keydown', ['$event'])
keyDown(event: KeyboardEvent) {
if (event.key === 'F11') {
event.preventDefault();
this.toggleFullscreen();
this.currentWindowHeight = window.innerHeight;
return false;
}
}
constructor(
protected libraryConfigSrv: LibraryConfigService,
protected documentService: DocumentService,
protected chatService: ChatService,
protected menuService: SidenavMenuService,
protected tokenService: TokenService,
protected participantService: ParticipantService,
protected webrtcService: WebrtcService,
protected openviduService: OpenViduService,
protected oVDevicesService: DeviceService,
protected actionService: ActionService,
protected loggerSrv: LoggerService
protected loggerSrv: LoggerService,
private cd: ChangeDetectorRef,
private libService: OpenViduAngularConfigService
) {
this.log = this.loggerSrv.get('ToolbarComponent');
}
ngOnDestroy(): void {
if (this.menuTogglingSubscription) {
this.menuTogglingSubscription.unsubscribe();
}
if (this.chatMessagesSubscription) {
this.chatMessagesSubscription.unsubscribe();
}
if (this.screenShareStateSubscription) {
this.screenShareStateSubscription.unsubscribe();
}
if (this.webcamVideoStateSubscription) {
this.webcamVideoStateSubscription.unsubscribe();
}
if (this.webcamAudioStateSubscription) {
this.webcamAudioStateSubscription.unsubscribe();
}
}
@HostListener('window:resize', ['$event'])
sizeChange(event) {
const maxHeight = window.screen.height;
const maxWidth = window.screen.width;
const curHeight = window.innerHeight;
const curWidth = window.innerWidth;
this.isFullscreenEnabled = maxWidth == curWidth && maxHeight == curHeight;
}
async ngOnInit() {
this.subscribeToToolbarDirectives();
await this.oVDevicesService.initializeDevices();
this.hasVideoDevices = this.oVDevicesService.hasVideoDeviceAvailable();
this.hasAudioDevices = this.oVDevicesService.hasAudioDeviceAvailable();
this.session = this.webrtcService.getWebcamSession();
this.session = this.openviduService.getWebcamSession();
this.subscribeToUserMediaProperties();
this.subscribeToReconnection();
this.subscribeToMenuToggling();
this.subscribeToChatMessages();
}
toggleMicrophone() {
this.onMicClicked.emit();
ngOnDestroy(): void {
if (this.menuTogglingSubscription) this.menuTogglingSubscription.unsubscribe();
if (this.chatMessagesSubscription) this.chatMessagesSubscription.unsubscribe();
if (this.localParticipantSubscription) this.localParticipantSubscription.unsubscribe();
if (this.screenshareButtonSub) this.screenshareButtonSub.unsubscribe();
if (this.fullscreenButtonSub) this.fullscreenButtonSub.unsubscribe();
if (this.leaveButtonSub) this.leaveButtonSub.unsubscribe();
if (this.participantsPanelButtonSub) this.participantsPanelButtonSub.unsubscribe();
if (this.chatPanelButtonSub) this.chatPanelButtonSub.unsubscribe();
if (this.displayLogoSub) this.displayLogoSub.unsubscribe();
if (this.displaySessionNameSub) this.displaySessionNameSub.unsubscribe();
}
if (this.participantService.isMyCameraEnabled()) {
this.webrtcService.publishAudio(
toggleMicrophone() {
this.onMicrophoneButtonClicked.emit();
if (this.participantService.isMyCameraActive()) {
this.openviduService.publishAudio(
this.participantService.getMyCameraPublisher(),
!this.participantService.hasCameraAudioActive()
);
return;
}
this.webrtcService.publishAudio(this.participantService.getMyScreenPublisher(), !this.participantService.hasScreenAudioActive());
this.openviduService.publishAudio(this.participantService.getMyScreenPublisher(), !this.participantService.hasScreenAudioActive());
}
async toggleCamera() {
this.onCamClicked.emit();
this.onCameraButtonClicked.emit();
try {
const publishVideo = !this.participantService.hasCameraVideoActive();
const publishAudio = this.participantService.hasCameraAudioActive();
// Disabling webcam
if (this.participantService.areBothEnabled()) {
this.webrtcService.publishVideo(this.participantService.getMyCameraPublisher(), publishVideo);
if (this.participantService.haveICameraAndScreenActive()) {
this.openviduService.publishVideo(this.participantService.getMyCameraPublisher(), publishVideo);
this.participantService.disableWebcamUser();
this.webrtcService.unpublish(this.participantService.getMyCameraPublisher());
this.webrtcService.publishAudio(this.participantService.getMyScreenPublisher(), publishAudio);
this.openviduService.unpublish(this.participantService.getMyCameraPublisher());
this.openviduService.publishAudio(this.participantService.getMyScreenPublisher(), publishAudio);
return;
}
// Enabling webcam
if (this.participantService.isOnlyMyScreenEnabled()) {
if (this.participantService.isOnlyMyScreenActive()) {
const hasAudio = this.participantService.hasScreenAudioActive();
if (!this.webrtcService.isWebcamSessionConnected()) {
await this.webrtcService.connectSession(this.webrtcService.getWebcamSession(), this.tokenService.getWebcamToken());
if (!this.openviduService.isWebcamSessionConnected()) {
await this.openviduService.connectSession(this.openviduService.getWebcamSession(), this.tokenService.getWebcamToken());
}
await this.webrtcService.publish(this.participantService.getMyCameraPublisher());
this.webrtcService.publishAudio(this.participantService.getMyScreenPublisher(), false);
this.webrtcService.publishAudio(this.participantService.getMyCameraPublisher(), hasAudio);
await this.openviduService.publish(this.participantService.getMyCameraPublisher());
this.openviduService.publishAudio(this.participantService.getMyScreenPublisher(), false);
this.openviduService.publishAudio(this.participantService.getMyCameraPublisher(), hasAudio);
this.participantService.enableWebcamUser();
}
// Muting/unmuting webcam
this.webrtcService.publishVideo(this.participantService.getMyCameraPublisher(), publishVideo);
this.openviduService.publishVideo(this.participantService.getMyCameraPublisher(), publishVideo);
} catch (error) {
this.log.e('There was an error toggling camera:', error.code, error.message);
this.actionService.openDialog('There was an error toggling camera:', error?.error || error?.message);
@ -166,19 +201,19 @@ export class ToolbarComponent implements OnInit, OnDestroy {
}
async toggleScreenShare() {
this.onScreenShareClicked.emit();
this.onScreenshareButtonClicked.emit();
try {
// Disabling screenShare
if (this.participantService.areBothEnabled()) {
if (this.participantService.haveICameraAndScreenActive()) {
this.participantService.disableScreenUser();
this.webrtcService.unpublish(this.participantService.getMyScreenPublisher());
this.openviduService.unpublish(this.participantService.getMyScreenPublisher());
return;
}
// Enabling screenShare
if (this.participantService.isOnlyMyCameraEnabled()) {
const willThereBeWebcam = this.participantService.isMyCameraEnabled() && this.participantService.hasCameraVideoActive();
if (this.participantService.isOnlyMyCameraActive()) {
const willThereBeWebcam = this.participantService.isMyCameraActive() && this.participantService.hasCameraVideoActive();
const hasAudio = willThereBeWebcam ? false : this.hasAudioDevices && this.participantService.hasCameraAudioActive();
const properties: PublisherProperties = {
videoSource: ScreenType.SCREEN,
@ -187,7 +222,7 @@ export class ToolbarComponent implements OnInit, OnDestroy {
publishAudio: hasAudio,
mirror: false
};
const screenPublisher = this.webrtcService.initPublisher(undefined, properties);
const screenPublisher = await this.openviduService.initPublisher(undefined, properties);
screenPublisher.once('accessAllowed', async (event) => {
// Listen to event fired when native stop button is clicked
@ -199,17 +234,20 @@ export class ToolbarComponent implements OnInit, OnDestroy {
this.toggleScreenShare();
});
this.log.d('ACCESS ALOWED screenPublisher');
this.participantService.enableScreenUser(screenPublisher);
this.participantService.activeMyScreenShare(screenPublisher);
if (!this.webrtcService.isScreenSessionConnected()) {
await this.webrtcService.connectSession(this.webrtcService.getScreenSession(), this.tokenService.getScreenToken());
if (!this.openviduService.isScreenSessionConnected()) {
await this.openviduService.connectSession(
this.openviduService.getScreenSession(),
this.tokenService.getScreenToken()
);
}
await this.webrtcService.publish(this.participantService.getMyScreenPublisher());
// this.webrtcService.sendNicknameSignal();
await this.openviduService.publish(this.participantService.getMyScreenPublisher());
// this.openviduService.sendNicknameSignal();
if (!this.participantService.hasCameraVideoActive()) {
// Disabling webcam
this.participantService.disableWebcamUser();
this.webrtcService.unpublish(this.participantService.getMyCameraPublisher());
this.openviduService.unpublish(this.participantService.getMyCameraPublisher());
}
});
@ -224,45 +262,41 @@ export class ToolbarComponent implements OnInit, OnDestroy {
// Disabling screnShare and enabling webcam
const hasAudio = this.participantService.hasScreenAudioActive();
if (!this.webrtcService.isWebcamSessionConnected()) {
await this.webrtcService.connectSession(this.webrtcService.getWebcamSession(), this.tokenService.getWebcamToken());
if (!this.openviduService.isWebcamSessionConnected()) {
await this.openviduService.connectSession(this.openviduService.getWebcamSession(), this.tokenService.getWebcamToken());
}
await this.webrtcService.publish(this.participantService.getMyCameraPublisher());
this.webrtcService.publishAudio(this.participantService.getMyScreenPublisher(), false);
this.webrtcService.publishAudio(this.participantService.getMyCameraPublisher(), hasAudio);
await this.openviduService.publish(this.participantService.getMyCameraPublisher());
this.openviduService.publishAudio(this.participantService.getMyScreenPublisher(), false);
this.openviduService.publishAudio(this.participantService.getMyCameraPublisher(), hasAudio);
this.participantService.enableWebcamUser();
this.participantService.disableScreenUser();
this.webrtcService.unpublish(this.participantService.getMyScreenPublisher());
this.openviduService.unpublish(this.participantService.getMyScreenPublisher());
} catch (error) {
this.log.e('There was an error toggling screen share:', error.code, error.message);
this.actionService.openDialog('There was an error toggling screen share:', error?.error || error?.message);
}
}
async replaceScreenTrack() {
const properties: PublisherProperties = {
videoSource: ScreenType.SCREEN,
publishVideo: true,
publishAudio: !this.participantService.isMyCameraEnabled(),
mirror: false
};
await this.webrtcService.replaceTrack(this.participantService.getMyScreenPublisher(), properties);
}
leaveSession() {
this.log.d('Leaving session...');
this.webrtcService.disconnect();
this.onLeaveSessionClicked.emit();
this.openviduService.disconnect();
this.onLeaveButtonClicked.emit();
}
toggleMenu(type: string) {
this.menuService.toggleMenu(<MenuType>type);
this.onChatClicked.emit();
toggleParticipantsPanel() {
this.onParticipantsPanelButtonClicked.emit();
this.menuService.toggleMenu(MenuType.PARTICIPANTS);
}
toggleChatPanel() {
this.onChatPanelButtonClicked.emit();
this.menuService.toggleMenu(MenuType.CHAT);
}
toggleFullscreen() {
this.documentService.toggleFullscreen('room-container');
this.isFullscreenEnabled = !this.isFullscreenEnabled;
this.isFullscreenActive = !this.isFullscreenActive;
this.documentService.toggleFullscreen('session-container');
this.onFullscreenButtonClicked.emit();
}
protected subscribeToReconnection() {
@ -292,18 +326,52 @@ export class ToolbarComponent implements OnInit, OnDestroy {
this.unreadMessages++;
}
this.messageList = messages;
this.cd.markForCheck();
});
}
protected subscribeToUserMediaProperties() {
this.screenShareStateSubscription = this.participantService.screenShareState.subscribe((enabled) => {
this.isScreenShareEnabled = enabled;
this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p) => {
if(p) {
this.isWebcamVideoActive = p.isCameraVideoActive();
this.isWebcamAudioActive = p.isCameraAudioActive();
this.isScreenShareActive = p.isScreenActive();
this.cd.markForCheck();
}
});
}
this.webcamVideoStateSubscription = this.participantService.webcamVideoActive.subscribe((enabled) => {
this.isWebcamVideoEnabled = enabled;
private subscribeToToolbarDirectives() {
this.minimalSub = this.libService.minimalObs.subscribe((value: boolean) => {
this.isMinimal = value;
this.cd.markForCheck();
});
this.webcamAudioStateSubscription = this.participantService.webcamAudioActive.subscribe((enabled) => {
this.isWebcamAudioEnabled = enabled;
this.screenshareButtonSub = this.libService.screenshareButtonObs.subscribe((value: boolean) => {
this.showScreenshareButton = value;
this.cd.markForCheck();
});
this.fullscreenButtonSub = this.libService.fullscreenButtonObs.subscribe((value: boolean) => {
this.showFullscreenButton = value;
this.cd.markForCheck();
});
this.leaveButtonSub = this.libService.leaveButtonObs.subscribe((value: boolean) => {
this.showLeaveButton = value;
this.cd.markForCheck();
});
this.chatPanelButtonSub = this.libService.chatPanelButtonObs.subscribe((value: boolean) => {
this.showChatPanelButton = value;
this.cd.markForCheck();
});
this.participantsPanelButtonSub = this.libService.participantsPanelButtonObs.subscribe((value: boolean) => {
this.showParticipantsPanelButton = value;
this.cd.markForCheck();
});
this.displayLogoSub = this.libService.displayLogoObs.subscribe((value: boolean) => {
this.showLogo = value;
this.cd.markForCheck();
});
this.displaySessionNameSub = this.libService.displaySessionNameObs.subscribe((value: boolean) => {
this.showSessionName = value;
this.cd.markForCheck();
});
}
}

View File

@ -1,6 +1,6 @@
.card-header {
background-color: var(--ov-primary-color);
border-radius: 10px 10px 0px 0px;
border-radius: var(--ov-panel-radius) var(--ov-panel-radius) 0px 0px;
text-align: center;
height: 50px;
}
@ -47,6 +47,12 @@
bottom: 0px;
}
#audio-wave-container {
position: absolute;
right: 20px;
top: 30px;
}
.cameraMessageContainer {
height: 100%;
width: 100%;
@ -61,6 +67,7 @@
}
.deviceButton {
box-shadow: none;
border-radius: var(--ov-buttons-radius);
background-color: var(--ov-secondary-color) !important;
color: var(--ov-light-color) !important;
}
@ -959,7 +966,7 @@
background-clip: padding-box;
border: 1px solid #999999;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 10px;
border-radius: var(--ov-panel-radius);
-webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
outline: 0;
@ -1074,7 +1081,7 @@
padding: 35px;
}
.videoContainer{
width: 90%;
width: 100%;
}
}
@media (min-width: 768px) {

View File

@ -19,21 +19,24 @@
<div class="row align-items-center">
<div class="col-sm-6 col-md-6 col-lg-6 leftSection">
<div class="spinner-container" *ngIf="isLoading">
<mat-spinner></mat-spinner>
<mat-spinner [diameter]="50"></mat-spinner>
</div>
<div class="videoContainer" *ngIf="!isLoading">
<div *ngFor="let connection of localParticipant | connections">
<div *ngFor="let stream of localParticipant | streams">
<!-- Only webcam video will be shown if webcamera is available -->
<ov-video
*ngIf="(connection.type === 'CAMERA' && hasVideoDevices) || connection.type === 'SCREEN'"
[streamManager]="connection.streamManager"
[ngClass]="{ ovVideoSmall: localParticipant.connections.size > 1 && connection.type === 'CAMERA' }"
*ngIf="(stream.type === 'CAMERA' && hasVideoDevices) || stream.type === 'SCREEN'"
[streamManager]="stream.streamManager"
[ngClass]="{ ovVideoSmall: localParticipant.streams.size > 1 && stream.type === 'CAMERA' }"
></ov-video>
<div class="cameraMessageContainer" *ngIf="connection.type === 'CAMERA' && !hasVideoDevices">
<div class="cameraMessageContainer" *ngIf="stream.type === 'CAMERA' && !hasVideoDevices">
<span *ngIf="!hasVideoDevices && !hasAudioDevices">Oops! Camera and microphone are not available</span>
<span *ngIf="!hasVideoDevices && hasAudioDevices">Oops! Camera is not available</span>
<span *ngIf="hasVideoDevices && !hasAudioDevices">Oops! Microphone is not available</span>
</div>
<div id="audio-wave-container" *ngIf="stream.type === 'CAMERA'">
<ov-audio-wave [streamManager]="stream.streamManager"></ov-audio-wave>
</div>
</div>
</div>
</div>
@ -48,19 +51,16 @@
(click)="toggleCam()"
class="deviceButton"
id="configCardCameraButton"
[class.warn-btn]="!isVideoActive"
[class.warn-btn]="isVideoMuted"
>
<mat-icon *ngIf="isVideoActive" matTooltip="Camera Enabled">videocam</mat-icon>
<mat-icon *ngIf="!isVideoActive" matTooltip="Camera Disabled">videocam_off</mat-icon>
<mat-icon *ngIf="!isVideoMuted" matTooltip="Camera Enabled">videocam</mat-icon>
<mat-icon *ngIf="isVideoMuted" matTooltip="Camera Disabled">videocam_off</mat-icon>
</button>
</div>
<div class="two" fxFlex="80" fxLayoutAlign="center center">
<mat-form-field class="alternate-theme">
<mat-select
placeholder="Camera Options"
[ngModel]="isVideoActive && !!cameraSelected ? cameraSelected.device : 'None'"
(selectionChange)="onCameraSelected($event)"
>
<mat-label>Camera Options</mat-label>
<mat-select [disabled]="isVideoMuted" (selectionChange)="onCameraSelected($event)">
<mat-option *ngFor="let camera of cameras" [value]="camera.device">
{{ camera.label }}
</mat-option>
@ -78,19 +78,16 @@
(click)="toggleMic()"
class="deviceButton"
id="configCardMicrophoneButton"
[class.warn-btn]="!isAudioActive"
[class.warn-btn]="isAudioMuted"
>
<mat-icon *ngIf="isAudioActive" matTooltip="Microphone Enabled">mic</mat-icon>
<mat-icon *ngIf="!isAudioActive" matTooltip="Microphone Disabled">mic_off</mat-icon>
<mat-icon *ngIf="!isAudioMuted" matTooltip="Microphone Enabled">mic</mat-icon>
<mat-icon *ngIf="isAudioMuted" matTooltip="Microphone Disabled">mic_off</mat-icon>
</button>
</div>
<div class="two" fxFlex="80" fxLayoutAlign="center center">
<mat-form-field class="alternate-theme">
<mat-select
placeholder="Microphone Options"
[ngModel]="isAudioActive && microphoneSelected ? microphoneSelected.device : 'None'"
(selectionChange)="onMicrophoneSelected($event)"
>
<mat-label>Microphone Options</mat-label>
<mat-select [disabled]="isAudioMuted" (selectionChange)="onMicrophoneSelected($event)">
<mat-option *ngFor="let microphone of microphones" [value]="microphone.device">
{{ microphone.label }}
</mat-option>

View File

@ -1,4 +1,4 @@
import { Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';
import { Subscription } from 'rxjs';
@ -7,15 +7,14 @@ import { Publisher, PublisherProperties } from 'openvidu-browser';
import { ILogger } from '../../models/logger.model';
import { CustomDevice } from '../../models/device.model';
import { Storage } from '../../models/storage.model';
import { ScreenType } from '../../models/video-type.model';
import { ScreenType, VideoType } from '../../models/video-type.model';
import { NicknameMatcher } from '../../matchers/nickname.matcher';
import { DeviceService } from '../../services/device/device.service';
import { LoggerService } from '../../services/logger/logger.service';
import { StorageService } from '../../services/storage/storage.service';
import { WebrtcService } from '../../services/webrtc/webrtc.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { ActionService } from '../../services/action/action.service';
import { ParticipantService } from '../../services/participant/participant.service';
import { ParticipantAbstractModel } from '../../models/participant.model';
@ -23,7 +22,8 @@ import { ParticipantAbstractModel } from '../../models/participant.model';
@Component({
selector: 'ov-user-settings',
templateUrl: './user-settings.component.html',
styleUrls: ['./user-settings.component.css']
styleUrls: ['./user-settings.component.css'],
// changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserSettingsComponent implements OnInit, OnDestroy {
@ViewChild('bodyCard') bodyCard: ElementRef;
@ -36,26 +36,26 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
microphones: CustomDevice[];
cameraSelected: CustomDevice;
microphoneSelected: CustomDevice;
isVideoActive = true;
isAudioActive = true;
isVideoMuted: boolean;
isAudioMuted: boolean;
screenShareEnabled: boolean;
localParticipant: ParticipantAbstractModel;
columns: number;
nicknameFormControl = new FormControl('', [Validators.maxLength(25), Validators.required]);
nicknameFormControl = new FormControl('', [Validators.maxLength(20), Validators.required]);
matcher = new NicknameMatcher();
hasVideoDevices: boolean;
hasAudioDevices: boolean;
isLoading = true;
private log: ILogger;
private oVUsersSubscription: Subscription;
private localParticipantSubscription: Subscription;
private screenShareStateSubscription: Subscription;
constructor(
private actionService: ActionService,
private deviceSrv: DeviceService,
private loggerSrv: LoggerService,
private openViduWebRTCService: WebrtcService,
private openviduService: OpenViduService,
private participantService: ParticipantService,
private storageSrv: StorageService
) {
@ -68,10 +68,11 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
}
async ngOnInit() {
this.subscribeToLocalParticipantEvents();
this.openViduWebRTCService.initialize();
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.columns = window.innerWidth > 900 ? 2 : 1;
this.setDevicesInfo();
@ -79,12 +80,11 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
await this.initwebcamPublisher();
}
this.isLoading = false;
}
ngOnDestroy() {
if (this.oVUsersSubscription) {
this.oVUsersSubscription.unsubscribe();
if (this.localParticipantSubscription) {
this.localParticipantSubscription.unsubscribe();
}
if (this.screenShareStateSubscription) {
@ -95,73 +95,78 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
async onCameraSelected(event: any) {
const videoSource = event?.value;
if (!!videoSource) {
// Is New deviceId different from the old one?
if (this.deviceSrv.needUpdateVideoTrack(videoSource)) {
const mirror = this.deviceSrv.cameraNeedsMirror(videoSource);
await this.openViduWebRTCService.republishTrack(videoSource, null, mirror);
this.deviceSrv.setCameraSelected(videoSource);
this.cameraSelected = this.deviceSrv.getCameraSelected();
}
// Publish Webcam video
this.openViduWebRTCService.publishVideo(this.participantService.getMyCameraPublisher(), true);
this.isVideoActive = true;
// Is New deviceId different from the old one?
if (this.deviceSrv.needUpdateVideoTrack(videoSource)) {
const mirror = this.deviceSrv.cameraNeedsMirror(videoSource);
//TODO: Uncomment this when replaceTrack issue is fixed
// const pp: PublisherProperties = { videoSource, audioSource: false, mirror };
// await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
// TODO: Remove this when replaceTrack issue is fixed
const pp: PublisherProperties = { videoSource, audioSource: this.microphoneSelected.device, mirror };
await this.openviduService.republishTrack(pp);
} else {
// Videosource is 'null' because of the user has selected 'None' or muted the camera
// Unpublish webcam
this.openViduWebRTCService.publishVideo(this.participantService.getMyCameraPublisher(), false);
//TODO: save 'None' device in storage
// this.deviceSrv.setCameraSelected(videoSource);
// this.cameraSelected = this.deviceSrv.getCameraSelected();
this.isVideoActive = false;
this.cameraSelected = videoSource;
this.deviceSrv.setCameraSelected(this.cameraSelected);
}
if (this.isVideoMuted) {
// Publish Webcam video
this.openviduService.publishVideo(this.participantService.getMyCameraPublisher(), true);
this.isVideoMuted = false;
}
}
async onMicrophoneSelected(event: any) {
const audioSource = event?.value;
// Is New deviceId different than older?
if (this.deviceSrv.needUpdateAudioTrack(audioSource)) {
//TODO: Uncomment this when replaceTrack issue is fixed
// const pp: PublisherProperties = { audioSource, videoSource: false };
// await this.openviduService.replaceTrack(VideoType.CAMERA, pp);
// TODO: Remove this when replaceTrack issue is fixed
const mirror = this.deviceSrv.cameraNeedsMirror(this.cameraSelected.device);
const pp: PublisherProperties = { videoSource: this.cameraSelected.device, audioSource, mirror };
await this.openviduService.republishTrack(pp);
if (!!audioSource) {
// Is New deviceId different than older?
if (this.deviceSrv.needUpdateAudioTrack(audioSource)) {
const mirror = this.deviceSrv.cameraNeedsMirror(this.cameraSelected.device);
await this.openViduWebRTCService.republishTrack(null, audioSource, mirror);
this.deviceSrv.setMicSelected(audioSource);
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
}
// Publish microphone
this.publishAudio(true);
this.isAudioActive = true;
return;
this.microphoneSelected = audioSource;
this.deviceSrv.setMicSelected(this.microphoneSelected);
}
if (this.isAudioMuted) {
// Enable microphone
this.openviduService.publishAudio(this.participantService.getMyCameraPublisher(), true);
this.isAudioMuted = true;
}
// Unpublish microhpone
this.publishAudio(false);
this.isAudioActive = false;
}
toggleCam() {
this.isVideoActive = !this.isVideoActive;
this.openViduWebRTCService.publishVideo(this.participantService.getMyCameraPublisher(), this.isVideoActive);
if (this.participantService.areBothEnabled()) {
const publish = this.isVideoMuted;
this.openviduService.publishVideo(this.participantService.getMyCameraPublisher(), publish);
if (this.participantService.haveICameraAndScreenActive()) {
// Cam will not published, disable webcam with screensharing active
this.participantService.disableWebcamUser();
this.openViduWebRTCService.publishAudio(this.participantService.getMyScreenPublisher(), this.isAudioActive);
} else if (this.participantService.isOnlyMyScreenEnabled()) {
this.openviduService.publishAudio(this.participantService.getMyScreenPublisher(), publish);
} else if (this.participantService.isOnlyMyScreenActive()) {
// Cam will be published, enable webcam
this.participantService.enableWebcamUser();
}
this.isVideoMuted = !this.isVideoMuted;
this.storageSrv.setVideoMuted(this.isVideoMuted);
}
toggleScreenShare() {
async toggleScreenShare() {
// Disabling screenShare
if (this.participantService.areBothEnabled()) {
if (this.participantService.haveICameraAndScreenActive()) {
this.participantService.disableScreenUser();
return;
}
// Enabling screenShare
if (this.participantService.isOnlyMyCameraEnabled()) {
const willThereBeWebcam = this.participantService.isMyCameraEnabled() && this.participantService.hasCameraVideoActive();
const hasAudio = willThereBeWebcam ? false : this.hasAudioDevices && this.isAudioActive;
if (this.participantService.isOnlyMyCameraActive()) {
const willThereBeWebcam = this.participantService.isMyCameraActive() && this.participantService.hasCameraVideoActive();
const hasAudio = willThereBeWebcam ? false : this.hasAudioDevices && this.isAudioMuted;
const properties: PublisherProperties = {
videoSource: ScreenType.SCREEN,
audioSource: this.hasAudioDevices ? undefined : null,
@ -169,7 +174,7 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
publishAudio: hasAudio,
mirror: false
};
const screenPublisher = this.openViduWebRTCService.initPublisher(undefined, properties);
const screenPublisher = await this.openviduService.initPublisher(undefined, properties);
screenPublisher.on('accessAllowed', (event) => {
screenPublisher.stream
@ -179,7 +184,7 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
this.log.d('Clicked native stop button. Stopping screen sharing');
this.toggleScreenShare();
});
this.participantService.enableScreenUser(screenPublisher);
this.participantService.activeMyScreenShare(screenPublisher);
if (!this.participantService.hasCameraVideoActive()) {
this.participantService.disableWebcamUser();
}
@ -199,8 +204,10 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
}
toggleMic() {
this.isAudioActive = !this.isAudioActive;
this.publishAudio(this.isAudioActive);
const publish = this.isAudioMuted;
this.openviduService.publishAudio(this.participantService.getMyCameraPublisher(), publish);
this.isAudioMuted = !this.isAudioMuted;
this.storageSrv.setAudioMuted(this.isAudioMuted);
}
eventKeyPress(event) {
@ -216,8 +223,8 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
joinSession() {
if (this.nicknameFormControl.valid) {
const nickname = this.nicknameFormControl.value;
this.participantService.setNickname(this.participantService.getMyCameraConnectionId(), nickname);
this.storageSrv.set(Storage.USER_NICKNAME, nickname);
this.participantService.setMyNickname(nickname);
this.storageSrv.setNickname(nickname);
return this.onJoinClicked.emit();
}
this.scrollToBottom();
@ -235,9 +242,8 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
this.cameraSelected = this.deviceSrv.getCameraSelected();
this.microphoneSelected = this.deviceSrv.getMicrophoneSelected();
this.isVideoActive = this.hasVideoDevices && this.cameraSelected.label !== 'None';
this.isAudioActive = this.hasAudioDevices && this.microphoneSelected.label !== 'None';
this.isVideoMuted = this.deviceSrv.isVideoMuted();
this.isAudioMuted = this.deviceSrv.isAudioMuted();
}
private scrollToBottom(): void {
@ -246,25 +252,16 @@ export class UserSettingsComponent implements OnInit, OnDestroy {
} catch (err) {}
}
private publishAudio(audio: boolean) {
this.participantService.isMyCameraEnabled()
? this.openViduWebRTCService.publishAudio(this.participantService.getMyCameraPublisher(), audio)
: this.openViduWebRTCService.publishAudio(this.participantService.getMyScreenPublisher(), audio);
}
private subscribeToLocalParticipantEvents() {
this.oVUsersSubscription = this.participantService.localParticipantObs.subscribe((p) => {
this.localParticipantSubscription = this.participantService.localParticipantObs.subscribe((p) => {
this.localParticipant = p;
});
this.screenShareStateSubscription = this.participantService.screenShareState.subscribe((enabled) => {
this.screenShareEnabled = enabled;
this.screenShareEnabled = p.isScreenActive();
});
}
private async initwebcamPublisher() {
const publisher = await this.openViduWebRTCService.initDefaultPublisher(undefined);
const publisher = await this.openviduService.initDefaultPublisher(undefined);
if (publisher) {
// this.handlePublisherSuccess(publisher);
this.handlePublisherError(publisher);
}

View File

@ -8,17 +8,6 @@ video {
padding: 0;
border: 0;
font-size: 100%;
border-radius: 5px;
border-radius: var(--ov-video-radius);
background-color: #000000;
}
.poster_img {
position: absolute;
z-index: 888;
max-width: 60%;
max-height: 60%;
bottom: 25%;
margin: auto;
right: 0;
left: 0;
}

View File

@ -1,12 +1,12 @@
import { AfterViewInit, Component, ElementRef, Input, EventEmitter, Output, ViewChild } from '@angular/core';
import { AfterViewInit, Component, ElementRef, Input, ViewChild } from '@angular/core';
import { StreamManager } from 'openvidu-browser';
import { VideoType } from '../../models/video-type.model';
@Component({
selector: 'ov-video',
template: `
<img *ngIf="!_streamManager?.stream?.videoActive && (type === 'CAMERA' || !type)" class="poster_img" alt="OpenVidu Logo" src="assets/images/poster.png" />
<video
class="OT_video-element"
#videoElement
[attr.id]="streamManager && _streamManager.stream ? 'video-' + _streamManager.stream.streamId : 'video-undefined'"
[muted]="mutedSound"
@ -16,11 +16,7 @@ import { VideoType } from '../../models/video-type.model';
})
export class VideoComponent implements AfterViewInit {
@Input() mutedSound: boolean;
@Output() toggleVideoSizeEvent = new EventEmitter<any>();
_streamManager: StreamManager;
_videoElement: ElementRef;
type: VideoType = VideoType.CAMERA;
@ -39,27 +35,18 @@ export class VideoComponent implements AfterViewInit {
@Input()
set streamManager(streamManager: StreamManager) {
setTimeout(() => {
if (streamManager) {
this._streamManager = streamManager;
if (!!this._videoElement && this._streamManager) {
this.type = <VideoType>this._streamManager?.stream?.typeOfVideo;
if (this.type === VideoType.SCREEN) {
this._videoElement.nativeElement.style.objectFit = 'contain';
this._videoElement.nativeElement.style.background = '#272727';
this.enableVideoSizeBig();
// this._videoElement.nativeElement.style.background = '#272727';
} else {
this._videoElement.nativeElement.style.objectFit = 'cover';
}
this._streamManager.addVideoElement(this._videoElement.nativeElement);
}
});
}
enableVideoSizeBig() {
// Doing video size bigger.
// Timeout because of connectionId is null and icon does not change
setTimeout(() => {
this.toggleVideoSizeEvent.emit(true);
}, 590);
}
}
}

View File

@ -2,7 +2,7 @@
height: 100%;
}
#user-settings-container {
#pre-join-container {
height: inherit;
}

View File

@ -1,32 +1,104 @@
<div id="call-container">
<div id="user-settings-container" *ngIf="!joinSessionClicked && !closeClicked">
<ov-user-settings (onJoinClicked)="_onJoinClicked()" (onCloseClicked)="onLeaveSessionClicked()"></ov-user-settings>
<div id="pre-join-container" *ngIf="showPrejoin && participantReady && !joinSessionClicked">
<ov-pre-join (onJoinButtonClicked)="_onJoinButtonClicked()"></ov-pre-join>
<!-- <ov-user-settings (onJoinClicked)="_onJoinClicked()" (onCloseClicked)="onLeaveSessionClicked()"></ov-user-settings> -->
</div>
<div id="spinner" *ngIf="joinSessionClicked && !isSessionAlive && !error">
<div id="spinner" *ngIf="(joinSessionClicked || !showPrejoin) && !participantReady && !error">
<mat-spinner [diameter]="50"></mat-spinner>
<span>Joining the room ...</span>
</div>
<div id="spinner" *ngIf="joinSessionClicked && !isSessionAlive && error">
<div id="spinner" *ngIf="joinSessionClicked && !participantReady && error">
<mat-icon class="error-icon">error</mat-icon>
<span>{{ errorMessage }}</span>
</div>
<div id="session-container" *ngIf="joinSessionClicked && isSessionAlive && !error">
<ov-session [tokens]="_tokens">
<div id="session-container" *ngIf="(joinSessionClicked || !showPrejoin) && participantReady && canPublish && !error">
<ov-session (onSessionCreated)="_onSessionCreated($event)">
<ng-template #toolbar>
<ng-container *ngIf="openviduAngularToolbarTemplate">
<ng-container *ngTemplateOutlet="openviduAngularToolbarTemplate"></ng-container>
</ng-container>
</ng-template>
<!-- Default toolbar -->
<!--<ng-template #toolbar toolbar>
<ov-toolbar
(onCamClicked)="onCamClicked()"
(onMicClicked)="onMicClicked()"
(onScreenShareClicked)="onScreenShareClicked()"
(onLeaveSessionClicked)="onLeaveSessionClicked()"
></ov-toolbar>
</ng-template>-->
<ng-template #panel>
<ng-container *ngIf="openviduAngularPanelTemplate">
<ng-container *ngTemplateOutlet="openviduAngularPanelTemplate"></ng-container>
</ng-container>
</ng-template>
<!-- <ov-layout layout></ov-layout> -->
<ng-template #layout>
<ng-container *ngIf="openviduAngularLayoutTemplate">
<ng-container *ngTemplateOutlet="openviduAngularLayoutTemplate"></ng-container>
</ng-container>
</ng-template>
</ov-session>
</div>
</div>
<ng-template #defaultToolbar>
<ov-toolbar
(onLeaveButtonClicked)="onLeaveButtonClicked()"
(onCameraButtonClicked)="onCameraButtonClicked()"
(onMicrophoneButtonClicked)="onMicrophoneButtonClicked()"
(onScreenshareButtonClicked)="onScreenshareButtonClicked()"
(onFullscreenButtonClicked)="onFullscreenButtonClicked()"
(onParticipantsPanelButtonClicked)="onParticipantsPanelButtonClicked()"
(onChatPanelButtonClicked)="onChatPanelButtonClicked()"
>
<ng-template #toolbarAdditionalButtons>
<ng-container *ngTemplateOutlet="openviduAngularToolbarAdditionalButtonsTemplate"></ng-container>
</ng-template>
</ov-toolbar>
</ng-template>
<ng-template #defaultPanel>
<ov-panel>
<ng-template #chatPanel>
<ng-container *ngTemplateOutlet="openviduAngularChatPanelTemplate"></ng-container>
</ng-template>
<ng-template #participantsPanel>
<ng-container *ngTemplateOutlet="openviduAngularParticipantsPanelTemplate"></ng-container>
</ng-template>
</ov-panel>
</ng-template>
<ng-template #defaultChatPanel>
<ov-chat-panel></ov-chat-panel>
</ng-template>
<ng-template #defaultParticipantsPanel>
<ov-participants-panel>
<ng-template #participantPanelItem let-participant>
<ng-container
*ngTemplateOutlet="openviduAngularParticipantPanelItemTemplate; context: { $implicit: participant }"
></ng-container>
</ng-template>
</ov-participants-panel>
</ng-template>
<ng-template #defaultParticipantPanelItem let-participant>
<ov-participant-panel-item [participant]="participant">
<ng-template #participantPanelItemElements>
<ng-container *ngTemplateOutlet="openviduAngularParticipantPanelItemElementsTemplate; context: { $implicit: participant }"></ng-container>
</ng-template>
</ov-participant-panel-item>
</ng-template>
<ng-template #defaultLayout>
<ov-layout>
<ng-template #stream let-stream>
<ng-container *ngTemplateOutlet="openviduAngularStreamTemplate; context: { $implicit: stream }"> </ng-container>
</ng-template>
</ov-layout>
</ng-template>
<ng-template #defaultStream let-stream>
<ov-stream [stream]="stream"></ov-stream>
</ng-template>

View File

@ -1,135 +1,304 @@
import {
AfterViewInit,
Component,
ContentChild,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
EventEmitter,
ViewChild,
ChangeDetectorRef,
AfterViewInit,
ViewContainerRef
TemplateRef,
ViewChild
} from '@angular/core';
import { LibraryConfigService } from '../../services/library-config/library-config.service';
import { ToolbarComponent } from '../toolbar/toolbar.component';
import { OpenViduErrorName, Session } from 'openvidu-browser';
import { Subscription } from 'rxjs';
import {
ChatPanelDirective,
LayoutDirective,
PanelDirective,
ParticipantPanelItemDirective,
ParticipantPanelItemElementsDirective,
ParticipantsPanelDirective,
StreamDirective,
ToolbarAdditionalButtonsDirective,
ToolbarDirective
} from '../../directives/template/openvidu-angular.directive';
import { ILogger } from '../../models/logger.model';
import { ParticipantProperties } from '../../models/participant.model';
import { ActionService } from '../../services/action/action.service';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
import { DeviceService } from '../../services/device/device.service';
import { LoggerService } from '../../services/logger/logger.service';
import { OpenViduService } from '../../services/openvidu/openvidu.service';
import { ParticipantService } from '../../services/participant/participant.service';
import { StorageService } from '../../services/storage/storage.service';
import { TokenService } from '../../services/token/token.service';
@Component({
selector: 'ov-videoconference',
templateUrl: './videoconference.component.html',
styleUrls: ['./videoconference.component.css']
})
export class VideoconferenceComponent implements OnInit, AfterViewInit {
@Input() sessionName: string;
@Input() userName: string;
@Input() openviduServerUrl: string;
@Input() openviduSecret: string;
// @Input() tokens: { webcam: string; screen: string };
export class VideoconferenceComponent implements OnInit, OnDestroy, AfterViewInit {
// *** Toolbar ***
@ContentChild(ToolbarDirective) externalToolbar: ToolbarDirective;
@ContentChild(ToolbarAdditionalButtonsDirective) externalToolbarAdditionalButtons: ToolbarAdditionalButtonsDirective;
@Output() onJoinClicked = new EventEmitter<any>();
@Output() onCloseClicked = new EventEmitter<any>();
// *** Panels ***
@ContentChild(PanelDirective) externalPanel: PanelDirective;
@ContentChild(ChatPanelDirective) externalChatPanel: ChatPanelDirective;
@ContentChild(ParticipantsPanelDirective) externalParticipantsPanel: ParticipantsPanelDirective;
@ContentChild(ParticipantPanelItemDirective) externalParticipantPanelItem: ParticipantPanelItemDirective;
@ContentChild(ParticipantPanelItemElementsDirective) externalParticipantPanelItemElements: ParticipantPanelItemElementsDirective;
joinSessionClicked: boolean = false;
closeClicked: boolean = false;
isSessionAlive: boolean = false;
_tokens: { webcam: string; screen: string };
error: boolean = false;
errorMessage: string = '';
// *** Layout ***
@ContentChild(LayoutDirective) externalLayout: LayoutDirective;
@ContentChild(StreamDirective) externalStream: StreamDirective;
_toolbar: ViewContainerRef;
@ViewChild('defaultToolbar', { static: false, read: TemplateRef }) defaultToolbarTemplate: TemplateRef<any>;
@ViewChild('defaultPanel', { static: false, read: TemplateRef }) defaultPanelTemplate: TemplateRef<any>;
@ViewChild('defaultChatPanel', { static: false, read: TemplateRef }) defaultChatPanelTemplate: TemplateRef<any>;
@ViewChild('defaultParticipantsPanel', { static: false, read: TemplateRef }) defaultParticipantsPanelTemplate: TemplateRef<any>;
@ViewChild('defaultParticipantPanelItem', { static: false, read: TemplateRef }) defaultParticipantPanelItemTemplate: TemplateRef<any>;
@ViewChild('defaultLayout', { static: false, read: TemplateRef }) defaultLayoutTemplate: TemplateRef<any>;
@ViewChild('defaultStream', { static: false, read: TemplateRef }) defaultStreamTemplate: TemplateRef<any>;
constructor(protected libraryConfigSrv: LibraryConfigService, private cd: ChangeDetectorRef) {}
openviduAngularToolbarTemplate: TemplateRef<any>;
openviduAngularToolbarAdditionalButtonsTemplate: TemplateRef<any>;
openviduAngularPanelTemplate: TemplateRef<any>;
openviduAngularChatPanelTemplate: TemplateRef<any>;
openviduAngularParticipantsPanelTemplate: TemplateRef<any>;
openviduAngularParticipantPanelItemTemplate: TemplateRef<any>;
openviduAngularParticipantPanelItemElementsTemplate: TemplateRef<any>;
openviduAngularLayoutTemplate: TemplateRef<any>;
openviduAngularStreamTemplate: TemplateRef<any>;
// @ViewChild('toolbar', { static: false, read: ViewContainerRef })
// set toolbar(reference: ViewContainerRef) {
// setTimeout(() => {
// console.log('setting ref', reference);
// this._toolbar = reference;
// if (this._toolbar) {
// let component = ToolbarComponent;
// if (this.libraryConfigSrv.isCustomComponentDefined('ov-toolbar')) {
// component = this.libraryConfigSrv.getToolbarComponent();
// }
// this._toolbar?.clear();
// this._toolbar.createComponent(component);
// }
// }, 100);
// }
ngAfterViewInit() {
// if(this.customToolbar && this.libraryConfigSrv.isCustomComponentDefined('ov-toolbar')){
// const viewContainerRef = this.customToolbar.viewContainerRef;
// viewContainerRef.clear();
// const componentRef = viewContainerRef.createComponent<any>(this.libraryConfigSrv.getToolbarComponent());
// } else {
// this.customToolbar.viewContainerRef.clear();
// const viewContainerRef = this.toolbar.viewContainerRef;
// viewContainerRef.clear();
// viewContainerRef.createComponent<any>(ToolbarComponent);
// }
}
ngOnInit() {
}
@Input('tokens')
@Input()
set tokens(tokens: { webcam: string; screen: string }) {
if (!!tokens?.webcam || !!this.tokens?.screen) {
// 1 token received
this.cd.detectChanges();
// this.cd.markForCheck();
this._tokens = tokens;
this.joinSessionClicked = true;
this.isSessionAlive = true;
} else {
if (!tokens || (!tokens.webcam && !tokens.screen)) {
//No tokens received
throw new Error('No tokens received');
// throw new Error('No tokens received');
this.log.w('No tokens received');
} else {
if (tokens.webcam || tokens.screen) {
this.tokenService.setWebcamToken(tokens.webcam);
this.tokenService.setScreenToken(tokens.screen);
this.canPublish = true;
}
}
}
async _onJoinClicked() {
this.onJoinClicked.emit();
// if (!this.tokens || (!this.tokens?.webcam && !this.tokens?.screen)) {
// //No tokens received
// *** Events ***
// if (!!this.sessionName && !!this.openviduServerUrl && !!this.openviduSecret) {
// // Generate tokens
// this._tokens = {
// webcam: await this.restService.getToken(this.sessionName, this.openviduServerUrl, this.openviduSecret),
// screen: await this.restService.getToken(this.sessionName, this.openviduServerUrl, this.openviduSecret)
// };
// } else {
// // No tokens received and can't generate them
// this.error = true;
// this.errorMessage = `Cannot access to OpenVidu Server with url '${this.openviduServerUrl}' to genere tokens for session '${this.sessionName}'`;
// throw this.errorMessage;
// }
// } else if (!this.tokens?.webcam || !this.tokens?.screen) {
// // 1 token received
// const aditionalToken = await this.restService.getToken(this.sessionName, this.openviduServerUrl, this.openviduSecret);
// this._tokens = {
// webcam: !!this.tokens.webcam ? this.tokens.webcam : aditionalToken,
// screen: !!this.tokens.screen ? this.tokens.screen : aditionalToken
// };
// } else {
// // 2 tokens received.
// this._tokens = {
// webcam: this.tokens.webcam,
// screen: this.tokens.screen
// };
// }
// this.joinSessionClicked = true;
// this.isSessionAlive = true;
}
onLeaveSessionClicked() {
this.isSessionAlive = false;
this.closeClicked = true;
// Event sent when user click on the join button in pre-join page
@Output() onJoinButtonClicked = new EventEmitter<any>();
// Event sent when user click on the join button in pre-join page
@Output() onToolbarLeaveButtonClicked = new EventEmitter<any>();
@Output() onToolbarCameraButtonClicked = new EventEmitter<any>();
@Output() onToolbarMicrophoneButtonClicked = new EventEmitter<any>();
@Output() onToolbarScreenshareButtonClicked = new EventEmitter<any>();
@Output() onToolbarFullscreenButtonClicked = new EventEmitter<any>();
@Output() onToolbarParticipantsPanelButtonClicked = new EventEmitter<any>();
@Output() onToolbarChatPanelButtonClicked = new EventEmitter<any>();
// Event sent when participant has joined the session
// @Output() onParticipantJoined = new EventEmitter<any>();
// Event sent when session has been created
@Output() onSessionCreated = new EventEmitter<any>();
// Event sent when participant has been created
@Output() onParticipantCreated = new EventEmitter<any>();
joinSessionClicked: boolean = false;
participantReady: boolean = false;
canPublish: boolean = false;
error: boolean = false;
errorMessage: string = '';
showPrejoin: boolean = true;
private externalParticipantName: string;
private prejoinSub: Subscription;
private participantNameSub: Subscription;
private log: ILogger;
constructor(
private loggerSrv: LoggerService,
private storageSrv: StorageService,
private participantService: ParticipantService,
private deviceSrv: DeviceService,
private openviduService: OpenViduService,
private actionService: ActionService,
private libService: OpenViduAngularConfigService,
private tokenService: TokenService
) {
this.log = this.loggerSrv.get('VideoconferenceComponent');
}
onMicClicked() {}
async ngOnInit() {
this.subscribeToVideconferenceDirectives();
await this.deviceSrv.initializeDevices();
const nickname = this.externalParticipantName || this.storageSrv.getNickname() || `OpenVidu_User${Math.floor(Math.random() * 100)}`;
const props: ParticipantProperties = {
local: true,
nickname
};
this.participantService.initLocalParticipant(props);
this.openviduService.initialize();
onCamClicked() {}
if (this.deviceSrv.hasVideoDeviceAvailable() || this.deviceSrv.hasAudioDeviceAvailable()) {
await this.initwebcamPublisher();
}
onScreenShareClicked() {}
this.onParticipantCreated.emit(this.participantService.getLocalParticipant());
}
onSpeakerLayoutClicked() {}
private async initwebcamPublisher() {
try {
const publisher = await this.openviduService.initDefaultPublisher(undefined);
if (publisher) {
publisher.once('accessDenied', (e: any) => {
this.handlePublisherError(e);
});
publisher.once('accessAllowed', () => {
this.participantReady = true;
});
}
} catch (error) {
this.actionService.openDialog(error.name.replace(/_/g, ' '), error.message, true);
this.log.e(error);
}
}
ngOnDestroy(): void {
if (this.prejoinSub) this.prejoinSub.unsubscribe();
if (this.participantNameSub) this.participantNameSub.unsubscribe();
}
ngAfterViewInit() {
if (this.externalToolbar) {
this.openviduAngularToolbarTemplate = this.externalToolbar.template;
this.log.d('Setting EXTERNAL TOOLBAR');
} else {
if (this.externalToolbarAdditionalButtons) {
this.log.d('Setting EXTERNAL TOOLBAR ADDITIONAL BUTTONS');
this.openviduAngularToolbarAdditionalButtonsTemplate = this.externalToolbarAdditionalButtons.template;
}
this.openviduAngularToolbarTemplate = this.defaultToolbarTemplate;
this.log.d('Setting DEFAULT TOOLBAR');
}
if (this.externalPanel) {
this.openviduAngularPanelTemplate = this.externalPanel.template;
this.log.d('Setting EXTERNAL PANEL');
} else {
this.log.d('Setting DEFAULT PANEL');
if (this.externalParticipantsPanel) {
this.openviduAngularParticipantsPanelTemplate = this.externalParticipantsPanel.template;
this.log.d('Setting EXTERNAL PARTICIPANTS PANEL');
} else {
this.log.d('Setting DEFAULT PARTICIPANTS PANEL');
if (this.externalParticipantPanelItem) {
this.openviduAngularParticipantPanelItemTemplate = this.externalParticipantPanelItem.template;
this.log.d('Setting EXTERNAL P ITEM');
} else {
if (this.externalParticipantPanelItemElements) {
this.log.d('Setting EXTERNAL PARTICIPANT PANEL ITEM ELEMENT');
this.openviduAngularParticipantPanelItemElementsTemplate = this.externalParticipantPanelItemElements.template;
}
this.openviduAngularParticipantPanelItemTemplate = this.defaultParticipantPanelItemTemplate;
this.log.d('Setting DEFAULT P ITEM');
}
this.openviduAngularParticipantsPanelTemplate = this.defaultParticipantsPanelTemplate;
}
if (this.externalChatPanel) {
this.openviduAngularChatPanelTemplate = this.externalChatPanel.template;
this.log.d('Setting EXTERNAL CHAT PANEL');
} else {
this.openviduAngularChatPanelTemplate = this.defaultChatPanelTemplate;
this.log.d('Setting DEFAULT CHAT PANEL');
}
this.openviduAngularPanelTemplate = this.defaultPanelTemplate;
}
if (this.externalLayout) {
this.openviduAngularLayoutTemplate = this.externalLayout.template;
this.log.d('Setting EXTERNAL LAYOUT');
} else {
this.log.d('Setting DEAFULT LAYOUT');
if (this.externalStream) {
this.openviduAngularStreamTemplate = this.externalStream.template;
this.log.d('Setting EXTERNAL STREAM');
} else {
this.openviduAngularStreamTemplate = this.defaultStreamTemplate;
this.log.d('Setting DEFAULT STREAM');
}
this.openviduAngularLayoutTemplate = this.defaultLayoutTemplate;
}
}
_onJoinButtonClicked() {
this.joinSessionClicked = true;
this.onJoinButtonClicked.emit();
}
onLeaveButtonClicked() {
this.joinSessionClicked = false;
this.participantReady = false;
this.onToolbarLeaveButtonClicked.emit();
}
onCameraButtonClicked() {
this.onToolbarCameraButtonClicked.emit();
}
onMicrophoneButtonClicked() {
this.onToolbarMicrophoneButtonClicked.emit();
}
onScreenshareButtonClicked() {
this.onToolbarScreenshareButtonClicked.emit();
}
onFullscreenButtonClicked() {
this.onToolbarFullscreenButtonClicked.emit();
}
onParticipantsPanelButtonClicked() {
this.onToolbarParticipantsPanelButtonClicked.emit();
}
onChatPanelButtonClicked() {
this.onToolbarChatPanelButtonClicked.emit();
}
_onSessionCreated(event: Session) {
this.onSessionCreated.emit(event);
}
private handlePublisherError(e: any) {
let message: string;
if (e.name === OpenViduErrorName.DEVICE_ALREADY_IN_USE) {
this.log.w('Video device already in use. Disabling video device...');
// Allow access to the room with only mic if camera device is already in use
this.deviceSrv.disableVideoDevices();
return this.initwebcamPublisher();
}
if (e.name === OpenViduErrorName.DEVICE_ACCESS_DENIED) {
message = 'Access to media devices was not allowed.';
this.deviceSrv.disableVideoDevices();
this.deviceSrv.disableAudioDevices();
return this.initwebcamPublisher();
} else if (e.name === OpenViduErrorName.NO_INPUT_SOURCE_SET) {
message = 'No video or audio devices have been found. Please, connect at least one.';
}
this.actionService.openDialog(e.name.replace(/_/g, ' '), message, true);
this.log.e(e.message);
}
private subscribeToVideconferenceDirectives() {
this.prejoinSub = this.libService.prejoin.subscribe((value: boolean) => {
this.showPrejoin = value;
// this.cd.markForCheck();
});
this.participantNameSub = this.libService.participantName.subscribe((nickname: string) => {
this.externalParticipantName = nickname;
});
}
}

View File

@ -1,19 +0,0 @@
import { Type } from '@angular/core';
export interface LibConfig {
environment: {
production: boolean;
useProdLibrary?: boolean;
customComponents?: { LibraryComponents: Type<any> };
};
}
export enum LibraryComponents {
TOOLBAR = 'ov-toolbar',
LAYOUT = 'ov-layout',
PANEL = 'ov-panel',
CHAT_PANEL = 'ov-chat-panel',
PARTICIPANTS_PANEL = 'ov-participants-panel',
STREAM = "ov-stream"
}

View File

@ -0,0 +1,10 @@
import { ParticipantProperties, StreamModel } from '../models/participant.model';
export interface OpenViduAngularConfig {
production?: boolean,
participantFactory?: ParticipantFactoryFunction,
webcomponent?: boolean
}
export type ParticipantFactoryFunction = (props: ParticipantProperties, streamModel: StreamModel) => any;

View File

@ -0,0 +1,66 @@
import { NgModule } from '@angular/core';
import { ParticipantPanelItemMuteButtonDirective } from './participant-panel-item.directive';
import {
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamSettingsButtonDirective
} from './stream.directive';
import {
ToolbarScreenshareButtonDirective,
ToolbarFullscreenButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarDisplaySessionNameDirective,
ToolbarDisplayLogoDirective,
LogoDirective
} from './toolbar.directive';
import {
AudioMutedDirective,
MinimalDirective,
PrejoinDirective,
VideoMutedDirective,
ParticipantNameDirective
} from './videoconference.directive';
@NgModule({
declarations: [
MinimalDirective,
PrejoinDirective,
VideoMutedDirective,
AudioMutedDirective,
ToolbarScreenshareButtonDirective,
ToolbarFullscreenButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarDisplaySessionNameDirective,
ToolbarDisplayLogoDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamSettingsButtonDirective,
LogoDirective,
ParticipantPanelItemMuteButtonDirective,
ParticipantNameDirective
],
exports: [
MinimalDirective,
PrejoinDirective,
VideoMutedDirective,
AudioMutedDirective,
ToolbarScreenshareButtonDirective,
ToolbarFullscreenButtonDirective,
ToolbarLeaveButtonDirective,
ToolbarParticipantsPanelButtonDirective,
ToolbarChatPanelButtonDirective,
ToolbarDisplaySessionNameDirective,
ToolbarDisplayLogoDirective,
StreamDisplayParticipantNameDirective,
StreamDisplayAudioDetectionDirective,
StreamSettingsButtonDirective,
LogoDirective,
ParticipantPanelItemMuteButtonDirective,
ParticipantNameDirective
]
})
export class ApiDirectiveModule {}

View File

@ -0,0 +1,37 @@
import { Directive, AfterViewInit, OnDestroy, Input, ElementRef } from '@angular/core';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
@Directive({
selector: 'ov-videoconference[participantPanelItemMuteButton], ov-participant-panel-item[muteButton]'
})
export class ParticipantPanelItemMuteButtonDirective implements AfterViewInit, OnDestroy {
@Input() set participantPanelItemMuteButton(value: boolean) {
this.muteValue = value;
this.update(this.muteValue);
}
@Input() set muteButton(value: boolean) {
this.muteValue = value;
this.update(this.muteValue);
}
muteValue: boolean = true;
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngAfterViewInit() {
this.update(this.muteValue);
}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.muteValue = true;
this.update(true);
}
update(value: boolean) {
if (this.libService.participantItemMuteButton.getValue() !== value) {
this.libService.participantItemMuteButton.next(value);
}
}
}

View File

@ -0,0 +1,108 @@
import { AfterViewInit, Directive, ElementRef, Input, OnDestroy } from '@angular/core';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
@Directive({
selector: 'ov-videoconference[streamDisplayParticipantName], ov-stream[displayParticipantName]'
})
export class StreamDisplayParticipantNameDirective implements AfterViewInit, OnDestroy {
@Input() set streamDisplayParticipantName(value: boolean) {
this.displayParticipantNameValue = value;
this.update(this.displayParticipantNameValue);
}
@Input() set displayParticipantName(value: boolean) {
this.displayParticipantNameValue = value;
this.update(this.displayParticipantNameValue);
}
displayParticipantNameValue: boolean;
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngOnDestroy(): void {
this.clear();
}
ngAfterViewInit() {
this.update(this.displayParticipantNameValue);
}
update(value: boolean) {
if (this.libService.displayParticipantName.getValue() !== value) {
this.libService.displayParticipantName.next(value);
}
}
clear() {
this.update(true);
}
}
@Directive({
selector: 'ov-videoconference[streamDisplayAudioDetection], ov-stream[displayAudioDetection]'
})
export class StreamDisplayAudioDetectionDirective implements AfterViewInit, OnDestroy {
@Input() set streamDisplayAudioDetection(value: boolean) {
this.displayAudioDetectionValue = value;
this.update(this.displayAudioDetectionValue);
}
@Input() set displayAudioDetection(value: boolean) {
this.displayAudioDetectionValue = value;
this.update(this.displayAudioDetectionValue);
}
displayAudioDetectionValue: boolean;
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngAfterViewInit() {
this.update(this.displayAudioDetectionValue);
}
ngOnDestroy(): void {
this.clear();
}
update(value: boolean) {
if (this.libService.displayAudioDetection.getValue() !== value) {
this.libService.displayAudioDetection.next(value);
}
}
clear() {
this.update(true);
}
}
@Directive({
selector: 'ov-videoconference[streamSettingsButton], ov-stream[settingsButton]'
})
export class StreamSettingsButtonDirective implements AfterViewInit, OnDestroy {
@Input() set streamSettingsButton(value: boolean) {
this.settingsValue = value;
this.update(this.settingsValue);
}
@Input() set settingsButton(value: boolean) {
this.settingsValue = value;
this.update(this.settingsValue);
}
settingsValue: boolean;
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngAfterViewInit() {
this.update(this.settingsValue);
}
ngOnDestroy(): void {
this.clear();
}
update(value: boolean) {
if (this.libService.settingsButton.getValue() !== value) {
this.libService.settingsButton.next(value);
}
}
clear() {
this.update(true);
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,112 @@
import { Directive, Input, ElementRef, OnDestroy, OnInit } from '@angular/core';
import { OpenViduAngularConfigService } from '../../services/config/openvidu-angular.config.service';
@Directive({
selector: 'ov-videoconference[minimal]'
})
export class MinimalDirective implements OnDestroy {
@Input() set minimal(value: boolean) {
this.update(value);
}
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.update(false);
}
update(value: boolean) {
if (this.libService.minimal.getValue() !== value) {
this.libService.minimal.next(value);
}
}
}
@Directive({
selector: 'ov-videoconference[participantName]'
})
export class ParticipantNameDirective implements OnInit {
// Avoiding update participantName dynamically.
// The participantName must be updated from UI
@Input() participantName: string;
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngOnInit(): void {
this.update(this.participantName);
}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.update('');
}
update(value: string) {
this.libService.participantName.next(value);
}
}
@Directive({
selector: 'ov-videoconference[prejoin]'
})
export class PrejoinDirective implements OnDestroy {
@Input() set prejoin(value: boolean) {
this.update(value);
}
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.update(true);
}
update(value: boolean) {
if (this.libService.prejoin.getValue() !== value) {
this.libService.prejoin.next(value);
}
}
}
@Directive({
selector: 'ov-videoconference[videoMuted]'
})
export class VideoMutedDirective implements OnDestroy {
@Input() set videoMuted(value: boolean) {
this.update(value);
}
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.update(false);
}
update(value: boolean) {
if (this.libService.videoMuted.getValue() !== value) {
this.libService.videoMuted.next(value);
}
}
}
@Directive({
selector: 'ov-videoconference[audioMuted]'
})
export class AudioMutedDirective implements OnDestroy {
@Input() set audioMuted(value: boolean) {
this.update(value);
}
constructor(public elementRef: ElementRef, private libService: OpenViduAngularConfigService) {}
ngOnDestroy(): void {
this.clear();
}
clear() {
this.update(false);
}
update(value: boolean) {
if (this.libService.audioMuted.getValue() !== value) {
this.libService.audioMuted.next(value);
}
}
}

View File

@ -0,0 +1,38 @@
import { NgModule } from '@angular/core';
import {
ChatPanelDirective,
LayoutDirective,
PanelDirective,
ParticipantPanelItemElementsDirective,
ParticipantPanelItemDirective,
ParticipantsPanelDirective,
StreamDirective,
ToolbarAdditionalButtonsDirective,
ToolbarDirective
} from './openvidu-angular.directive';
@NgModule({
declarations: [
ChatPanelDirective,
LayoutDirective,
PanelDirective,
ParticipantPanelItemDirective,
ParticipantsPanelDirective,
StreamDirective,
ToolbarDirective,
ToolbarAdditionalButtonsDirective,
ParticipantPanelItemElementsDirective
],
exports: [
ChatPanelDirective,
LayoutDirective,
PanelDirective,
ParticipantPanelItemDirective,
ParticipantsPanelDirective,
StreamDirective,
ToolbarDirective,
ToolbarAdditionalButtonsDirective,
ParticipantPanelItemElementsDirective
]
})
export class OpenViduAngularDirectiveModule {}

View File

@ -0,0 +1,65 @@
import { Directive, TemplateRef, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[ovToolbar]'
})
export class ToolbarDirective {
constructor(public template: TemplateRef<any>, public viewContainer: ViewContainerRef) {}
}
@Directive({
selector: '[ovToolbarAdditionalButtons]'
})
export class ToolbarAdditionalButtonsDirective {
constructor(public template: TemplateRef<any>, public viewContainer: ViewContainerRef) {}
}
@Directive({
selector: '[ovPanel]'
})
export class PanelDirective {
constructor(public template: TemplateRef<any>, public viewContainer: ViewContainerRef) {}
}
@Directive({
selector: '[ovChatPanel]'
})
export class ChatPanelDirective {
constructor(public template: TemplateRef<any>, public viewContainer: ViewContainerRef) {}
}
@Directive({
selector: '[ovParticipantsPanel]'
})
export class ParticipantsPanelDirective {
constructor(public template: TemplateRef<any>, public viewContainer: ViewContainerRef) {}
}
@Directive({
selector: '[ovParticipantPanelItem]'
})
export class ParticipantPanelItemDirective {
constructor(public template: TemplateRef<any>, public viewContainer: ViewContainerRef) {}
}
@Directive({
selector: '[ovParticipantPanelItemElements]'
})
export class ParticipantPanelItemElementsDirective {
constructor(public template: TemplateRef<any>, public viewContainer: ViewContainerRef) {}
}
@Directive({
selector: '[ovLayout]'
})
export class LayoutDirective {
constructor(public template: TemplateRef<any>, public container: ViewContainerRef) {}
}
@Directive({
selector: '[ovStream]'
})
export class StreamDirective {
constructor(public template: TemplateRef<any>, public container: ViewContainerRef) {}
}

View File

@ -2,7 +2,8 @@ export enum LayoutClass {
ROOT_ELEMENT = 'OT_root',
BIG_ELEMENT = 'OV_big',
SMALL_ELEMENT = 'OV_small',
SIDENAV_CONTAINER = 'sidenav-container'
SIDENAV_CONTAINER = 'sidenav-container',
NO_SIZE_ELEMENT = 'no-size'
}
export enum SidenavMode {
@ -140,7 +141,7 @@ export class OpenViduLayout {
}
getLayoutContainer(): HTMLElement {
return this.layoutContainer
return this.layoutContainer;
}
/**

View File

@ -2,40 +2,58 @@ import { Publisher, StreamManager } from 'openvidu-browser';
import { VideoType } from './video-type.model';
export interface StreamModel {
local: boolean;
connected: boolean;
nickname: string;
type: VideoType;
streamManager: StreamManager;
videoEnlarged: boolean;
connectionId: string;
participant?: ParticipantAbstractModel
}
export interface ParticipantProperties {
local: boolean;
nickname: string;
id?: string;
colorProfile?: string;
isMutedForcibly?: boolean;
}
export abstract class ParticipantAbstractModel {
connections: Map<VideoType, StreamModel> = new Map();
streams: Map<VideoType, StreamModel> = new Map();
id: string;
local: boolean;
nickname: string;
colorProfile: string;
isMutedForcibly: boolean;
constructor(model?: StreamModel, id?: string) {
constructor(props: ParticipantProperties, model?: StreamModel) {
this.id = props.id ? props.id : new Date().getTime().toString();
this.local = props.local;
this.nickname = props.nickname;
this.colorProfile = !!props.colorProfile ? props.colorProfile : `hsl(${Math.random()*360}, 100%, 80%)`;
this.isMutedForcibly = typeof props.isMutedForcibly === 'boolean' ? props.isMutedForcibly : false;
let streamModel: StreamModel = {
local: model ? model.local : true,
connected: true,
nickname: model ? model.nickname : 'OpenVidu_User',
connected: model ? model.connected : true,
type: model ? model.type : VideoType.CAMERA,
streamManager: model ? model.streamManager : null,
videoEnlarged: model ? model.videoEnlarged : false,
connectionId: model ? model.connectionId : null
connectionId: model ? model.connectionId : null,
participant: this
};
this.connections.set(streamModel.type, streamModel);
this.id = id ? id : new Date().getTime().toString();
this.streams.set(streamModel.type, streamModel);
}
addConnection(streamModel: StreamModel) {
this.connections.set(streamModel.type, streamModel);
streamModel.participant = this;
this.streams.set(streamModel.type, streamModel);
}
public isCameraAudioActive(): boolean {
const cameraConnection = this.getCameraConnection();
return cameraConnection.connected && cameraConnection.streamManager.stream.audioActive;
if(cameraConnection) {
return cameraConnection.connected && cameraConnection.streamManager?.stream?.audioActive;
}
return this.isScreenAudioActive();;
}
public isCameraVideoActive(): boolean {
@ -44,25 +62,28 @@ export abstract class ParticipantAbstractModel {
}
isScreenAudioActive(): boolean {
const screenConnection = this.getScreenConnection();
return screenConnection?.connected && screenConnection?.streamManager?.stream?.audioActive;
if(screenConnection){
return screenConnection?.connected && screenConnection?.streamManager?.stream?.audioActive;
}
return false;
}
hasConnectionType(type: VideoType): boolean {
return this.connections.has(type);
return this.streams.has(type);
}
public getCameraConnection(): StreamModel {
return this.connections.get(VideoType.CAMERA);
return this.streams.get(VideoType.CAMERA);
}
public getScreenConnection(): StreamModel {
return this.connections.get(VideoType.SCREEN);
return this.streams.get(VideoType.SCREEN);
}
getConnectionTypesEnabled(): VideoType[] {
getConnectionTypesActive(): VideoType[] {
let connType = [];
if (this.isCameraEnabled()) connType.push(VideoType.CAMERA);
if (this.isScreenEnabled()) connType.push(VideoType.SCREEN);
if (this.isCameraActive()) connType.push(VideoType.CAMERA);
if (this.isScreenActive()) connType.push(VideoType.SCREEN);
return connType;
}
@ -74,43 +95,51 @@ export abstract class ParticipantAbstractModel {
this.getScreenConnection().connectionId = connectionId;
}
removeConnection(connectionId: string) {
this.connections.delete(this.getConnectionById(connectionId).type);
removeConnection(connectionId: string): StreamModel {
const removeStream = this.getConnectionById(connectionId);
this.streams.delete(removeStream.type);
return removeStream;
}
hasConnectionId(connectionId: string): boolean {
return Array.from(this.connections.values()).some((conn) => conn.connectionId === connectionId);
return Array.from(this.streams.values()).some((conn) => conn.connectionId === connectionId);
}
getConnectionById(connectionId: string): StreamModel {
return Array.from(this.connections.values()).find((conn) => conn.connectionId === connectionId);
return Array.from(this.streams.values()).find((conn) => conn.connectionId === connectionId);
}
getAvailableConnections(): StreamModel[] {
return Array.from(this.connections.values()).filter((conn) => conn.connected);
return Array.from(this.streams.values()).filter((conn) => conn.connected);
}
isLocal(): boolean {
return Array.from(this.connections.values()).every((conn) => conn.local);
return this.local;
// return Array.from(this.streams.values()).every((conn) => conn.local);
}
setNickname(nickname: string) {
this.connections.forEach((conn) => {
if (conn.type === VideoType.CAMERA) {
conn.nickname = nickname;
} else {
conn.nickname = `${nickname}_${conn.type}`;
}
});
this.nickname = nickname;
// this.streams.forEach((conn) => {
// if (conn.type === VideoType.CAMERA) {
// conn.nickname = nickname;
// } else {
// conn.nickname = `${nickname}_${conn.type}`;
// }
// });
}
getCameraNickname(): string {
return this.getCameraConnection()?.nickname;
getNickname() {
return this.nickname;
}
getScreenNickname(): string {
return this.getScreenConnection()?.nickname;
}
// getCameraNickname(): string {
// return this.getCameraConnection()?.nickname;
// }
// getScreenNickname(): string {
// return this.getScreenConnection()?.nickname;
// }
setCameraPublisher(publisher: Publisher) {
const cameraConnection = this.getCameraConnection();
@ -123,13 +152,13 @@ export abstract class ParticipantAbstractModel {
}
setPublisher(connType: VideoType, publisher: StreamManager) {
const connection = this.connections.get(connType);
const connection = this.streams.get(connType);
if(connection) {
connection.streamManager = publisher;
}
}
isCameraEnabled(): boolean {
isCameraActive(): boolean {
return this.getCameraConnection()?.connected;
}
@ -143,11 +172,11 @@ export abstract class ParticipantAbstractModel {
if (cameraConnection) cameraConnection.connected = false;
}
isScreenEnabled(): boolean {
isScreenActive(): boolean {
return this.getScreenConnection()?.connected;
}
enablescreen() {
enableScreen() {
const screenConnection = this.getScreenConnection();
if (screenConnection) screenConnection.connected = true;
}
@ -156,12 +185,21 @@ export abstract class ParticipantAbstractModel {
const screenConnection = this.getScreenConnection();
if (screenConnection) screenConnection.connected = false;
}
setAllVideoEnlarged(enlarged: boolean) {
this.connections.forEach((conn) => (conn.videoEnlarged = enlarged));
this.streams.forEach((conn) => (conn.videoEnlarged = enlarged));
}
setCameraEnlarged(enlarged: boolean) {
this.streams.get(VideoType.CAMERA).videoEnlarged = enlarged;
}
setScreenEnlarged(enlarged: boolean) {
this.streams.get(VideoType.SCREEN).videoEnlarged = enlarged;
}
toggleVideoEnlarged(connectionId: string) {
this.connections.forEach((conn) => {
this.streams.forEach((conn) => {
if (conn.connectionId === connectionId) {
conn.videoEnlarged = !conn.videoEnlarged;
}
@ -169,7 +207,11 @@ export abstract class ParticipantAbstractModel {
}
someHasVideoEnlarged(): boolean {
return Array.from(this.connections.values()).some((conn) => conn.videoEnlarged);
return Array.from(this.streams.values()).some((conn) => conn.videoEnlarged);
}
setMutedForcibly(muted: boolean){
this.isMutedForcibly = muted;
}
}

View File

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

View File

@ -26,7 +26,7 @@ import { CommonModule } from '@angular/common';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { UserSettingsComponent } from './components/user-settings/user-settings.component';
// import { UserSettingsComponent } from './components/user-settings/user-settings.component';
import { ToolbarComponent } from './components/toolbar/toolbar.component';
import { VideoComponent } from './components/video/video.component';
import { ChatPanelComponent } from './components/panel/chat-panel/chat-panel.component';
@ -36,36 +36,37 @@ import { StreamComponent } from './components/stream/stream.component';
import { DialogTemplateComponent } from './components/material/dialog.component';
import { LinkifyPipe } from './pipes/linkify.pipe';
import { TooltipListPipe } from './pipes/tooltip-list.pipe';
import { ConnectionsEnabledPipe, NicknamePipe, ParticipantConnectionsPipe } from './pipes/participant-connections.pipe';
import { StreamTypesEnabledPipe, ParticipantStreamsPipe } from './pipes/participant.pipe';
import { LibConfig } from './config/lib.config';
import { OpenViduAngularConfig } from './config/openvidu-angular.config';
import { CdkOverlayContainer } from './config/custom-cdk-overlay';
import { DeviceService } from './services/device/device.service';
import { LoggerService } from './services/logger/logger.service';
import { PlatformService } from './services/platform/platform.service';
import { StorageService } from './services/storage/storage.service';
import { TokenService } from './services/token/token.service';
import { LibraryConfigService } from './services/library-config/library-config.service';
import { WebrtcService } from './services/webrtc/webrtc.service';
import { OpenViduAngularConfigService } from './services/config/openvidu-angular.config.service';
import { OpenViduService } from './services/openvidu/openvidu.service';
import { ActionService } from './services/action/action.service';
import { ChatService } from './services/chat/chat.service';
import { DocumentService } from './services/document/document.service';
import { LayoutService } from './services/layout/layout.service';
import { SidenavMenuService } from './services/sidenav-menu/sidenav-menu.service';
import { ParticipantService } from './services/participant/participant.service';
import { ParticipantItemComponent } from './components/panel/participants-panel/participant-item/participant-item.component';
import { ParticipantPanelItemComponent } from './components/panel/participants-panel/participant-panel-item/participant-panel-item.component';
import { ParticipantsPanelComponent } from './components/panel/participants-panel/participants-panel/participants-panel.component';
import { VideoconferenceComponent } from './components/videoconference/videoconference.component';
import { PanelComponent } from './components/panel/panel.component';
import { AudioWaveComponent } from './components/audio-wave/audio-wave.component';
import { PreJoinComponent } from './components/pre-join/pre-join.component';
// Used for loading dynamic components because of Inputs and Outputs are not supported in the official Angular way
// https://github.com/angular/angular/issues/15360
import { DynamicIoModule } from 'ng-dynamic-component';
import { AvatarProfileComponent } from './components/avatar-profile/avatar-profile.component';
import { OpenViduAngularDirectiveModule } from './directives/template/openvidu-angular.directive.module';
import { ApiDirectiveModule } from './directives/api/api.directive.module';
@NgModule({
declarations: [
UserSettingsComponent,
// UserSettingsComponent,
VideoComponent,
ToolbarComponent,
ChatPanelComponent,
@ -74,14 +75,15 @@ import { DynamicIoModule } from 'ng-dynamic-component';
StreamComponent,
DialogTemplateComponent,
LinkifyPipe,
TooltipListPipe,
ParticipantConnectionsPipe,
ConnectionsEnabledPipe,
NicknamePipe,
ParticipantItemComponent,
ParticipantStreamsPipe,
StreamTypesEnabledPipe,
ParticipantPanelItemComponent,
ParticipantsPanelComponent,
VideoconferenceComponent,
PanelComponent,
AudioWaveComponent,
PanelComponent,
AvatarProfileComponent,
PreJoinComponent,
],
imports: [
CommonModule,
@ -109,7 +111,8 @@ import { DynamicIoModule } from 'ng-dynamic-component';
MatMenuModule,
MatDividerModule,
MatListModule,
DynamicIoModule
OpenViduAngularDirectiveModule,
ApiDirectiveModule
],
providers: [
ActionService,
@ -125,29 +128,37 @@ import { DynamicIoModule } from 'ng-dynamic-component';
ParticipantService,
StorageService,
TokenService,
WebrtcService
OpenViduService
],
exports: [
VideoconferenceComponent,
UserSettingsComponent,
// UserSettingsComponent,
ToolbarComponent,
PanelComponent,
ParticipantsPanelComponent,
ParticipantPanelItemComponent,
ChatPanelComponent,
SessionComponent,
LayoutComponent,
StreamComponent,
VideoComponent,
ParticipantConnectionsPipe,
CommonModule
AudioWaveComponent,
PreJoinComponent,
ParticipantStreamsPipe,
StreamTypesEnabledPipe,
CommonModule,
OpenViduAngularDirectiveModule,
ApiDirectiveModule
],
entryComponents: [DialogTemplateComponent]
})
export class OpenviduAngularModule {
static forRoot(environment): ModuleWithProviders<OpenviduAngularModule> {
export class OpenViduAngularModule {
static forRoot(config): ModuleWithProviders<OpenViduAngularModule> {
// console.log(`${library.name} config: ${environment}`);
const libConfig: LibConfig = { environment };
const libConfig: OpenViduAngularConfig = config;
return {
ngModule: OpenviduAngularModule,
providers: [LibraryConfigService, { provide: 'LIB_CONFIG', useValue: libConfig }]
ngModule: OpenViduAngularModule,
providers: [OpenViduAngularConfigService, { provide: 'OPENVIDU_ANGULAR_CONFIG', useValue: libConfig }]
};
}
}

View File

@ -1,36 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
import { StreamModel, ParticipantAbstractModel } from '../models/participant.model';
@Pipe({ name: 'connections' })
export class ParticipantConnectionsPipe implements PipeTransform {
constructor() {}
transform(participants: ParticipantAbstractModel[] | ParticipantAbstractModel): StreamModel[] {
let connections: StreamModel[] = [];
if (Array.isArray(participants)) {
participants.forEach((p) => {
connections = connections.concat(Array.from(p.connections.values()));
});
} else {
connections = Array.from(participants.connections.values());
}
return connections;
}
}
@Pipe({ name: 'connectionsEnabled', pure: false })
export class ConnectionsEnabledPipe implements PipeTransform {
constructor() {}
transform(participant: ParticipantAbstractModel): string {
return `(${participant.getConnectionTypesEnabled().toString().replace(',', ', ')})`;
}
}
@Pipe({ name: 'nickname', pure: false })
export class NicknamePipe implements PipeTransform {
constructor() {}
transform(participant: ParticipantAbstractModel): string {
return participant.getCameraNickname();
}
}

View File

@ -0,0 +1,32 @@
import { Pipe, PipeTransform } from '@angular/core';
import { StreamModel, ParticipantAbstractModel } from '../models/participant.model';
@Pipe({ name: 'streams' })
export class ParticipantStreamsPipe implements PipeTransform {
constructor() {}
transform(participants: ParticipantAbstractModel[] | ParticipantAbstractModel): StreamModel[] {
let streams: StreamModel[] = [];
if(participants && Object.keys(participants).length > 0){
if (Array.isArray(participants)) {
participants.forEach((p) => {
streams = streams.concat(p.getAvailableConnections());
});
} else {
streams = participants.getAvailableConnections();
}
}
return streams;
}
}
@Pipe({ name: 'streamTypesEnabled' })
export class StreamTypesEnabledPipe implements PipeTransform {
constructor() {}
transform(participant: ParticipantAbstractModel): string {
const activeStreams = participant?.getConnectionTypesActive().toString();
return `(${activeStreams.replace(',', ', ')})`;
}
}

View File

@ -1,82 +0,0 @@
// import { Pipe, PipeTransform } from '@angular/core';
// import { SettingsModel } from '../models/settings.model';
// @Pipe({ name: 'hasChat', pure: true })
// export class HasChatPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.hasChat();
// }
// }
// @Pipe({ name: 'hasAudio', pure: true })
// export class HasAudioPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.hasAudio();
// }
// }
// @Pipe({ name: 'hasVideo', pure: true })
// export class HasVideoPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.hasVideo();
// }
// }
// @Pipe({ name: 'isAutoPublish', pure: true })
// export class IsAutoPublishPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.isAutoPublish();
// }
// }
// @Pipe({ name: 'hasScreenSharing', pure: true })
// export class HasScreenSharingPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.hasScreenSharing();
// }
// }
// @Pipe({ name: 'hasFullscreen', pure: true })
// export class HasFullscreenPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.hasFullscreen();
// }
// }
// @Pipe({ name: 'hasLayoutSpeaking', pure: true })
// export class HasLayoutSpeakingPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.hasLayoutSpeaking();
// }
// }
// @Pipe({ name: 'hasExit', pure: true })
// export class HasExitPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.hasExit();
// }
// }
// @Pipe({ name: 'hasToolbar', pure: true })
// export class HasToolbarPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.hasToolbar();
// }
// }
// @Pipe({ name: 'hasFooter', pure: true })
// export class HasFooterPipe implements PipeTransform {
// constructor() {}
// transform(ovSettings: SettingsModel): boolean {
// return !ovSettings || ovSettings.hasFooter();
// }
// }

View File

@ -1,13 +0,0 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'tooltipList', pure: true })
export class TooltipListPipe implements PipeTransform {
transform(lines: string[]): string {
let list = '';
lines.forEach((line) => {
list += '• ' + line + '\n';
});
return list;
}
}

View File

@ -7,7 +7,7 @@ import { ChatMessage } from '../../models/chat.model';
import { INotificationOptions } from '../../models/notification-options.model';
import { ActionService } from '../action/action.service';
import { WebrtcService } from '../webrtc/webrtc.service';
import { OpenViduService } from '../openvidu/openvidu.service';
import { LoggerService } from '../logger/logger.service';
import { Signal } from '../../models/signal.model';
import { SidenavMenuService } from '../sidenav-menu/sidenav-menu.service';
@ -19,27 +19,29 @@ import { MenuType } from '../../models/menu.model';
})
export class ChatService {
messagesObs: Observable<ChatMessage[]>;
private messageSound: HTMLAudioElement;
protected _messageList = <BehaviorSubject<ChatMessage[]>>new BehaviorSubject([]);
protected messageList: ChatMessage[] = [];
protected log: ILogger;
constructor(
protected loggerSrv: LoggerService,
protected webrtcService: WebrtcService,
protected openviduService: OpenViduService,
protected participantService: ParticipantService,
protected menuService: SidenavMenuService,
protected actionService: ActionService
) {
this.log = this.loggerSrv.get('ChatService');
this.messagesObs = this._messageList.asObservable();
this.messageSound = new Audio('assets/audio/message_sound.mp3');
this.messageSound.volume = 0.5;
}
subscribeToChat() {
const session = this.webrtcService.getWebcamSession();
const session = this.openviduService.getWebcamSession();
session.on(`signal:${Signal.CHAT}`, (event: any) => {
const connectionId = event.from.connectionId;
const data = JSON.parse(event.data);
const isMyOwnConnection = this.webrtcService.isMyOwnConnection(connectionId);
const isMyOwnConnection = this.openviduService.isMyOwnConnection(connectionId);
this.messageList.push({
isLocal: isMyOwnConnection,
nickname: data.nickname,
@ -52,6 +54,8 @@ export class ChatService {
buttonActionText: 'READ'
};
this.launchNotification(notificationOptions);
this.messageSound.play().catch(() => {});
}
this._messageList.next(this.messageList);
});
@ -62,10 +66,10 @@ export class ChatService {
if (message !== '' && message !== ' ') {
const data = {
message: message,
nickname: this.participantService.getWebcamNickname()
nickname: this.participantService.getMyNickname()
};
this.webrtcService.sendSignal(Signal.CHAT, undefined, data);
this.openviduService.sendSignal(Signal.CHAT, undefined, data);
}
}

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { OpenViduAngularConfig } from '../../config/openvidu-angular.config';
@Injectable()
export class OpenViduAngularConfigServiceMock {
private configuration: OpenViduAngularConfig;
constructor() {
this.configuration = {production: false};
}
getConfig(): OpenViduAngularConfig {
return this.configuration;
}
isProduction(): boolean {
return this.configuration?.production;
}
}

View File

@ -0,0 +1,22 @@
import { TestBed } from '@angular/core/testing';
import { OpenViduAngularConfig } from '../../config/openvidu-angular.config';
import { OpenViduAngularConfigService } from './openvidu-angular.config.service';
describe('OpenViduAngularConfigService', () => {
let service: OpenViduAngularConfigService;
const config: OpenViduAngularConfig = { production: false };
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
OpenViduAngularConfigService,
{provide: 'LIB_CONFIG', useValue: config}]
});
service = TestBed.inject(OpenViduAngularConfigService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,94 @@
import { Inject, Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { OpenViduAngularConfig, ParticipantFactoryFunction } from '../../config/openvidu-angular.config';
// import { version } from '../../../../package.json';
@Injectable()
export class OpenViduAngularConfigService {
private configuration: OpenViduAngularConfig;
minimal = <BehaviorSubject<boolean>>new BehaviorSubject(false);
minimalObs: Observable<boolean>;
participantName = <BehaviorSubject<string>>new BehaviorSubject('');
participantNameObs: Observable<string>;
prejoin = <BehaviorSubject<boolean>>new BehaviorSubject(true);
prejoinObs: Observable<boolean>;
videoMuted = <BehaviorSubject<boolean>>new BehaviorSubject(false);
videoMutedObs: Observable<boolean>;
audioMuted = <BehaviorSubject<boolean>>new BehaviorSubject(false);
audioMutedObs: Observable<boolean>;
screenshareButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
screenshareButtonObs: Observable<boolean>;
fullscreenButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
fullscreenButtonObs: Observable<boolean>;
leaveButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
leaveButtonObs: Observable<boolean>;
participantsPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
participantsPanelButtonObs: Observable<boolean>;
chatPanelButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
chatPanelButtonObs: Observable<boolean>;
displaySessionName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displaySessionNameObs: Observable<boolean>;
displayLogo = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayLogoObs: Observable<boolean>;
displayParticipantName = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayParticipantNameObs: Observable<boolean>;
displayAudioDetection = <BehaviorSubject<boolean>>new BehaviorSubject(true);
displayAudioDetectionObs: Observable<boolean>;
settingsButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
settingsButtonObs: Observable<boolean>;
participantItemMuteButton = <BehaviorSubject<boolean>>new BehaviorSubject(true);
participantItemMuteButtonObs: Observable<boolean>;
constructor(@Inject('OPENVIDU_ANGULAR_CONFIG') config: OpenViduAngularConfig) {
this.configuration = config;
console.log(this.configuration);
if(this.isProduction()) console.log('OpenVidu Angular Production Mode');
this.minimalObs = this.minimal.asObservable();
this.participantNameObs = this.participantName.asObservable();
this.prejoinObs = this.prejoin.asObservable();
this.videoMutedObs = this.videoMuted.asObservable();
this.audioMutedObs = this.audioMuted.asObservable();
//Toolbar observables
this.screenshareButtonObs = this.screenshareButton.asObservable();
this.fullscreenButtonObs = this.fullscreenButton.asObservable();
this.leaveButtonObs = this.leaveButton.asObservable();
this.participantsPanelButtonObs = this.participantsPanelButton.asObservable();
this.chatPanelButtonObs = this.chatPanelButton.asObservable();
this.displaySessionNameObs = this.displaySessionName.asObservable();
this.displayLogoObs = this.displayLogo.asObservable();
//Stream observables
this.displayParticipantNameObs = this.displayParticipantName.asObservable();
this.displayAudioDetectionObs = this.displayAudioDetection.asObservable();
this.settingsButtonObs = this.settingsButton.asObservable();
// Participant item observables
this.participantItemMuteButtonObs = this.participantItemMuteButton.asObservable();
}
getConfig(): OpenViduAngularConfig {
return this.configuration;
}
isProduction(): boolean {
return this.configuration?.production;
}
// isWebcomponent(): boolean {
// return this.configuration?.webcomponent;
// }
hasParticipantFactory(): boolean {
return typeof this.getConfig().participantFactory === "function";
}
getParticipantFactory(): ParticipantFactoryFunction {
return this.getConfig().participantFactory;
}
}

View File

@ -1,9 +1,9 @@
import { Injectable } from '@angular/core';
import { Device, OpenVidu } from 'openvidu-browser';
import { Device, OpenVidu, OpenViduError, OpenViduErrorName } from 'openvidu-browser';
import { CameraType, DeviceType, CustomDevice } from '../../models/device.model';
import { ILogger } from '../../models/logger.model';
import { Storage } from '../../models/storage.model';
import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service';
import { LoggerService } from '../logger/logger.service';
import { PlatformService } from '../platform/platform.service';
@ -23,7 +23,18 @@ export class DeviceService {
private videoDevicesDisabled: boolean;
private audioDevicesDisabled: boolean;
constructor(private loggerSrv: LoggerService, private platformSrv: PlatformService, private storageSrv: StorageService) {
// Initialized with Storage.VIDEO_MUTED info saved on storage
private _isVideoMuted: boolean;
// Initialized with Storage.AUDIO_MUTED info saved on storage
private _isAudioMuted: boolean;
private deviceAccessDeniedError: boolean = false;
constructor(
private loggerSrv: LoggerService,
private platformSrv: PlatformService,
private storageSrv: StorageService,
private libSrv: OpenViduAngularConfigService
) {
this.log = this.loggerSrv.get('DevicesService');
this.OV = new OpenVidu();
}
@ -33,44 +44,57 @@ export class DeviceService {
}
async initializeDevices() {
// Requesting media permissions. Sometimes, browser doens't launch the media permissions modal.
const mediaStream = await this.OV.getUserMedia({audioSource: undefined, videoSource: undefined });
try {
// Forcing media permissions request.
// 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());
} catch (error) {
this.deviceAccessDeniedError = (<OpenViduError>error).name === OpenViduErrorName.DEVICE_ACCESS_DENIED;
}
this.devices = await this.OV.getDevices();
const customDevices = this.initializeCustomDevices(this.devices);
this.cameras = customDevices.cameras;
this.microphones = customDevices.microphones;
mediaStream?.getAudioTracks().forEach((track) => track.stop());
mediaStream?.getVideoTracks().forEach((track) => track.stop());
this._isVideoMuted = this.storageSrv.isVideoMuted() || this.libSrv.videoMuted.getValue();
this._isAudioMuted = this.storageSrv.isAudioMuted() || this.libSrv.audioMuted.getValue();;
this.log.d('Media devices',customDevices);
this.log.d('Media devices', customDevices);
}
private initializeCustomDevices(defaultVDevices: Device[]) {
const FIRST_POSITION = 0;
const defaultMicrophones = defaultVDevices.filter((device) => device.kind === DeviceType.AUDIO_INPUT);
const defaultCameras = defaultVDevices.filter((device) => device.kind === DeviceType.VIDEO_INPUT);
const defaultMicrophones: Device[] = defaultVDevices.filter((device) => device.kind === DeviceType.AUDIO_INPUT);
const defaultCameras: Device[] = defaultVDevices.filter((device) => device.kind === DeviceType.VIDEO_INPUT);
const customDevices: { cameras: CustomDevice[]; microphones: CustomDevice[] } = {
cameras: [{ label: 'None', device: null, type: null }],
microphones: [{ label: 'None', device: null, type: null }]
cameras: [],
microphones: []
};
if (this.hasAudioDeviceAvailable) {
if (defaultMicrophones.length > 0) {
defaultMicrophones.forEach((device: Device) => {
customDevices.microphones.push({ label: device.label, device: device.deviceId });
});
// Settings microphone selected
// Setting microphone selected
const storageMicrophone = this.getMicrophoneFromStogare();
if (!!storageMicrophone) {
this.microphoneSelected = storageMicrophone;
} else if (customDevices.microphones.length > 0) {
this.microphoneSelected = customDevices.microphones.find((d) => d.label !== 'None');
if (this.deviceAccessDeniedError && customDevices.microphones.length > 1) {
// We assume that the default device is already in use
// Assign an alternative device with the aim of avoiding the DEVICE_ALREADY_IN_USE error
this.microphoneSelected = customDevices.microphones[1];
} else {
this.microphoneSelected = customDevices.microphones[0];
}
}
}
if (this.hasVideoDeviceAvailable) {
if (defaultCameras.length > 0) {
defaultCameras.forEach((device: Device, index: number) => {
const myDevice: CustomDevice = {
label: device.label,
@ -96,12 +120,26 @@ export class DeviceService {
if (!!storageCamera) {
this.cameraSelected = storageCamera;
} else if (customDevices.cameras.length > 0) {
this.cameraSelected = customDevices.cameras.find((d) => d.label !== 'None');
if (this.deviceAccessDeniedError && customDevices.cameras.length > 1) {
// We assume that the default device is already in use
// Assign an alternative device with the aim of avoiding the DEVICE_ALREADY_IN_USE error
this.cameraSelected = customDevices.cameras[1];
} else {
this.cameraSelected = customDevices.cameras[0];
}
}
}
return customDevices;
}
isVideoMuted(): boolean {
return this.hasVideoDeviceAvailable() && this._isVideoMuted;
}
isAudioMuted(): boolean {
return this.hasAudioDeviceAvailable() && this._isAudioMuted;
}
getCameraSelected(): CustomDevice {
return this.cameraSelected;
}
@ -137,11 +175,11 @@ export class DeviceService {
}
hasVideoDeviceAvailable(): boolean {
return !this.videoDevicesDisabled && !this.cameras.every((device) => device.label === 'None');
return !this.videoDevicesDisabled && this.cameras.length > 0;
}
hasAudioDeviceAvailable(): boolean {
return !this.audioDevicesDisabled && !this.microphones.every((device) => device.label === 'None');
return !this.audioDevicesDisabled && this.microphones.length > 0;
}
cameraNeedsMirror(deviceField: string): boolean {
@ -161,7 +199,7 @@ export class DeviceService {
}
clear() {
this.OV = new OpenVidu();
// this.OV = new OpenVidu();
this.devices = [];
this.cameras = [];
this.microphones = [];
@ -180,24 +218,24 @@ export class DeviceService {
}
private getMicrophoneFromStogare(): CustomDevice {
let storageDevice = this.storageSrv.get(Storage.AUDIO_DEVICE);
if (!!storageDevice) {
const storageDevice: CustomDevice = this.storageSrv.getAudioDevice();
if (!!storageDevice && this.microphones.some((device) => device.device === storageDevice.device)) {
return storageDevice;
}
}
private getCameraFromStorage() {
let storageDevice = this.storageSrv.get(Storage.VIDEO_DEVICE);
if (!!storageDevice) {
const storageDevice: CustomDevice = this.storageSrv.getVideoDevice();
if (!!storageDevice && this.cameras.some((device) => device.device === storageDevice.device)) {
return storageDevice;
}
}
private saveCameraToStorage(cam: CustomDevice) {
this.storageSrv.set(Storage.VIDEO_DEVICE, cam);
this.storageSrv.setVideoDevice(cam);
}
private saveMicrophoneToStorage(mic: CustomDevice) {
this.storageSrv.set(Storage.AUDIO_DEVICE, mic);
this.storageSrv.setAudioDevice(mic);
}
}

View File

@ -48,23 +48,8 @@ export class DocumentService {
}
}
removeAllBigElementClass() {
const elements: HTMLCollectionOf<Element> = document.getElementsByClassName(LayoutClass.BIG_ELEMENT);
while (elements.length > 0) {
this.removeBigElementClass(elements[0]);
}
}
removeBigElementClass(element: HTMLElement | Element) {
element?.classList.remove(LayoutClass.BIG_ELEMENT);
}
toggleBigElementClass(element: HTMLElement | Element) {
if (element?.className.includes(LayoutClass.BIG_ELEMENT)) {
this.removeBigElementClass(element);
} else {
element.classList.add(LayoutClass.BIG_ELEMENT);
}
removeNoSizeElementClass(element: HTMLElement | Element) {
element?.classList.remove(LayoutClass.NO_SIZE_ELEMENT);
}
isSmallElement(element: HTMLElement | Element): boolean {

View File

@ -17,50 +17,53 @@ export class LayoutService {
}
initialize(timeout: number = null) {
if (!!timeout) {
if (typeof timeout === 'number' && timeout >= 0) {
setTimeout(() => {
this._initialize();
this.sendLayoutWidthEvent();
}, timeout);
} else {
this._initialize();
this.sendLayoutWidthEvent();
}
this.sendLayoutWidthEvent();
}
private _initialize() {
this.openviduLayout = new OpenViduLayout();
this.openviduLayoutOptions = this.getOptions();
this.openviduLayout.initLayoutContainer(document.getElementById('layout'), this.openviduLayoutOptions);
const element = document.getElementById('layout');
this.openviduLayout.initLayoutContainer(element, this.openviduLayoutOptions);
}
getOptions(): OpenViduLayoutOptions {
private getOptions(): OpenViduLayoutOptions {
const options = {
maxRatio: 3 / 2, // The narrowest ratio that will be used (default 2x3)
minRatio: 9 / 15, // The widest ratio that will be used (default 16x9)
minRatio: 9 / 16, // The widest ratio that will be used (default 16x9)
fixedRatio: false /* If this is true then the aspect ratio of the video is maintained
and minRatio and maxRatio are ignored (default false) */,
bigClass: LayoutClass.BIG_ELEMENT, // The class to add to elements that should be sized bigger
smallClass: LayoutClass.SMALL_ELEMENT,
bigPercentage: 0.85, // The maximum percentage of space the big ones should take up
bigPercentage: 0.7, // The maximum percentage of space the big ones should take up
bigFixedRatio: false, // fixedRatio for the big ones
bigMaxRatio: 3 / 2, // The narrowest ratio to use for the big elements (default 2x3)
bigMaxRatio: 9 / 16, // The narrowest ratio to use for the big elements (default 2x3)
bigMinRatio: 9 / 16, // The widest ratio to use for the big elements (default 16x9)
bigFirst: true, // Whether to place the big one in the top left (true) or bottom right
animate: true // Whether you want to animate the transitions. Invalid property, to disable it remove transition: all .1s linear;
animate: true // Whether you want to animate the transitions. Deprecated property, to disable it remove the transaction property on OT_publisher css class
};
return options;
}
update(timeout?: number) {
if (!!this.openviduLayout) {
if (!timeout) {
update(timeout: number = null) {
const updateAux = () => {
if (!!this.openviduLayout) {
this.openviduLayout.updateLayout();
} else {
setTimeout(() => {
this.openviduLayout.updateLayout();
}, timeout);
this.sendLayoutWidthEvent();
}
this.sendLayoutWidthEvent();
};
if (typeof timeout === 'number' && timeout >= 0) {
setTimeout(() => updateAux(), timeout);
} else {
updateAux();
}
}
@ -74,10 +77,10 @@ export class LayoutService {
private sendLayoutWidthEvent() {
const sidenavLayoutElement = this.documentService.getHTMLElementByClassName(
this.openviduLayout.getLayoutContainer(),
this.openviduLayout?.getLayoutContainer(),
LayoutClass.SIDENAV_CONTAINER
);
if(sidenavLayoutElement && sidenavLayoutElement.clientWidth) {
if (sidenavLayoutElement && sidenavLayoutElement.clientWidth) {
this._layoutWidthObs.next(sidenavLayoutElement.clientWidth);
}
}

View File

@ -1,19 +0,0 @@
import { Injectable } from '@angular/core';
import { LibConfig } from '../../config/lib.config';
@Injectable()
export class LibraryConfigServiceMock {
private configuration: LibConfig;
constructor() {
this.configuration = {environment: {production: false}};
}
getConfig(): LibConfig {
return this.configuration;
}
isProduction(): boolean {
return this.configuration?.environment?.production;
}
}

View File

@ -1,22 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { LibConfig } from '../../config/lib.config';
import { LibraryConfigService } from './library-config.service';
describe('LibraryConfigService', () => {
let service: LibraryConfigService;
const libConfig: LibConfig = { environment: {production: false} };
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
LibraryConfigService,
{provide: 'LIB_CONFIG', useValue: libConfig}]
});
service = TestBed.inject(LibraryConfigService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -1,91 +0,0 @@
import { Inject, Injectable, Type } from '@angular/core';
import { LayoutComponent } from '../../components/layout/layout.component';
import { ChatPanelComponent } from '../../components/panel/chat-panel/chat-panel.component';
import { PanelComponent } from '../../components/panel/panel.component';
import { ParticipantsPanelComponent } from '../../components/panel/participants-panel/participants-panel/participants-panel.component';
import { StreamComponent } from '../../components/stream/stream.component';
import { ToolbarComponent } from '../../components/toolbar/toolbar.component';
import { LibConfig, LibraryComponents } from '../../config/lib.config';
// import { version } from '../../../../package.json';
@Injectable()
export class LibraryConfigService {
private configuration: LibConfig;
constructor(@Inject('LIB_CONFIG') config: LibConfig) {
this.configuration = config;
console.log(this.configuration);
if(this.isProduction()) console.log('Production Mode');
// console.log(version)
}
getConfig(): LibConfig {
return this.configuration;
}
isProduction(): boolean {
return this.configuration?.environment?.production;
}
getDynamicComponent(name: LibraryComponents) {
let component: Type<any>;
switch (name) {
case LibraryComponents.LAYOUT:
component = LayoutComponent;
if (this.isCustomComponentDefined(LibraryComponents.LAYOUT)) {
component = this.getCustomComponent(LibraryComponents.LAYOUT);
}
return component;
case LibraryComponents.TOOLBAR:
component = ToolbarComponent;
if (this.isCustomComponentDefined(LibraryComponents.TOOLBAR)) {
component = this.getCustomComponent(LibraryComponents.TOOLBAR);
}
return component;
case LibraryComponents.PANEL:
component = PanelComponent;
// Full custom panel
// if (this.isCustomComponentDefined(LibraryComponents.PANEL)) {
// component = this.getCustomComponent(LibraryComponents.PANEL);
// }
return component;
case LibraryComponents.CHAT_PANEL:
component = ChatPanelComponent;
if (this.isCustomComponentDefined(LibraryComponents.CHAT_PANEL)) {
component = this.getCustomComponent(LibraryComponents.CHAT_PANEL);
}
return component;
case LibraryComponents.PARTICIPANTS_PANEL:
component = ParticipantsPanelComponent;
if (this.isCustomComponentDefined(LibraryComponents.PARTICIPANTS_PANEL)) {
component = this.getCustomComponent(LibraryComponents.PARTICIPANTS_PANEL);
}
return component;
case LibraryComponents.STREAM:
component = StreamComponent;
if (this.isCustomComponentDefined(LibraryComponents.STREAM)) {
component = this.getCustomComponent(LibraryComponents.STREAM);
}
return component;
}
}
isCustomComponentDefined(component: string): boolean {
return !!this.configuration?.environment?.customComponents && !!this.configuration.environment.customComponents[component];
}
getCustomComponent(component: string){
return this.configuration.environment.customComponents[component];
}
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { ILogService } from '../../models/logger.model';
import { LibraryConfigService } from '../library-config/library-config.service';
import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service';
@Injectable({
providedIn: 'root'
@ -15,7 +15,7 @@ export class LoggerService implements ILogService {
['[', '] ERROR: ']
];
constructor(private libraryConfigSrv: LibraryConfigService) {
constructor(private openviduAngularConfigSrv: OpenViduAngularConfigService) {
}
private getLoggerFns(prefix: string) {
@ -28,7 +28,7 @@ export class LoggerService implements ILogService {
}
public get(prefix: string) {
const prodMode = this.libraryConfigSrv.isProduction();
const prodMode = this.openviduAngularConfigSrv.isProduction();
const loggerService = this;
return {
d: function(...args: any[]) {

View File

@ -5,7 +5,7 @@ import { Signal } from '../../models/signal.model';
@Injectable({
providedIn: 'root'
})
export class WebrtcServiceMock {
export class OpenViduServiceMock {
private OV: OpenVidu = null;
private OVScreen: OpenVidu = null;

View File

@ -4,15 +4,15 @@ import { LoggerServiceMock } from '../logger/logger.service.mock';
import { LocalUserService } from '../local-user/local-user.service';
import { LocalUserServiceMock } from '../local-user/local-user.service.mock';
import { WebrtcService } from './webrtc.service';
import { OpenViduService } from './openvidu.service';
import { PlatformService } from '../platform/platform.service';
import { PlatformServiceMock } from '../platform/platform.service.mock';
import { LibraryConfigService } from '../library-config/library-config.service';
import { LibraryConfigServiceMock } from '../library-config/library-config.service.mock';
describe('WebrtcService', () => {
let service: WebrtcService;
describe('OpenViduService', () => {
let service: OpenViduService;
beforeEach(() => {
TestBed.configureTestingModule({
@ -23,7 +23,7 @@ describe('WebrtcService', () => {
{ provide: LibraryConfigService, useClass: LibraryConfigServiceMock }
]
});
service = TestBed.inject(WebrtcService);
service = TestBed.inject(OpenViduService);
});
it('should be created', () => {

View File

@ -1,11 +1,20 @@
import { Injectable } from '@angular/core';
import { Connection, OpenVidu, Publisher, PublisherProperties, Session, SignalOptions } from 'openvidu-browser';
import {
Connection,
OpenVidu,
OpenViduError,
OpenViduErrorName,
Publisher,
PublisherProperties,
Session,
SignalOptions
} from 'openvidu-browser';
import { LoggerService } from '../../services/logger/logger.service';
import { LoggerService } from '../logger/logger.service';
import { ILogger } from '../../models/logger.model';
import { Signal } from '../../models/signal.model';
import { LibraryConfigService } from '../library-config/library-config.service';
import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service';
import { PlatformService } from '../platform/platform.service';
import { DeviceService } from '../device/device.service';
import { CameraType } from '../../models/device.model';
@ -15,40 +24,42 @@ import { ParticipantService } from '../participant/participant.service';
@Injectable({
providedIn: 'root'
})
export class WebrtcService {
export class OpenViduService {
protected OV: OpenVidu = null;
protected OVScreen: OpenVidu = null;
protected webcamSession: Session = null;
protected screenSession: Session = null;
protected videoSource = undefined;
protected audioSource = undefined;
protected screenMediaStream: MediaStream = null;
protected webcamMediaStream: MediaStream = null;
protected log: ILogger;
constructor(
protected libraryConfigSrv: LibraryConfigService,
protected openviduAngularConfigSrv: OpenViduAngularConfigService,
protected platformService: PlatformService,
protected loggerSrv: LoggerService,
private participantService: ParticipantService,
protected deviceService: DeviceService
) {
this.log = this.loggerSrv.get('WebRTCService');
this.log = this.loggerSrv.get('OpenViduService');
}
initialize() {
this.OV = new OpenVidu();
if (this.libraryConfigSrv.isProduction()) this.OV.enableProdMode();
if (this.openviduAngularConfigSrv.isProduction()) this.OV.enableProdMode();
this.webcamSession = this.OV.initSession();
// Initialize screen session only if it is not mobile platform
if (!this.platformService.isMobile()) {
this.OVScreen = new OpenVidu();
if (this.libraryConfigSrv.isProduction()) this.OVScreen.enableProdMode();
if (this.openviduAngularConfigSrv.isProduction()) this.OVScreen.enableProdMode();
this.screenSession = this.OVScreen.initSession();
}
}
getSession(): Session {
return this.getWebcamSession();
}
getWebcamSession(): Session {
return this.webcamSession;
}
@ -67,7 +78,7 @@ export class WebrtcService {
async connectSession(session: Session, token: string): Promise<void> {
if (!!token && session) {
const nickname = this.participantService.getWebcamNickname();
const nickname = this.participantService.getMyNickname();
const participantId = this.participantService.getMyParticipantId();
if (session === this.webcamSession) {
this.log.d('Connecting webcam session');
@ -90,60 +101,43 @@ export class WebrtcService {
}
}
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() {
this.disconnectSession(this.webcamSession);
this.disconnectSession(this.screenSession);
this.videoSource = undefined;
this.audioSource = undefined;
this.stopTracks(this.participantService.getMyCameraPublisher()?.stream?.getMediaStream());
this.stopTracks(this.participantService.getMyScreenPublisher()?.stream?.getMediaStream());
// this.stopTracks(this.participantService.getMyCameraPublisher()?.stream?.getMediaStream());
// this.stopTracks(this.participantService.getMyScreenPublisher()?.stream?.getMediaStream());
}
/**
* Initialize a publisher checking devices saved on storage or if participant have devices available.
*/
async initDefaultPublisher(targetElement: string | HTMLElement): Promise<Publisher> {
await this.deviceService.initializeDevices();
const hasVideoDevices = this.deviceService.hasVideoDeviceAvailable();
const hasAudioDevices = this.deviceService.hasAudioDeviceAvailable();
const isVideoActive = hasVideoDevices && this.deviceService.getCameraSelected().label !== 'None';
const isAudioActive = hasAudioDevices && this.deviceService.getMicrophoneSelected().label !== 'None';
const isVideoActive = !this.deviceService.isVideoMuted();
const isAudioActive = !this.deviceService.isAudioMuted();
let videoSource = null;
let audioSource = null;
if(isVideoActive){
if (hasVideoDevices) {
// Video is active, assign the device selected
videoSource = this.deviceService.getCameraSelected().device
} else if(!isVideoActive && hasVideoDevices) {
videoSource = this.deviceService.getCameraSelected().device;
} else if (!isVideoActive && hasVideoDevices) {
// Video is muted, assign the default device
videoSource = undefined;
// videoSource = undefined;
}
if(isAudioActive){
if (hasAudioDevices) {
// Audio is active, assign the device selected
audioSource = this.deviceService.getMicrophoneSelected().device
} else if(!isAudioActive && hasAudioDevices) {
audioSource = this.deviceService.getMicrophoneSelected().device;
} else if (!isAudioActive && hasAudioDevices) {
// Audio is muted, assign the default device
audioSource = undefined;
// audioSource = undefined;
}
// const videoSource = publishVideo ? this.deviceService.getCameraSelected().device : false;
// const audioSource = publishAudio ? this.deviceService.getMicrophoneSelected().device : false;
const mirror = this.deviceService.getCameraSelected() && this.deviceService.getCameraSelected().type === CameraType.FRONT;
@ -154,40 +148,19 @@ export class WebrtcService {
publishAudio: isAudioActive,
mirror
};
if (isVideoActive || isAudioActive) {
const publisher = this.initPublisher(targetElement, properties);
if (hasVideoDevices || hasAudioDevices) {
const publisher = await this.initPublisher(targetElement, properties);
this.participantService.setMyCameraPublisher(publisher);
this.participantService.updateLocalParticipant();
return publisher;
} else {
this.participantService.setMyCameraPublisher(null);
}
}
initPublisher(targetElement: string | HTMLElement, properties: PublisherProperties): Publisher {
async initPublisher(targetElement: string | HTMLElement, properties: PublisherProperties): Promise<Publisher> {
this.log.d('Initializing publisher with properties: ', properties);
const publisher = this.OV.initPublisher(targetElement, properties);
// this.participantService.setMyCameraPublisher(publisher);
publisher.once('streamPlaying', () => {
(<HTMLElement>publisher.videos[0].video).parentElement.classList.remove('custom-class');
});
return publisher;
}
//TODO: This method is used by replaceTrack. Check if it's neecessary
private destroyPublisher(publisher: Publisher): void {
if (!!publisher) {
// publisher.off('streamAudioVolumeChange');
if (publisher.stream.getWebRtcPeer()) {
publisher.stream.disposeWebRtcPeer();
}
publisher.stream.disposeMediaStream();
if (publisher.id === this.participantService.getMyCameraPublisher().id) {
this.participantService.setMyCameraPublisher(publisher);
} else if (publisher.id === this.participantService.getMyScreenPublisher().id) {
this.participantService.setMyScreenPublisher(publisher);
}
}
return await this.OV.initPublisherAsync(targetElement, properties);
}
async publish(publisher: Publisher): Promise<void> {
@ -220,30 +193,29 @@ export class WebrtcService {
publishVideo(publisher: Publisher, value: boolean): void {
if (!!publisher) {
publisher.publishVideo(value);
// Send event to subscribers because of video has changed and view must update
this.participantService.updateUsersStatus();
this.participantService.updateLocalParticipant();
}
}
publishAudio(publisher: Publisher, value: boolean): void {
if (!!publisher) {
publisher.publishAudio(value);
// Send event to subscribers because of video has changed and view must update
this.participantService.updateUsersStatus();
this.participantService.updateLocalParticipant();
}
}
republishTrack(videoSource: string, audioSource: string, mirror: boolean = true): Promise<void> {
return new Promise((resolve, reject) => {
// TODO: Remove this method when replaceTrack issue is fixed
// https://github.com/OpenVidu/openvidu/pull/700
republishTrack(properties: PublisherProperties): Promise<void> {
const {videoSource, audioSource, mirror} = properties;
return new Promise(async (resolve, reject) => {
if (!!videoSource) {
this.log.d('Replacing video track ' + videoSource);
this.videoSource = videoSource;
// this.stopVideoTracks(this.webcamUser.getStreamManager().stream.getMediaStream());
}
if (!!audioSource) {
this.log.d('Replacing audio track ' + audioSource);
this.audioSource = audioSource;
// this.stopAudioTracks(this.webcamUser.getStreamManager().stream.getMediaStream());
}
this.destroyPublisher(this.participantService.getMyCameraPublisher());
const properties: PublisherProperties = {
@ -254,7 +226,7 @@ export class WebrtcService {
mirror
};
const publisher = this.initPublisher(undefined, properties);
const publisher = await this.initPublisher(undefined, properties);
this.participantService.setMyCameraPublisher(publisher);
publisher.once('streamPlaying', () => {
@ -276,33 +248,56 @@ export class WebrtcService {
};
this.webcamSession.signal(signalOptions);
if (type === Signal.NICKNAME_CHANGED && !!this.getScreenSession().connection) {
signalOptions.data = JSON.stringify({ clientData: this.participantService.getScreenNickname() });
this.getScreenSession()?.signal(signalOptions);
}
// TODO: Check if it is necessary
// if (type === Signal.NICKNAME_CHANGED && !!this.getScreenSession().connection) {
// signalOptions.data = JSON.stringify({ clientData: this.participantService.getScreenNickname() });
// this.getScreenSession()?.signal(signalOptions);
// }
}
async replaceTrack(currentPublisher: Publisher, newProperties: PublisherProperties) {
async replaceTrack(videoType: VideoType, props: PublisherProperties) {
try {
if (!!currentPublisher) {
if (currentPublisher === this.participantService.getMyCameraPublisher()) {
// This branch has been disabled because echo problems replacing audio track.
// this.stopAudioTracks(this.webcamMediaStream);
// this.stopVideoTracks(this.webcamMediaStream);
// this.webcamMediaStream = await this.OV.getUserMedia(newProperties);
// const videoTrack: MediaStreamTrack = this.webcamMediaStream.getVideoTracks()[0];
// const audioTrack: MediaStreamTrack = this.webcamMediaStream.getAudioTracks()[0];
// if(!!videoTrack){
// await this.participantService.getMyCameraPublisher().replaceTrack(videoTrack);
// }
// if(!!audioTrack) {
// await this.participantService.getMyCameraPublisher().replaceTrack(audioTrack);
// }
} else if (currentPublisher === this.participantService.getMyScreenPublisher()) {
const newScreenMediaStream = await this.OVScreen.getUserMedia(newProperties);
this.stopTracks(this.screenMediaStream);
this.log.d(`Replacing ${videoType} track`, props);
if (videoType === VideoType.CAMERA) {
//TODO: Uncomment this section when replaceTrack issue is fixed
// https://github.com/OpenVidu/openvidu/pull/700
throw('Replace track feature has a bug. We are trying to fix it');
// let mediaStream: MediaStream;
// const oldMediaStream = this.participantService.getMyCameraPublisher().stream.getMediaStream();
// const isFirefoxPlatform = this.platformService.isFirefox();
// const isReplacingAudio = !!props.audioSource;
// const isReplacingVideo = !!props.videoSource;
// if (isReplacingVideo) {
// if (isFirefoxPlatform) {
// // Firefox throw an exception trying to get a new MediaStreamTrack if the older one is not stopped
// // NotReadableError: Concurrent mic process limit. Stopping tracks before call to getUserMedia
// oldMediaStream.getVideoTracks()[0].stop();
// }
// mediaStream = await this.createMediaStream(props);
// // Replace video track
// const videoTrack: MediaStreamTrack = mediaStream.getVideoTracks()[0];
// await this.participantService.getMyCameraPublisher().replaceTrack(videoTrack);
// } else if (isReplacingAudio) {
// if (isFirefoxPlatform) {
// // Firefox throw an exception trying to get a new MediaStreamTrack if the older one is not stopped
// // NotReadableError: Concurrent mic process limit. Stopping tracks before call to getUserMedia
// oldMediaStream.getAudioTracks()[0].stop();
// }
// mediaStream = await this.createMediaStream(props);
// // Replace audio track
// const audioTrack: MediaStreamTrack = mediaStream.getAudioTracks()[0];
// await this.participantService.getMyCameraPublisher().replaceTrack(audioTrack);
// }
} else if (videoType === VideoType.SCREEN) {
let newScreenMediaStream;
try {
newScreenMediaStream = await this.OVScreen.getUserMedia(props);
this.participantService.getMyScreenPublisher().stream.getMediaStream().getVideoTracks()[0].stop();
await this.participantService.getMyScreenPublisher().replaceTrack(newScreenMediaStream.getVideoTracks()[0]);
this.screenMediaStream = newScreenMediaStream;
} catch (error) {
this.log.w('Cannot create the new MediaStream', error);
}
}
} catch (error) {
@ -310,9 +305,56 @@ export class WebrtcService {
}
}
private destroyPublisher(publisher: Publisher): void {
if (!!publisher) {
if (publisher.stream.getWebRtcPeer()) {
publisher.stream.disposeWebRtcPeer();
}
publisher.stream.disposeMediaStream();
if (publisher.id === this.participantService.getMyCameraPublisher().id) {
this.participantService.setMyCameraPublisher(publisher);
} else if (publisher.id === this.participantService.getMyScreenPublisher().id) {
this.participantService.setMyScreenPublisher(publisher);
}
}
}
private async createMediaStream(pp: PublisherProperties): Promise<MediaStream> {
let mediaStream: MediaStream;
const isFirefoxPlatform = this.platformService.isFirefox();
const isReplacingAudio = !!pp.audioSource;
const isReplacingVideo = !!pp.videoSource;
try {
mediaStream = await this.OV.getUserMedia(pp);
} catch (error) {
if ((<OpenViduError>error).name === OpenViduErrorName.DEVICE_ACCESS_DENIED) {
if (isFirefoxPlatform) {
this.log.w('The device requested is not available. Restoring the older one');
// The track requested is not available so we are getting the old tracks ids for recovering the track
if (isReplacingVideo) {
pp.videoSource = this.deviceService.getCameraSelected().device;
} else if (isReplacingAudio) {
pp.audioSource = this.deviceService.getMicrophoneSelected().device;
}
mediaStream = await this.OV.getUserMedia(pp);
// TODO show error alert informing that the new device is not available
}
}
} finally {
return mediaStream;
}
}
needSendNicknameSignal(): boolean {
const oldNickname: string = JSON.parse(this.webcamSession.connection.data).clientData;
return oldNickname !== this.participantService.getWebcamNickname();
let oldNickname: string;
try {
const connData = JSON.parse(this.webcamSession.connection.data.split('%/%')[0]);
oldNickname = connData.clientData;
} catch (error) {
this.log.e(error);
}
return oldNickname !== this.participantService.getMyNickname();
}
isMyOwnConnection(connectionId: string): boolean {
@ -331,11 +373,25 @@ export class WebrtcService {
return remoteCameraConnections;
}
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());
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;
}
}
}
// 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

@ -2,8 +2,9 @@ import { Injectable } from '@angular/core';
import { Publisher, Subscriber } from 'openvidu-browser';
import { BehaviorSubject, Observable } from 'rxjs';
import { ILogger } from '../../models/logger.model';
import { StreamModel, ParticipantAbstractModel, ParticipantModel } from '../../models/participant.model';
import { StreamModel, ParticipantAbstractModel, ParticipantModel, ParticipantProperties } from '../../models/participant.model';
import { VideoType } from '../../models/video-type.model';
import { OpenViduAngularConfigService } from '../config/openvidu-angular.config.service';
import { LoggerService } from '../logger/logger.service';
@Injectable({
@ -12,13 +13,7 @@ import { LoggerService } from '../logger/logger.service';
export class ParticipantService {
//Local participants observables
localParticipantObs: Observable<ParticipantAbstractModel>;
screenShareState: Observable<boolean>;
webcamVideoActive: Observable<boolean>;
webcamAudioActive: Observable<boolean>;
protected _localParticipant = <BehaviorSubject<ParticipantAbstractModel>>new BehaviorSubject(null);
protected _screensharing = <BehaviorSubject<boolean>>new BehaviorSubject(false);
protected _cameraVideoActive = <BehaviorSubject<boolean>>new BehaviorSubject(true);
protected _cameraAudioActive = <BehaviorSubject<boolean>>new BehaviorSubject(true);
//Remote participants observable
remoteParticipantsObs: Observable<ParticipantAbstractModel[]>;
@ -29,20 +24,21 @@ export class ParticipantService {
protected log: ILogger;
constructor(protected loggerSrv: LoggerService) {
constructor(protected openviduAngularConfigSrv: OpenViduAngularConfigService, protected loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('ParticipantService');
// this.participantsObs = this._participants.asObservable();
this.localParticipantObs = this._localParticipant.asObservable();
this.remoteParticipantsObs = this._remoteParticipants.asObservable();
this.screenShareState = this._screensharing.asObservable();
this.webcamVideoActive = this._cameraVideoActive.asObservable();
this.webcamAudioActive = this._cameraAudioActive.asObservable();
}
this.localParticipant = this.newParticipant();
initLocalParticipant(props: ParticipantProperties) {
this.localParticipant = this.newParticipant(props);
this.updateLocalParticipant();
}
getLocalParticipant(): ParticipantAbstractModel {
return this.localParticipant;
}
getMyParticipantId(): string {
return this.localParticipant.id;
}
@ -84,98 +80,85 @@ export class ParticipantService {
this.updateLocalParticipant();
}
enableScreenUser(screenPublisher: Publisher) {
activeMyScreenShare(screenPublisher: Publisher) {
this.log.d('Enabling screen publisher');
const steramModel: StreamModel = {
local: true,
type: VideoType.SCREEN,
videoEnlarged: true,
streamManager: screenPublisher,
nickname: `${this.localParticipant.getCameraNickname()}_${VideoType.SCREEN}`,
connected: true,
connectionId: null
};
this.resetRemoteStreamsToNormalSize();
this.resetMyStreamsToNormalSize();
this.localParticipant.addConnection(steramModel);
this._screensharing.next(true);
this.updateLocalParticipant();
}
disableScreenUser() {
this.localParticipant.disableScreen();
this.updateLocalParticipant();
this._screensharing.next(false);
}
updateUsersStatus() {
this._cameraVideoActive.next(this.localParticipant.isCameraVideoActive());
if (this.isMyCameraEnabled()) {
this._cameraAudioActive.next(this.localParticipant.isCameraAudioActive());
} else {
this._cameraAudioActive.next(this.hasScreenAudioActive());
}
setMyNickname(nickname: string) {
this.localParticipant.setNickname(nickname);
this.updateLocalParticipant();
}
setNickname(connectionId: string, nickname: string) {
if (this.localParticipant.hasConnectionId(connectionId)) {
this.localParticipant.setNickname(nickname);
// this.updateLocalParticipant();
} else {
const participant = this.getRemoteParticipantByConnectionId(connectionId);
if (participant) {
participant.setNickname(nickname);
// this.updateRemoteParticipants();
}
}
getMyNickname(): string {
return this.localParticipant.nickname;
}
// getWebcamNickname(): string {
// return this.localParticipant.getCameraNickname();
// }
getWebcamNickname(): string {
return this.localParticipant.getCameraNickname();
}
// getScreenNickname(): string {
// return this.localParticipant.getScreenNickname();
// }
getScreenNickname(): string {
return this.localParticipant.getScreenNickname();
}
resetUsersZoom() {
this.localParticipant?.setAllVideoEnlarged(false);
}
toggleZoom(connectionId: string) {
toggleMyVideoEnlarged(connectionId: string) {
this.localParticipant.toggleVideoEnlarged(connectionId);
}
resetMyStreamsToNormalSize() {
if(this.localParticipant.someHasVideoEnlarged()){
this.localParticipant.setAllVideoEnlarged(false);
this.updateLocalParticipant();
}
}
clear() {
this.disableScreenUser();
this.localParticipant = this.newParticipant();
this._screensharing.next(false);
// this.localParticipant = this.newParticipant();
// this._screensharing.next(false);
this.remoteParticipants = [];
this._remoteParticipants = <BehaviorSubject<ParticipantAbstractModel[]>>new BehaviorSubject([]);
this.remoteParticipantsObs = this._remoteParticipants.asObservable();
this.updateLocalParticipant();
}
isMyCameraEnabled(): boolean {
return this.localParticipant.isCameraEnabled();
isMyCameraActive(): boolean {
return this.localParticipant.isCameraActive();
}
isMyScreenEnabled(): boolean {
return this.localParticipant.isScreenEnabled();
isMyScreenActive(): boolean {
return this.localParticipant.isScreenActive();
}
isOnlyMyCameraEnabled(): boolean {
return this.isMyCameraEnabled() && !this.isMyScreenEnabled();
isOnlyMyCameraActive(): boolean {
return this.isMyCameraActive() && !this.isMyScreenActive();
}
isOnlyMyScreenEnabled(): boolean {
return this.isMyScreenEnabled() && !this.isMyCameraEnabled();
isOnlyMyScreenActive(): boolean {
return this.isMyScreenActive() && !this.isMyCameraActive();
}
areBothEnabled(): boolean {
return this.isMyCameraEnabled() && this.isMyScreenEnabled();
haveICameraAndScreenActive(): boolean {
return this.isMyCameraActive() && this.isMyScreenActive();
}
hasCameraVideoActive(): boolean {
@ -190,13 +173,8 @@ export class ParticipantService {
return this.localParticipant.isScreenAudioActive();
}
protected updateLocalParticipant() {
// Cloning localParticipant object for not applying changes on the global variable
let participantsWithConnectionAvailable: ParticipantAbstractModel = Object.assign(this.newParticipant(), this.localParticipant);
const availableConnections = participantsWithConnectionAvailable.getAvailableConnections();
const availableConnectionsMap = new Map(availableConnections.map((conn) => [conn.type, conn]));
participantsWithConnectionAvailable.connections = availableConnectionsMap;
this._localParticipant.next(participantsWithConnectionAvailable);
updateLocalParticipant() {
this._localParticipant.next(Object.assign(Object.create(this.localParticipant),this.localParticipant));
}
/**
@ -205,35 +183,51 @@ export class ParticipantService {
addRemoteConnection(connectionId:string, data: string, subscriber: Subscriber) {
const steramModel: StreamModel = {
local: false,
type: this.getTypeConnectionData(data),
videoEnlarged: false,
const type: VideoType = this.getTypeConnectionData(data);
const streamModel: StreamModel = {
type,
videoEnlarged: type === VideoType.SCREEN,
streamManager: subscriber,
nickname: this.getNicknameFromConnectionData(data),
connected: true,
connected: true,
connectionId
};
const participantId = this.getParticipantIdFromData(data);
// Avoiding create a new participant if participantId param is not exist in connection data
// participant Id is necessary for allowing to have multiple connection in one participant
const participantId = this.getParticipantIdFromData(data) || connectionId;
const participantAdded = this.getRemoteParticipantById(participantId);
if (!!participantAdded) {
this.log.d('Adding connection to existing participant: ', participantId);
if(participantAdded.hasConnectionType(steramModel.type)) {
if(participantAdded.hasConnectionType(streamModel.type)) {
this.log.d('Participant has publisher, updating it');
participantAdded.setPublisher(steramModel.type, subscriber);
participantAdded.setPublisher(streamModel.type, subscriber);
} else {
this.log.d('Participant has not publisher, adding it');
participantAdded.addConnection(steramModel);
if(streamModel.type === VideoType.SCREEN) {
this.resetRemoteStreamsToNormalSize();
this.resetMyStreamsToNormalSize();
}
participantAdded.addConnection(streamModel);
}
} else {
this.log.d('Creating new participant with id: ', participantId);
const remoteParticipant = this.newParticipant(steramModel, participantId);
this.log.w('Creating new participant with id: ', participantId);
const props: ParticipantProperties = {
nickname: this.getNicknameFromConnectionData(data),
local: false,
id: participantId
}
const remoteParticipant = this.newParticipant(props, streamModel);
this.remoteParticipants.push(remoteParticipant);
}
this.updateRemoteParticipants();
}
resetRemoteStreamsToNormalSize() {
this.remoteParticipants.forEach(participant => participant.setAllVideoEnlarged(false));
this.updateRemoteParticipants();
}
removeConnectionByConnectionId(connectionId: string) {
this.log.w('Deleting connection: ', connectionId);
let participant = null;
@ -244,16 +238,28 @@ export class ParticipantService {
}
if (participant) {
participant.removeConnection(connectionId);
const removeStream: StreamModel = participant.removeConnection(connectionId);
//TODO: Timeout of X seconds?? Its possible sometimes the connections map was empty but must not be deleted
if (participant.connections.size === 0) {
if (participant.streams.size === 0) {
// Remove participants without connections
this.remoteParticipants = this.remoteParticipants.filter((p) => p !== participant);
}
if(removeStream.type === VideoType.SCREEN){
const remoteScreens = this.remoteParticipants.filter(p => p.isScreenActive());
if(remoteScreens.length > 0){
// Enlarging the last screen connection active
const lastScreenActive = remoteScreens[remoteScreens.length -1];
lastScreenActive.setScreenEnlarged(true);
} else if(this.localParticipant.isScreenActive()) {
// Enlarging my screen if thereare not any remote screen active
this.localParticipant.setScreenEnlarged(true);
}
}
this.updateRemoteParticipants();
}
}
protected getRemoteParticipantByConnectionId(connectionId: string): ParticipantAbstractModel {
getRemoteParticipantByConnectionId(connectionId: string): ParticipantAbstractModel {
return this.remoteParticipants.find((p) => p.hasConnectionId(connectionId));
}
@ -264,15 +270,11 @@ export class ParticipantService {
return this.remoteParticipants.some((p) => p.someHasVideoEnlarged());
}
toggleUserZoom(connectionId: string) {
toggleRemoteVideoEnlarged(connectionId: string) {
const p = this.getRemoteParticipantByConnectionId(connectionId);
p.toggleVideoEnlarged(connectionId);
}
resetRemotesZoom() {
this.remoteParticipants.forEach((u) => u.setAllVideoEnlarged(false));
}
getNicknameFromConnectionData(data: string): string {
try {
return JSON.parse(data).clientData;
@ -281,6 +283,18 @@ export class ParticipantService {
}
}
setRemoteNickname(connectionId: string, nickname: string) {
const participant = this.getRemoteParticipantByConnectionId(connectionId);
if (participant) {
participant.setNickname(nickname);
// this.updateRemoteParticipants();
}
}
updateRemoteParticipants() {
this._remoteParticipants.next([...this.remoteParticipants]);
}
protected getTypeConnectionData(data: string): VideoType {
try {
return JSON.parse(data).type;
@ -297,10 +311,11 @@ export class ParticipantService {
}
}
protected updateRemoteParticipants() {
this._remoteParticipants.next(this.remoteParticipants);
}
protected newParticipant(steramModel?: StreamModel, participantId?: string) {
return new ParticipantModel(steramModel, participantId);
protected newParticipant(props: ParticipantProperties, streamModel?: StreamModel) {
if(this.openviduAngularConfigSrv.hasParticipantFactory()){
return this.openviduAngularConfigSrv.getParticipantFactory().apply(this, [props, streamModel]);
}
return new ParticipantModel(props, streamModel);
}
}

View File

@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { RestService } from './rest.service';
describe('RestService', () => {
let service: RestService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(RestService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -1,75 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { catchError, throwError } from 'rxjs';
import { Buffer} from 'buffer';
@Injectable({
providedIn: 'root'
})
export class RestService {
constructor(private http: HttpClient) {}
async getToken(sessionId: string, openviduServerUrl: string, openviduSecret: string): Promise<string> {
if (!!openviduServerUrl && !!openviduSecret) {
const _sessionId = await this.createSession(sessionId, openviduServerUrl, openviduSecret);
return await this.createToken(_sessionId, openviduServerUrl, openviduSecret);
} else {
return Promise.reject(`Error requesting a token to ${openviduServerUrl} with session id: ${sessionId}`);
}
}
private createSession(sessionId: string, openviduServerUrl: string, openviduSecret: string): Promise<string> {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ customSessionId: sessionId });
const options = {
headers: new HttpHeaders({
Authorization: 'Basic ' + this.btoa('OPENVIDUAPP:' + openviduSecret),
'Content-Type': 'application/json'
})
};
return this.http
.post<any>(openviduServerUrl + '/openvidu/api/sessions', body, options)
.pipe(
catchError((error) => {
if (error.status === 409) {
resolve(sessionId);
}
if (error.statusText === 'Unknown Error') {
reject({ status: 401, message: 'ERR_CERT_AUTHORITY_INVALID' });
}
return throwError(() => new Error(error));
})
)
.subscribe((response) => {
resolve(response.id);
});
});
}
private createToken(sessionId: string, openviduServerUrl: string, openviduSecret: string): Promise<string> {
return new Promise((resolve, reject) => {
const body = JSON.stringify({});
const options = {
headers: new HttpHeaders({
Authorization: 'Basic ' + this.btoa('OPENVIDUAPP:' + openviduSecret),
'Content-Type': 'application/json'
})
};
return this.http
.post<any>(openviduServerUrl + '/openvidu/api/sessions/' + sessionId + '/connection', body, options)
.pipe(
catchError((error) => {
reject(error);
return throwError(() => new Error(error));
})
)
.subscribe((response) => {
resolve(response.token);
});
});
}
private btoa(str: string): string {
return Buffer.from(str).toString('base64');
}
}

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { ILogger } from '../../models/logger.model';
import { LoggerService } from '../logger/logger.service';
import { Storage } from '../../models/storage.model';
@Injectable({
providedIn: 'root'
@ -13,12 +14,49 @@ export class 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 });
// this.log.d('Storing on localStorage "' + key + '" with value "' + value + '"');
this.storage.setItem(key, value);
}
public get(key: string): any {
private get(key: string): any {
const value = JSON.parse(this.storage.getItem(key));
return value?.item ? value.item : null;
}

View File

@ -1,6 +1,4 @@
import { Injectable } from '@angular/core';
import { ILogger } from '../../models/logger.model';
import { LoggerService } from '../logger/logger.service';
@Injectable({
providedIn: 'root'
@ -8,19 +6,14 @@ import { LoggerService } from '../logger/logger.service';
export class TokenService {
private webcamToken = '';
private screenToken = '';
private sessionId = '';
private log: ILogger;
constructor(private loggerSrv: LoggerService) {
this.log = this.loggerSrv.get('TokenService');
}
constructor() {}
setWebcamToken(token: string){
setWebcamToken(token: string) {
this.webcamToken = token;
}
setScreenToken(token: string){
setScreenToken(token: string) {
this.screenToken = token;
}

View File

@ -5,12 +5,12 @@
export * from './lib/openvidu-angular.module';
// Services
export * from './lib/services/webrtc/webrtc.service';
export * from './lib/services/openvidu/openvidu.service';
export * from './lib/services/participant/participant.service';
export * from './lib/services/chat/chat.service';
export * from './lib/services/platform/platform.service';
export * from './lib/services/logger/logger.service';
export * from './lib/services/library-config/library-config.service';
export * from './lib/services/config/openvidu-angular.config.service';
export * from './lib/services/document/document.service';
export * from './lib/services/token/token.service';
export * from './lib/services/device/device.service';
@ -22,21 +22,35 @@ export * from './lib/services/storage/storage.service';
// Components
export * from './lib/components/videoconference/videoconference.component';
export * from './lib/components/user-settings/user-settings.component';
// export * from './lib/components/user-settings/user-settings.component';
export * from './lib/components/toolbar/toolbar.component';
export * from './lib/components/panel/panel.component';
export * from './lib/components/panel/chat-panel/chat-panel.component';
export * from './lib/components/panel/participants-panel/participants-panel/participants-panel.component';
export * from './lib/components/panel/participants-panel/participant-panel-item/participant-panel-item.component';
export * from './lib/components/session/session.component';
export * from './lib/components/layout/layout.component';
export * from './lib/components/stream/stream.component';
export * from './lib/components/video/video.component';
export * from './lib/components/audio-wave/audio-wave.component';
export * from './lib/components/pre-join/pre-join.component';
// Models
export * from './lib/models/participant.model';
export * from './lib/config/lib.config';
export * from './lib/config/openvidu-angular.config';
export * from './lib/models/logger.model';
export * from './lib/models/video-type.model';
export * from './lib/models/notification-options.model';
// Pipes
export * from './lib/pipes/participant-connections.pipe';
export * from './lib/pipes/participant.pipe';
// Directives
export * from './lib/directives/api/api.directive.module';
export * from './lib/directives/template/openvidu-angular.directive.module';
export * from './lib/directives/template/openvidu-angular.directive';
export * from './lib/directives/api/toolbar.directive';
export * from './lib/directives/api/stream.directive';
export * from './lib/directives/api/videoconference.directive';
export * from './lib/directives/api/participant-panel-item.directive';

View File

@ -17,7 +17,7 @@ import { LayoutTestComponent } from './components/layout-test/layout-test.compon
import { StreamTestComponent } from './components/stream-test/stream-test.component';
import {
OpenviduAngularModule,
OpenViduAngularModule,
UserSettingsComponent,
ChatPanelComponent,
ToolbarComponent,
@ -41,7 +41,7 @@ import { MatButtonModule } from '@angular/material/button';
BrowserModule,
MatButtonModule,
BrowserAnimationsModule,
OpenviduAngularModule.forRoot(environment),
OpenViduAngularModule.forRoot(environment),
AppRoutingModule // Order is important, AppRoutingModule must be the last import for useHash working
],
providers: [VideoconferenceComponent, UserSettingsComponent, ToolbarComponent, ChatPanelComponent, SessionComponent, LayoutComponent],

View File

@ -31,7 +31,7 @@
<div id="stream-test-container">
<ov-stream [participant]="participant"></ov-stream>
<ov-stream [stream]="stream"></ov-stream>
</div>

View File

@ -8,11 +8,11 @@ import { ParticipantModel } from 'openvidu-angular';
})
export class StreamTestComponent implements OnInit {
participant: ParticipantModel;
stream: ParticipantModel;
constructor() { }
ngOnInit(): void {
this.participant = new ParticipantModel();
this.stream = new ParticipantModel();
}
}

View File

@ -1,20 +1,95 @@
<div id="call-container">
<div id="userSettings" *ngIf="!joinSessionClicked && !closeClicked || !isSessionAlive">
<ov-user-settings (onJoinClicked)="onJoinClicked()" (onCloseClicked)="onCloseClicked()"></ov-user-settings>
</div>
<ov-videoconference
[tokens]="tokens"
(onJoinButtonClicked)="onJoinClicked()"
(onToolbarLeaveButtonClicked)="onToolbarLeaveButtonClicked()"
(onToolbarCameraButtonClicked)="onToolbarCameraButtonClicked()"
(onToolbarMicrophoneButtonClicked)="onToolbarMicrophoneButtonClicked()"
(onToolbarScreenshareButtonClicked)="onToolbarScreenshareButtonClicked()"
(onToolbarFullscreenButtonClicked)="onToolbarFullscreenButtonClicked()"
(onToolbarParticipantsPanelButtonClicked)="onToolbarParticipantsPanelButtonClicked()"
(onToolbarChatPanelButtonClicked)="onToolbarChatPanelButtonClicked()"
[prejoin]="false"
>
<div *ngIf="joinSessionClicked && isSessionAlive" style="height: 100%;">
<ov-session [tokens]="tokens">
<ng-template #toolbar>
<ov-toolbar
(onCamClicked)="onCamClicked()"
(onMicClicked)="onMicClicked()"
(onScreenShareClicked)="onScreenShareClicked()"
(onSpeakerLayoutClicked)="onSpeakerLayoutClicked()"
(onLeaveSessionClicked)="onLeaveSessionClicked()"
></ov-toolbar>
</ng-template>
<ov-layout layout></ov-layout>
</ov-session>
</div>
<!-- <div *ovParticipantPanelItemElements>
<p>EXTRA INFO</p>
</div> -->
<!-- <div *ovParticipantPanelItemElements="let participant">
<p>N: {{participant?.nickname}}</p>
</div>
<ov-toolbar
*ovToolbar
[screenshareButton]="true"
(onLeaveButtonClicked)="onLeaveButtonClicked()"
(onCameraButtonClicked)="onCameraButtonClicked()"
(onMicrophoneButtonClicked)="onMicrophoneButtonClicked()"
(onScreenshareButtonClicked)="onScreenshareButtonClicked()"
(onFullscreenButtonClicked)="onFullscreenButtonClicked()"
(onParticipantsPanelButtonClicked)="onParticipantsPanelButtonClicked()"
(onChatPanelButtonClicked)="onChatPanelButtonClicked()"
>
<div *ovToolbarAdditionalButtons>
<button mat-icon-button>
<mat-icon matTooltip="Exit Fullscreen">fullscreen_exit</mat-icon>
</button>
<button mat-icon-button>
<mat-icon matTooltip="Exit Fullscreen">fullscreen_exit</mat-icon>
</button>
</div>
</ov-toolbar> -->
<!-- <div *ovToolbar [screenshareButton]="value" [fullscreenButton]="value">
<ov-toolbar (onCamClicked)="onCamClicked()"></ov-toolbar>
</div> -->
<!--
<ov-panel *ovPanel>
<ov-chat-panel *ovChatPanel></ov-chat-panel>
<ov-participants-panel *ovParticipantsPanel>
<div *ovParticipantPanelItem="let participant">
<ov-participant-panel-item [participant]="participant"></ov-participant-panel-item>
<button mat-icon-button id="hand-notification" *ngIf="participant.hasHandRaised">
<mat-icon>front_hand</mat-icon>
</button>
</div>
</ov-participants-panel>
</ov-panel> -->
<!-- <ov-toolbar chatPanel (onCamClicked)="onCamClicked()"></ov-toolbar> -->
<!-- <div *ovParticipantPanelItem="let participant">
<ov-participant-panel-item [participant]="participant" [muteButton]="false">
<div *ovParticipantPanelItemElements>
<button mat-icon-button id="hand-notification" >
<mat-icon>front_hand</mat-icon>
</button>
<span> OK</span>
</div>
</ov-participant-panel-item>
</div> -->
<!-- <ov-layout *ovLayout >
<div *ovStream="let stream">
<p>EXTERNAL STREAM INSIDE OF LAYOUT</p>
<ov-stream [stream]="stream"></ov-stream>
</div>
</ov-layout> -->
<!-- <div *ovStream="let stream">
<p>EXTERNAL STREAM</p>
<ov-stream [stream]="stream"></ov-stream>
</div> -->
</ov-videoconference>

View File

@ -1,14 +1,14 @@
import { Component, OnInit } from '@angular/core';
import { RestService } from '../services/rest.service';
import { Router } from '@angular/router';
@Component({
selector: 'app-call',
templateUrl: './call.component.html',
styleUrls: ['./call.component.scss']
})
export class CallComponent implements OnInit {
sessionId = 'prueba-majestuosa-amable';
value = true;
sessionId = 'qqqq';
tokens: { webcam: string; screen: string };
joinSessionClicked: boolean = false;
@ -17,33 +17,71 @@ export class CallComponent implements OnInit {
constructor(private restService: RestService, private router: Router) {}
ngOnInit() {
// this.onJoinClicked();
async ngOnInit() {
this.tokens = {
webcam: await this.restService.getToken(this.sessionId),
screen: await this.restService.getToken(this.sessionId)
};
}
async onJoinClicked() {
console.warn('VC JOIN BUTTON CLICKED');
this.tokens = {
webcam: await this.restService.getToken(this.sessionId),
screen: await this.restService.getToken(this.sessionId)
};
this.joinSessionClicked = true;
this.isSessionAlive = true;
}
onCloseClicked() {
this.closeClicked = true;
this.router.navigate([`/`]);
// setInterval(() => {
// this.value = !this.value;
// }, 1000);
}
onMicClicked() {}
onToolbarCameraButtonClicked() {
console.warn('VC camera CLICKED');
}
onToolbarMicrophoneButtonClicked() {
console.warn('VC microphone CLICKED');
}
onToolbarScreenshareButtonClicked() {
console.warn('VC screenshare CLICKED');
}
onToolbarFullscreenButtonClicked() {
console.warn('VC fullscreen CLICKED');
}
onToolbarParticipantsPanelButtonClicked() {
console.warn('VC participants CLICKED');
}
onToolbarChatPanelButtonClicked() {
console.warn('VC chat CLICKED');
}
onCamClicked() {}
onScreenShareClicked() {}
onSpeakerLayoutClicked() {}
onLeaveSessionClicked() {
onToolbarLeaveButtonClicked() {
this.isSessionAlive = false;
console.log('VC LEAVE BUTTON CLICKED');
}
onCameraButtonClicked() {
console.warn('TOOLBAR camera CLICKED');
}
onMicrophoneButtonClicked() {
console.warn('TOOLBAR microphone CLICKED');
}
onScreenshareButtonClicked() {
console.warn('TOOLBAR screenshare CLICKED');
}
onFullscreenButtonClicked() {
console.warn('TOOLBAR fullscreen CLICKED');
}
onParticipantsPanelButtonClicked() {
console.warn('TOOLBAR participants CLICKED');
}
onChatPanelButtonClicked() {
console.warn('TOOLBAR chat CLICKED');
}
onLeaveButtonClicked() {
this.isSessionAlive = false;
console.log('TOOLBAR LEAVE CLICKED');
}
}

View File

@ -56,21 +56,13 @@ $openvidu-components-theme: mat.define-light-theme((
-webkit-font-smoothing: antialiased;
}
// Default openvidu-webcomponent colors
:root {
--ov-primary-color: #303030;
--ov-secondary-color: #586063;
--ov-tertiary-color: #598eff;
--ov-warn-color: #EB5144;
--ov-accent-color: #ffae35;
--ov-dark-color: #1d1d1d;
--ov-dark-light-color: #43484A;
--ov-light-color: #ffffff;
--ov-light-dark-color: #f1f1f1;
}
html, body { height: 100%; overflow: hidden;}
body { margin: 0; font-family: 'Roboto','RobotoDraft',Helvetica,Arial,sans-serif;}
body { margin: 0; font-family: 'Roboto','RobotoDraft',Helvetica,Arial,sans-serif;}
#poster-text {
padding: 0px !important;
}
ov-chat-panel .text-info {
margin: 3px auto !important;
}

View File

@ -1,69 +1,218 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, ElementRef, Input, OnInit, Output, EventEmitter } from '@angular/core';
import { ILogger, LoggerService, OpenViduService } from 'openvidu-angular';
import { Session } from 'openvidu-browser';
import { ParticipantAbstractModel } from '../../../projects/openvidu-angular/src/lib/models/participant.model';
export interface SessionConfig {
sessionName: string;
userName: string;
tokens: { webcam: string; screen: string };
export interface TokenModel {
webcam: string;
screen: string;
}
@Component({
template: `<ov-videoconference
*ngIf="successParams"
[sessionName]="_sessionConfig.sessionName"
[userName]="_sessionConfig.userName"
[openviduServerUrl]="openviduServerUrl"
[openviduSecret]="openviduSecret"
[tokens]="_sessionConfig.tokens"
></ov-videoconference>`
template: `
<ov-videoconference
*ngIf="success"
[participantName]="_participantName"
[tokens]="_tokens"
[minimal]="_minimal"
[prejoin]="_prejoin"
[videoMuted]="_videoMuted"
[audioMuted]="_audioMuted"
[toolbarScreenshareButton]="_toolbarScreenshareButton"
[toolbarFullscreenButton]="_toolbarFullscreenButton"
[toolbarLeaveButton]="_toolbarLeaveButton"
[toolbarChatPanelButton]="_toolbarChatPanelButton"
[toolbarParticipantsPanelButton]="_toolbarParticipantsPanelButton"
[toolbarDisplayLogo]="_toolbarDisplayLogo"
[toolbarDisplaySessionName]="_toolbarDisplaySessionName"
[streamDisplayParticipantName]="_streamDisplayParticipantName"
[streamDisplayAudioDetection]="_streamDisplayAudioDetection"
[streamSettingsButton]="_streamSettingsButton"
[participantPanelItemMuteButton]="_participantPanelItemMuteButton"
(onJoinButtonClicked)="_onJoinButtonClicked()"
(onToolbarLeaveButtonClicked)="_onToolbarLeaveButtonClicked()"
(onToolbarCameraButtonClicked)="_onToolbarCameraButtonClicked()"
(onToolbarMicrophoneButtonClicked)="_onToolbarMicrophoneButtonClicked()"
(onToolbarScreenshareButtonClicked)="_onToolbarScreenshareButtonClicked()"
(onToolbarParticipantsPanelButtonClicked)="_onToolbarParticipantsPanelButtonClicked()"
(onToolbarChatPanelButtonClicked)="_onToolbarChatPanelButtonClicked()"
(onToolbarFullscreenButtonClicked)="_onToolbarFullscreenButtonClicked()"
(onSessionCreated)="_onSessionCreated($event)"
(onParticipantCreated)="_onParticipantCreated($event)"
></ov-videoconference>
`
})
export class OpenviduWebComponentComponent implements OnInit {
@Input() openviduServerUrl: string;
@Input() openviduSecret: string;
_sessionConfig: SessionConfig;
_tokens: TokenModel;
_minimal: boolean = false;
_participantName: string;
_prejoin: boolean = true;
_videoMuted: boolean = false;
_audioMuted: boolean = false;
_toolbarScreenshareButton: boolean = true;
_toolbarFullscreenButton: boolean = true;
_toolbarLeaveButton: boolean = true;
_toolbarChatPanelButton: boolean = true;
_toolbarParticipantsPanelButton: boolean = true;
_toolbarDisplayLogo: boolean = true;
_toolbarDisplaySessionName: boolean = true;
_streamDisplayParticipantName: boolean = true;
_streamDisplayAudioDetection: boolean = true;
_streamSettingsButton: boolean = true;
_participantPanelItemMuteButton: boolean = true;
successParams: boolean = false;
@Input() set minimal(value: string | boolean) {
this._minimal = this.castToBoolean(value);
}
@Input() set participantName(value: string) {
this._participantName = value;
}
@Input() set prejoin(value: string | boolean) {
this._prejoin = this.castToBoolean(value);
}
@Input() set videoMuted(value: string | boolean) {
this._videoMuted = this.castToBoolean(value);
}
@Input() set audioMuted(value: string | boolean) {
this._audioMuted = this.castToBoolean(value);
}
constructor() {}
@Input() set toolbarScreenshareButton(value: string | boolean) {
this._toolbarScreenshareButton = this.castToBoolean(value);
}
@Input() set toolbarFullscreenButton(value: string | boolean) {
this._toolbarFullscreenButton = this.castToBoolean(value);
}
@Input() set toolbarLeaveButton(value: string | boolean) {
this._toolbarLeaveButton = this.castToBoolean(value);
}
@Input() set toolbarChatPanelButton(value: string | boolean) {
this._toolbarChatPanelButton = this.castToBoolean(value);
}
@Input() set toolbarParticipantsPanelButton(value: string | boolean) {
this._toolbarParticipantsPanelButton = this.castToBoolean(value);
}
@Input() set toolbarDisplayLogo(value: string | boolean) {
this._toolbarDisplayLogo = this.castToBoolean(value);
}
@Input() set toolbarDisplaySessionName(value: string | boolean) {
this._toolbarDisplaySessionName = this.castToBoolean(value);
}
@Input() set streamDisplayParticipantName(value: string | boolean) {
this._streamDisplayParticipantName = this.castToBoolean(value);
}
@Input() set streamDisplayAudioDetection(value: string | boolean) {
this._streamDisplayAudioDetection = this.castToBoolean(value);
}
@Input() set streamSettingsButton(value: string | boolean) {
this._streamSettingsButton = this.castToBoolean(value);
}
@Input() set participantPanelItemMuteButton(value: string | boolean) {
this._participantPanelItemMuteButton = this.castToBoolean(value);
}
@Output() onJoinButtonClicked = new EventEmitter<any>();
@Output() onToolbarLeaveButtonClicked = new EventEmitter<any>();
@Output() onToolbarCameraButtonClicked = new EventEmitter<any>();
@Output() onToolbarMicrophoneButtonClicked = new EventEmitter<any>();
@Output() onToolbarScreenshareButtonClicked = new EventEmitter<any>();
@Output() onToolbarParticipantsPanelButtonClicked = new EventEmitter<any>();
@Output() onToolbarChatPanelButtonClicked = new EventEmitter<any>();
@Output() onToolbarFullscreenButtonClicked = new EventEmitter<any>();
@Output() onSessionCreated = new EventEmitter<any>();
@Output() onParticipantCreated = new EventEmitter<any>();
success: boolean = false;
private log: ILogger;
constructor(private loggerService: LoggerService, private host: ElementRef, private openviduService: OpenViduService) {
this.log = this.loggerService.get('WebComponent');
this.host.nativeElement.leaveSession = this.leaveSession.bind(this);
}
ngOnInit(): void {}
@Input('sessionConfig')
set sessionConfig(config: SessionConfig | string) {
console.log('Webcomponent sessionConfig: ', config);
if (typeof config === 'string') {
@Input('tokens')
set tokens(value: TokenModel | string) {
this.log.d('Webcomponent tokens: ', value);
try {
this._tokens = this.castToJson(value);
this.success = !!this._tokens?.webcam && !!this._tokens?.screen;
} catch (error) {
this.log.e(error);
if (!this.success) {
this.log.e('Parameters received are incorrect: ', value);
this.log.e('Session cannot start');
}
}
}
_onJoinButtonClicked() {
this.onJoinButtonClicked.emit();
}
_onToolbarLeaveButtonClicked() {
this.onToolbarLeaveButtonClicked.emit();
}
_onToolbarCameraButtonClicked() {
this.onToolbarCameraButtonClicked.emit();
}
_onToolbarMicrophoneButtonClicked() {
this.onToolbarMicrophoneButtonClicked.emit();
}
_onToolbarScreenshareButtonClicked() {
this.onToolbarScreenshareButtonClicked.emit();
}
_onToolbarParticipantsPanelButtonClicked() {
this.onToolbarParticipantsPanelButtonClicked.emit();
}
_onToolbarChatPanelButtonClicked() {
this.onToolbarChatPanelButtonClicked.emit();
}
_onToolbarFullscreenButtonClicked() {
this.onToolbarFullscreenButtonClicked.emit();
}
_onSessionCreated(event: Session) {
this.onSessionCreated.emit(event);
}
_onParticipantCreated(event: ParticipantAbstractModel) {
this.onParticipantCreated.emit(event);
}
leaveSession() {
this.openviduService.disconnect();
}
private castToBoolean(value: string | boolean): boolean {
if (typeof value === 'boolean') {
return value;
} else if (typeof value === 'string') {
if (value !== 'true' && value !== 'false') {
throw new Error('Parameter has an incorrect string value.');
}
return value === 'true';
} else {
throw new Error('Parameter has not a valid type. The parameters must to be string or boolean.');
}
}
private castToJson(value: TokenModel | string) {
if (typeof value === 'string') {
try {
console.log('STRING')
config = JSON.parse(config);
return JSON.parse(value);
} catch (error) {
console.error('Unexpected JSON', error);
this.log.e('Unexpected JSON', error);
throw 'Unexpected JSON';
}
}
if (this.isEmpty(<SessionConfig>config)) {
// Leaving session when sessionConfig is empty
} else if (typeof value === 'object') {
return value;
} else {
console.log("URL",this.openviduServerUrl);
console.log('SECRET',this.openviduSecret);
this.successParams = this.isCorrectParams(<SessionConfig>config);
this._sessionConfig = <SessionConfig>config;
if (!this.successParams) {
console.error('Parameters received are incorrect: ', config);
console.error('Session cannot start');
}
throw new Error(
'Parameter has not a valid type. The parameters must to be string or TokenModel {webcam:string, screen: string}.'
);
}
}
private isCorrectParams(config: SessionConfig): boolean {
console.log(config)
const canGenerateToken = !!config.sessionName && !!config.userName && !!this.openviduServerUrl && !!this.openviduSecret;
const hasToken = !!config.tokens?.webcam && !!config.tokens?.screen && !!config.userName;
return canGenerateToken || hasToken;
}
private isEmpty(config: SessionConfig): boolean {
return Object.keys(config).length === 0;
}
}

View File

@ -5,23 +5,23 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { OpenviduWebComponentComponent } from './openvidu-webcomponent.component';
import { OpenviduAngularModule, VideoconferenceComponent } from 'openvidu-angular';
import { OpenViduAngularModule, VideoconferenceComponent } from 'openvidu-angular';
import { environment } from '../../environments/environment';
import { createCustomElement } from '@angular/elements';
@NgModule({
declarations: [OpenviduWebComponentComponent],
imports: [CommonModule, BrowserModule, BrowserAnimationsModule, OpenviduAngularModule.forRoot(environment)],
imports: [CommonModule, BrowserModule, BrowserAnimationsModule, OpenViduAngularModule.forRoot(environment)],
// exports: [OpenviduWebComponentComponent],
providers: [{ provide: APP_BASE_HREF, useValue: '/' }, VideoconferenceComponent]
providers: [{ provide: APP_BASE_HREF, useValue: '/' }, VideoconferenceComponent],
})
export class OpenviduWebComponentModule implements DoBootstrap {
constructor(private injector: Injector) {}
ngDoBootstrap(): void {
const element = createCustomElement(OpenviduWebComponentComponent, {
injector: this.injector
injector: this.injector,
});
customElements.define('openvidu-webcomponent', element);

View File

@ -27,54 +27,54 @@ export class RestService {
}
}
private createSession(sessionId: string, openviduServerUrl: string, openviduSecret: string): Promise<string> {
return new Promise((resolve, reject) => {
const body = JSON.stringify({ customSessionId: sessionId });
const options = {
headers: new HttpHeaders({
Authorization: 'Basic ' + btoa('OPENVIDUAPP:' + openviduSecret),
'Content-Type': 'application/json'
})
};
return this.http
.post<any>(openviduServerUrl + '/openvidu/api/sessions', body, options)
.pipe(
catchError((error) => {
if (error.status === 409) {
resolve(sessionId);
}
if (error.statusText === 'Unknown Error') {
reject({ status: 401, message: 'ERR_CERT_AUTHORITY_INVALID' });
}
return observableThrowError(error);
})
)
.subscribe((response) => {
resolve(response.id);
});
});
}
// private createSession(sessionId: string, openviduServerUrl: string, openviduSecret: string): Promise<string> {
// return new Promise((resolve, reject) => {
// const body = JSON.stringify({ customSessionId: sessionId });
// const options = {
// headers: new HttpHeaders({
// Authorization: 'Basic ' + btoa('OPENVIDUAPP:' + openviduSecret),
// 'Content-Type': 'application/json'
// })
// };
// return this.http
// .post<any>(openviduServerUrl + '/openvidu/api/sessions', body, options)
// .pipe(
// catchError((error) => {
// if (error.status === 409) {
// resolve(sessionId);
// }
// if (error.statusText === 'Unknown Error') {
// reject({ status: 401, message: 'ERR_CERT_AUTHORITY_INVALID' });
// }
// return observableThrowError(error);
// })
// )
// .subscribe((response) => {
// resolve(response.id);
// });
// });
// }
private createToken(sessionId: string, openviduServerUrl: string, openviduSecret: string): Promise<string> {
return new Promise((resolve, reject) => {
const body = JSON.stringify({});
const options = {
headers: new HttpHeaders({
Authorization: 'Basic ' + btoa('OPENVIDUAPP:' + openviduSecret),
'Content-Type': 'application/json'
})
};
return this.http
.post<any>(openviduServerUrl + '/openvidu/api/sessions/' + sessionId + '/connection', body, options)
.pipe(
catchError((error) => {
reject(error);
return observableThrowError(error);
})
)
.subscribe((response) => {
resolve(response.token);
});
});
}
// private createToken(sessionId: string, openviduServerUrl: string, openviduSecret: string): Promise<string> {
// return new Promise((resolve, reject) => {
// const body = JSON.stringify({});
// const options = {
// headers: new HttpHeaders({
// Authorization: 'Basic ' + btoa('OPENVIDUAPP:' + openviduSecret),
// 'Content-Type': 'application/json'
// })
// };
// return this.http
// .post<any>(openviduServerUrl + '/openvidu/api/sessions/' + sessionId + '/connection', body, options)
// .pipe(
// catchError((error) => {
// reject(error);
// return observableThrowError(error);
// })
// )
// .subscribe((response) => {
// resolve(response.token);
// });
// });
// }
}

View File

@ -1,3 +1,3 @@
export const environment = {
production: true
production: true,
};

View File

@ -3,7 +3,7 @@
// The list of file replacements can be found in `angular.json`.
export const environment = {
production: false
production: false,
};
/*

View File

@ -36,9 +36,8 @@ $openvidu-components-theme: mat.define-light-theme((
@include mat.all-component-themes($openvidu-components-theme);
// Custom openvidu-components colors
// Custom openvidu-components styles
:root {
--ov-primary-color: #303030;
--ov-secondary-color: #586063;
--ov-tertiary-color: #598eff;
@ -50,6 +49,11 @@ $openvidu-components-theme: mat.define-light-theme((
--ov-light-color: #ffffff;
--ov-light-dark-color: #f1f1f1;
--ov-buttons-radius: 50%; // border-radius property
--ov-leave-button-radius: 10px;
--ov-video-radius: 5px;
--ov-panel-radius: 5px;
}

Some files were not shown because too many files have changed in this diff Show More