Restructure communication components
separates the "Jitsi component" into an own module, several services and components.
This commit is contained in:
parent
e0da18a0e6
commit
1504e33607
@ -256,7 +256,7 @@ export class DiffService {
|
|||||||
* @returns {Element}
|
* @returns {Element}
|
||||||
*/
|
*/
|
||||||
public getLineNumberNode(fragment: DocumentFragment, lineNumber: number): Element {
|
public getLineNumberNode(fragment: DocumentFragment, lineNumber: number): Element {
|
||||||
return fragment.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
|
return fragment?.querySelector('os-linebreak.os-line-number.line-number-' + lineNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,6 +16,22 @@ const slideOut = [
|
|||||||
)
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const fadeInOut = trigger('fadeInOut', [
|
||||||
|
state(
|
||||||
|
'true',
|
||||||
|
style({
|
||||||
|
opacity: 1
|
||||||
|
})
|
||||||
|
),
|
||||||
|
state(
|
||||||
|
'false',
|
||||||
|
style({
|
||||||
|
opacity: 0.2
|
||||||
|
})
|
||||||
|
),
|
||||||
|
transition('true <=> false', animate('1s'))
|
||||||
|
]);
|
||||||
|
|
||||||
export const collapseAndFade = trigger('collapse', [
|
export const collapseAndFade = trigger('collapse', [
|
||||||
state('in', style({ opacity: 1, height: '100%' })),
|
state('in', style({ opacity: 1, height: '100%' })),
|
||||||
transition(':enter', [style({ opacity: 0, height: 0 }), animate(fadeSpeed.fast)]),
|
transition(':enter', [style({ opacity: 0, height: 0 }), animate(fadeSpeed.fast)]),
|
||||||
|
@ -1,295 +0,0 @@
|
|||||||
<!-- iFrame Dialog -->
|
|
||||||
<div class="jitsi-fake-dialog-wrapper" [ngClass]="{ 'jitsi-dialog-hide': !isJitsiDialogOpen }">
|
|
||||||
<mat-card class="jitsi-fake-dialog">
|
|
||||||
<div class="jitsi-iframe-wrapper" #jitsi></div>
|
|
||||||
<div class="jitsi-dialog-actions">
|
|
||||||
<div>
|
|
||||||
<a
|
|
||||||
type="button"
|
|
||||||
mat-icon-button
|
|
||||||
color="primary"
|
|
||||||
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
|
|
||||||
target="_blank"
|
|
||||||
(click)="stopJitsi()"
|
|
||||||
[href]="jitsiMeetUrl"
|
|
||||||
>
|
|
||||||
<mat-icon>open_in_new</mat-icon>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="dialog-hangup"
|
|
||||||
type="button"
|
|
||||||
mat-button
|
|
||||||
color="warn"
|
|
||||||
(click)="!!videoStreamUrl ? viewStream() : stopJitsi()"
|
|
||||||
>
|
|
||||||
<os-icon-container icon="{{ !!videoStreamUrl ? 'meeting_room' : 'call_end' }}">
|
|
||||||
<span *ngIf="videoStreamUrl">
|
|
||||||
{{ 'Exit conference' | translate }}
|
|
||||||
</span>
|
|
||||||
<span *ngIf="!videoStreamUrl">
|
|
||||||
{{ 'Hang up' | translate }}
|
|
||||||
</span>
|
|
||||||
</os-icon-container>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<button class="dialog-hide" type="button" mat-button color="primary" (click)="hideJitsiDialog()">
|
|
||||||
<os-icon-container icon="aspect_ratio">
|
|
||||||
<span>{{ 'Minimize' | translate }}</span>
|
|
||||||
</os-icon-container>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="jitsi-integration" *ngIf="showConferenceBar">
|
|
||||||
<!-- Audio-Conference-bar -->
|
|
||||||
<div class="jitsi-bar">
|
|
||||||
<span class="control-icon-wrapper apply-theme">
|
|
||||||
<ng-container *ngIf="currentState == state.jitsi">
|
|
||||||
<!-- Exit jitsi -->
|
|
||||||
<button
|
|
||||||
mat-mini-fab
|
|
||||||
class="indicator quick-icon"
|
|
||||||
color="accent"
|
|
||||||
(click)="viewStream()"
|
|
||||||
matTooltip="{{ 'Exit live conference and continue livestream' | translate }}"
|
|
||||||
*ngIf="videoStreamUrl && canSeeLiveStream && !isJitsiDialogOpen"
|
|
||||||
>
|
|
||||||
<mat-icon color="warn">meeting_room</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- mute/unmute button -->
|
|
||||||
<button
|
|
||||||
class="indicator quick-icon"
|
|
||||||
mat-mini-fab
|
|
||||||
*ngIf="isJoined && !isJitsiDialogOpen"
|
|
||||||
(click)="toggleMute()"
|
|
||||||
matTooltip="{{ 'Mute / Unmute' | translate }}"
|
|
||||||
>
|
|
||||||
<mat-icon color="{{ muted ? 'primary' : 'warn' }}">{{ muted ? 'moff' : 'mic' }}</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- disconnected icon -->
|
|
||||||
<mat-icon class="indicator" *ngIf="!isJoined && !videoStreamUrl">cloud_off</mat-icon>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="currentState == state.stream">
|
|
||||||
<!-- Enter conference from stream -->
|
|
||||||
<button
|
|
||||||
*ngIf="enableJitsi && isAccessPermitted"
|
|
||||||
class="quick-icon indicator"
|
|
||||||
mat-mini-fab
|
|
||||||
(click)="enterConferenceRoom()"
|
|
||||||
matTooltip="{{ 'Enter live conference' | translate }}"
|
|
||||||
>
|
|
||||||
<mat-icon
|
|
||||||
color="primary"
|
|
||||||
[@fadeInOut]="isEnterMeetingRoomVisible"
|
|
||||||
(@fadeInOut.done)="triggerMeetingRoomButtonAnimation()"
|
|
||||||
>
|
|
||||||
meeting_room
|
|
||||||
</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="indicator"
|
|
||||||
type="button"
|
|
||||||
mat-icon-button
|
|
||||||
matTooltip="{{ 'Add yourself to the current list of speakers to join the conference' | translate }}"
|
|
||||||
*ngIf="enableJitsi && !isAccessPermitted"
|
|
||||||
[routerLink]="['/agenda/speakers']"
|
|
||||||
>
|
|
||||||
<mat-icon> no_meeting_room </mat-icon>
|
|
||||||
</a>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<!-- Call support button -->
|
|
||||||
<button
|
|
||||||
class="indicator quick-icon"
|
|
||||||
mat-mini-fab
|
|
||||||
(click)="enterSupportRoom()"
|
|
||||||
[disabled]="isJitsiActive"
|
|
||||||
matTooltip="{{ 'Help desk' | translate }}"
|
|
||||||
*ngIf="canAccessSupport"
|
|
||||||
>
|
|
||||||
<mat-icon color="primary">live_help</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- applause button -->
|
|
||||||
<button
|
|
||||||
class="quick-icon indicator"
|
|
||||||
[disabled]="applauseDisabled"
|
|
||||||
mat-mini-fab
|
|
||||||
(click)="sendApplause()"
|
|
||||||
matTooltip="{{ 'Give applause' | translate }}"
|
|
||||||
*ngIf="showApplause"
|
|
||||||
[matBadge]="showApplauseBadge ? applauseLevel : null"
|
|
||||||
matBadgeColor="accent"
|
|
||||||
>
|
|
||||||
<mat-icon svgIcon="clapping_hands"></mat-icon>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="list-wrapper apply-theme"
|
|
||||||
[ngClass]="{
|
|
||||||
'stream-width-wrapper': currentState == state.stream,
|
|
||||||
'audio-list-wrapper': currentState == state.jitsi,
|
|
||||||
'cast-shadow': showJitsiWindow
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<os-applause-bar-display *ngIf="showApplause && isApplauseTypeBar" class="applause"></os-applause-bar-display>
|
|
||||||
|
|
||||||
<!-- open-window button -->
|
|
||||||
<button class="toggle-list-button" mat-button (click)="toggleShowJitsi()">
|
|
||||||
<ng-container *ngIf="currentState == state.jitsi">
|
|
||||||
<div *ngIf="!connectToHelpDesk" class="ellipsis-overflow">{{ 'Live conference' | translate }}</div>
|
|
||||||
<div *ngIf="connectToHelpDesk" class="ellipsis-overflow">{{ 'Help desk' | translate }}</div>
|
|
||||||
<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 weird things here -->
|
|
||||||
<div class="ellipsis-overflow">{{ 'Livestream' | translate }}</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<mat-icon class="opened-indicator" *ngIf="!showJitsiWindow">keyboard_arrow_up</mat-icon>
|
|
||||||
<mat-icon class="opened-indicator" *ngIf="showJitsiWindow">keyboard_arrow_down </mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- unfolded list -->
|
|
||||||
<div
|
|
||||||
class="jitsi-list"
|
|
||||||
[ngClass]="{
|
|
||||||
'hide-height': !showJitsiWindow
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<ng-container *ngIf="currentState == state.jitsi">
|
|
||||||
<!-- Jitsi content window -->
|
|
||||||
<div class="content">
|
|
||||||
<!-- The "somewhere else active" warning -->
|
|
||||||
<div class="disconnected" *ngIf="isJitsiActiveInAnotherTab && !isJitsiActive">
|
|
||||||
<span>{{
|
|
||||||
'The live conference is already running in your OpenSlides session.' | translate
|
|
||||||
}}</span>
|
|
||||||
<button mat-button color="warn" (click)="forceStart()">
|
|
||||||
<span>{{ 'Reenter to live 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">
|
|
||||||
<os-applause-particle-display
|
|
||||||
*ngIf="isApplauseTypeParticles"
|
|
||||||
class="room-list-applause-particles"
|
|
||||||
></os-applause-particle-display>
|
|
||||||
<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">
|
|
||||||
<ng-container *ngIf="showVideoPlayer()">
|
|
||||||
<os-video-player
|
|
||||||
[videoUrl]="videoStreamUrl"
|
|
||||||
[showParticles]="isApplauseTypeParticles"
|
|
||||||
(started)="onSteamLoaded()"
|
|
||||||
></os-video-player>
|
|
||||||
</ng-container>
|
|
||||||
<div class="disconnected" *ngIf="isStreamInOtherTab()">
|
|
||||||
<button class="restart-stream-button" mat-button color="warn" (click)="deleteStreamingLock()">
|
|
||||||
<span>{{ 'Restart livestream' | translate }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="currentState == state.jitsi">
|
|
||||||
<!-- Custom control buttons -->
|
|
||||||
<div>
|
|
||||||
<mat-divider></mat-divider>
|
|
||||||
<div class="control-grid">
|
|
||||||
<div class="control-buttons">
|
|
||||||
<!-- Hangup -->
|
|
||||||
<button
|
|
||||||
mat-mini-fab
|
|
||||||
color="warn"
|
|
||||||
(click)="stopJitsi()"
|
|
||||||
*ngIf="isJitsiActive && isJoined"
|
|
||||||
matTooltip="{{ 'Leave' | translate }}"
|
|
||||||
>
|
|
||||||
<mat-icon>call_end</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Enter jitsi manually -->
|
|
||||||
<button
|
|
||||||
mat-mini-fab
|
|
||||||
color="accent"
|
|
||||||
(click)="enterConferenceRoom()"
|
|
||||||
[disabled]="
|
|
||||||
!enableJitsi || isJitsiActive || isJitsiActiveInAnotherTab || !isAccessPermitted
|
|
||||||
"
|
|
||||||
*ngIf="!isJoined"
|
|
||||||
matTooltip="{{ 'Enter conference' | translate }}"
|
|
||||||
>
|
|
||||||
<mat-icon>call</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- open dialog -->
|
|
||||||
<button
|
|
||||||
mat-icon-button
|
|
||||||
class="open-jitsi-in-tab"
|
|
||||||
color="accent"
|
|
||||||
(click)="toggleConferenceDialog()"
|
|
||||||
[disabled]="!isJitsiActive"
|
|
||||||
matTooltip="{{ 'Show/Hide video conference' | translate }}"
|
|
||||||
>
|
|
||||||
<mat-icon>
|
|
||||||
{{ isJitsiDialogOpen ? 'aspect_ratio' : 'voice_chat' }}
|
|
||||||
</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@ -1,200 +0,0 @@
|
|||||||
.jitsi-fake-dialog-wrapper {
|
|
||||||
z-index: 98;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
padding: 20px 10% 20px 5%;
|
|
||||||
|
|
||||||
.jitsi-fake-dialog {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 100%;
|
|
||||||
height: 90%;
|
|
||||||
|
|
||||||
.jitsi-iframe-wrapper {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jitsi-dialog-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
div {
|
|
||||||
min-width: 33%;
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-hangup {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-hide {
|
|
||||||
margin-left: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jitsi-dialog-hide {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jitsi-integration {
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 99;
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 20px;
|
|
||||||
bottom: 0;
|
|
||||||
|
|
||||||
.cast-shadow {
|
|
||||||
box-shadow: -3px -3px 10px 0px rgba(0, 0, 0, 0.2) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.jitsi-bar {
|
|
||||||
display: flex;
|
|
||||||
position: relative;
|
|
||||||
justify-content: flex-end;
|
|
||||||
$wrapper-padding: 5px;
|
|
||||||
$bar-height: 40px;
|
|
||||||
|
|
||||||
.control-icon-wrapper {
|
|
||||||
pointer-events: all;
|
|
||||||
z-index: 1;
|
|
||||||
min-height: $bar-height;
|
|
||||||
display: flex;
|
|
||||||
margin-top: auto;
|
|
||||||
padding-right: 0.5em;
|
|
||||||
padding: $wrapper-padding 0 $wrapper-padding $wrapper-padding;
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
|
|
||||||
.indicator {
|
|
||||||
width: 40px;
|
|
||||||
text-align: center;
|
|
||||||
margin: auto $wrapper-padding auto 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-width-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 100px;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-list-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 100px;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.list-wrapper {
|
|
||||||
position: relative;
|
|
||||||
pointer-events: all;
|
|
||||||
min-height: $bar-height;
|
|
||||||
padding-top: $wrapper-padding;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
|
|
||||||
.applause {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 90px;
|
|
||||||
left: -90px;
|
|
||||||
bottom: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-list-button {
|
|
||||||
position: relative;
|
|
||||||
line-height: normal;
|
|
||||||
width: 100%;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0 2.5em;
|
|
||||||
margin-bottom: $wrapper-padding;
|
|
||||||
font-weight: normal;
|
|
||||||
text-align: right;
|
|
||||||
|
|
||||||
.opened-indicator {
|
|
||||||
position: absolute;
|
|
||||||
right: $wrapper-padding;
|
|
||||||
top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dominant-speaker {
|
|
||||||
font-weight: 500;
|
|
||||||
width: fit-content;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jitsi-list {
|
|
||||||
.content {
|
|
||||||
height: 40vh;
|
|
||||||
max-height: 100%;
|
|
||||||
clear: both;
|
|
||||||
|
|
||||||
.disconnected {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: inherit;
|
|
||||||
padding-left: 1em;
|
|
||||||
padding-right: 1em;
|
|
||||||
|
|
||||||
span {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-members {
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.room-list-applause-particles {
|
|
||||||
position: absolute;
|
|
||||||
height: 100%;
|
|
||||||
width: 70px;
|
|
||||||
right: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member-list {
|
|
||||||
max-height: 100%;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.member-list-entry {
|
|
||||||
margin: 5px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.focused {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-grid {
|
|
||||||
padding: $wrapper-padding 0;
|
|
||||||
display: grid;
|
|
||||||
grid-template-areas: 'exit buttons new-tab';
|
|
||||||
grid-template-columns: 40px auto 40px;
|
|
||||||
|
|
||||||
.exit-conference {
|
|
||||||
grid-area: exit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-buttons {
|
|
||||||
grid-area: buttons;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.open-jitsi-in-tab {
|
|
||||||
grid-area: new-tab;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
@import '~@angular/material/theming';
|
|
||||||
|
|
||||||
@mixin os-jitsi-theme($theme) {
|
|
||||||
$primary: map-get($theme, primary);
|
|
||||||
$accent: map-get($theme, accent);
|
|
||||||
$warn: map-get($theme, warn);
|
|
||||||
$foreground: map-get($theme, foreground);
|
|
||||||
$background: map-get($theme, background);
|
|
||||||
|
|
||||||
.jitsi-bar {
|
|
||||||
.apply-theme {
|
|
||||||
background-color: mat-color($primary);
|
|
||||||
|
|
||||||
.quick-icon:not([disabled]) {
|
|
||||||
background-color: mat-color($primary, default-contrast);
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator {
|
|
||||||
color: mat-color($primary, default-contrast);
|
|
||||||
|
|
||||||
svg path {
|
|
||||||
fill: mat-color($primary) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-list-button {
|
|
||||||
span {
|
|
||||||
color: mat-color($primary, default-contrast);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.jitsi-list {
|
|
||||||
background-color: mat-color($background, card);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,742 +0,0 @@
|
|||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
ElementRef,
|
|
||||||
HostListener,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
ViewChild,
|
|
||||||
ViewEncapsulation
|
|
||||||
} from '@angular/core';
|
|
||||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
|
||||||
|
|
||||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
|
||||||
import { delay, distinctUntilChanged, map } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { ConstantsService } from 'app/core/core-services/constants.service';
|
|
||||||
import { OperatorService } from 'app/core/core-services/operator.service';
|
|
||||||
import { Deferred } from 'app/core/promises/deferred';
|
|
||||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
|
||||||
import { ApplauseService, ApplauseType } from 'app/core/ui-services/applause.service';
|
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
|
||||||
import { UserMediaPermService } from 'app/core/ui-services/user-media-perm.service';
|
|
||||||
import { UserListIndexType } from 'app/site/agenda/models/view-list-of-speakers';
|
|
||||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
|
||||||
import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-list-of-speakers.service';
|
|
||||||
|
|
||||||
declare var JitsiMeetExternalAPI: any;
|
|
||||||
|
|
||||||
interface JitsiMember {
|
|
||||||
id: string;
|
|
||||||
displayName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConferenceJoinedResult {
|
|
||||||
roomName: string;
|
|
||||||
id: string;
|
|
||||||
displayName: string;
|
|
||||||
formattedDisplayName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DisplayNameChangeResult {
|
|
||||||
// Yes, in this case "displayname" really does not have a capital n. Thank you jitsi.
|
|
||||||
displayname: string;
|
|
||||||
formattedDisplayName: string;
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface JitsiSettings {
|
|
||||||
JITSI_DOMAIN: string;
|
|
||||||
JITSI_ROOM_NAME: string;
|
|
||||||
JITSI_ROOM_PASSWORD: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ConferenceMember {
|
|
||||||
name: string;
|
|
||||||
focus: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ConferenceState {
|
|
||||||
stream,
|
|
||||||
jitsi
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'os-jitsi',
|
|
||||||
templateUrl: './jitsi.component.html',
|
|
||||||
styleUrls: ['./jitsi.component.scss'],
|
|
||||||
animations: [
|
|
||||||
trigger('fadeInOut', [
|
|
||||||
state(
|
|
||||||
'true',
|
|
||||||
style({
|
|
||||||
opacity: 1
|
|
||||||
})
|
|
||||||
),
|
|
||||||
state(
|
|
||||||
'false',
|
|
||||||
style({
|
|
||||||
opacity: 0.2
|
|
||||||
})
|
|
||||||
),
|
|
||||||
transition('true <=> false', animate('1s'))
|
|
||||||
])
|
|
||||||
],
|
|
||||||
encapsulation: ViewEncapsulation.None,
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
|
||||||
})
|
|
||||||
export class JitsiComponent extends BaseViewComponentDirective implements OnInit, OnDestroy {
|
|
||||||
public enableJitsi: boolean;
|
|
||||||
|
|
||||||
private autoconnect: boolean;
|
|
||||||
private defaultRoomName: string;
|
|
||||||
private actualRoomName: string;
|
|
||||||
private roomPassword: string;
|
|
||||||
private jitsiDomain: string;
|
|
||||||
private isSupportEnabled: boolean;
|
|
||||||
|
|
||||||
public connectToHelpDesk = false;
|
|
||||||
|
|
||||||
public restricted = false;
|
|
||||||
public videoStreamUrl: string;
|
|
||||||
private nextSpeakerAmount: number;
|
|
||||||
|
|
||||||
// do not set the password twice
|
|
||||||
private isPasswortSet = false;
|
|
||||||
|
|
||||||
public isJitsiDialogOpen = false;
|
|
||||||
public showJitsiWindow = true;
|
|
||||||
public muted = true;
|
|
||||||
|
|
||||||
public showApplause: boolean;
|
|
||||||
public applauseDisabled = false;
|
|
||||||
private applauseTimeout: number;
|
|
||||||
|
|
||||||
@ViewChild('jitsi')
|
|
||||||
private jitsiNode: ElementRef;
|
|
||||||
|
|
||||||
// JitsiMeet api object
|
|
||||||
private api: any | null;
|
|
||||||
|
|
||||||
public get isJitsiActive(): boolean {
|
|
||||||
return !!this.api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isJoined: boolean;
|
|
||||||
private streamRunning = false;
|
|
||||||
|
|
||||||
private options: object;
|
|
||||||
|
|
||||||
private lockLoaded: Deferred<void> = new Deferred();
|
|
||||||
private constantsLoaded: Deferred<void> = new Deferred();
|
|
||||||
private configsLoaded: Deferred<void> = new Deferred();
|
|
||||||
|
|
||||||
// storage locks
|
|
||||||
public isJitsiActiveInAnotherTab = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* undefined is controlled behaviour, meaning, this property was not
|
|
||||||
* checked yet.
|
|
||||||
* Thus, false-checks have to be explicit
|
|
||||||
*/
|
|
||||||
private streamLoadedOnce: boolean;
|
|
||||||
|
|
||||||
private RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
|
|
||||||
private STREAM_RUNNING_STORAGE_KEY = 'streamIsRunning';
|
|
||||||
|
|
||||||
// JitsiID to ConferenceMember
|
|
||||||
public members = {};
|
|
||||||
public currentDominantSpeaker: JitsiMember;
|
|
||||||
|
|
||||||
public get memberList(): string[] {
|
|
||||||
return Object.keys(this.members);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isRoomPasswordProtected(): boolean {
|
|
||||||
return this.roomPassword?.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get canAccessSupport(): boolean {
|
|
||||||
return this.isSupportEnabled && this.enableJitsi && !!this.defaultRoomName;
|
|
||||||
}
|
|
||||||
|
|
||||||
private isOnCurrentLos: boolean;
|
|
||||||
|
|
||||||
public canSeeLiveStream: boolean;
|
|
||||||
|
|
||||||
public canManageSpeaker: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Jitsi|URL|Perm||Show
|
|
||||||
* =====|===|====||====
|
|
||||||
* 0 | 0 | 0 || 0
|
|
||||||
* 0 | 0 | 1 || 0
|
|
||||||
* 0 | 1 | 0 || 0
|
|
||||||
* 0 | 1 | 1 || 1
|
|
||||||
* 1 | 0 | 0 || 1
|
|
||||||
* 1 | 0 | 1 || 1
|
|
||||||
* 1 | 1 | 0 || 0
|
|
||||||
* 1 | 1 | 1 || 1
|
|
||||||
*/
|
|
||||||
public get showConferenceBar(): boolean {
|
|
||||||
if (this.enableJitsi) {
|
|
||||||
if (this.videoStreamUrl && !this.canSeeLiveStream) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return this.videoStreamUrl && this.canSeeLiveStream;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isAccessPermitted(): boolean {
|
|
||||||
return !this.restricted || this.canManageSpeaker || this.isOnCurrentLos;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get jitsiMeetUrl(): string {
|
|
||||||
return `https://${this.jitsiDomain}/${this.actualRoomName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The conference state, to determine if the user consumes the stream or can
|
|
||||||
* contribute to jitsi
|
|
||||||
*/
|
|
||||||
public state = ConferenceState;
|
|
||||||
public currentState: ConferenceState;
|
|
||||||
public isEnterMeetingRoomVisible = true;
|
|
||||||
|
|
||||||
public applauseLevel = 0;
|
|
||||||
private showApplauseLevel: boolean;
|
|
||||||
private isApplausBarUsed: boolean;
|
|
||||||
|
|
||||||
public get showApplauseBadge(): boolean {
|
|
||||||
return this.showApplauseLevel && this.applauseLevel > 0 && (!this.showJitsiWindow || !this.isApplausBarUsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
private get applauseType(): ApplauseType {
|
|
||||||
return this.applauseService.applauseType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isApplauseTypeBar(): boolean {
|
|
||||||
return this.applauseType === ApplauseType.bar;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get isApplauseTypeParticles(): boolean {
|
|
||||||
return this.applauseType === ApplauseType.particles;
|
|
||||||
}
|
|
||||||
|
|
||||||
private configOverwrite = {
|
|
||||||
startAudioOnly: false,
|
|
||||||
// allows jitsi on mobile devices
|
|
||||||
disableDeepLinking: true,
|
|
||||||
startWithAudioMuted: false,
|
|
||||||
startWithVideoMuted: false,
|
|
||||||
useNicks: true,
|
|
||||||
enableWelcomePage: false,
|
|
||||||
enableUserRolesBasedOnToken: false,
|
|
||||||
enableFeaturesBasedOnToken: false,
|
|
||||||
disableThirdPartyRequests: true,
|
|
||||||
enableNoAudioDetection: false,
|
|
||||||
enableNoisyMicDetection: false
|
|
||||||
};
|
|
||||||
|
|
||||||
private interfaceConfigOverwrite = {
|
|
||||||
DISABLE_VIDEO_BACKGROUND: true,
|
|
||||||
INVITATION_POWERED_BY: false,
|
|
||||||
DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
|
|
||||||
DISABLE_PRESENCE_STATUS: true,
|
|
||||||
TOOLBAR_ALWAYS_VISIBLE: true,
|
|
||||||
TOOLBAR_TIMEOUT: 10000000,
|
|
||||||
TOOLBAR_BUTTONS: [
|
|
||||||
'microphone',
|
|
||||||
'camera',
|
|
||||||
'desktop',
|
|
||||||
'fullscreen',
|
|
||||||
'fodeviceselection',
|
|
||||||
'profile',
|
|
||||||
'chat',
|
|
||||||
'recording',
|
|
||||||
'livestreaming',
|
|
||||||
'etherpad',
|
|
||||||
'sharedvideo',
|
|
||||||
'settings',
|
|
||||||
'videoquality',
|
|
||||||
'filmstrip',
|
|
||||||
'feedback',
|
|
||||||
'stats',
|
|
||||||
'shortcuts',
|
|
||||||
'tileview',
|
|
||||||
'download',
|
|
||||||
'help',
|
|
||||||
'mute-everyone'
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
public constructor(
|
|
||||||
titleService: Title,
|
|
||||||
translate: TranslateService,
|
|
||||||
snackBar: MatSnackBar,
|
|
||||||
private operator: OperatorService,
|
|
||||||
private storageMap: StorageMap,
|
|
||||||
private userRepo: UserRepositoryService,
|
|
||||||
private constantsService: ConstantsService,
|
|
||||||
private configService: ConfigService,
|
|
||||||
private closService: CurrentListOfSpeakersService,
|
|
||||||
private userMediaPermService: UserMediaPermService,
|
|
||||||
private applauseService: ApplauseService,
|
|
||||||
private cd: ChangeDetectorRef
|
|
||||||
) {
|
|
||||||
super(titleService, translate, snackBar);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ngOnInit(): Promise<void> {
|
|
||||||
await this.setUp();
|
|
||||||
if (this.canSeeLiveStream && this.videoStreamUrl) {
|
|
||||||
this.currentState = ConferenceState.stream;
|
|
||||||
} else {
|
|
||||||
this.currentState = ConferenceState.jitsi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ngOnDestroy(): Promise<void> {
|
|
||||||
super.ngOnDestroy();
|
|
||||||
this.stopConference();
|
|
||||||
}
|
|
||||||
|
|
||||||
// closing the tab should also try to stop jitsi.
|
|
||||||
// this will usually not be cought by ngOnDestroy
|
|
||||||
@HostListener('window:beforeunload', ['$event'])
|
|
||||||
public async beforeunload($event: any): Promise<void> {
|
|
||||||
await this.stopConference();
|
|
||||||
}
|
|
||||||
|
|
||||||
public triggerMeetingRoomButtonAnimation(): void {
|
|
||||||
if (this.canManageSpeaker) {
|
|
||||||
this.isEnterMeetingRoomVisible = true;
|
|
||||||
} else {
|
|
||||||
this.isEnterMeetingRoomVisible = !this.isEnterMeetingRoomVisible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async stopConference(): Promise<void> {
|
|
||||||
await this.stopJitsi();
|
|
||||||
if (this.streamLoadedOnce && this.streamRunning) {
|
|
||||||
await this.deleteStreamingLock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setUp(): Promise<void> {
|
|
||||||
this.subscriptions.push(
|
|
||||||
// if the operators users has changes, check if we have to start the animation
|
|
||||||
this.operator
|
|
||||||
.getUserObservable()
|
|
||||||
.pipe(delay(0))
|
|
||||||
.subscribe(() => {
|
|
||||||
this.canManageSpeaker = this.operator.hasPerms(this.permission.agendaCanManageListOfSpeakers);
|
|
||||||
this.canSeeLiveStream = this.operator.hasPerms(this.permission.coreCanSeeLiveStream);
|
|
||||||
this.isEnterMeetingRoomVisible = this.canManageSpeaker;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
|
|
||||||
this.storageMap
|
|
||||||
.watch(this.RTC_LOGGED_STORAGE_KEY)
|
|
||||||
.pipe(distinctUntilChanged())
|
|
||||||
.subscribe((inUse: boolean) => {
|
|
||||||
this.isJitsiActiveInAnotherTab = inUse;
|
|
||||||
this.lockLoaded.resolve();
|
|
||||||
if (!inUse && !this.isJitsiActive) {
|
|
||||||
this.startJitsi();
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
this.storageMap
|
|
||||||
.watch(this.STREAM_RUNNING_STORAGE_KEY)
|
|
||||||
.pipe(distinctUntilChanged())
|
|
||||||
.subscribe((running: boolean) => {
|
|
||||||
this.streamLoadedOnce = !!running;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.lockLoaded;
|
|
||||||
|
|
||||||
this.constantsService.get<JitsiSettings>('Settings').subscribe(settings => {
|
|
||||||
if (settings) {
|
|
||||||
this.jitsiDomain = settings.JITSI_DOMAIN;
|
|
||||||
this.defaultRoomName = settings.JITSI_ROOM_NAME;
|
|
||||||
this.roomPassword = settings.JITSI_ROOM_PASSWORD;
|
|
||||||
this.constantsLoaded.resolve();
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.constantsLoaded;
|
|
||||||
|
|
||||||
this.subscriptions.push(
|
|
||||||
this.configService.get<boolean>('general_system_conference_auto_connect').subscribe(autoconnect => {
|
|
||||||
this.autoconnect = autoconnect;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<boolean>('general_system_conference_show').subscribe(show => {
|
|
||||||
this.enableJitsi = show && !!this.jitsiDomain && !!this.defaultRoomName;
|
|
||||||
if (this.enableJitsi && this.autoconnect) {
|
|
||||||
this.startJitsi();
|
|
||||||
} else {
|
|
||||||
this.stopJitsi();
|
|
||||||
}
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<boolean>('general_system_conference_los_restriction').subscribe(restricted => {
|
|
||||||
this.restricted = restricted;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService
|
|
||||||
.get<number>('general_system_conference_auto_connect_next_speakers')
|
|
||||||
.subscribe(nextSpeakerAmount => {
|
|
||||||
this.nextSpeakerAmount = nextSpeakerAmount;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<string>('general_system_stream_url').subscribe(url => {
|
|
||||||
this.onLiveStreamAvailable(url);
|
|
||||||
this.configsLoaded.resolve();
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<boolean>('general_system_conference_open_microphone').subscribe(open => {
|
|
||||||
this.configOverwrite.startWithAudioMuted = !open;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<boolean>('general_system_conference_open_video').subscribe(open => {
|
|
||||||
this.configOverwrite.startWithVideoMuted = !open;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<boolean>('general_system_applause_enable').subscribe(enable => {
|
|
||||||
this.showApplause = enable;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<number>('general_system_stream_applause_timeout').subscribe(timeout => {
|
|
||||||
this.applauseTimeout = (timeout || 1) * 1000;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<boolean>('general_system_applause_show_level').subscribe(show => {
|
|
||||||
this.showApplauseLevel = show;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<any>('general_system_applause_type').subscribe(type => {
|
|
||||||
if (type === 'applause-type-bar') {
|
|
||||||
this.isApplausBarUsed = true;
|
|
||||||
} else {
|
|
||||||
this.isApplausBarUsed = false;
|
|
||||||
}
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.configService.get<boolean>('general_system_conference_enable_helpdesk').subscribe(enabled => {
|
|
||||||
this.isSupportEnabled = enabled;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.configsLoaded;
|
|
||||||
|
|
||||||
this.subscriptions.push(
|
|
||||||
// check if the operator is on the clos, remove from room if not permitted
|
|
||||||
this.closService.currentListOfSpeakersObservable
|
|
||||||
.pipe(
|
|
||||||
map(los => los?.findUserIndexOnList(this.operator.user.id) ?? -1),
|
|
||||||
distinctUntilChanged()
|
|
||||||
)
|
|
||||||
.subscribe(userLosIndex => {
|
|
||||||
this.autoJoinJitsiByLosIndex(userLosIndex);
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}),
|
|
||||||
this.applauseService.applauseLevelObservable.subscribe(applauseLevel => {
|
|
||||||
this.applauseLevel = applauseLevel || 0;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleMute(): void {
|
|
||||||
if (this.isJitsiActive) {
|
|
||||||
this.api.executeCommand('toggleAudio');
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async forceStart(): Promise<void> {
|
|
||||||
await this.deleteJitsiLock();
|
|
||||||
await this.stopJitsi();
|
|
||||||
await this.startJitsi();
|
|
||||||
}
|
|
||||||
|
|
||||||
private startJitsi(): void {
|
|
||||||
if (!this.isJitsiActiveInAnotherTab && this.enableJitsi && !this.isJitsiActive && this.jitsiNode) {
|
|
||||||
this.enterConferenceRoom();
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async enterConversation(): Promise<void> {
|
|
||||||
await this.operator.loaded;
|
|
||||||
try {
|
|
||||||
await this.userMediaPermService.requestMediaAccess();
|
|
||||||
this.storageMap.set(this.RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
|
||||||
this.setConferenceState(ConferenceState.jitsi);
|
|
||||||
this.setOptions();
|
|
||||||
if (this.api) {
|
|
||||||
this.api.dispose();
|
|
||||||
this.api = undefined;
|
|
||||||
}
|
|
||||||
this.api = new JitsiMeetExternalAPI(this.jitsiDomain, this.options);
|
|
||||||
const jitsiname = this.userRepo.getShortName(this.operator.user);
|
|
||||||
this.api.executeCommand('displayName', jitsiname);
|
|
||||||
this.loadApiCallbacks();
|
|
||||||
} catch (e) {
|
|
||||||
this.raiseError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private loadApiCallbacks(): void {
|
|
||||||
this.api.on('videoConferenceJoined', (info: ConferenceJoinedResult) => {
|
|
||||||
this.onEnterConference(info);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.api.on('participantJoined', (newMember: JitsiMember) => {
|
|
||||||
this.addMember(newMember);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.api.on('participantLeft', (oldMember: { id: string }) => {
|
|
||||||
this.removeMember(oldMember);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.api.on('displayNameChange', (member: DisplayNameChangeResult) => {
|
|
||||||
this.renameMember(member);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.api.on('audioMuteStatusChanged', (isMuted: { muted: boolean }) => {
|
|
||||||
this.muted = isMuted.muted;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.api.on('readyToClose', () => {
|
|
||||||
this.stopJitsi();
|
|
||||||
});
|
|
||||||
|
|
||||||
this.api.on('dominantSpeakerChanged', (newSpeaker: { id: string }) => {
|
|
||||||
this.newDominantSpeaker(newSpeaker.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.api.on('passwordRequired', () => {
|
|
||||||
this.setRoomPassword();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private onEnterConference(info: ConferenceJoinedResult): void {
|
|
||||||
this.isJoined = true;
|
|
||||||
this.addMember({ displayName: info.displayName, id: info.id });
|
|
||||||
this.setRoomPassword();
|
|
||||||
if (this.videoStreamUrl) {
|
|
||||||
this.showJitsiDialog();
|
|
||||||
}
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
private autoJoinJitsiByLosIndex(operatorClosIndex: number): void {
|
|
||||||
if (operatorClosIndex !== UserListIndexType.NotOnList) {
|
|
||||||
if (!this.isOnCurrentLos) {
|
|
||||||
this.isOnCurrentLos = true;
|
|
||||||
this.triggerMeetingRoomButtonAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.nextSpeakerAmount &&
|
|
||||||
this.nextSpeakerAmount > 0 &&
|
|
||||||
operatorClosIndex > UserListIndexType.Active &&
|
|
||||||
operatorClosIndex <= this.nextSpeakerAmount &&
|
|
||||||
!this.isJitsiActive
|
|
||||||
) {
|
|
||||||
this.enterConferenceRoom();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.isOnCurrentLos = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.isAccessPermitted) {
|
|
||||||
this.viewStream();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private setRoomPassword(): void {
|
|
||||||
if (this.roomPassword && !this.isPasswortSet) {
|
|
||||||
// You can only set the password after the server has recognized that you are
|
|
||||||
// the moderator. There is no event listener for that.
|
|
||||||
setTimeout(() => {
|
|
||||||
this.api.executeCommand('password', this.roomPassword);
|
|
||||||
this.isPasswortSet = true;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private newDominantSpeaker(newSpeakerId: string): void {
|
|
||||||
if (this.currentDominantSpeaker && this.members[this.currentDominantSpeaker.id]) {
|
|
||||||
this.members[this.currentDominantSpeaker.id].focus = false;
|
|
||||||
}
|
|
||||||
this.members[newSpeakerId].focus = true;
|
|
||||||
this.currentDominantSpeaker = {
|
|
||||||
id: newSpeakerId,
|
|
||||||
displayName: this.members[newSpeakerId].name
|
|
||||||
};
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
private addMember(newMember: JitsiMember): void {
|
|
||||||
this.members[newMember.id] = {
|
|
||||||
name: newMember.displayName,
|
|
||||||
focus: false
|
|
||||||
} as ConferenceMember;
|
|
||||||
}
|
|
||||||
|
|
||||||
private removeMember(oldMember: { id: string }): void {
|
|
||||||
if (this.members[oldMember.id]) {
|
|
||||||
delete this.members[oldMember.id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private renameMember(member: DisplayNameChangeResult): void {
|
|
||||||
if (this.members[member.id]) {
|
|
||||||
this.members[member.id].name = member.displayname;
|
|
||||||
}
|
|
||||||
if (this.currentDominantSpeaker?.id === member.id) {
|
|
||||||
this.newDominantSpeaker(member.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearMembers(): void {
|
|
||||||
this.members = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stopJitsi(): Promise<void> {
|
|
||||||
this.connectToHelpDesk = false;
|
|
||||||
if (this.isJitsiActive) {
|
|
||||||
this.api.executeCommand('hangup');
|
|
||||||
this.clearMembers();
|
|
||||||
await this.deleteJitsiLock();
|
|
||||||
this.api.dispose();
|
|
||||||
this.api = undefined;
|
|
||||||
this.hideJitsiDialog();
|
|
||||||
}
|
|
||||||
this.isJoined = false;
|
|
||||||
this.isPasswortSet = false;
|
|
||||||
this.currentDominantSpeaker = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setOptions(): void {
|
|
||||||
this.options = {
|
|
||||||
roomName: this.actualRoomName,
|
|
||||||
parentNode: this.jitsiNode.nativeElement,
|
|
||||||
configOverwrite: this.configOverwrite,
|
|
||||||
interfaceConfigOverwrite: this.interfaceConfigOverwrite
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleShowJitsi(): void {
|
|
||||||
this.showJitsiWindow = !this.showJitsiWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleConferenceDialog(): void {
|
|
||||||
if (this.isJitsiDialogOpen) {
|
|
||||||
this.hideJitsiDialog();
|
|
||||||
} else {
|
|
||||||
this.showJitsiDialog();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public hideJitsiDialog(): void {
|
|
||||||
this.isJitsiDialogOpen = false;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
public showJitsiDialog(): void {
|
|
||||||
this.isJitsiDialogOpen = true;
|
|
||||||
this.showJitsiWindow = false;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
public viewStream(): void {
|
|
||||||
this.stopJitsi();
|
|
||||||
this.setConferenceState(ConferenceState.stream);
|
|
||||||
this.showJitsiWindow = true;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onSteamLoaded(): void {
|
|
||||||
/**
|
|
||||||
* explicit false check, undefined would mean that this was not checked yet
|
|
||||||
*/
|
|
||||||
if (this.streamLoadedOnce === false) {
|
|
||||||
this.storageMap.set(this.STREAM_RUNNING_STORAGE_KEY, true).subscribe(() => {
|
|
||||||
this.streamRunning = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public showVideoPlayer(): boolean {
|
|
||||||
if (!this.canSeeLiveStream) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.streamRunning || this.streamLoadedOnce === false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isStreamInOtherTab(): boolean {
|
|
||||||
return !this.streamRunning && this.streamLoadedOnce;
|
|
||||||
}
|
|
||||||
|
|
||||||
public enterConferenceRoom(): void {
|
|
||||||
this.actualRoomName = this.defaultRoomName;
|
|
||||||
this.connectToHelpDesk = false;
|
|
||||||
this.enterConversation();
|
|
||||||
}
|
|
||||||
|
|
||||||
public enterSupportRoom(): void {
|
|
||||||
this.actualRoomName = `${this.defaultRoomName}-SUPPORT`;
|
|
||||||
this.connectToHelpDesk = true;
|
|
||||||
this.enterConversation();
|
|
||||||
}
|
|
||||||
|
|
||||||
private onLiveStreamAvailable(liveStreamUrl: string): void {
|
|
||||||
this.videoStreamUrl = liveStreamUrl;
|
|
||||||
// this is the "dead" state; you would see the jitsi state; but are not connected
|
|
||||||
// or the connection is prohibited. If this occurs and a live stream
|
|
||||||
// becomes available, switch to the stream state
|
|
||||||
if (this.videoStreamUrl && this.currentState === ConferenceState.jitsi && !this.isJitsiActive) {
|
|
||||||
this.viewStream();
|
|
||||||
} else if (!this.videoStreamUrl && this.enableJitsi) {
|
|
||||||
this.setConferenceState(ConferenceState.jitsi);
|
|
||||||
}
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async deleteJitsiLock(): Promise<void> {
|
|
||||||
await this.storageMap.delete(this.RTC_LOGGED_STORAGE_KEY).toPromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async deleteStreamingLock(): Promise<void> {
|
|
||||||
await this.storageMap.delete(this.STREAM_RUNNING_STORAGE_KEY).toPromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
private setConferenceState(newState: ConferenceState): void {
|
|
||||||
this.currentState = newState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public sendApplause(): void {
|
|
||||||
this.applauseDisabled = true;
|
|
||||||
this.applauseService.sendApplause();
|
|
||||||
setTimeout(() => {
|
|
||||||
this.applauseDisabled = false;
|
|
||||||
this.cd.markForCheck();
|
|
||||||
}, this.applauseTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,10 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input, ViewEncapsulation } from '@angular/core';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-progress',
|
selector: 'os-progress',
|
||||||
templateUrl: './progress.component.html',
|
templateUrl: './progress.component.html',
|
||||||
styleUrls: ['./progress.component.scss']
|
styleUrls: ['./progress.component.scss'],
|
||||||
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class ProgressComponent {
|
export class ProgressComponent {
|
||||||
@Input()
|
@Input()
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
<div class="video-wrapper">
|
<div class="video-wrapper">
|
||||||
<os-applause-particle-display *ngIf="showParticles" class="applause-particles"></os-applause-particle-display>
|
<div class="player-container" [ngClass]="{ hide: !isUrlOnline && usingVjs }" *ngIf="isStable">
|
||||||
<div class="player-container" [ngClass]="{ hide: !isUrlOnline && usingVjs }">
|
|
||||||
<div *ngIf="usingVjs">
|
<div *ngIf="usingVjs">
|
||||||
<video #vjs class="video-js" controls preload="none"></video>
|
<video #vjs class="video-js" controls preload="none"></video>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,15 +4,6 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.applause-particles {
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
pointer-events: none !important;
|
|
||||||
width: 100px;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.is-offline-wrapper {
|
.is-offline-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
|
import { ThrowStmt } from '@angular/compiler';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
|
ApplicationRef,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
ElementRef,
|
ElementRef,
|
||||||
EventEmitter,
|
EventEmitter,
|
||||||
@ -17,6 +20,7 @@ import { ajax, AjaxResponse } from 'rxjs/ajax';
|
|||||||
import { catchError, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import videojs from 'video.js';
|
import videojs from 'video.js';
|
||||||
|
|
||||||
|
import { OpenSlidesStatusService } from 'app/core/core-services/openslides-status.service';
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
|
|
||||||
enum MimeType {
|
enum MimeType {
|
||||||
@ -34,32 +38,41 @@ enum Player {
|
|||||||
selector: 'os-video-player',
|
selector: 'os-video-player',
|
||||||
templateUrl: './video-player.component.html',
|
templateUrl: './video-player.component.html',
|
||||||
styleUrls: ['./video-player.component.scss'],
|
styleUrls: ['./video-player.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None
|
||||||
})
|
})
|
||||||
export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
export class VideoPlayerComponent implements AfterViewInit, OnDestroy {
|
||||||
@ViewChild('vjs', { static: false })
|
@ViewChild('vjs', { static: false })
|
||||||
private vjsPlayerElementRef: ElementRef;
|
private vjsPlayerElementRef: ElementRef;
|
||||||
|
|
||||||
private _videoUrl: string;
|
private _videoUrl: string;
|
||||||
|
|
||||||
|
public isStable = false;
|
||||||
|
private afterViewInitDone = false;
|
||||||
|
|
||||||
|
private youtubeQuerryParams = '?rel=0&iv_load_policy=3&modestbranding=1&autoplay=1';
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
public set videoUrl(value: string) {
|
public set videoUrl(value: string) {
|
||||||
|
if (!value.trim()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._videoUrl = value.trim();
|
this._videoUrl = value.trim();
|
||||||
this.playerType = this.determinePlayer(this.videoUrl);
|
this.playerType = this.determinePlayer(this.videoUrl);
|
||||||
|
|
||||||
if (this.usingVjs) {
|
if (this.usingVjs) {
|
||||||
this.mimeType = this.determineContentTypeByUrl(this.videoUrl);
|
this.mimeType = this.determineContentTypeByUrl(this.videoUrl);
|
||||||
this.initVjs();
|
if (this.afterViewInitDone) {
|
||||||
|
this.initVjs();
|
||||||
|
}
|
||||||
} else if (this.usingYouTube) {
|
} else if (this.usingYouTube) {
|
||||||
this.stopVJS();
|
this.stopVJS();
|
||||||
this.unloadVjs();
|
this.unloadVjs();
|
||||||
this.youTubeVideoId = this.getYouTubeVideoId(this.videoUrl);
|
this.youTubeVideoId = this.getYouTubeVideoId(this.videoUrl);
|
||||||
}
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Input()
|
|
||||||
public showParticles: boolean;
|
|
||||||
|
|
||||||
public get videoUrl(): string {
|
public get videoUrl(): string {
|
||||||
return this._videoUrl;
|
return this._videoUrl;
|
||||||
}
|
}
|
||||||
@ -83,17 +96,39 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public get youTubeVideoUrl(): string {
|
public get youTubeVideoUrl(): string {
|
||||||
return `https://www.youtube.com/embed/${this.youTubeVideoId}?autoplay=1`;
|
return `https://www.youtube.com/embed/${this.youTubeVideoId}${this.youtubeQuerryParams}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
public constructor(config: ConfigService) {
|
public constructor(
|
||||||
|
config: ConfigService,
|
||||||
|
private cd: ChangeDetectorRef,
|
||||||
|
private osStatus: OpenSlidesStatusService
|
||||||
|
) {
|
||||||
config.get<string>('general_system_stream_poster').subscribe(posterUrl => {
|
config.get<string>('general_system_stream_poster').subscribe(posterUrl => {
|
||||||
this.posterUrl = posterUrl?.trim();
|
this.posterUrl = posterUrl?.trim();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* external iFrame will block loading, since for some reason the app will
|
||||||
|
* not become stable if an iFrame was loaded.
|
||||||
|
* (or just goes instable again, for some unknown reason)
|
||||||
|
* This will result in an endless spinner
|
||||||
|
* It's crucial to render external
|
||||||
|
* Videos AFTER the app was stable
|
||||||
|
*/
|
||||||
|
this.osStatus.stable.then(() => {
|
||||||
|
this.isStable = true;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngAfterViewInit(): void {
|
public ngAfterViewInit(): void {
|
||||||
this.started.next();
|
if (this.usingVjs) {
|
||||||
|
this.initVjs();
|
||||||
|
} else {
|
||||||
|
this.started.next();
|
||||||
|
}
|
||||||
|
this.afterViewInitDone = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy(): void {
|
public ngOnDestroy(): void {
|
||||||
@ -102,7 +137,6 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
|||||||
|
|
||||||
private stopVJS(): void {
|
private stopVJS(): void {
|
||||||
if (this.vjsPlayer) {
|
if (this.vjsPlayer) {
|
||||||
this.vjsPlayer.src = '';
|
|
||||||
this.vjsPlayer.pause();
|
this.vjsPlayer.pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,6 +170,7 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
|||||||
} else {
|
} else {
|
||||||
this.isUrlOnline = false;
|
this.isUrlOnline = false;
|
||||||
}
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async onRefreshVideo(): Promise<void> {
|
public async onRefreshVideo(): Promise<void> {
|
||||||
@ -145,7 +180,6 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
|||||||
|
|
||||||
private async initVjs(): Promise<void> {
|
private async initVjs(): Promise<void> {
|
||||||
await this.isUrlReachable();
|
await this.isUrlReachable();
|
||||||
|
|
||||||
if (!this.vjsPlayer && this.usingVjs && this.vjsPlayerElementRef) {
|
if (!this.vjsPlayer && this.usingVjs && this.vjsPlayerElementRef) {
|
||||||
this.vjsPlayer = videojs(this.vjsPlayerElementRef.nativeElement, {
|
this.vjsPlayer = videojs(this.vjsPlayerElementRef.nativeElement, {
|
||||||
textTrackSettings: false,
|
textTrackSettings: false,
|
||||||
@ -159,11 +193,15 @@ export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private playVjsVideo(): void {
|
private playVjsVideo(): void {
|
||||||
|
if (!this.isUrlOnline) {
|
||||||
|
this.stopVJS();
|
||||||
|
}
|
||||||
if (this.usingVjs && this.vjsPlayer && this.isUrlOnline) {
|
if (this.usingVjs && this.vjsPlayer && this.isUrlOnline) {
|
||||||
this.vjsPlayer.src({
|
this.vjsPlayer.src({
|
||||||
src: this.videoUrl,
|
src: this.videoUrl,
|
||||||
type: this.mimeType
|
type: this.mimeType
|
||||||
});
|
});
|
||||||
|
this.started.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,14 +127,10 @@ 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 { VideoPlayerComponent } from './components/video-player/video-player.component';
|
|
||||||
import { ListOfSpeakersContentComponent } from './components/list-of-speakers-content/list-of-speakers-content.component';
|
import { ListOfSpeakersContentComponent } from './components/list-of-speakers-content/list-of-speakers-content.component';
|
||||||
import { ApplauseBarDisplayComponent } from './components/applause-bar-display/applause-bar-display.component';
|
|
||||||
import { ProgressComponent } from './components/progress/progress.component';
|
import { ProgressComponent } from './components/progress/progress.component';
|
||||||
import { NgParticlesModule } from 'ng-particles';
|
|
||||||
import { ApplauseParticleDisplayComponent } from './components/applause-particle-display/applause-particle-display.component';
|
|
||||||
import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/point-of-order-dialog.component';
|
import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/point-of-order-dialog.component';
|
||||||
|
import { VideoPlayerComponent } from './components/video-player/video-player.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Share Module for all "dumb" components and pipes.
|
* Share Module for all "dumb" components and pipes.
|
||||||
@ -199,8 +195,7 @@ import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/
|
|||||||
PblNgridTargetEventsModule,
|
PblNgridTargetEventsModule,
|
||||||
PdfViewerModule,
|
PdfViewerModule,
|
||||||
NgxMaterialTimepickerModule,
|
NgxMaterialTimepickerModule,
|
||||||
ChartsModule,
|
ChartsModule
|
||||||
NgParticlesModule
|
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
FormsModule,
|
FormsModule,
|
||||||
@ -304,10 +299,11 @@ import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/
|
|||||||
VotingPrivacyWarningComponent,
|
VotingPrivacyWarningComponent,
|
||||||
MotionPollDetailContentComponent,
|
MotionPollDetailContentComponent,
|
||||||
AssignmentPollDetailContentComponent,
|
AssignmentPollDetailContentComponent,
|
||||||
JitsiComponent,
|
|
||||||
VideoPlayerComponent,
|
|
||||||
ListOfSpeakersContentComponent,
|
ListOfSpeakersContentComponent,
|
||||||
PointOfOrderDialogComponent
|
PointOfOrderDialogComponent,
|
||||||
|
ListOfSpeakersContentComponent,
|
||||||
|
ProgressComponent,
|
||||||
|
VideoPlayerComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
PermsDirective,
|
PermsDirective,
|
||||||
@ -369,13 +365,10 @@ import { PointOfOrderDialogComponent } from './components/point-of-order-dialog/
|
|||||||
VotingPrivacyWarningComponent,
|
VotingPrivacyWarningComponent,
|
||||||
MotionPollDetailContentComponent,
|
MotionPollDetailContentComponent,
|
||||||
AssignmentPollDetailContentComponent,
|
AssignmentPollDetailContentComponent,
|
||||||
JitsiComponent,
|
|
||||||
VideoPlayerComponent,
|
|
||||||
ListOfSpeakersContentComponent,
|
ListOfSpeakersContentComponent,
|
||||||
ApplauseBarDisplayComponent,
|
|
||||||
ProgressComponent,
|
ProgressComponent,
|
||||||
ApplauseParticleDisplayComponent,
|
PointOfOrderDialogComponent,
|
||||||
PointOfOrderDialogComponent
|
VideoPlayerComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
<div class="interaction-bar-wrapper" *ngIf="!(isConfStateNone | async)">
|
||||||
|
<ng-container *ngIf="isConfStateJitsi | async">
|
||||||
|
<ng-container *ngIf="(isJoined | async) && !(showCallDialog | async)">
|
||||||
|
<!-- show dialog -->
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
class="action-bar-shadow background-default"
|
||||||
|
matTooltip="{{ 'Show conference room' | translate }}"
|
||||||
|
(click)="maximizeCallDialog()"
|
||||||
|
@fade
|
||||||
|
>
|
||||||
|
<mat-icon
|
||||||
|
color="primary"
|
||||||
|
[@fadeInOut]="meetingActiveAnimHelper"
|
||||||
|
(@fadeInOut.done)="triggerCallHiddenAnimation()"
|
||||||
|
>
|
||||||
|
fullscreen
|
||||||
|
</mat-icon>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="(showLiveConf | async) && (isConfStateStream | async)">
|
||||||
|
<!-- Enter conference from stream -->
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
class="action-bar-shadow"
|
||||||
|
[class.background-default]="canEnterCall"
|
||||||
|
[class.fake-disabled]="!canEnterCall"
|
||||||
|
matTooltip="{{ enterRoomTooltip | translate }}"
|
||||||
|
(click)="enterConferenceRoom(canEnterCall)"
|
||||||
|
@fade
|
||||||
|
>
|
||||||
|
<mat-icon color="primary"> phone </mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Call support button -->
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
class="action-bar-shadow background-default"
|
||||||
|
(click)="enterSupportRoom()"
|
||||||
|
matTooltip="{{ 'Access help desk' | translate }}"
|
||||||
|
*ngIf="isSupportEnabled | async"
|
||||||
|
@fade
|
||||||
|
>
|
||||||
|
<mat-icon color="primary">help_outline</mat-icon>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<!-- applause button -->
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
class="action-bar-shadow background-default"
|
||||||
|
matTooltip="{{ 'Send applause' | translate }}"
|
||||||
|
[disabled]="applauseDisabled"
|
||||||
|
[matBadge]="applauseLevelObservable | async"
|
||||||
|
[matBadgeHidden]="!(applauseLevelObservable | async)"
|
||||||
|
(click)="sendApplause()"
|
||||||
|
*ngIf="showApplause | async"
|
||||||
|
@fade
|
||||||
|
>
|
||||||
|
<mat-icon class="svg-primary" svgIcon="clapping_hands"></mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,17 @@
|
|||||||
|
:host {
|
||||||
|
margin: auto 0.5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interaction-bar-wrapper {
|
||||||
|
width: auto;
|
||||||
|
transition: all 2s linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar-shadow {
|
||||||
|
box-shadow: 0px 0px 4px 1px rgba(0, 0, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-button-base {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
margin-bottom: 0.5em;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { ActionBarComponent } from './action-bar.component';
|
||||||
|
|
||||||
|
describe('ActionBarComponent', () => {
|
||||||
|
let component: ActionBarComponent;
|
||||||
|
let fixture: ComponentFixture<ActionBarComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [ActionBarComponent],
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ActionBarComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,124 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, ViewEncapsulation } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { fadeAnimation, fadeInOut } from 'app/shared/animations';
|
||||||
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
|
import { ApplauseService } from '../../services/applause.service';
|
||||||
|
import { CallRestrictionService } from '../../services/call-restriction.service';
|
||||||
|
import { InteractionService } from '../../services/interaction.service';
|
||||||
|
import { RtcService } from '../../services/rtc.service';
|
||||||
|
|
||||||
|
const canEnterTooltip = _('Enter conference room');
|
||||||
|
const cannotEnterTooltip = _('Add yourself to the current list of speakers to join the conference');
|
||||||
|
@Component({
|
||||||
|
selector: 'os-action-bar',
|
||||||
|
templateUrl: './action-bar.component.html',
|
||||||
|
styleUrls: ['./action-bar.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [fadeInOut, fadeAnimation]
|
||||||
|
})
|
||||||
|
export class ActionBarComponent extends BaseViewComponentDirective {
|
||||||
|
public applauseLevel = 0;
|
||||||
|
public applauseDisabled = false;
|
||||||
|
|
||||||
|
public showApplause: Observable<boolean> = this.applauseService.showApplause;
|
||||||
|
public applauseLevelObservable: Observable<number> = this.applauseService.applauseLevelObservable;
|
||||||
|
public applauseTimeout = this.applauseService.applauseTimeout;
|
||||||
|
public isJoined: Observable<boolean> = this.rtcService.isJoinedObservable;
|
||||||
|
public showCallDialog: Observable<boolean> = this.rtcService.showCallDialogObservable;
|
||||||
|
public showLiveConf: Observable<boolean> = this.interactionService.showLiveConfObservable;
|
||||||
|
|
||||||
|
public isSupportEnabled: Observable<boolean> = this.rtcService.isSupportEnabled;
|
||||||
|
|
||||||
|
private canEnterCallObservable: Observable<boolean> = this.callRestrictionService.canEnterCallObservable;
|
||||||
|
public canEnterCall = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* for the pulse animation
|
||||||
|
*/
|
||||||
|
public enterCallAnimHelper = true;
|
||||||
|
public meetingActiveAnimHelper = true;
|
||||||
|
|
||||||
|
public get isConfStateStream(): Observable<boolean> {
|
||||||
|
return this.interactionService.isConfStateStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isConfStateJitsi(): Observable<boolean> {
|
||||||
|
return this.interactionService.isConfStateJitsi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isConfStateNone(): Observable<boolean> {
|
||||||
|
return this.interactionService.isConfStateNone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get enterRoomTooltip(): string {
|
||||||
|
if (this.canEnterCall) {
|
||||||
|
return _(canEnterTooltip);
|
||||||
|
} else {
|
||||||
|
return _(cannotEnterTooltip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
titleService: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
matSnackBar: MatSnackBar,
|
||||||
|
private router: Router,
|
||||||
|
private callRestrictionService: CallRestrictionService,
|
||||||
|
private interactionService: InteractionService,
|
||||||
|
private rtcService: RtcService,
|
||||||
|
private applauseService: ApplauseService,
|
||||||
|
private cd: ChangeDetectorRef
|
||||||
|
) {
|
||||||
|
super(titleService, translate, matSnackBar);
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.canEnterCallObservable.subscribe(canEnter => {
|
||||||
|
this.canEnterCall = canEnter;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enterConferenceRoom(canEnter: boolean): Promise<void> {
|
||||||
|
if (canEnter) {
|
||||||
|
this.interactionService
|
||||||
|
.enterCall()
|
||||||
|
.then(() => this.rtcService.enterConferenceRoom())
|
||||||
|
.catch(this.raiseError);
|
||||||
|
} else {
|
||||||
|
const navUrl = '/autopilot';
|
||||||
|
this.router.navigate([navUrl]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enterSupportRoom(): void {
|
||||||
|
this.interactionService
|
||||||
|
.enterCall()
|
||||||
|
.then(() => this.rtcService.enterSupportRoom())
|
||||||
|
.catch(this.raiseError);
|
||||||
|
}
|
||||||
|
|
||||||
|
public maximizeCallDialog(): void {
|
||||||
|
this.rtcService.showCallDialog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sendApplause(): void {
|
||||||
|
this.applauseDisabled = true;
|
||||||
|
this.applauseService.sendApplause();
|
||||||
|
this.cd.markForCheck();
|
||||||
|
setTimeout(() => {
|
||||||
|
this.applauseDisabled = false;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}, this.applauseTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
public triggerCallHiddenAnimation(): void {
|
||||||
|
this.meetingActiveAnimHelper = !this.meetingActiveAnimHelper;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
<div class="bar-wrapper" *ngIf="isApplauseTypeBar">
|
<div class="bar-wrapper">
|
||||||
<os-progress class="progress-bar" [value]="percent">
|
<os-progress class="progress-bar" [value]="percent">
|
||||||
<div class="level-indicator">
|
<div class="level-indicator">
|
||||||
<div class="level">
|
<div class="level">
|
@ -11,16 +11,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.level-indicator {
|
.level-indicator {
|
||||||
height: 50px;
|
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.level {
|
.level {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-top: 25px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.particle-display {
|
|
||||||
// height: 100%;
|
|
||||||
}
|
|
@ -4,10 +4,10 @@ import { Title } from '@angular/platform-browser';
|
|||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { Applause, ApplauseService, ApplauseType } from 'app/core/ui-services/applause.service';
|
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
import { fadeAnimation } from 'app/shared/animations';
|
import { fadeAnimation } from 'app/shared/animations';
|
||||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
|
import { ApplauseService, ApplauseType } from 'app/site/interaction/services/applause.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'os-applause-bar-display',
|
selector: 'os-applause-bar-display',
|
||||||
@ -26,10 +26,6 @@ export class ApplauseBarDisplayComponent extends BaseViewComponentDirective {
|
|||||||
return !!this.level;
|
return !!this.level;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isApplauseTypeBar(): boolean {
|
|
||||||
return this.applauseService.applauseType === ApplauseType.bar;
|
|
||||||
}
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
title: Title,
|
title: Title,
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
@ -7,10 +7,10 @@ import { Subject } from 'rxjs';
|
|||||||
import { auditTime } from 'rxjs/operators';
|
import { auditTime } from 'rxjs/operators';
|
||||||
import { Container } from 'tsparticles';
|
import { Container } from 'tsparticles';
|
||||||
|
|
||||||
import { ApplauseService } from 'app/core/ui-services/applause.service';
|
|
||||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
import { ElementSize } from 'app/shared/directives/resized.directive';
|
import { ElementSize } from 'app/shared/directives/resized.directive';
|
||||||
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
|
import { ApplauseService } from 'app/site/interaction/services/applause.service';
|
||||||
import { particleConfig, particleOptions } from './particle-options';
|
import { particleConfig, particleOptions } from './particle-options';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -79,7 +79,6 @@ export class ApplauseParticleDisplayComponent extends BaseViewComponentDirective
|
|||||||
private setParticleLevel(level: number): void {
|
private setParticleLevel(level: number): void {
|
||||||
if (this.particleContainer) {
|
if (this.particleContainer) {
|
||||||
const emitters = this.particleContainer.plugins.get('emitters') as any;
|
const emitters = this.particleContainer.plugins.get('emitters') as any;
|
||||||
// TODO: Use `Emitters` instead of any.
|
|
||||||
if (emitters) {
|
if (emitters) {
|
||||||
emitters.array[0].emitterOptions.rate.quantity = level;
|
emitters.array[0].emitterOptions.rate.quantity = level;
|
||||||
}
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
<!-- iFrame Dialog -->
|
||||||
|
<div class="jitsi-fake-dialog-wrapper" [ngClass]="{ 'dialog-mobile': (isMobile | async) }">
|
||||||
|
<mat-card class="jitsi-fake-dialog">
|
||||||
|
<div class="jitsi-dialog-actions">
|
||||||
|
<span>
|
||||||
|
<a
|
||||||
|
mat-icon-button
|
||||||
|
type="button"
|
||||||
|
color="primary"
|
||||||
|
matTooltip="{{ 'Open Jitsi in new tab' | translate }}"
|
||||||
|
target="_blank"
|
||||||
|
(click)="hangUp()"
|
||||||
|
[href]="jitsiMeetUrl | async"
|
||||||
|
*osPerms="'agenda.can_manage_list_of_speakers'"
|
||||||
|
>
|
||||||
|
<mat-icon>open_in_new</mat-icon>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
<span class="right">
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
type="button"
|
||||||
|
color="primary"
|
||||||
|
matTooltip="{{ 'Minimize' | translate }}"
|
||||||
|
(click)="hideDialog()"
|
||||||
|
>
|
||||||
|
<mat-icon>minimize</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- does not exist yet -->
|
||||||
|
<!-- <button
|
||||||
|
mat-icon-button
|
||||||
|
type="button"
|
||||||
|
color="primary"
|
||||||
|
matTooltip="{{ 'Full screen' | translate }}"
|
||||||
|
(click)="fullScreen()()"
|
||||||
|
>
|
||||||
|
<mat-icon>fullscreen</mat-icon>
|
||||||
|
</button> -->
|
||||||
|
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
type="button"
|
||||||
|
color="primary"
|
||||||
|
matTooltip="{{ 'Exit conference room' | translate }}"
|
||||||
|
(click)="hangUp()"
|
||||||
|
>
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="jitsi-iframe-wrapper" #jitsi></div>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
@ -0,0 +1,32 @@
|
|||||||
|
.dialog-mobile {
|
||||||
|
left: 15px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jitsi-fake-dialog-wrapper {
|
||||||
|
z-index: -1;
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: none;
|
||||||
|
left: 270px;
|
||||||
|
top: 20px;
|
||||||
|
right: 30px;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
.jitsi-fake-dialog {
|
||||||
|
padding: 0 5px 5px 5px;
|
||||||
|
pointer-events: all;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
height: 90%;
|
||||||
|
|
||||||
|
.jitsi-iframe-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jitsi-dialog-actions {
|
||||||
|
.right {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { CallDialogComponent } from './call-dialog.component';
|
||||||
|
|
||||||
|
describe('CallDialogComponent', () => {
|
||||||
|
let component: CallDialogComponent;
|
||||||
|
let fixture: ComponentFixture<CallDialogComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [CallDialogComponent],
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CallDialogComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
EventEmitter,
|
||||||
|
Output,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||||
|
import { RtcService } from '../../services/rtc.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-call-dialog',
|
||||||
|
templateUrl: './call-dialog.component.html',
|
||||||
|
styleUrls: ['./call-dialog.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class CallDialogComponent implements AfterViewInit {
|
||||||
|
@ViewChild('jitsi')
|
||||||
|
private jitsiNode: ElementRef;
|
||||||
|
|
||||||
|
public jitsiMeetUrl: Observable<string> = this.rtcService.jitsiMeetUrl;
|
||||||
|
|
||||||
|
public isMobile: Observable<boolean> = this.vp.isMobileSubject;
|
||||||
|
|
||||||
|
public constructor(private cd: ChangeDetectorRef, private rtcService: RtcService, private vp: ViewportService) {}
|
||||||
|
|
||||||
|
public ngAfterViewInit(): void {
|
||||||
|
this.rtcService.setJitsiNode(this.jitsiNode);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
public fullScreen(): void {
|
||||||
|
this.rtcService.enterFullScreen();
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
public hangUp(): void {
|
||||||
|
this.rtcService.stopJitsi();
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
public hideDialog(): void {
|
||||||
|
this.rtcService.showCallDialog = false;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
<os-call-dialog [ngClass]="{ hide: !(isJitsiDialogOpen | async) }"></os-call-dialog>
|
||||||
|
|
||||||
|
<div class="jitsi-list">
|
||||||
|
<!-- Jitsi content window -->
|
||||||
|
<div class="content">
|
||||||
|
<!-- The "somewhere else active" warning -->
|
||||||
|
<div class="disconnected" *ngIf="(isJitsiActiveInAnotherTab | async) && !isJitsiActive">
|
||||||
|
<span>{{ 'A conference is already running in your OpenSlides session.' | translate }}</span>
|
||||||
|
<button mat-button color="warn" (click)="forceStart()">
|
||||||
|
<span>{{ 'Reenter to conference room' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="disconnected" *ngIf="isDisconnected">
|
||||||
|
<mat-icon>cloud_off</mat-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="disconnected" *ngIf="isConnecting">
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- user list -->
|
||||||
|
<div class="room-members" *ngIf="isConnected">
|
||||||
|
<os-applause-particle-display
|
||||||
|
*ngIf="showParticles | async"
|
||||||
|
class="room-list-applause-particles"
|
||||||
|
></os-applause-particle-display>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<!-- Custom control buttons -->
|
||||||
|
<div>
|
||||||
|
<mat-divider></mat-divider>
|
||||||
|
<div class="control-grid">
|
||||||
|
<div class="control-buttons">
|
||||||
|
<!-- Hangup -->
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
color="warn"
|
||||||
|
(click)="hangUp()"
|
||||||
|
*ngIf="showHangUp"
|
||||||
|
matTooltip="{{ 'Leave' | translate }}"
|
||||||
|
>
|
||||||
|
<mat-icon>call_end</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Enter jitsi manually -->
|
||||||
|
<button
|
||||||
|
mat-mini-fab
|
||||||
|
color="accent"
|
||||||
|
(click)="callRoom()"
|
||||||
|
[disabled]="!(canEnterCall | async) || isConnecting"
|
||||||
|
*ngIf="!isJoined"
|
||||||
|
matTooltip="{{ 'Enter conference room' | translate }}"
|
||||||
|
>
|
||||||
|
<mat-icon>call</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="exit">
|
||||||
|
<!-- Exit jitsi, view stream -->
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
color="primary"
|
||||||
|
matTooltip="{{ 'Continue livestream' | translate }}"
|
||||||
|
(click)="viewStream()"
|
||||||
|
*ngIf="!!(liveStreamUrl | async)?.trim()"
|
||||||
|
>
|
||||||
|
<mat-icon>live_tv</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,64 @@
|
|||||||
|
$wrapper-padding: 5px;
|
||||||
|
|
||||||
|
.jitsi-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
|
||||||
|
.disconnected {
|
||||||
|
display: flex;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-members {
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.room-list-applause-particles {
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 70px;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
max-height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.member-list-entry {
|
||||||
|
margin: 5px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-grid {
|
||||||
|
padding: $wrapper-padding;
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: 'empty buttons exit';
|
||||||
|
grid-template-columns: 40px auto 40px;
|
||||||
|
|
||||||
|
.exit {
|
||||||
|
grid-area: exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-buttons {
|
||||||
|
grid-area: buttons;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,21 +2,21 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
import { JitsiComponent } from './jitsi.component';
|
import { CallComponent } from './call.component';
|
||||||
|
|
||||||
describe('JitsiComponent', () => {
|
describe('CallComponent', () => {
|
||||||
let component: JitsiComponent;
|
let component: CallComponent;
|
||||||
let fixture: ComponentFixture<JitsiComponent>;
|
let fixture: ComponentFixture<CallComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [E2EImportsModule],
|
declarations: [CallComponent],
|
||||||
declarations: [JitsiComponent]
|
imports: [E2EImportsModule]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(JitsiComponent);
|
fixture = TestBed.createComponent(CallComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
@ -0,0 +1,189 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
HostListener,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
|
import { ApplauseService } from '../../services/applause.service';
|
||||||
|
import { CallRestrictionService } from '../../services/call-restriction.service';
|
||||||
|
import { InteractionService } from '../../services/interaction.service';
|
||||||
|
import { RtcService } from '../../services/rtc.service';
|
||||||
|
import { StreamService } from '../../services/stream.service';
|
||||||
|
|
||||||
|
const helpDeskTitle = _('Help desk');
|
||||||
|
const liveConferenceTitle = _('Conference room');
|
||||||
|
const disconnectedTitle = _('disconnected');
|
||||||
|
const connectingTitle = _('connecting ...');
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-call',
|
||||||
|
templateUrl: './call.component.html',
|
||||||
|
styleUrls: ['./call.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class CallComponent extends BaseViewComponentDirective implements OnInit, AfterViewInit, OnDestroy {
|
||||||
|
public isJitsiActiveInAnotherTab: Observable<boolean> = this.rtcService.inOtherTab;
|
||||||
|
public canEnterCall: Observable<boolean> = this.callRestrictionService.canEnterCallObservable;
|
||||||
|
public isJitsiDialogOpen: Observable<boolean> = this.rtcService.showCallDialogObservable;
|
||||||
|
|
||||||
|
public isJitsiActive: boolean;
|
||||||
|
public isJoined: boolean;
|
||||||
|
|
||||||
|
public get showHangUp(): boolean {
|
||||||
|
return this.isJitsiActive && this.isJoined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private dominantSpeaker: string;
|
||||||
|
private members = {};
|
||||||
|
public get memberList(): string[] {
|
||||||
|
return Object.keys(this.members);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isDisconnected(): boolean {
|
||||||
|
return !this.isJitsiActive && !this.isJoined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isConnecting(): boolean {
|
||||||
|
return this.isJitsiActive && !this.isJoined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isConnected(): boolean {
|
||||||
|
return this.isJitsiActive && this.isJoined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get showParticles(): Observable<boolean> {
|
||||||
|
return this.applauseService.showParticles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get canSeeLiveStream(): Observable<boolean> {
|
||||||
|
return this.streamService.canSeeLiveStreamObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get liveStreamUrl(): Observable<string> {
|
||||||
|
return this.streamService.liveStreamUrlObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
private autoConnect: boolean;
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public conferenceTitle: EventEmitter<string> = new EventEmitter();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public conferenceSubtitle: EventEmitter<string> = new EventEmitter();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
titleService: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
snackBar: MatSnackBar,
|
||||||
|
private callRestrictionService: CallRestrictionService,
|
||||||
|
private rtcService: RtcService,
|
||||||
|
private applauseService: ApplauseService,
|
||||||
|
private interactionService: InteractionService,
|
||||||
|
private streamService: StreamService,
|
||||||
|
private cd: ChangeDetectorRef
|
||||||
|
) {
|
||||||
|
super(titleService, translate, snackBar);
|
||||||
|
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.rtcService.isJitsiActiveObservable.subscribe(active => {
|
||||||
|
this.isJitsiActive = active;
|
||||||
|
this.updateSubtitle();
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.rtcService.isJoinedObservable.subscribe(isJoined => {
|
||||||
|
this.isJoined = isJoined;
|
||||||
|
this.updateSubtitle();
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.rtcService.memberObservableObservable.subscribe(members => {
|
||||||
|
this.members = members;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.rtcService.dominantSpeakerObservable.subscribe(domSpeaker => {
|
||||||
|
this.dominantSpeaker = domSpeaker?.displayName;
|
||||||
|
this.updateSubtitle();
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.rtcService.autoConnect.subscribe(auto => {
|
||||||
|
this.autoConnect = auto;
|
||||||
|
}),
|
||||||
|
|
||||||
|
this.rtcService.connectedToHelpDesk.subscribe(onHelpDesk => {
|
||||||
|
if (onHelpDesk) {
|
||||||
|
this.conferenceTitle.next(helpDeskTitle);
|
||||||
|
} else {
|
||||||
|
this.conferenceTitle.next(liveConferenceTitle);
|
||||||
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.updateSubtitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngAfterViewInit(): void {
|
||||||
|
if (this.autoConnect) {
|
||||||
|
this.callRoom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// closing the tab should also try to stop jitsi.
|
||||||
|
// this will usually not be caught by ngOnDestroy
|
||||||
|
@HostListener('window:beforeunload', ['$event'])
|
||||||
|
public beforeunload($event: any): void {
|
||||||
|
this.rtcService.stopJitsi();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.rtcService.stopJitsi();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSubtitle(): void {
|
||||||
|
if (this.isJitsiActive && this.isJoined) {
|
||||||
|
this.conferenceSubtitle.next(this.dominantSpeaker || '');
|
||||||
|
} else if (this.isJitsiActive && !this.isJoined) {
|
||||||
|
this.conferenceSubtitle.next(connectingTitle);
|
||||||
|
} else {
|
||||||
|
this.conferenceSubtitle.next(disconnectedTitle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async callRoom(): Promise<void> {
|
||||||
|
await this.rtcService.enterConferenceRoom().catch(this.raiseError);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
public forceStart(): void {
|
||||||
|
this.rtcService.enterConferenceRoom(true).catch(this.raiseError);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
public hangUp(): void {
|
||||||
|
this.rtcService.stopJitsi();
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}
|
||||||
|
|
||||||
|
public viewStream(): void {
|
||||||
|
this.interactionService.viewStream();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
<div *ngIf="!(isConfStateNone | async)" class="interaction-container-wrapper">
|
||||||
|
<div class="container-head background-primary" (click)="showHideBody()">
|
||||||
|
<div class="container-head-wrapper">
|
||||||
|
<div class="ellipsis-overflow container-head-title">
|
||||||
|
{{ containerHeadTitle | translate }}
|
||||||
|
</div>
|
||||||
|
<div class="ellipsis-overflow">
|
||||||
|
{{ containerHeadSubtitle | translate }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="container-body background-card"
|
||||||
|
[ngClass]="{ 'container-body-with-applause-bar': showApplauseBar | async, 'container-body-hide': !showBody }"
|
||||||
|
>
|
||||||
|
<os-applause-bar-display
|
||||||
|
*ngIf="(isApplausEnabled | async) && (showApplauseBar | async)"
|
||||||
|
></os-applause-bar-display>
|
||||||
|
<ng-container *ngIf="isConfStateStream | async">
|
||||||
|
<os-stream
|
||||||
|
class="video-player"
|
||||||
|
(streamTitle)="updateTitle($event)"
|
||||||
|
(streamSubtitle)="updateSubtitle($event)"
|
||||||
|
></os-stream>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container *ngIf="isConfStateJitsi | async">
|
||||||
|
<os-call
|
||||||
|
class="call-body"
|
||||||
|
(conferenceTitle)="updateTitle($event)"
|
||||||
|
(conferenceSubtitle)="updateSubtitle($event)"
|
||||||
|
></os-call>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,59 @@
|
|||||||
|
$radius: 5px;
|
||||||
|
|
||||||
|
:host {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.interaction-container-wrapper {
|
||||||
|
box-shadow: 0px 0px 4px 2px rgba(0, 0, 0, 0.2);
|
||||||
|
border-top-left-radius: $radius;
|
||||||
|
border-top-right-radius: $radius;
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 70vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-head {
|
||||||
|
height: 50px;
|
||||||
|
display: flex;
|
||||||
|
border-top-left-radius: $radius;
|
||||||
|
border-top-right-radius: $radius;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
.container-head-wrapper {
|
||||||
|
margin-left: 1em;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-head-title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-body {
|
||||||
|
display: flex;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 280px;
|
||||||
|
transition: all 350ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-body-with-applause-bar {
|
||||||
|
max-width: 530px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-player {
|
||||||
|
display: block;
|
||||||
|
width: 500px;
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.call-body {
|
||||||
|
display: block;
|
||||||
|
width: 250px;
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-body-hide {
|
||||||
|
max-width: 250px;
|
||||||
|
max-height: 0px;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { InteractionContainerComponent } from './interaction-container.component';
|
||||||
|
|
||||||
|
describe('InteractionContainerComponent', () => {
|
||||||
|
let component: InteractionContainerComponent;
|
||||||
|
let fixture: ComponentFixture<InteractionContainerComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [InteractionContainerComponent],
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(InteractionContainerComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,117 @@
|
|||||||
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component } from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { combineLatest, forkJoin, merge, Observable } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, filter, mergeAll, mergeMap, withLatestFrom } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
|
import { ApplauseService } from '../../services/applause.service';
|
||||||
|
import { InteractionService } from '../../services/interaction.service';
|
||||||
|
import { RtcService } from '../../services/rtc.service';
|
||||||
|
import { StreamService } from '../../services/stream.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-interaction-container',
|
||||||
|
templateUrl: './interaction-container.component.html',
|
||||||
|
styleUrls: ['./interaction-container.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class InteractionContainerComponent extends BaseViewComponentDirective {
|
||||||
|
public showBody = false;
|
||||||
|
|
||||||
|
private streamRunning = false;
|
||||||
|
private streamLoadedOnce = false;
|
||||||
|
|
||||||
|
public containerHeadTitle = '';
|
||||||
|
public containerHeadSubtitle = '';
|
||||||
|
|
||||||
|
public get isApplausEnabled(): Observable<boolean> {
|
||||||
|
return this.applauseService.showApplause;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get showApplauseBar(): Observable<boolean> {
|
||||||
|
return this.applauseService.showBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isConfStateStream(): Observable<boolean> {
|
||||||
|
return this.interactionService.isConfStateStream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isConfStateJitsi(): Observable<boolean> {
|
||||||
|
return this.interactionService.isConfStateJitsi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isConfStateNone(): Observable<boolean> {
|
||||||
|
return this.interactionService.isConfStateNone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isStreamInOtherTab(): boolean {
|
||||||
|
return !this.streamRunning && this.streamLoadedOnce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
titleService: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
matSnackBar: MatSnackBar,
|
||||||
|
rtcService: RtcService,
|
||||||
|
streamService: StreamService,
|
||||||
|
private interactionService: InteractionService,
|
||||||
|
private applauseService: ApplauseService,
|
||||||
|
private cd: ChangeDetectorRef
|
||||||
|
) {
|
||||||
|
super(titleService, translate, matSnackBar);
|
||||||
|
this.subscriptions.push(
|
||||||
|
interactionService.conferenceStateObservable.pipe(distinctUntilChanged()).subscribe(state => {
|
||||||
|
if (state) {
|
||||||
|
this.clearTitles();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
rtcService.showCallDialogObservable.subscribe(show => {
|
||||||
|
if (show) {
|
||||||
|
this.showBody = false;
|
||||||
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
streamService.streamLoadedOnceObservable.subscribe(loadedOnce => {
|
||||||
|
this.streamLoadedOnce = loadedOnce;
|
||||||
|
if (!this.isStreamInOtherTab) {
|
||||||
|
this.showBody = true;
|
||||||
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
streamService.isStreamRunningObservable.subscribe(running => {
|
||||||
|
this.streamRunning = running || false;
|
||||||
|
if (!this.isStreamInOtherTab) {
|
||||||
|
this.showBody = true;
|
||||||
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearTitles(): void {
|
||||||
|
this.containerHeadTitle = '';
|
||||||
|
this.containerHeadSubtitle = '';
|
||||||
|
this.cd.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
public showHideBody(): void {
|
||||||
|
this.showBody = !this.showBody;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateTitle(title: string): void {
|
||||||
|
if (title !== this.containerHeadTitle) {
|
||||||
|
this.containerHeadTitle = title ?? '';
|
||||||
|
this.cd.detectChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateSubtitle(title: string): void {
|
||||||
|
if (title !== this.containerHeadSubtitle) {
|
||||||
|
this.containerHeadSubtitle = title ?? '';
|
||||||
|
this.cd.detectChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
<div *osPerms="permission.coreCanSeeLiveStream; and: showVideoPlayer">
|
||||||
|
<os-applause-particle-display
|
||||||
|
*ngIf="showParticles | async"
|
||||||
|
class="applause-particles"
|
||||||
|
></os-applause-particle-display>
|
||||||
|
<os-video-player [videoUrl]="liveStreamUrl" (started)="onSteamLoaded()"></os-video-player>
|
||||||
|
</div>
|
||||||
|
<div *osPerms="permission.coreCanSeeLiveStream; complement: true">
|
||||||
|
<span> {{ 'You are not allowed to see the live stream' | translate }} </span>
|
||||||
|
</div>
|
||||||
|
<div class="disconnected" *ngIf="isStreamInOtherTab">
|
||||||
|
<button class="restart-stream-button" mat-button color="warn" (click)="forceLoadStream()">
|
||||||
|
<span>{{ 'Restart livestream' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
@ -0,0 +1,8 @@
|
|||||||
|
.applause-particles {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
pointer-events: none !important;
|
||||||
|
width: 100px;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { StreamComponent } from './stream.component';
|
||||||
|
|
||||||
|
describe('StreamComponent', () => {
|
||||||
|
let component: StreamComponent;
|
||||||
|
let fixture: ComponentFixture<StreamComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [StreamComponent],
|
||||||
|
imports: [E2EImportsModule]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(StreamComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,115 @@
|
|||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
HostListener,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
Output
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { BaseViewComponentDirective } from 'app/site/base/base-view';
|
||||||
|
import { ApplauseService } from '../../services/applause.service';
|
||||||
|
import { StreamService } from '../../services/stream.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'os-stream',
|
||||||
|
templateUrl: './stream.component.html',
|
||||||
|
styleUrls: ['./stream.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class StreamComponent extends BaseViewComponentDirective implements AfterViewInit, OnDestroy {
|
||||||
|
private streamRunning = false;
|
||||||
|
|
||||||
|
public liveStreamUrl: string;
|
||||||
|
private streamLoadedOnce: boolean;
|
||||||
|
|
||||||
|
public get showParticles(): Observable<boolean> {
|
||||||
|
return this.applauseService.showParticles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get showVideoPlayer(): boolean {
|
||||||
|
return this.streamRunning || this.streamLoadedOnce === false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isStreamInOtherTab(): boolean {
|
||||||
|
return !this.streamRunning && this.streamLoadedOnce;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public streamTitle: EventEmitter<string> = new EventEmitter();
|
||||||
|
|
||||||
|
@Output()
|
||||||
|
public streamSubtitle: EventEmitter<string> = new EventEmitter();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
titleService: Title,
|
||||||
|
translate: TranslateService,
|
||||||
|
matSnackBar: MatSnackBar,
|
||||||
|
private streamService: StreamService,
|
||||||
|
private applauseService: ApplauseService,
|
||||||
|
private cd: ChangeDetectorRef
|
||||||
|
) {
|
||||||
|
super(titleService, translate, matSnackBar);
|
||||||
|
|
||||||
|
this.subscriptions.push(
|
||||||
|
this.streamService.liveStreamUrlObservable.subscribe(url => {
|
||||||
|
this.liveStreamUrl = url?.trim();
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
this.streamService.streamLoadedOnceObservable.subscribe(loadedOnce => {
|
||||||
|
this.streamLoadedOnce = loadedOnce || false;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
|
this.streamService.isStreamRunningObservable.subscribe(running => {
|
||||||
|
this.streamRunning = running || false;
|
||||||
|
this.cd.markForCheck();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngAfterViewInit(): void {
|
||||||
|
this.streamTitle.next('Livestream');
|
||||||
|
this.streamSubtitle.next('');
|
||||||
|
this.cd.detectChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// closing the tab should also try to stop jitsi.
|
||||||
|
// this will usually not be caught by ngOnDestroy
|
||||||
|
@HostListener('window:beforeunload', ['$event'])
|
||||||
|
public async beforeunload($event: any): Promise<void> {
|
||||||
|
this.beforeViewCloses();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
this.beforeViewCloses();
|
||||||
|
}
|
||||||
|
|
||||||
|
private beforeViewCloses(): void {
|
||||||
|
if (this.streamLoadedOnce && this.streamRunning) {
|
||||||
|
this.streamService.deleteStreamingLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public forceLoadStream(): void {
|
||||||
|
this.streamService.deleteStreamingLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSteamLoaded(): void {
|
||||||
|
/**
|
||||||
|
* explicit false check, undefined would mean that this was not checked yet
|
||||||
|
*/
|
||||||
|
if (this.streamLoadedOnce === false) {
|
||||||
|
this.streamService.setStreamingLock();
|
||||||
|
this.streamService.setStreamRunning(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
client/src/app/site/interaction/interaction.module.ts
Normal file
28
client/src/app/site/interaction/interaction.module.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { NgParticlesModule } from 'ng-particles';
|
||||||
|
|
||||||
|
import { ActionBarComponent } from './components/action-bar/action-bar.component';
|
||||||
|
import { ApplauseBarDisplayComponent } from './components/applause-bar-display/applause-bar-display.component';
|
||||||
|
import { ApplauseParticleDisplayComponent } from './components/applause-particle-display/applause-particle-display.component';
|
||||||
|
import { CallDialogComponent } from './components/call-dialog/call-dialog.component';
|
||||||
|
import { CallComponent } from './components/call/call.component';
|
||||||
|
import { InteractionContainerComponent } from './components/interaction-container/interaction-container.component';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { StreamComponent } from './components/stream/stream.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
ApplauseBarDisplayComponent,
|
||||||
|
ApplauseParticleDisplayComponent,
|
||||||
|
ActionBarComponent,
|
||||||
|
InteractionContainerComponent,
|
||||||
|
StreamComponent,
|
||||||
|
CallComponent,
|
||||||
|
CallDialogComponent
|
||||||
|
],
|
||||||
|
imports: [CommonModule, SharedModule, NgParticlesModule],
|
||||||
|
exports: [ActionBarComponent, InteractionContainerComponent]
|
||||||
|
})
|
||||||
|
export class InteractionModule {}
|
@ -3,9 +3,9 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from '../../../core/ui-services/config.service';
|
||||||
import { HttpService } from '../core-services/http.service';
|
import { HttpService } from '../../../core/core-services/http.service';
|
||||||
import { NotifyService } from '../core-services/notify.service';
|
import { NotifyService } from '../../../core/core-services/notify.service';
|
||||||
|
|
||||||
export interface Applause {
|
export interface Applause {
|
||||||
level: number;
|
level: number;
|
||||||
@ -21,35 +21,54 @@ const applausePath = '/system/applause';
|
|||||||
const applauseNotifyMessageName = 'applause';
|
const applauseNotifyMessageName = 'applause';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
// providedIn: InteractionModule
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
|
// provided: InteractionModule
|
||||||
})
|
})
|
||||||
export class ApplauseService {
|
export class ApplauseService {
|
||||||
private minApplauseLevel: number;
|
private minApplauseLevel: number;
|
||||||
private maxApplauseLevel: number;
|
private maxApplauseLevel: number;
|
||||||
private presentApplauseUsers: number;
|
private presentApplauseUsers: number;
|
||||||
|
private applauseTypeObservable: Observable<ApplauseType>;
|
||||||
|
|
||||||
public applauseType: ApplauseType;
|
public showApplause: Observable<boolean>;
|
||||||
|
public showApplauseLevel: boolean;
|
||||||
|
public applauseTimeout: number;
|
||||||
|
|
||||||
private applauseLevelSubject: Subject<number> = new Subject<number>();
|
private applauseLevelSubject: Subject<number> = new Subject<number>();
|
||||||
public applauseLevelObservable = this.applauseLevelSubject.asObservable();
|
public applauseLevelObservable: Observable<number> = this.applauseLevelSubject.asObservable();
|
||||||
|
|
||||||
private get maxApplause(): number {
|
private get maxApplause(): number {
|
||||||
return this.maxApplauseLevel || this.presentApplauseUsers || 0;
|
return this.maxApplauseLevel || this.presentApplauseUsers || 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get showParticles(): Observable<boolean> {
|
||||||
|
return this.applauseTypeObservable.pipe(map(type => type === ApplauseType.particles));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get showBar(): Observable<boolean> {
|
||||||
|
return this.applauseTypeObservable.pipe(map(type => type === ApplauseType.bar));
|
||||||
|
}
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
private httpService: HttpService,
|
private httpService: HttpService,
|
||||||
private notifyService: NotifyService
|
private notifyService: NotifyService
|
||||||
) {
|
) {
|
||||||
|
this.showApplause = configService.get<boolean>('general_system_applause_enable');
|
||||||
|
this.applauseTypeObservable = configService.get<ApplauseType>('general_system_applause_type');
|
||||||
|
|
||||||
configService.get<number>('general_system_applause_min_amount').subscribe(minLevel => {
|
configService.get<number>('general_system_applause_min_amount').subscribe(minLevel => {
|
||||||
this.minApplauseLevel = minLevel;
|
this.minApplauseLevel = minLevel;
|
||||||
});
|
});
|
||||||
configService.get<number>('general_system_applause_max_amount').subscribe(maxLevel => {
|
configService.get<number>('general_system_applause_max_amount').subscribe(maxLevel => {
|
||||||
this.maxApplauseLevel = maxLevel;
|
this.maxApplauseLevel = maxLevel;
|
||||||
});
|
});
|
||||||
configService.get<ApplauseType>('general_system_applause_type').subscribe((type: ApplauseType) => {
|
configService.get<boolean>('general_system_applause_show_level').subscribe(show => {
|
||||||
this.applauseType = type;
|
this.showApplauseLevel = show;
|
||||||
|
});
|
||||||
|
configService.get<number>('general_system_stream_applause_timeout').subscribe(timeout => {
|
||||||
|
this.applauseTimeout = (timeout || 1) * 1000;
|
||||||
});
|
});
|
||||||
this.notifyService
|
this.notifyService
|
||||||
.getMessageObservable<Applause>(applauseNotifyMessageName)
|
.getMessageObservable<Applause>(applauseNotifyMessageName)
|
@ -0,0 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { CallRestrictionService } from './call-restriction.service';
|
||||||
|
|
||||||
|
describe('CallRestrictionService', () => {
|
||||||
|
let service: CallRestrictionService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({ imports: [E2EImportsModule] });
|
||||||
|
service = TestBed.inject(CallRestrictionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,95 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||||
|
import { distinctUntilChanged, map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
|
||||||
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
|
import { UserListIndexType } from 'app/site/agenda/models/view-list-of-speakers';
|
||||||
|
import { CurrentListOfSpeakersService } from 'app/site/projector/services/current-list-of-speakers.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class CallRestrictionService {
|
||||||
|
private canManageSpeaker: boolean;
|
||||||
|
private restricted: boolean;
|
||||||
|
private isOnCurrentLos: boolean;
|
||||||
|
private nextSpeakerAmount: number;
|
||||||
|
|
||||||
|
public isAccessRestricted: Observable<boolean>;
|
||||||
|
|
||||||
|
private canEnterCallSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
public canEnterCallObservable = this.canEnterCallSubject.asObservable();
|
||||||
|
|
||||||
|
private hasToEnterCallSubject = new Subject<void>();
|
||||||
|
public hasToEnterCallObservable = this.hasToEnterCallSubject.asObservable();
|
||||||
|
|
||||||
|
private hasToLeaveCallSubject = new Subject<void>();
|
||||||
|
public hasToLeaveCallObservable = this.hasToLeaveCallSubject.asObservable();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
configService: ConfigService,
|
||||||
|
operator: OperatorService,
|
||||||
|
closService: CurrentListOfSpeakersService
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* general access perm
|
||||||
|
*/
|
||||||
|
operator.getUserObservable().subscribe(() => {
|
||||||
|
this.canManageSpeaker = operator.hasPerms(Permission.agendaCanManageListOfSpeakers);
|
||||||
|
this.updateCanEnterCall();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LosRestriction
|
||||||
|
*/
|
||||||
|
this.isAccessRestricted = configService.get<boolean>('general_system_conference_los_restriction');
|
||||||
|
this.isAccessRestricted.subscribe(restricted => {
|
||||||
|
this.restricted = restricted;
|
||||||
|
this.updateCanEnterCall();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is User In Clos
|
||||||
|
*/
|
||||||
|
closService.currentListOfSpeakersObservable
|
||||||
|
.pipe(
|
||||||
|
map(los => los?.findUserIndexOnList(operator.user.id) ?? -1),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
.subscribe(userLosIndex => {
|
||||||
|
this.isOnCurrentLos = userLosIndex !== UserListIndexType.NotOnList;
|
||||||
|
this.updateCanEnterCall();
|
||||||
|
this.updateAutoJoinJitsiByLosIndex(userLosIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of next speakers
|
||||||
|
*/
|
||||||
|
configService
|
||||||
|
.get<number>('general_system_conference_auto_connect_next_speakers')
|
||||||
|
.subscribe(nextSpeakerAmount => {
|
||||||
|
this.nextSpeakerAmount = nextSpeakerAmount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateCanEnterCall(): void {
|
||||||
|
this.canEnterCallSubject.next(!this.restricted || this.canManageSpeaker || this.isOnCurrentLos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateAutoJoinJitsiByLosIndex(operatorClosIndex: number): void {
|
||||||
|
if (operatorClosIndex !== UserListIndexType.NotOnList) {
|
||||||
|
if (
|
||||||
|
this.nextSpeakerAmount &&
|
||||||
|
this.nextSpeakerAmount > 0 &&
|
||||||
|
operatorClosIndex > UserListIndexType.Active &&
|
||||||
|
operatorClosIndex <= this.nextSpeakerAmount
|
||||||
|
) {
|
||||||
|
this.hasToEnterCallSubject.next();
|
||||||
|
}
|
||||||
|
} else if (operatorClosIndex === UserListIndexType.NotOnList && this.restricted) {
|
||||||
|
this.hasToLeaveCallSubject.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { InteractionService } from './interaction.service';
|
||||||
|
|
||||||
|
describe('InteractionService', () => {
|
||||||
|
let service: InteractionService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({ imports: [E2EImportsModule] });
|
||||||
|
service = TestBed.inject(InteractionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
153
client/src/app/site/interaction/services/interaction.service.ts
Normal file
153
client/src/app/site/interaction/services/interaction.service.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
|
import { CallRestrictionService } from './call-restriction.service';
|
||||||
|
import { RtcService } from './rtc.service';
|
||||||
|
import { StreamService } from './stream.service';
|
||||||
|
|
||||||
|
export enum ConferenceState {
|
||||||
|
none,
|
||||||
|
stream,
|
||||||
|
jitsi
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class InteractionService {
|
||||||
|
private conferenceStateSubject = new BehaviorSubject<ConferenceState>(ConferenceState.none);
|
||||||
|
public conferenceStateObservable = this.conferenceStateSubject.asObservable();
|
||||||
|
public showLiveConfObservable: Observable<boolean>;
|
||||||
|
private get conferenceState(): ConferenceState {
|
||||||
|
return this.conferenceStateSubject.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isJitsiEnabled: boolean;
|
||||||
|
private isInCall: boolean;
|
||||||
|
private isJitsiActive: boolean;
|
||||||
|
private hasLiveStreamUrl: boolean;
|
||||||
|
private canSeeLiveStream: boolean;
|
||||||
|
private showLiveConf: boolean;
|
||||||
|
|
||||||
|
public get isConfStateStream(): Observable<boolean> {
|
||||||
|
return this.conferenceStateObservable.pipe(map(state => state === ConferenceState.stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isConfStateJitsi(): Observable<boolean> {
|
||||||
|
return this.conferenceStateObservable.pipe(map(state => state === ConferenceState.jitsi));
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isConfStateNone(): Observable<boolean> {
|
||||||
|
return this.conferenceStateObservable.pipe(map(state => state === ConferenceState.none));
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private configService: ConfigService,
|
||||||
|
private streamService: StreamService,
|
||||||
|
private rtcService: RtcService,
|
||||||
|
private callRestrictionService: CallRestrictionService
|
||||||
|
) {
|
||||||
|
this.showLiveConfObservable = this.configService.get<boolean>('general_system_conference_show');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If you want to somehow simplify this using rxjs merge-map magic or something
|
||||||
|
* be my guest.
|
||||||
|
*/
|
||||||
|
this.streamService.liveStreamUrlObservable.subscribe(url => {
|
||||||
|
this.hasLiveStreamUrl = !!url?.trim() ?? false;
|
||||||
|
this.detectDeadState();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.streamService.canSeeLiveStreamObservable.subscribe(canSee => {
|
||||||
|
this.canSeeLiveStream = canSee;
|
||||||
|
this.detectDeadState();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rtcService.isJitsiEnabledObservable.subscribe(enabled => {
|
||||||
|
this.isJitsiEnabled = enabled;
|
||||||
|
this.detectDeadState();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rtcService.isJoinedObservable.subscribe(joined => {
|
||||||
|
this.isInCall = joined;
|
||||||
|
this.detectDeadState();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.rtcService.isJitsiActiveObservable.subscribe(isActive => {
|
||||||
|
this.isJitsiActive = isActive;
|
||||||
|
this.detectDeadState();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.callRestrictionService.hasToEnterCallObservable.subscribe(() => {
|
||||||
|
if (!this.isInCall) {
|
||||||
|
this.enterCall();
|
||||||
|
this.rtcService.enterConferenceRoom();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.callRestrictionService.hasToLeaveCallObservable.subscribe(() => {
|
||||||
|
this.viewStream();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.showLiveConfObservable.subscribe(showConf => {
|
||||||
|
this.showLiveConf = showConf;
|
||||||
|
this.detectDeadState();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.detectDeadState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enterCall(): Promise<void> {
|
||||||
|
if (this.conferenceState !== ConferenceState.jitsi) {
|
||||||
|
this.setConferenceState(ConferenceState.jitsi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public viewStream(): void {
|
||||||
|
if (this.conferenceState !== ConferenceState.stream) {
|
||||||
|
this.setConferenceState(ConferenceState.stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setConferenceState(newState: ConferenceState): void {
|
||||||
|
if (newState !== this.conferenceState) {
|
||||||
|
this.conferenceStateSubject.next(newState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this is the "dead" state; you would see the jitsi state; but are not connected
|
||||||
|
* or the connection is prohibited. If this occurs and a live stream
|
||||||
|
* becomes available, switch to the stream state
|
||||||
|
*/
|
||||||
|
private detectDeadState(): void {
|
||||||
|
if (
|
||||||
|
this.isInCall === undefined ||
|
||||||
|
this.isJitsiActive === undefined ||
|
||||||
|
this.hasLiveStreamUrl === undefined ||
|
||||||
|
this.conferenceState === undefined ||
|
||||||
|
this.canSeeLiveStream === undefined ||
|
||||||
|
this.isJitsiEnabled === undefined
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* most importantly, if there is a call, to not change the state!
|
||||||
|
*/
|
||||||
|
if (this.isInCall || this.isJitsiActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasLiveStreamUrl && this.canSeeLiveStream) {
|
||||||
|
this.viewStream();
|
||||||
|
} else if (this.showLiveConf && (!this.hasLiveStreamUrl || !this.canSeeLiveStream) && this.isJitsiEnabled) {
|
||||||
|
this.enterCall();
|
||||||
|
} else {
|
||||||
|
this.setConferenceState(ConferenceState.none);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
client/src/app/site/interaction/services/rtc.service.spec.ts
Normal file
18
client/src/app/site/interaction/services/rtc.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { RtcService } from './rtc.service';
|
||||||
|
|
||||||
|
describe('RtcService', () => {
|
||||||
|
let service: RtcService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({ imports: [E2EImportsModule] });
|
||||||
|
service = TestBed.inject(RtcService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
397
client/src/app/site/interaction/services/rtc.service.ts
Normal file
397
client/src/app/site/interaction/services/rtc.service.ts
Normal file
@ -0,0 +1,397 @@
|
|||||||
|
import { ElementRef, Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||||
|
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||||
|
import { distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { ConstantsService } from 'app/core/core-services/constants.service';
|
||||||
|
import { OperatorService } from 'app/core/core-services/operator.service';
|
||||||
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
|
import { CallRestrictionService } from './call-restriction.service';
|
||||||
|
import { UserMediaPermService } from './user-media-perm.service';
|
||||||
|
|
||||||
|
export const RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
|
||||||
|
|
||||||
|
interface JitsiMember {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConferenceJoinedResult {
|
||||||
|
roomName: string;
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
formattedDisplayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConferenceMember {
|
||||||
|
name: string;
|
||||||
|
focus: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DisplayNameChangeResult {
|
||||||
|
// Yes, in this case "displayname" really does not have a capital n. Thank you jitsi.
|
||||||
|
displayname: string;
|
||||||
|
formattedDisplayName: string;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MemberKicked {
|
||||||
|
kicked: {
|
||||||
|
id: string;
|
||||||
|
local: boolean;
|
||||||
|
};
|
||||||
|
kicker: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Jitsi
|
||||||
|
*/
|
||||||
|
declare var JitsiMeetExternalAPI: any;
|
||||||
|
|
||||||
|
const configOverwrite = {
|
||||||
|
startAudioOnly: false,
|
||||||
|
// allows jitsi on mobile devices
|
||||||
|
disableDeepLinking: true,
|
||||||
|
startWithAudioMuted: true,
|
||||||
|
startWithVideoMuted: true,
|
||||||
|
useNicks: true,
|
||||||
|
enableWelcomePage: false,
|
||||||
|
enableUserRolesBasedOnToken: false,
|
||||||
|
enableFeaturesBasedOnToken: false,
|
||||||
|
disableThirdPartyRequests: true,
|
||||||
|
enableNoAudioDetection: false,
|
||||||
|
enableNoisyMicDetection: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const interfaceConfigOverwrite = {
|
||||||
|
DISABLE_VIDEO_BACKGROUND: true,
|
||||||
|
INVITATION_POWERED_BY: false,
|
||||||
|
DISABLE_JOIN_LEAVE_NOTIFICATIONS: true,
|
||||||
|
DISABLE_PRESENCE_STATUS: true,
|
||||||
|
TOOLBAR_ALWAYS_VISIBLE: true,
|
||||||
|
TOOLBAR_TIMEOUT: 10000000,
|
||||||
|
TOOLBAR_BUTTONS: [
|
||||||
|
'microphone',
|
||||||
|
'camera',
|
||||||
|
'desktop',
|
||||||
|
'fullscreen',
|
||||||
|
'fodeviceselection',
|
||||||
|
'profile',
|
||||||
|
'chat',
|
||||||
|
'recording',
|
||||||
|
'livestreaming',
|
||||||
|
'etherpad',
|
||||||
|
'sharedvideo',
|
||||||
|
'settings',
|
||||||
|
'videoquality',
|
||||||
|
'filmstrip',
|
||||||
|
'feedback',
|
||||||
|
'stats',
|
||||||
|
'shortcuts',
|
||||||
|
'tileview',
|
||||||
|
'download',
|
||||||
|
'help',
|
||||||
|
'mute-everyone',
|
||||||
|
'hangup'
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface JitsiConfig {
|
||||||
|
JITSI_DOMAIN: string;
|
||||||
|
JITSI_ROOM_NAME: string;
|
||||||
|
JITSI_ROOM_PASSWORD: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class RtcService {
|
||||||
|
private jitsiConfig: JitsiConfig;
|
||||||
|
private isJitsiEnabledSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
public isJitsiEnabledObservable = this.isJitsiEnabledSubject.asObservable();
|
||||||
|
|
||||||
|
public autoConnect: Observable<boolean>;
|
||||||
|
|
||||||
|
// JitsiMeet api object
|
||||||
|
private api: any | null;
|
||||||
|
private get isJitsiActive(): boolean {
|
||||||
|
return !!this.api;
|
||||||
|
}
|
||||||
|
|
||||||
|
private options: Object;
|
||||||
|
private jitsiNode: ElementRef;
|
||||||
|
|
||||||
|
private actualRoomName: string;
|
||||||
|
|
||||||
|
public isSupportEnabled: Observable<boolean>;
|
||||||
|
private connectedToHelpDeskSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
public connectedToHelpDesk = this.connectedToHelpDeskSubject.asObservable();
|
||||||
|
|
||||||
|
public isJitsiActiveInAnotherTab = false;
|
||||||
|
private isJoinedSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
public isJoinedObservable = this.isJoinedSubject.asObservable();
|
||||||
|
|
||||||
|
private isPasswordSet = false;
|
||||||
|
|
||||||
|
private isJitsiActiveSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
public isJitsiActiveObservable = this.isJitsiActiveSubject.asObservable();
|
||||||
|
|
||||||
|
private get defaultRoomName(): string {
|
||||||
|
return this.jitsiConfig?.JITSI_ROOM_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
private jitsiMeetUrlSubject = new Subject<string>();
|
||||||
|
public jitsiMeetUrl = this.jitsiMeetUrlSubject.asObservable();
|
||||||
|
|
||||||
|
private members = {};
|
||||||
|
private memberSubject = new BehaviorSubject<Object>(this.members);
|
||||||
|
public memberObservableObservable = this.memberSubject.asObservable();
|
||||||
|
|
||||||
|
private dominantSpeaker: JitsiMember;
|
||||||
|
private dominantSpeakerSubject = new Subject<JitsiMember>();
|
||||||
|
public dominantSpeakerObservable = this.dominantSpeakerSubject.asObservable();
|
||||||
|
|
||||||
|
private isMutedSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
public isMuted = this.isMutedSubject.asObservable();
|
||||||
|
|
||||||
|
private canEnterCall: boolean;
|
||||||
|
public inOtherTab: Observable<boolean>;
|
||||||
|
|
||||||
|
private showCallDialogSubject = new BehaviorSubject<boolean>(false);
|
||||||
|
public showCallDialogObservable = this.showCallDialogSubject.asObservable();
|
||||||
|
public set showCallDialog(show: boolean) {
|
||||||
|
this.showCallDialogSubject.next(show);
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
constantsService: ConstantsService,
|
||||||
|
configService: ConfigService,
|
||||||
|
callRestrictionService: CallRestrictionService,
|
||||||
|
private userMediaPermService: UserMediaPermService,
|
||||||
|
private storageMap: StorageMap,
|
||||||
|
private operator: OperatorService
|
||||||
|
) {
|
||||||
|
this.isSupportEnabled = configService.get<boolean>('general_system_conference_enable_helpdesk');
|
||||||
|
this.autoConnect = configService.get<boolean>('general_system_conference_auto_connect');
|
||||||
|
|
||||||
|
constantsService.get<JitsiConfig>('Settings').subscribe(settings => {
|
||||||
|
this.jitsiConfig = settings;
|
||||||
|
this.isJitsiEnabledSubject.next(!!settings.JITSI_DOMAIN && !!settings.JITSI_ROOM_NAME);
|
||||||
|
});
|
||||||
|
configService.get<boolean>('general_system_conference_open_microphone').subscribe(open => {
|
||||||
|
configOverwrite.startWithAudioMuted = !open;
|
||||||
|
});
|
||||||
|
configService.get<boolean>('general_system_conference_open_video').subscribe(open => {
|
||||||
|
configOverwrite.startWithVideoMuted = !open;
|
||||||
|
});
|
||||||
|
callRestrictionService.canEnterCallObservable.subscribe(canEnter => {
|
||||||
|
this.canEnterCall = canEnter;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.inOtherTab = this.storageMap
|
||||||
|
.watch(RTC_LOGGED_STORAGE_KEY, { type: 'boolean' })
|
||||||
|
.pipe(distinctUntilChanged());
|
||||||
|
}
|
||||||
|
|
||||||
|
public setJitsiNode(jitsiNode: ElementRef): void {
|
||||||
|
this.jitsiNode = jitsiNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleMute(): void {
|
||||||
|
if (this.isJitsiActive) {
|
||||||
|
this.api.executeCommand('toggleAudio');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enterSupportRoom(): Promise<void> {
|
||||||
|
this.connectedToHelpDeskSubject.next(true);
|
||||||
|
this.actualRoomName = `${this.defaultRoomName}-SUPPORT`;
|
||||||
|
await this.enterConversation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async enterConferenceRoom(force?: boolean): Promise<void> {
|
||||||
|
if (!this.canEnterCall) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (force) {
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
this.connectedToHelpDeskSubject.next(false);
|
||||||
|
this.actualRoomName = this.defaultRoomName;
|
||||||
|
await this.enterConversation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setRoomPassword(): void {
|
||||||
|
if (this.jitsiConfig?.JITSI_ROOM_PASSWORD && !this.isPasswordSet) {
|
||||||
|
// You can only set the password after the server has recognized that you are
|
||||||
|
// the moderator. There is no event listener for that.
|
||||||
|
setTimeout(() => {
|
||||||
|
this.api.executeCommand('password', this.jitsiConfig?.JITSI_ROOM_PASSWORD);
|
||||||
|
this.isPasswordSet = true;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async enterConversation(): Promise<void> {
|
||||||
|
await this.operator.loaded;
|
||||||
|
await this.userMediaPermService.requestMediaAccess();
|
||||||
|
this.storageMap.set(RTC_LOGGED_STORAGE_KEY, true).subscribe(() => {});
|
||||||
|
this.setOptions();
|
||||||
|
if (this.api) {
|
||||||
|
this.api.dispose();
|
||||||
|
this.api = undefined;
|
||||||
|
}
|
||||||
|
this.api = new JitsiMeetExternalAPI(this.jitsiConfig?.JITSI_DOMAIN, this.options);
|
||||||
|
this.isJitsiActiveSubject.next(true);
|
||||||
|
const jitsiName = this.operator.viewUser.getShortName();
|
||||||
|
this.api.executeCommand('displayName', jitsiName);
|
||||||
|
this.loadApiCallbacks();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadApiCallbacks(): void {
|
||||||
|
this.isMutedSubject.next(configOverwrite.startWithAudioMuted);
|
||||||
|
|
||||||
|
this.api.on('videoConferenceJoined', (info: ConferenceJoinedResult) => {
|
||||||
|
this.onEnterConference(info);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('participantJoined', (newMember: JitsiMember) => {
|
||||||
|
this.addMember(newMember);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('participantLeft', (oldMember: { id: string }) => {
|
||||||
|
this.removeMember(oldMember);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('displayNameChange', (member: DisplayNameChangeResult) => {
|
||||||
|
this.renameMember(member);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('audioMuteStatusChanged', (isMuted: { muted: boolean }) => {
|
||||||
|
this.isMutedSubject.next(isMuted.muted);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('readyToClose', () => {
|
||||||
|
this.stopJitsi();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('dominantSpeakerChanged', (newSpeaker: { id: string }) => {
|
||||||
|
this.newDominantSpeaker(newSpeaker.id);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('passwordRequired', () => {
|
||||||
|
this.setRoomPassword();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.api.on('participantKickedOut', (kicked: MemberKicked) => {
|
||||||
|
this.onMemberKicked(kicked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEnterConference(info: ConferenceJoinedResult): void {
|
||||||
|
this.isJoinedSubject.next(true);
|
||||||
|
this.showCallDialogSubject.next(true);
|
||||||
|
this.addMember({ displayName: info.displayName, id: info.id });
|
||||||
|
this.setRoomPassword();
|
||||||
|
}
|
||||||
|
|
||||||
|
private addMember(newMember: JitsiMember): void {
|
||||||
|
this.members[newMember.id] = {
|
||||||
|
name: newMember.displayName,
|
||||||
|
focus: false
|
||||||
|
} as ConferenceMember;
|
||||||
|
this.updateMemberSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private onMemberKicked(kick: MemberKicked): void {
|
||||||
|
if (kick.kicked.local) {
|
||||||
|
this.stopJitsi();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeMember(oldMember: { id: string }): void {
|
||||||
|
if (this.members[oldMember.id]) {
|
||||||
|
delete this.members[oldMember.id];
|
||||||
|
this.updateMemberSubject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renameMember(member: DisplayNameChangeResult): void {
|
||||||
|
if (this.members[member.id]) {
|
||||||
|
this.members[member.id].name = member.displayname;
|
||||||
|
this.updateMemberSubject();
|
||||||
|
}
|
||||||
|
if (this.dominantSpeaker?.id === member.id) {
|
||||||
|
this.newDominantSpeaker(member.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private newDominantSpeaker(newSpeakerId: string): void {
|
||||||
|
if (this.dominantSpeaker && this.members[this.dominantSpeaker.id]) {
|
||||||
|
this.members[this.dominantSpeaker.id].focus = false;
|
||||||
|
}
|
||||||
|
this.members[newSpeakerId].focus = true;
|
||||||
|
this.updateMemberSubject();
|
||||||
|
this.dominantSpeaker = {
|
||||||
|
id: newSpeakerId,
|
||||||
|
displayName: this.members[newSpeakerId].name
|
||||||
|
};
|
||||||
|
this.dominantSpeakerSubject.next(this.dominantSpeaker);
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearMembers(): void {
|
||||||
|
this.members = {};
|
||||||
|
this.updateMemberSubject();
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateMemberSubject(): void {
|
||||||
|
this.memberSubject.next(this.members);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/jitsi/jitsi-meet/issues/8244
|
||||||
|
*/
|
||||||
|
public enterFullScreen(): void {}
|
||||||
|
|
||||||
|
private disconnect(): void {
|
||||||
|
if (this.isJitsiActive) {
|
||||||
|
this.api?.executeCommand('hangup');
|
||||||
|
this.api?.dispose();
|
||||||
|
this.api = undefined;
|
||||||
|
}
|
||||||
|
this.clearMembers();
|
||||||
|
this.deleteJitsiLock();
|
||||||
|
this.dominantSpeaker = null;
|
||||||
|
this.dominantSpeakerSubject.next(this.dominantSpeaker);
|
||||||
|
this.isJoinedSubject.next(false);
|
||||||
|
this.showCallDialogSubject.next(false);
|
||||||
|
this.isPasswordSet = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopJitsi(): void {
|
||||||
|
this.disconnect();
|
||||||
|
this.connectedToHelpDeskSubject.next(false);
|
||||||
|
this.isJitsiActiveSubject.next(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setOptions(): void {
|
||||||
|
this.setJitsiMeetUrl();
|
||||||
|
this.options = {
|
||||||
|
roomName: this.actualRoomName,
|
||||||
|
parentNode: this.jitsiNode.nativeElement,
|
||||||
|
configOverwrite: configOverwrite,
|
||||||
|
interfaceConfigOverwrite: interfaceConfigOverwrite
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private setJitsiMeetUrl(): void {
|
||||||
|
this.jitsiMeetUrlSubject.next(`https://${this.jitsiConfig.JITSI_DOMAIN}/${this.actualRoomName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteJitsiLock(): void {
|
||||||
|
this.storageMap.delete(RTC_LOGGED_STORAGE_KEY).subscribe();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,18 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
|
import { StreamService } from './stream.service';
|
||||||
|
|
||||||
|
describe('StreamService', () => {
|
||||||
|
let service: StreamService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({ imports: [E2EImportsModule] });
|
||||||
|
service = TestBed.inject(StreamService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
58
client/src/app/site/interaction/services/stream.service.ts
Normal file
58
client/src/app/site/interaction/services/stream.service.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
|
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
import { distinctUntilChanged } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { OperatorService, Permission } from 'app/core/core-services/operator.service';
|
||||||
|
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||||
|
|
||||||
|
const STREAM_RUNNING_STORAGE_KEY = 'streamIsRunning';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class StreamService {
|
||||||
|
public liveStreamUrlObservable: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* undefined is controlled behavior, meaning, this property was not
|
||||||
|
* checked yet.
|
||||||
|
* Thus, false-checks have to be explicit
|
||||||
|
*/
|
||||||
|
public streamLoadedOnceObservable: Observable<boolean>;
|
||||||
|
|
||||||
|
private isStreamRunningSubject = new Subject<boolean>();
|
||||||
|
public isStreamRunningObservable = this.isStreamRunningSubject.asObservable();
|
||||||
|
|
||||||
|
private canSeeLiveStreamSubject = new Subject<boolean>();
|
||||||
|
public canSeeLiveStreamObservable = this.canSeeLiveStreamSubject.asObservable();
|
||||||
|
|
||||||
|
public constructor(private storageMap: StorageMap, operator: OperatorService, configService: ConfigService) {
|
||||||
|
this.liveStreamUrlObservable = configService.get<string>('general_system_stream_url');
|
||||||
|
|
||||||
|
this.streamLoadedOnceObservable = this.storageMap
|
||||||
|
.watch(STREAM_RUNNING_STORAGE_KEY, { type: 'boolean' })
|
||||||
|
.pipe(distinctUntilChanged());
|
||||||
|
|
||||||
|
operator.getUserObservable().subscribe(() => {
|
||||||
|
this.canSeeLiveStreamSubject.next(operator.hasPerms(Permission.coreCanSeeLiveStream));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setStreamingLock(): void {
|
||||||
|
this.storageMap.set(STREAM_RUNNING_STORAGE_KEY, true).subscribe(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setStreamRunning(running: boolean): void {
|
||||||
|
this.isStreamRunningSubject.next(running);
|
||||||
|
}
|
||||||
|
|
||||||
|
public deleteStreamingLock(): void {
|
||||||
|
/**
|
||||||
|
* subscriptions are faster than promises. This will fire more reliable
|
||||||
|
* than converting it to promise first
|
||||||
|
*/
|
||||||
|
this.storageMap.delete(STREAM_RUNNING_STORAGE_KEY).subscribe();
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ import { Injectable } from '@angular/core';
|
|||||||
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { OverlayService } from './overlay.service';
|
import { OverlayService } from '../../../core/ui-services/overlay.service';
|
||||||
|
|
||||||
const givePermsMessage = _('Please allow OpenSlides to access your microphone and/or camera');
|
const givePermsMessage = _('Please allow OpenSlides to access your microphone and/or camera');
|
||||||
const accessDeniedMessage = _('Media access is denied');
|
const accessDeniedMessage = _('Media access is denied');
|
@ -114,7 +114,8 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbars">
|
<div class="toolbars">
|
||||||
<os-jitsi></os-jitsi>
|
<os-action-bar></os-action-bar>
|
||||||
|
<os-interaction-container></os-interaction-container>
|
||||||
</div>
|
</div>
|
||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
</mat-sidenav-container>
|
</mat-sidenav-container>
|
||||||
|
@ -136,9 +136,16 @@ mat-sidenav-container {
|
|||||||
|
|
||||||
.toolbars {
|
.toolbars {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
/**
|
||||||
|
* right 0 would overlap the browser scrollbar
|
||||||
|
*/
|
||||||
|
right: 20px;
|
||||||
|
z-index: 99;
|
||||||
|
pointer-events: none;
|
||||||
* {
|
* {
|
||||||
margin-top: auto;
|
pointer-events: initial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,11 +2,12 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
|
||||||
import { SharedModule } from 'app/shared/shared.module';
|
import { SharedModule } from 'app/shared/shared.module';
|
||||||
|
import { InteractionModule } from './interaction/interaction.module';
|
||||||
import { SiteRoutingModule } from './site-routing.module';
|
import { SiteRoutingModule } from './site-routing.module';
|
||||||
import { SiteComponent } from './site.component';
|
import { SiteComponent } from './site.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [CommonModule, SharedModule, SiteRoutingModule],
|
imports: [CommonModule, SharedModule, SiteRoutingModule, InteractionModule],
|
||||||
declarations: [SiteComponent]
|
declarations: [SiteComponent]
|
||||||
})
|
})
|
||||||
export class SiteModule {}
|
export class SiteModule {}
|
||||||
|
@ -42,7 +42,6 @@ $narrow-spacing: (
|
|||||||
@import '~app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
|
@import '~app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
|
||||||
@import '~app/site/assignments/modules/assignment-poll/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
|
@import '~app/site/assignments/modules/assignment-poll/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
|
||||||
@import '~app/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss';
|
@import '~app/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss';
|
||||||
@import '~app/shared/components/jitsi/jitsi.component.scss-theme.scss';
|
|
||||||
@import '~app/shared/components/list-view-table/list-view-table.component.scss-theme.scss';
|
@import '~app/shared/components/list-view-table/list-view-table.component.scss-theme.scss';
|
||||||
@import '~app/site/common/components/user-statistics/user-statistics.component.scss-theme.scss';
|
@import '~app/site/common/components/user-statistics/user-statistics.component.scss-theme.scss';
|
||||||
@import '~app/site/login/components/login-wrapper/login-wrapper.component.scss-theme.scss';
|
@import '~app/site/login/components/login-wrapper/login-wrapper.component.scss-theme.scss';
|
||||||
@ -69,7 +68,6 @@ $narrow-spacing: (
|
|||||||
@include os-motion-poll-detail-style($theme);
|
@include os-motion-poll-detail-style($theme);
|
||||||
@include os-assignment-poll-detail-style($theme);
|
@include os-assignment-poll-detail-style($theme);
|
||||||
@include os-progress-snack-bar-style($theme);
|
@include os-progress-snack-bar-style($theme);
|
||||||
@include os-jitsi-theme($theme);
|
|
||||||
@include os-list-view-table-theme($theme);
|
@include os-list-view-table-theme($theme);
|
||||||
@include os-user-statistics-style($theme);
|
@include os-user-statistics-style($theme);
|
||||||
@include os-login-wrapper-theme($theme);
|
@include os-login-wrapper-theme($theme);
|
||||||
|
@ -35,10 +35,32 @@
|
|||||||
color: mat-color(if($is-dark-theme, $accent, $primary));
|
color: mat-color(if($is-dark-theme, $accent, $primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
//custom table header for search button, filtering and more. Used in ListViews
|
/**
|
||||||
|
* normal current mat bg color with primary text color.
|
||||||
|
* important is required to overwrite materials default
|
||||||
|
* button color
|
||||||
|
*/
|
||||||
.custom-table-header,
|
.custom-table-header,
|
||||||
.background--default {
|
.background-default {
|
||||||
background: mat-color($background, background);
|
background: mat-color($background, background) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fake-disabled {
|
||||||
|
background: mat-color($background, unselected-chip) !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
|
||||||
|
.mat-button-wrapper {
|
||||||
|
.mat-icon {
|
||||||
|
color: mat-color($foreground, disabled-button) !important;
|
||||||
|
svg path {
|
||||||
|
fill: mat-color($foreground, disabled-button) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.background-default[disabled] {
|
||||||
|
@extend .fake-disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
.underline {
|
.underline {
|
||||||
@ -192,6 +214,11 @@
|
|||||||
color: mat-color($primary, default-contrast) !important;
|
color: mat-color($primary, default-contrast) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.background-card {
|
||||||
|
background: mat-color($background, card);
|
||||||
|
color: mat-color($foreground, text);
|
||||||
|
}
|
||||||
|
|
||||||
.primary-foreground {
|
.primary-foreground {
|
||||||
color: mat-color($primary);
|
color: mat-color($primary);
|
||||||
}
|
}
|
||||||
@ -199,4 +226,10 @@
|
|||||||
.accent-foreground {
|
.accent-foreground {
|
||||||
color: mat-color($accent);
|
color: mat-color($accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.svg-primary {
|
||||||
|
svg path {
|
||||||
|
fill: mat-color($primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -164,7 +164,7 @@ def get_config_variables():
|
|||||||
name="general_system_stream_poster",
|
name="general_system_stream_poster",
|
||||||
default_value="",
|
default_value="",
|
||||||
label="Livestream poster image url",
|
label="Livestream poster image url",
|
||||||
help_text="Shows if livestream is not started. Recommended image format: 500x200px, PNG or JPG",
|
help_text="Shows if livestream is not started. Recommended image format: 500x280px, PNG or JPG",
|
||||||
weight=147,
|
weight=147,
|
||||||
subgroup="Live conference",
|
subgroup="Live conference",
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user