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