@vidispine/vdt-videojs 21.4.0-pre.2 → 22.1.0-pre.3

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 (54) 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} +621 -6
  9. package/dist/index.es.js +2099 -0
  10. package/dist/index.js +2112 -0
  11. package/{lib → dist}/index.umd.js +910 -2039
  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 -3241
  33. package/lib/index.esm.js +0 -3228
  34. package/lib/package.json +0 -13
  35. package/lib/src/constants.js +0 -51
  36. package/lib/src/externalControls.js +0 -307
  37. package/lib/src/helpOverlay.js +0 -106
  38. package/lib/src/helpers.js +0 -34
  39. package/lib/src/index.js +0 -13
  40. package/lib/src/menu.js +0 -365
  41. package/lib/src/osd.js +0 -91
  42. package/lib/src/player.js +0 -943
  43. package/lib/src/seekBar.js +0 -833
  44. package/lib/src/timeCodeDisplay.js +0 -253
  45. package/lib/src/videoTime.js +0 -66
  46. package/log.js +0 -24
  47. package/src/_index.stories.js +0 -24
  48. package/src/externalControls.stories.js +0 -6
  49. package/src/helpOverlay.stories.js +0 -6
  50. package/src/menu.classMethods.md +0 -47
  51. package/src/player.classMethods.md +0 -108
  52. package/src/player.stories.js +0 -14
  53. package/src/seekBar.stories.js +0 -8
  54. package/tmp/player.css +0 -1
