@vidispine/vdt-videojs 21.3.0 → 22.1.0-pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +4 -4
  2. package/deprecated-app/README.md +21 -0
  3. package/{demo.html → deprecated-app/index.html} +1 -1
  4. package/{demo.js → deprecated-app/index.js} +69 -41
  5. package/{samples → deprecated-app/samples}/birds-es.vvt +0 -0
  6. package/{samples → deprecated-app/samples}/birds.vvt +0 -0
  7. package/{tests.js → deprecated-app/tests.js} +87 -54
  8. package/{lib/style.css → dist/index.css} +755 -55
  9. package/dist/index.es.js +2099 -0
  10. package/dist/index.js +2112 -0
  11. package/dist/index.umd.js +2115 -0
  12. package/package.json +29 -85
  13. package/rollup.config.js +54 -0
  14. package/src/constants.js +1 -1
  15. package/src/externalControls.js +14 -18
  16. package/src/helpOverlay.js +12 -16
  17. package/src/helpers.js +3 -3
  18. package/src/index.js +3 -9
  19. package/src/index.stories.mdx +59 -0
  20. package/src/menu.api.stories.mdx +59 -0
  21. package/src/menu.js +23 -19
  22. package/src/{menu.example.md → menu.usage.stories.mdx} +31 -21
  23. package/src/osd.js +24 -18
  24. package/src/player.api.stories.mdx +120 -0
  25. package/src/player.js +157 -108
  26. package/src/player.scss +2 -0
  27. package/src/{player.example.md → player.usage.stories.mdx} +18 -8
  28. package/src/seekBar.js +85 -48
  29. package/src/timeCodeDisplay.js +39 -31
  30. package/src/videoTime.js +4 -4
  31. package/build.js +0 -233
  32. package/lib/index.cjs.js +0 -3240
  33. package/lib/index.esm.js +0 -3227
  34. package/lib/index.umd.js +0 -3243
  35. package/lib/package.json +0 -13
  36. package/lib/src/constants.js +0 -51
  37. package/lib/src/externalControls.js +0 -307
  38. package/lib/src/helpOverlay.js +0 -106
  39. package/lib/src/helpers.js +0 -34
  40. package/lib/src/index.js +0 -13
  41. package/lib/src/menu.js +0 -365
  42. package/lib/src/osd.js +0 -91
  43. package/lib/src/player.js +0 -943
  44. package/lib/src/seekBar.js +0 -833
  45. package/lib/src/timeCodeDisplay.js +0 -253
  46. package/lib/src/videoTime.js +0 -66
  47. package/log.js +0 -24
  48. package/src/_index.stories.js +0 -24
  49. package/src/externalControls.stories.js +0 -6
  50. package/src/helpOverlay.stories.js +0 -6
  51. package/src/menu.classMethods.md +0 -47
  52. package/src/player.classMethods.md +0 -108
  53. package/src/player.stories.js +0 -14
  54. package/src/seekBar.stories.js +0 -8
  55. package/tmp/player.css +0 -1
