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 *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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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