Merge pull request #5376 from tsiegleauq/integrate-streams
Integrate streams
This commit is contained in:
commit
43b13e314e
14
.travis.yml
14
.travis.yml
@ -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
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
||||
<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">
|
||||
|
||||
<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>
|
||||
|
@ -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;
|
||||
|
@ -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(() => {});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<div class="video-wrapper">
|
||||
<video #videoPlayer class="video-js" controls preload="none"></video>
|
||||
</div>
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
@ -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`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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: [
|
||||
{
|
||||
|
@ -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[];
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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>
|
||||
|
||||
|
@ -134,3 +134,15 @@ mat-sidenav-container {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbars {
|
||||
z-index: 99;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
|
||||
* {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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",
|
||||
)
|
||||
|
||||
|
25
openslides/core/migrations/0033_live_stream_permission.py
Normal file
25
openslides/core/migrations/0033_live_stream_permission.py
Normal 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"),
|
||||
),
|
||||
},
|
||||
),
|
||||
]
|
@ -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"),
|
||||
)
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user