@vidispine/vdt-videojs 23.2.0-pre.1 → 23.2.0

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/src/player.js DELETED
@@ -1,1029 +0,0 @@
1
- /* eslint-disable no-underscore-dangle */
2
- /* eslint-disable import/no-named-as-default-member */
3
- /*
4
- Eslint rules disabled for lines like these:
5
-
6
- this.props.target && this.props.target.appendChild(this.videoEl); //validity check before expression
7
- document.getElementsByClassName('vjs-volume-panel')[0] //rule asks for destructuring
8
- this.videojs.hasStarted_ //dangling underscore produced by video.js in this case
9
- */
10
- // eslint-disable-next-line max-classes-per-file
11
- import mousetrap from 'mousetrap';
12
- import videojs from 'video.js/dist/alt/video.core.min';
13
-
14
- import constants from './constants';
15
- import { reactiveSetters } from './helpers';
16
- import HelpOverlay from './helpOverlay';
17
- import Menu from './menu';
18
- import OSD from './osd';
19
- import SeekBar from './seekBar';
20
- import TimeCodeDisplay from './timeCodeDisplay';
21
- import VideoTime from './videoTime';
22
-
23
- import './player.scss';
24
-
25
- const correctMimeTypes = {
26
- 'audio/x-aac': 'audio/aac',
27
- };
28
- const sanitizeMimeType = (mimeType) => correctMimeTypes[mimeType] || mimeType;
29
-
30
- export default class Player {
31
- constructor(props) {
32
- this.props = props; // save props
33
- reactiveSetters.apply(this); // ability to handle new props via setters
34
-
35
- const classNames = [
36
- // add conditional css classes here for the whole container
37
- 'video-js',
38
- 'vjs-big-play-centered',
39
- this.props.posterUrl ? constants.classNames.poster : '',
40
- this.props.subtitles && this.props.subtitles.length > 0 ? constants.classNames.subtitles : '',
41
- 'init', // make player height 0 while no metadata for video size available with css
42
- ];
43
-
44
- this.externalTimecodes = {
45
- // either one of these can hold an array of TimeCodeDisplay instances
46
- duration: [],
47
- playback: [],
48
- };
49
-
50
- this.timeConverter = new VideoTime({
51
- frameRate: this.props.frameRate || constants.defaults.frameRate,
52
- });
53
-
54
- /*
55
- video.js requires a html video to initialize
56
- */
57
-
58
- this.videoEl = document.createElement('video');
59
- this.videoEl.crossOrigin = this.props.crossOrigin;
60
- this.videoEl.id = 'vdt-video';
61
- this.videoEl.className = classNames.join(' ');
62
- this.videoEl.controls = true; // remove this line for no-controls videojs player
63
-
64
- // attach sources, sort mp4 on top of source list
65
- if (this.props.sources) {
66
- // sort mp4 at the top
67
- if (this.props.sources.length > 1) {
68
- this.props.sources.sort((a) => (a.type.indexOf('mp4') > 0 ? 1 : -1));
69
- }
70
-
71
- // construct <source> els
72
- this.props.sources.forEach((source) => {
73
- const aSource = document.createElement('source');
74
- aSource.src = source.src;
75
- aSource.type = sanitizeMimeType(source.type);
76
- this.videoEl.appendChild(aSource);
77
- });
78
- } // handle else with err later
79
-
80
- this.videoEl.onloadedmetadata = () => {
81
- // read-only duration timecode
82
- if (this.durationTimeCode) {
83
- this.setDurationTimeCode();
84
- }
85
-
86
- if (this.videojs.el_) this.videojs.el_.classList.remove('init');
87
- };
88
-
89
- // if video has not loaded by now, show the player w/ errors
90
- setTimeout(() => {
91
- if (this.videojs.el_) this.videojs.el_.classList.remove('init');
92
- }, 2000);
93
-
94
- // attach subtitle tracks (these should come in last in the <video /> node)
95
- if (this.props.subtitles) {
96
- this.props.subtitles.forEach((sub) => {
97
- const subtitleTag = document.createElement('track');
98
- subtitleTag.kind = 'subtitles';
99
- subtitleTag.srclang = sub.lang;
100
- subtitleTag.label = sub.label;
101
- // subtitleTag.default = (index === 0) //can use this later to set a default track
102
-
103
- if (typeof sub.src === 'string') {
104
- subtitleTag.src = sub.src; // external file
105
- } else {
106
- subtitleTag.src = window.URL.createObjectURL(sub.src); // file url from blob
107
- }
108
-
109
- this.videoEl.appendChild(subtitleTag);
110
- });
111
- }
112
-
113
- // attach to dom and construct videojs player
114
- if (this.props.target) this.props.target.appendChild(this.videoEl);
115
-
116
- const showDefaultTimeDisplay = this.props.timecode !== true;
117
- const controlsEnabled = this.props.controls !== false;
118
- const options = this.props.options || {};
119
-
120
- // eslint-disable-next-line new-cap
121
- this.videojs = new videojs('vdt-video', {
122
- responsive: true,
123
- fluid: true,
124
- controls: controlsEnabled,
125
- alwaysCaptureHotkeys: false,
126
- controlBar: {
127
- RemainingTimeDisplay: false,
128
- timeDivider: showDefaultTimeDisplay, // relates to built-in time display
129
- durationDisplay: showDefaultTimeDisplay,
130
- currentTimeDisplay: showDefaultTimeDisplay,
131
- },
132
- inactivityTimeout: this.audioOnly
133
- ? constants.defaults.audioInactivityTimeout
134
- : constants.defaults.inactivityTimeout,
135
- preload: 'auto',
136
- captionsButton: false,
137
- subsCapsButton: false,
138
- subtitlesButton: false,
139
- textTrackSettings: false,
140
- html5: {
141
- nativeTextTracks: false,
142
- },
143
- languages: {
144
- en: {
145
- 'captions off': 'off',
146
- 'subtitles off': 'off',
147
- },
148
- },
149
- ...options,
150
- });
151
-
152
- // set the poster
153
- if (this.props.posterUrl) this.videojs.poster(this.props.posterUrl);
154
-
155
- // set other props that are passed
156
- this.handleNewProps(this.props);
157
-
158
- // lifecycle
159
- this.videojs.ready(() => {
160
- this.bindHotkeys();
161
- this.bindEvents();
162
- if (this.props.onReady) this.props.onReady(this.videojs);
163
-
164
- this.restoreSettings();
165
- this.setupTimeCode();
166
- this.doEsothericWork();
167
- this.setupMenu();
168
- this.setupCustomSeekbar();
169
- });
170
- }
171
-
172
- setupCustomSeekbar() {
173
- if (this.props.seekBar) {
174
- // disable the built-in videojs seekbar
175
- this.videojs.controlBar.progressControl.disable();
176
- this.videojs.controlBar.progressControl.hide();
177
-
178
- const target = this.container.getElementsByClassName('vjs-control-bar')[0];
179
- const props = {
180
- target,
181
- player: this,
182
- onDark: this.props.onDark,
183
- ...this.props.seekBar,
184
- };
185
-
186
- this.seekBar = new SeekBar(props);
187
-
188
- // show & hide markers when controls hide
189
- this.videojs.on('userinactive', () => {
190
- if (!this.controlsBelowPlayer) {
191
- if (this.seekBar) this.seekBar.hideMarkers?.();
192
- } else {
193
- this.seekBar?.showMarkers?.();
194
- }
195
- });
196
-
197
- this.videojs.on('useractive', () => {
198
- if (this.seekBar) this.seekBar.showMarkers();
199
- });
200
- }
201
- }
202
-
203
- setupTimeCode(externalTimecodeInstance = null) {
204
- const props = {
205
- onInput: (e, nodeName, values) => {
206
- // TODO - check for out of bounds input
207
- this.seekTo(this.timeConverter.smpteToCurrentTimeFromStart(values));
208
- },
209
- onInputFocus: () => {
210
- /*
211
- lock the control control bar in place to
212
- prevent it from unexpectedly animating away while
213
- doing timecode input
214
- */
215
- this.videojs.options_.inactivityTimeout = 0;
216
- this.videojs.cache_.inactivityTimeout = 0;
217
- this.videojs.reportUserActivity();
218
-
219
- // pause the video, expected behaviour when editing timecode position
220
- this.videojs.pause();
221
- },
222
- onInputBlur: () => {
223
- this.videojs.options().inactivityTimeout = constants.defaults.inactivityTimeout;
224
- this.videojs.cache_.inactivityTimeout = constants.defaults.inactivityTimeout;
225
- },
226
- };
227
-
228
- if (externalTimecodeInstance !== null) {
229
- // setting up an external playback timecode
230
- // eslint-disable-next-line no-param-reassign
231
- externalTimecodeInstance.props = Object.assign(externalTimecodeInstance.props, props);
232
- this.externalTimecodes.playback.push(externalTimecodeInstance);
233
- } else if (this.props.timecode === true) {
234
- // player uses timecode
235
- // construct an in-frame timecode, if controls are enabled
236
- if (this.props.controls !== false) {
237
- const target = document.getElementsByClassName('vjs-volume-panel')[0];
238
- // main timecode representing playhead position
239
- this.timeCode = new TimeCodeDisplay(Object.assign(props, { target, appendAfter: true }));
240
-
241
- // read-only timecode representing video duration
242
- this.durationTimeCode = new TimeCodeDisplay({
243
- target: this.timeCode.container,
244
- appendAfter: true,
245
- });
246
- }
247
- }
248
-
249
- if (this.timeCode || this.externalTimecodes.playback.length > 0) {
250
- // subscribe to events
251
- this.videojs.on('timeupdate', () => {
252
- this.updateTimeCode();
253
- });
254
-
255
- this.videojs.on('play', () => {
256
- this.updateTimeCodeFrame();
257
- });
258
- }
259
- }
260
-
261
- updateTimeCode() {
262
- // HH:MM:SS (the usual)
263
- const currentSecond = this.videojs.currentTime();
264
- const props = this.timeConverter.secondsFromStartToSMPTE(currentSecond);
265
-
266
- if (this.timeCode) this.timeCode.update(props);
267
-
268
- if (this.externalTimecodes.playback.length > 0) {
269
- this.externalTimecodes.playback.forEach((timecode) => {
270
- timecode.update(props);
271
- });
272
- }
273
- }
274
-
275
- updateTimeCodeFrame() {
276
- // update the frame in this second -> 00:00:00:XX
277
- const frame = this.timeConverter.currentFrame(this.videojs.currentTime()); // -> :XX
278
-
279
- if (this.timeCode) this.timeCode.update({ frame }); // TODO - figure out a way to drop frames
280
-
281
- if (this.externalTimecodes.playback.length > 0) {
282
- this.externalTimecodes.playback.forEach((timecode) => {
283
- timecode.update({ frame });
284
- });
285
- }
286
-
287
- // recursive invocation if still playing
288
- if (!this.videojs.paused()) {
289
- this.rafID = requestAnimationFrame(() => {
290
- this.updateTimeCodeFrame();
291
- });
292
- }
293
- }
294
-
295
- setDurationTimeCode() {
296
- // happens once
297
- const durationInSeconds = this.videojs.duration();
298
- const props = this.timeConverter.secondsFromStartToSMPTE(durationInSeconds);
299
- props.frame = this.timeConverter.secondsToFrame(durationInSeconds);
300
-
301
- this.durationTimeCode.update(props);
302
- this.durationTimeCode.container.classList.add('duration');
303
- }
304
-
305
- bindHotkeys() {
306
- // keyboard shortcuts
307
- this.bindings = [
308
- {
309
- key: ['space', 'k'],
310
- method: (e) => {
311
- // prevents page scroll on space, just like YT
312
- if (e.preventDefault) e.preventDefault();
313
- this.togglePlayback();
314
- },
315
- },
316
- {
317
- key: 'j',
318
- method: () => {
319
- if (this.videojs.hasStarted_) this.jumpBackward();
320
- },
321
- },
322
- {
323
- key: 'l',
324
- method: () => {
325
- if (this.videojs.hasStarted_) this.jumpForward();
326
- },
327
- },
328
- {
329
- key: ['.', 'right'],
330
- method: () => {
331
- this.frameForward();
332
- },
333
- },
334
- {
335
- key: [',', 'left'],
336
- method: () => {
337
- this.frameBackward();
338
- },
339
- },
340
- {
341
- key: 'f',
342
- method: () => {
343
- this.toggleFullscreen();
344
- },
345
- },
346
- {
347
- key: 'm',
348
- method: () => {
349
- this.toggleMute();
350
- },
351
- },
352
- {
353
- key: 'up',
354
- method: (e) => {
355
- if (e.preventDefault) e.preventDefault(); // prevent page scroll
356
- this.volumeUp();
357
- },
358
- },
359
- {
360
- key: 'down',
361
- method: (e) => {
362
- if (e.preventDefault) e.preventDefault(); // prevent page scroll
363
- this.volumeDown();
364
- },
365
- },
366
- {
367
- key: 'h',
368
- method: () => {
369
- this.help.toggle();
370
- },
371
- },
372
- {
373
- key: 'n',
374
- method: () => {
375
- if (this.menu) this.menu.toggle();
376
- },
377
- },
378
- ];
379
-
380
- if (this.props.subtitles) {
381
- this.bindings.push({
382
- key: 'c',
383
- method: () => {
384
- this.toggleSubs();
385
- },
386
- });
387
- }
388
-
389
- // bind
390
- this.bindings.forEach((binding) => {
391
- mousetrap.bind(binding.key, (e) => {
392
- binding.method(e);
393
- this.onHotkeyPressed(binding.key);
394
- });
395
- });
396
- }
397
-
398
- unbindHotKeys() {
399
- if (this.bindings)
400
- this.bindings.forEach((binding) => {
401
- mousetrap.unbind(binding.key);
402
- });
403
- }
404
-
405
- bindEvents() {
406
- this.videojs.on('volumechange', () => {
407
- // persist volume changes
408
- this.persistSetting(constants.settings.volume, this.volume);
409
- });
410
-
411
- this.videojs.textTracks().on('change', () => {
412
- // determine subtitles state
413
- const tracks = this.videojs.textTracks().tracks_;
414
- const selected = tracks.filter((track) => track.mode === 'showing');
415
- const areAnyTxtTracksActive = selected.length > 0; // -> true if any text tracks are showing
416
-
417
- if (areAnyTxtTracksActive) {
418
- // update container with appropriate className
419
- this.videojs.el_.classList.add(constants.classNames.subtitlesActive);
420
- this.persistSetting(constants.settings.subs, true);
421
- this.persistSetting(constants.settings.subLang, selected[0].language);
422
- } else {
423
- this.videojs.el_.classList.remove(constants.classNames.subtitlesActive);
424
- this.persistSetting(constants.settings.subs, false);
425
- }
426
- });
427
- }
428
-
429
- volumeUp() {
430
- this.videojs.controlBar.volumePanel.volumeControl.volumeBar.stepForward();
431
- this.osd.feedback(constants.osd.volchange, { volume: this.volume });
432
- }
433
-
434
- volumeDown() {
435
- this.videojs.controlBar.volumePanel.volumeControl.volumeBar.stepBack();
436
- this.osd.feedback(constants.osd.volchange, { volume: this.volume });
437
- }
438
-
439
- toggleMute() {
440
- if (!this.videojs.muted()) {
441
- this.videojs.muted(true);
442
- this.osd.feedback(constants.osd.volchange, { volume: this.volume });
443
- } else {
444
- this.videojs.muted(false);
445
- this.osd.feedback(constants.osd.volchange, { volume: this.volume });
446
- }
447
- }
448
-
449
- toggleFullscreen() {
450
- if (this.videojs.isFullscreen()) {
451
- this.videojs.exitFullscreen();
452
- } else {
453
- this.videojs.requestFullscreen();
454
- }
455
- }
456
-
457
- seek(secs) {
458
- // relative to current playback time
459
- let time = this.videojs.currentTime() + secs;
460
-
461
- if (time < 0) {
462
- time = 0;
463
- }
464
-
465
- this.videojs.currentTime(time);
466
- }
467
-
468
- seekTo(sec) {
469
- if (!this.videojs.hasStarted()) this.videojs.hasStarted(true);
470
- this.videojs.currentTime(sec);
471
- }
472
-
473
- jumpForward() {
474
- this.seek(constants.defaults.jumpSeconds);
475
- this.osd.feedback(constants.osd.jumpForward);
476
- }
477
-
478
- jumpBackward() {
479
- this.seek(-constants.defaults.jumpSeconds);
480
- this.osd.feedback(constants.osd.jumpBackward);
481
- }
482
-
483
- frameForward() {
484
- const thisFrame = this.timeConverter.secondsToFramesFromStart(this.videojs.currentTime());
485
- const nextFrame = Math.round(thisFrame) + 1;
486
-
487
- this.videojs.currentTime(this.timeConverter.frameToCurrentTimeFromStart(nextFrame));
488
- }
489
-
490
- frameBackward() {
491
- const thisFrame = this.timeConverter.secondsToFramesFromStart(this.videojs.currentTime());
492
- const previousFrame = Math.round(thisFrame) - 1;
493
-
494
- this.videojs.currentTime(this.timeConverter.frameToCurrentTimeFromStart(previousFrame));
495
- }
496
-
497
- secondsToFramesFromStart(seconds) {
498
- const frameRate = this.props.frameRate || constants.defaults.frameRate;
499
- let frame = seconds * frameRate;
500
- frame = Math.round(frame);
501
- return frame; // we're on this frame, could be for example 23.45
502
- }
503
-
504
- doEsothericWork() {
505
- // non critical parts, that are nice to have
506
- if (this.props.color) {
507
- // change the seekbar color from props
508
- document.getElementsByClassName('vjs-play-progress')[0].style.backgroundColor =
509
- this.props.color;
510
- }
511
-
512
- // feedback gizmo, mainly for feedback on hotkey-presses
513
- this.osd = new OSD({
514
- target: this.videoEl.parentElement,
515
- });
516
-
517
- // add a cinema button if a callback is passed in props
518
- if (this.props.onCinema) {
519
- this.videoEl.parentElement.classList.add('has-cinema-button');
520
- this.cinema = false;
521
-
522
- this.addButtonToControlBar({
523
- onClick: () => {
524
- this.props.onCinema(!this.cinema);
525
- this.cinema = !this.cinema;
526
- },
527
- componentName: 'CinemaButton',
528
- className: 'vdt-cinema-button',
529
- title: 'Cinema-mode',
530
- });
531
- }
532
-
533
- // add a screenshot button if a callback is passed in props
534
- if (this.props.onScreenshot) {
535
- this.videoEl.parentElement.classList.add('has-screenshot-button');
536
-
537
- this.addButtonToControlBar({
538
- onClick: () => {
539
- this.props.onScreenshot(this.videojs.currentTime());
540
- },
541
- componentName: 'ScreenshotButton',
542
- className: 'vdt-screenshot-button',
543
- title: 'Screenshot',
544
- });
545
- }
546
-
547
- // add a help overlay explaining keyboard controls
548
- this.help = new HelpOverlay({
549
- target: this.videoEl.parentElement,
550
- onToggle: (willShow) => {
551
- if (this.menu && this.menu.opened) {
552
- this.menu.close();
553
- }
554
- if (willShow && !this.controlsBelowPlayer) {
555
- this.hideControls();
556
- } else {
557
- this.showControls();
558
- }
559
- },
560
- });
561
- }
562
-
563
- restoreSettings() {
564
- // restore settings, like volume level or language track
565
- // get values from storage
566
- const volume = localStorage.getItem(constants.settings.volume);
567
- const subsEnabled = localStorage.getItem(constants.settings.subs);
568
- const subLang = localStorage.getItem(constants.settings.subLang);
569
-
570
- // values vs default behaviours in a neatly readable map
571
- this.settings = {
572
- // avoid .99999999 and other float pecularities in js
573
- [constants.settings.volume]: volume !== null ? Math.round(volume * 100) / 100 : 1,
574
- [constants.settings.subLang]: subLang !== null ? subLang : 'en',
575
- [constants.settings.subs]: subsEnabled !== null ? JSON.parse(subsEnabled) : false,
576
- };
577
-
578
- // apply values (settings)
579
- this.volume = this.settings[constants.settings.volume]; // volume
580
-
581
- if (this.settings[constants.settings.subs]) {
582
- // subtitles
583
- this.enableSubs();
584
- }
585
- }
586
-
587
- persistSetting(key, val) {
588
- this.settings[key] = val;
589
- localStorage.setItem(key, val);
590
- }
591
-
592
- enableSubs() {
593
- const subs = this.videojs.textTracks();
594
-
595
- for (let i = 0; i < subs.length; i += 1) {
596
- const sub = subs[i];
597
- if (sub.language === this.settings[constants.settings.subLang]) {
598
- sub.mode = 'showing';
599
- } else {
600
- sub.mode = 'hidden';
601
- }
602
- }
603
- }
604
-
605
- disableSubs() {
606
- const subs = this.videojs.textTracks();
607
-
608
- for (let i = 0; i < subs.length; i += 1) {
609
- const sub = subs[i];
610
- sub.mode = 'hidden';
611
- }
612
- }
613
-
614
- toggleSubs() {
615
- if (this.videojs.textTracks().length > 0) {
616
- // check if subtitles exist
617
- if (
618
- this.videojs.textTracks().tracks_.filter((track) => track.mode === 'showing').length > 0
619
- ) {
620
- // a certain subtitle track is enabled
621
- this.disableSubs();
622
- } else {
623
- this.enableSubs();
624
- }
625
- }
626
- }
627
-
628
- /*
629
- A quick example
630
- options = {
631
- onClick: ()=>{},
632
- componentName: 'ScreenshotButton',
633
- className: 'vdt-screenshot-button'
634
- }
635
- */
636
- addButtonToControlBar(options) {
637
- const Button = videojs.getComponent('Button');
638
- const CustomButton = videojs.extend(Button, {
639
- /* eslint-disable func-names, object-shorthand */
640
- constructor: function () {
641
- // eslint-disable-next-line prefer-rest-params
642
- Button.apply(this, arguments);
643
- if (options.className) this.el_.classList.add(options.className);
644
- if (options.title) {
645
- this.el_.title = options.title;
646
- }
647
- },
648
- handleClick: () => {
649
- if (options.onClick) options.onClick();
650
- },
651
- });
652
-
653
- videojs.registerComponent(options.componentName, CustomButton);
654
- this.videojs.getChild('controlBar').addChild(options.componentName, {}, 7);
655
- }
656
-
657
- // convenient getters / setters
658
- get volume() {
659
- return this.videojs.controlBar.volumePanel.volumeControl.volumeBar.getPercent();
660
- }
661
-
662
- set volume(floatVal) {
663
- if (floatVal === 0) {
664
- this.videojs.muted(true);
665
- } else {
666
- this.videojs.muted(false);
667
- this.videojs.volume(floatVal);
668
- }
669
- }
670
-
671
- setupMenu() {
672
- const menuItems = this.props.menuItems || [];
673
- const sourcesMenuItems = [];
674
- if (this.props.sources.length > 1) {
675
- // show only if more than 1 source
676
- this.props.sources.forEach((source) => {
677
- if (source.label) {
678
- sourcesMenuItems.push({
679
- title: source.label,
680
- onClick: () => {
681
- this.src = source;
682
- },
683
- });
684
- }
685
- });
686
-
687
- menuItems.push({
688
- title: 'Sources',
689
- getActiveIndex: () => {
690
- const matchingSource = this.props.sources.find(
691
- (source) => source.src === this.videojs.src(),
692
- );
693
- return sourcesMenuItems.indexOf(
694
- sourcesMenuItems.find((menuItem) => menuItem.title === matchingSource.label),
695
- );
696
- },
697
- items: sourcesMenuItems,
698
- });
699
- }
700
-
701
- // subtitles
702
- if (this.props.subtitles && this.props.subtitles.length > 0) {
703
- // off option for subs
704
- const subtitleMenuItems = [
705
- {
706
- title: 'Off',
707
- onClick: () => {
708
- this.disableSubs();
709
- },
710
- },
711
- ];
712
-
713
- // quick toggle in the control bar
714
- // this.addButtonToControlBar({
715
- // onClick: () => {
716
- // this.props.onCinema(!this.cinema);
717
- // this.cinema = !this.cinema;
718
- // },
719
- // componentName: 'CinemaButton',
720
- // className: 'vdt-cinema-button',
721
- // });
722
-
723
- this.props.subtitles.forEach((subtitle, index) => {
724
- subtitleMenuItems.push({
725
- title: subtitle.label,
726
- onClick: () => {
727
- const subs = this.videojs.textTracks();
728
- for (let i = 0; i < subs.length; i += 1) {
729
- const sub = subs[i];
730
- if (i === index) {
731
- sub.mode = 'showing';
732
- } else {
733
- sub.mode = 'hidden';
734
- }
735
- }
736
- },
737
- });
738
- });
739
-
740
- menuItems.push({
741
- title: 'Subtitles',
742
- getActiveIndex: () => {
743
- const subs = this.videojs.textTracks();
744
- for (let i = 0; i < subs.length; i += 1) {
745
- const sub = subs[i];
746
- if (sub.mode === 'showing') {
747
- return i + 1; // offset "off" option
748
- }
749
- }
750
-
751
- return 0; // if none are showing, then probably "off"
752
- },
753
- items: subtitleMenuItems,
754
- });
755
- }
756
-
757
- if (menuItems.length > 0) {
758
- const target = this.videojs.el_;
759
-
760
- this.menu = new Menu({
761
- target,
762
- items: menuItems,
763
- onOpen: () => {
764
- if (!this.controlsBelowPlayer) this.hideControls();
765
- },
766
- onClose: () => {
767
- if (!this.controlsBelowPlayer) this.videojs.controls(true);
768
- },
769
- });
770
-
771
- // add a cogwheel button to the player
772
- this.videoEl.parentElement.classList.add('has-vdt-menu');
773
-
774
- this.addButtonToControlBar({
775
- onClick: () => {
776
- this.menu.toggle();
777
- },
778
- componentName: 'SettingsMenuButton',
779
- className: 'vdt-settings-menu-button',
780
- title: 'Settings',
781
- });
782
- } else {
783
- // no menu items, no menu
784
- }
785
- }
786
-
787
- hideControls() {
788
- this.videojs.userActive(false);
789
- this.videojs.controls(false);
790
- this.videojs.fluid(true);
791
-
792
- this.videoEl.parentElement.classList.remove('vjs-user-active');
793
- this.videojs.userActive(false);
794
- }
795
-
796
- showControls() {
797
- this.videojs.fluid(!this.controlsBelowPlayer);
798
- this.videojs.controls(true);
799
- this.videojs.userActive(true);
800
- }
801
-
802
- toggleControls() {
803
- if (this.videojs.controls()) {
804
- this.hideControls();
805
- } else {
806
- this.showControls();
807
- }
808
- }
809
-
810
- onHotkeyPressed(key) {
811
- if (key !== 'h' && this.help.state.active) {
812
- this.help.hide();
813
- }
814
- }
815
-
816
- // eslint-disable-next-line class-methods-use-this
817
- triggerHotkey(key) {
818
- mousetrap.trigger(key);
819
- }
820
-
821
- toStart() {
822
- this.videojs.currentTime(0);
823
- }
824
-
825
- toEnd() {
826
- this.videojs.currentTime(this.videojs.duration());
827
- }
828
-
829
- replay() {
830
- this.toStart();
831
- this.videojs.play();
832
- }
833
-
834
- play() {
835
- this.videojs.play();
836
- }
837
-
838
- pause() {
839
- if (!this.videojs.paused()) this.videojs.pause();
840
- }
841
-
842
- togglePlayback() {
843
- if (this.videojs.paused()) {
844
- this.play();
845
- if (this.videojs.hasStarted_) this.osd.feedback(constants.osd.play);
846
- } else {
847
- this.pause();
848
- this.osd.feedback(constants.osd.pause);
849
- }
850
- }
851
-
852
- updateSmpte(valuesObj) {
853
- // Object.keys and array.forEach are too slow, classic for is faster
854
- const startCount = this.nodeNames.length - 1;
855
- let i;
856
- for (i = startCount; i > -1; i -= 1) {
857
- // updates follow frame -> seconds -> minutes -> hours order
858
- const key = this.nodeNames[i];
859
-
860
- if (
861
- typeof valuesObj[key] !== 'undefined' &&
862
- valuesObj[key] !== parseInt(this.nodes[key].value, 10) &&
863
- key !== this.state.selectedNode
864
- ) {
865
- // re-render node if value is defineed and is different from current value
866
- const str = valuesObj[key].toString();
867
- this.nodes[key].value = this.toDigitPairStr(str);
868
- }
869
- }
870
- }
871
-
872
- destroy() {
873
- // clean up
874
- this.videojs.paused(true);
875
- if (this.rafID) cancelAnimationFrame(this.rafID);
876
- this.videojs.dispose();
877
- this.unbindHotKeys();
878
- if (this.seekBar) this.seekBar.destroy();
879
- }
880
-
881
- get container() {
882
- return this.videoEl.parentElement;
883
- }
884
-
885
- set onDark(bool) {
886
- if (bool) {
887
- this.container.classList.add('on-dark');
888
- } else {
889
- this.container.classList.remove('on-dark');
890
- }
891
-
892
- if (this.seekBar) {
893
- this.seekBar.onDark = bool;
894
- }
895
- }
896
-
897
- get onDark() {
898
- return this.container.classList.contains('on-dark');
899
- }
900
-
901
- set controlsBelowPlayer(bool) {
902
- if (this.videojs.controls()) {
903
- if (bool) {
904
- this.container.classList.add('controls-below-player');
905
- this.videojs.fluid(false);
906
- } else {
907
- this.container.classList.remove('controls-below-player');
908
- this.videojs.fluid(true);
909
- }
910
- }
911
- }
912
-
913
- get controlsBelowPlayer() {
914
- return this.container.classList.contains('controls-below-player');
915
- }
916
-
917
- get smpte() {
918
- const currentSecond = this.videojs.currentTime();
919
- return this.timeConverter.secondsFromStartToSMPTE(currentSecond);
920
- }
921
-
922
- set smpte(o) {
923
- this.updateSmpte(o);
924
- }
925
-
926
- set src({ src, type = 'video/mp4', label = null }) {
927
- const currentTime = this.videojs.currentTime();
928
- // iterate trough sources, find existing matches
929
- const source = this.props.sources.find((aSource) => aSource.src === src);
930
- // if no match try adding source
931
-
932
- const onReady = () => {
933
- let initdone = false;
934
- // wait for video metadata to load, then set time
935
- this.videojs.on('loadedmetadata', () => {
936
- this.videojs.currentTime(currentTime);
937
- });
938
-
939
- // iPhone/iPad need to play first, then set the time
940
- // events: https://www.w3.org/TR/html5/embedded-content-0.html#mediaevents
941
- this.videojs.on('canplaythrough', () => {
942
- if (!initdone) {
943
- this.videojs.currentTime(currentTime);
944
- initdone = true;
945
- this.videojs.play();
946
- }
947
- });
948
- };
949
-
950
- if (source && Array.isArray(source) && source.length > 0) {
951
- // TODO - if(source.length > 1) - try one by one until a successful load happens
952
- this.videojs.addClass('vjs-waiting');
953
- this.videojs.src({
954
- ...source[0],
955
- });
956
- onReady();
957
- } else {
958
- this.videojs.addClass('vjs-waiting');
959
- this.videojs.src(this.addSrc(src, type, label));
960
- onReady();
961
- }
962
- }
963
-
964
- get src() {
965
- return this.videojs.src();
966
- }
967
-
968
- addSrc(src, type, label) {
969
- const newSource = {
970
- src,
971
- type,
972
- label,
973
- };
974
- const index = this.props.sources.push(newSource) - 1;
975
-
976
- return this.props.sources[index];
977
- }
978
-
979
- // eslint-disable-next-line class-methods-use-this, no-unused-vars
980
- removeSrc(url) {
981
- // should remove all url matching sources
982
- }
983
-
984
- set controls(bool) {
985
- if (bool) {
986
- this.showControls();
987
- } else {
988
- this.hideControls();
989
- }
990
- }
991
-
992
- get percentLoaded() {
993
- const r = this.videojs.buffered();
994
-
995
- const percentLoaded = (r.end(0) / this.videojs.duration()) * 100;
996
- return percentLoaded;
997
- }
998
- }
999
-
1000
- /*
1001
- Some video.js prototype overrides, that
1002
- help with undesired behaviour
1003
- */
1004
-
1005
- // take over space button, prevent video.js default behaviour
1006
- const button = videojs.prototype.constructor.getComponent('Button');
1007
- // eslint-disable-next-line func-names
1008
- button.prototype.handleKeyDown = function (e) {
1009
- if (e.keyCode === 32) {
1010
- e.preventDefault();
1011
- }
1012
- };
1013
- videojs.prototype.constructor.registerComponent('Button', button);
1014
-
1015
- // video.js opens menu on hover by default, this prevents that
1016
- const subsButton = videojs.prototype.constructor.getComponent('SubsCapsButton');
1017
- class CustomSubsCapsButton extends subsButton {
1018
- constructor(player, options = {}) {
1019
- super(player, options);
1020
- this.menuButton_.off('mouseenter');
1021
- this.menuButton_.off('mouseleave');
1022
- }
1023
- }
1024
-
1025
- /*
1026
- TODO - see what can be done about strange behaviour
1027
- when cc button is clicked with the menu showing
1028
- */
1029
- videojs.prototype.constructor.registerComponent('SubsCapsButton', CustomSubsCapsButton);