Merge pull request #5376 from tsiegleauq/integrate-streams

Integrate streams
This commit is contained in:
Emanuel Schütze 2020-06-11 13:53:04 +02:00 committed by GitHub
commit 43b13e314e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 781 additions and 158 deletions

View File

@ -25,7 +25,7 @@ matrix:
- name: "Installing npm dependencies"
language: node_js
node_js: "10.13"
node_js: "12.18"
cache:
- directories:
- "client/node_modules"
@ -39,7 +39,7 @@ matrix:
- stage: "Run tests"
name: "Client: Testing"
language: node_js
node_js: "10.13"
node_js: "12.18"
apt:
sources:
- google-chrome
@ -56,7 +56,7 @@ matrix:
- name: "Client: Production Build (ES5)"
language: node_js
node_js: "10.13"
node_js: "12.18"
install:
- cd client
- sed -i '/\"target\"/c\\"target\":\"es5\",' tsconfig.json
@ -65,7 +65,7 @@ matrix:
- name: "Client: Production Build (ES2015)"
language: node_js
node_js: "10.13"
node_js: "12.18"
install:
- cd client
- echo "Firefox ESR" > browserslist
@ -74,7 +74,7 @@ matrix:
- name: "Client: Build"
language: node_js
node_js: "10.13"
node_js: "12.18"
script:
- cd client
- npm run build-debug
@ -111,14 +111,14 @@ matrix:
- name: "Client: Linting"
language: node_js
node_js: "10.13"
node_js: "12.18"
script:
- cd client
- npm run lint-check
- name: "Client: Code Formatting Check"
language: node_js
node_js: "10.13"
node_js: "12.18"
script:
- cd client
- npm list --depth=0 || cat --help

View File

