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:
parent
f57fe05e26
commit
372f1eaa7e
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
@ -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 { 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,
|
||||
|
Loading…
Reference in New Issue
Block a user