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

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