Merge pull request #5770 from tsiegleauq/youtube-urls

Support youtube as livestream URL
This commit is contained in:
Emanuel Schütze 2021-02-05 14:29:51 +01:00 committed by GitHub
commit 09bc7f093a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 328 additions and 352 deletions

View File

@ -227,13 +227,14 @@
</ng-container>
<ng-container *ngIf="currentState == state.stream">
<os-vjs-player
<ng-container *ngIf="showVideoPlayer()">
<os-video-player
[videoUrl]="videoStreamUrl"
[showParticles]="isApplauseTypeParticles"
(started)="onSteamStarted()"
*ngIf="(canSeeLiveStream && !streamActiveInAnotherTab) || streamRunning"
></os-vjs-player>
<div class="disconnected" *ngIf="streamActiveInAnotherTab && !streamRunning">
(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>

View File

@ -1,5 +1,15 @@
import { animate, state, style, transition, trigger } from '@angular/animations';
import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
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';
@ -76,7 +86,8 @@ enum ConferenceState {
transition('true <=> false', animate('1s'))
])
],
encapsulation: ViewEncapsulation.None
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class JitsiComponent extends BaseViewComponentDirective implements OnInit, OnDestroy {
public enableJitsi: boolean;
@ -116,7 +127,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
}
public isJoined: boolean;
public streamRunning: boolean;
private streamRunning = false;
private options: object;
@ -126,7 +137,13 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
// storage locks
public isJitsiActiveInAnotherTab = false;
public streamActiveInAnotherTab = 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';
@ -271,7 +288,8 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
private configService: ConfigService,
private closService: CurrentListOfSpeakersService,
private userMediaPermService: UserMediaPermService,
private applauseService: ApplauseService
private applauseService: ApplauseService,
private cd: ChangeDetectorRef
) {
super(titleService, translate, snackBar);
}
@ -307,7 +325,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
private async stopConference(): Promise<void> {
await this.stopJitsi();
if (this.streamActiveInAnotherTab && this.streamRunning) {
if (this.streamLoadedOnce && this.streamRunning) {
await this.deleteStreamingLock();
}
}
@ -322,6 +340,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
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
@ -332,13 +351,15 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
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.streamActiveInAnotherTab = running;
this.streamLoadedOnce = !!running;
this.cd.markForCheck();
})
);
@ -393,6 +414,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
}),
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') {
@ -415,9 +437,13 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
map(los => los?.findUserIndexOnList(this.operator.user.id) ?? -1),
distinctUntilChanged()
)
.subscribe(userLosIndex => this.autoJoinJitsiByLosIndex(userLosIndex)),
.subscribe(userLosIndex => {
this.autoJoinJitsiByLosIndex(userLosIndex);
this.cd.markForCheck();
}),
this.applauseService.applauseLevelObservable.subscribe(applauseLevel => {
this.applauseLevel = applauseLevel || 0;
this.cd.markForCheck();
})
);
}
@ -437,6 +463,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
private startJitsi(): void {
if (!this.isJitsiActiveInAnotherTab && this.enableJitsi && !this.isJitsiActive && this.jitsiNode) {
this.enterConferenceRoom();
this.cd.markForCheck();
}
}
@ -501,6 +528,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
if (this.videoStreamUrl) {
this.showJitsiDialog();
}
this.cd.markForCheck();
}
private autoJoinJitsiByLosIndex(operatorClosIndex: number): void {
@ -548,6 +576,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
id: newSpeakerId,
displayName: this.members[newSpeakerId].name
};
this.cd.markForCheck();
}
private addMember(newMember: JitsiMember): void {
@ -614,22 +643,42 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
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 onSteamStarted(): void {
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;
this.storageMap.set(this.STREAM_RUNNING_STORAGE_KEY, true).subscribe(() => {});
});
}
}
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 {
@ -654,6 +703,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
} else if (!this.videoStreamUrl && this.enableJitsi) {
this.setConferenceState(ConferenceState.jitsi);
}
this.cd.markForCheck();
}
private async deleteJitsiLock(): Promise<void> {
@ -673,6 +723,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
this.applauseService.sendApplause();
setTimeout(() => {
this.applauseDisabled = false;
this.cd.markForCheck();
}, this.applauseTimeout);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,35 @@
<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 }">
<div *ngIf="usingVjs">
<video #vjs class="video-js" controls preload="none"></video>
</div>
<div *ngIf="usingYouTube" class="youtube-player">
<iframe
class="youtube-iFrame"
type="text/html"
frameborder="0"
allow="autoplay; encrypted-media"
allowfullscreen
[src]="youTubeVideoUrl | trust: 'resourceUrl'"
></iframe>
</div>
</div>
<div *ngIf="usingVjs && !isUrlOnline" class="is-offline-wrapper">
<ng-container *ngIf="!posterUrl">
<p>
{{ 'Currently no livestream available.' | translate }}
</p>
<button mat-raised-button (click)="onRefreshVideo()" color="primary">
<mat-icon>refresh</mat-icon>&nbsp;
<span>{{ 'Refresh' | translate }}</span>
</button>
</ng-container>
<div *ngIf="posterUrl" class="offlineposter">
<button mat-mini-fab (click)="onRefreshVideo()" color="accent" matTooltip="{{ 'Refresh' | translate }}">
<mat-icon>refresh</mat-icon>
</button>
<img [src]="posterUrl" />
</div>
</div>
</div>

