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"
|
- 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