package/lib/index.esm.js DELETED
@@ -1,3227 +0,0 @@
1
- import videojs from 'video.js/dist/alt/video.core.min';
2
- import mousetrap from 'mousetrap';
3
-
4
- var name = "@vidispine/vdt-videojs";
5
- var version = "21.3.0";
6
- var license = "UNLICENSED";
7
- var main = "lib/index.cjs.js";
8
- var module = "lib/index.es.js";
9
- var author = "Oto Inša <otoinsa@bertelsmann.de>";
10
- var scripts = {
11
- cleanup: "rm -rf ./dist && rm -rf ./.cache",
12
- start: "npm run cleanup && parcel ./demo.html --no-source-maps --no-hmr -p 2222 -t browser --open --out-dir ./dev",
13
- "build-legacy": "parcel build ./src/player.js --out-dir ./lib --no-source-maps",
14
- build: "node ./build.js",
15
- "build:watch": "node ./build.js --watch",
16
- "build:demo": "rm -rf ./demo/ && parcel build ./demo.html --out-file index.html --out-dir ./demo --no-source-maps --public-url ./",
17
- docs: "jsdoc ./src/player.js -d ./docs/"
18
- };
19
- var devDependencies = {
20
- "@babel/core": "^7.7.7",
21
- "babel-plugin-transform-runtime": "^6.23.0",
22
- chokidar: "^3.3.1",
23
- colors: "^1.4.0",
24
- "parcel-bundler": "^1.12.4",
25
- rollup: "^2.20.0",
26
- "rollup-plugin-json": "^4.0.0",
27
- sass: "^1.24.3",
28
- "strip-comments": "^2.0.1"
29
- };
30
- var dependencies = {
31
- mousetrap: "^1.6.3",
32
- "video.js": "^7.6.6"
33
- };
34
- var browserslist = [
35
- "since 2017-06"
36
- ];
37
- var changelog = {
38
- "0.1.2": [
39
- "Removed unwanted videojs markup",
40
- "Implemented playback hotkeys",
41
- "Borrowed styling ideas from a popular online video platform",
42
- "Visual feedback gizmo as hotkey feedback",
43
- "Subtitle support (VVT)",
44
- "Persistance - volume level",
45
- "Custom timecode display (SMPTE)",
46
- "Frame forward / frame backward 6unctionality"
47
- ],
48
- "0.1.3": [
49
- "Renamed 'subtitles off' to just 'off'",
50
- "Timecode SMPTE - current time in SMPTE with input capabilities",
51
- "Timecode SMPTE - total video duration timecode (read-only)",
52
- "Settings persistance - storing wether subtitles should be on and the selected language as well"
53
- ],
54
- "0.1.4": [
55
- "Subtitles toggle hotkey - c",
56
- "Now accepts Blob as subtitles source file",
57
- "Timecode input improvements",
58
- "Screenshot button with currentTime callback",
59
- "Moved OSD in its own class"
60
- ],
61
- "0.1.6": [
62
- "Help overlay for keyboard controls",
63
- "External player controls component (js, React)",
64
- "External seek-bar component (js, React)"
65
- ],
66
- "0.1.7": [
67
- "In / Out markers for seek bar"
68
- ],
69
- "0.1.8": [
70
- "External TimeCodeDisplay support for player"
71
- ],
72
- "0.1.9": [
73
- "Multiple regions with annotations for SeekBar"
74
- ],
75
- "0.2.2": [
76
- "Overall version bump (improvements)"
77
- ],
78
- "0.3.0": [
79
- "controlsBelowPlayer - reactive option",
80
- "onDark - used in conjuction with controlsBelowPlayer for dark backgrounds",
81
- "Bug fixes, convenience class methods"
82
- ],
83
- "0.3.1": [
84
- "SeekBar - Seek animation, dark mode improvements",
85
- "Bug fixes, refactor & code sharing improvements"
86
- ],
87
- "0.5.3": [
88
- "vdt-videojs now provides a commonjs module with named exports"
89
- ],
90
- "0.6.1": [
91
- "Internal refactor before splitting main features into videojs plugins",
92
- "More bugfixes"
93
- ]
94
- };
95
- var wishlist = [
96
- "Timecode offset capabilities - i.e. start time from 10:00:00:00"
97
- ];
98
- var backlog = [
99
- "External player controls component",
100
- "Check for hijacked hotkeys and player focus"
101
- ];
102
- var pkgInfo = {
103
- name: name,
104
- version: version,
105
- license: license,
106
- "private": false,
107
- main: main,
108
- module: module,
109
- author: author,
110
- scripts: scripts,
111
- devDependencies: devDependencies,
112
- dependencies: dependencies,
113
- browserslist: browserslist,
114
- changelog: changelog,
115
- wishlist: wishlist,
116
- backlog: backlog
117
- };
118
-
119
- const defaults = Object.freeze({
120
- frameRate: (30000 / 1001),
121
- jumpSeconds: 10,
122
- inactivityTimeout: 1200, // hide player controls if user inactive for this duration
123
- audioInactivityTimeout: 0,
124
- });
125
-
126
- const osd = Object.freeze({
127
- play: 0,
128
- pause: 1,
129
- volchange: 2,
130
- jumpForward: 3,
131
- jumpBackward: 4,
132
- });
133
-
134
- const settings = Object.freeze({
135
- volume: 'volume-setting',
136
- subLang: 'subtitles-language',
137
- subs: 'subtitles-enabled',
138
- });
139
-
140
- const classNames = Object.freeze({
141
- poster: 'has-poster',
142
- subtitles: 'has-subtitles',
143
- subtitlesActive: 'subtitles-active',
144
- onDark: 'on-dark',
145
- loading: 'loading',
146
- });
147
-
148
- const colors = Object.freeze({
149
- gray: '#787878',
150
- light: 'rgba(255,255,255,.75)',
151
- });
152
-
153
- const packageInfo = Object.freeze({
154
- name: pkgInfo.name.split('/')[1],
155
- vendor: pkgInfo.name.split('/')[0],
156
- });
157
-
158
- const constants = Object.freeze({
159
- defaults,
160
- settings,
161
- classNames,
162
- colors,
163
- osd,
164
- packageInfo,
165
- });
166
-
167
- function truncate(input, len) {
168
- if (input.length > len) return `${input.substring(0, len)}...`;
169
- return input;
170
- }
171
-
172
-
173
- /*
174
- Make certain component props React-ive
175
- by being able to call respective component .handleNewProps()
176
- method in react wrappers
177
- */
178
- function reactiveSetters() {
179
- // will try to find a matching setter in context
180
- // TODO - switch to for loop for more efficient updates
181
- function handleNewProps(props) {
182
- if (typeof props === 'object') {
183
- const keys = Object.getOwnPropertyNames(props);
184
- const proto = Object.getPrototypeOf(this);
185
- keys.forEach((key) => {
186
- // check for own setters / getters
187
- const descriptor = Object.getOwnPropertyDescriptor(proto, key);
188
-
189
- if (descriptor && descriptor.set) { // if a setter is defined, apply the prop
190
- this[key] = props[key];
191
- }
192
- });
193
- }
194
- }
195
-
196
- // extends a context with handleNewProps method
197
- this.handleNewProps = handleNewProps;
198
- }
199
-
200
- // an elegant way to prevent xss attacks
201
- function sanitize(str) {
202
- const temp = document.createElement('div');
203
- temp.textContent = str;
204
- return temp.innerHTML;
205
- }
206
-
207
- /* eslint-disable prefer-destructuring */
208
-
209
- class OSD {
210
- constructor(props) {
211
- this.props = props;
212
-
213
- this.state = {
214
- enabled: true,
215
- };
216
-
217
- this.container = document.createElement('div');
218
- this.container.className = 'vdt-player-osd';
219
-
220
- // vertically center with css
221
- this.container.innerHTML = `
222
- <table>
223
- <tbody>
224
- <td>
225
- <div class="osd-feedback-icon"></div>
226
- </td>
227
- </tbody>
228
- </table>
229
- `;
230
-
231
- this.iconEl = this.container.getElementsByClassName('osd-feedback-icon')[0];
232
-
233
- this.props.target && this.props.target.appendChild(this.container);
234
- }
235
-
236
- feedback(feedbackType, details = null) { // details - {} - can contain additional data
237
- if (!this.state.enabled) {
238
- return;
239
- }
240
-
241
- let className;
242
-
243
- switch (feedbackType) {
244
- case constants.osd.pause:
245
- className = 'pause';
246
- break;
247
- case constants.osd.play:
248
- className = 'play';
249
- break;
250
- case constants.osd.volchange:
251
- const currentVolumeLevel = details.volume;
252
- let perceivedVolumeLevel; // 'low' | 'mid' | 'high' string to utilize video js eisting icons
253
-
254
- /*
255
- case switch will be too slow for this, if's are 2x more performant for this case
256
- (Except in IE)
257
- so... no case switch inside case switch this time :/
258
- */
259
-
260
- // alues here are a bit of a guess, but they seem to match videojs thresholds
261
- if (currentVolumeLevel === 0) { perceivedVolumeLevel = 'null'; } else
262
- if (currentVolumeLevel > 0.65) { perceivedVolumeLevel = 'high'; } else
263
- if (currentVolumeLevel < 0.35) { perceivedVolumeLevel = 'low'; } else { perceivedVolumeLevel = 'mid'; }
264
-
265
- className = `volchange ${perceivedVolumeLevel}`;
266
- break;
267
- case constants.osd.jumpForward:
268
- className = 'forward-jump';
269
- break;
270
- case constants.osd.jumpBackward:
271
- className = 'backward-jump';
272
- break;
273
- }
274
-
275
-
276
- if (this.osdTimeout) { // should clear previous timeout/classname if in progress
277
- this.container.classList.remove('active');
278
- clearTimeout(this.osdTimeout);
279
- }
280
-
281
- // do feedback
282
- this.container.classList.add('active');
283
- this.osdTimeout = setTimeout(() => {
284
- this.container.classList.remove('active');
285
- }, 200);
286
-
287
- // get specific with classnames
288
- this.iconEl.className = `osd-feedback-icon ${className}`; // gets an overwrite each time
289
- }
290
-
291
- disable() {
292
- this.state.enabled = false;
293
- }
294
-
295
- enable() {
296
- this.state.enabled = true;
297
- }
298
- }
299
-
300
- /* eslint-disable no-restricted-globals */
301
- /* eslint-disable class-methods-use-this */
302
- /* eslint-disable no-plusplus */
303
- /* eslint-disable no-unused-expressions */
304
-
305
- const timeCodeNodeNames = [
306
- 'hours',
307
- 'minutes',
308
- 'seconds',
309
- 'frame',
310
- ];
311
-
312
- class TimeCodeDisplay {// displays and renders smpte timecodes
313
- constructor(props) {
314
- this.props = props;
315
-
316
- this.state = {
317
- selectedNode: null, // avoid updating values in a selected node
318
- };
319
-
320
- const classNames = [
321
- 'vdt-timecode',
322
- 'vjs-time-control', // will apply default videojs style to the whole container, centers things vertically
323
- (!(this.props.onInput)) ? 'duration' : '', // if no input capability, assume it's a duration timecode
324
- ];
325
- this.container = document.createElement('div');
326
- this.container.className = classNames.join(' ');
327
-
328
- this.nodeNames = [].concat(timeCodeNodeNames); // used internally
329
- this.nodes = {}; // store sub-element nodes for mapped access here
330
-
331
- this.nodeNames.forEach((nodeName, index) => {
332
- const node = document.createElement('input');
333
- node.className = nodeName;
334
- node.tabIndex = -1; // prevent default Tab behaviour for cycling inputs
335
-
336
- if (this.props.onInput) { // optional input capabilities
337
- node.maxLength = 2;
338
-
339
- node.onfocus = (e) => {
340
- this.handleInputFocus(e, nodeName);
341
- };
342
-
343
- node.onblur = (e) => {
344
- this.handleInputBlur(e, nodeName);
345
- };
346
-
347
- node.onmouseup = (e) => {
348
- e.preventDefault();
349
- e.currentTarget.select();
350
- };
351
-
352
- node.onkeydown = (e) => {
353
- this.handleTimeCodeInput(e, nodeName);
354
- };
355
-
356
- node.onkeyup = (e) => {
357
- this.handleTimeCodeInputOnKeyUp(e, nodeName);
358
- };
359
- } else {
360
- node.readOnly = true; // display-only timecode
361
- }
362
-
363
- this.nodes[nodeName] = node;
364
-
365
-
366
- const colon = document.createElement('span');
367
- colon.innerHTML = ':';
368
-
369
-
370
- this.container.appendChild(node);
371
- (index !== this.nodeNames.length - 1) && this.container.appendChild(colon);
372
- });
373
-
374
- if (this.props.target) {
375
- if (this.props.appendAfter) { // so it's possible to append after volume control, for example
376
- this.props.target.parentNode.insertBefore(this.container, this.props.target.nextSibling);
377
- } else {
378
- this.props.target.appendChild(this.container);
379
- }
380
-
381
- // start at 0 values for now, if player will "remember" playback position, revisit this
382
- this.reset();
383
- } else {
384
- // eslint-disable-next-line no-console
385
- console.error(`Unable to instantiate a new ${this.constructor.name} without target DOM node`); // no joy
386
- }
387
- }
388
-
389
- update(valuesObj) { // Object.keys and array.forEach are too slow, classic for is faster
390
- const startCount = this.nodeNames.length - 1;
391
- let i;
392
- for (i = startCount; i > -1; i--) { // updates follow frame -> seconds -> minutes -> hours order
393
- const key = this.nodeNames[i];
394
-
395
- if (typeof valuesObj[key] !== 'undefined' && (valuesObj[key] !== parseInt(this.nodes[key].value, 10)) && key !== this.state.selectedNode) { // re-render node if value is defineed and is different from current value
396
- const str = valuesObj[key].toString();
397
- this.nodes[key].value = this.toDigitPairStr(str);
398
- }
399
- }
400
- }
401
-
402
- reset() {
403
- this.update({
404
- hours: 0,
405
- minutes: 0,
406
- seconds: 0,
407
- frame: 0,
408
- });
409
- }
410
-
411
- handleInputFocus(e, nodeName) {
412
- this.state.selectedNode = nodeName; // prevent updates on this input
413
- this.nodes[nodeName].select();
414
- this.props.onInputFocus && this.props.onInputFocus(nodeName);
415
- }
416
-
417
- handleInputBlur(e, nodeName) {
418
- this.state.selectedNode = null; // allow updates on this input
419
- this.props.onInputBlur && this.props.onInputBlur(nodeName);
420
- }
421
-
422
- sanitize(input) {
423
- const output = input.replace(/[^0-9]/g, '');
424
- return output;
425
- }
426
-
427
- toDigitPairStr(strValue) {
428
- return (strValue.length < 2) ? `0${strValue}` : strValue;
429
- }
430
-
431
- handleTimeCodeInput(e, nodeName) {
432
- const nodeIndex = this.nodeNames.indexOf(nodeName);
433
-
434
- switch (e.code) {
435
- case 'Enter': // do work before submitting input
436
- this.checkAndNormalizeAllInputs();
437
-
438
- this.props.onInput && this.props.onInput(e, nodeName, {
439
- hours: this.hours,
440
- minutes: this.minutes,
441
- seconds: this.seconds,
442
- frame: this.frame,
443
- });
444
-
445
- this[nodeName] = e.currentTarget.value;
446
- e.currentTarget.blur(); // done
447
- break;
448
- case 'Tab': // custom tab behaviour
449
- e.preventDefault();
450
-
451
- // tab trough inputs in a loop fashion
452
- if (!e.shiftKey) {
453
- if ((nodeIndex + 1) <= (this.nodeNames.length - 1)) {
454
- this.nodes[this.nodeNames[nodeIndex + 1]].focus(); // focus next
455
- } else {
456
- this.nodes[this.nodeNames[0]].focus(); // focus first
457
- }
458
- } else if ((nodeIndex - 1) >= 0) {
459
- this.nodes[this.nodeNames[nodeIndex - 1]].focus(); // focus previous
460
- } else {
461
- this.nodes[this.nodeNames[this.nodeNames.length - 1]].focus(); // focus last
462
- }
463
- break;
464
- }
465
- }
466
-
467
- handleTimeCodeInputOnKeyUp(e, nodeName) {
468
- /*
469
- cycle to next input if this one is done being populated,
470
- has to happen onkeyup, since the timecode is already populated
471
- with valid values most of the time, keydown would always trigger
472
- false positive length check.
473
-
474
- Also check if the input is a number (!isNaN),
475
- to prevent double-triggering Tab key or other logic
476
- */
477
- if (this.nodes[nodeName].value.length === 2 && !isNaN(e.key)) {
478
- const nodeIndex = this.nodeNames.indexOf(nodeName); // determine position in timecode
479
-
480
- if ((nodeIndex + 1) <= (this.nodeNames.length - 1)) { // check bounds
481
- this.nodes[this.nodeNames[nodeIndex + 1]].focus(); // focus next
482
- }
483
- }
484
- }
485
-
486
- checkAndNormalizeAllInputs() {
487
- this.nodeNames.forEach((nodeName) => {
488
- if (this.nodes[nodeName].value.length < 1) { // somebody left the input blank
489
- this.nodes[nodeName].value = '00';
490
- }
491
-
492
- // make sure displayed timecode stays consistent after input and before time-based updates
493
- this.nodes[nodeName].value = this.toDigitPairStr(this.sanitize(this.nodes[nodeName].value));
494
- });
495
- }
496
-
497
- // setters and getters
498
-
499
- set frame(val) {
500
- this.nodes.frame.value = this.toDigitPairStr(val);
501
- }
502
-
503
- get frame() {
504
- return parseInt(this.nodes.frame.value, 10);
505
- }
506
-
507
- set seconds(val) {
508
- this.nodes.seconds.value = this.toDigitPairStr(val);
509
- }
510
-
511
- get seconds() {
512
- return parseInt(this.nodes.seconds.value, 10);
513
- }
514
-
515
- set minutes(val) {
516
- this.nodes.minutes.value = this.toDigitPairStr(val);
517
- }
518
-
519
- get minutes() {
520
- return parseInt(this.nodes.minutes.value, 10);
521
- }
522
-
523
- set hours(val) {
524
- this.nodes.hours.value = this.toDigitPairStr(val);
525
- }
526
-
527
- get hours() {
528
- return parseInt(this.nodes.hours.value, 10);
529
- }
530
-
531
- getFormatted(key) {
532
- return this.toDigitPairStr(this[key].toString());
533
- }
534
-
535
- get smpte() {
536
- return {
537
- hours: this.hours,
538
- minutes: this.minutes,
539
- seconds: this.seconds,
540
- frame: this.frame,
541
- };
542
- }
543
-
544
- set spmte(o) {
545
- this.update(o);
546
- }
547
-
548
- get smpteString() {
549
- return `${this.getFormatted('hours')}:${this.getFormatted('minutes')}:${this.getFormatted('seconds')}:${this.getFormatted('frame')}`;
550
- }
551
-
552
- set external(bool) {
553
- if (bool) {
554
- this.container.classList.add('external');
555
- } else {
556
- this.container.classList.remove('external');
557
- }
558
- }
559
-
560
- get external() {
561
- return (this.container.classList.contains('external'));
562
- }
563
- }
564
-
565
- /* eslint-disable no-plusplus */
566
-
567
- class HelpOverlay {
568
- constructor(props) {
569
- this.props = props;
570
-
571
- this.state = {
572
- active: this.props.active || false,
573
- };
574
-
575
- this.container = document.createElement('div');
576
- this.container.className = 'vdt-help-overlay';
577
- this.container.innerHTML = '';
578
- this.container.onclick = () => {
579
- this.hide();
580
- };
581
-
582
- this.props.target && this.props.target.appendChild(this.container);
583
-
584
- this.draw();
585
- }
586
-
587
- draw() { // draws keyboard controls
588
- let tableRowMarkup = ''; // will insert a DOM string here
589
- const rows = [
590
- ['⎵', 'Play / Pause'],
591
- ['⇧', 'Increase volume '],
592
- ['⇩', 'Decrease volume'],
593
- ['⇨', 'Step frame forward'],
594
- ['⇦', 'Step frame backward'],
595
- ['M', 'Mute / Unmute'],
596
-
597
- ['J', 'Seek 10 seconds backward'],
598
- ['K', 'Play / Pause'],
599
- ['L', 'Seek 10 seconds forward'],
600
- ['C', 'Toggle subtitles'],
601
- ['N', 'Toggle settings menu'],
602
- ['H', 'Toggle this help overlay'],
603
- ];
604
-
605
-
606
- // split the data for 2 columns
607
- const halfpointIndex = Math.round(rows.length / 2);
608
- const columns = [
609
- rows.slice(0, halfpointIndex),
610
- rows.slice(halfpointIndex, rows.length),
611
- ];
612
-
613
- // table will niceley auto adjust td's according to length
614
- for (let i = 0; i < halfpointIndex; i++) {
615
- tableRowMarkup += `
616
- <tr>
617
- <td><div>${columns[0][i][0]}</div></td>
618
- <td>- ${columns[0][i][1]}</td>
619
-
620
- <td>${(columns[1][i]) ? `<div>${columns[1][i][0]}</div>` : ''}</td>
621
- <td>${(columns[1][i]) ? `- ${columns[1][i][1]}` : ''}</td>
622
- </tr>
623
- `;
624
- }
625
-
626
- /*
627
- insert table row markup in the table;
628
- note that there are two tables going
629
- on here - one is for vertically centering the
630
- content of help overlay (the other table)
631
- */
632
-
633
- this.container.innerHTML = `
634
- <table>
635
- <tbody>
636
- <tr>
637
- <td class="vdt-help-contents">
638
- <table><tbody>${tableRowMarkup}</tbody></table>
639
- </td>
640
- </tr>
641
- </tbody>
642
- </table>
643
- `;
644
- }
645
-
646
- show() {
647
- if (!this.state.active) {
648
- this.container.classList.add('active');
649
- this.state.active = true;
650
-
651
- mousetrap.bind('esc', () => { this.hide(); });
652
-
653
- this.props.onToggle && this.props.onToggle(true);
654
- }
655
- }
656
-
657
- hide() {
658
- if (this.state.active) {
659
- this.container.classList.remove('active');
660
- this.state.active = false;
661
-
662
- mousetrap.unbind('esc');
663
-
664
- this.props.onToggle && this.props.onToggle(false);
665
- }
666
- }
667
-
668
- toggle() {
669
- if (this.state.active) {
670
- this.hide();
671
- } else {
672
- this.show();
673
- }
674
- }
675
- }
676
-
677
- /* eslint-disable no-param-reassign */
678
-
679
-
680
- const constants$1 = Object.freeze({
681
- className: 'vdt-menu',
682
- menuClassName: 'menu',
683
- subLvlClassName: 'sub-level',
684
- menuItemClassName: 'menu-item',
685
- animationLength: 300,
686
- defaults: {
687
- bgColor: 'rgba(0,0,0,.8)',
688
- textColor: '#999',
689
- textColorDark: '#333',
690
- textColorLight: 'rgba(255,255,255)',
691
- bgColorLight: 'rgba(255,255,255,.2)',
692
- bgColorSelected: 'rgba(255,255,255,.1)',
693
- },
694
- });
695
-
696
-
697
- const CSS = () => `
698
- .${constants$1.menuClassName}{
699
- position: relative;
700
- height: 100%;
701
- width: 200%;
702
- // min-width: 200px;
703
- right: 0px;
704
- bottom: 0px;
705
- background: ${constants$1.defaults.bgColor};
706
- z-index: 1px;
707
- transform: translate3d(100%, 0px, 0px);
708
- transition: all 300ms ease-in-out;
709
- box-shadow: 0px 0px 30px rgb;
710
- -webkit-backdrop-filter: blur(10px);
711
- overflow: hidden;
712
- }
713
-
714
- .${constants$1.menuClassName} .level-1{
715
- float: left;
716
- }
717
-
718
- .${constants$1.menuClassName} div h3{
719
- padding: 16px 24px;
720
- font-size: 12px;
721
- border-bottom: 1px solid ${constants$1.defaults.bgColorLight}
722
- }
723
-
724
- .${constants$1.menuClassName} .level-2 h3{
725
- cursor: pointer;
726
- position: relative;
727
- }
728
- .${constants$1.menuClassName} .level-2 h3:before{
729
- content: '◂';
730
- margin-left: -12px;
731
- width: 12px;
732
- display: inline-block;
733
- position: absolute;
734
- margin-top: -1px;
735
- }
736
-
737
- .${constants$1.menuClassName} .level-1,
738
- .${constants$1.menuClassName} .level-2{
739
- transform: translate3d(0px, 0px, 0px);
740
- transition: all 300ms ease-in-out;
741
- width: 50%;
742
- height: 100%;
743
- float: left;
744
- }
745
-
746
- .${constants$1.menuClassName}.sub-level .level-1,
747
- .${constants$1.menuClassName}.sub-level .level-2{
748
- transform: translate3d(-100%, 0px, 0px);
749
- }
750
-
751
- .${constants$1.menuClassName} table{
752
- border-collapse: collapse;
753
- width: 100%;
754
- }
755
-
756
- .${constants$1.menuClassName}.open{
757
- transform: translate3d(0px, 0px, 0px);
758
- }
759
-
760
- .${constants$1.menuClassName} tr{
761
- cursor: pointer;
762
- height: 40px;
763
- }
764
-
765
- .${constants$1.menuClassName} tr.selected{
766
- background: ${constants$1.defaults.bgColorSelected};
767
- }
768
-
769
-
770
- .${constants$1.menuClassName} tr td{
771
- color: white;
772
- font-size: 15px;
773
- vertical-align: middle;
774
- }
775
-
776
-
777
- .${constants$1.menuClassName} tr:hover td{
778
- color: white;
779
- background: ${constants$1.defaults.bgColorLight};
780
- }
781
-
782
- .${constants$1.menuClassName} tr td.title{
783
- padding-top: 12px;
784
- padding-bottom: 12px;
785
- font-weight: 300;
786
- font-family: Helvetica, Arial, sans-serif;
787
- }
788
-
789
- .${constants$1.menuClassName} tr td.caret{
790
- font-size: 12px;
791
- }
792
-
793
- .${constants$1.menuClassName} tr td:first-child{
794
- padding-left: 24px;
795
- }
796
-
797
- .${constants$1.menuClassName} tr td:last-child{
798
- padding-right: 16px;
799
- }
800
-
801
- .${constants$1.menuClassName} tr.has-items td:last-child{
802
- text-align: right;
803
- }
804
-
805
- .${constants$1.menuClassName} tr.has-items td.subtitle{
806
- font-size: 12px;
807
- text-align: right;
808
- color: ${constants$1.defaults.textColorLight};
809
- }
810
- `;
811
-
812
- class MenuDisplayItem {
813
- constructor(props) {
814
- this.props = props;
815
-
816
- const classNames = [
817
- constants$1.menuItemClassName,
818
- this.props.items && 'has-items',
819
- this.props.selected && 'selected',
820
- ];
821
- this.container = document.createElement('tr');
822
- this.container.className = classNames.join(' ');
823
-
824
-
825
- this.props.target && this.props.target.appendChild(this.container);
826
-
827
- // bind click
828
- this.container.onclick = (event) => {
829
- this.props.onClick && this.props.onClick(event);
830
- };
831
-
832
- this.render();
833
- }
834
-
835
- render() {
836
- if (this.props.items) { // will transition to 2nd level
837
- this.container.innerHTML = `
838
- <td class="title"></td>
839
- <td class="subtitle"></td>
840
- <td class="caret">▷</td>
841
- `;
842
- } else {
843
- this.container.innerHTML = `
844
- <td class="title"></td>
845
- `;
846
- }
847
-
848
- const titleNode = this.container.getElementsByClassName('title')[0];
849
- const subtitleNode = (this.props.items) ? this.container.getElementsByClassName('subtitle')[0] : false;
850
-
851
- titleNode.textContent = this.props.title;
852
- subtitleNode && (subtitleNode.textContent = this.props.subtitle);
853
- }
854
- }
855
-
856
- class Menu {
857
- constructor(props) {
858
- this.props = props;
859
-
860
- this.container = document.createElement('div');
861
- this.container.className = `${constants$1.className}`;
862
-
863
- this.state = {
864
- opened: false,
865
- selectedItem: 0,
866
- items: [],
867
- subItems: [],
868
- };
869
-
870
- this.mount();
871
- this.draw();
872
- }
873
-
874
- mount() {
875
- // this.props.target && this.props.target.appendChild(this.container)
876
- if (this.props.target) {
877
- // container node, acts as an overlay in is stylable from outside
878
- this.container.style.position = 'absolute';
879
- this.container.style.bottom = this.container.style.right = '0px';
880
- this.container.style.height = '100%';
881
- this.container.style.overflow = 'hidden';
882
- this.container.style.width = '30%';
883
- this.container.style.minWidth = '250px';
884
- this.container.style.maxWidth = '320px';
885
-
886
- // shadow node encapsulates style and prevents css collisions
887
- this.shadow = this.container.attachShadow({ mode: 'open' });
888
- this.props.target && this.props.target.appendChild(this.container);
889
-
890
-
891
- // node for menu itself
892
- this.menuContainer = document.createElement('div');
893
- this.menuContainer.className = 'menu';
894
-
895
- this.shadow.appendChild(this.menuContainer);
896
-
897
- // attach styles
898
- this.style = document.createElement('style');
899
- this.style.type = 'text/css';
900
- this.style.innerHTML = CSS();
901
- this.shadow.appendChild(this.style);
902
- }
903
- }
904
-
905
- draw() { // first draw
906
- const mainDiv = document.createElement('div');
907
- mainDiv.className = 'level-1';
908
- this.mainLevel = document.createElement('table');
909
- mainDiv.innerHTML = '<h3>Settings</h3>';
910
- mainDiv.appendChild(this.mainLevel);
911
-
912
- const subDiv = document.createElement('div');
913
- subDiv.className = 'level-2';
914
- this.subLvlTitle = document.createElement('h3');
915
- this.subLvlTitle.onclick = () => { this.up(); };
916
- subDiv.appendChild(this.subLvlTitle);
917
- this.secondaryLevel = document.createElement('table');
918
- subDiv.appendChild(this.secondaryLevel);
919
-
920
-
921
- this.menuContainer.appendChild(mainDiv);
922
- this.menuContainer.appendChild(subDiv);
923
-
924
- this.props.items.forEach((menuItem) => {
925
- // get selected item title if such exists
926
- const activeTitle = menuItem.items[menuItem.getActiveIndex(menuItem.items)].title || '';
927
- const displayItem = new MenuDisplayItem({
928
- target: this.mainLevel,
929
- subtitle: activeTitle,
930
- onClick: () => {
931
- this.handleSupItemSelected(menuItem);
932
- },
933
- ...menuItem,
934
- });
935
-
936
- this.state.items.push(displayItem);
937
- });
938
- }
939
-
940
- open() {
941
- this.render();
942
-
943
- this.menuContainer.classList.add('open');
944
- this.props.onOpen && this.props.onOpen();
945
- this.state.opened = true;
946
-
947
- // close the menu when user clicks outside it
948
- if (!this.handleClicksWhenMenuOpened) {
949
- this.handleClicksWhenMenuOpened = function (event) {
950
- if (this.state.opened) {
951
- event.stopPropagation();
952
- const isClickInside = this.container.contains(event.target);
953
- if (!isClickInside) {
954
- this.close();
955
- }
956
- }
957
- }.bind(this);
958
- }
959
-
960
- this.handleClicksWhenMenuOpenedTimeout = setTimeout(() => {
961
- document.addEventListener('click', this.handleClicksWhenMenuOpened);
962
- mousetrap.bind('esc', this.close.bind(this)); // esc closes menu aswell
963
- }, constants$1.animationLength);
964
- }
965
-
966
- close() {
967
- this.menuContainer.classList.remove('open');
968
- this.state.opened = false;
969
-
970
- this.handleClicksWhenMenuOpenedTimeout && clearTimeout(this.handleClicksWhenMenuOpenedTimeout);
971
- document.removeEventListener('click', this.handleClicksWhenMenuOpened);
972
-
973
- this.props.onClose && this.props.onClose();
974
-
975
- mousetrap.unbind('esc');
976
- }
977
-
978
- toggle() {
979
- if (this.state.opened) {
980
- this.close();
981
- } else {
982
- this.open();
983
- }
984
- }
985
-
986
- up() { // go to level 1
987
- this.menuContainer.classList.remove(constants$1.subLvlClassName);
988
- this.render();
989
- }
990
-
991
- handleSupItemSelected(item) {
992
- this.subLvlTitle.textContent = item.title;
993
- this.renderSubItems(item.items, item.getActiveIndex(item.items));
994
- this.menuContainer.classList.add(constants$1.subLvlClassName);
995
- }
996
-
997
- renderSubItems(items, activeIndex = false) {
998
- this.secondaryLevel.innerHTML = '';
999
-
1000
- const subItemRefs = [];
1001
- items.forEach((menuItem, index) => {
1002
- const displayItem = new MenuDisplayItem({
1003
- target: this.secondaryLevel,
1004
- ...menuItem,
1005
- onClick: (event) => {
1006
- this.handleSubItemSelected(event, menuItem);
1007
- },
1008
- selected: (index === activeIndex), // mark selected
1009
- });
1010
-
1011
- subItemRefs.push(displayItem);
1012
- });
1013
-
1014
- this.state.subItems = subItemRefs;
1015
- }
1016
-
1017
- handleSubItemSelected(event, item) {
1018
- this.close();
1019
- item.onClick && item.onClick(event, item);
1020
- this.up();
1021
- }
1022
-
1023
- get opened() {
1024
- return this.state.opened;
1025
- }
1026
-
1027
- set opened(bool) {
1028
- this.state.opened = bool;
1029
- }
1030
-
1031
- render() {
1032
- // update selections at level 1
1033
- this.state.items.forEach((menuDisplayItem, index) => {
1034
- const { items } = this.props;
1035
- const activeIndex = items[index].getActiveIndex(items[index].items);
1036
- const activeTitle = items[index].items[activeIndex].title;
1037
- menuDisplayItem.props.subtitle = activeTitle;
1038
- menuDisplayItem.render();
1039
- });
1040
- }
1041
- }
1042
-
1043
- /* eslint-disable max-len */
1044
-
1045
- const constants$2 = Object.freeze({
1046
- className: 'vdt-seekbar',
1047
- defaults: {
1048
- bgColor: '#fafafa',
1049
- fillColor: '#f00',
1050
- regionColor: '#f6e58d', // yellow
1051
- markerWidth: 16,
1052
- regionAccentColor: '#00a8ff',
1053
- transitionTime: 300, // ms
1054
- },
1055
- markerTypes: {
1056
- in: 'in',
1057
- out: 'out',
1058
- },
1059
- });
1060
-
1061
- const CSS$1 = (bgColor, fillColor) => `
1062
- .${constants$2.className}{
1063
- user-select: none;
1064
- background: ${colors.light};
1065
- height: 6px;
1066
- width: 100%;
1067
- position: relative;
1068
- cursor: pointer;
1069
- z-index: 5;
1070
- }
1071
-
1072
- .${classNames.onDark}.${constants$2.className}{
1073
- background: ${colors.gray};
1074
- }
1075
-
1076
- .fill, .load-indicator{
1077
- height: 100%;
1078
- display: inline-block;
1079
- position: absolute;
1080
- left: 0px;
1081
- top: 0px;
1082
- background: ${fillColor};
1083
- transition: width ${constants$2.defaults.transitionTime}ms linear;
1084
- z-index: 2;
1085
- }
1086
-
1087
- .load-indicator{
1088
- background: #e1e1e1;
1089
- z-index: 1;
1090
- opacity: .2;
1091
- transition: none;
1092
- }
1093
-
1094
- .${classNames.onDark}.${constants$2.className} .load-indicator{
1095
- background: white;
1096
- }
1097
-
1098
- .${constants$2.className}.dragging .fill{
1099
- transition: none;
1100
- }
1101
-
1102
-
1103
- .marker, .region{
1104
- position: absolute;
1105
- top: 0px;
1106
- height: 100%;
1107
- background: cyan;
1108
- width: 1px;
1109
- display: inline-block;
1110
- z-index: 4;
1111
- }
1112
-
1113
- .marker{
1114
- transition: opacity ${constants$2.defaults.transitionTime}ms linear;
1115
- }
1116
-
1117
- .marker{ display: none; }
1118
- .marker.active{ display: initial; }
1119
-
1120
- .marker .handle{
1121
- position: absolute;
1122
- bottom: 10px; right: 0px;
1123
- width: ${constants$2.defaults.markerWidth}px;
1124
- height: 28px;
1125
- background: #999;
1126
- border-radius: 3px;
1127
- cursor: col-resize;
1128
- z-index: 3;
1129
- box-sizing: border-box;
1130
- }
1131
-
1132
- .marker.regional .handle{
1133
- background: ${constants$2.defaults.regionAccentColor};
1134
- box-shadow: -2px 1px 1px rgba(0,0,0,.1);
1135
- }
1136
-
1137
- .marker.regional.in .handle{
1138
- box-shadow: 2px 1px 1px rgba(0,0,0,.1);
1139
- }
1140
-
1141
- .marker.in .handle{
1142
- border-bottom-left-radius: 40px;
1143
- }
1144
-
1145
- .marker.out .handle{
1146
- left: 1px;
1147
- right: initial;
1148
- border-bottom-right-radius: 20px;
1149
- }
1150
-
1151
- .handle:active{
1152
- background: ${constants$2.defaults.regionColor};
1153
- }
1154
-
1155
- .region{
1156
- position: absolute;
1157
- width: auto;
1158
- background: ${constants$2.defaults.regionColor};
1159
- opacity: .7;
1160
- }
1161
-
1162
- .region.nav{
1163
- background: transparent;
1164
- opacity: 1;
1165
- border-left: 1px solid cyan;
1166
- }
1167
-
1168
- .region.nav .bubble,
1169
- .region.nav:after{
1170
- content: " ";
1171
- height: 16px;
1172
- bottom: 10px;
1173
- width: 16px;
1174
- border-radius: 16px;
1175
- background: ${constants$2.defaults.regionAccentColor};
1176
- position: absolute;
1177
- margin-left: -8px;
1178
- transition: transform 200ms ease-out;
1179
- transform: translate3d(0px, 0px, 0px);
1180
- transform-origin: bottom center;
1181
- }
1182
-
1183
- .region.nav .popout{
1184
- width: 160px;
1185
- z-index: 4;
1186
- background: rgba(28, 28, 28, .7);
1187
- color: white;
1188
- font-size: 14px;
1189
- min-height: 100px;
1190
- bottom: 16px;
1191
- left: -80px;
1192
- position: absolute;
1193
- border-radius: 8px;
1194
- -webkit-backdrop-filter: blur(10px);
1195
- font-family: Arial, Helvetica, sans-serif;
1196
- overflow: hidden;
1197
- opacity: 0;
1198
- visibility: hidden;
1199
- }
1200
-
1201
- .region.nav.right-aligned .popout{
1202
- right: 0px;
1203
- left: initial;
1204
- }
1205
-
1206
- .region.nav.left-aligned .popout{
1207
- left: -32px;
1208
- }
1209
-
1210
- .region.nav .popout .title{
1211
- padding: 4px 14px;
1212
- text-align: center;
1213
- color: black;
1214
- background: white;
1215
- }
1216
-
1217
- .region.nav .popout .annotation{
1218
- padding: 8px 12px;
1219
- font-weight: light;
1220
- color: rgba(255,255,255,.9);
1221
- }
1222
-
1223
- .region.nav:after{
1224
- width: 1px;
1225
- left: 8px;
1226
- height: 10px;
1227
- bottom: 0px;
1228
- background: #00a8ff;
1229
- }
1230
-
1231
- .region.nav:hover{
1232
- background: rgba(28, 28, 28, .5);
1233
- }
1234
-
1235
- .region.nav.has-popout:hover .bubble{
1236
- background: rgba(28, 28, 28, .8);
1237
- opacity: 1;
1238
- z-index: 3;
1239
- }
1240
-
1241
- .region.nav:hover:after,
1242
- .region.nav:hover:before{
1243
- background: rgba(28, 28, 28, .8);
1244
- }
1245
-
1246
- .region.nav:hover .popout{
1247
- opacity: 1;
1248
- visibility: visible;
1249
- }
1250
-
1251
- .${constants$2.className}.dragging .bubble{
1252
- transform: scale3d(.75,.75,.75);
1253
- }
1254
-
1255
- .${constants$2.className}.hide-markers .marker{
1256
- opacity: 0;
1257
- }
1258
-
1259
- `;
1260
-
1261
- class Region {
1262
- constructor(props) {
1263
- const regionDefaultProps = {
1264
- in: null,
1265
- out: null,
1266
- title: '',
1267
- annotation: '',
1268
- el: null, // can store a
1269
- };
1270
-
1271
- Object.assign(this, regionDefaultProps, props); // apply props
1272
- }
1273
- }
1274
-
1275
- class SeekBar {
1276
- constructor(props) {
1277
- this.props = props;
1278
- reactiveSetters.apply(this); // ability to handle new props via setters
1279
-
1280
- this.container = document.createElement('div');
1281
- this.container.className = `${constants$2.className}-wrapper`;
1282
-
1283
- this.state = {
1284
- in: null, // in seconds from 0
1285
- out: null,
1286
- markersEnabled: (this.props.markersEnabled === undefined) ? true : this.props.markersEnabled,
1287
- regions: [], // array with regions providing {in: 0, out: 12} and possibly more
1288
- dragging: false,
1289
- };
1290
-
1291
- this.markers = {};
1292
-
1293
- if (this.props.target) {
1294
- this.shadow = this.container.attachShadow({ mode: 'open' });
1295
- this.props.target.appendChild(this.container);
1296
- // attach styles
1297
- this.style = document.createElement('style');
1298
- this.style.type = 'text/css';
1299
-
1300
- // colors from props
1301
- const bgColor = (this.props.bgColor) ? this.props.bgColor : constants$2.defaults.bgColor;
1302
- const fillColor = (this.props.fillColor) ? this.props.fillColor : constants$2.defaults.fillColor;
1303
-
1304
- this.style.innerHTML = CSS$1(sanitize(bgColor), sanitize(fillColor));
1305
- this.shadow.appendChild(this.style);
1306
- }
1307
-
1308
- this.bindEvents();
1309
- this.draw();
1310
- this.bindHotkeys();
1311
-
1312
- /*
1313
- run this for "in" and "out" points set in constructor
1314
- when the player metadata is ready (duration data is there)
1315
- */
1316
- const { player } = this.props;
1317
- if (player) {
1318
- player.videojs.on('loadedmetadata', () => {
1319
- this.handleNewProps(this.props);
1320
- });
1321
- }
1322
- }
1323
-
1324
- /*
1325
- Video.js seekbar works a bit differently - it
1326
- sets an interval and checks the playback position
1327
- very frequently, could do it that way -
1328
- then could use requestAnimationFrame and
1329
- get a smoother animation at more expense
1330
- */
1331
- bindEvents() { // bind to vdt player instance
1332
- if (this.props.player) {
1333
- const { player } = this.props;
1334
- player.videojs.on('timeupdate', () => {
1335
- const progressFraction = player.videojs.currentTime() / player.videojs.duration();
1336
- this.fill.style.width = `${100 * progressFraction}%`;
1337
- });
1338
-
1339
- player.videojs.on('progress', () => {
1340
- this.percentLoaded = player.percentLoaded;
1341
- });
1342
- }
1343
- }
1344
-
1345
- bindHotkeys() {
1346
- this.bindings = [];
1347
-
1348
- // bindings related to player
1349
- if (this.props.player) {
1350
- const { player } = this.props;
1351
- this.bindings = [
1352
- {
1353
- key: ['i'],
1354
- method: () => {
1355
- if (this.markersEnabled) this.in = player.videojs.currentTime();
1356
- },
1357
- },
1358
- {
1359
- key: ['o'],
1360
- method: () => {
1361
- if (this.markersEnabled) this.out = player.videojs.currentTime();
1362
- },
1363
- },
1364
- {
1365
- key: ['shift+i'],
1366
- method: () => {
1367
- if (this.markersEnabled) this.navigateToIn();
1368
- },
1369
- },
1370
- {
1371
- key: ['shift+o'],
1372
- method: () => {
1373
- if (this.markersEnabled) this.navigateToOut();
1374
- },
1375
- },
1376
- ];
1377
- }
1378
-
1379
- if (this.props.player) {
1380
- const { player } = this.props;
1381
- mousetrap.bind('i', () => {
1382
- if (this.markersEnabled) this.in = player.videojs.currentTime();
1383
- });
1384
-
1385
- mousetrap.bind('o', () => {
1386
- if (this.markersEnabled) this.out = player.videojs.currentTime();
1387
- });
1388
-
1389
- mousetrap.bind('shift+i', () => {
1390
- if (this.markersEnabled) this.navigateToIn();
1391
- });
1392
-
1393
- mousetrap.bind('shift+o', () => {
1394
- if (this.markersEnabled) this.navigateToOut();
1395
- });
1396
- }
1397
-
1398
- // bind
1399
- this.bindings.forEach((binding) => {
1400
- mousetrap.bind(binding.key, (e) => {
1401
- binding.method(e);
1402
- });
1403
- });
1404
- }
1405
-
1406
- unbindHotKeys() {
1407
- this.bindings && this.bindings.forEach((binding) => {
1408
- mousetrap.unbind(binding.key);
1409
- });
1410
- }
1411
-
1412
- handleSeek(event) {
1413
- // check if target is a region
1414
- if (this.props.player) {
1415
- const { player } = this.props;
1416
- if (this.state.regions) {
1417
- let clickedOnRegion = false;
1418
- let preventSeek = false; // prevent seek from happening
1419
- this.state.regions.forEach((region) => {
1420
- if (region.el.contains(event.target)) {
1421
- // player.seekTo(region.in);
1422
-
1423
- // callback for clicking on a region, if callback returns false, no seek happens
1424
- // TODO - make a convention of returning true / false to break from seek
1425
- if (this.props.onRegionClick) {
1426
- preventSeek = (this.props.onRegionClick(region));
1427
- if (!preventSeek) {
1428
- player.seekTo(region.in);
1429
- }
1430
- } else {
1431
- player.seekTo(region.in);
1432
- }
1433
-
1434
- clickedOnRegion = true;
1435
- }
1436
- });
1437
-
1438
- /*
1439
- exit from method, TODO - implement foreach
1440
- for breaking more efficiently
1441
- */
1442
-
1443
- if (clickedOnRegion) return;
1444
- }
1445
-
1446
- const seekFractionX = (event.clientX - this.bar.getBoundingClientRect().left) / this.bar.offsetWidth;
1447
- // -> 0.34732
1448
-
1449
- const timeToSeek = seekFractionX * player.videojs.duration();
1450
- // -> something like 32.2323 (in seconds)
1451
- player.seekTo(timeToSeek);
1452
- } // TODO - make a callback for more generic use case (non-vdt player)
1453
- }
1454
-
1455
- handleSeekDrag(mouseDownEvent) {
1456
- const barOffset = this.bar.getBoundingClientRect().left; // px from page left side
1457
- const barWidth = this.bar.getBoundingClientRect().width; // seek bar width
1458
- const duration = this.props.player.videojs.duration(); // in secondsā
1459
- let left;
1460
-
1461
- const drag = (e) => {
1462
- left = e.clientX - barOffset;
1463
- const pxDragged = Math.abs(mouseDownEvent.clientX - e.clientX);
1464
-
1465
- if (left >= 0 && left < barWidth && pxDragged > 1) { // prevent out-of-bounds
1466
- this.dragging = true;
1467
- const fractionFromStart = parseFloat(left) / barWidth;
1468
- const seconds = fractionFromStart * duration;
1469
- this.props.player.videojs.currentTime(seconds);
1470
- }
1471
-
1472
- e.preventDefault();
1473
- };
1474
-
1475
- const stopDrag = (e) => {
1476
- this.dragging = false;
1477
-
1478
- // remove listeners
1479
- document.removeEventListener('mousemove', drag, false);
1480
- document.removeEventListener('mouseup', stopDrag, false);
1481
-
1482
- const fractionFromStart = parseFloat(left) / barWidth;
1483
- const seconds = fractionFromStart * duration;
1484
- this.props.player.videojs.currentTime(seconds);
1485
-
1486
- e.stopPropagation();
1487
- };
1488
-
1489
- // add listeners
1490
- document.addEventListener('mousemove', drag, false);
1491
- document.addEventListener('mouseup', stopDrag, false);
1492
- }
1493
-
1494
- draw() {
1495
- this.bar = document.createElement('div');
1496
- this.bar.className = constants$2.className;
1497
- this.bar.onclick = (e) => {
1498
- if (!e.target.classList.contains('marker') && !e.target.classList.contains('handle')) {
1499
- this.handleSeek(e);
1500
- }
1501
- };
1502
-
1503
- // handle drag
1504
-
1505
- this.bar.addEventListener('mousedown', (e) => {
1506
- e.preventDefault(); // prevents selection cursor
1507
-
1508
- if (!this.dragging) { // don't attach listeners if something is already dragging
1509
- this.handleSeekDrag(e);
1510
- }
1511
- }, false);
1512
-
1513
- this.fill = document.createElement('div');
1514
- this.fill.className = 'fill';
1515
-
1516
- this.loadIndicator = document.createElement('div');
1517
- this.loadIndicator.className = 'load-indicator';
1518
-
1519
- this.region = document.createElement('div');
1520
- this.region.className = 'region';
1521
-
1522
- this.bar.appendChild(this.fill);
1523
- this.bar.appendChild(this.loadIndicator);
1524
- this.bar.appendChild(this.region);
1525
- this.shadow.appendChild(this.bar);
1526
-
1527
- this.drawMarkers();
1528
- }
1529
-
1530
- drawMarkers() { // TODO - add touch events
1531
- if (this.markersEnabled) {
1532
- const markerIn = this.markers[constants$2.markerTypes.in] = document.createElement('div');
1533
- const markerOut = this.markers[constants$2.markerTypes.out] = document.createElement('div');
1534
- markerIn.className = `marker ${constants$2.markerTypes.in}`;
1535
- markerOut.className = `marker ${constants$2.markerTypes.out}`;
1536
-
1537
- const handleIn = document.createElement('div');
1538
- const handleOut = document.createElement('div');
1539
- handleIn.className = handleOut.className = 'handle';
1540
-
1541
- handleIn.addEventListener('mousedown', (e) => {
1542
- e.preventDefault(); // prevents selection cursor
1543
- e.stopPropagation(); // prevent triggering seek bar drag
1544
-
1545
- this.handleMarkerDrag(markerIn, constants$2.markerTypes.in, e);
1546
- }, false);
1547
-
1548
- handleOut.addEventListener('mousedown', (e) => {
1549
- e.preventDefault(); // prevents selection cursor
1550
- e.stopPropagation(); // prevent triggering seek bar drag
1551
-
1552
- this.handleMarkerDrag(markerOut, constants$2.markerTypes.out, e);
1553
- }, false);
1554
-
1555
- markerIn.appendChild(handleIn);
1556
- markerOut.appendChild(handleOut);
1557
-
1558
- this.bar.appendChild(markerIn);
1559
- this.bar.appendChild(markerOut);
1560
- }
1561
- }
1562
-
1563
- updateMarkers(marker) {
1564
- this.markers[marker.type].style.left = `${marker.value * 100}%`;
1565
- this.markers[marker.type].classList.add('active');
1566
-
1567
- // check if marker mathes a region and add a classname to it
1568
- if (this.state.regions) {
1569
- // check if any region contains in / out matching value
1570
- const matchingRegions = [];
1571
- const isRegional = (matchingRegions.length > 0);
1572
- if (isRegional) {
1573
- this.markers[marker.type].classList.add('regional');
1574
- } else {
1575
- this.markers[marker.type].classList.remove('regional');
1576
- }
1577
- }
1578
-
1579
- this.drawInAndOutRegion();
1580
- }
1581
-
1582
- drawInAndOutRegion() { // triggered after drawMarkers if in + out is set
1583
- if (this.state.in !== null && this.state.out !== null) {
1584
- this.region.style.left = this.markers[constants$2.markerTypes.in].style.left;
1585
- this.region.style.right = `${100 - parseFloat(this.markers[constants$2.markerTypes.out].style.left.replace('%', ''))}%`;
1586
- }
1587
- }
1588
-
1589
- /*
1590
- TODO - handle touch events,
1591
- but find out if there is a mobile use
1592
- case before that
1593
- */
1594
- handleMarkerDrag(markerNode, type, mouseDownEvent) { // marker node, marker type (constants.markerTypes)
1595
- const barOffset = this.bar.getBoundingClientRect().left; // px from page left side
1596
- const barWidth = this.bar.getBoundingClientRect().width; // seek bar width
1597
- const duration = this.props.player.videojs.duration(); // in secondsā
1598
- let dragHappened = false;
1599
-
1600
- const drag = (e) => {
1601
- const pxDragged = Math.abs(mouseDownEvent.clientX - e.clientX);
1602
- const markerOffset = (type === constants$2.markerTypes.in) ? (constants$2.defaults.markerWidth / 2) : -(constants$2.defaults.markerWidth / 2);
1603
- const left = e.clientX - barOffset + markerOffset;
1604
-
1605
- if (left >= 0 && left < barWidth && pxDragged > 1) { // prevent out-of-bounds
1606
- dragHappened = this.dragging = true;
1607
- markerNode.style.left = `${left}px`;
1608
- markerNode.classList.add('active');
1609
-
1610
- const fractionFromStart = parseFloat(markerNode.getBoundingClientRect().left - barOffset) / barWidth;
1611
- const seconds = fractionFromStart * duration;
1612
- this.props.player.videojs.currentTime(seconds);
1613
- }
1614
-
1615
- e.preventDefault();
1616
- };
1617
-
1618
- const stopDrag = (e) => {
1619
- this.dragging = false;
1620
-
1621
- // remove listeners
1622
- document.removeEventListener('mousemove', drag, false);
1623
- document.removeEventListener('mouseup', stopDrag, false);
1624
-
1625
- const fractionFromStart = parseFloat(markerNode.getBoundingClientRect().left - barOffset) / barWidth;
1626
- const seconds = fractionFromStart * duration;
1627
- this[type] = seconds;
1628
- this.props.player.videojs.currentTime(seconds);
1629
-
1630
- /*
1631
- if drag did not take place,
1632
- user just clicked the marker -
1633
- jump to the time of marker in player
1634
- */
1635
- if (!dragHappened) {
1636
- if (type === constants$2.markerTypes.in) {
1637
- this.navigateToIn();
1638
- } else {
1639
- this.navigateToOut();
1640
- }
1641
- }
1642
-
1643
- this.props.onDragFinish && this.props.onDragFinish({ type, seconds });
1644
- e.stopPropagation();
1645
- };
1646
-
1647
- // add listeners
1648
- document.addEventListener('mousemove', drag, false);
1649
- document.addEventListener('mouseup', stopDrag, false);
1650
- }
1651
-
1652
- /*
1653
- hide dead markers (for state parity) or ones that
1654
- make no sense (like out point before in point)
1655
- */
1656
- cleanupMarkers() {
1657
- let removedMarker = false;
1658
- if (this.state.in === null) {
1659
- this.markers[constants$2.markerTypes.in].classList.remove('active');
1660
- removedMarker = true;
1661
- }
1662
-
1663
- if (this.state.out === null) {
1664
- this.markers[constants$2.markerTypes.out].classList.remove('active');
1665
- removedMarker = true;
1666
- }
1667
-
1668
- if (removedMarker) { // if a marker was removed, region no longer is valid
1669
- this.region.style.left = 'initial';
1670
- this.region.style.right = 'initial';
1671
- }
1672
- }
1673
-
1674
- clearMarkers() {
1675
- this.in = null;
1676
- this.out = null;
1677
- }
1678
-
1679
- // utilizes player instance from props
1680
- setInFromCurrent() {
1681
- this.in = this.props.player.videojs.currentTime();
1682
- }
1683
-
1684
- setOutFromCurrent() {
1685
- this.out = this.props.player.videojs.currentTime();
1686
- }
1687
-
1688
- navigateToIn() {
1689
- const { player } = this.props;
1690
- player.seekTo(this.in);
1691
- }
1692
-
1693
- navigateToOut() {
1694
- const { player } = this.props;
1695
- player.seekTo(this.out);
1696
- }
1697
-
1698
- // setters and getters
1699
- set in(val) {
1700
- if (!this.state.markersEnabled) return;
1701
-
1702
- this.state.in = val;
1703
-
1704
- if (val !== null && val !== undefined) {
1705
- const { player } = this.props;
1706
- this.updateMarkers({
1707
- type: constants$2.markerTypes.in,
1708
- value: val / player.videojs.duration(),
1709
- });
1710
-
1711
- // make sure in point is not set after out point
1712
- if (this.state.out !== null && val > this.state.out) {
1713
- this.out = null;
1714
- }
1715
-
1716
- this.props.onIn && this.props.onIn(this.state.in);
1717
- } else {
1718
- this.cleanupMarkers();
1719
- }
1720
- }
1721
-
1722
- get in() {
1723
- return this.state.in;
1724
- }
1725
-
1726
- set out(val) {
1727
- if (!this.state.markersEnabled) return;
1728
-
1729
- this.state.out = val;
1730
-
1731
- if (val !== null && val !== undefined) {
1732
- const { player } = this.props;
1733
- this.updateMarkers({
1734
- type: constants$2.markerTypes.out,
1735
- value: val / player.videojs.duration(),
1736
- });
1737
-
1738
- // make sure out point is not set before in point
1739
- if (this.state.in != null && val < this.state.in) {
1740
- this.in = null;
1741
- }
1742
-
1743
- this.props.onOut && this.props.onOut(this.state.out);
1744
- } else {
1745
- this.cleanupMarkers();
1746
- }
1747
- }
1748
-
1749
- get out() {
1750
- return this.state.out;
1751
- }
1752
-
1753
- get markersEnabled() {
1754
- return this.state.markersEnabled;
1755
- }
1756
-
1757
- set markersEnabled(bool) {
1758
- if (typeof bool === 'boolean') {
1759
- if (!bool) {
1760
- this.clearMarkers();
1761
- this.state.markersEnabled = false;
1762
- } else {
1763
- !this.markersEnabled && this.drawMarkers();
1764
- this.state.markersEnabled = true;
1765
- }
1766
- }
1767
- }
1768
-
1769
- set regions(regionArray) { // different than i / o markers
1770
- if (this.state.regions.length < 1) { // first time add
1771
- regionArray && regionArray.forEach((region) => {
1772
- this.addRegion(region);
1773
- });
1774
- } else { // state redraw
1775
- this.state.regions.forEach((region, index) => {
1776
- this.removeRegion(index);
1777
- });
1778
-
1779
- this.state.regions = [];
1780
- this.regions = regionArray; // run again
1781
- }
1782
- }
1783
-
1784
- get regions() {
1785
- return this.state.regions;
1786
- }
1787
-
1788
- set dragging(bool) {
1789
- // update only if worth updating
1790
- if (bool && !this.state.dragging) {
1791
- this.state.dragging = true;
1792
- this.bar.classList.add('dragging');
1793
- } else if (!bool && this.state.dragging) {
1794
- this.state.dragging = false;
1795
- this.bar.classList.remove('dragging');
1796
- }
1797
- }
1798
-
1799
- get dragging() {
1800
- return this.state.dragging;
1801
- }
1802
-
1803
- addRegion(regionObj) {
1804
- if (regionObj.constructor.name === Region.name) {
1805
- this.state.regions.push(regionObj);
1806
- this.drawRegion(regionObj);
1807
- } else { // try to construct region if has accepted properties
1808
- this.addRegion(new Region(regionObj));
1809
- }
1810
- }
1811
-
1812
- removeRegion(param) {
1813
- if (typeof param === typeof Region) ; else { // it's an index
1814
- this.state.regions[param].el.remove();
1815
- this.state.regions[param] = null;
1816
- delete this.state.regions[param];
1817
- }
1818
- }
1819
-
1820
- drawRegion(region) {
1821
- const duration = this.props.player.videojs.duration(); // seconds
1822
- const inPoint = region.in;
1823
-
1824
- const leftPercentage = (inPoint / duration) * 100;
1825
- const rightPercentage = ((duration - region.out) / duration) * 100;
1826
-
1827
- const regionEl = document.createElement('div');
1828
- regionEl.className = 'region nav ';
1829
- regionEl.innerHTML = '<div class="bubble"></div>';
1830
-
1831
- if (region.title && region.annotation) {
1832
- let alignmentClassName = '';
1833
- if (leftPercentage > 70) alignmentClassName = 'right-aligned';
1834
- if (leftPercentage < 30) alignmentClassName = 'left-aligned';
1835
- regionEl.className += ` has-popout ${alignmentClassName}`;
1836
-
1837
- const popout = document.createElement('div');
1838
-
1839
- popout.className = 'popout';
1840
- popout.innerHTML = `
1841
- <div class="title">
1842
- ${sanitize(region.title)}
1843
- </div>
1844
- <div class="annotation">
1845
- ${truncate(sanitize(region.annotation), 72)}
1846
- </div>
1847
- `;
1848
-
1849
- regionEl.appendChild(popout);
1850
- }
1851
-
1852
- regionEl.style.left = `${leftPercentage}%`;
1853
- regionEl.style.right = `${rightPercentage}%`;
1854
-
1855
- region.el = regionEl;
1856
-
1857
- this.bar.appendChild(regionEl);
1858
- }
1859
-
1860
- set onDark(bool) {
1861
- if (bool) {
1862
- this.bar.classList.add(classNames.onDark);
1863
- } else {
1864
- this.bar.classList.remove(classNames.onDark);
1865
- }
1866
- }
1867
-
1868
- get onDark() {
1869
- return this.container.classList.contains(classNames.onDark);
1870
- }
1871
-
1872
- set percentLoaded(val) {
1873
- this.loadIndicator.style.width = `${val}%`;
1874
- }
1875
-
1876
- destroy() {
1877
- this.unbindHotKeys();
1878
- }
1879
-
1880
- toggleMarkers() {
1881
- if (this.bar.classList.contains('hide-markers')) {
1882
- this.showMarkers();
1883
- } else {
1884
- this.hideMarkers();
1885
- }
1886
- }
1887
-
1888
- hideMarkers() {
1889
- this.bar.classList.add('hide-markers');
1890
- }
1891
-
1892
- showMarkers() {
1893
- this.bar.classList.remove('hide-markers');
1894
- }
1895
- }
1896
-
1897
- SeekBar.Region = Region;
1898
-
1899
- // TODO - move this to @vidispine/vdt-js when finalized and use as dependency
1900
-
1901
- class VideoTime {
1902
- constructor(props) {
1903
- if (props.frameRate === undefined) {
1904
- throw new Error('No "frameRate" option provided, refer to docs.');
1905
- } else {
1906
- this.props = props;
1907
- }
1908
- }
1909
-
1910
- secondsToFramesFromStart(seconds) {
1911
- let frame = seconds * this.props.frameRate;
1912
- frame = Math.round(frame);
1913
- return frame; // we're on this frame, could be for example 23.45
1914
- }
1915
-
1916
- currentTimeToFramesFromStart(currentTime) {
1917
- return this.secondsToFramesFromStart(currentTime);
1918
- }
1919
-
1920
- secondsToFrame(seconds) {
1921
- let frame = this.secondsToFramesFromStart(seconds) % this.props.frameRate;
1922
- frame = Math.round(frame);
1923
- return frame;
1924
- }
1925
-
1926
- currentFrame(currentTime) {
1927
- return Math.round(this.secondsToFrame(currentTime));
1928
- }
1929
-
1930
- frameToCurrentTimeFromStart(frame) {
1931
- return frame / this.props.frameRate; // in seconds from start
1932
- }
1933
-
1934
- smpteToCurrentTimeFromStart(values) {
1935
- return (
1936
- values.hours * 3600
1937
- + values.minutes * 60
1938
- + values.seconds
1939
- + values.frame / this.props.frameRate
1940
- );
1941
- }
1942
-
1943
- secondsFromStartToSMPTE(secondsFromStart) {
1944
- let secondsFS = secondsFromStart;
1945
- const hours = Math.floor(secondsFS / 3600);
1946
- const minutesWithoutHours = secondsFS / 60 - 60 * hours;
1947
- // eslint-disable-next-line no-param-reassign
1948
- secondsFS %= 3600;
1949
-
1950
- const minutes = Math.floor(minutesWithoutHours);
1951
- const seconds = Math.floor(secondsFS % 60);
1952
- const frame = this.secondsToFrame(secondsFS);
1953
-
1954
- return {
1955
- hours,
1956
- minutes,
1957
- seconds,
1958
- frame,
1959
- };
1960
- }
1961
- }
1962
-
1963
- /* eslint-disable no-param-reassign */
1964
-
1965
- const correctMimeTypes = {
1966
- 'audio/x-aac': 'audio/aac',
1967
- };
1968
- const sanitizeMimeType = (mimeType) => correctMimeTypes[mimeType] || mimeType;
1969
-
1970
- class Player {
1971
- constructor(props) {
1972
- this.props = props; // save props
1973
- reactiveSetters.apply(this); // ability to handle new props via setters
1974
-
1975
- const classNames = [ // add conditional css classes here for the whole container
1976
- 'video-js',
1977
- 'vjs-big-play-centered',
1978
- (this.props.posterUrl) ? constants.classNames.poster : '',
1979
- (this.props.subtitles && this.props.subtitles.length > 0) ? constants.classNames.subtitles : '',
1980
- 'init', // make player height 0 while no metadata for video size available with css
1981
- ];
1982
-
1983
- this.externalTimecodes = {
1984
- // either one of these can hold an array of TimeCodeDisplay instances
1985
- duration: [],
1986
- playback: [],
1987
- };
1988
-
1989
- this.timeConverter = new VideoTime({
1990
- frameRate: this.props.frameRate || constants.defaults.frameRate,
1991
- });
1992
-
1993
- /*
1994
- video.js requires a html video to initialize
1995
- */
1996
-
1997
- this.videoEl = document.createElement('video');
1998
- this.videoEl.crossOrigin = this.props.crossOrigin;
1999
- this.videoEl.id = 'vdt-video';
2000
- this.videoEl.className = classNames.join(' ');
2001
- this.videoEl.controls = true; // remove this line for no-controls videojs player
2002
-
2003
- // attach sources, sort mp4 on top of source list
2004
- if (this.props.sources) {
2005
- // sort mp4 at the top
2006
- if (this.props.sources.length > 1) {
2007
- this.props.sources.sort((a) => ((a.type.indexOf('mp4') > 0) ? 1 : -1));
2008
- }
2009
-
2010
- // construct <source> els
2011
- this.props.sources.forEach((source) => {
2012
- const aSource = document.createElement('source');
2013
- aSource.src = source.src;
2014
- aSource.type = sanitizeMimeType(source.type);
2015
- this.videoEl.appendChild(aSource);
2016
- });
2017
- }// handle else with err later
2018
-
2019
- this.videoEl.onloadedmetadata = () => { // read-only duration timecode
2020
- if (this.durationTimeCode) {
2021
- this.setDurationTimeCode();
2022
- }
2023
-
2024
- this.videojs.el_.classList.remove('init');
2025
- };
2026
-
2027
- // if video has not loaded by now, show the player w/ errors
2028
- setTimeout(() => {
2029
- if (this.videojs.el_) {
2030
- this.videojs.el_.classList.remove('init');
2031
- }
2032
- }, 2000);
2033
-
2034
- // attach subtitle tracks (these should come in last in the <video /> node)
2035
- if (this.props.subtitles) {
2036
- this.props.subtitles.forEach((sub) => {
2037
- const subtitleTag = document.createElement('track');
2038
- subtitleTag.kind = 'subtitles';
2039
- subtitleTag.srclang = sub.lang;
2040
- subtitleTag.label = sub.label;
2041
- // subtitleTag.default = (index === 0) //can use this later to set a default track
2042
-
2043
- if (typeof sub.src === 'string') {
2044
- subtitleTag.src = sub.src; // external file
2045
- } else {
2046
- subtitleTag.src = window.URL.createObjectURL(sub.src); // file url from blob
2047
- }
2048
-
2049
- this.videoEl.appendChild(subtitleTag);
2050
- });
2051
- }
2052
-
2053
- // attach to dom and construct videojs player
2054
- if (this.props.target) {
2055
- this.props.target && this.props.target.appendChild(this.videoEl);
2056
- }
2057
-
2058
- const showDefaultTimeDisplay = (this.props.timecode !== true);
2059
- const controlsEnabled = (this.props.controls !== false);
2060
- const options = this.props.options || {};
2061
-
2062
- this.videojs = new videojs('vdt-video', {
2063
- responsive: true,
2064
- fluid: true,
2065
- controls: controlsEnabled,
2066
- alwaysCaptureHotkeys: false,
2067
- controlBar: {
2068
- RemainingTimeDisplay: false,
2069
- timeDivider: showDefaultTimeDisplay, // relates to built-in time display
2070
- durationDisplay: showDefaultTimeDisplay,
2071
- currentTimeDisplay: showDefaultTimeDisplay,
2072
- },
2073
- inactivityTimeout: (this.audioOnly) ? constants.defaults.audioInactivityTimeout : constants.defaults.inactivityTimeout,
2074
- preload: 'auto',
2075
- captionsButton: false,
2076
- subsCapsButton: false,
2077
- subtitlesButton: false,
2078
- textTrackSettings: false,
2079
- html5: {
2080
- nativeTextTracks: false,
2081
- },
2082
- languages: {
2083
- en: {
2084
- 'captions off': 'off',
2085
- 'subtitles off': 'off',
2086
- },
2087
- },
2088
- ...options,
2089
- });
2090
-
2091
- // set the poster
2092
- this.props.posterUrl && this.videojs.poster(this.props.posterUrl);
2093
-
2094
- // set other props that are passed
2095
- this.handleNewProps(this.props);
2096
-
2097
- // lifecycle
2098
- this.videojs.ready(() => {
2099
- this.bindHotkeys();
2100
- this.bindEvents();
2101
- this.props.onReady && this.props.onReady(this.videojs);
2102
-
2103
- this.restoreSettings();
2104
- this.setupTimeCode();
2105
- this.doEsothericWork();
2106
- this.setupMenu();
2107
- this.setupCustomSeekbar();
2108
- });
2109
- }
2110
-
2111
- setupCustomSeekbar() {
2112
- if (this.props.seekBar) {
2113
- // disable the built-in videojs seekbar
2114
- this.videojs.controlBar.progressControl.disable();
2115
- this.videojs.controlBar.progressControl.hide();
2116
-
2117
- const target = this.container.getElementsByClassName('vjs-control-bar')[0];
2118
- const props = {
2119
- target,
2120
- player: this,
2121
- onDark: this.props.onDark,
2122
- ...this.props.seekBar,
2123
- };
2124
-
2125
- this.seekBar = new SeekBar(props);
2126
-
2127
- // show & hide markers when controls hide
2128
- this.videojs.on('userinactive', () => {
2129
- if (!this.controlsBelowPlayer) {
2130
- this.seekBar && this.seekBar.hideMarkers();
2131
- } else {
2132
- this.seekBar && this.seekBar.showMarkers();
2133
- }
2134
- });
2135
-
2136
- this.videojs.on('useractive', () => {
2137
- this.seekBar && this.seekBar.showMarkers();
2138
- });
2139
- }
2140
- }
2141
-
2142
- setupTimeCode(externalTimecodeInstance = null) {
2143
- const props = {
2144
- onInput: (e, nodeName, values) => {
2145
- // TODO - check for out of bounds input
2146
- this.seekTo(this.timeConverter.smpteToCurrentTimeFromStart(values));
2147
- },
2148
- onInputFocus: () => {
2149
- /*
2150
- lock the control control bar in place to
2151
- prevent it from unexpectedly animating away while
2152
- doing timecode input
2153
- */
2154
- this.videojs.options_.inactivityTimeout = 0;
2155
- this.videojs.cache_.inactivityTimeout = 0;
2156
- this.videojs.reportUserActivity();
2157
-
2158
- // pause the video, expected behaviour when editing timecode position
2159
- this.videojs.pause();
2160
- },
2161
- onInputBlur: () => {
2162
- this.videojs.options().inactivityTimeout = constants.defaults.inactivityTimeout;
2163
- this.videojs.cache_.inactivityTimeout = constants.defaults.inactivityTimeout;
2164
- },
2165
- };
2166
-
2167
- if (externalTimecodeInstance !== null) { // setting up an external playback timecode
2168
- externalTimecodeInstance.props = Object.assign(externalTimecodeInstance.props, props);
2169
- this.externalTimecodes.playback.push(externalTimecodeInstance);
2170
- } else if (this.props.timecode === true) { // player uses timecode
2171
- // construct an in-frame timecode, if controls are enabled
2172
- if (this.props.controls !== false) {
2173
- const target = document.getElementsByClassName('vjs-volume-panel')[0];
2174
- // main timecode representing playhead position
2175
- this.timeCode = new TimeCodeDisplay(Object.assign(props, { target, appendAfter: true }));
2176
-
2177
- // read-only timecode representing video duration
2178
- this.durationTimeCode = new TimeCodeDisplay({
2179
- target: this.timeCode.container,
2180
- appendAfter: true,
2181
- });
2182
- }
2183
- }
2184
-
2185
- if (this.timeCode || this.externalTimecodes.playback.length > 0) {
2186
- // subscribe to events
2187
- this.videojs.on('timeupdate', () => {
2188
- this.updateTimeCode();
2189
- });
2190
-
2191
- this.videojs.on('play', () => {
2192
- this.updateTimeCodeFrame();
2193
- });
2194
- }
2195
- }
2196
-
2197
- updateTimeCode() { // HH:MM:SS (the usual)
2198
- const currentSecond = this.videojs.currentTime();
2199
- const props = this.timeConverter.secondsFromStartToSMPTE(currentSecond);
2200
-
2201
- this.timeCode && this.timeCode.update(props);
2202
-
2203
- if (this.externalTimecodes.playback.length > 0) {
2204
- this.externalTimecodes.playback.forEach((timecode) => {
2205
- timecode.update(props);
2206
- });
2207
- }
2208
- }
2209
-
2210
- updateTimeCodeFrame() { // update the frame in this second -> 00:00:00:XX
2211
- const frame = this.timeConverter.currentFrame(this.videojs.currentTime()); // -> :XX
2212
-
2213
- this.timeCode && this.timeCode.update({ frame }); // TODO - figure out a way to drop frames
2214
-
2215
- if (this.externalTimecodes.playback.length > 0) {
2216
- this.externalTimecodes.playback.forEach((timecode) => {
2217
- timecode.update({ frame });
2218
- });
2219
- }
2220
-
2221
- // recursive invocation if still playing
2222
- if (!this.videojs.paused()) {
2223
- this.rafID = requestAnimationFrame(() => { this.updateTimeCodeFrame(); });
2224
- }
2225
- }
2226
-
2227
- setDurationTimeCode() { // happens once
2228
- const durationInSeconds = this.videojs.duration();
2229
- const props = this.timeConverter.secondsFromStartToSMPTE(durationInSeconds);
2230
- props.frame = this.timeConverter.secondsToFrame(durationInSeconds);
2231
-
2232
- this.durationTimeCode.update(props);
2233
- this.durationTimeCode.container.classList.add('duration');
2234
- }
2235
-
2236
- bindHotkeys() { // keyboard shortcuts
2237
- this.bindings = [
2238
- {
2239
- key: ['space', 'k'],
2240
- method: (e) => {
2241
- // prevents page scroll on space, just like YT
2242
- (e.preventDefault) && e.preventDefault();
2243
- this.togglePlayback();
2244
- },
2245
- },
2246
- {
2247
- key: 'j',
2248
- method: () => {
2249
- this.videojs.hasStarted_ && this.jumpBackward();
2250
- },
2251
- },
2252
- {
2253
- key: 'l',
2254
- method: () => {
2255
- this.videojs.hasStarted_ && this.jumpForward();
2256
- },
2257
- },
2258
- {
2259
- key: ['.', 'right'],
2260
- method: () => {
2261
- this.frameForward();
2262
- },
2263
- },
2264
- {
2265
- key: [',', 'left'],
2266
- method: () => {
2267
- this.frameBackward();
2268
- },
2269
- },
2270
- {
2271
- key: 'f',
2272
- method: () => {
2273
- this.toggleFullscreen();
2274
- },
2275
- },
2276
- {
2277
- key: 'm',
2278
- method: () => {
2279
- this.toggleMute();
2280
- },
2281
- },
2282
- {
2283
- key: 'up',
2284
- method: (e) => {
2285
- e.preventDefault && e.preventDefault(); // prevent page scroll
2286
- this.volumeUp();
2287
- },
2288
- },
2289
- {
2290
- key: 'down',
2291
- method: (e) => {
2292
- e.preventDefault && e.preventDefault(); // prevent page scroll
2293
- this.volumeDown();
2294
- },
2295
- },
2296
- {
2297
- key: 'h',
2298
- method: () => {
2299
- this.help.toggle();
2300
- },
2301
- },
2302
- {
2303
- key: 'n',
2304
- method: () => {
2305
- this.menu && this.menu.toggle();
2306
- },
2307
- },
2308
- ];
2309
-
2310
- if (this.props.subtitles) {
2311
- this.bindings.push({
2312
- key: 'c',
2313
- method: () => {
2314
- this.toggleSubs();
2315
- },
2316
- });
2317
- }
2318
-
2319
- // bind
2320
- this.bindings.forEach((binding) => {
2321
- mousetrap.bind(binding.key, (e) => {
2322
- binding.method(e);
2323
- this.onHotkeyPressed(binding.key);
2324
- });
2325
- });
2326
- }
2327
-
2328
- unbindHotKeys() {
2329
- this.bindings && this.bindings.forEach((binding) => {
2330
- mousetrap.unbind(binding.key);
2331
- });
2332
- }
2333
-
2334
- bindEvents() {
2335
- this.videojs.on('volumechange', () => { // persist volume changes
2336
- this.persistSetting(constants.settings.volume, this.volume);
2337
- });
2338
-
2339
- this.videojs.textTracks().on('change', () => { // determine subtitles state
2340
- const tracks = this.videojs.textTracks().tracks_;
2341
- const selected = tracks.filter((track) => (track.mode === 'showing'));
2342
- const areAnyTxtTracksActive = selected.length > 0; // -> true if any text tracks are showing
2343
-
2344
- if (areAnyTxtTracksActive) { // update container with appropriate className
2345
- this.videojs.el_.classList.add(constants.classNames.subtitlesActive);
2346
- this.persistSetting(constants.settings.subs, true);
2347
- this.persistSetting(constants.settings.subLang, selected[0].language);
2348
- } else {
2349
- this.videojs.el_.classList.remove(constants.classNames.subtitlesActive);
2350
- this.persistSetting(constants.settings.subs, false);
2351
- }
2352
- });
2353
- }
2354
-
2355
- volumeUp() {
2356
- this.videojs.controlBar.volumePanel.volumeControl.volumeBar.stepForward();
2357
- this.osd.feedback(constants.osd.volchange, { volume: this.volume });
2358
- }
2359
-
2360
- volumeDown() {
2361
- this.videojs.controlBar.volumePanel.volumeControl.volumeBar.stepBack();
2362
- this.osd.feedback(constants.osd.volchange, { volume: this.volume });
2363
- }
2364
-
2365
- toggleMute() {
2366
- if (!this.videojs.muted()) {
2367
- this.videojs.muted(true);
2368
- this.osd.feedback(constants.osd.volchange, { volume: this.volume });
2369
- } else {
2370
- this.videojs.muted(false);
2371
- this.osd.feedback(constants.osd.volchange, { volume: this.volume });
2372
- }
2373
- }
2374
-
2375
- toggleFullscreen() {
2376
- if (this.videojs.isFullscreen()) {
2377
- this.videojs.exitFullscreen();
2378
- } else {
2379
- this.videojs.requestFullscreen();
2380
- }
2381
- }
2382
-
2383
- seek(secs) { // relative to current playback time
2384
- let time = this.videojs.currentTime() + secs;
2385
-
2386
- if (time < 0) {
2387
- time = 0;
2388
- }
2389
-
2390
- this.videojs.currentTime(time);
2391
- }
2392
-
2393
- seekTo(sec) {
2394
- !this.videojs.hasStarted() && this.videojs.hasStarted(true);
2395
- this.videojs.currentTime(sec);
2396
- }
2397
-
2398
- jumpForward() {
2399
- this.seek(constants.defaults.jumpSeconds);
2400
- this.osd.feedback(constants.osd.jumpForward);
2401
- }
2402
-
2403
- jumpBackward() {
2404
- this.seek(-constants.defaults.jumpSeconds);
2405
- this.osd.feedback(constants.osd.jumpBackward);
2406
- }
2407
-
2408
- frameForward() {
2409
- const thisFrame = this.timeConverter.secondsToFramesFromStart(this.videojs.currentTime());
2410
- const nextFrame = Math.round(thisFrame) + 1;
2411
-
2412
- this.videojs.currentTime(this.timeConverter.frameToCurrentTimeFromStart(nextFrame));
2413
- }
2414
-
2415
- frameBackward() {
2416
- const thisFrame = this.timeConverter.secondsToFramesFromStart(this.videojs.currentTime());
2417
- const previousFrame = Math.round(thisFrame) - 1;
2418
-
2419
- this.videojs.currentTime(this.timeConverter.frameToCurrentTimeFromStart(previousFrame));
2420
- }
2421
-
2422
- secondsToFramesFromStart(seconds) {
2423
- const frameRate = this.props.frameRate || constants.defaults.frameRate;
2424
- let frame = seconds * frameRate;
2425
- frame = Math.round(frame);
2426
- return frame; // we're on this frame, could be for example 23.45
2427
- }
2428
-
2429
- doEsothericWork() { // non critical parts, that are nice to have
2430
- if (this.props.color) { // change the seekbar color from props
2431
- document.getElementsByClassName('vjs-play-progress')[0].style.backgroundColor = this.props.color;
2432
- }
2433
-
2434
- // feedback gizmo, mainly for feedback on hotkey-presses
2435
- this.osd = new OSD({
2436
- target: this.videoEl.parentElement,
2437
- });
2438
-
2439
- // add a cinema button if a callback is passed in props
2440
- if (this.props.onCinema) {
2441
- this.videoEl.parentElement.classList.add('has-cinema-button');
2442
- this.cinema = false;
2443
-
2444
- this.addButtonToControlBar({
2445
- onClick: () => {
2446
- this.props.onCinema(!this.cinema);
2447
- this.cinema = !this.cinema;
2448
- },
2449
- componentName: 'CinemaButton',
2450
- className: 'vdt-cinema-button',
2451
- title: 'Cinema-mode',
2452
- });
2453
- }
2454
-
2455
- // add a screenshot button if a callback is passed in props
2456
- if (this.props.onScreenshot) {
2457
- this.videoEl.parentElement.classList.add('has-screenshot-button');
2458
-
2459
- this.addButtonToControlBar({
2460
- onClick: () => {
2461
- this.props.onScreenshot(this.videojs.currentTime());
2462
- },
2463
- componentName: 'ScreenshotButton',
2464
- className: 'vdt-screenshot-button',
2465
- title: 'Screenshot',
2466
- });
2467
- }
2468
-
2469
- // add a help overlay explaining keyboard controls
2470
- this.help = new HelpOverlay({
2471
- target: this.videoEl.parentElement,
2472
- onToggle: (willShow) => {
2473
- if (this.menu && this.menu.opened) {
2474
- this.menu.close();
2475
- }
2476
- if (willShow && !this.controlsBelowPlayer) {
2477
- this.hideControls();
2478
- } else {
2479
- this.showControls();
2480
- }
2481
- },
2482
- });
2483
- }
2484
-
2485
- restoreSettings() { // restore settings, like volume level or language track
2486
- // get values from storage
2487
- const volume = localStorage.getItem(constants.settings.volume);
2488
- const subsEnabled = localStorage.getItem(constants.settings.subs);
2489
- const subLang = localStorage.getItem(constants.settings.subLang);
2490
-
2491
- // values vs default behaviours in a neatly readable map
2492
- this.settings = {
2493
- // avoid .99999999 and other float pecularities in js
2494
- [constants.settings.volume]: (volume !== null) ? Math.round(volume * 100) / 100 : 1,
2495
- [constants.settings.subLang]: (subLang !== null) ? subLang : 'en',
2496
- [constants.settings.subs]: (subsEnabled !== null) ? JSON.parse(subsEnabled) : false,
2497
- };
2498
-
2499
- // apply values (settings)
2500
- this.volume = this.settings[constants.settings.volume]; // volume
2501
-
2502
- if (this.settings[constants.settings.subs]) { // subtitles
2503
- this.enableSubs();
2504
- }
2505
- }
2506
-
2507
- persistSetting(key, val) {
2508
- this.settings[key] = val;
2509
- localStorage.setItem(key, val);
2510
- }
2511
-
2512
- enableSubs() {
2513
- const subs = this.videojs.textTracks();
2514
-
2515
- for (let i = 0; i < subs.length; i++) {
2516
- const sub = subs[i];
2517
- if (sub.language === this.settings[constants.settings.subLang]) {
2518
- sub.mode = 'showing';
2519
- } else {
2520
- sub.mode = 'hidden';
2521
- }
2522
- }
2523
- }
2524
-
2525
- disableSubs() {
2526
- const subs = this.videojs.textTracks();
2527
-
2528
- for (let i = 0; i < subs.length; i++) {
2529
- const sub = subs[i];
2530
- sub.mode = 'hidden';
2531
- }
2532
- }
2533
-
2534
- toggleSubs() {
2535
- if (this.videojs.textTracks().length > 0) { // check if subtitles exist
2536
- if (this.videojs.textTracks().tracks_.filter((track) => (track.mode === 'showing')).length > 0) { // a certain subtitle track is enabled
2537
- this.disableSubs();
2538
- } else {
2539
- this.enableSubs();
2540
- }
2541
- }
2542
- }
2543
-
2544
- /*
2545
- A quick example
2546
- options = {
2547
- onClick: ()=>{},
2548
- componentName: 'ScreenshotButton',
2549
- className: 'vdt-screenshot-button'
2550
- }
2551
- */
2552
- addButtonToControlBar(options) {
2553
- const Button = videojs.getComponent('Button');
2554
- const CustomButton = videojs.extend(Button, {
2555
- /* eslint-disable func-names, object-shorthand */
2556
- constructor: function () {
2557
- Button.apply(this, arguments);
2558
- options.className && this.el_.classList.add(options.className);
2559
- if (options.title) { this.el_.title = options.title; }
2560
- },
2561
- handleClick: () => {
2562
- options.onClick && options.onClick();
2563
- },
2564
- });
2565
-
2566
- videojs.registerComponent(options.componentName, CustomButton);
2567
- this.videojs.getChild('controlBar').addChild(options.componentName, {}, 7);
2568
- }
2569
-
2570
- // convenient getters / setters
2571
- get volume() {
2572
- return this.videojs.controlBar.volumePanel.volumeControl.volumeBar.getPercent();
2573
- }
2574
-
2575
- set volume(floatVal) {
2576
- if (floatVal === 0) {
2577
- this.videojs.muted(true);
2578
- } else {
2579
- this.videojs.muted(false);
2580
- this.videojs.volume(floatVal);
2581
- }
2582
- }
2583
-
2584
- setupMenu() {
2585
- const menuItems = this.props.menuItems || [];
2586
- const sourcesMenuItems = [];
2587
- if (this.props.sources.length > 1) { // show only if more than 1 source
2588
- this.props.sources.forEach((source) => {
2589
- if (source.label) {
2590
- sourcesMenuItems.push({
2591
- title: source.label,
2592
- onClick: () => {
2593
- this.src = source;
2594
- },
2595
- });
2596
- }
2597
- });
2598
-
2599
- menuItems.push({
2600
- title: 'Sources',
2601
- getActiveIndex: () => {
2602
- const matchingSource = this.props.sources.find((source) => (source.src === this.videojs.src()));
2603
- return sourcesMenuItems.indexOf(sourcesMenuItems.find((menuItem) => (menuItem.title === matchingSource.label)));
2604
- },
2605
- items: sourcesMenuItems,
2606
- });
2607
- }
2608
-
2609
- // subtitles
2610
- if (this.props.subtitles && this.props.subtitles.length > 0) {
2611
- // off option for subs
2612
- const subtitleMenuItems = [{
2613
- title: 'Off',
2614
- onClick: () => {
2615
- this.disableSubs();
2616
- },
2617
- }];
2618
-
2619
- // quick toggle in the control bar
2620
- // this.addButtonToControlBar({
2621
- // onClick: () => {
2622
- // this.props.onCinema(!this.cinema);
2623
- // this.cinema = !this.cinema;
2624
- // },
2625
- // componentName: 'CinemaButton',
2626
- // className: 'vdt-cinema-button',
2627
- // });
2628
-
2629
- this.props.subtitles.forEach((subtitle, index) => {
2630
- subtitleMenuItems.push({
2631
- title: subtitle.label,
2632
- onClick: () => {
2633
- const subs = this.videojs.textTracks();
2634
- for (let i = 0; i < subs.length; i++) {
2635
- const sub = subs[i];
2636
- if (i === index) {
2637
- sub.mode = 'showing';
2638
- } else {
2639
- sub.mode = 'hidden';
2640
- }
2641
- }
2642
- },
2643
- });
2644
- });
2645
-
2646
- menuItems.push(
2647
- {
2648
- title: 'Subtitles',
2649
- getActiveIndex: () => {
2650
- const subs = this.videojs.textTracks();
2651
- for (let i = 0; i < subs.length; i++) {
2652
- const sub = subs[i];
2653
- if (sub.mode === 'showing') {
2654
- return i + 1; // offset "off" option
2655
- }
2656
- }
2657
-
2658
- return 0; // if none are showing, then probably "off"
2659
- },
2660
- items: subtitleMenuItems,
2661
- },
2662
- );
2663
- }
2664
-
2665
- if (menuItems.length > 0) {
2666
- const target = this.videojs.el_;
2667
-
2668
- this.menu = new Menu({
2669
- target,
2670
- items: menuItems,
2671
- onOpen: () => {
2672
- !this.controlsBelowPlayer && this.hideControls();
2673
- },
2674
- onClose: () => {
2675
- !this.controlsBelowPlayer && this.videojs.controls(true);
2676
- },
2677
- });
2678
-
2679
- // add a cogwheel button to the player
2680
- this.videoEl.parentElement.classList.add('has-vdt-menu');
2681
-
2682
- this.addButtonToControlBar({
2683
- onClick: () => {
2684
- this.menu.toggle();
2685
- },
2686
- componentName: 'SettingsMenuButton',
2687
- className: 'vdt-settings-menu-button',
2688
- title: 'Settings',
2689
- });
2690
- }
2691
- }
2692
-
2693
- hideControls() {
2694
- this.videojs.userActive(false);
2695
- this.videojs.controls(false);
2696
- this.videojs.fluid(true);
2697
-
2698
- this.videoEl.parentElement.classList.remove('vjs-user-active');
2699
- this.videojs.userActive(false);
2700
- }
2701
-
2702
- showControls() {
2703
- this.videojs.fluid(!this.controlsBelowPlayer);
2704
- this.videojs.controls(true);
2705
- this.videojs.userActive(true);
2706
- }
2707
-
2708
- toggleControls() {
2709
- if (this.videojs.controls()) {
2710
- this.hideControls();
2711
- } else {
2712
- this.showControls();
2713
- }
2714
- }
2715
-
2716
- onHotkeyPressed(key) {
2717
- if (key !== 'h' && this.help.state.active) {
2718
- this.help.hide();
2719
- }
2720
- }
2721
-
2722
- // eslint-disable-next-line class-methods-use-this
2723
- triggerHotkey(key) {
2724
- mousetrap.trigger(key);
2725
- }
2726
-
2727
- toStart() {
2728
- this.videojs.currentTime(0);
2729
- }
2730
-
2731
- toEnd() {
2732
- this.videojs.currentTime(this.videojs.duration());
2733
- }
2734
-
2735
- replay() {
2736
- this.toStart();
2737
- this.videojs.play();
2738
- }
2739
-
2740
- play() {
2741
- this.videojs.play();
2742
- }
2743
-
2744
- pause() {
2745
- !this.videojs.paused() && this.videojs.pause();
2746
- }
2747
-
2748
- togglePlayback() {
2749
- if (this.videojs.paused()) {
2750
- this.play();
2751
- this.videojs.hasStarted_ && this.osd.feedback(constants.osd.play);
2752
- } else {
2753
- this.pause();
2754
- this.osd.feedback(constants.osd.pause);
2755
- }
2756
- }
2757
-
2758
- destroy() { // clean up
2759
- this.videojs.paused(true);
2760
- this.rafID && cancelAnimationFrame(this.rafID);
2761
- this.videojs.dispose();
2762
- this.unbindHotKeys();
2763
- this.seekBar && this.seekBar.destroy();
2764
- }
2765
-
2766
- get container() {
2767
- return this.videoEl.parentElement;
2768
- }
2769
-
2770
- set onDark(bool) {
2771
- if (bool) {
2772
- this.container.classList.add('on-dark');
2773
- } else {
2774
- this.container.classList.remove('on-dark');
2775
- }
2776
-
2777
- if (this.seekBar) {
2778
- this.seekBar.onDark = bool;
2779
- }
2780
- }
2781
-
2782
- get onDark() {
2783
- return this.container.classList.contains('on-dark');
2784
- }
2785
-
2786
- set controlsBelowPlayer(bool) {
2787
- if (this.videojs.controls()) {
2788
- if (bool) {
2789
- this.container.classList.add('controls-below-player');
2790
- this.videojs.fluid(false);
2791
- } else {
2792
- this.container.classList.remove('controls-below-player');
2793
- this.videojs.fluid(true);
2794
- }
2795
- }
2796
- }
2797
-
2798
- get controlsBelowPlayer() {
2799
- return this.container.classList.contains('controls-below-player');
2800
- }
2801
-
2802
- get smpte() {
2803
- const currentSecond = this.videojs.currentTime();
2804
- return this.timeConverter.secondsFromStartToSMPTE(currentSecond);
2805
- }
2806
-
2807
- set smpte(smpte) {
2808
- const currentSecond = this.videojs.currentTime();
2809
- return this.timeConverter.secondsFromStartToSMPTE(currentSecond);
2810
- }
2811
-
2812
- set src({ src, type = 'video/mp4', label = null }) {
2813
- const currentTime = this.videojs.currentTime();
2814
- // iterate trough sources, find existing matches
2815
- const source = this.props.sources.find((aSource) => (aSource.src === src));
2816
- // if no match try adding source
2817
-
2818
- const onReady = () => {
2819
- let initdone = false;
2820
- // wait for video metadata to load, then set time
2821
- this.videojs.on('loadedmetadata', () => {
2822
- this.videojs.currentTime(currentTime);
2823
- });
2824
-
2825
- // iPhone/iPad need to play first, then set the time
2826
- // events: https://www.w3.org/TR/html5/embedded-content-0.html#mediaevents
2827
- this.videojs.on('canplaythrough', () => {
2828
- if (!initdone) {
2829
- this.videojs.currentTime(currentTime);
2830
- initdone = true;
2831
- this.videojs.play();
2832
- }
2833
- });
2834
- };
2835
-
2836
- if (source && typeof souce === 'Array' && source.length > 0) {
2837
- // TODO - if(source.length > 1) - try one by one until a successful load happens
2838
- this.videojs.addClass('vjs-waiting');
2839
- this.videojs.src({
2840
- ...source[0],
2841
- });
2842
- onReady();
2843
- } else {
2844
- this.videojs.addClass('vjs-waiting');
2845
- this.videojs.src(this.addSrc(src, type, label));
2846
- onReady();
2847
- }
2848
- }
2849
-
2850
- get src() {
2851
- return this.videojs.src();
2852
- }
2853
-
2854
- addSrc(src, type, label) {
2855
- const newSource = {
2856
- src,
2857
- type,
2858
- label,
2859
- };
2860
- const index = this.props.sources.push(newSource) - 1;
2861
-
2862
- return this.props.sources[index];
2863
- }
2864
-
2865
- removeSrc(url) {
2866
- // should remove all url matching sources
2867
- }
2868
-
2869
- set controls(bool) {
2870
- if (bool) {
2871
- this.showControls();
2872
- } else {
2873
- this.hideControls();
2874
- }
2875
- }
2876
-
2877
- get percentLoaded() {
2878
- const r = this.videojs.buffered();
2879
-
2880
- const percentLoaded = (r.end(0) / this.videojs.duration()) * 100;
2881
- return percentLoaded;
2882
- }
2883
- }
2884
-
2885
- /*
2886
- Some video.js prototype overrides, that
2887
- help with undesired behaviour
2888
- */
2889
-
2890
- // take over space button, prevent video.js default behaviour
2891
- const button = videojs.prototype.constructor.getComponent('Button');
2892
- // eslint-disable-next-line func-names
2893
- button.prototype.handleKeyDown = function (e) {
2894
- if (e.keyCode === 32) {
2895
- e.preventDefault();
2896
- }
2897
- };
2898
- videojs.prototype.constructor.registerComponent('Button', button);
2899
-
2900
- // video.js opens menu on hover by default, this prevents that
2901
- const subsButton = videojs.prototype.constructor.getComponent('SubsCapsButton');
2902
- class CustomSubsCapsButton extends subsButton {
2903
- constructor(player, options = {}) {
2904
- super(player, options);
2905
- this.menuButton_.off('mouseenter');
2906
- this.menuButton_.off('mouseleave');
2907
- }
2908
- }
2909
-
2910
- /*
2911
- TODO - see what can be done about strange behaviour
2912
- when cc button is clicked with the menu showing
2913
- */
2914
- videojs.prototype.constructor.registerComponent('SubsCapsButton', CustomSubsCapsButton);
2915
-
2916
- /* eslint-disable no-plusplus */
2917
-
2918
- const buttonNames = Object.freeze({
2919
- play: 'play-pause',
2920
- start: 'start',
2921
- fb: 'frame-backward',
2922
- ff: 'frame-forward',
2923
- end: 'end',
2924
- });
2925
-
2926
- const buttons = [ // order matters
2927
- buttonNames.start,
2928
- buttonNames.fb,
2929
- buttonNames.play,
2930
- buttonNames.ff,
2931
- buttonNames.end,
2932
- ];
2933
-
2934
- /*
2935
- Since this could possibly be used in a
2936
- context without the player and vice versa -
2937
- will try and make it as self-contained
2938
- as possible, currently utilizing
2939
- shadow DOM (style encapsulation as main reason),
2940
- https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM
2941
- BEWARE - IE < Edge is unsupported
2942
- */
2943
-
2944
- const CSS$2 = `
2945
- .vdt-external-controls{
2946
- display: flex;
2947
- justify-content: center;
2948
- align-items: center;
2949
- }
2950
-
2951
- .control{
2952
- flex: 1;
2953
- flex-grow: 1;
2954
- height: 100%;
2955
- align-contents: center;
2956
- align-self: center;
2957
- text-align: center;
2958
- vertical-align: middle;
2959
- cursor: pointer;
2960
- user-select: none;
2961
- height: 20px;
2962
- padding-top: 10px;
2963
- padding-bottom: 10px;
2964
- }
2965
-
2966
- .control .icon{
2967
- display: inline-block;
2968
- height: 20px;
2969
- white-space: nowrap;
2970
- opacity: .9;
2971
- }
2972
-
2973
- .control:hover .icon{ opacity: 1; }
2974
-
2975
- .control .icon:before{
2976
- opacity: 1;
2977
- flex: 1;
2978
- align-self: center;
2979
- content: " ";
2980
- display: inline-block;
2981
- margin: 0 auto;
2982
-
2983
- /* triangles */
2984
- width: 0;
2985
- height: 0;
2986
- border-style: solid;
2987
- border-width: 16px 0 16px 23px;
2988
- border-color: transparent transparent transparent ${colors.gray};
2989
- }
2990
-
2991
- .vdt-external-controls.${classNames.onDark} .control .icon:before{
2992
- border-color: transparent transparent transparent ${colors.light};
2993
- }
2994
-
2995
- /* specific buttons */
2996
-
2997
- .control.${buttonNames.start},
2998
- .control.${buttonNames.end}{
2999
- height: 20px;
3000
- }
3001
-
3002
- .control.${buttonNames.play}{
3003
- height: 30px;
3004
- padding-top: 0px;
3005
- padding-bottom: 0px;
3006
- }
3007
-
3008
- .control.${buttonNames.play} .icon{
3009
- height: 30px;
3010
- }
3011
-
3012
- .control.${buttonNames.ff} .icon:before,
3013
- .control.${buttonNames.fb} .icon:before,
3014
- .control.${buttonNames.end} .icon:before,
3015
- .control.${buttonNames.start} .icon:before{
3016
- border-width: 10px 0 10px 16px;
3017
- }
3018
-
3019
- .control.${buttonNames.fb} .icon,
3020
- .control.${buttonNames.start}{
3021
- transform: rotate(180deg);
3022
- }
3023
-
3024
- .control.${buttonNames.end} .icon:after,
3025
- .control.${buttonNames.start} .icon:after{
3026
- content: " ";
3027
- width: 4px;
3028
- height: 20px;
3029
- display: inline-block;
3030
- background: ${colors.gray};
3031
- }
3032
-
3033
- .vdt-external-controls.${classNames.onDark} .control.${buttonNames.end} .icon:after,
3034
- .vdt-external-controls.${classNames.onDark} .control.${buttonNames.start} .icon:after{
3035
- background: ${colors.light};
3036
- }
3037
-
3038
- /* stateful styles */
3039
- .control.${buttonNames.play}.playing .icon:before{
3040
- height: 30px;
3041
- width: 6px;
3042
- border-width: 0px 8px 0px 8px;
3043
- border-color: transparent ${colors.gray} transparent ${colors.gray};
3044
- }
3045
-
3046
- .vdt-external-controls.${classNames.onDark} .control.${buttonNames.play}.playing .icon:before{
3047
- border-color: transparent ${colors.light} transparent ${colors.light};
3048
- }
3049
-
3050
- .vdt-external-controls.${classNames.loading} .control.${buttonNames.play}{
3051
- border-color: transparent #f00 transparent #f00;
3052
- }
3053
- `;
3054
-
3055
- class ExternalControls {
3056
- constructor(props) {
3057
- this.props = props;
3058
- reactiveSetters.apply(this);
3059
-
3060
- this.state = {
3061
- playing: false,
3062
- onDark: false,
3063
- loading: false,
3064
- };
3065
-
3066
- this.shadowRoot = document.createElement('div'); // will attach shadow here, after appending to target
3067
- this.shadowRoot.className = 'vdt-external-controls-wrapper';
3068
- this.container = document.createElement('div');
3069
- this.container.className = 'vdt-external-controls';
3070
-
3071
- if (this.props.target) {
3072
- this.props.target.appendChild(this.shadowRoot);
3073
- this.shadow = this.shadowRoot.attachShadow({ mode: 'open' });
3074
- // attach styles
3075
- this.style = document.createElement('style');
3076
- this.style.type = 'text/css';
3077
- this.style.appendChild(document.createTextNode(CSS$2));
3078
- this.shadow.appendChild(this.style);
3079
-
3080
- // attach container to shadow root
3081
- this.shadow.appendChild(this.container);
3082
- }
3083
-
3084
- this.handleNewProps(props);
3085
- this.bindEvents();
3086
- this.draw();
3087
- }
3088
-
3089
- draw() {
3090
- this.buttons = {}; // map of html nodes for DOM manipulations
3091
- const excludes = this.props.excludes || [];
3092
-
3093
- buttons.forEach((buttonName) => {
3094
- if (excludes.indexOf(buttonName) < 0) {
3095
- const btn = document.createElement('div');
3096
- btn.className = `control ${buttonName}`;
3097
- btn.innerHTML = '<span class="icon"></span>';
3098
-
3099
- btn.onclick = () => {
3100
- this.handlePress(buttonName);
3101
- };
3102
-
3103
- this.buttons[buttonName] = btn;
3104
- this.container.appendChild(btn);
3105
- }
3106
- });
3107
- }
3108
-
3109
- /*
3110
- bind to play / pause / stop and other events,
3111
- so that controls reflect the player state
3112
- */
3113
- bindEvents() {
3114
- if (this.props.player && this.props.player.videojs) {
3115
- const { player } = this.props;
3116
-
3117
- player.videojs.on('play', () => {
3118
- this.playing = true;
3119
- });
3120
-
3121
- player.videojs.on('pause', () => {
3122
- this.playing = false;
3123
- });
3124
-
3125
- player.videojs.on('stop', () => {
3126
- this.playing = false;
3127
- });
3128
- }
3129
- }
3130
-
3131
- handlePress(buttonName) {
3132
- if (this.props.player) { // control a vdt player instance directly
3133
- const player = this.props.player;
3134
- console.log(player);
3135
- player.osd && player.osd.disable(); // temporarily disable OSD
3136
-
3137
- switch (buttonName) {
3138
- case buttonNames.start:
3139
- player.toStart();
3140
- break;
3141
- case buttonNames.fb:
3142
- player.triggerHotkey('left');
3143
- break;
3144
- case buttonNames.play:
3145
- player.triggerHotkey('k'); // TODO - triggering hotkeys has a side effect - OSD will give feedback
3146
- break;
3147
- case buttonNames.ff:
3148
- player.triggerHotkey('right');
3149
- break;
3150
- case buttonNames.end:
3151
- player.toEnd();
3152
- break;
3153
- }
3154
-
3155
- player.osd && player.osd.enable(); // re-enable OSD
3156
- } else {
3157
- switch (buttonName) {
3158
- case buttonNames.start:
3159
- this.props.onPrev && this.props.onPrev();
3160
- break;
3161
- case buttonNames.fb:
3162
- this.props.onFB && this.props.onFB();
3163
- break;
3164
- case buttonNames.play:
3165
- if (this.playing) {
3166
- this.props.onPause && this.props.onPause();
3167
- } else {
3168
- this.props.onPlay && this.props.onPlay();
3169
- }
3170
- break;
3171
- case buttonNames.ff:
3172
- this.props.onFF && this.props.onFF();
3173
- break;
3174
- case buttonNames.end:
3175
- this.props.onNext && this.props.onNext();
3176
- break;
3177
- }
3178
- }
3179
- }
3180
-
3181
- // setters and getters for state
3182
- set playing(bool) {
3183
- if (bool) {
3184
- this.state.playing = true;
3185
- this.buttons[buttonNames.play].classList.add('playing');
3186
- } else {
3187
- this.state.playing = false;
3188
- this.buttons[buttonNames.play].classList.remove('playing');
3189
- }
3190
- }
3191
-
3192
- get playing() {
3193
- return this.state.playing;
3194
- }
3195
-
3196
- set onDark(bool) {
3197
- if (bool) {
3198
- this.state.onDark = true;
3199
- this.container.classList.add(classNames.onDark);
3200
- } else {
3201
- this.state.onDark = false;
3202
- this.container.classList.remove(classNames.onDark);
3203
- }
3204
- }
3205
-
3206
- get onDark() {
3207
- return this.container.classList.contains(classNames.onDark);
3208
- }
3209
-
3210
- set loading(bool) {
3211
- if (bool) {
3212
- this.state.loading = true;
3213
- this.container.classList.add(classNames.loading);
3214
- } else {
3215
- this.state.loading = false;
3216
- this.container.classList.remove(classNames.loading);
3217
- }
3218
- }
3219
-
3220
- get loading() {
3221
- return this.state.loading;
3222
- }
3223
- }
3224
-
3225
- ExternalControls.buttonNames = buttonNames;
3226
-
3227
- export { ExternalControls, Player, SeekBar, TimeCodeDisplay, VideoTime };