Merge pull request #5770 from tsiegleauq/youtube-urls
Support youtube as livestream URL
This commit is contained in:
commit
09bc7f093a
@ -227,13 +227,14 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<ng-container *ngIf="currentState == state.stream">
|
<ng-container *ngIf="currentState == state.stream">
|
||||||
<os-vjs-player
|
<ng-container *ngIf="showVideoPlayer()">
|
||||||
[videoUrl]="videoStreamUrl"
|
<os-video-player
|
||||||
[showParticles]="isApplauseTypeParticles"
|
[videoUrl]="videoStreamUrl"
|
||||||
(started)="onSteamStarted()"
|
[showParticles]="isApplauseTypeParticles"
|
||||||
*ngIf="(canSeeLiveStream && !streamActiveInAnotherTab) || streamRunning"
|
(started)="onSteamLoaded()"
|
||||||
></os-vjs-player>
|
></os-video-player>
|
||||||
<div class="disconnected" *ngIf="streamActiveInAnotherTab && !streamRunning">
|
</ng-container>
|
||||||
|
<div class="disconnected" *ngIf="isStreamInOtherTab()">
|
||||||
<button class="restart-stream-button" mat-button color="warn" (click)="deleteStreamingLock()">
|
<button class="restart-stream-button" mat-button color="warn" (click)="deleteStreamingLock()">
|
||||||
<span>{{ 'Restart livestream' | translate }}</span>
|
<span>{{ 'Restart livestream' | translate }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,5 +1,15 @@
|
|||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
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 { MatSnackBar } from '@angular/material/snack-bar';
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
|
|
||||||
@ -76,7 +86,8 @@ enum ConferenceState {
|
|||||||
transition('true <=> false', animate('1s'))
|
transition('true <=> false', animate('1s'))
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
encapsulation: ViewEncapsulation.None
|
encapsulation: ViewEncapsulation.None,
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class JitsiComponent extends BaseViewComponentDirective implements OnInit, OnDestroy {
|
export class JitsiComponent extends BaseViewComponentDirective implements OnInit, OnDestroy {
|
||||||
public enableJitsi: boolean;
|
public enableJitsi: boolean;
|
||||||
@ -116,7 +127,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
}
|
}
|
||||||
|
|
||||||
public isJoined: boolean;
|
public isJoined: boolean;
|
||||||
public streamRunning: boolean;
|
private streamRunning = false;
|
||||||
|
|
||||||
private options: object;
|
private options: object;
|
||||||
|
|
||||||
@ -126,7 +137,13 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
|
|
||||||
// storage locks
|
// storage locks
|
||||||
public isJitsiActiveInAnotherTab = false;
|
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 RTC_LOGGED_STORAGE_KEY = 'rtcIsLoggedIn';
|
||||||
private STREAM_RUNNING_STORAGE_KEY = 'streamIsRunning';
|
private STREAM_RUNNING_STORAGE_KEY = 'streamIsRunning';
|
||||||
@ -271,7 +288,8 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
private configService: ConfigService,
|
private configService: ConfigService,
|
||||||
private closService: CurrentListOfSpeakersService,
|
private closService: CurrentListOfSpeakersService,
|
||||||
private userMediaPermService: UserMediaPermService,
|
private userMediaPermService: UserMediaPermService,
|
||||||
private applauseService: ApplauseService
|
private applauseService: ApplauseService,
|
||||||
|
private cd: ChangeDetectorRef
|
||||||
) {
|
) {
|
||||||
super(titleService, translate, snackBar);
|
super(titleService, translate, snackBar);
|
||||||
}
|
}
|
||||||
@ -307,7 +325,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
|
|
||||||
private async stopConference(): Promise<void> {
|
private async stopConference(): Promise<void> {
|
||||||
await this.stopJitsi();
|
await this.stopJitsi();
|
||||||
if (this.streamActiveInAnotherTab && this.streamRunning) {
|
if (this.streamLoadedOnce && this.streamRunning) {
|
||||||
await this.deleteStreamingLock();
|
await this.deleteStreamingLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -322,6 +340,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
this.canManageSpeaker = this.operator.hasPerms(this.permission.agendaCanManageListOfSpeakers);
|
this.canManageSpeaker = this.operator.hasPerms(this.permission.agendaCanManageListOfSpeakers);
|
||||||
this.canSeeLiveStream = this.operator.hasPerms(this.permission.coreCanSeeLiveStream);
|
this.canSeeLiveStream = this.operator.hasPerms(this.permission.coreCanSeeLiveStream);
|
||||||
this.isEnterMeetingRoomVisible = this.canManageSpeaker;
|
this.isEnterMeetingRoomVisible = this.canManageSpeaker;
|
||||||
|
this.cd.markForCheck();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
this.storageMap
|
this.storageMap
|
||||||
@ -332,13 +351,15 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
this.lockLoaded.resolve();
|
this.lockLoaded.resolve();
|
||||||
if (!inUse && !this.isJitsiActive) {
|
if (!inUse && !this.isJitsiActive) {
|
||||||
this.startJitsi();
|
this.startJitsi();
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
this.storageMap
|
this.storageMap
|
||||||
.watch(this.STREAM_RUNNING_STORAGE_KEY)
|
.watch(this.STREAM_RUNNING_STORAGE_KEY)
|
||||||
.pipe(distinctUntilChanged())
|
.pipe(distinctUntilChanged())
|
||||||
.subscribe((running: boolean) => {
|
.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.configService.get<boolean>('general_system_applause_show_level').subscribe(show => {
|
||||||
this.showApplauseLevel = show;
|
this.showApplauseLevel = show;
|
||||||
|
this.cd.markForCheck();
|
||||||
}),
|
}),
|
||||||
this.configService.get<any>('general_system_applause_type').subscribe(type => {
|
this.configService.get<any>('general_system_applause_type').subscribe(type => {
|
||||||
if (type === 'applause-type-bar') {
|
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),
|
map(los => los?.findUserIndexOnList(this.operator.user.id) ?? -1),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
)
|
)
|
||||||
.subscribe(userLosIndex => this.autoJoinJitsiByLosIndex(userLosIndex)),
|
.subscribe(userLosIndex => {
|
||||||
|
this.autoJoinJitsiByLosIndex(userLosIndex);
|
||||||
|
this.cd.markForCheck();
|
||||||
|
}),
|
||||||
this.applauseService.applauseLevelObservable.subscribe(applauseLevel => {
|
this.applauseService.applauseLevelObservable.subscribe(applauseLevel => {
|
||||||
this.applauseLevel = applauseLevel || 0;
|
this.applauseLevel = applauseLevel || 0;
|
||||||
|
this.cd.markForCheck();
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -437,6 +463,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
private startJitsi(): void {
|
private startJitsi(): void {
|
||||||
if (!this.isJitsiActiveInAnotherTab && this.enableJitsi && !this.isJitsiActive && this.jitsiNode) {
|
if (!this.isJitsiActiveInAnotherTab && this.enableJitsi && !this.isJitsiActive && this.jitsiNode) {
|
||||||
this.enterConferenceRoom();
|
this.enterConferenceRoom();
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,6 +528,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
if (this.videoStreamUrl) {
|
if (this.videoStreamUrl) {
|
||||||
this.showJitsiDialog();
|
this.showJitsiDialog();
|
||||||
}
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
private autoJoinJitsiByLosIndex(operatorClosIndex: number): void {
|
private autoJoinJitsiByLosIndex(operatorClosIndex: number): void {
|
||||||
@ -548,6 +576,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
id: newSpeakerId,
|
id: newSpeakerId,
|
||||||
displayName: this.members[newSpeakerId].name
|
displayName: this.members[newSpeakerId].name
|
||||||
};
|
};
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
private addMember(newMember: JitsiMember): void {
|
private addMember(newMember: JitsiMember): void {
|
||||||
@ -614,22 +643,42 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
|
|
||||||
public hideJitsiDialog(): void {
|
public hideJitsiDialog(): void {
|
||||||
this.isJitsiDialogOpen = false;
|
this.isJitsiDialogOpen = false;
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
public showJitsiDialog(): void {
|
public showJitsiDialog(): void {
|
||||||
this.isJitsiDialogOpen = true;
|
this.isJitsiDialogOpen = true;
|
||||||
this.showJitsiWindow = false;
|
this.showJitsiWindow = false;
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
public viewStream(): void {
|
public viewStream(): void {
|
||||||
this.stopJitsi();
|
this.stopJitsi();
|
||||||
this.setConferenceState(ConferenceState.stream);
|
this.setConferenceState(ConferenceState.stream);
|
||||||
this.showJitsiWindow = true;
|
this.showJitsiWindow = true;
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSteamStarted(): void {
|
public onSteamLoaded(): void {
|
||||||
this.streamRunning = true;
|
/**
|
||||||
this.storageMap.set(this.STREAM_RUNNING_STORAGE_KEY, true).subscribe(() => {});
|
* 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 {
|
public enterConferenceRoom(): void {
|
||||||
@ -654,6 +703,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
} else if (!this.videoStreamUrl && this.enableJitsi) {
|
} else if (!this.videoStreamUrl && this.enableJitsi) {
|
||||||
this.setConferenceState(ConferenceState.jitsi);
|
this.setConferenceState(ConferenceState.jitsi);
|
||||||
}
|
}
|
||||||
|
this.cd.markForCheck();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async deleteJitsiLock(): Promise<void> {
|
private async deleteJitsiLock(): Promise<void> {
|
||||||
@ -673,6 +723,7 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
|
|||||||
this.applauseService.sendApplause();
|
this.applauseService.sendApplause();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.applauseDisabled = false;
|
this.applauseDisabled = false;
|
||||||
|
this.cd.markForCheck();
|
||||||
}, this.applauseTimeout);
|
}, this.applauseTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
||||||
|
<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>
|
@ -16,11 +16,12 @@
|
|||||||
.is-offline-wrapper {
|
.is-offline-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
width: 500px;
|
||||||
|
height: 200px;
|
||||||
|
|
||||||
.offlineposter {
|
.offlineposter {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 500px;
|
|
||||||
height: 200px;
|
|
||||||
img {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
@ -40,25 +41,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.player-container {
|
.player-container {
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.video-js {
|
.video-js {
|
||||||
margin: auto;
|
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-control-bar {
|
||||||
.vjs-subs-caps-button {
|
.vjs-subs-caps-button {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
@ -73,5 +60,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.youtube-player {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.youtube-iFrame {
|
||||||
|
width: 100%;
|
||||||
|
height: 280px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,11 +2,11 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { E2EImportsModule } from 'e2e-imports.module';
|
import { E2EImportsModule } from 'e2e-imports.module';
|
||||||
|
|
||||||
import { VjsPlayerComponent } from './vjs-player.component';
|
import { VideoPlayerComponent } from './video-player.component';
|
||||||
|
|
||||||
describe('VjsPlayerComponent', () => {
|
describe('VjsPlayerComponent', () => {
|
||||||
let component: VjsPlayerComponent;
|
let component: VideoPlayerComponent;
|
||||||
let fixture: ComponentFixture<VjsPlayerComponent>;
|
let fixture: ComponentFixture<VideoPlayerComponent>;
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@ -15,7 +15,7 @@ describe('VjsPlayerComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(VjsPlayerComponent);
|
fixture = TestBed.createComponent(VideoPlayerComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
@ -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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
|
||||||
<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>
|
|
@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -127,8 +127,7 @@ import { AssignmentPollDetailContentComponent } from './components/assignment-po
|
|||||||
import { GlobalSpinnerComponent } from './components/global-spinner/global-spinner.component';
|
import { GlobalSpinnerComponent } from './components/global-spinner/global-spinner.component';
|
||||||
import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
import { UserMenuComponent } from './components/user-menu/user-menu.component';
|
||||||
import { JitsiComponent } from './components/jitsi/jitsi.component';
|
import { JitsiComponent } from './components/jitsi/jitsi.component';
|
||||||
import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component';
|
import { VideoPlayerComponent } from './components/video-player/video-player.component';
|
||||||
import { LiveStreamComponent } from './components/live-stream/live-stream.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 { ApplauseBarDisplayComponent } from './components/applause-bar-display/applause-bar-display.component';
|
||||||
import { ProgressComponent } from './components/progress/progress.component';
|
import { ProgressComponent } from './components/progress/progress.component';
|
||||||
@ -303,8 +302,7 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
|||||||
MotionPollDetailContentComponent,
|
MotionPollDetailContentComponent,
|
||||||
AssignmentPollDetailContentComponent,
|
AssignmentPollDetailContentComponent,
|
||||||
JitsiComponent,
|
JitsiComponent,
|
||||||
VjsPlayerComponent,
|
VideoPlayerComponent,
|
||||||
LiveStreamComponent,
|
|
||||||
ListOfSpeakersContentComponent
|
ListOfSpeakersContentComponent
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
@ -367,8 +365,7 @@ import { ApplauseParticleDisplayComponent } from './components/applause-particle
|
|||||||
MotionPollDetailContentComponent,
|
MotionPollDetailContentComponent,
|
||||||
AssignmentPollDetailContentComponent,
|
AssignmentPollDetailContentComponent,
|
||||||
JitsiComponent,
|
JitsiComponent,
|
||||||
VjsPlayerComponent,
|
VideoPlayerComponent,
|
||||||
LiveStreamComponent,
|
|
||||||
ListOfSpeakersContentComponent,
|
ListOfSpeakersContentComponent,
|
||||||
ApplauseBarDisplayComponent,
|
ApplauseBarDisplayComponent,
|
||||||
ProgressComponent,
|
ProgressComponent,
|
||||||
|
Loading…
Reference in New Issue
Block a user