From 1439444b2eb01c4fae58776dc78f37e110c4f3b6 Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 30 Apr 2020 11:07:32 +0200 Subject: [PATCH] Integrate jitsi-meet in OpenSlides - minimal jitsi client in the bottom right of the screen - can be shown and hidden like a messenger - allows to mute, unmute, call, stop call - automatically connects to a conference - shows a list of users connected to the room - jitsi iframe is currently hidden - "open in jitsi meet" link - only one connection will be opened if using multiple tabs - JITSI_DOMAIN and JITSI_ROOM_NAME must be present in the settings.py - config variables in settings --- client/angular.json | 2 +- .../components/jitsi/jitsi.component.html | 149 ++++++++ .../components/jitsi/jitsi.component.scss | 116 ++++++ .../jitsi/jitsi.component.scss-theme.scss | 33 ++ .../components/jitsi/jitsi.component.spec.ts | 27 ++ .../components/jitsi/jitsi.component.ts | 341 ++++++++++++++++++ .../list-view-table.component.scss | 5 + client/src/app/shared/shared.module.ts | 7 +- .../projector-detail.component.html | 2 +- .../projector-list-entry.component.html | 2 +- .../projector-list-entry.component.ts | 8 + client/src/app/site/site.component.html | 1 + client/src/assets/jitsi/external_api.js | 2 + client/src/styles.scss | 10 +- openslides/core/apps.py | 3 + openslides/core/config_variables.py | 18 + 16 files changed, 720 insertions(+), 6 deletions(-) create mode 100644 client/src/app/shared/components/jitsi/jitsi.component.html create mode 100644 client/src/app/shared/components/jitsi/jitsi.component.scss create mode 100644 client/src/app/shared/components/jitsi/jitsi.component.scss-theme.scss create mode 100644 client/src/app/shared/components/jitsi/jitsi.component.spec.ts create mode 100644 client/src/app/shared/components/jitsi/jitsi.component.ts create mode 100644 client/src/assets/jitsi/external_api.js diff --git a/client/angular.json b/client/angular.json index 9bdf02a82..30c0361f1 100644 --- a/client/angular.json +++ b/client/angular.json @@ -43,7 +43,7 @@ } ], "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" }, "configurations": { diff --git a/client/src/app/shared/components/jitsi/jitsi.component.html b/client/src/app/shared/components/jitsi/jitsi.component.html new file mode 100644 index 000000000..5dc6babf9 --- /dev/null +++ b/client/src/app/shared/components/jitsi/jitsi.component.html @@ -0,0 +1,149 @@ +
+
+ + + + + + cloud_off + + + + + + + + + + + +
+ +
+ +
+ {{ 'Another instance of Jitsi is active in you OpenSlides session' | translate }} + +
+ +
+ {{ 'disconnected' | translate }} +
+ + +
+ + +
+
+
    +
  1. +
    + {{ members[memberId].name }} +
    +
  2. +
+
+
+
+ + +
+ +
+
+ + + + + +
+ + + +
+
+
+
+
+
diff --git a/client/src/app/shared/components/jitsi/jitsi.component.scss b/client/src/app/shared/components/jitsi/jitsi.component.scss new file mode 100644 index 000000000..8f28f8fb4 --- /dev/null +++ b/client/src/app/shared/components/jitsi/jitsi.component.scss @@ -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; + } + } + } + } + } +} diff --git a/client/src/app/shared/components/jitsi/jitsi.component.scss-theme.scss b/client/src/app/shared/components/jitsi/jitsi.component.scss-theme.scss new file mode 100644 index 000000000..0132a0439 --- /dev/null +++ b/client/src/app/shared/components/jitsi/jitsi.component.scss-theme.scss @@ -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); + } +} diff --git a/client/src/app/shared/components/jitsi/jitsi.component.spec.ts b/client/src/app/shared/components/jitsi/jitsi.component.spec.ts new file mode 100644 index 000000000..ec145d053 --- /dev/null +++ b/client/src/app/shared/components/jitsi/jitsi.component.spec.ts @@ -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; + + 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(); + }); +}); diff --git a/client/src/app/shared/components/jitsi/jitsi.component.ts b/client/src/app/shared/components/jitsi/jitsi.component.ts new file mode 100644 index 000000000..2d1793299 --- /dev/null +++ b/client/src/app/shared/components/jitsi/jitsi.component.ts @@ -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 = new Deferred(); + private constantsLoaded: Deferred = 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 { + await this.stopJitsi(); + } + + private async setUp(): Promise { + 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('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('general_system_conference_show') + .subscribe(autoconnect => (this.autoconnect = autoconnect)); + + this.configService.get('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 { + 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 { + 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 { + 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 { + await this.storageMap.delete(this.RTC_LOGGED_STORAGE_KEY).toPromise(); + } +} diff --git a/client/src/app/shared/components/list-view-table/list-view-table.component.scss b/client/src/app/shared/components/list-view-table/list-view-table.component.scss index 05d5d3014..7cd233d80 100644 --- a/client/src/app/shared/components/list-view-table/list-view-table.component.scss +++ b/client/src/app/shared/components/list-view-table/list-view-table.component.scss @@ -10,6 +10,11 @@ $pbl-height: var(--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 { height: inherit; } diff --git a/client/src/app/shared/shared.module.ts b/client/src/app/shared/shared.module.ts index 5dd09f6a6..1aef46782 100644 --- a/client/src/app/shared/shared.module.ts +++ b/client/src/app/shared/shared.module.ts @@ -126,6 +126,7 @@ import { AssignmentPollDetailContentComponent } from './components/assignment-po import { GlobalSpinnerComponent } from './components/global-spinner/global-spinner.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. @@ -292,7 +293,8 @@ import { UserMenuComponent } from './components/user-menu/user-menu.component'; PollPercentBasePipe, VotingPrivacyWarningComponent, MotionPollDetailContentComponent, - AssignmentPollDetailContentComponent + AssignmentPollDetailContentComponent, + JitsiComponent ], declarations: [ PermsDirective, @@ -352,7 +354,8 @@ import { UserMenuComponent } from './components/user-menu/user-menu.component'; PollPercentBasePipe, VotingPrivacyWarningComponent, MotionPollDetailContentComponent, - AssignmentPollDetailContentComponent + AssignmentPollDetailContentComponent, + JitsiComponent ], providers: [ { diff --git a/client/src/app/site/projector/components/projector-detail/projector-detail.component.html b/client/src/app/site/projector/components/projector-detail/projector-detail.component.html index 6e3a7c3bd..f5dd9311a 100644 --- a/client/src/app/site/projector/components/projector-detail/projector-detail.component.html +++ b/client/src/app/site/projector/components/projector-detail/projector-detail.component.html @@ -41,7 +41,7 @@
- +
diff --git a/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.html b/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.html index a39582243..99208999a 100644 --- a/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.html +++ b/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.html @@ -24,7 +24,7 @@ -
+
diff --git a/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.ts b/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.ts index de1f27d79..49305e5df 100644 --- a/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.ts +++ b/client/src/app/site/projector/components/projector-list-entry/projector-list-entry.component.ts @@ -35,6 +35,14 @@ export class ProjectorListEntryComponent extends BaseViewComponent implements On return this._projector; } + public get projectionTarget(): '_blank' | '_self' { + if (this.operator.hasPerms('core.can_manage_projector')) { + return '_self'; + } else { + return '_blank'; + } + } + private _projector: ViewProjector; /** diff --git a/client/src/app/site/site.component.html b/client/src/app/site/site.component.html index 20bd32292..6b762b894 100644 --- a/client/src/app/site/site.component.html +++ b/client/src/app/site/site.component.html @@ -89,6 +89,7 @@ arrow_forward_ios
+
diff --git a/client/src/assets/jitsi/external_api.js b/client/src/assets/jitsi/external_api.js new file mode 100644 index 000000000..a55ba43b0 --- /dev/null +++ b/client/src/assets/jitsi/external_api.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.JitsiMeetExternalAPI=t():e.JitsiMeetExternalAPI=t()}(window,(function(){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var i=t[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,n),i.l=!0,i.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(r,i,function(t){return e[t]}.bind(null,i));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/libs/",n(n.s=6)}([function(e,t,n){"use strict";(function(e){n.d(t,"a",(function(){return s})),n.d(t,"b",(function(){return o})),n.d(t,"c",(function(){return a})),n.d(t,"d",(function(){return c})),n.d(t,"e",(function(){return u})),n.d(t,"f",(function(){return l})),n.d(t,"g",(function(){return h})),n.d(t,"h",(function(){return p}));var r=n(5);const i=n.n(r).a.getLogger(e);function s(e){return e.sendRequest({type:"devices",name:"getAvailableDevices"}).catch(e=>(i.error(e),{}))}function o(e){return e.sendRequest({type:"devices",name:"getCurrentDevices"}).catch(e=>(i.error(e),{}))}function a(e,t){return e.sendRequest({deviceType:t,type:"devices",name:"isDeviceChangeAvailable"})}function c(e){return e.sendRequest({type:"devices",name:"isDeviceListAvailable"})}function u(e){return e.sendRequest({type:"devices",name:"isMultipleAudioInputSupported"})}function l(e,t,n){return d(e,{id:n,kind:"audioinput",label:t})}function h(e,t,n){return d(e,{id:n,kind:"audiooutput",label:t})}function d(e,t){return e.sendRequest({type:"devices",name:"setDevice",device:t})}function p(e,t,n){return d(e,{id:n,kind:"videoinput",label:t})}}).call(this,"modules/API/external/functions.js")},function(e,t){var n={trace:0,debug:1,info:2,log:3,warn:4,error:5};o.consoleTransport=console;var r=[o.consoleTransport];o.addGlobalTransport=function(e){-1===r.indexOf(e)&&r.push(e)},o.removeGlobalTransport=function(e){var t=r.indexOf(e);-1!==t&&r.splice(t,1)};var i={};function s(){var e=arguments[0],t=arguments[1],s=Array.prototype.slice.call(arguments,2);if(!(n[t]1&&h.push("<"+o.methodName+">: ");var d=h.concat(s);l.bind(u).apply(u,d)}}}function o(e,t,r,i){this.id=t,this.options=i||{},this.transports=r,this.transports||(this.transports=[]),this.level=n[e];for(var o=Object.keys(n),a=0;a0&&o.length>i&&!o.warned){o.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+o.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=o.length,a=c,console&&console.warn&&console.warn(a)}return e}function h(){for(var e=[],t=0;t0&&(o=t[0]),o instanceof Error)throw o;var a=new Error("Unhandled error."+(o?" ("+o.message+")":""));throw a.context=o,a}var c=i[e];if(void 0===c)return!1;if("function"==typeof c)s(c,this,t);else{var u=c.length,l=v(c,u);for(n=0;n=0;s--)if(n[s]===t||n[s].listener===t){o=n[s].listener,i=s;break}if(i<0)return this;0===i?n.shift():function(e,t){for(;t+1=0;r--)this.removeListener(e,t[r]);return this},a.prototype.listeners=function(e){return p(this,e,!0)},a.prototype.rawListeners=function(e){return p(this,e,!1)},a.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):f.call(e,t)},a.prototype.listenerCount=f,a.prototype.eventNames=function(){return this._eventsCount>0?r(this._events):[]}},function(e,t){e.exports=function(e){var t,n=e.scope,r=e.window,i=e.windowForEventListening||window,s={},o=[],a={},c=!1,u=function(e){var t;try{t=JSON.parse(e.data)}catch(e){return}if(t.postis&&t.scope===n){var r=s[t.method];if(r)for(var i=0;i=this.maxEntryLength&&this._flush(!0,!0)},i.prototype.start=function(){this._reschedulePublishInterval()},i.prototype._reschedulePublishInterval=function(){this.storeLogsIntervalID&&(window.clearTimeout(this.storeLogsIntervalID),this.storeLogsIntervalID=null),this.storeLogsIntervalID=window.setTimeout(this._flush.bind(this,!1,!0),this.storeInterval)},i.prototype.flush=function(){this._flush(!1,!0)},i.prototype._flush=function(e,t){this.totalLen>0&&(this.logStorage.isReady()||e)&&(this.logStorage.isReady()?(this.outputCache.length&&(this.outputCache.forEach(function(e){this.logStorage.storeLogs(e)}.bind(this)),this.outputCache=[]),this.logStorage.storeLogs(this.queue)):this.outputCache.push(this.queue),this.queue=[],this.totalLen=0),t&&this._reschedulePublishInterval()},i.prototype.stop=function(){this._flush(!1,!1)},e.exports=i},function(e,t,n){"use strict";n.r(t);var r=n(2),i=n.n(r);const s="org.jitsi.meet:",o="(//[^/?#]+)",a="([^?#]*)",c="^([a-z][a-z0-9\\.\\+-]*:)";function u(e){const t=new RegExp(`${c}+`,"gi"),n=t.exec(e);if(n){let r=n[n.length-1].toLowerCase();"http:"!==r&&"https:"!==r&&(r="https:"),(e=e.substring(t.lastIndex)).startsWith("//")&&(e=r+e)}return e}function l(e={}){const t=[];for(const n in e)try{t.push(`${n}=${encodeURIComponent(JSON.stringify(e[n]))}`)}catch(e){console.warn(`Error encoding ${n}: ${e}`)}return t}function h(e){const t={toString:d};let n,r,i;if(e=e.replace(/\s/g,""),(r=(n=new RegExp(c,"gi")).exec(e))&&(t.protocol=r[1].toLowerCase(),e=e.substring(n.lastIndex)),r=(n=new RegExp(`^${o}`,"gi")).exec(e)){let i=r[1].substring(2);e=e.substring(n.lastIndex);const s=i.indexOf("@");-1!==s&&(i=i.substring(s+1)),t.host=i;const o=i.lastIndexOf(":");-1!==o&&(t.port=i.substring(o+1),i=i.substring(0,o)),t.hostname=i}if((r=(n=new RegExp(`^${a}`,"gi")).exec(e))&&(i=r[1],e=e.substring(n.lastIndex)),i?i.startsWith("/")||(i=`/${i}`):i="/",t.pathname=i,e.startsWith("?")){let n=e.indexOf("#",1);-1===n&&(n=e.length),t.search=e.substring(0,n),e=e.substring(n)}else t.search="";return t.hash=e.startsWith("#")?e:"",t}function d(e){const{hash:t,host:n,pathname:r,protocol:i,search:s}=e||this;let o="";return i&&(o+=i),n&&(o+=`//${n}`),o+=r||"/",s&&(o+=s),t&&(o+=t),o}function p(e){let t;const n=h(u(t=e.serverURL&&e.room?new URL(e.room,e.serverURL).toString():e.room?e.room:e.url||""));if(!n.protocol){let t=e.protocol||e.scheme;t&&(t.endsWith(":")||(t+=":"),n.protocol=t)}let{pathname:r}=n;if(!n.host){const t=e.domain||e.host||e.hostname;if(t){const{host:e,hostname:i,pathname:o,port:a}=h(u(`${s}//${t}`));e&&(n.host=e,n.hostname=i,n.port=a),"/"===r&&"/"!==o&&(r=o)}}const i=e.roomName||e.room;!i||!n.pathname.endsWith("/")&&n.pathname.endsWith(`/${i}`)||(r.endsWith("/")||(r+="/"),r+=i),n.pathname=r;const{jwt:o}=e;if(o){let{search:e}=n;-1===e.indexOf("?jwt=")&&-1===e.indexOf("&jwt=")&&(e.startsWith("?")||(e=`?${e}`),1===e.length||(e+="&"),e+=`jwt=${o}`,n.search=e)}let{hash:a}=n;for(const t of["config","interfaceConfig","devices","userInfo"]){const n=l(e[`${t}Overwrite`]||e[t]||e[`${t}Override`]);if(n.length){let e=`${t}.${n.join(`&${t}.`)}`;a.length?e=`&${e}`:a="#",a+=e}}return n.hash=a,n.toString()||void 0}const f=function(e,t=!1,n="hash"){const r="search"===n?e.search:e.hash,i={},s=r&&r.substr(1).split("&")||[];if("hash"===n&&1===s.length){const e=s[0];if(e.startsWith("/")&&1===e.split("&").length)return i}return s.forEach(e=>{const n=e.split("="),r=n[0];if(!r)return;let s;try{if(s=n[1],!t){const e=decodeURIComponent(s).replace(/\\&/,"&");s="undefined"===e?void 0:JSON.parse(e)}}catch(e){return void function(e,t=""){console.error(t,e),window.onerror&&window.onerror(t,null,null,null,e)}(e,`Failed to parse URL parameter value: ${String(s)}`)}i[r]=s}),i}(window.location).jitsi_meet_external_api_id;var v=n(3),m=n.n(v);function g(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}const y={window:window.opener||window.parent},b="message";class _{constructor({postisOptions:e}={}){this.postis=m()(function(e){for(var t=1;t{},this.postis.listen(b,e=>this._receiveCallback(e))}dispose(){this.postis.destroy()}send(e){this.postis.send({method:b,params:e})}setReceiveCallback(e){this._receiveCallback=e}}const w="event",L="request",O="response";class x{constructor({backend:e}={}){this._listeners=new Map,this._requestID=0,this._responseHandlers=new Map,this._unprocessedMessages=new Set,this.addListener=this.on,e&&this.setBackend(e)}_disposeBackend(){this._backend&&(this._backend.dispose(),this._backend=null)}_onMessageReceived(e){if(e.type===O){const t=this._responseHandlers.get(e.id);t&&(t(e),this._responseHandlers.delete(e.id))}else e.type===L?this.emit("request",e.data,(t,n)=>{this._backend.send({type:O,error:n,id:e.id,result:t})}):this.emit("event",e.data)}dispose(){this._responseHandlers.clear(),this._unprocessedMessages.clear(),this.removeAllListeners(),this._disposeBackend()}emit(e,...t){const n=this._listeners.get(e);let r=!1;return n&&n.size&&n.forEach(e=>{r=e(...t)||r}),r||this._unprocessedMessages.add(t),r}on(e,t){let n=this._listeners.get(e);return n||(n=new Set,this._listeners.set(e,n)),n.add(t),this._unprocessedMessages.forEach(e=>{t(...e)&&this._unprocessedMessages.delete(e)}),this}removeAllListeners(e){return e?this._listeners.delete(e):this._listeners.clear(),this}removeListener(e,t){const n=this._listeners.get(e);return n&&n.delete(t),this}sendEvent(e={}){this._backend&&this._backend.send({type:w,data:e})}sendRequest(e){if(!this._backend)return Promise.reject(new Error("No transport backend defined!"));this._requestID++;const t=this._requestID;return new Promise((n,r)=>{this._responseHandlers.set(t,({error:e,result:t})=>{void 0!==t?n(t):r(void 0!==e?e:new Error("Unexpected response format!"))}),this._backend.send({type:L,data:e,id:t})})}setBackend(e){this._disposeBackend(),this._backend=e,this._backend.setReceiveCallback(this._onMessageReceived.bind(this))}}const j={};let C;"number"==typeof f&&(j.scope=`jitsi_meet_external_api_${f}`),(window.JitsiMeetJS||(window.JitsiMeetJS={}),window.JitsiMeetJS.app||(window.JitsiMeetJS.app={}),window.JitsiMeetJS.app).setExternalTransportBackend=e=>C.setBackend(e);var E=n(4),S=n(0);function I(e,t){if(null==e)return{};var n,r,i=function(e,t){if(null==e)return{};var n,r,i={},s=Object.keys(e);for(r=0;r=0||(i[n]=e[n]);return i}(e,t);if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(i[n]=e[n])}return i}function R(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}n.d(t,"default",(function(){return $}));const k=["css/all.css","libs/alwaysontop.min.js"],P={avatarUrl:"avatar-url",displayName:"display-name",email:"email",hangup:"video-hangup",password:"password",sendEndpointTextMessage:"send-endpoint-text-message",sendTones:"send-tones",subject:"subject",submitFeedback:"submit-feedback",toggleAudio:"toggle-audio",toggleChat:"toggle-chat",toggleFilmStrip:"toggle-film-strip",toggleShareScreen:"toggle-share-screen",toggleTileView:"toggle-tile-view",toggleVideo:"toggle-video"},N={"avatar-changed":"avatarChanged","audio-availability-changed":"audioAvailabilityChanged","audio-mute-status-changed":"audioMuteStatusChanged","camera-error":"cameraError","device-list-changed":"deviceListChanged","display-name-change":"displayNameChange","email-change":"emailChange","endpoint-text-message-received":"endpointTextMessageReceived","feedback-submitted":"feedbackSubmitted","feedback-prompt-displayed":"feedbackPromptDisplayed","filmstrip-display-changed":"filmstripDisplayChanged","incoming-message":"incomingMessage","mic-error":"micError","outgoing-message":"outgoingMessage","participant-joined":"participantJoined","participant-kicked-out":"participantKickedOut","participant-left":"participantLeft","password-required":"passwordRequired","proxy-connection-event":"proxyConnectionEvent","video-ready-to-close":"readyToClose","video-conference-joined":"videoConferenceJoined","video-conference-left":"videoConferenceLeft","video-availability-changed":"videoAvailabilityChanged","video-mute-status-changed":"videoMuteStatusChanged","screen-sharing-status-changed":"screenSharingStatusChanged","dominant-speaker-changed":"dominantSpeakerChanged","subject-change":"subjectChange","suspend-detected":"suspendDetected","tile-view-changed":"tileViewChanged"};let M=0;function A(e,t){e._numberOfParticipants+=t}function D(e,t={}){return p(function(e){for(var t=1;t0&&this.invite(h),this._isLargeVideoVisible=!0,this._numberOfParticipants=0,this._participants={},this._myUserID=void 0,this._onStageParticipant=void 0,this._setupListeners(),M++}_createIFrame(e,t,n){const r=`jitsiConferenceFrame${M}`;this._frame=document.createElement("iframe"),this._frame.allow="camera; microphone; display-capture",this._frame.src=this._url,this._frame.name=r,this._frame.id=r,this._setSize(e,t),this._frame.setAttribute("allowFullScreen","true"),this._frame.style.border=0,n&&(this._frame.onload=n),this._frame=this._parentNode.appendChild(this._frame)}_getAlwaysOnTopResources(){const e=this._frame.contentWindow,t=e.document;let n="";const r=t.querySelector("base");if(r&&r.href)n=r.href;else{const{protocol:t,host:r}=e.location;n=`${t}//${r}`}return k.map(e=>new URL(e,n).href)}_getOnStageParticipant(){return this._onStageParticipant}_getLargeVideo(){const e=this.getIFrame();if(this._isLargeVideoVisible&&e&&e.contentWindow&&e.contentWindow.document)return e.contentWindow.document.getElementById("largeVideo")}_getParticipantVideo(e){const t=this.getIFrame();if(t&&t.contentWindow&&t.contentWindow.document)return void 0===e||e===this._myUserID?t.contentWindow.document.getElementById("localVideo_container"):t.contentWindow.document.querySelector(`#participant_${e} video`)}_setSize(e,t){const n=T(e),r=T(t);void 0!==n&&(this._frame.style.height=n),void 0!==r&&(this._frame.style.width=r)}_setupListeners(){this._transport.on("event",e=>{let{name:t}=e,n=I(e,["name"]);const r=n.id;switch(t){case"video-conference-joined":this._myUserID=r,this._participants[r]={avatarURL:n.avatarURL};case"participant-joined":this._participants[r]=this._participants[r]||{},this._participants[r].displayName=n.displayName,this._participants[r].formattedDisplayName=n.formattedDisplayName,A(this,1);break;case"participant-left":A(this,-1),delete this._participants[r];break;case"display-name-change":{const e=this._participants[r];e&&(e.displayName=n.displayname,e.formattedDisplayName=n.formattedDisplayName);break}case"email-change":{const e=this._participants[r];e&&(e.email=n.email);break}case"avatar-changed":{const e=this._participants[r];e&&(e.avatarURL=n.avatarURL);break}case"on-stage-participant-changed":this._onStageParticipant=r,this.emit("largeVideoChanged");break;case"large-video-visibility-changed":this._isLargeVideoVisible=n.isVisible,this.emit("largeVideoChanged");break;case"video-conference-left":A(this,-1),delete this._participants[this._myUserID]}const i=N[t];return!!i&&(this.emit(i,n),!0)})}addEventListener(e,t){this.on(e,t)}addEventListeners(e){for(const t in e)this.addEventListener(t,e[t])}dispose(){this.emit("_willDispose"),this._transport.dispose(),this.removeAllListeners(),this._frame&&this._frame.parentNode&&this._frame.parentNode.removeChild(this._frame)}executeCommand(e,...t){e in P?this._transport.sendEvent({data:t,name:P[e]}):console.error("Not supported command name.")}executeCommands(e){for(const t in e)this.executeCommand(t,e[t])}getAvailableDevices(){return Object(S.a)(this._transport)}getCurrentDevices(){return Object(S.b)(this._transport)}isAudioAvailable(){return this._transport.sendRequest({name:"is-audio-available"})}isDeviceChangeAvailable(e){return Object(S.c)(this._transport,e)}isDeviceListAvailable(){return Object(S.d)(this._transport)}isMultipleAudioInputSupported(){return Object(S.e)(this._transport)}invite(e){return Array.isArray(e)&&0!==e.length?this._transport.sendRequest({name:"invite",invitees:e}):Promise.reject(new TypeError("Invalid Argument"))}isAudioMuted(){return this._transport.sendRequest({name:"is-audio-muted"})}getAvatarURL(e){const{avatarURL:t}=this._participants[e]||{};return t}getDisplayName(e){const{displayName:t}=this._participants[e]||{};return t}getEmail(e){const{email:t}=this._participants[e]||{};return t}_getFormattedDisplayName(e){const{formattedDisplayName:t}=this._participants[e]||{};return t}getIFrame(){return this._frame}getNumberOfParticipants(){return this._numberOfParticipants}isVideoAvailable(){return this._transport.sendRequest({name:"is-video-available"})}isVideoMuted(){return this._transport.sendRequest({name:"is-video-muted"})}removeEventListener(e){this.removeAllListeners(e)}removeEventListeners(e){e.forEach(e=>this.removeEventListener(e))}sendProxyConnectionEvent(e){this._transport.sendEvent({data:[e],name:"proxy-connection-event"})}setAudioInputDevice(e,t){return Object(S.f)(this._transport,e,t)}setAudioOutputDevice(e,t){return Object(S.g)(this._transport,e,t)}setVideoInputDevice(e,t){return Object(S.h)(this._transport,e,t)}_getElectronPopupsConfig(){return Promise.resolve(E)}}}])})); +//# sourceMappingURL=external_api.min.map \ No newline at end of file diff --git a/client/src/styles.scss b/client/src/styles.scss index cb4032953..347a925fe 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -32,6 +32,7 @@ @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/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss'; +@import './app/shared/components/jitsi/jitsi.component.scss-theme.scss'; /** fonts */ @import './assets/styles/fonts.scss'; @@ -64,6 +65,7 @@ $narrow-spacing: ( @include os-motion-poll-detail-style($theme); @include os-assignment-poll-detail-style($theme); @include os-progress-snack-bar-style($theme); + @include os-jitsi-theme($theme); } /** Load projector specific SCSS values */ @@ -335,9 +337,15 @@ b, text-align: center; color: gray; } -mat-card { + +.regular-shadow { box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.37) !important; } + +.mat-card { + @extend .regular-shadow; +} + .os-card { max-width: 770px; margin-top: 20px !important; diff --git a/openslides/core/apps.py b/openslides/core/apps.py index 5a3fe5155..3b4711b6e 100644 --- a/openslides/core/apps.py +++ b/openslides/core/apps.py @@ -145,6 +145,9 @@ class CoreAppConfig(AppConfig): "PING_INTERVAL", "PING_TIMEOUT", "ENABLE_ELECTRONIC_VOTING", + "JITSI_DOMAIN", + "JITSI_ROOM_NAME", + "JITSI_ROOM_PASSWORD", ] client_settings_dict = {} for key in client_settings_keys: diff --git a/openslides/core/config_variables.py b/openslides/core/config_variables.py index 3f1db909d..b78333402 100644 --- a/openslides/core/config_variables.py +++ b/openslides/core/config_variables.py @@ -122,6 +122,24 @@ def get_config_variables(): 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 yield ConfigVariable(