package/lib/index.esm.js DELETED
@@ -1,3228 +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.4.0-pre.2";
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$2 = 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$2.osd.pause:
245
- className = 'pause';
246
- break;
247
- case constants$2.osd.play:
248
- className = 'play';
249
- break;
250
- case constants$2.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$2.osd.jumpForward:
268
- className = 'forward-jump';
269
- break;
270
- case constants$2.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$2 = () => `
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$2();
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 = 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.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.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.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.className} .load-indicator{
1095
- background: white;
1096
- }
1097
-
1098
- .${constants.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.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.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.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.defaults.regionColor};
1153
- }
1154
-
1155
- .region{
1156
- position: absolute;
1157
- width: auto;
1158
- background: ${constants.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.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.className}.dragging .bubble{
1252
- transform: scale3d(.75,.75,.75);
1253
- }
1254
-
1255
- .${constants.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.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.defaults.bgColor;
1302
- const fillColor = (this.props.fillColor) ? this.props.fillColor : constants.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.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.markerTypes.in] = document.createElement('div');
1533
- const markerOut = this.markers[constants.markerTypes.out] = document.createElement('div');
1534
- markerIn.className = `marker ${constants.markerTypes.in}`;
1535
- markerOut.className = `marker ${constants.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.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.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.markerTypes.in].style.left;
1585
- this.region.style.right = `${100 - parseFloat(this.markers[constants.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.markerTypes.in) ? (constants.defaults.markerWidth / 2) : -(constants.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.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.markerTypes.in].classList.remove('active');
1660
- removedMarker = true;
1661
- }
1662
-
1663
- if (this.state.out === null) {
1664
- this.markers[constants.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.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.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
- delete this;
1905
- throw new Error('No "frameRate" option provided, refer to docs.');
1906
- } else {
1907
- this.props = props;
1908
- }
1909
- }
1910
-
1911
- secondsToFramesFromStart(seconds) {
1912
- let frame = seconds * this.props.frameRate;
1913
- frame = Math.round(frame);
1914
- return frame; // we're on this frame, could be for example 23.45
1915
- }
1916
-
1917
- currentTimeToFramesFromStart(currentTime) {
1918
- return this.secondsToFramesFromStart(currentTime);
1919
- }
1920
-
1921
- secondsToFrame(seconds) {
1922
- let frame = this.secondsToFramesFromStart(seconds) % this.props.frameRate;
1923
- frame = Math.round(frame);
1924
- return frame;
1925
- }
1926
-
1927
- currentFrame(currentTime) {
1928
- return Math.round(this.secondsToFrame(currentTime));
1929
- }
1930
-
1931
- frameToCurrentTimeFromStart(frame) {
1932
- return frame / this.props.frameRate; // in seconds from start
1933
- }
1934
-
1935
- smpteToCurrentTimeFromStart(values) {
1936
- return (
1937
- values.hours * 3600
1938
- + values.minutes * 60
1939
- + values.seconds
1940
- + values.frame / this.props.frameRate
1941
- );
1942
- }
1943
-
1944
- secondsFromStartToSMPTE(secondsFromStart) {
1945
- let secondsFS = secondsFromStart;
1946
- const hours = Math.floor(secondsFS / 3600);
1947
- const minutesWithoutHours = secondsFS / 60 - 60 * hours;
1948
- // eslint-disable-next-line no-param-reassign
1949
- secondsFS %= 3600;
1950
-
1951
- const minutes = Math.floor(minutesWithoutHours);
1952
- const seconds = Math.floor(secondsFS % 60);
1953
- const frame = this.secondsToFrame(secondsFS);
1954
-
1955
- return {
1956
- hours,
1957
- minutes,
1958
- seconds,
1959
- frame,
1960
- };
1961
- }
1962
- }
1963
-
1964
- /* eslint-disable no-param-reassign */
1965
-
1966
- const correctMimeTypes = {
1967
- 'audio/x-aac': 'audio/aac',
1968
- };
1969
- const sanitizeMimeType = (mimeType) => correctMimeTypes[mimeType] || mimeType;
1970
-
1971
- class Player {
1972
- constructor(props) {
1973
- this.props = props; // save props
1974
- reactiveSetters.apply(this); // ability to handle new props via setters
1975
-
1976
- const classNames = [ // add conditional css classes here for the whole container
1977
- 'video-js',
1978
- 'vjs-big-play-centered',
1979
- (this.props.posterUrl) ? constants$2.classNames.poster : '',
1980
- (this.props.subtitles && this.props.subtitles.length > 0) ? constants$2.classNames.subtitles : '',
1981
- 'init', // make player height 0 while no metadata for video size available with css
1982
- ];
1983
-
1984
- this.externalTimecodes = {
1985
- // either one of these can hold an array of TimeCodeDisplay instances
1986
- duration: [],
1987
- playback: [],
1988
- };
1989
-
1990
- this.timeConverter = new VideoTime({
1991
- frameRate: this.props.frameRate || constants$2.defaults.frameRate,
1992
- });
1993
-
1994
- /*
1995
- video.js requires a html video to initialize
1996
- */
1997
-
1998
- this.videoEl = document.createElement('video');
1999
- this.videoEl.crossOrigin = this.props.crossOrigin;
2000
- this.videoEl.id = 'vdt-video';
2001
- this.videoEl.className = classNames.join(' ');
2002
- this.videoEl.controls = true; // remove this line for no-controls videojs player
2003
-
2004
- // attach sources, sort mp4 on top of source list
2005
- if (this.props.sources) {
2006
- // sort mp4 at the top
2007
- if (this.props.sources.length > 1) {
2008
- this.props.sources.sort((a) => ((a.type.indexOf('mp4') > 0) ? 1 : -1));
2009
- }
2010
-
2011
- // construct <source> els
2012
- this.props.sources.forEach((source) => {
2013
- const aSource = document.createElement('source');
2014
- aSource.src = source.src;
2015
- aSource.type = sanitizeMimeType(source.type);
2016
- this.videoEl.appendChild(aSource);
2017
- });
2018
- }// handle else with err later
2019
-
2020
- this.videoEl.onloadedmetadata = () => { // read-only duration timecode
2021
- if (this.durationTimeCode) {
2022
- this.setDurationTimeCode();
2023
- }
2024
-
2025
- this.videojs.el_.classList.remove('init');
2026
- };
2027
-
2028
- // if video has not loaded by now, show the player w/ errors
2029
- setTimeout(() => {
2030
- if (this.videojs.el_) {
2031
- this.videojs.el_.classList.remove('init');
2032
- }
2033
- }, 2000);
2034
-
2035
- // attach subtitle tracks (these should come in last in the <video /> node)
2036
- if (this.props.subtitles) {
2037
- this.props.subtitles.forEach((sub) => {
2038
- const subtitleTag = document.createElement('track');
2039
- subtitleTag.kind = 'subtitles';
2040
- subtitleTag.srclang = sub.lang;
2041
- subtitleTag.label = sub.label;
2042
- // subtitleTag.default = (index === 0) //can use this later to set a default track
2043
-
2044
- if (typeof sub.src === 'string') {
2045
- subtitleTag.src = sub.src; // external file
2046
- } else {
2047
- subtitleTag.src = window.URL.createObjectURL(sub.src); // file url from blob
2048
- }
2049
-
2050
- this.videoEl.appendChild(subtitleTag);
2051
- });
2052
- }
2053
-
2054
- // attach to dom and construct videojs player
2055
- if (this.props.target) {
2056
- this.props.target && this.props.target.appendChild(this.videoEl);
2057
- }
2058
-
2059
- const showDefaultTimeDisplay = (this.props.timecode !== true);
2060
- const controlsEnabled = (this.props.controls !== false);
2061
- const options = this.props.options || {};
2062
-
2063
- this.videojs = new videojs('vdt-video', {
2064
- responsive: true,
2065
- fluid: true,
2066
- controls: controlsEnabled,
2067
- alwaysCaptureHotkeys: false,
2068
- controlBar: {
2069
- RemainingTimeDisplay: false,
2070
- timeDivider: showDefaultTimeDisplay, // relates to built-in time display
2071
- durationDisplay: showDefaultTimeDisplay,
2072
- currentTimeDisplay: showDefaultTimeDisplay,
2073
- },
2074
- inactivityTimeout: (this.audioOnly) ? constants$2.defaults.audioInactivityTimeout : constants$2.defaults.inactivityTimeout,
2075
- preload: 'auto',
2076
- captionsButton: false,
2077
- subsCapsButton: false,
2078
- subtitlesButton: false,
2079
- textTrackSettings: false,
2080
- html5: {
2081
- nativeTextTracks: false,
2082
- },
2083
- languages: {
2084
- en: {
2085
- 'captions off': 'off',
2086
- 'subtitles off': 'off',
2087
- },
2088
- },
2089
- ...options,
2090
- });
2091
-
2092
- // set the poster
2093
- this.props.posterUrl && this.videojs.poster(this.props.posterUrl);
2094
-
2095
- // set other props that are passed
2096
- this.handleNewProps(this.props);
2097
-
2098
- // lifecycle
2099
- this.videojs.ready(() => {
2100
- this.bindHotkeys();
2101
- this.bindEvents();
2102
- this.props.onReady && this.props.onReady(this.videojs);
2103
-
2104
- this.restoreSettings();
2105
- this.setupTimeCode();
2106
- this.doEsothericWork();
2107
- this.setupMenu();
2108
- this.setupCustomSeekbar();
2109
- });
2110
- }
2111
-
2112
- setupCustomSeekbar() {
2113
- if (this.props.seekBar) {
2114
- // disable the built-in videojs seekbar
2115
- this.videojs.controlBar.progressControl.disable();
2116
- this.videojs.controlBar.progressControl.hide();
2117
-
2118
- const target = this.container.getElementsByClassName('vjs-control-bar')[0];
2119
- const props = {
2120
- target,
2121
- player: this,
2122
- onDark: this.props.onDark,
2123
- ...this.props.seekBar,
2124
- };
2125
-
2126
- this.seekBar = new SeekBar(props);
2127
-
2128
- // show & hide markers when controls hide
2129
- this.videojs.on('userinactive', () => {
2130
- if (!this.controlsBelowPlayer) {
2131
- this.seekBar && this.seekBar.hideMarkers();
2132
- } else {
2133
- this.seekBar && this.seekBar.showMarkers();
2134
- }
2135
- });
2136
-
2137
- this.videojs.on('useractive', () => {
2138
- this.seekBar && this.seekBar.showMarkers();
2139
- });
2140
- }
2141
- }
2142
-
2143
- setupTimeCode(externalTimecodeInstance = null) {
2144
- const props = {
2145
- onInput: (e, nodeName, values) => {
2146
- // TODO - check for out of bounds input
2147
- this.seekTo(this.timeConverter.smpteToCurrentTimeFromStart(values));
2148
- },
2149
- onInputFocus: () => {
2150
- /*
2151
- lock the control control bar in place to
2152
- prevent it from unexpectedly animating away while
2153
- doing timecode input
2154
- */
2155
- this.videojs.options_.inactivityTimeout = 0;
2156
- this.videojs.cache_.inactivityTimeout = 0;
2157
- this.videojs.reportUserActivity();
2158
-
2159
- // pause the video, expected behaviour when editing timecode position
2160
- this.videojs.pause();
2161
- },
2162
- onInputBlur: () => {
2163
- this.videojs.options().inactivityTimeout = constants$2.defaults.inactivityTimeout;
2164
- this.videojs.cache_.inactivityTimeout = constants$2.defaults.inactivityTimeout;
2165
- },
2166
- };
2167
-
2168
- if (externalTimecodeInstance !== null) { // setting up an external playback timecode
2169
- externalTimecodeInstance.props = Object.assign(externalTimecodeInstance.props, props);
2170
- this.externalTimecodes.playback.push(externalTimecodeInstance);
2171
- } else if (this.props.timecode === true) { // player uses timecode
2172
- // construct an in-frame timecode, if controls are enabled
2173
- if (this.props.controls !== false) {
2174
- const target = document.getElementsByClassName('vjs-volume-panel')[0];
2175
- // main timecode representing playhead position
2176
- this.timeCode = new TimeCodeDisplay(Object.assign(props, { target, appendAfter: true }));
2177
-
2178
- // read-only timecode representing video duration
2179
- this.durationTimeCode = new TimeCodeDisplay({
2180
- target: this.timeCode.container,
2181
- appendAfter: true,
2182
- });
2183
- }
2184
- }
2185
-
2186
- if (this.timeCode || this.externalTimecodes.playback.length > 0) {
2187
- // subscribe to events
2188
- this.videojs.on('timeupdate', () => {
2189
- this.updateTimeCode();
2190
- });
2191
-
2192
- this.videojs.on('play', () => {
2193
- this.updateTimeCodeFrame();
2194
- });
2195
- }
2196
- }
2197
-
2198
- updateTimeCode() { // HH:MM:SS (the usual)
2199
- const currentSecond = this.videojs.currentTime();
2200
- const props = this.timeConverter.secondsFromStartToSMPTE(currentSecond);
2201
-
2202
- this.timeCode && this.timeCode.update(props);
2203
-
2204
- if (this.externalTimecodes.playback.length > 0) {
2205
- this.externalTimecodes.playback.forEach((timecode) => {
2206
- timecode.update(props);
2207
- });
2208
- }
2209
- }
2210
-
2211
- updateTimeCodeFrame() { // update the frame in this second -> 00:00:00:XX
2212
- const frame = this.timeConverter.currentFrame(this.videojs.currentTime()); // -> :XX
2213
-
2214
- this.timeCode && this.timeCode.update({ frame }); // TODO - figure out a way to drop frames
2215
-
2216
- if (this.externalTimecodes.playback.length > 0) {
2217
- this.externalTimecodes.playback.forEach((timecode) => {
2218
- timecode.update({ frame });
2219
- });
2220
- }
2221
-
2222
- // recursive invocation if still playing
2223
- if (!this.videojs.paused()) {
2224
- this.rafID = requestAnimationFrame(() => { this.updateTimeCodeFrame(); });
2225
- }
2226
- }
2227
-
2228
- setDurationTimeCode() { // happens once
2229
- const durationInSeconds = this.videojs.duration();
2230
- const props = this.timeConverter.secondsFromStartToSMPTE(durationInSeconds);
2231
- props.frame = this.timeConverter.secondsToFrame(durationInSeconds);
2232
-
2233
- this.durationTimeCode.update(props);
2234
- this.durationTimeCode.container.classList.add('duration');
2235
- }
2236
-
2237
- bindHotkeys() { // keyboard shortcuts
2238
- this.bindings = [
2239
- {
2240
- key: ['space', 'k'],
2241
- method: (e) => {
2242
- // prevents page scroll on space, just like YT
2243
- (e.preventDefault) && e.preventDefault();
2244
- this.togglePlayback();
2245
- },
2246
- },
2247
- {
2248
- key: 'j',
2249
- method: () => {
2250
- this.videojs.hasStarted_ && this.jumpBackward();
2251
- },
2252
- },
2253
- {
2254
- key: 'l',
2255
- method: () => {
2256
- this.videojs.hasStarted_ && this.jumpForward();
2257
- },
2258
- },
2259
- {
2260
- key: ['.', 'right'],
2261
- method: () => {
2262
- this.frameForward();
2263
- },
2264
- },
2265
- {
2266
- key: [',', 'left'],
2267
- method: () => {
2268
- this.frameBackward();
2269
- },
2270
- },
2271
- {
2272
- key: 'f',
2273
- method: () => {
2274
- this.toggleFullscreen();
2275
- },
2276
- },
2277
- {
2278
- key: 'm',
2279
- method: () => {
2280
- this.toggleMute();
2281
- },
2282
- },
2283
- {
2284
- key: 'up',
2285
- method: (e) => {
2286
- e.preventDefault && e.preventDefault(); // prevent page scroll
2287
- this.volumeUp();
2288
- },
2289
- },
2290
- {
2291
- key: 'down',
2292
- method: (e) => {
2293
- e.preventDefault && e.preventDefault(); // prevent page scroll
2294
- this.volumeDown();
2295
- },
2296
- },
2297
- {
2298
- key: 'h',
2299
- method: () => {
2300
- this.help.toggle();
2301
- },
2302
- },
2303
- {
2304
- key: 'n',
2305
- method: () => {
2306
- this.menu && this.menu.toggle();
2307
- },
2308
- },
2309
- ];
2310
-
2311
- if (this.props.subtitles) {
2312
- this.bindings.push({
2313
- key: 'c',
2314
- method: () => {
2315
- this.toggleSubs();
2316
- },
2317
- });
2318
- }
2319
-
2320
- // bind
2321
- this.bindings.forEach((binding) => {
2322
- mousetrap.bind(binding.key, (e) => {
2323
- binding.method(e);
2324
- this.onHotkeyPressed(binding.key);
2325
- });
2326
- });
2327
- }
2328
-
2329
- unbindHotKeys() {
2330
- this.bindings && this.bindings.forEach((binding) => {
2331
- mousetrap.unbind(binding.key);
2332
- });
2333
- }
2334
-
2335
- bindEvents() {
2336
- this.videojs.on('volumechange', () => { // persist volume changes
2337
- this.persistSetting(constants$2.settings.volume, this.volume);
2338
- });
2339
-
2340
- this.videojs.textTracks().on('change', () => { // determine subtitles state
2341
- const tracks = this.videojs.textTracks().tracks_;
2342
- const selected = tracks.filter((track) => (track.mode === 'showing'));
2343
- const areAnyTxtTracksActive = selected.length > 0; // -> true if any text tracks are showing
2344
-
2345
- if (areAnyTxtTracksActive) { // update container with appropriate className
2346
- this.videojs.el_.classList.add(constants$2.classNames.subtitlesActive);
2347
- this.persistSetting(constants$2.settings.subs, true);
2348
- this.persistSetting(constants$2.settings.subLang, selected[0].language);
2349
- } else {
2350
- this.videojs.el_.classList.remove(constants$2.classNames.subtitlesActive);
2351
- this.persistSetting(constants$2.settings.subs, false);
2352
- }
2353
- });
2354
- }
2355
-
2356
- volumeUp() {
2357
- this.videojs.controlBar.volumePanel.volumeControl.volumeBar.stepForward();
2358
- this.osd.feedback(constants$2.osd.volchange, { volume: this.volume });
2359
- }
2360
-
2361
- volumeDown() {
2362
- this.videojs.controlBar.volumePanel.volumeControl.volumeBar.stepBack();
2363
- this.osd.feedback(constants$2.osd.volchange, { volume: this.volume });
2364
- }
2365
-
2366
- toggleMute() {
2367
- if (!this.videojs.muted()) {
2368
- this.videojs.muted(true);
2369
- this.osd.feedback(constants$2.osd.volchange, { volume: this.volume });
2370
- } else {
2371
- this.videojs.muted(false);
2372
- this.osd.feedback(constants$2.osd.volchange, { volume: this.volume });
2373
- }
2374
- }
2375
-
2376
- toggleFullscreen() {
2377
- if (this.videojs.isFullscreen()) {
2378
- this.videojs.exitFullscreen();
2379
- } else {
2380
- this.videojs.requestFullscreen();
2381
- }
2382
- }
2383
-
2384
- seek(secs) { // relative to current playback time
2385
- let time = this.videojs.currentTime() + secs;
2386
-
2387
- if (time < 0) {
2388
- time = 0;
2389
- }
2390
-
2391
- this.videojs.currentTime(time);
2392
- }
2393
-
2394
- seekTo(sec) {
2395
- !this.videojs.hasStarted() && this.videojs.hasStarted(true);
2396
- this.videojs.currentTime(sec);
2397
- }
2398
-
2399
- jumpForward() {
2400
- this.seek(constants$2.defaults.jumpSeconds);
2401
- this.osd.feedback(constants$2.osd.jumpForward);
2402
- }
2403
-
2404
- jumpBackward() {
2405
- this.seek(-constants$2.defaults.jumpSeconds);
2406
- this.osd.feedback(constants$2.osd.jumpBackward);
2407
- }
2408
-
2409
- frameForward() {
2410
- const thisFrame = this.timeConverter.secondsToFramesFromStart(this.videojs.currentTime());
2411
- const nextFrame = Math.round(thisFrame) + 1;
2412
-
2413
- this.videojs.currentTime(this.timeConverter.frameToCurrentTimeFromStart(nextFrame));
2414
- }
2415
-
2416
- frameBackward() {
2417
- const thisFrame = this.timeConverter.secondsToFramesFromStart(this.videojs.currentTime());
2418
- const previousFrame = Math.round(thisFrame) - 1;
2419
-
2420
- this.videojs.currentTime(this.timeConverter.frameToCurrentTimeFromStart(previousFrame));
2421
- }
2422
-
2423
- secondsToFramesFromStart(seconds) {
2424
- const frameRate = this.props.frameRate || constants$2.defaults.frameRate;
2425
- let frame = seconds * frameRate;
2426
- frame = Math.round(frame);
2427
- return frame; // we're on this frame, could be for example 23.45
2428
- }
2429
-
2430
- doEsothericWork() { // non critical parts, that are nice to have
2431
- if (this.props.color) { // change the seekbar color from props
2432
- document.getElementsByClassName('vjs-play-progress')[0].style.backgroundColor = this.props.color;
2433
- }
2434
-
2435
- // feedback gizmo, mainly for feedback on hotkey-presses
2436
- this.osd = new OSD({
2437
- target: this.videoEl.parentElement,
2438
- });
2439
-
2440
- // add a cinema button if a callback is passed in props
2441
- if (this.props.onCinema) {
2442
- this.videoEl.parentElement.classList.add('has-cinema-button');
2443
- this.cinema = false;
2444
-
2445
- this.addButtonToControlBar({
2446
- onClick: () => {
2447
- this.props.onCinema(!this.cinema);
2448
- this.cinema = !this.cinema;
2449
- },
2450
- componentName: 'CinemaButton',
2451
- className: 'vdt-cinema-button',
2452
- title: 'Cinema-mode',
2453
- });
2454
- }
2455
-
2456
- // add a screenshot button if a callback is passed in props
2457
- if (this.props.onScreenshot) {
2458
- this.videoEl.parentElement.classList.add('has-screenshot-button');
2459
-
2460
- this.addButtonToControlBar({
2461
- onClick: () => {
2462
- this.props.onScreenshot(this.videojs.currentTime());
2463
- },
2464
- componentName: 'ScreenshotButton',
2465
- className: 'vdt-screenshot-button',
2466
- title: 'Screenshot',
2467
- });
2468
- }
2469
-
2470
- // add a help overlay explaining keyboard controls
2471
- this.help = new HelpOverlay({
2472
- target: this.videoEl.parentElement,
2473
- onToggle: (willShow) => {
2474
- if (this.menu && this.menu.opened) {
2475
- this.menu.close();
2476
- }
2477
- if (willShow && !this.controlsBelowPlayer) {
2478
- this.hideControls();
2479
- } else {
2480
- this.showControls();
2481
- }
2482
- },
2483
- });
2484
- }
2485
-
2486
- restoreSettings() { // restore settings, like volume level or language track
2487
- // get values from storage
2488
- const volume = localStorage.getItem(constants$2.settings.volume);
2489
- const subsEnabled = localStorage.getItem(constants$2.settings.subs);
2490
- const subLang = localStorage.getItem(constants$2.settings.subLang);
2491
-
2492
- // values vs default behaviours in a neatly readable map
2493
- this.settings = {
2494
- // avoid .99999999 and other float pecularities in js
2495
- [constants$2.settings.volume]: (volume !== null) ? Math.round(volume * 100) / 100 : 1,
2496
- [constants$2.settings.subLang]: (subLang !== null) ? subLang : 'en',
2497
- [constants$2.settings.subs]: (subsEnabled !== null) ? JSON.parse(subsEnabled) : false,
2498
- };
2499
-
2500
- // apply values (settings)
2501
- this.volume = this.settings[constants$2.settings.volume]; // volume
2502
-
2503
- if (this.settings[constants$2.settings.subs]) { // subtitles
2504
- this.enableSubs();
2505
- }
2506
- }
2507
-
2508
- persistSetting(key, val) {
2509
- this.settings[key] = val;
2510
- localStorage.setItem(key, val);
2511
- }
2512
-
2513
- enableSubs() {
2514
- const subs = this.videojs.textTracks();
2515
-
2516
- for (let i = 0; i < subs.length; i++) {
2517
- const sub = subs[i];
2518
- if (sub.language === this.settings[constants$2.settings.subLang]) {
2519
- sub.mode = 'showing';
2520
- } else {
2521
- sub.mode = 'hidden';
2522
- }
2523
- }
2524
- }
2525
-
2526
- disableSubs() {
2527
- const subs = this.videojs.textTracks();
2528
-
2529
- for (let i = 0; i < subs.length; i++) {
2530
- const sub = subs[i];
2531
- sub.mode = 'hidden';
2532
- }
2533
- }
2534
-
2535
- toggleSubs() {
2536
- if (this.videojs.textTracks().length > 0) { // check if subtitles exist
2537
- if (this.videojs.textTracks().tracks_.filter((track) => (track.mode === 'showing')).length > 0) { // a certain subtitle track is enabled
2538
- this.disableSubs();
2539
- } else {
2540
- this.enableSubs();
2541
- }
2542
- }
2543
- }
2544
-
2545
- /*
2546
- A quick example
2547
- options = {
2548
- onClick: ()=>{},
2549
- componentName: 'ScreenshotButton',
2550
- className: 'vdt-screenshot-button'
2551
- }
2552
- */
2553
- addButtonToControlBar(options) {
2554
- const Button = videojs.getComponent('Button');
2555
- const CustomButton = videojs.extend(Button, {
2556
- /* eslint-disable func-names, object-shorthand */
2557
- constructor: function () {
2558
- Button.apply(this, arguments);
2559
- options.className && this.el_.classList.add(options.className);
2560
- if (options.title) { this.el_.title = options.title; }
2561
- },
2562
- handleClick: () => {
2563
- options.onClick && options.onClick();
2564
- },
2565
- });
2566
-
2567
- videojs.registerComponent(options.componentName, CustomButton);
2568
- this.videojs.getChild('controlBar').addChild(options.componentName, {}, 7);
2569
- }
2570
-
2571
- // convenient getters / setters
2572
- get volume() {
2573
- return this.videojs.controlBar.volumePanel.volumeControl.volumeBar.getPercent();
2574
- }
2575
-
2576
- set volume(floatVal) {
2577
- if (floatVal === 0) {
2578
- this.videojs.muted(true);
2579
- } else {
2580
- this.videojs.muted(false);
2581
- this.videojs.volume(floatVal);
2582
- }
2583
- }
2584
-
2585
- setupMenu() {
2586
- const menuItems = this.props.menuItems || [];
2587
- const sourcesMenuItems = [];
2588
- if (this.props.sources.length > 1) { // show only if more than 1 source
2589
- this.props.sources.forEach((source) => {
2590
- if (source.label) {
2591
- sourcesMenuItems.push({
2592
- title: source.label,
2593
- onClick: () => {
2594
- this.src = source;
2595
- },
2596
- });
2597
- }
2598
- });
2599
-
2600
- menuItems.push({
2601
- title: 'Sources',
2602
- getActiveIndex: () => {
2603
- const matchingSource = this.props.sources.find((source) => (source.src === this.videojs.src()));
2604
- return sourcesMenuItems.indexOf(sourcesMenuItems.find((menuItem) => (menuItem.title === matchingSource.label)));
2605
- },
2606
- items: sourcesMenuItems,
2607
- });
2608
- }
2609
-
2610
- // subtitles
2611
- if (this.props.subtitles && this.props.subtitles.length > 0) {
2612
- // off option for subs
2613
- const subtitleMenuItems = [{
2614
- title: 'Off',
2615
- onClick: () => {
2616
- this.disableSubs();
2617
- },
2618
- }];
2619
-
2620
- // quick toggle in the control bar
2621
- // this.addButtonToControlBar({
2622
- // onClick: () => {
2623
- // this.props.onCinema(!this.cinema);
2624
- // this.cinema = !this.cinema;
2625
- // },
2626
- // componentName: 'CinemaButton',
2627
- // className: 'vdt-cinema-button',
2628
- // });
2629
-
2630
- this.props.subtitles.forEach((subtitle, index) => {
2631
- subtitleMenuItems.push({
2632
- title: subtitle.label,
2633
- onClick: () => {
2634
- const subs = this.videojs.textTracks();
2635
- for (let i = 0; i < subs.length; i++) {
2636
- const sub = subs[i];
2637
- if (i === index) {
2638
- sub.mode = 'showing';
2639
- } else {
2640
- sub.mode = 'hidden';
2641
- }
2642
- }
2643
- },
2644
- });
2645
- });
2646
-
2647
- menuItems.push(
2648
- {
2649
- title: 'Subtitles',
2650
- getActiveIndex: () => {
2651
- const subs = this.videojs.textTracks();
2652
- for (let i = 0; i < subs.length; i++) {
2653
- const sub = subs[i];
2654
- if (sub.mode === 'showing') {
2655
- return i + 1; // offset "off" option
2656
- }
2657
- }
2658
-
2659
- return 0; // if none are showing, then probably "off"
2660
- },
2661
- items: subtitleMenuItems,
2662
- },
2663
- );
2664
- }
2665
-
2666
- if (menuItems.length > 0) {
2667
- const target = this.videojs.el_;
2668
-
2669
- this.menu = new Menu({
2670
- target,
2671
- items: menuItems,
2672
- onOpen: () => {
2673
- !this.controlsBelowPlayer && this.hideControls();
2674
- },
2675
- onClose: () => {
2676
- !this.controlsBelowPlayer && this.videojs.controls(true);
2677
- },
2678
- });
2679
-
2680
- // add a cogwheel button to the player
2681
- this.videoEl.parentElement.classList.add('has-vdt-menu');
2682
-
2683
- this.addButtonToControlBar({
2684
- onClick: () => {
2685
- this.menu.toggle();
2686
- },
2687
- componentName: 'SettingsMenuButton',
2688
- className: 'vdt-settings-menu-button',
2689
- title: 'Settings',
2690
- });
2691
- }
2692
- }
2693
-
2694
- hideControls() {
2695
- this.videojs.userActive(false);
2696
- this.videojs.controls(false);
2697
- this.videojs.fluid(true);
2698
-
2699
- this.videoEl.parentElement.classList.remove('vjs-user-active');
2700
- this.videojs.userActive(false);
2701
- }
2702
-
2703
- showControls() {
2704
- this.videojs.fluid(!this.controlsBelowPlayer);
2705
- this.videojs.controls(true);
2706
- this.videojs.userActive(true);
2707
- }
2708
-
2709
- toggleControls() {
2710
- if (this.videojs.controls()) {
2711
- this.hideControls();
2712
- } else {
2713
- this.showControls();
2714
- }
2715
- }
2716
-
2717
- onHotkeyPressed(key) {
2718
- if (key !== 'h' && this.help.state.active) {
2719
- this.help.hide();
2720
- }
2721
- }
2722
-
2723
- // eslint-disable-next-line class-methods-use-this
2724
- triggerHotkey(key) {
2725
- mousetrap.trigger(key);
2726
- }
2727
-
2728
- toStart() {
2729
- this.videojs.currentTime(0);
2730
- }
2731
-
2732
- toEnd() {
2733
- this.videojs.currentTime(this.videojs.duration());
2734
- }
2735
-
2736
- replay() {
2737
- this.toStart();
2738
- this.videojs.play();
2739
- }
2740
-
2741
- play() {
2742
- this.videojs.play();
2743
- }
2744
-
2745
- pause() {
2746
- !this.videojs.paused() && this.videojs.pause();
2747
- }
2748
-
2749
- togglePlayback() {
2750
- if (this.videojs.paused()) {
2751
- this.play();
2752
- this.videojs.hasStarted_ && this.osd.feedback(constants$2.osd.play);
2753
- } else {
2754
- this.pause();
2755
- this.osd.feedback(constants$2.osd.pause);
2756
- }
2757
- }
2758
-
2759
- destroy() { // clean up
2760
- this.videojs.paused(true);
2761
- this.rafID && cancelAnimationFrame(this.rafID);
2762
- this.videojs.dispose();
2763
- this.unbindHotKeys();
2764
- this.seekBar && this.seekBar.destroy();
2765
- }
2766
-
2767
- get container() {
2768
- return this.videoEl.parentElement;
2769
- }
2770
-
2771
- set onDark(bool) {
2772
- if (bool) {
2773
- this.container.classList.add('on-dark');
2774
- } else {
2775
- this.container.classList.remove('on-dark');
2776
- }
2777
-
2778
- if (this.seekBar) {
2779
- this.seekBar.onDark = bool;
2780
- }
2781
- }
2782
-
2783
- get onDark() {
2784
- return this.container.classList.contains('on-dark');
2785
- }
2786
-
2787
- set controlsBelowPlayer(bool) {
2788
- if (this.videojs.controls()) {
2789
- if (bool) {
2790
- this.container.classList.add('controls-below-player');
2791
- this.videojs.fluid(false);
2792
- } else {
2793
- this.container.classList.remove('controls-below-player');
2794
- this.videojs.fluid(true);
2795
- }
2796
- }
2797
- }
2798
-
2799
- get controlsBelowPlayer() {
2800
- return this.container.classList.contains('controls-below-player');
2801
- }
2802
-
2803
- get smpte() {
2804
- const currentSecond = this.videojs.currentTime();
2805
- return this.timeConverter.secondsFromStartToSMPTE(currentSecond);
2806
- }
2807
-
2808
- set smpte(smpte) {
2809
- const currentSecond = this.videojs.currentTime();
2810
- return this.timeConverter.secondsFromStartToSMPTE(currentSecond);
2811
- }
2812
-
2813
- set src({ src, type = 'video/mp4', label = null }) {
2814
- const currentTime = this.videojs.currentTime();
2815
- // iterate trough sources, find existing matches
2816
- const source = this.props.sources.find((aSource) => (aSource.src === src));
2817
- // if no match try adding source
2818
-
2819
- const onReady = () => {
2820
- let initdone = false;
2821
- // wait for video metadata to load, then set time
2822
- this.videojs.on('loadedmetadata', () => {
2823
- this.videojs.currentTime(currentTime);
2824
- });
2825
-
2826
- // iPhone/iPad need to play first, then set the time
2827
- // events: https://www.w3.org/TR/html5/embedded-content-0.html#mediaevents
2828
- this.videojs.on('canplaythrough', () => {
2829
- if (!initdone) {
2830
- this.videojs.currentTime(currentTime);
2831
- initdone = true;
2832
- this.videojs.play();
2833
- }
2834
- });
2835
- };
2836
-
2837
- if (source && typeof souce === 'Array' && source.length > 0) {
2838
- // TODO - if(source.length > 1) - try one by one until a successful load happens
2839
- this.videojs.addClass('vjs-waiting');
2840
- this.videojs.src({
2841
- ...source[0],
2842
- });
2843
- onReady();
2844
- } else {
2845
- this.videojs.addClass('vjs-waiting');
2846
- this.videojs.src(this.addSrc(src, type, label));
2847
- onReady();
2848
- }
2849
- }
2850
-
2851
- get src() {
2852
- return this.videojs.src();
2853
- }
2854
-
2855
- addSrc(src, type, label) {
2856
- const newSource = {
2857
- src,
2858
- type,
2859
- label,
2860
- };
2861
- const index = this.props.sources.push(newSource) - 1;
2862
-
2863
- return this.props.sources[index];
2864
- }
2865
-
2866
- removeSrc(url) {
2867
- // should remove all url matching sources
2868
- }
2869
-
2870
- set controls(bool) {
2871
- if (bool) {
2872
- this.showControls();
2873
- } else {
2874
- this.hideControls();
2875
- }
2876
- }
2877
-
2878
- get percentLoaded() {
2879
- const r = this.videojs.buffered();
2880
-
2881
- const percentLoaded = (r.end(0) / this.videojs.duration()) * 100;
2882
- return percentLoaded;
2883
- }
2884
- }
2885
-
2886
- /*
2887
- Some video.js prototype overrides, that
2888
- help with undesired behaviour
2889
- */
2890
-
2891
- // take over space button, prevent video.js default behaviour
2892
- const button = videojs.prototype.constructor.getComponent('Button');
2893
- // eslint-disable-next-line func-names
2894
- button.prototype.handleKeyDown = function (e) {
2895
- if (e.keyCode === 32) {
2896
- e.preventDefault();
2897
- }
2898
- };
2899
- videojs.prototype.constructor.registerComponent('Button', button);
2900
-
2901
- // video.js opens menu on hover by default, this prevents that
2902
- const subsButton = videojs.prototype.constructor.getComponent('SubsCapsButton');
2903
- class CustomSubsCapsButton extends subsButton {
2904
- constructor(player, options = {}) {
2905
- super(player, options);
2906
- this.menuButton_.off('mouseenter');
2907
- this.menuButton_.off('mouseleave');
2908
- }
2909
- }
2910
-
2911
- /*
2912
- TODO - see what can be done about strange behaviour
2913
- when cc button is clicked with the menu showing
2914
- */
2915
- videojs.prototype.constructor.registerComponent('SubsCapsButton', CustomSubsCapsButton);
2916
-
2917
- /* eslint-disable no-plusplus */
2918
-
2919
- const buttonNames = Object.freeze({
2920
- play: 'play-pause',
2921
- start: 'start',
2922
- fb: 'frame-backward',
2923
- ff: 'frame-forward',
2924
- end: 'end',
2925
- });
2926
-
2927
- const buttons = [ // order matters
2928
- buttonNames.start,
2929
- buttonNames.fb,
2930
- buttonNames.play,
2931
- buttonNames.ff,
2932
- buttonNames.end,
2933
- ];
2934
-
2935
- /*
2936
- Since this could possibly be used in a
2937
- context without the player and vice versa -
2938
- will try and make it as self-contained
2939
- as possible, currently utilizing
2940
- shadow DOM (style encapsulation as main reason),
2941
- https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM
2942
- BEWARE - IE < Edge is unsupported
2943
- */
2944
-
2945
- const CSS = `
2946
- .vdt-external-controls{
2947
- display: flex;
2948
- justify-content: center;
2949
- align-items: center;
2950
- }
2951
-
2952
- .control{
2953
- flex: 1;
2954
- flex-grow: 1;
2955
- height: 100%;
2956
- align-contents: center;
2957
- align-self: center;
2958
- text-align: center;
2959
- vertical-align: middle;
2960
- cursor: pointer;
2961
- user-select: none;
2962
- height: 20px;
2963
- padding-top: 10px;
2964
- padding-bottom: 10px;
2965
- }
2966
-
2967
- .control .icon{
2968
- display: inline-block;
2969
- height: 20px;
2970
- white-space: nowrap;
2971
- opacity: .9;
2972
- }
2973
-
2974
- .control:hover .icon{ opacity: 1; }
2975
-
2976
- .control .icon:before{
2977
- opacity: 1;
2978
- flex: 1;
2979
- align-self: center;
2980
- content: " ";
2981
- display: inline-block;
2982
- margin: 0 auto;
2983
-
2984
- /* triangles */
2985
- width: 0;
2986
- height: 0;
2987
- border-style: solid;
2988
- border-width: 16px 0 16px 23px;
2989
- border-color: transparent transparent transparent ${colors.gray};
2990
- }
2991
-
2992
- .vdt-external-controls.${classNames.onDark} .control .icon:before{
2993
- border-color: transparent transparent transparent ${colors.light};
2994
- }
2995
-
2996
- /* specific buttons */
2997
-
2998
- .control.${buttonNames.start},
2999
- .control.${buttonNames.end}{
3000
- height: 20px;
3001
- }
3002
-
3003
- .control.${buttonNames.play}{
3004
- height: 30px;
3005
- padding-top: 0px;
3006
- padding-bottom: 0px;
3007
- }
3008
-
3009
- .control.${buttonNames.play} .icon{
3010
- height: 30px;
3011
- }
3012
-
3013
- .control.${buttonNames.ff} .icon:before,
3014
- .control.${buttonNames.fb} .icon:before,
3015
- .control.${buttonNames.end} .icon:before,
3016
- .control.${buttonNames.start} .icon:before{
3017
- border-width: 10px 0 10px 16px;
3018
- }
3019
-
3020
- .control.${buttonNames.fb} .icon,
3021
- .control.${buttonNames.start}{
3022
- transform: rotate(180deg);
3023
- }
3024
-
3025
- .control.${buttonNames.end} .icon:after,
3026
- .control.${buttonNames.start} .icon:after{
3027
- content: " ";
3028
- width: 4px;
3029
- height: 20px;
3030
- display: inline-block;
3031
- background: ${colors.gray};
3032
- }
3033
-
3034
- .vdt-external-controls.${classNames.onDark} .control.${buttonNames.end} .icon:after,
3035
- .vdt-external-controls.${classNames.onDark} .control.${buttonNames.start} .icon:after{
3036
- background: ${colors.light};
3037
- }
3038
-
3039
- /* stateful styles */
3040
- .control.${buttonNames.play}.playing .icon:before{
3041
- height: 30px;
3042
- width: 6px;
3043
- border-width: 0px 8px 0px 8px;
3044
- border-color: transparent ${colors.gray} transparent ${colors.gray};
3045
- }
3046
-
3047
- .vdt-external-controls.${classNames.onDark} .control.${buttonNames.play}.playing .icon:before{
3048
- border-color: transparent ${colors.light} transparent ${colors.light};
3049
- }
3050
-
3051
- .vdt-external-controls.${classNames.loading} .control.${buttonNames.play}{
3052
- border-color: transparent #f00 transparent #f00;
3053
- }
3054
- `;
3055
-
3056
- class ExternalControls {
3057
- constructor(props) {
3058
- this.props = props;
3059
- reactiveSetters.apply(this);
3060
-
3061
- this.state = {
3062
- playing: false,
3063
- onDark: false,
3064
- loading: false,
3065
- };
3066
-
3067
- this.shadowRoot = document.createElement('div'); // will attach shadow here, after appending to target
3068
- this.shadowRoot.className = 'vdt-external-controls-wrapper';
3069
- this.container = document.createElement('div');
3070
- this.container.className = 'vdt-external-controls';
3071
-
3072
- if (this.props.target) {
3073
- this.props.target.appendChild(this.shadowRoot);
3074
- this.shadow = this.shadowRoot.attachShadow({ mode: 'open' });
3075
- // attach styles
3076
- this.style = document.createElement('style');
3077
- this.style.type = 'text/css';
3078
- this.style.appendChild(document.createTextNode(CSS));
3079
- this.shadow.appendChild(this.style);
3080
-
3081
- // attach container to shadow root
3082
- this.shadow.appendChild(this.container);
3083
- }
3084
-
3085
- this.handleNewProps(props);
3086
- this.bindEvents();
3087
- this.draw();
3088
- }
3089
-
3090
- draw() {
3091
- this.buttons = {}; // map of html nodes for DOM manipulations
3092
- const excludes = this.props.excludes || [];
3093
-
3094
- buttons.forEach((buttonName) => {
3095
- if (excludes.indexOf(buttonName) < 0) {
3096
- const btn = document.createElement('div');
3097
- btn.className = `control ${buttonName}`;
3098
- btn.innerHTML = '<span class="icon"></span>';
3099
-
3100
- btn.onclick = () => {
3101
- this.handlePress(buttonName);
3102
- };
3103
-
3104
- this.buttons[buttonName] = btn;
3105
- this.container.appendChild(btn);
3106
- }
3107
- });
3108
- }
3109
-
3110
- /*
3111
- bind to play / pause / stop and other events,
3112
- so that controls reflect the player state
3113
- */
3114
- bindEvents() {
3115
- if (this.props.player && this.props.player.videojs) {
3116
- const { player } = this.props;
3117
-
3118
- player.videojs.on('play', () => {
3119
- this.playing = true;
3120
- });
3121
-
3122
- player.videojs.on('pause', () => {
3123
- this.playing = false;
3124
- });
3125
-
3126
- player.videojs.on('stop', () => {
3127
- this.playing = false;
3128
- });
3129
- }
3130
- }
3131
-
3132
- handlePress(buttonName) {
3133
- if (this.props.player) { // control a vdt player instance directly
3134
- const player = this.props.player;
3135
- console.log(player);
3136
- player.osd && player.osd.disable(); // temporarily disable OSD
3137
-
3138
- switch (buttonName) {
3139
- case buttonNames.start:
3140
- player.toStart();
3141
- break;
3142
- case buttonNames.fb:
3143
- player.triggerHotkey('left');
3144
- break;
3145
- case buttonNames.play:
3146
- player.triggerHotkey('k'); // TODO - triggering hotkeys has a side effect - OSD will give feedback
3147
- break;
3148
- case buttonNames.ff:
3149
- player.triggerHotkey('right');
3150
- break;
3151
- case buttonNames.end:
3152
- player.toEnd();
3153
- break;
3154
- }
3155
-
3156
- player.osd && player.osd.enable(); // re-enable OSD
3157
- } else {
3158
- switch (buttonName) {
3159
- case buttonNames.start:
3160
- this.props.onPrev && this.props.onPrev();
3161
- break;
3162
- case buttonNames.fb:
3163
- this.props.onFB && this.props.onFB();
3164
- break;
3165
- case buttonNames.play:
3166
- if (this.playing) {
3167
- this.props.onPause && this.props.onPause();
3168
- } else {
3169
- this.props.onPlay && this.props.onPlay();
3170
- }
3171
- break;
3172
- case buttonNames.ff:
3173
- this.props.onFF && this.props.onFF();
3174
- break;
3175
- case buttonNames.end:
3176
- this.props.onNext && this.props.onNext();
3177
- break;
3178
- }
3179
- }
3180
- }
3181
-
3182
- // setters and getters for state
3183
- set playing(bool) {
3184
- if (bool) {
3185
- this.state.playing = true;
3186
- this.buttons[buttonNames.play].classList.add('playing');
3187
- } else {
3188
- this.state.playing = false;
3189
- this.buttons[buttonNames.play].classList.remove('playing');
3190
- }
3191
- }
3192
-
3193
- get playing() {
3194
- return this.state.playing;
3195
- }
3196
-
3197
- set onDark(bool) {
3198
- if (bool) {
3199
- this.state.onDark = true;
3200
- this.container.classList.add(classNames.onDark);
3201
- } else {
3202
- this.state.onDark = false;
3203
- this.container.classList.remove(classNames.onDark);
3204
- }
3205
- }
3206
-
3207
- get onDark() {
3208
- return this.container.classList.contains(classNames.onDark);
3209
- }
3210
-
3211
- set loading(bool) {
3212
- if (bool) {
3213
- this.state.loading = true;
3214
- this.container.classList.add(classNames.loading);
3215
- } else {
3216
- this.state.loading = false;
3217
- this.container.classList.remove(classNames.loading);
3218
- }
3219
- }
3220
-
3221
- get loading() {
3222
- return this.state.loading;
3223
- }
3224
- }
3225
-
3226
- ExternalControls.buttonNames = buttonNames;
3227
-
3228
- export { ExternalControls, Player, SeekBar, TimeCodeDisplay, VideoTime };