Merge pull request #5309 from tsiegleauq/integrate-jitsi-meet-client
Integrate jitsi-meet in OpenSlides
This commit is contained in:
commit
a47285c0ff
@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["src/styles.scss"],
|
"styles": ["src/styles.scss"],
|
||||||
"scripts": ["node_modules/tinymce/tinymce.min.js"],
|
"scripts": ["node_modules/tinymce/tinymce.min.js", "src/assets/jitsi/external_api.js"],
|
||||||
"webWorkerTsConfig": "tsconfig.worker.json"
|
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
|
149
client/src/app/shared/components/jitsi/jitsi.component.html
Normal file
149
client/src/app/shared/components/jitsi/jitsi.component.html
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<div class="jitsi-integration">
|
||||||
|
<div
|
||||||
|
class="jitsi-bar"
|
||||||
|
[ngClass]="{
|
||||||
|
'cdk-visually-hidden': !enableJitsi,
|
||||||
|
'cast-shadow': !showJitsiWindow
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="control-icon-wrapper apply-theme"
|
||||||
|
[ngClass]="{
|
||||||
|
'cast-shadow': showJitsiWindow
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- mute/unmute button -->
|
||||||
|
<button
|
||||||
|
class="indicator quick-icon"
|
||||||
|
mat-mini-fab
|
||||||
|
*ngIf="isJoined"
|
||||||
|
(click)="toggleMute()"
|
||||||
|
matTooltip="{{ 'mute/unmute' | translate }}"
|
||||||
|
>
|
||||||
|
<mat-icon color="{{ muted ? 'primary' : 'warn' }}">{{ muted ? 'moff' : 'mic' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- disconnected icon -->
|
||||||
|
<mat-icon class="indicator" *ngIf="!isJoined">cloud_off</mat-icon>
|
||||||
|
|
||||||
|
<!-- hide unhide video -->
|
||||||
|
<!-- <button class="quick-icon" mat-mini-fab [disabled]="!isJoined">
|
||||||
|
<mat-icon *ngIf="isJoined" color="primary">videocam_off</mat-icon>
|
||||||
|
</button> -->
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="list-wrapper apply-theme"
|
||||||
|
[ngClass]="{
|
||||||
|
'cast-shadow': showJitsiWindow
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- <span class="list-wrapper apply-theme regular-shadow"> -->
|
||||||
|
<!-- open-window button -->
|
||||||
|
<button class="toggle-list-button" mat-button (click)="toggleShowJitsi()">
|
||||||
|
<span> {{ 'Audio Conference' | translate }}</span>
|
||||||
|
<mat-icon class="opened-indicator" *ngIf="!showJitsiWindow">keyboard_arrow_up</mat-icon>
|
||||||
|
<mat-icon class="opened-indicator" *ngIf="showJitsiWindow">keyboard_arrow_down </mat-icon>
|
||||||
|
|
||||||
|
<div class="one-line">
|
||||||
|
|
||||||
|
<span *ngIf="currentDominantSpeaker">
|
||||||
|
» <span class="dominant-speaker">{{ currentDominantSpeaker.displayName }}</span>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="!isJitsiActive">
|
||||||
|
<i>{{ 'disconnected' | translate }}</i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- unfolded list -->
|
||||||
|
<div
|
||||||
|
class="jitsi-list"
|
||||||
|
[ngClass]="{
|
||||||
|
'cdk-visually-hidden': !showJitsiWindow
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<!-- Jitsi content window -->
|
||||||
|
<div class="content">
|
||||||
|
<!-- The "somewhere else active" warning -->
|
||||||
|
<div class="disconnected" *ngIf="isJitsiActiveInAnotherTab && !isJitsiActive">
|
||||||
|
<span>{{ 'Another instance of Jitsi is active in you OpenSlides session' | translate }}</span>
|
||||||
|
<button mat-button color="warn" (click)="forceStart()">
|
||||||
|
<span>{{ 'Force Jitsi to reload' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="disconnected" *ngIf="!isJitsiActiveInAnotherTab && !isJitsiActive">
|
||||||
|
<span>{{ 'disconnected' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden jitsy container -->
|
||||||
|
<div [ngStyle]="{ display: 'none' }" #jitsi></div>
|
||||||
|
|
||||||
|
<!-- user list -->
|
||||||
|
<div class="room-members" *ngIf="isJitsiActive">
|
||||||
|
<div class="member-list">
|
||||||
|
<ol>
|
||||||
|
<li
|
||||||
|
*ngFor="let memberId of memberList; trackBy: trackByIndex"
|
||||||
|
[ngClass]="{
|
||||||
|
focused: members[memberId].focus
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="member-list-entry">
|
||||||
|
{{ members[memberId].name }}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Custom control buttons -->
|
||||||
|
<div>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div class="control-grid">
|
||||||
|
<div class="control-buttons">
|
||||||
|
<!-- Hangup -->
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
color="warn"
|
||||||
|
(click)="stopJitsi()"
|
||||||
|
*ngIf="isJitsiActive && isJoined"
|
||||||
|
matTooltip="{{ 'Leave' | translate }}"
|
||||||
|
>
|
||||||
|
<mat-icon>call_end</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Enter jitsi manually -->
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
color="accent"
|
||||||
|
(click)="enterConversation()"
|
||||||
|
[disabled]="isJitsiActive || isJitsiActiveInAnotherTab"
|
||||||
|
*ngIf="!isJoined"
|
||||||
|
matTooltip="{{ 'Enter conference' | translate }}"
|
||||||
|
>
|
||||||
|
<mat-icon>call</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Open in new tab -->
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
class="open-jitsi-in-tab"
|
||||||
|
color="accent"
|
||||||
|
(click)="openExternal()"
|
||||||
|
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
|
||||||
|
[disabled]="isRoomPasswordProtected"
|
||||||
|
>
|
||||||
|
<mat-icon>
|
||||||
|
open_in_new
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
116
client/src/app/shared/components/jitsi/jitsi.component.scss
Normal file
116
client/src/app/shared/components/jitsi/jitsi.component.scss
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
.jitsi-integration {
|
||||||
|
.cast-shadow {
|
||||||
|
box-shadow: -3px -3px 10px 0px rgba(0, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jitsi-bar {
|
||||||
|
z-index: 99;
|
||||||
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
right: 20px;
|
||||||
|
bottom: 0;
|
||||||
|
$wrapper-padding: 5px;
|
||||||
|
$bar-height: 40px;
|
||||||
|
|
||||||
|
.control-icon-wrapper {
|
||||||
|
z-index: 1;
|
||||||
|
min-height: $bar-height;
|
||||||
|
display: flex;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-right: 0.5em;
|
||||||
|
padding: $wrapper-padding 0 $wrapper-padding $wrapper-padding;
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
margin: auto $wrapper-padding auto 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-wrapper {
|
||||||
|
min-height: $bar-height;
|
||||||
|
padding-top: $wrapper-padding;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
width: 250px;
|
||||||
|
max-width: 250px;
|
||||||
|
|
||||||
|
.toggle-list-button {
|
||||||
|
position: relative;
|
||||||
|
line-height: normal;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 2.5em;
|
||||||
|
margin-bottom: $wrapper-padding;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.opened-indicator {
|
||||||
|
position: absolute;
|
||||||
|
right: $wrapper-padding;
|
||||||
|
top: $wrapper-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dominant-speaker {
|
||||||
|
font-weight: 500;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jitsi-list {
|
||||||
|
.content {
|
||||||
|
height: 40vh;
|
||||||
|
clear: both;
|
||||||
|
|
||||||
|
.disconnected {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: inherit;
|
||||||
|
padding-left: 1em;
|
||||||
|
padding-right: 1em;
|
||||||
|
|
||||||
|
span {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-members {
|
||||||
|
height: inherit;
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.member-list-entry {
|
||||||
|
margin: 5px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-grid {
|
||||||
|
padding: $wrapper-padding 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: 'helper buttons new-tab';
|
||||||
|
grid-template-columns: 40px auto 40px;
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
grid-area: buttons;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.open-jitsi-in-tab {
|
||||||
|
grid-area: new-tab;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
@import '~@angular/material/theming';
|
||||||
|
|
||||||
|
@mixin os-jitsi-theme($theme) {
|
||||||
|
$primary: map-get($theme, primary);
|
||||||
|
$accent: map-get($theme, accent);
|
||||||
|
$warn: map-get($theme, accent);
|
||||||
|
$foreground: map-get($theme, foreground);
|
||||||
|
$background: map-get($theme, background);
|
||||||
|
|
||||||
|
.jitsi-bar {
|
||||||
|
.apply-theme {
|
||||||
|
background-color: mat-color($primary);
|
||||||
|
|
||||||
|
.quick-icon:not([disabled]) {
|
||||||
|
background-color: mat-color($primary, default-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
color: mat-color($primary, default-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-list-button {
|
||||||
|
span {
|
||||||
|
color: mat-color($primary, default-contrast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.jitsi-list {
|
||||||
|
background-color: mat-color($background, card);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { JitsiComponent } from './jitsi.component';
|
||||||
|
|
||||||
|
describe('JitsiComponent', () => {
|
||||||
|
let component: JitsiComponent;
|
||||||
|
let fixture: ComponentFixture<JitsiComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [E2EImportsModule],
|
||||||
|
declarations: [JitsiComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(JitsiComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
341
client/src/app/shared/components/jitsi/jitsi.component.ts
Normal file
341
client/src/app/shared/components/jitsi/jitsi.component.ts
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
import { Component, ElementRef, HostListener, OnDestroy, ViewChild } from '@angular/core';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { BaseComponent } from 'app/base.component';
|
||||||
|
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||||
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
|
import { Deferred } from 'app/core/promises/deferred';
|
||||||
|
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||||
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
|
|
||||||
|
declare var JitsiMeetExternalAPI: any;
|
||||||
|
|
||||||
|
interface JitsiMember {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConferenceJoinedResult {
|
||||||
|
roomName: string;
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
formattedDisplayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DisplayNameChangeResult {
|
||||||
|
// Yes, in this case "displayname" really does not have a capital n. Thank you jitsi.
|
||||||
|
displayname: string;
|
||||||
|
formattedDisplayName: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JitsiSettings {
|
||||||
|
JITSI_DOMAIN: string;
|
||||||
|
JITSI_ROOM_NAME: string;
|
||||||
|
JITSI_ROOM_PASSWORD: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConferenceMember {
|
||||||
|
name: string;
|
||||||
|
focus: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-jitsi',
|
||||||
|
templateUrl: './jitsi.component.html',
|
||||||
|
styleUrls: ['./jitsi.component.scss']
|
||||||
|
})
|
||||||
|
export class JitsiComponent extends BaseComponent implements OnDestroy {
|
||||||
|
public enableJitsi: boolean;
|
||||||
|
private autoconnect: boolean;
|
||||||
|
private roomName: string;
|
||||||
|
private roomPassword: string;
|
||||||
|
private jitsiDomain: string;
|
||||||
|
|
||||||
|
// do not set the password twice
|
||||||
|
private isPasswortSet = false;
|
||||||
|
|
||||||
|
public showJitsiWindow = false;
|
||||||
|
public muted = true;
|
||||||
|
|
||||||
|
@ViewChild('jitsi')
|
||||||
|
private jitsiNode: ElementRef;
|
||||||
|
|
||||||
|
// JitsiMeet api object
|
||||||
|
private api: any | null;
|
||||||
|
|
||||||
|
public get isJitsiActive(): boolean {
|
||||||
|
return !!this.api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isJoined: boolean;
|
||||||
|
|
||||||
|
private options: object;
|
||||||
|
|
||||||
|
private lockLoaded: Deferred<void> = new Deferred();
|
||||||
|
private constantsLoaded: Deferred<void> = new Deferred();
|
||||||
|
|
||||||
|
// storage locks
|
||||||
|
public isJitsiActiveInAnotherTab: boolean;
|
||||||
|
private RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
|
||||||
|
|
||||||
|
// JitsiID to ConferenceMember
|
||||||
|
public members = {};
|
||||||
|
public currentDominantSpeaker: JitsiMember;
|
||||||
|
|
||||||
|
public get memberList(): string[] {
|
||||||
|
return Object.keys(this.members);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isRoomPasswordProtected(): boolean {
|
||||||
|
return this.roomPassword?.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private configOverwrite = {
|
||||||
|
startAudioOnly: true,
|
||||||
|
// allows jitsi on mobile devices
|
||||||
|
disableDeepLinking: true,
|
||||||
|
startWithAudioMuted: true,
|
||||||
|
startWithVideoMuted: true,
|
||||||
|
useNicks: true,
|
||||||
|
enableWelcomePage: false,
|
||||||
|
enableUserRolesBasedOnToken: false,
|
||||||
|
enableFeaturesBasedOnToken: false,
|
||||||
|
disableThirdPartyRequests: true,
|
||||||
|
enableNoAudioDetection: false,
|
||||||
|
enableNoisyMicDetection: false
|
||||||
|
};
|
||||||
|
|
||||||
|
private interfaceConfigOverwrite = {
|
||||||
|
filmStripOnly: true,
|
||||||
|
INITIAL_TOOLBAR_TIMEOUT: 2000,
|
||||||
|
TOOLBAR_TIMEOUT: 400,
|
||||||
|
SHOW_JITSI_WATERMARK: false,
|
||||||
|
SHOW_WATERMARK_FOR_GUESTS: false,
|
||||||
|
INVITATION_POWERED_BY: false,
|
||||||
|
DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
|
||||||
|
TOOLBAR_BUTTONS: [],
|
||||||
|
SETTINGS_SECTIONS: []
|
||||||
|
};
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
titleService: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
private operator: OperatorService,
|
||||||
|
private storageMap: StorageMap,
|
||||||
|
private userRepo: UserRepositoryService,
|
||||||
|
private constantsService: ConstantsService,
|
||||||
|
private configService: ConfigService
|
||||||
|
) {
|
||||||
|
super(titleService, translate);
|
||||||
|
this.setUp();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
this.stopJitsi();
|
||||||
|
}
|
||||||
|
|
||||||
|
// closing the tab should also try to stop jitsi.
|
||||||
|
// this will usually not be cought by ngOnDestroy
|
||||||
|
@HostListener('window:beforeunload', ['$event'])
|
||||||
|
public async beforeunload($event: any): Promise<void> {
|
||||||
|
await this.stopJitsi();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async setUp(): Promise<void> {
|
||||||
|
this.storageMap
|
||||||
|
.watch(this.RTC_LOGGED_STORAGE_KEY)
|
||||||
|
.pipe(distinctUntilChanged())
|
||||||
|
.subscribe((inUse: boolean) => {
|
||||||
|
this.isJitsiActiveInAnotherTab = inUse;
|
||||||
|
this.lockLoaded.resolve();
|
||||||
|
if (!inUse && !this.isJitsiActive) {
|
||||||
|
this.startJitsi();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.lockLoaded;
|
||||||
|
this.constantsService.get<JitsiSettings>('Settings').subscribe(settings => {
|
||||||
|
if (settings) {
|
||||||
|
this.jitsiDomain = settings.JITSI_DOMAIN;
|
||||||
|
this.roomName = settings.JITSI_ROOM_NAME;
|
||||||
|
this.roomPassword = settings.JITSI_ROOM_PASSWORD;
|
||||||
|
this.constantsLoaded.resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.constantsLoaded;
|
||||||
|
this.configService
|
||||||
|
.get<boolean>('general_system_conference_show')
|
||||||
|
.subscribe(autoconnect => (this.autoconnect = autoconnect));
|
||||||
|
|
||||||
|
this.configService.get<boolean>('general_system_conference_auto_connect').subscribe(show => {
|
||||||
|
this.enableJitsi = show && !!this.jitsiDomain && !!this.roomName;
|
||||||
|
if (this.enableJitsi && this.autoconnect) {
|
||||||
|
this.startJitsi();
|
||||||
|
} else {
|
||||||
|
this.stopJitsi();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleMute(): void {
|
||||||
|
if (this.isJitsiActive) {
|
||||||
|
this.api.executeCommand('toggleAudio');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async forceStart(): Promise<void> {
|
||||||
|
await this.deleteJitsiLock();
|
||||||
|
await this.stopJitsi();
|
||||||
|
await this.startJitsi();
|
||||||
|
}
|
||||||
|
|
||||||
|
private startJitsi(): void {
|
||||||
|
if (!this.isJitsiActiveInAnotherTab && this.enableJitsi && !this.isJitsiActive && this.jitsiNode) {
|
||||||
|
this.enterConversation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enterConversation(): Promise<void> {
|
||||||
|
await this.operator.loaded;
|
||||||
|
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
||||||
|
this.setOptions();
|
||||||
|
this.api = new JitsiMeetExternalAPI(this.jitsiDomain, this.options);
|
||||||
|
|
||||||
|
const jitsiname = this.userRepo.getShortName(this.operator.user);
|
||||||
|
this.api.executeCommand('displayName', jitsiname);
|
||||||
|
this.loadApiCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadApiCallbacks(): void {
|
||||||
|
this.api.on('videoConferenceJoined', (info: ConferenceJoinedResult) => {
|
||||||
|
this.onEnterConference(info);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('participantJoined', (newMember: JitsiMember) => {
|
||||||
|
this.addMember(newMember);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('participantLeft', (oldMember: { id: string }) => {
|
||||||
|
this.removeMember(oldMember);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('displayNameChange', (member: DisplayNameChangeResult) => {
|
||||||
|
this.renameMember(member);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('audioMuteStatusChanged', (isMuted: { muted: boolean }) => {
|
||||||
|
this.muted = isMuted.muted;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('readyToClose', () => {
|
||||||
|
this.stopJitsi();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('dominantSpeakerChanged', (newSpeaker: { id: string }) => {
|
||||||
|
this.newDominantSpeaker(newSpeaker.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('passwordRequired', () => {
|
||||||
|
this.setRoomPassword();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEnterConference(info: ConferenceJoinedResult): void {
|
||||||
|
this.isJoined = true;
|
||||||
|
this.addMember({ displayName: info.displayName, id: info.id });
|
||||||
|
this.setRoomPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setRoomPassword(): void {
|
||||||
|
if (this.roomPassword && !this.isPasswortSet) {
|
||||||
|
// You can only set the password after the server has recognized that you are
|
||||||
|
// the moderator. There is no event listener for that.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.api.executeCommand('password', this.roomPassword);
|
||||||
|
this.isPasswortSet = true;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private newDominantSpeaker(newSpeakerId: string): void {
|
||||||
|
if (this.currentDominantSpeaker && this.members[this.currentDominantSpeaker.id]) {
|
||||||
|
this.members[this.currentDominantSpeaker.id].focus = false;
|
||||||
|
}
|
||||||
|
this.members[newSpeakerId].focus = true;
|
||||||
|
this.currentDominantSpeaker = {
|
||||||
|
id: newSpeakerId,
|
||||||
|
displayName: this.members[newSpeakerId].name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private addMember(newMember: JitsiMember): void {
|
||||||
|
this.members[newMember.id] = {
|
||||||
|
name: newMember.displayName,
|
||||||
|
focus: false
|
||||||
|
} as ConferenceMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeMember(oldMember: { id: string }): void {
|
||||||
|
if (this.members[oldMember.id]) {
|
||||||
|
delete this.members[oldMember.id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renameMember(member: DisplayNameChangeResult): void {
|
||||||
|
if (this.members[member.id]) {
|
||||||
|
this.members[member.id].name = member.displayname;
|
||||||
|
}
|
||||||
|
if (this.currentDominantSpeaker?.id === member.id) {
|
||||||
|
this.newDominantSpeaker(member.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearMembers(): void {
|
||||||
|
this.members = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stopJitsi(): Promise<void> {
|
||||||
|
if (this.isJitsiActive) {
|
||||||
|
this.api.executeCommand('hangup');
|
||||||
|
this.clearMembers();
|
||||||
|
await this.deleteJitsiLock();
|
||||||
|
this.api.dispose();
|
||||||
|
this.api = undefined;
|
||||||
|
}
|
||||||
|
this.isJoined = false;
|
||||||
|
this.isPasswortSet = false;
|
||||||
|
this.currentDominantSpeaker = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setOptions(): void {
|
||||||
|
this.options = {
|
||||||
|
roomName: this.roomName,
|
||||||
|
parentNode: this.jitsiNode.nativeElement,
|
||||||
|
configOverwrite: this.configOverwrite,
|
||||||
|
interfaceConfigOverwrite: this.interfaceConfigOverwrite
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleShowJitsi(): void {
|
||||||
|
this.showJitsiWindow = !this.showJitsiWindow;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getJitsiMeetUrl(): string {
|
||||||
|
return `https://${this.jitsiDomain}/${this.roomName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public openExternal(): void {
|
||||||
|
this.stopJitsi();
|
||||||
|
window.open(this.getJitsiMeetUrl(), '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async deleteJitsiLock(): Promise<void> {
|
||||||
|
await this.storageMap.delete(this.RTC_LOGGED_STORAGE_KEY).toPromise();
|
||||||
|
}
|
||||||
|
}
|
@ -10,6 +10,11 @@ $pbl-height: var(--pbl-height);
|
|||||||
height: $pbl-height;
|
height: $pbl-height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// additional space if the jitsi integration is active
|
||||||
|
.pbl-ngrid-row:last-of-type {
|
||||||
|
margin-bottom: 30px !important;
|
||||||
|
}
|
||||||
|
|
||||||
.pbl-ngrid-cell {
|
.pbl-ngrid-cell {
|
||||||
height: inherit;
|
height: inherit;
|
||||||
}
|
}
|
||||||
|
@ -126,6 +126,7 @@ import { AssignmentPollDetailContentComponent } from './components/assignment-po
|
|||||||
|
|
||||||
import { GlobalSpinnerComponent } from './components/global-spinner/global-spinner.component';
|
import { GlobalSpinnerComponent } from './components/global-spinner/global-spinner.component';
|
||||||
import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
||||||
|
import { JitsiComponent } from './components/jitsi/jitsi.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share Module for all "dumb" components and pipes.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -292,7 +293,8 @@ import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
|||||||
PollPercentBasePipe,
|
PollPercentBasePipe,
|
||||||
VotingPrivacyWarningComponent,
|
VotingPrivacyWarningComponent,
|
||||||
MotionPollDetailContentComponent,
|
MotionPollDetailContentComponent,
|
||||||
AssignmentPollDetailContentComponent
|
AssignmentPollDetailContentComponent,
|
||||||
|
JitsiComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -352,7 +354,8 @@ import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
|||||||
PollPercentBasePipe,
|
PollPercentBasePipe,
|
||||||
VotingPrivacyWarningComponent,
|
VotingPrivacyWarningComponent,
|
||||||
MotionPollDetailContentComponent,
|
MotionPollDetailContentComponent,
|
||||||
AssignmentPollDetailContentComponent
|
AssignmentPollDetailContentComponent,
|
||||||
|
JitsiComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
<os-grid-layout *ngIf="projector">
|
<os-grid-layout *ngIf="projector">
|
||||||
<os-tile [preferredSize]="projectorTileSizeLeft">
|
<os-tile [preferredSize]="projectorTileSizeLeft">
|
||||||
<div *ngIf="projector" class="projector-detail-wrapper column-left">
|
<div *ngIf="projector" class="projector-detail-wrapper column-left">
|
||||||
<a [routerLink]="['/projector', projector.id]">
|
<a [routerLink]="['/projector', projector.id]" target="_blank">
|
||||||
<div id="projector">
|
<div id="projector">
|
||||||
<os-projector [projector]="projector"></os-projector>
|
<os-projector [projector]="projector"></os-projector>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container class="meta-text-block-content">
|
<ng-container class="meta-text-block-content">
|
||||||
<a class="no-markup" [routerLink]="getDetailLink()">
|
<a class="no-markup" [routerLink]="getDetailLink()" [target]="projectionTarget">
|
||||||
<div class="projector">
|
<div class="projector">
|
||||||
<os-projector [projector]="projector"></os-projector>
|
<os-projector [projector]="projector"></os-projector>
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,6 +35,14 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On
|
|||||||
return this._projector;
|
return this._projector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get projectionTarget(): '_blank' | '_self' {
|
||||||
|
if (this.operator.hasPerms('core.can_manage_projector')) {
|
||||||
|
return '_self';
|
||||||
|
} else {
|
||||||
|
return '_blank';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _projector: ViewProjector;
|
private _projector: ViewProjector;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -89,6 +89,7 @@
|
|||||||
<mat-icon>arrow_forward_ios</mat-icon>
|
<mat-icon>arrow_forward_ios</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<os-jitsi></os-jitsi>
|
||||||
<div (touchstart)="swipe($event, 'start')" (touchend)="swipe($event, 'end')" class="content">
|
<div (touchstart)="swipe($event, 'start')" (touchend)="swipe($event, 'end')" class="content">
|
||||||
<main>
|
<main>
|
||||||
<router-outlet #o="outlet"></router-outlet>
|
<router-outlet #o="outlet"></router-outlet>
|
||||||
|
2
client/src/assets/jitsi/external_api.js
Normal file
2
client/src/assets/jitsi/external_api.js
Normal file
File diff suppressed because one or more lines are too long
@ -32,6 +32,7 @@
|
|||||||
@import './app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
|
@import './app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
|
||||||
@import './app/site/assignments/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
|
@import './app/site/assignments/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
|
||||||
@import './app/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss';
|
@import './app/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss';
|
||||||
|
@import './app/shared/components/jitsi/jitsi.component.scss-theme.scss';
|
||||||
|
|
||||||
/** fonts */
|
/** fonts */
|
||||||
@import './assets/styles/fonts.scss';
|
@import './assets/styles/fonts.scss';
|
||||||
@ -64,6 +65,7 @@ $narrow-spacing: (
|
|||||||
@include os-motion-poll-detail-style($theme);
|
@include os-motion-poll-detail-style($theme);
|
||||||
@include os-assignment-poll-detail-style($theme);
|
@include os-assignment-poll-detail-style($theme);
|
||||||
@include os-progress-snack-bar-style($theme);
|
@include os-progress-snack-bar-style($theme);
|
||||||
|
@include os-jitsi-theme($theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load projector specific SCSS values */
|
/** Load projector specific SCSS values */
|
||||||
@ -335,9 +337,15 @@ b,
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: gray;
|
color: gray;
|
||||||
}
|
}
|
||||||
mat-card {
|
|
||||||
|
.regular-shadow {
|
||||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37) !important;
|
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mat-card {
|
||||||
|
@extend .regular-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
.os-card {
|
.os-card {
|
||||||
max-width: 770px;
|
max-width: 770px;
|
||||||
margin-top: 20px !important;
|
margin-top: 20px !important;
|
||||||
|
@ -145,6 +145,9 @@ class CoreAppConfig(AppConfig):
|
|||||||
"PING_INTERVAL",
|
"PING_INTERVAL",
|
||||||
"PING_TIMEOUT",
|
"PING_TIMEOUT",
|
||||||
"ENABLE_ELECTRONIC_VOTING",
|
"ENABLE_ELECTRONIC_VOTING",
|
||||||
|
"JITSI_DOMAIN",
|
||||||
|
"JITSI_ROOM_NAME",
|
||||||
|
"JITSI_ROOM_PASSWORD",
|
||||||
]
|
]
|
||||||
client_settings_dict = {}
|
client_settings_dict = {}
|
||||||
for key in client_settings_keys:
|
for key in client_settings_keys:
|
||||||
|
@ -122,6 +122,24 @@ def get_config_variables():
|
|||||||
subgroup="System",
|
subgroup="System",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name="general_system_conference_auto_connect",
|
||||||
|
default_value=False,
|
||||||
|
input_type="boolean",
|
||||||
|
label="Show the audio conferece integration at the bottom right corner",
|
||||||
|
weight=142,
|
||||||
|
subgroup="System",
|
||||||
|
)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name="general_system_conference_show",
|
||||||
|
default_value=False,
|
||||||
|
input_type="boolean",
|
||||||
|
label="Allow users to automatically connect to the audio conference",
|
||||||
|
weight=143,
|
||||||
|
subgroup="System",
|
||||||
|
)
|
||||||
|
|
||||||
# General export settings
|
# General export settings
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
|
Loading…
Reference in New Issue
Block a user