@ -43,7 +43,11 @@
}
],
"styles": ["src/styles.scss"],
"scripts": ["node_modules/tinymce/tinymce.min.js", "src/assets/jitsi/external_api.js"],
"scripts": [
"node_modules/tinymce/tinymce.min.js",
"node_modules/video.js/dist/video.min.js",
"src/assets/jitsi/external_api.js"
],
"webWorkerTsConfig": "tsconfig.worker.json"
},
"configurations": {

View File

@ -51,6 +51,7 @@
"@pebula/ngrid-material": "2.0.0-rc.1",
"@pebula/utils": "1.0.2",
"@tinymce/tinymce-angular": "^3.3.1",
"@videojs/http-streaming": "^1.13.3",
"acorn": "^7.1.0",
"chart.js": "^2.9.2",
"core-js": "^3.6.4",
@ -61,8 +62,8 @@
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
"moment": "^2.24.0",
"ng2-charts": "^2.3.0",
"ngx-file-drop": "^8.0.8",
"ng2-pdf-viewer": "^6.1.2",
"ngx-file-drop": "^8.0.8",
"ngx-mat-select-search": "^2.1.2",
"ngx-material-timepicker": "^5.5.1",
"ngx-papaparse": "^4.0.2",
@ -71,6 +72,7 @@
"rxjs": "^6.5.4",
"tinymce": "5.2.2",
"tslib": "^1.10.0",
"video.js": "^7.7.6",
"zone.js": "~0.10.2"
},
"devDependencies": {

View File

@ -2,9 +2,10 @@ import { Injectable } from '@angular/core';
import { environment } from 'environments/environment';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { auditTime, filter } from 'rxjs/operators';
import { auditTime, filter, map } from 'rxjs/operators';
import { Group } from 'app/shared/models/users/group';
import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-list-of-speakers.service';
import { ViewUser } from 'app/site/users/models/view-user';
import { CollectionStringMapperService } from './collection-string-mapper.service';
import { DataStoreService } from './data-store.service';
@ -39,6 +40,7 @@ export enum Permission {
coreCanSeeFrontpage = 'core.can_see_frontpage',
coreCanSeeProjector = 'core.can_see_projector',
coreCanManageTags = 'core.can_manage_tags',
coreCanSeeLiveStream = 'core.can_see_livestream',
mediafilesCanManage = 'mediafiles.can_manage',
mediafilesCanSee = 'mediafiles.can_see',
motionsCanCreate = 'motions.can_create',
@ -208,7 +210,8 @@ export class OperatorService implements OnAfterAppsLoaded {
private offlineService: OfflineService,
private collectionStringMapper: CollectionStringMapperService,
private storageService: StorageService,
private OSStatus: OpenSlidesStatusService
private OSStatus: OpenSlidesStatusService,
private closService: CurrentListOfSpeakersService
) {
this.DS.getChangeObservable(User).subscribe(newModel => {
if (this._user && this._user.id === newModel.id) {
@ -459,6 +462,16 @@ export class OperatorService implements OnAfterAppsLoaded {
await this.http.post(environment.urlPrefix + '/users/setpresence/', isPresent);
}
public isOnCurrentListOfSpeakersObservable(): Observable<boolean> {
return this.closService.currentListOfSpeakersObservable.pipe(
map(los => {
if (los) {
return los.isUserOnList(this.user.id);
}
})
);
}
/**
* Returns a default WhoAmI response
*/

View File

@ -1,6 +1,8 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { HttpService } from 'app/core/core-services/http.service';
import { RelationManagerService } from 'app/core/core-services/relation-manager.service';
@ -138,8 +140,21 @@ export class ProjectorRepositoryService extends BaseRepository<ViewProjector, Pr
/**
* return the id of the current reference projector
* prefer the observable whenever possible
*/
public getReferenceProjectorId(): number {
// TODO: After logging in, this is null this.getViewModelList() is null
return this.getViewModelList().find(projector => projector.isReferenceProjector).id;
}
public getReferenceProjectorIdObservable(): Observable<number> {
return this.getViewModelListObservable().pipe(
map(projectors => {
const refProjector = projectors.find(projector => projector.isReferenceProjector);
if (refProjector) {
return refProjector.id;
}
})
);
}
}

View File

@ -1,30 +1,35 @@
<div class="jitsi-integration">
<!-- iFrame Dialog -->
<ng-template #conferenceDialog>
<div class="jitsi-iframe-wrapper" #jitsi></div>
<div mat-dialog-actions>
<button
type="button"
mat-button
color="primary"
(click)="openExternal()"
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
>
<mat-icon>open_in_new</mat-icon>
</button>
<!-- iFrame Dialog -->
<ng-template #conferenceDialog>
<div class="jitsi-iframe-wrapper" #jitsi></div>
<div mat-dialog-actions>
<button
type="button"
mat-button
color="primary"
(click)="openExternal()"
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
>
<mat-icon>open_in_new</mat-icon>
</button>
<button class="minimize-jitsi-dialog-button" type="button" mat-button color="primary" (click)="toggleConferenceDialog()">
<span>{{ 'Minimize' | translate }}</span>
<mat-icon>fullscreen_exit</mat-icon>
</button>
</div>
</ng-template>
<button
class="minimize-jitsi-dialog-button"
type="button"
mat-button
color="primary"
(click)="hideJitsiDialog()"
>
<span>{{ 'Minimize' | translate }}</span>
<mat-icon>fullscreen_exit</mat-icon>
</button>
</div>
</ng-template>
<div class="jitsi-integration" *ngIf="canSeeLiveStream && (enableJitsi || videoStreamUrl)">
<!-- Audio-Conference-bar -->
<div
class="jitsi-bar"
[ngClass]="{
'cdk-visually-hidden': !enableJitsi,
'cast-shadow': !showJitsiWindow
}"
>
@ -34,48 +39,85 @@
'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>
<ng-container *ngIf="currentState == state.jitsi">
<!-- Exit jitsi -->
<button
mat-mini-fab
class="indicator quick-icon"
color="accent"
(click)="viewStream()"
matTooltip="{{ 'Exit live conference and view the live stream' | translate }}"
*ngIf="videoStreamUrl"
>
<mat-icon color="warn">
meeting_room
</mat-icon>
</button>
<!-- disconnected icon -->
<mat-icon class="indicator" *ngIf="!isJoined">cloud_off</mat-icon>
<!-- 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>
<!-- hide unhide video -->
<!-- <button class="quick-icon" mat-mini-fab [disabled]="!isJoined">
<mat-icon *ngIf="isJoined" color="primary">videocam_off</mat-icon>
</button> -->
<!-- disconnected icon -->
<mat-icon class="indicator" *ngIf="!isJoined && !videoStreamUrl">cloud_off</mat-icon>
</ng-container>
<ng-container *ngIf="currentState == state.stream">
<!-- Enter conference from stream -->
<button
*ngIf="enableJitsi && isAccessPermitted"
class="indicator quick-icon"
mat-mini-fab
(click)="enterConversation()"
matTooltip="{{ 'Enter live conference' | translate }}"
>
<mat-icon color="primary">meeting_room</mat-icon>
</button>
<mat-icon *ngIf="enableJitsi && !isAccessPermitted" class="indicator">no_meeting_room</mat-icon>
</ng-container>
</span>
<span
class="list-wrapper apply-theme"
[ngClass]="{
'stream-width-wrapper': currentState == state.stream,
'audio-list-wrapper': currentState == state.jitsi,
'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>
<ng-container *ngIf="currentState == state.jitsi">
<span> {{ 'Live conference' | translate }}</span>
<div class="one-line">
&nbsp;
<span *ngIf="currentDominantSpeaker">
» <span class="dominant-speaker">{{ currentDominantSpeaker.displayName }}</span>
</span>
<span *ngIf="!isJitsiActive">
<i>{{ 'disconnected' | translate }}</i>
</span>
<span *ngIf="isJitsiActive && !isJoined">
<i>{{ 'connecting...' | translate }}</i>
</span>
</div>
</ng-container>
<ng-container *ngIf="currentState == state.stream">
<!-- os-icon-container does weid things here -->
<span> {{ 'Live stream' | translate }}</span>
</ng-container>
<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">
&nbsp;
<span *ngIf="currentDominantSpeaker">
» <span class="dominant-speaker">{{ currentDominantSpeaker.displayName }}</span>
</span>
<span *ngIf="!isJitsiActive">
<i>{{ 'disconnected' | translate }}</i>
</span>
</div>
</button>
<!-- unfolded list -->
@ -85,85 +127,109 @@
'cdk-visually-hidden': !showJitsiWindow
}"
>
<!-- Jitsi content window -->
<div class="content">
<!-- The "somewhere else active" warning -->
<div class="disconnected" *ngIf="isJitsiActiveInAnotherTab && !isJitsiActive">
<span>{{
'The audio conference is already running in your OpenSlides session.' | translate
}}</span>
<button mat-button color="warn" (click)="forceStart()">
<span>{{ 'Reenter to audio conference' | translate }}</span>
<ng-container *ngIf="currentState == state.jitsi">
<!-- Jitsi content window -->
<div class="content">
<!-- The "somewhere else active" warning -->
<div class="disconnected" *ngIf="isJitsiActiveInAnotherTab && !isJitsiActive">
<span>{{
'The audio conference is already running in your OpenSlides session.' | translate
}}</span>
<button mat-button color="warn" (click)="forceStart()">
<span>{{ 'Reenter to audio conference' | translate }}</span>
</button>
</div>
<div class="disconnected" *ngIf="!isJitsiActiveInAnotherTab && !isJitsiActive">
<span>{{ 'disconnected' | translate }}</span>
</div>
<div class="disconnected" *ngIf="isJitsiActive && !isJoined">
<span>{{ 'connecting...' | translate }}</span>
</div>
<!-- user list -->
<div class="room-members" *ngIf="isJitsiActive && isJoined">
<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>
</ng-container>
<ng-container *ngIf="currentState == state.stream">
<os-vjs-player
[videoUrl]="videoStreamUrl"
(started)="onSteamStarted()"
*ngIf="!streamActiveInAnotherTab || streamRunning"
></os-vjs-player>
<div class="disconnected" *ngIf="streamActiveInAnotherTab && !streamRunning">
<span>{{ 'The video stream is already running in your OpenSlides session.' | translate }}</span>
<button mat-button color="warn" (click)="deleteStreamingLock()">
<span>{{ 'Force the stream to reload' | translate }}</span>
</button>
</div>
</ng-container>
<div class="disconnected" *ngIf="!isJitsiActiveInAnotherTab && !isJitsiActive">
<span>{{ 'disconnected' | translate }}</span>
</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
}"
<ng-container *ngIf="currentState == state.jitsi">
<!-- 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 }}"
>
<div class="member-list-entry">
{{ members[memberId].name }}
</div>
</li>
</ol>
</div>
</div>
</div>
<mat-icon>call_end</mat-icon>
</button>
<!-- 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]="
!enableJitsi || isJitsiActive || isJitsiActiveInAnotherTab || !isAccessPermitted
"
*ngIf="!isJoined"
matTooltip="{{ 'Enter conference' | translate }}"
>
<mat-icon>call</mat-icon>
</button>
</div>
<!-- Enter jitsi manually -->
<!-- open dialog -->
<button
mat-mini-fab
mat-icon-button
class="open-jitsi-in-tab"
color="accent"
(click)="enterConversation()"
[disabled]="isJitsiActive || isJitsiActiveInAnotherTab"
*ngIf="!isJoined"
matTooltip="{{ 'Enter conference' | translate }}"
(click)="toggleConferenceDialog()"
[disabled]="!isJitsiActive"
matTooltip="{{ 'Maximize / minimize Jitsi window' | translate }}"
>
<mat-icon>call</mat-icon>
<mat-icon>
{{ isJitsiDialogOpen ? 'fullscreen_exit' : 'fullscreen' }}
</mat-icon>
</button>
</div>
<!-- open dialog -->
<button
mat-icon-button
class="open-jitsi-in-tab"
color="accent"
(click)="toggleConferenceDialog()"
[disabled]="!isJitsiActive"
matTooltip="{{ 'Maximize Jitsi window' | translate }}"
>
<mat-icon>
fullscreen
</mat-icon>
</button>
</div>
</div>
</ng-container>
</div>
</span>
</div>

