Support youtube as livestream URL

Allows to use youtube as as live stream url config,
will load the youtube embedded player in an iframe instead
of video.js
This commit is contained in:
Sean 2020-12-11 16:22:37 +01:00
parent f57fe05e26
commit 372f1eaa7e
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
[videoUrl]="videoStreamUrl"
[showParticles]="isApplauseTypeParticles"
(started)="onSteamStarted()"
*ngIf="(canSeeLiveStream && !streamActiveInAnotherTab) || streamRunning"
></os-vjs-player>
<div class="disconnected" *ngIf="streamActiveInAnotherTab && !streamRunning">
<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>

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 {
this.streamRunning = true;
this.storageMap.set(this.STREAM_RUNNING_STORAGE_KEY, true).subscribe(() => {});
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 {
@ -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,