View File

@ -16,11 +16,12 @@
.is-offline-wrapper {
width: 100%;
text-align: center;
width: 500px;
height: 200px;
.offlineposter {
position: relative;
width: 500px;
height: 200px;
img {
max-width: 100%;
max-height: 100%;
@ -40,25 +41,11 @@
}
.player-container {
height: 100%;
width: 100%;
.video-js {
margin: auto;
// we keep the button for now
// .vjs-big-play-button {
// left: 0;
// top: 0;
// width: 100%;
// height: 100%;
// border: 0;
// border-radius: 0;
// background-color: rgba(0, 0, 0, 0);
// .vjs-icon-placeholder {
// display: none !important;
// }
// }
.vjs-control-bar {
.vjs-subs-caps-button {
display: none !important;
@ -73,5 +60,16 @@
}
}
}
.youtube-player {
height: 100%;
width: 100%;
display: flex;
.youtube-iFrame {
width: 100%;
height: 280px;
}
}
}
}

View File

@ -2,11 +2,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { VjsPlayerComponent } from './vjs-player.component';
import { VideoPlayerComponent } from './video-player.component';
describe('VjsPlayerComponent', () => {
let component: VjsPlayerComponent;
let fixture: ComponentFixture<VjsPlayerComponent>;
let component: VideoPlayerComponent;
let fixture: ComponentFixture<VideoPlayerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
@ -15,7 +15,7 @@ describe('VjsPlayerComponent', () => {
}));
beforeEach(() => {
fixture = TestBed.createComponent(VjsPlayerComponent);
fixture = TestBed.createComponent(VideoPlayerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -0,0 +1,201 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { of } from 'rxjs';
import { ajax, AjaxResponse } from 'rxjs/ajax';
import { catchError, map } from 'rxjs/operators';
import videojs from 'video.js';
import { ConfigService } from 'app/core/ui-services/config.service';
enum MimeType {
mp4 = 'video/mp4',
mpd = 'application/dash+xml',
m3u8 = 'application/x-mpegURL'
}
enum Player {
vjs,
youtube
}
@Component({
selector: 'os-video-player',
templateUrl: './video-player.component.html',
styleUrls: ['./video-player.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class VideoPlayerComponent implements OnDestroy, AfterViewInit {
@ViewChild('vjs', { static: false })
private vjsPlayerElementRef: ElementRef;
private _videoUrl: string;
@Input()
public set videoUrl(value: string) {
this._videoUrl = value.trim();
this.playerType = this.determinePlayer(this.videoUrl);
if (this.usingVjs) {
this.mimeType = this.determineContentTypeByUrl(this.videoUrl);
this.initVjs();
} else if (this.usingYouTube) {
this.stopVJS();
this.unloadVjs();
this.youTubeVideoId = this.getYouTubeVideoId(this.videoUrl);
}
}
@Input()
public showParticles: boolean;
public get videoUrl(): string {
return this._videoUrl;
}
public posterUrl: string;
public vjsPlayer: videojs.Player;
public youTubeVideoId: string;
public isUrlOnline: boolean;
private playerType: Player;
private mimeType: MimeType;
@Output()
private started: EventEmitter<void> = new EventEmitter();
public get usingYouTube(): boolean {
return this.playerType === Player.youtube;
}
public get usingVjs(): boolean {
return this.playerType === Player.vjs;
}
public get youTubeVideoUrl(): string {
return `https://www.youtube.com/embed/${this.youTubeVideoId}?autoplay=1`;
}
public constructor(config: ConfigService) {
config.get<string>('general_system_stream_poster').subscribe(posterUrl => {
this.posterUrl = posterUrl?.trim();
});
}
public ngAfterViewInit(): void {
this.started.next();
}
public ngOnDestroy(): void {
this.unloadVjs();
}
private stopVJS(): void {
if (this.vjsPlayer) {
this.vjsPlayer.src = '';
this.vjsPlayer.pause();
}
}
private unloadVjs(): void {
if (this.vjsPlayer) {
this.vjsPlayer.dispose();
this.vjsPlayer = null;
}
}
private async isUrlReachable(): Promise<void> {
/**
* Using observable would not make sense, because without it would not automatically update
* if a Ressource switches from online to offline
*/
const ajaxResponse: AjaxResponse = await ajax(this.videoUrl)
.pipe(
map(response => response),
catchError(error => {
return of(error);
})
)
.toPromise();
/**
* there is no enum for http status codes in the whole Angular stack...
*/
if (ajaxResponse.status === 200) {
this.isUrlOnline = true;
} else {
this.isUrlOnline = false;
}
}
public async onRefreshVideo(): Promise<void> {
await this.isUrlReachable();
this.playVjsVideo();
}
private async initVjs(): Promise<void> {
await this.isUrlReachable();
if (!this.vjsPlayer && this.usingVjs && this.vjsPlayerElementRef) {
this.vjsPlayer = videojs(this.vjsPlayerElementRef.nativeElement, {
textTrackSettings: false,
fluid: true,
autoplay: 'any',
liveui: true,
poster: this.posterUrl
});
}
this.playVjsVideo();
}
private playVjsVideo(): void {
if (this.usingVjs && this.vjsPlayer && this.isUrlOnline) {
this.vjsPlayer.src({
src: this.videoUrl,
type: this.mimeType
});
}
}
private determinePlayer(videoUrl: string): Player {
if (videoUrl.includes('youtu.be') || videoUrl.includes('youtube.')) {
return Player.youtube;
} else {
return Player.vjs;
}
}
private getYouTubeVideoId(url: string): string {
const regExp = /^.*(youtu\.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
const match = url.match(regExp);
if (match && match[2].length === 11) {
return match[2];
}
}
private determineContentTypeByUrl(url: string): MimeType {
if (url) {
if (url.startsWith('rtmp')) {
throw new Error(`$rtmp (flash) streams cannot be supported`);
} else {
const extension = url?.split('.')?.pop();
const mimeType = MimeType[extension];
if (mimeType) {
return mimeType;
} else {
throw new Error(`${url} has an unknown mime type`);
}
}
}
}
}

View File

@ -1,23 +0,0 @@
<div class="video-wrapper">
<os-applause-particle-display *ngIf="showParticles" class="applause-particles"></os-applause-particle-display>
<div class="player-container" [ngClass]="{ hide: !isUrlOnline }">
<video #videoPlayer class="video-js" controls preload="none"></video>
</div>
<div *ngIf="!isUrlOnline" class="is-offline-wrapper">
<ng-container *ngIf="!posterUrl">
<p>
{{ 'Currently no livestream available.' | translate }}
</p>
<button mat-raised-button (click)="checkVideoUrl()" color="primary">
<mat-icon>refresh</mat-icon>&nbsp;
<span>{{ 'Refresh' | translate }}</span>
</button>
</ng-container>
<div *ngIf="posterUrl" class="offlineposter">
<button mat-mini-fab (click)="checkVideoUrl()" color="accent" matTooltip="{{ 'Refresh' | translate }}">
<mat-icon>refresh</mat-icon>
</button>
<img [src]="posterUrl" />
</div>
</div>
</div>

View File

@ -1,145 +0,0 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core';
import { of } from 'rxjs';
import { ajax, AjaxResponse } from 'rxjs/ajax';
import { catchError, map } from 'rxjs/operators';
import videojs from 'video.js';
import { ConfigService } from 'app/core/ui-services/config.service';
interface VideoSource {
src: string;
type: MimeType;
}
enum MimeType {
mp4 = 'video/mp4',
mpd = 'application/dash+xml',
m3u8 = 'application/x-mpegURL'
}
@Component({
selector: 'os-vjs-player',
templateUrl: './vjs-player.component.html',
styleUrls: ['./vjs-player.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class VjsPlayerComponent implements OnInit, OnDestroy {
@ViewChild('videoPlayer', { static: true })
private videoPlayer: ElementRef;
private _videoUrl: string;
public posterUrl: string;
public player: videojs.Player;
public isUrlOnline: boolean;
@Input()
public set videoUrl(value: string) {
this._videoUrl = value.trim();
this.checkVideoUrl();
}
@Input()
public showParticles: boolean;
@Output()
private started: EventEmitter<void> = new EventEmitter();
public get videoUrl(): string {
return this._videoUrl;
}
private get videoSource(): VideoSource {
return {
src: this.videoUrl,
type: this.determineContentTypeByUrl(this.videoUrl)
};
}
public constructor(config: ConfigService) {
config.get<string>('general_system_stream_poster').subscribe(posterUrl => {
this.posterUrl = posterUrl?.trim();
});
}
public async ngOnInit(): Promise<void> {
this.initPlayer();
}
public ngOnDestroy(): void {
if (this.player) {
this.player.dispose();
}
}
public async checkVideoUrl(): Promise<void> {
/**
* Using observable would not make sense, because without it would not automatically update
* if a Ressource switches from online to offline
*/
const ajaxResponse: AjaxResponse = await ajax(this.videoUrl)
.pipe(
map(response => response),
catchError(error => {
return of(error);
})
)
.toPromise();
/**
* there is no enum for http status codes in the whole Angular stack...
*/
if (ajaxResponse.status === 200) {
this.isUrlOnline = true;
this.playVideo();
} else {
this.isUrlOnline = false;
if (this.player) {
this.player.pause();
}
this.player.src('');
}
}
private initPlayer(): void {
if (!this.player) {
this.player = videojs(this.videoPlayer.nativeElement, {
textTrackSettings: false,
fluid: true,
autoplay: 'any',
liveui: true,
poster: this.posterUrl
});
}
}
private playVideo(): void {
this.player.src(this.videoSource);
this.started.next();
}
private determineContentTypeByUrl(url: string): MimeType {
if (url) {
if (url.startsWith('rtmp')) {
throw new Error(`$rtmp (flash) streams cannot be supported`);
} else {
const extension = url?.split('.')?.pop();
const mimeType = MimeType[extension];
if (mimeType) {
return mimeType;
} else {
throw new Error(`${url} has an unknown mime type`);
}
}
}
}
}

View File

@ -127,8 +127,7 @@ import { AssignmentPollDetailContentComponent } from './components/assignment-po
import { GlobalSpinnerComponent } from './components/global-spinner/global-spinner.component';
import { UserMenuComponent } from './components/user-menu/user-menu.component';
import { JitsiComponent } from './components/jitsi/jitsi.component';
import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component';
import { LiveStreamComponent } from './components/live-stream/live-stream.component';
import { VideoPlayerComponent } from './components/video-player/video-player.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';
@ -303,8 +302,7 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
MotionPollDetailContentComponent,
AssignmentPollDetailContentComponent,
JitsiComponent,
VjsPlayerComponent,
LiveStreamComponent,
VideoPlayerComponent,
ListOfSpeakersContentComponent
],
declarations: [
@ -367,8 +365,7 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
MotionPollDetailContentComponent,
AssignmentPollDetailContentComponent,
JitsiComponent,
VjsPlayerComponent,
LiveStreamComponent,
VideoPlayerComponent,
ListOfSpeakersContentComponent,
ApplauseBarDisplayComponent,
ProgressComponent,