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