Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(video): add download button and overlay to video player (DEV-1151) #798

Merged
merged 10 commits into from Aug 17, 2022
Expand Up @@ -4,10 +4,20 @@
<!-- in case of an error -->
<app-status [status]="404" [url]="src.fileValue.fileUrl" [representation]="'video'" *ngIf="failedToLoad"></app-status>

<video class="video" #videoEle *ngIf="!fileHasChanged; else loading" [src]="video" type="video/mp4" preload="auto" [muted]="muted"
(click)="togglePlay()" (timeupdate)="timeUpdate($event)" (loadedmetadata)="loadedMetadata()"
(canplaythrough)="loadedVideo()">
</video>
<div class="video-container">
<video class="video" #videoEle *ngIf="!fileHasChanged; else loading" [src]="video" type="video/mp4" preload="auto" [muted]="muted"
(click)="togglePlay()" (timeupdate)="timeUpdate($event)" (loadedmetadata)="loadedMetadata()"
(canplaythrough)="loadedVideo()">
</video>

<div class="overlay">
<div>
<mat-icon (click)="updateTimeFromButton(-10)">replay_10</mat-icon>
<mat-icon (click)="togglePlay()">{{ reachedTheEnd ? "replay" : (play ? "pause" : "play_arrow") }}</mat-icon>
<mat-icon (click)="updateTimeFromButton(10)">forward_10</mat-icon>
</div>
</div>
</div>

<ng-template #loading>
<app-progress-indicator></app-progress-indicator>
Expand All @@ -23,7 +33,6 @@
[fileHasChanged]="fileHasChanged"
(loaded)="displayPreview(!$event)">
</app-video-preview>

</div>
</div>
</div>
Expand All @@ -37,6 +46,25 @@
</mat-toolbar-row>

<mat-toolbar-row class="action">
<!-- vertical more button with menu to open and copy url -->
<button mat-icon-button [matMenuTriggerFor]="more">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #more="matMenu">
<button mat-menu-item (click)="openVideoInNewTab(this.src.fileValue.fileUrl)">
Open file in new tab
</button>
<button mat-menu-item [cdkCopyToClipboard]="this.src.fileValue.fileUrl"
(click)="openSnackBar('URL copied to clipboard!')">
Copy URL to clipboard
</button>
<button mat-menu-item (click)="downloadVideo(this.src.fileValue.fileUrl)">
Download file
</button>
<button mat-menu-item (click)="openReplaceFileDialog()">
Replace file
</button>
</mat-menu>
<button mat-icon-button (click)="goToStart()" [disabled]="currentTime === 0 || failedToLoad"
matTooltip="Stop and go to start" [matTooltipPosition]="matTooltipPos">
<mat-icon>skip_previous</mat-icon>
Expand All @@ -46,37 +74,27 @@
[disabled]="failedToLoad">
<mat-icon>{{ reachedTheEnd ? "replay" : (play ? "pause" : "play_arrow") }}</mat-icon>
</button>

<span class="empty-space"></span>
<span class="fill-remaining-space"></span>
<button mat-icon-button (click)="updateTimeFromButton(-10)"
matTooltip="10 seconds backward" [matTooltipPosition]="matTooltipPos"
[disabled]="failedToLoad">
<mat-icon>replay_10</mat-icon>
</button>

<div (wheel)="updateTimeFromScroll($event)">
<p class="mat-body-1 time" (click)="togglePlay()">
{{ currentTime | appTime }}
<span *ngIf="duration">/ {{ duration | appTime }}</span>
</p>
</div>

<button mat-icon-button (click)="updateTimeFromButton(10)"
matTooltip="10 seconds forward" [matTooltipPosition]="matTooltipPos"
[disabled]="failedToLoad">
<mat-icon>forward_10</mat-icon>
</button>
<!-- <button mat-icon-button (click)="muted = !muted" [matTooltip]="(muted ? 'Unmute' : 'Mute')"-->
<!-- [matTooltipPosition]="matTooltipPos"-->
<!-- [disabled]="failedToLoad">-->
<!-- <mat-icon>-->
<!-- {{ muted ? "volume_mute" : "volume_up" }}-->
<!-- </mat-icon>-->
<!-- </button>-->
<span class="fill-remaining-space"></span>
<button mat-icon-button (click)="muted = !muted" [matTooltip]="(muted ? 'Unmute' : 'Mute')"
[matTooltipPosition]="matTooltipPos"
[disabled]="failedToLoad">
<mat-icon>
{{ muted ? "volume_mute" : "volume_up" }}
</mat-icon>
</button>
<button mat-icon-button (click)="openReplaceFileDialog()" class="replace-file" matTooltip="Replace video file" [matTooltipPosition]="matTooltipPos">
<mat-icon>cloud_upload</mat-icon>
</button>
<span class="empty-space"></span>
<span class="empty-space"></span>

