/
video.component.ts
379 lines (310 loc) · 11.1 KB
/
video.component.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
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 {
ApiResponseError,
Constants,
KnoraApiConnection,
ReadMovingImageFileValue,
ReadResource,
UpdateFileValue,
UpdateResource,
UpdateValue,
WriteValueResponse
} from '@dasch-swiss/dsp-js';
import { mergeMap } from 'rxjs/operators';
import { DspApiConnectionToken } from 'src/app/main/declarations/dsp-api-tokens';
import { DialogComponent } from 'src/app/main/dialog/dialog.component';
import { ErrorHandlerService } from 'src/app/main/services/error-handler.service';
import { SplitSize } from 'src/app/workspace/results/results.component';
import { EmitEvent, Events, UpdatedFileEventValue, ValueOperationEventService } from '../../services/value-operation-event.service';
import { PointerValue } from '../av-timeline/av-timeline.component';
import { FileRepresentation } from '../file-representation';
import { RepresentationService } from '../representation.service';
@Component({
selector: 'app-video',
templateUrl: './video.component.html',
styleUrls: ['./video.component.scss']
})
export class VideoComponent implements OnInit, AfterViewInit {
@Input() src: FileRepresentation;
@Input() start?= 0;
@Input() parentResource: ReadResource;
@Input() splitSizeChanged: SplitSize;
@Output() loaded = new EventEmitter<boolean>();
@ViewChild('videoEle') videoEle: ElementRef;
@ViewChild('timeline') timeline: ElementRef;
@ViewChild('progress') progress: ElementRef;
@ViewChild('preview') preview: ElementRef;
loading = true;
failedToLoad = false;
// video file url
video: SafeUrl;
// video information
aspectRatio: number;
// preview image information
frameWidth = 160;
halfFrameWidth: number = Math.round(this.frameWidth / 2);
frameHeight: number;
lastFrameNr: number;
// preview images are organised in matrix files; we'll need the last number of those files
matrixWidth: number = Math.round(this.frameWidth * 6);
matrixHeight: number;
lastMatrixNr: number;
lastMatrixLine: number;
// size of progress bar / timeline
timelineDimension: DOMRect;
// time information
duration: number;
currentTime: number = this.start;
previewTime = 0;
// seconds per pixel to calculate preview image on timeline
secondsPerPixel: number;
// percent of video loaded
currentBuffer: number;
// status
play = false;
reachedTheEnd = false;
// volume
volume = .75;
muted = false;
// video player mode
cinemaMode = false;
// matTooltipPosition
matTooltipPos = 'below';
// if file was replaced, we have to reload the preview
fileHasChanged = false;
constructor(
@Inject(DspApiConnectionToken) private _dspApiConnection: KnoraApiConnection,
private _dialog: MatDialog,
private _sanitizer: DomSanitizer,
private _errorHandler: ErrorHandlerService,
private _rs: RepresentationService,
private _valueOperationEventService: ValueOperationEventService
) { }
@HostListener('document:keydown', ['$event']) onKeydownHandler(event: KeyboardEvent) {
if (event.key === 'Escape' && this.cinemaMode) {
this.cinemaMode = false;
}
}
ngOnInit(): void {
this.video = this._sanitizer.bypassSecurityTrustUrl(this.src.fileValue.fileUrl);
this.failedToLoad = !this._rs.doesFileExist(this.src.fileValue.fileUrl);
this.fileHasChanged = false;
}
ngAfterViewInit() {
this.loaded.emit(true);
}
/**
* stop playing and go back to start
*/
goToStart() {
this.videoEle.nativeElement.pause();
this.play = false;
this.currentTime = 0;
this.videoEle.nativeElement.currentTime = this.currentTime;
}
/**
* toggle play / pause
*/
togglePlay() {
if (!this.failedToLoad) {
this.play = !this.play;
if (this.play) {
this.videoEle.nativeElement.play();
} else {
this.videoEle.nativeElement.pause();
}
}
}
/**
* toggle player size between cinema (big) and normal mode
*/
toggleCinemaMode() {
this.cinemaMode = !this.cinemaMode;
}
/**
* update current time info and buffer size
* @param ev Event
*/
timeUpdate(ev: Event) {
// current time
this.currentTime = this.videoEle.nativeElement.currentTime;
// buffer progress
this.currentBuffer = (this.videoEle.nativeElement.buffered.end(0) / this.duration) * 100;
let range = 0;
const bf = this.videoEle.nativeElement.buffered;
while (!(bf.start(range) <= this.currentTime && this.currentTime <= bf.end(range))) {
range += 1;
}
if (this.currentTime === this.duration && this.play) {
this.play = false;
this.reachedTheEnd = true;
} else {
this.reachedTheEnd = false;
}
// --> TODO: activate the buffer information
// const loadStartPercentage = (bf.start(range) / this.duration) * 100;
// const loadEndPercentage = (bf.end(range) / this.duration) * 100;
// const loadPercentage = (loadEndPercentage - loadStartPercentage);
}
/**
* as soon as video has status "loadedmetadata" we're able to read
* information about duration, video size and set volume
*
*/
loadedMetadata() {
// get video duration
this.duration = this.videoEle.nativeElement.duration;
// set default volume
this.videoEle.nativeElement.volume = this.volume;
// load preview file
this.displayPreview(true);
}
/**
* what happens when video is loaded
*/
loadedVideo() {
this.loading = false;
this.play = !this.videoEle.nativeElement.paused;
}
/**
* video navigation from button (-/+ 10 sec)
*
* @param range positive or negative number value
*/
updateTimeFromButton(range: number) {
if (range > 0 && this.currentTime > (this.duration - 10)) {
this._navigate(this.duration);
} else if (range < 0 && this.currentTime < 10) {
this._navigate(0);
} else {
this._navigate(this.currentTime + range);
}
}
/**
* video navigation from timeline / progress bar
*
* @param ev MatSliderChange
*/
updateTimeFromSlider(time: number) {
this._navigate(time);
}
/**
* video navigation from scroll event
*
* @param ev WheelEvent
*/
updateTimeFromScroll(ev: WheelEvent) {
ev.preventDefault();
this._navigate(this.currentTime + (ev.deltaY / 25));
}
/**
* event on mouses move on timeline
* @param ev MouseEvent
*/
mouseMove(ev: MouseEvent) {
this._calcPreviewTime(ev);
}
/**
* show preview image during "mousemove" on progress bar / timeline
*
* @param ev PointerValue
*/
updatePreview(ev: PointerValue) {
this.displayPreview(true);
this.previewTime = Math.round(ev.time);
// position from left:
let leftPosition: number = (ev.position - this.timelineDimension.x) - this.halfFrameWidth;
// prevent overflow of preview image on the left
if (leftPosition <= 8) {
leftPosition = 8;
}
// prevent overflow of preview image on the right
if (leftPosition >= (this.timelineDimension.width - this.frameWidth + 8)) {
leftPosition = this.timelineDimension.width - this.frameWidth + 8;
}
// set preview positon on x axis
this.preview.nativeElement.style.left = leftPosition + 'px';
}
/**
* show preview image or hide it
*
* @param status true = show ('block'), false = hide ('none')
*/
displayPreview(status: boolean) {
this.preview.nativeElement.style.display = (status ? 'block' : 'none');
}
/**
* opens replace file dialog
*
*/
openReplaceFileDialog() {
const propId = this.parentResource.properties[Constants.HasMovingImageFileValue][0].id;
const dialogConfig: MatDialogConfig = {
width: '800px',
maxHeight: '80vh',
position: {
top: '112px'
},
data: { mode: 'replaceFile', title: 'Video (mp4)', subtitle: 'Update the video file of this resource', representation: 'movingImage', id: propId },
disableClose: true
};
const dialogRef = this._dialog.open(
DialogComponent,
dialogConfig
);
dialogRef.afterClosed().subscribe((data) => {
if (data) {
this._replaceFile(data);
}
});
}
/**
* general video navigation: Update current video time from position
*
* @param position Pixelnumber
*/
private _navigate(position: number) {
this.videoEle.nativeElement.currentTime = position;
}
/**
* calcs time in preview frame
* @param ev
*/
private _calcPreviewTime(ev: MouseEvent) {
this.previewTime = Math.round((ev.offsetX / this.timeline.nativeElement.clientWidth) * this.duration);
this.previewTime = this.previewTime > this.duration ? this.duration : this.previewTime;
this.previewTime = this.previewTime < 0 ? 0 : this.previewTime;
}
/**
* replaces file
* @param file
*/
private _replaceFile(file: UpdateFileValue) {
this.goToStart();
this.fileHasChanged = true;
const updateRes = new UpdateResource();
updateRes.id = this.parentResource.id;
updateRes.type = this.parentResource.type;
updateRes.property = Constants.HasMovingImageFileValue;
updateRes.value = file;
this._dspApiConnection.v2.values.updateValue(updateRes as UpdateResource<UpdateValue>).pipe(
mergeMap((res: WriteValueResponse) => this._dspApiConnection.v2.values.getValue(this.parentResource.id, res.uuid))
).subscribe(
(res2: ReadResource) => {
this.src.fileValue.fileUrl = (res2.properties[Constants.HasMovingImageFileValue][0] as ReadMovingImageFileValue).fileUrl;
this.src.fileValue.filename = (res2.properties[Constants.HasMovingImageFileValue][0] as ReadMovingImageFileValue).filename;
this.src.fileValue.strval = (res2.properties[Constants.HasMovingImageFileValue][0] as ReadMovingImageFileValue).strval;
this.ngOnInit();
this.loadedMetadata();
this._valueOperationEventService.emit(
new EmitEvent(Events.FileValueUpdated, new UpdatedFileEventValue(
res2.properties[Constants.HasMovingImageFileValue][0])));
},
(error: ApiResponseError) => {
this._errorHandler.showMessage(error);
}
);
}
}