View File

@ -19,11 +19,8 @@
}
.jitsi-bar {
z-index: 99;
display: flex;
position: fixed;
right: 20px;
bottom: 0;
margin-right: 20px;
$wrapper-padding: 5px;
$bar-height: 40px;
@ -43,17 +40,26 @@
}
}
.stream-width-wrapper {
width: 500px;
max-width: 500px;
}
.audio-list-wrapper {
width: 300px;
max-width: 300px;
}
.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%;
height: 40px;
padding: 0 2.5em;
margin-bottom: $wrapper-padding;
font-weight: normal;
@ -62,7 +68,7 @@
.opened-indicator {
position: absolute;
right: $wrapper-padding;
top: $wrapper-padding;
top: 8px;
}
.dominant-speaker {
@ -113,9 +119,13 @@
.control-grid {
padding: $wrapper-padding 0;
display: grid;
grid-template-areas: 'helper buttons new-tab';
grid-template-areas: 'exit buttons new-tab';
grid-template-columns: 40px auto 40px;
.exit-conference {
grid-area: exit;
}
.control-buttons {
grid-area: buttons;
margin: auto;

View File

@ -55,6 +55,11 @@ interface ConferenceMember {
focus: boolean;
}
enum ConferenceState {
stream,
jitsi
}
@Component({
selector: 'os-jitsi',
templateUrl: './jitsi.component.html',
@ -63,14 +68,19 @@ interface ConferenceMember {
})
export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
public enableJitsi: boolean;
private autoconnect: boolean;
private roomName: string;
private roomPassword: string;
private jitsiDomain: string;
public restricted = false;
public videoStreamUrl: string;
// do not set the password twice
private isPasswortSet = false;
public isJitsiDialogOpen = false;
public showJitsiWindow = false;
public muted = true;
@ -90,15 +100,21 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
}
public isJoined: boolean;
public streamRunning: boolean;
private options: object;
private lockLoaded: Deferred<void> = new Deferred();
private constantsLoaded: Deferred<void> = new Deferred();
private configsLoaded: Deferred<void> = new Deferred();
// storage locks
public isJitsiActiveInAnotherTab: boolean;
public streamActiveInAnotherTab: boolean;
private RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
private STREAM_RUNNING_STORAGE_KEY = 'streamIsRunning';
private CONFERENCE_STATE_STORAGE_KEY = 'conferenceState';
// JitsiID to ConferenceMember
public members = {};
@ -112,6 +128,27 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
return this.roomPassword?.length > 0;
}
public get canSeeLiveStream(): boolean {
return this.operator.hasPerms(this.permission.coreCanSeeLiveStream);
}
private isOnCurrentLos: boolean;
public get isAccessPermitted(): boolean {
return (
!this.restricted ||
this.operator.hasPerms(this.permission.agendaCanManageListOfSpeakers) ||
this.isOnCurrentLos
);
}
/**
* The conference state, to determine if the user consumes the stream or can
* contribute to jitsi
*/
public state = ConferenceState;
public currentState: ConferenceState;
private configOverwrite = {
startAudioOnly: false,
// allows jitsi on mobile devices
@ -158,15 +195,22 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
this.setUp();
}
public ngOnDestroy(): void {
this.stopJitsi();
public async ngOnDestroy(): Promise<void> {
this.stopConference();
}
// 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.stopConference();
}
private async stopConference(): Promise<void> {
await this.stopJitsi();
if (this.streamActiveInAnotherTab && this.streamRunning) {
await this.deleteStreamingLock();
}
}
private async setUp(): Promise<void> {
@ -181,6 +225,13 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
}
});
this.storageMap
.watch(this.STREAM_RUNNING_STORAGE_KEY)
.pipe(distinctUntilChanged())
.subscribe((running: boolean) => {
this.streamActiveInAnotherTab = running;
});
await this.lockLoaded;
this.constantsService.get<JitsiSettings>('Settings').subscribe(settings => {
if (settings) {
@ -193,10 +244,10 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
await this.constantsLoaded;
this.configService
.get<boolean>('general_system_conference_show')
.get<boolean>('general_system_conference_auto_connect')
.subscribe(autoconnect => (this.autoconnect = autoconnect));
this.configService.get<boolean>('general_system_conference_auto_connect').subscribe(show => {
this.configService.get<boolean>('general_system_conference_show').subscribe(show => {
this.enableJitsi = show && !!this.jitsiDomain && !!this.roomName;
if (this.enableJitsi && this.autoconnect) {
this.startJitsi();
@ -204,6 +255,51 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
this.stopJitsi();
}
});
this.configService.get<boolean>('general_system_conference_los_restriction').subscribe(restricted => {
this.restricted = restricted;
});
this.configService.get<string>('general_system_stream_url').subscribe(url => {
this.videoStreamUrl = url;
this.configsLoaded.resolve();
});
await this.configsLoaded;
// after configs are loaded
this.storageMap
.watch(this.CONFERENCE_STATE_STORAGE_KEY)
.pipe(distinctUntilChanged())
.subscribe((confState: ConferenceState) => {
if (confState in ConferenceState) {
if (this.enableJitsi && !this.videoStreamUrl) {
this.currentState = ConferenceState.jitsi;
} else if (!this.enableJitsi && this.videoStreamUrl) {
this.currentState = ConferenceState.stream;
} else {
this.currentState = confState;
}
} else {
this.setDefaultConfState();
}
// show stream window when the state changes to stream
if (this.currentState === ConferenceState.stream && !this.streamActiveInAnotherTab) {
this.showJitsiWindow = true;
}
});
// check if the user is on the clos, remove from room if not permitted
this.operator
.isOnCurrentListOfSpeakersObservable()
.pipe(distinctUntilChanged())
.subscribe(isOnList => {
this.isOnCurrentLos = isOnList;
console.log('this.isOnCurrentLos: ', this.isOnCurrentLos);
if (!this.isAccessPermitted) {
this.viewStream();
}
});
}
public toggleMute(): void {
@ -227,6 +323,7 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
public async enterConversation(): Promise<void> {
await this.operator.loaded;
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
this.setConferenceState(ConferenceState.jitsi);
this.setOptions();
this.api = new JitsiMeetExternalAPI(this.jitsiDomain, this.options);
@ -273,6 +370,9 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
this.isJoined = true;
this.addMember({ displayName: info.displayName, id: info.id });
this.setRoomPassword();
if (this.videoStreamUrl) {
this.showJitsiDialog();
}
}
private setRoomPassword(): void {
@ -355,21 +455,26 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
}
public toggleConferenceDialog(): void {
// there is no good way to detect the current classes in MatDialogRef or conferenceDialog.
// searching the global cdk-overlay-pane is the only thing which works
const pane = document.querySelector('.cdk-overlay-pane') as HTMLElement;
if (pane.classList.contains('jitsi-dialog-hide')) {
this.confDialogRef.removePanelClass('jitsi-dialog-hide');
if (this.isJitsiDialogOpen) {
this.hideJitsiDialog();
} else {
this.confDialogRef.addPanelClass('jitsi-dialog-hide');
this.showJitsiDialog();
}
}
private hideJitsiDialog(): void {
const pane = document.querySelector('.cdk-overlay-pane') as HTMLElement;
if (!pane.classList.contains('jitsi-dialog-hide')) {
this.confDialogRef.addPanelClass('jitsi-dialog-hide');
}
public hideJitsiDialog(): void {
this.confDialogRef.addPanelClass('jitsi-dialog-hide');
this.isJitsiDialogOpen = false;
}
public showJitsiDialog(): void {
this.confDialogRef.removePanelClass('jitsi-dialog-hide');
this.isJitsiDialogOpen = true;
}
public async viewStream(): Promise<void> {
this.stopJitsi();
this.setConferenceState(ConferenceState.stream);
}
public openExternal(): void {
@ -377,7 +482,28 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
window.open(this.getJitsiMeetUrl(), '_blank');
}
public onSteamStarted(): void {
this.streamRunning = true;
this.storageMap.set(this.STREAM_RUNNING_STORAGE_KEY, true).subscribe(() => {});
}
private async deleteJitsiLock(): Promise<void> {
await this.storageMap.delete(this.RTC_LOGGED_STORAGE_KEY).toPromise();
}
public async deleteStreamingLock(): Promise<void> {
await this.storageMap.delete(this.STREAM_RUNNING_STORAGE_KEY).toPromise();
}
private setDefaultConfState(): void {
this.videoStreamUrl
? this.setConferenceState(ConferenceState.stream)
: this.setConferenceState(ConferenceState.jitsi);
}
private setConferenceState(newState: ConferenceState): void {
if (this.currentState !== newState) {
this.storageMap.set(this.CONFERENCE_STATE_STORAGE_KEY, newState).subscribe(() => {});
}
}
}

View File

@ -0,0 +1,29 @@
<div class="stream-integration">
<div
class="stream-wrapper"
[ngClass]="{
'cdk-visually-hidden': !showStream
}"
>
<div *ngIf="!isUserInConference">
<os-vjs-player></os-vjs-player>
</div>
<div class="user-in-conf-warning" *ngIf="isUserInConference">
<span>
{{ 'The stream is disableb because you are inside a conference' | translate }}
</span>
<button mat-raised-button color="warn" (click)="forceReloadStream()">
{{ 'Force the stream to reload' | translate }}
</button>
</div>
</div>
<div class="stream-bar">
<button class="toggle-list-button" color="primary" mat-flat-button (click)="toggleShowStream()">
<span> {{ 'Live stream' | translate }}</span>
<mat-icon class="opened-indicator" *ngIf="!showStream">keyboard_arrow_up</mat-icon>
<mat-icon class="opened-indicator" *ngIf="showStream">keyboard_arrow_down </mat-icon>
</button>
</div>
</div>

View File

@ -0,0 +1,40 @@
.stream-integration {
.stream-wrapper {
// position: absolute;
margin-right: 20px;
margin-bottom: 5px;
float: right;
}
.user-in-conf-warning {
width: 300px;
height: 200px;
padding: 20px;
background-color: white;
button {
display: block;
}
}
.stream-bar {
margin-right: 20px;
// display: flex;
margin-right: 20px;
$wrapper-padding: 5px;
$bar-height: 40px;
.toggle-list-button {
height: 50px;
display: block;
margin-left: auto;
padding-right: 0.5em;
font-weight: normal;
text-align: right;
line-height: normal;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
}
}

View File

@ -0,0 +1,27 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { LiveStreamComponent } from './live-stream.component';
describe('LiveStreamComponent', () => {
let component: LiveStreamComponent;
let fixture: ComponentFixture<LiveStreamComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule],
declarations: [LiveStreamComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(LiveStreamComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,43 @@
import { Component, OnInit } from '@angular/core';
import { StorageMap } from '@ngx-pwa/local-storage';
import { distinctUntilChanged } from 'rxjs/operators';
@Component({
selector: 'os-live-stream',
templateUrl: './live-stream.component.html',
styleUrls: ['./live-stream.component.scss']
})
export class LiveStreamComponent implements OnInit {
public showStream = false;
private RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
public isUserInConference: boolean;
public constructor(private storageMap: StorageMap) {}
public ngOnInit(): void {
this.storageMap
.watch(this.RTC_LOGGED_STORAGE_KEY)
.pipe(distinctUntilChanged())
.subscribe((inUse: boolean) => {
this.isUserInConference = inUse;
});
}
public toggleShowStream(): void {
this.showStream = !this.showStream;
}
public async forceReloadStream(): Promise<void> {
await this.deleteJitsiLock();
}
/**
* todo: DUP
*/
private async deleteJitsiLock(): Promise<void> {
await this.storageMap.delete(this.RTC_LOGGED_STORAGE_KEY).toPromise();
}
}

View File

@ -0,0 +1,3 @@
<div class="video-wrapper">
<video #videoPlayer class="video-js" controls preload="none"></video>
</div>

View File

@ -0,0 +1,25 @@
.video-wrapper {
display: flex;
width: 100%;
.video-js {
margin: auto;
.vjs-control-bar {
.vjs-subs-caps-button {
display: none !important;
}
.vjs-descriptions-button {
display: none !important;
}
.vjs-picture-in-picture-control {
display: none !important;
}
.vjs-audio-button {
display: none !important;
}
}
}
}

View File

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

View File

@ -0,0 +1,99 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import videojs from 'video.js';
interface VideoSource {
src: string;
type: MimeType;
}
enum MimeType {
mp4 = 'video/mp4',
mpd = 'application/dash+xml',
m3u8 = 'application/x-mpegURL'
}
@Component({
selector: 'os-vjs-player',
templateUrl: './vjs-player.component.html',
styleUrls: ['./vjs-player.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class VjsPlayerComponent implements OnInit, OnDestroy {
@ViewChild('videoPlayer', { static: true }) private videoPlayer: ElementRef;
private _videoUrl: string;
@Input()
public set videoUrl(value: string) {
this._videoUrl = value;
this.playVideo();
}
@Output()
private started: EventEmitter<void> = new EventEmitter();
public get videoUrl(): string {
return this._videoUrl;
}
public player: videojs.Player;
private get videoSource(): VideoSource {
return {
src: this.videoUrl,
type: this.determineContentTypeByUrl(this.videoUrl)
};
}
public constructor() {}
public async ngOnInit(): Promise<void> {
this.player = videojs(this.videoPlayer.nativeElement, {
textTrackSettings: false,
fluid: true,
autoplay: 'any',
liveui: true
});
this.playVideo();
}
public ngOnDestroy(): void {
if (this.player) {
this.player.dispose();
}
}
private playVideo(): void {
if (this.player) {
this.player.src(this.videoSource);
this.started.next();
}
}
private determineContentTypeByUrl(url: string): MimeType {
if (url) {
if (url.startsWith('rtmp')) {
throw new Error(`$rtmp (flash) streams cannot be supported`);
} else {
const extension = url?.split('.')?.pop();
const mimeType = MimeType[extension];
if (mimeType) {
return mimeType;
} else {
throw new Error(`${url} has an unknown mime type`);
}
}
}
}
}

View File

@ -127,6 +127,8 @@ 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';
import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component';
import { LiveStreamComponent } from './components/live-stream/live-stream.component';
/**
* Share Module for all "dumb" components and pipes.
@ -294,7 +296,9 @@ import { JitsiComponent } from './components/jitsi/jitsi.component';
VotingPrivacyWarningComponent,
MotionPollDetailContentComponent,
AssignmentPollDetailContentComponent,
JitsiComponent
JitsiComponent,
VjsPlayerComponent,
LiveStreamComponent
],
declarations: [
PermsDirective,
@ -355,7 +359,9 @@ import { JitsiComponent } from './components/jitsi/jitsi.component';
VotingPrivacyWarningComponent,
MotionPollDetailContentComponent,
AssignmentPollDetailContentComponent,
JitsiComponent
JitsiComponent,
VjsPlayerComponent,
LiveStreamComponent
],
providers: [
{

View File

@ -63,6 +63,10 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
public hasSpeakerSpoken(checkSpeaker: ViewSpeaker): boolean {
return this.finishedSpeakers.findIndex(speaker => speaker.user_id === checkSpeaker.user_id) !== -1;
}
public isUserOnList(userId: number): boolean {
return !!this.speakers.find(speaker => speaker.user_id === userId);
}
}
interface IListOfSpeakersRelations {
speakers: ViewSpeaker[];

View File

@ -18,6 +18,11 @@ import { ViewProjector } from '../models/view-projector';
providedIn: 'root'
})
export class CurrentListOfSpeakersService {
/**
* Id of the current lost of speakers projector. Filled through observer
*/
private closId: number;
/**
* This map holds the current (number or null) los-id for the the projector.
* It is used to check, if the reference has changed (this clos id changed for one projector).
@ -34,6 +39,10 @@ export class CurrentListOfSpeakersService {
*/
private currentListOfSpeakers: { [projectorId: number]: BehaviorSubject<ViewListOfSpeakers | null> } = {};
private currentListOfSpeakerSubject = new BehaviorSubject<ViewListOfSpeakers>(null);
public currentListOfSpeakersObservable = this.currentListOfSpeakerSubject.asObservable();
public constructor(
private projectorService: ProjectorService,
private projectorRepo: ProjectorRepositoryService,
@ -46,6 +55,21 @@ export class CurrentListOfSpeakersService {
this.setListOfSpeakersForProjector(projector);
}
});
this.projectorRepo.getReferenceProjectorIdObservable().subscribe(closId => {
if (closId) {
this.closId = closId;
this.currentListOfSpeakerSubject.next(this.getCurrentListOfSpeakers());
}
});
}
/**
* Use the subject to get it
*/
private getCurrentListOfSpeakers(): ViewListOfSpeakers | null {
const refProjector = this.projectorRepo.getViewModel(this.closId);
return this.getCurrentListOfSpeakersForProjector(refProjector);
}
/**

View File

@ -89,12 +89,16 @@
<mat-icon>arrow_forward_ios</mat-icon>
</button>
</div>
<os-jitsi></os-jitsi>
<div (touchstart)="swipe($event, 'start')" (touchend)="swipe($event, 'end')" class="content">
<main>
<router-outlet #o="outlet"></router-outlet>
</main>
</div>
<div class="toolbars">
<!-- test -->
<os-jitsi></os-jitsi>
<!-- <os-live-stream></os-live-stream> -->
</div>
</mat-sidenav-content>
</mat-sidenav-container>

View File

@ -134,3 +134,15 @@ mat-sidenav-container {
}
}
}
.toolbars {
z-index: 99;
position: fixed;
right: 0;
bottom: 0;
display: flex;
* {
margin-top: auto;
}
}

View File

@ -46,6 +46,9 @@ $narrow-spacing: (
spacing: $pbl-spacing-theme-narrow
);
/** Videjs */
@import '~video.js/dist/video-js.css';
/** Mix the component-related style-rules */
@mixin openslides-components-theme($theme) {
@include os-site-theme($theme);

View File

@ -97,30 +97,48 @@ def get_config_variables():
)
yield ConfigVariable(
name="general_system_conference_auto_connect",
name="general_system_conference_show",
default_value=False,
input_type="boolean",
label="Show audio conference window",
label="Show live conference window",
help_text="Server settings required to activate Jitsi Meet integration.",
weight=140,
subgroup="System",
)
yield ConfigVariable(
name="general_system_conference_show",
name="general_system_conference_auto_connect",
default_value=False,
input_type="boolean",
label="Connect all users to audio conference automatically",
label="Connect all users to live conference automatically",
help_text="Server settings required to activate Jitsi Meet integration.",
weight=141,
subgroup="System",
)
yield ConfigVariable(
name="general_system_conference_los_restriction",
default_value=False,
input_type="boolean",
label="Allow only speakers and permitted users to enter the live conference",
help_text="Server settings required to activate Jitsi Meet integration.",
weight=142,
subgroup="System",
)
yield ConfigVariable(
name="general_system_stream_url",
default_value="",
label="Live stream url",
weight=143,
subgroup="System",
)
yield ConfigVariable(
name="general_login_info_text",
default_value="",
label="Show this text on the login page",
weight=144,
weight=145,
subgroup="System",
)

View File

@ -0,0 +1,25 @@
# Generated by Django 2.2.12 on 2020-06-05 09:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("core", "0032_add_monospace_font"),
]
operations = [
migrations.AlterModelOptions(
name="projector",
options={
"default_permissions": (),
"permissions": (
("can_see_projector", "Can see the projector"),
("can_manage_projector", "Can manage the projector"),
("can_see_frontpage", "Can see the front page"),
("can_see_livestream", "Can see the live stream"),
),
},
),
]

View File

@ -118,6 +118,7 @@ class Projector(RESTModelMixin, models.Model):
("can_see_projector", "Can see the projector"),
("can_manage_projector", "Can manage the projector"),
("can_see_frontpage", "Can see the front page"),
("can_see_livestream", "Can see the live stream"),
)