<button mat-icon-button (click)="toggleCinemaMode()" [disabled]="failedToLoad"
[matTooltip]="(cinemaMode ? 'Default view' : 'Cinema mode')" [matTooltipPosition]="matTooltipPos">
<mat-icon>{{cinemaMode ? "fullscreen_exit" : "fullscreen"}}</mat-icon>
Expand Down
Expand Up @@ -11,6 +11,31 @@
position: relative;
height: 450px;

.overlay {
opacity: 0;
background: rgba(0, 0, 0, 0.58);
z-index: 1;
height: 100%;
width: 100%;
position: absolute;
display: flex;
justify-content: center;
align-items: center;

mat-icon {
border-radius: 24px;
height:40px !important;
width:40px !important;
font-size:40px !important;
cursor: pointer;
padding: 4px;
}

mat-icon:hover {
background: rgba(0,0,0, 0.4);
}
}

.video {
position: absolute;
display: block;
Expand All @@ -22,6 +47,10 @@
height: 100%;
}

.video-container:hover .overlay {
opacity: 1;
}

.preview-line {
width: 100%;
width: calc(100% - 16px);
Expand Down
62 changes: 60 additions & 2 deletions src/app/workspace/resource/representation/video/video.component.ts
@@ -1,6 +1,7 @@
import { AfterViewInit, Component, ElementRef, EventEmitter, HostListener, Inject, Input, OnInit, Output, ViewChild } from '@angular/core';
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import {
ApiResponseError,
Constants,
Expand All @@ -21,6 +22,7 @@ import { EmitEvent, Events, UpdatedFileEventValue, ValueOperationEventService }
import { PointerValue } from '../av-timeline/av-timeline.component';
import { FileRepresentation } from '../file-representation';
import { RepresentationService } from '../representation.service';
import { NotificationService } from '../../../../main/services/notification.service';

@Component({
selector: 'app-video',
Expand Down Expand Up @@ -48,7 +50,7 @@ export class VideoComponent implements OnInit, AfterViewInit {
@ViewChild('preview') preview: ElementRef;

loading = true;

originalFilename: string;
failedToLoad = false;

// video file url
Expand Down Expand Up @@ -102,10 +104,12 @@ export class VideoComponent implements OnInit, AfterViewInit {

constructor(
@Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection,
private readonly _http: HttpClient,
private _dialog: MatDialog,
private _sanitizer: DomSanitizer,
private _errorHandler: ErrorHandlerService,
private _rs: RepresentationService,
private _errorHandler: ErrorHandlerService,
private _notification: NotificationService,
private _valueOperationEventService: ValueOperationEventService
) { }

Expand All @@ -119,6 +123,7 @@ export class VideoComponent implements OnInit, AfterViewInit {
this.video = this._sanitizer.bypassSecurityTrustUrl(this.src.fileValue.fileUrl);
this.failedToLoad = !this._rs.doesFileExist(this.src.fileValue.fileUrl);
this.fileHasChanged = false;
this._getOriginalFilename();
}

ngAfterViewInit() {
Expand Down Expand Up @@ -293,6 +298,39 @@ export class VideoComponent implements OnInit, AfterViewInit {
this.preview.nativeElement.style.display = (status ? 'block' : 'none');
}

/**
* display message to confirm the copy of the citation link (ARK URL)
*/
openSnackBar(message: string) {
this._notification.openSnackBar(message);
}

async downloadVideo(url: string) {
try {
const res = await this._http.get(url, { responseType: 'blob' }).toPromise();
this.downloadFile(res);
} catch (e) {
this._errorHandler.showMessage(e);
}
}

downloadFile(data) {
const url = window.URL.createObjectURL(data);
const e = document.createElement('a');
e.href = url;

// set filename
if (this.originalFilename === undefined) {
e.download = url.substr(url.lastIndexOf('/') + 1);
} else {
e.download = this.originalFilename;
}

document.body.appendChild(e);
e.click();
document.body.removeChild(e);
}

/**
* opens replace file dialog
*
Expand Down Expand Up @@ -321,6 +359,10 @@ export class VideoComponent implements OnInit, AfterViewInit {
});
}

openVideoInNewTab(url: string) {
window.open(url, '_blank');
}

/**
* general video navigation: Update current video time from position
*
Expand All @@ -340,6 +382,22 @@ export class VideoComponent implements OnInit, AfterViewInit {
this.previewTime = this.previewTime < 0 ? 0 : this.previewTime;
}

private _getOriginalFilename() {
const requestOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' }),
withCredentials: true
};

const index = this.src.fileValue.fileUrl.indexOf(this.src.fileValue.filename);
const pathToJson = this.src.fileValue.fileUrl.substring(0, index + this.src.fileValue.filename.length) + '/knora.json';

this._http.get(pathToJson, requestOptions).subscribe(
res => {
this.originalFilename = res['originalFilename'];
}
);
}

/**
* replaces file
* @param file
Expand Down