Integrate streams
Integrate live streaming inside the jitsi/rtc components. Live streaming works without jitsi, but is using the same components for a fluid integration. A streaming URL can be set in the settings page. Users EITHER consume the live stream OR are presend in a jitsi live conference. To consume both the live stream and the jitsi conference, users may use a dedicated jitsi tab in their session. The jitsi users can be restricted to only allow thouse with the right the manage speakers or being present on the "current list of speakers", automatically simulating a virtual plenum
This commit is contained in:
parent
fbb0be6fb4
commit
0d9738b72d
14
.travis.yml
14
.travis.yml
@ -25,7 +25,7 @@ matrix:
|
|||||||
|
|
||||||
- name: "Installing npm dependencies"
|
- name: "Installing npm dependencies"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.13"
|
node_js: "12.18"
|
||||||
cache:
|
cache:
|
||||||
- directories:
|
- directories:
|
||||||
- "client/node_modules"
|
- "client/node_modules"
|
||||||
@ -39,7 +39,7 @@ matrix:
|
|||||||
- stage: "Run tests"
|
- stage: "Run tests"
|
||||||
name: "Client: Testing"
|
name: "Client: Testing"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.13"
|
node_js: "12.18"
|
||||||
apt:
|
apt:
|
||||||
sources:
|
sources:
|
||||||
- google-chrome
|
- google-chrome
|
||||||
@ -56,7 +56,7 @@ matrix:
|
|||||||
|
|
||||||
- name: "Client: Production Build (ES5)"
|
- name: "Client: Production Build (ES5)"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.13"
|
node_js: "12.18"
|
||||||
install:
|
install:
|
||||||
- cd client
|
- cd client
|
||||||
- sed -i '/\"target\"/c\\"target\":\"es5\",' tsconfig.json
|
- sed -i '/\"target\"/c\\"target\":\"es5\",' tsconfig.json
|
||||||
@ -65,7 +65,7 @@ matrix:
|
|||||||
|
|
||||||
- name: "Client: Production Build (ES2015)"
|
- name: "Client: Production Build (ES2015)"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.13"
|
node_js: "12.18"
|
||||||
install:
|
install:
|
||||||
- cd client
|
- cd client
|
||||||
- echo "Firefox ESR" > browserslist
|
- echo "Firefox ESR" > browserslist
|
||||||
@ -74,7 +74,7 @@ matrix:
|
|||||||
|
|
||||||
- name: "Client: Build"
|
- name: "Client: Build"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.13"
|
node_js: "12.18"
|
||||||
script:
|
script:
|
||||||
- cd client
|
- cd client
|
||||||
- npm run build-debug
|
- npm run build-debug
|
||||||
@ -111,14 +111,14 @@ matrix:
|
|||||||
|
|
||||||
- name: "Client: Linting"
|
- name: "Client: Linting"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.13"
|
node_js: "12.18"
|
||||||
script:
|
script:
|
||||||
- cd client
|
- cd client
|
||||||
- npm run lint-check
|
- npm run lint-check
|
||||||
|
|
||||||
- name: "Client: Code Formatting Check"
|
- name: "Client: Code Formatting Check"
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js: "10.13"
|
node_js: "12.18"
|
||||||
script:
|
script:
|
||||||
- cd client
|
- cd client
|
||||||
- npm list --depth=0 || cat --help
|
- npm list --depth=0 || cat --help
|
||||||
|
@ -43,7 +43,11 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": ["src/styles.scss"],
|
"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"
|
"webWorkerTsConfig": "tsconfig.worker.json"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
"@pebula/ngrid-material": "2.0.0-rc.1",
|
"@pebula/ngrid-material": "2.0.0-rc.1",
|
||||||
"@pebula/utils": "1.0.2",
|
"@pebula/utils": "1.0.2",
|
||||||
"@tinymce/tinymce-angular": "^3.3.1",
|
"@tinymce/tinymce-angular": "^3.3.1",
|
||||||
|
"@videojs/http-streaming": "^1.13.3",
|
||||||
"acorn": "^7.1.0",
|
"acorn": "^7.1.0",
|
||||||
"chart.js": "^2.9.2",
|
"chart.js": "^2.9.2",
|
||||||
"core-js": "^3.6.4",
|
"core-js": "^3.6.4",
|
||||||
@ -61,8 +62,8 @@
|
|||||||
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
|
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"ng2-charts": "^2.3.0",
|
"ng2-charts": "^2.3.0",
|
||||||
"ngx-file-drop": "^8.0.8",
|
|
||||||
"ng2-pdf-viewer": "^6.1.2",
|
"ng2-pdf-viewer": "^6.1.2",
|
||||||
|
"ngx-file-drop": "^8.0.8",
|
||||||
"ngx-mat-select-search": "^2.1.2",
|
"ngx-mat-select-search": "^2.1.2",
|
||||||
"ngx-material-timepicker": "^5.5.1",
|
"ngx-material-timepicker": "^5.5.1",
|
||||||
"ngx-papaparse": "^4.0.2",
|
"ngx-papaparse": "^4.0.2",
|
||||||
@ -71,6 +72,7 @@
|
|||||||
"rxjs": "^6.5.4",
|
"rxjs": "^6.5.4",
|
||||||
"tinymce": "5.2.2",
|
"tinymce": "5.2.2",
|
||||||
"tslib": "^1.10.0",
|
"tslib": "^1.10.0",
|
||||||
|
"video.js": "^7.7.6",
|
||||||
"zone.js": "~0.10.2"
|
"zone.js": "~0.10.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -2,9 +2,10 @@ import { Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { environment } from 'environments/environment';
|
import { environment } from 'environments/environment';
|
||||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
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 { 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 { ViewUser } from 'app/site/users/models/view-user';
|
||||||
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
import { CollectionStringMapperService } from './collection-string-mapper.service';
|
||||||
import { DataStoreService } from './data-store.service';
|
import { DataStoreService } from './data-store.service';
|
||||||
@ -39,6 +40,7 @@ export enum Permission {
|
|||||||
coreCanSeeFrontpage = 'core.can_see_frontpage',
|
coreCanSeeFrontpage = 'core.can_see_frontpage',
|
||||||
coreCanSeeProjector = 'core.can_see_projector',
|
coreCanSeeProjector = 'core.can_see_projector',
|
||||||
coreCanManageTags = 'core.can_manage_tags',
|
coreCanManageTags = 'core.can_manage_tags',
|
||||||
|
coreCanSeeLiveStream = 'core.can_see_livestream',
|
||||||
mediafilesCanManage = 'mediafiles.can_manage',
|
mediafilesCanManage = 'mediafiles.can_manage',
|
||||||
mediafilesCanSee = 'mediafiles.can_see',
|
mediafilesCanSee = 'mediafiles.can_see',
|
||||||
motionsCanCreate = 'motions.can_create',
|
motionsCanCreate = 'motions.can_create',
|
||||||
@ -208,7 +210,8 @@ export class OperatorService implements OnAfterAppsLoaded {
|
|||||||
private offlineService: OfflineService,
|
private offlineService: OfflineService,
|
||||||
private collectionStringMapper: CollectionStringMapperService,
|
private collectionStringMapper: CollectionStringMapperService,
|
||||||
private storageService: StorageService,
|
private storageService: StorageService,
|
||||||
private OSStatus: OpenSlidesStatusService
|
private OSStatus: OpenSlidesStatusService,
|
||||||
|
private closService: CurrentListOfSpeakersService
|
||||||
) {
|
) {
|
||||||
this.DS.getChangeObservable(User).subscribe(newModel => {
|
this.DS.getChangeObservable(User).subscribe(newModel => {
|
||||||
if (this._user && this._user.id === newModel.id) {
|
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);
|
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
|
* Returns a default WhoAmI response
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/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 { HttpService } from 'app/core/core-services/http.service';
|
||||||
import { RelationManagerService } from 'app/core/core-services/relation-manager.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
|
* return the id of the current reference projector
|
||||||
|
* prefer the observable whenever possible
|
||||||
*/
|
*/
|
||||||
public getReferenceProjectorId(): number {
|
public getReferenceProjectorId(): number {
|
||||||
|
// TODO: After logging in, this is null this.getViewModelList() is null
|
||||||
return this.getViewModelList().find(projector => projector.isReferenceProjector).id;
|
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 -->
|
||||||
<!-- iFrame Dialog -->
|
<ng-template #conferenceDialog>
|
||||||
<ng-template #conferenceDialog>
|
<div class="jitsi-iframe-wrapper" #jitsi></div>
|
||||||
<div class="jitsi-iframe-wrapper" #jitsi></div>
|
<div mat-dialog-actions>
|
||||||
<div mat-dialog-actions>
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
mat-button
|
||||||
mat-button
|
color="primary"
|
||||||
color="primary"
|
(click)="openExternal()"
|
||||||
(click)="openExternal()"
|
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
|
||||||
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
|
>
|
||||||
>
|
<mat-icon>open_in_new</mat-icon>
|
||||||
<mat-icon>open_in_new</mat-icon>
|
</button>
|
||||||
</button>
|
|
||||||
|
|
||||||
<button class="minimize-jitsi-dialog-button" type="button" mat-button color="primary" (click)="toggleConferenceDialog()">
|
<button
|
||||||
<span>{{ 'Minimize' | translate }}</span>
|
class="minimize-jitsi-dialog-button"
|
||||||
<mat-icon>fullscreen_exit</mat-icon>
|
type="button"
|
||||||
</button>
|
mat-button
|
||||||
</div>
|
color="primary"
|
||||||
</ng-template>
|
(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 -->
|
<!-- Audio-Conference-bar -->
|
||||||
<div
|
<div
|
||||||
class="jitsi-bar"
|
class="jitsi-bar"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
'cdk-visually-hidden': !enableJitsi,
|
|
||||||
'cast-shadow': !showJitsiWindow
|
'cast-shadow': !showJitsiWindow
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
@ -34,48 +39,85 @@
|
|||||||
'cast-shadow': showJitsiWindow
|
'cast-shadow': showJitsiWindow
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- mute/unmute button -->
|
<ng-container *ngIf="currentState == state.jitsi">
|
||||||
<button
|
<!-- Exit jitsi -->
|
||||||
class="indicator quick-icon"
|
<button
|
||||||
mat-mini-fab
|
mat-mini-fab
|
||||||
*ngIf="isJoined"
|
class="indicator quick-icon"
|
||||||
(click)="toggleMute()"
|
color="accent"
|
||||||
matTooltip="{{ 'Mute / Unmute' | translate }}"
|
(click)="viewStream()"
|
||||||
>
|
matTooltip="{{ 'Exit live conference and view the live stream' | translate }}"
|
||||||
<mat-icon color="{{ muted ? 'primary' : 'warn' }}">{{ muted ? 'moff' : 'mic' }}</mat-icon>
|
*ngIf="videoStreamUrl"
|
||||||
</button>
|
>
|
||||||
|
<mat-icon color="warn">
|
||||||
|
meeting_room
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- disconnected icon -->
|
<!-- mute/unmute button -->
|
||||||
<mat-icon class="indicator" *ngIf="!isJoined">cloud_off</mat-icon>
|
<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 -->
|
<!-- disconnected icon -->
|
||||||
<!-- <button class="quick-icon" mat-mini-fab [disabled]="!isJoined">
|
<mat-icon class="indicator" *ngIf="!isJoined && !videoStreamUrl">cloud_off</mat-icon>
|
||||||
<mat-icon *ngIf="isJoined" color="primary">videocam_off</mat-icon>
|
</ng-container>
|
||||||
</button> -->
|
|
||||||
|
<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>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="list-wrapper apply-theme"
|
class="list-wrapper apply-theme"
|
||||||
[ngClass]="{
|
[ngClass]="{
|
||||||
|
'stream-width-wrapper': currentState == state.stream,
|
||||||
|
'audio-list-wrapper': currentState == state.jitsi,
|
||||||
'cast-shadow': showJitsiWindow
|
'cast-shadow': showJitsiWindow
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- <span class="list-wrapper apply-theme regular-shadow"> -->
|
|
||||||
<!-- open-window button -->
|
<!-- open-window button -->
|
||||||
<button class="toggle-list-button" mat-button (click)="toggleShowJitsi()">
|
<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_up</mat-icon>
|
||||||
<mat-icon class="opened-indicator" *ngIf="showJitsiWindow">keyboard_arrow_down </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>
|
</button>
|
||||||
|
|
||||||
<!-- unfolded list -->
|
<!-- unfolded list -->
|
||||||
@ -85,85 +127,109 @@
|
|||||||
'cdk-visually-hidden': !showJitsiWindow
|
'cdk-visually-hidden': !showJitsiWindow
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<!-- Jitsi content window -->
|
<ng-container *ngIf="currentState == state.jitsi">
|
||||||
<div class="content">
|
<!-- Jitsi content window -->
|
||||||
<!-- The "somewhere else active" warning -->
|
<div class="content">
|
||||||
<div class="disconnected" *ngIf="isJitsiActiveInAnotherTab && !isJitsiActive">
|
<!-- The "somewhere else active" warning -->
|
||||||
<span>{{
|
<div class="disconnected" *ngIf="isJitsiActiveInAnotherTab && !isJitsiActive">
|
||||||
'The audio conference is already running in your OpenSlides session.' | translate
|
<span>{{
|
||||||
}}</span>
|
'The audio conference is already running in your OpenSlides session.' | translate
|
||||||
<button mat-button color="warn" (click)="forceStart()">
|
}}</span>
|
||||||
<span>{{ 'Reenter to audio conference' | 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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<div class="disconnected" *ngIf="!isJitsiActiveInAnotherTab && !isJitsiActive">
|
<ng-container *ngIf="currentState == state.jitsi">
|
||||||
<span>{{ 'disconnected' | translate }}</span>
|
<!-- Custom control buttons -->
|
||||||
</div>
|
<div>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
<!-- user list -->
|
<div class="control-grid">
|
||||||
<div class="room-members" *ngIf="isJitsiActive">
|
<div class="control-buttons">
|
||||||
<div class="member-list">
|
<!-- Hangup -->
|
||||||
<ol>
|
<button
|
||||||
<li
|
mat-mini-fab
|
||||||
*ngFor="let memberId of memberList; trackBy: trackByIndex"
|
color="warn"
|
||||||
[ngClass]="{
|
(click)="stopJitsi()"
|
||||||
focused: members[memberId].focus
|
*ngIf="isJitsiActive && isJoined"
|
||||||
}"
|
matTooltip="{{ 'Leave' | translate }}"
|
||||||
>
|
>
|
||||||
<div class="member-list-entry">
|
<mat-icon>call_end</mat-icon>
|
||||||
{{ members[memberId].name }}
|
</button>
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Custom control buttons -->
|
<!-- Enter jitsi manually -->
|
||||||
<div>
|
<button
|
||||||
<mat-divider></mat-divider>
|
mat-mini-fab
|
||||||
<div class="control-grid">
|
color="accent"
|
||||||
<div class="control-buttons">
|
(click)="enterConversation()"
|
||||||
<!-- Hangup -->
|
[disabled]="
|
||||||
<button
|
!enableJitsi || isJitsiActive || isJitsiActiveInAnotherTab || !isAccessPermitted
|
||||||
mat-mini-fab
|
"
|
||||||
color="warn"
|
*ngIf="!isJoined"
|
||||||
(click)="stopJitsi()"
|
matTooltip="{{ 'Enter conference' | translate }}"
|
||||||
*ngIf="isJitsiActive && isJoined"
|
>
|
||||||
matTooltip="{{ 'Leave' | translate }}"
|
<mat-icon>call</mat-icon>
|
||||||
>
|
</button>
|
||||||
<mat-icon>call_end</mat-icon>
|
</div>
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Enter jitsi manually -->
|
<!-- open dialog -->
|
||||||
<button
|
<button
|
||||||
mat-mini-fab
|
mat-icon-button
|
||||||
|
class="open-jitsi-in-tab"
|
||||||
color="accent"
|
color="accent"
|
||||||
(click)="enterConversation()"
|
(click)="toggleConferenceDialog()"
|
||||||
[disabled]="isJitsiActive || isJitsiActiveInAnotherTab"
|
[disabled]="!isJitsiActive"
|
||||||
*ngIf="!isJoined"
|
matTooltip="{{ 'Maximize / minimize Jitsi window' | translate }}"
|
||||||
matTooltip="{{ 'Enter conference' | translate }}"
|
|
||||||
>
|
>
|
||||||
<mat-icon>call</mat-icon>
|
<mat-icon>
|
||||||
|
{{ isJitsiDialogOpen ? 'fullscreen_exit' : 'fullscreen' }}
|
||||||
|
</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -19,11 +19,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.jitsi-bar {
|
.jitsi-bar {
|
||||||
z-index: 99;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
position: fixed;
|
margin-right: 20px;
|
||||||
right: 20px;
|
|
||||||
bottom: 0;
|
|
||||||
$wrapper-padding: 5px;
|
$wrapper-padding: 5px;
|
||||||
$bar-height: 40px;
|
$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 {
|
.list-wrapper {
|
||||||
min-height: $bar-height;
|
min-height: $bar-height;
|
||||||
padding-top: $wrapper-padding;
|
padding-top: $wrapper-padding;
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
width: 250px;
|
|
||||||
max-width: 250px;
|
|
||||||
|
|
||||||
.toggle-list-button {
|
.toggle-list-button {
|
||||||
position: relative;
|
position: relative;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 40px;
|
||||||
padding: 0 2.5em;
|
padding: 0 2.5em;
|
||||||
margin-bottom: $wrapper-padding;
|
margin-bottom: $wrapper-padding;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
@ -62,7 +68,7 @@
|
|||||||
.opened-indicator {
|
.opened-indicator {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: $wrapper-padding;
|
right: $wrapper-padding;
|
||||||
top: $wrapper-padding;
|
top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dominant-speaker {
|
.dominant-speaker {
|
||||||
@ -113,9 +119,13 @@
|
|||||||
.control-grid {
|
.control-grid {
|
||||||
padding: $wrapper-padding 0;
|
padding: $wrapper-padding 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas: 'helper buttons new-tab';
|
grid-template-areas: 'exit buttons new-tab';
|
||||||
grid-template-columns: 40px auto 40px;
|
grid-template-columns: 40px auto 40px;
|
||||||
|
|
||||||
|
.exit-conference {
|
||||||
|
grid-area: exit;
|
||||||
|
}
|
||||||
|
|
||||||
.control-buttons {
|
.control-buttons {
|
||||||
grid-area: buttons;
|
grid-area: buttons;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
@ -55,6 +55,11 @@ interface ConferenceMember {
|
|||||||
focus: boolean;
|
focus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ConferenceState {
|
||||||
|
stream,
|
||||||
|
jitsi
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-jitsi',
|
selector: 'os-jitsi',
|
||||||
templateUrl: './jitsi.component.html',
|
templateUrl: './jitsi.component.html',
|
||||||
@ -63,14 +68,19 @@ interface ConferenceMember {
|
|||||||
})
|
})
|
||||||
export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
||||||
public enableJitsi: boolean;
|
public enableJitsi: boolean;
|
||||||
|
|
||||||
private autoconnect: boolean;
|
private autoconnect: boolean;
|
||||||
private roomName: string;
|
private roomName: string;
|
||||||
private roomPassword: string;
|
private roomPassword: string;
|
||||||
private jitsiDomain: string;
|
private jitsiDomain: string;
|
||||||
|
|
||||||
|
public restricted = false;
|
||||||
|
public videoStreamUrl: string;
|
||||||
|
|
||||||
// do not set the password twice
|
// do not set the password twice
|
||||||
private isPasswortSet = false;
|
private isPasswortSet = false;
|
||||||
|
|
||||||
|
public isJitsiDialogOpen = false;
|
||||||
public showJitsiWindow = false;
|
public showJitsiWindow = false;
|
||||||
public muted = true;
|
public muted = true;
|
||||||
|
|
||||||
@ -90,15 +100,21 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isJoined: boolean;
|
public isJoined: boolean;
|
||||||
|
public streamRunning: boolean;
|
||||||
|
|
||||||
private options: object;
|
private options: object;
|
||||||
|
|
||||||
private lockLoaded: Deferred<void> = new Deferred();
|
private lockLoaded: Deferred<void> = new Deferred();
|
||||||
private constantsLoaded: Deferred<void> = new Deferred();
|
private constantsLoaded: Deferred<void> = new Deferred();
|
||||||
|
private configsLoaded: Deferred<void> = new Deferred();
|
||||||
|
|
||||||
// storage locks
|
// storage locks
|
||||||
public isJitsiActiveInAnotherTab: boolean;
|
public isJitsiActiveInAnotherTab: boolean;
|
||||||
|
public streamActiveInAnotherTab: boolean;
|
||||||
|
|
||||||
private RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
|
private RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
|
||||||
|
private STREAM_RUNNING_STORAGE_KEY = 'streamIsRunning';
|
||||||
|
private CONFERENCE_STATE_STORAGE_KEY = 'conferenceState';
|
||||||
|
|
||||||
// JitsiID to ConferenceMember
|
// JitsiID to ConferenceMember
|
||||||
public members = {};
|
public members = {};
|
||||||
@ -112,6 +128,27 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
|||||||
return this.roomPassword?.length > 0;
|
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 = {
|
private configOverwrite = {
|
||||||
startAudioOnly: false,
|
startAudioOnly: false,
|
||||||
// allows jitsi on mobile devices
|
// allows jitsi on mobile devices
|
||||||
@ -158,15 +195,22 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
|||||||
this.setUp();
|
this.setUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy(): void {
|
public async ngOnDestroy(): Promise<void> {
|
||||||
this.stopJitsi();
|
this.stopConference();
|
||||||
}
|
}
|
||||||
|
|
||||||
// closing the tab should also try to stop jitsi.
|
// closing the tab should also try to stop jitsi.
|
||||||
// this will usually not be cought by ngOnDestroy
|
// this will usually not be cought by ngOnDestroy
|
||||||
@HostListener('window:beforeunload', ['$event'])
|
@HostListener('window:beforeunload', ['$event'])
|
||||||
public async beforeunload($event: any): Promise<void> {
|
public async beforeunload($event: any): Promise<void> {
|
||||||
|
await this.stopConference();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async stopConference(): Promise<void> {
|
||||||
await this.stopJitsi();
|
await this.stopJitsi();
|
||||||
|
if (this.streamActiveInAnotherTab && this.streamRunning) {
|
||||||
|
await this.deleteStreamingLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setUp(): Promise<void> {
|
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;
|
await this.lockLoaded;
|
||||||
this.constantsService.get<JitsiSettings>('Settings').subscribe(settings => {
|
this.constantsService.get<JitsiSettings>('Settings').subscribe(settings => {
|
||||||
if (settings) {
|
if (settings) {
|
||||||
@ -193,10 +244,10 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
await this.constantsLoaded;
|
await this.constantsLoaded;
|
||||||
this.configService
|
this.configService
|
||||||
.get<boolean>('general_system_conference_show')
|
.get<boolean>('general_system_conference_auto_connect')
|
||||||
.subscribe(autoconnect => (this.autoconnect = autoconnect));
|
.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;
|
this.enableJitsi = show && !!this.jitsiDomain && !!this.roomName;
|
||||||
if (this.enableJitsi && this.autoconnect) {
|
if (this.enableJitsi && this.autoconnect) {
|
||||||
this.startJitsi();
|
this.startJitsi();
|
||||||
@ -204,6 +255,51 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
|||||||
this.stopJitsi();
|
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 {
|
public toggleMute(): void {
|
||||||
@ -227,6 +323,7 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
|||||||
public async enterConversation(): Promise<void> {
|
public async enterConversation(): Promise<void> {
|
||||||
await this.operator.loaded;
|
await this.operator.loaded;
|
||||||
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
||||||
|
this.setConferenceState(ConferenceState.jitsi);
|
||||||
this.setOptions();
|
this.setOptions();
|
||||||
this.api = new JitsiMeetExternalAPI(this.jitsiDomain, this.options);
|
this.api = new JitsiMeetExternalAPI(this.jitsiDomain, this.options);
|
||||||
|
|
||||||
@ -273,6 +370,9 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
|||||||
this.isJoined = true;
|
this.isJoined = true;
|
||||||
this.addMember({ displayName: info.displayName, id: info.id });
|
this.addMember({ displayName: info.displayName, id: info.id });
|
||||||
this.setRoomPassword();
|
this.setRoomPassword();
|
||||||
|
if (this.videoStreamUrl) {
|
||||||
|
this.showJitsiDialog();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setRoomPassword(): void {
|
private setRoomPassword(): void {
|
||||||
@ -355,21 +455,26 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public toggleConferenceDialog(): void {
|
public toggleConferenceDialog(): void {
|
||||||
// there is no good way to detect the current classes in MatDialogRef or conferenceDialog.
|
if (this.isJitsiDialogOpen) {
|
||||||
// searching the global cdk-overlay-pane is the only thing which works
|
this.hideJitsiDialog();
|
||||||
const pane = document.querySelector('.cdk-overlay-pane') as HTMLElement;
|
|
||||||
if (pane.classList.contains('jitsi-dialog-hide')) {
|
|
||||||
this.confDialogRef.removePanelClass('jitsi-dialog-hide');
|
|
||||||
} else {
|
} else {
|
||||||
this.confDialogRef.addPanelClass('jitsi-dialog-hide');
|
this.showJitsiDialog();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private hideJitsiDialog(): void {
|
public hideJitsiDialog(): void {
|
||||||
const pane = document.querySelector('.cdk-overlay-pane') as HTMLElement;
|
this.confDialogRef.addPanelClass('jitsi-dialog-hide');
|
||||||
if (!pane.classList.contains('jitsi-dialog-hide')) {
|
this.isJitsiDialogOpen = false;
|
||||||
this.confDialogRef.addPanelClass('jitsi-dialog-hide');
|
}
|
||||||
}
|
|
||||||
|
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 {
|
public openExternal(): void {
|
||||||
@ -377,7 +482,28 @@ export class JitsiComponent extends BaseComponent implements OnInit, OnDestroy {
|
|||||||
window.open(this.getJitsiMeetUrl(), '_blank');
|
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> {
|
private async deleteJitsiLock(): Promise<void> {
|
||||||
await this.storageMap.delete(this.RTC_LOGGED_STORAGE_KEY).toPromise();
|
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 { GlobalSpinnerComponent } from './components/global-spinner/global-spinner.component';
|
||||||
import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
||||||
import { JitsiComponent } from './components/jitsi/jitsi.component';
|
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.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -294,7 +296,9 @@ import { JitsiComponent } from './components/jitsi/jitsi.component';
|
|||||||
VotingPrivacyWarningComponent,
|
VotingPrivacyWarningComponent,
|
||||||
MotionPollDetailContentComponent,
|
MotionPollDetailContentComponent,
|
||||||
AssignmentPollDetailContentComponent,
|
AssignmentPollDetailContentComponent,
|
||||||
JitsiComponent
|
JitsiComponent,
|
||||||
|
VjsPlayerComponent,
|
||||||
|
LiveStreamComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -355,7 +359,9 @@ import { JitsiComponent } from './components/jitsi/jitsi.component';
|
|||||||
VotingPrivacyWarningComponent,
|
VotingPrivacyWarningComponent,
|
||||||
MotionPollDetailContentComponent,
|
MotionPollDetailContentComponent,
|
||||||
AssignmentPollDetailContentComponent,
|
AssignmentPollDetailContentComponent,
|
||||||
JitsiComponent
|
JitsiComponent,
|
||||||
|
VjsPlayerComponent,
|
||||||
|
LiveStreamComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@ -63,6 +63,10 @@ export class ViewListOfSpeakers extends BaseViewModelWithContentObject<ListOfSpe
|
|||||||
public hasSpeakerSpoken(checkSpeaker: ViewSpeaker): boolean {
|
public hasSpeakerSpoken(checkSpeaker: ViewSpeaker): boolean {
|
||||||
return this.finishedSpeakers.findIndex(speaker => speaker.user_id === checkSpeaker.user_id) !== -1;
|
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 {
|
interface IListOfSpeakersRelations {
|
||||||
speakers: ViewSpeaker[];
|
speakers: ViewSpeaker[];
|
||||||
|
@ -18,6 +18,11 @@ import { ViewProjector } from '../models/view-projector';
|
|||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class CurrentListOfSpeakersService {
|
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.
|
* 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).
|
* 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 currentListOfSpeakers: { [projectorId: number]: BehaviorSubject<ViewListOfSpeakers | null> } = {};
|
||||||
|
|
||||||
|
private currentListOfSpeakerSubject = new BehaviorSubject<ViewListOfSpeakers>(null);
|
||||||
|
|
||||||
|
public currentListOfSpeakersObservable = this.currentListOfSpeakerSubject.asObservable();
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private projectorService: ProjectorService,
|
private projectorService: ProjectorService,
|
||||||
private projectorRepo: ProjectorRepositoryService,
|
private projectorRepo: ProjectorRepositoryService,
|
||||||
@ -46,6 +55,21 @@ export class CurrentListOfSpeakersService {
|
|||||||
this.setListOfSpeakersForProjector(projector);
|
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>
|
<mat-icon>arrow_forward_ios</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<os-jitsi></os-jitsi>
|
|
||||||
<div (touchstart)="swipe($event, 'start')" (touchend)="swipe($event, 'end')" class="content">
|
<div (touchstart)="swipe($event, 'start')" (touchend)="swipe($event, 'end')" class="content">
|
||||||
<main>
|
<main>
|
||||||
<router-outlet #o="outlet"></router-outlet>
|
<router-outlet #o="outlet"></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="toolbars">
|
||||||
|
<!-- test -->
|
||||||
|
<os-jitsi></os-jitsi>
|
||||||
|
<!-- <os-live-stream></os-live-stream> -->
|
||||||
|
</div>
|
||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
</mat-sidenav-container>
|
</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
|
spacing: $pbl-spacing-theme-narrow
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** Videjs */
|
||||||
|
@import '~video.js/dist/video-js.css';
|
||||||
|
|
||||||
/** Mix the component-related style-rules */
|
/** Mix the component-related style-rules */
|
||||||
@mixin openslides-components-theme($theme) {
|
@mixin openslides-components-theme($theme) {
|
||||||
@include os-site-theme($theme);
|
@include os-site-theme($theme);
|
||||||
|
@ -97,30 +97,48 @@ def get_config_variables():
|
|||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name="general_system_conference_auto_connect",
|
name="general_system_conference_show",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type="boolean",
|
input_type="boolean",
|
||||||
label="Show audio conference window",
|
label="Show live conference window",
|
||||||
help_text="Server settings required to activate Jitsi Meet integration.",
|
help_text="Server settings required to activate Jitsi Meet integration.",
|
||||||
weight=140,
|
weight=140,
|
||||||
subgroup="System",
|
subgroup="System",
|
||||||
)
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name="general_system_conference_show",
|
name="general_system_conference_auto_connect",
|
||||||
default_value=False,
|
default_value=False,
|
||||||
input_type="boolean",
|
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.",
|
help_text="Server settings required to activate Jitsi Meet integration.",
|
||||||
weight=142,
|
weight=142,
|
||||||
subgroup="System",
|
subgroup="System",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
yield ConfigVariable(
|
||||||
|
name="general_system_stream_url",
|
||||||
|
default_value="",
|
||||||
|
label="Live stream url",
|
||||||
|
weight=143,
|
||||||
|
subgroup="System",
|
||||||
|
)
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
name="general_login_info_text",
|
name="general_login_info_text",
|
||||||
default_value="",
|
default_value="",
|
||||||
label="Show this text on the login page",
|
label="Show this text on the login page",
|
||||||
weight=144,
|
weight=145,
|
||||||
subgroup="System",
|
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_see_projector", "Can see the projector"),
|
||||||
("can_manage_projector", "Can manage the projector"),
|
("can_manage_projector", "Can manage the projector"),
|
||||||
("can_see_frontpage", "Can see the front page"),
|
("can_see_frontpage", "Can see the front page"),
|
||||||
|
("can_see_livestream", "Can see the live stream"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user