@mindedge/vuetify-player 0.3.1 → 0.4.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.
@@ -1,17 +1,36 @@
1
1
  <template>
2
2
  <v-container>
3
3
  <v-row>
4
- <v-col :cols="!options.expandedCaptions ? 12 : 6">
5
- <div v-if="buffering" class="player-overlay">
6
- <v-progress-circular
7
- :size="50"
8
- indeterminate
9
- ></v-progress-circular>
4
+ <v-col
5
+ ref="playerContainer"
6
+ :cols="!state.expandedCaptions ? 12 : 6"
7
+ class="pb-0 mb-0"
8
+ >
9
+ <div
10
+ v-if="resolvedType === 'video' && buffering"
11
+ class="player-overlay"
12
+ >
13
+ <div class="player-overlay--icon">
14
+ <v-progress-circular
15
+ :size="50"
16
+ indeterminate
17
+ ></v-progress-circular>
18
+ </div>
19
+ </div>
20
+ <div
21
+ v-if="resolvedType === 'video' && state.replay"
22
+ class="player-overlay"
23
+ >
24
+ <div class="player-overlay--icon">
25
+ <v-icon class="player-overlay--replay-icon">
26
+ mdi-replay
27
+ </v-icon>
28
+ </div>
10
29
  </div>
11
30
  <video
12
31
  ref="player"
13
32
  tabindex="0"
14
- :class="'player-' + type"
33
+ :class="'player-' + resolvedType"
15
34
  :height="attributes.height"
16
35
  :width="attributes.width"
17
36
  :autoplay="attributes.autoplay"
@@ -27,7 +46,7 @@
27
46
  :playsinline="attributes.playsinline"
28
47
  :poster="src.poster || attributes.poster"
29
48
  :preload="attributes.preload"
30
- @click="onPlayToggle"
49
+ @click="playToggle"
31
50
  @seeking="onSeeking"
32
51
  @timeupdate="onTimeupdate"
33
52
  @progress="onMediaProgress"
@@ -44,6 +63,8 @@
44
63
  @emptied="$emit('emptied', $event)"
45
64
  @stalled="$emit('stalled', $event)"
46
65
  @abort="$emit('abort', $event)"
66
+ @focusin="$emit('focusin', $event)"
67
+ @focusout="$emit('focusout', $event)"
47
68
  >
48
69
  <source
49
70
  v-for="(source, index) of current.sources"
@@ -65,12 +86,13 @@
65
86
  </video>
66
87
 
67
88
  <div
89
+ ref="controlsContainer"
68
90
  class="controls-container"
69
91
  v-if="attributes.controls"
70
92
  @mouseover="onControlsHover"
71
93
  >
72
94
  <v-slide-y-reverse-transition>
73
- <div v-if="player && options.controls" class="controls">
95
+ <div v-if="player && state.controls" class="controls">
74
96
  <v-slider
75
97
  dark
76
98
  v-model="currentPercent"
@@ -89,25 +111,23 @@
89
111
  >
90
112
  <template #prepend>
91
113
  <!-- Play button -->
92
- <v-tooltip v-if="!showReplay" top>
93
- <template
94
- v-slot:activator="{ on, attrs }"
95
- >
114
+ <v-tooltip v-if="!state.replay" top>
115
+ <template #activator="{ on, attrs }">
96
116
  <v-btn
97
117
  small
98
118
  text
99
119
  v-bind="attrs"
100
120
  v-on="on"
101
- @click="onPlayToggle"
121
+ @click="playToggle"
102
122
  >
103
123
  <v-icon>{{
104
- options.paused
124
+ state.paused
105
125
  ? 'mdi-play'
106
126
  : 'mdi-pause'
107
127
  }}</v-icon>
108
128
  <span class="d-sr-only">
109
129
  {{
110
- options.paused
130
+ state.paused
111
131
  ? t(
112
132
  language,
113
133
  'player.play'
@@ -121,17 +141,15 @@
121
141
  </v-btn>
122
142
  </template>
123
143
  <span>{{
124
- options.paused
144
+ state.paused
125
145
  ? t(language, 'player.play')
126
146
  : t(language, 'player.pause')
127
147
  }}</span>
128
148
  </v-tooltip>
129
149
 
130
150
  <!-- Replay button -->
131
- <v-tooltip v-if="showReplay" top>
132
- <template
133
- v-slot:activator="{ on, attrs }"
134
- >
151
+ <v-tooltip v-if="state.replay" top>
152
+ <template #activator="{ on, attrs }">
135
153
  <v-btn
136
154
  small
137
155
  text
@@ -160,15 +178,13 @@
160
178
  v-if="attributes.rewind && !activeAd"
161
179
  top
162
180
  >
163
- <template
164
- v-slot:activator="{ on, attrs }"
165
- >
181
+ <template #activator="{ on, attrs }">
166
182
  <v-btn
167
183
  small
168
184
  text
169
185
  v-bind="attrs"
170
186
  v-on="on"
171
- @click="onRewind"
187
+ @click="rewind"
172
188
  >
173
189
  <v-icon>mdi-rewind-10</v-icon>
174
190
  <span class="sr-only">{{
@@ -192,22 +208,21 @@
192
208
  current.tracks &&
193
209
  current.tracks.length > 0
194
210
  "
211
+ :attach="$refs.controlsContainer"
195
212
  open-on-hover
196
- top
197
213
  offset-y
214
+ top
198
215
  >
199
- <template
200
- v-slot:activator="{ on, attrs }"
201
- >
216
+ <template #activator="{ on, attrs }">
202
217
  <v-btn
203
218
  small
204
219
  text
205
220
  v-bind="attrs"
206
221
  v-on="on"
207
- @click="onCCToggle"
222
+ @click="CCToggle"
208
223
  >
209
224
  <v-icon>{{
210
- options.cc
225
+ state.cc
211
226
  ? 'mdi-closed-caption'
212
227
  : 'mdi-closed-caption-outline'
213
228
  }}</v-icon>
@@ -240,45 +255,47 @@
240
255
  </v-menu>
241
256
 
242
257
  <!-- Volume -->
243
- <v-menu open-on-hover top offset-y>
244
- <template
245
- v-slot:activator="{ on, attrs }"
246
- >
258
+ <v-menu
259
+ :attach="$refs.controlsContainer"
260
+ open-on-hover
261
+ offset-y
262
+ top
263
+ >
264
+ <template #activator="{ on, attrs }">
247
265
  <v-btn
248
266
  small
249
267
  text
250
268
  v-bind="attrs"
251
269
  v-on="on"
252
- @click="onMuteToggle"
270
+ @click="muteToggle"
253
271
  >
254
272
  <v-icon
255
273
  v-if="
256
- !options.muted &&
257
- options.volume > 0.75
274
+ !state.muted &&
275
+ state.volume > 0.75
258
276
  "
259
277
  >mdi-volume-high</v-icon
260
278
  >
261
279
  <v-icon
262
280
  v-if="
263
- !options.muted &&
264
- options.volume >=
265
- 0.25 &&
266
- options.volume <= 0.75
281
+ !state.muted &&
282
+ state.volume >= 0.25 &&
283
+ state.volume <= 0.75
267
284
  "
268
285
  >mdi-volume-medium</v-icon
269
286
  >
270
287
  <v-icon
271
288
  v-if="
272
- !options.muted &&
273
- options.volume > 0 &&
274
- options.volume < 0.25
289
+ !state.muted &&
290
+ state.volume > 0 &&
291
+ state.volume < 0.25
275
292
  "
276
293
  >mdi-volume-low</v-icon
277
294
  >
278
295
  <v-icon
279
296
  v-if="
280
- options.muted ||
281
- options.volume === 0
297
+ state.muted ||
298
+ state.volume === 0
282
299
  "
283
300
  >mdi-volume-off</v-icon
284
301
  >
@@ -299,31 +316,29 @@
299
316
  )
300
317
  }}</span>
301
318
  <v-slider
302
- v-model="options.volume"
319
+ v-model="state.volume"
303
320
  inverse-label
304
321
  :min="0"
305
322
  :max="1"
306
323
  :step="0.1"
307
324
  vertical
308
- @change="onVolumeChange"
325
+ @change="volumeChange"
309
326
  ></v-slider>
310
327
  </v-sheet>
311
328
  </v-menu>
312
329
 
313
330
  <!-- Fullscreen -->
314
- <v-tooltip v-if="fullscreenEnabled" top>
315
- <template
316
- v-slot:activator="{ on, attrs }"
317
- >
331
+ <v-tooltip v-if="allowFullscreen" top>
332
+ <template #activator="{ on, attrs }">
318
333
  <v-btn
319
334
  small
320
335
  text
321
336
  v-bind="attrs"
322
337
  v-on="on"
323
- @click="onFullscreen"
338
+ @click="fullscreenToggle"
324
339
  >
325
340
  <v-icon>{{
326
- !options.fullscreen
341
+ !state.fullscreen
327
342
  ? 'mdi-fullscreen'
328
343
  : 'mdi-fullscreen-exit'
329
344
  }}</v-icon>
@@ -350,9 +365,7 @@
350
365
  "
351
366
  top
352
367
  >
353
- <template
354
- v-slot:activator="{ on, attrs }"
355
- >
368
+ <template #activator="{ on, attrs }">
356
369
  <v-btn
357
370
  small
358
371
  text
@@ -380,13 +393,8 @@
380
393
  </v-tooltip>
381
394
 
382
395
  <!-- Remote playback -->
383
- <v-tooltip
384
- v-if="options.remoteplayback"
385
- top
386
- >
387
- <template
388
- v-slot:activator="{ on, attrs }"
389
- >
396
+ <v-tooltip v-if="allowRemotePlayback" top>
397
+ <template #activator="{ on, attrs }">
390
398
  <v-btn
391
399
  small
392
400
  text
@@ -412,10 +420,8 @@
412
420
  </v-tooltip>
413
421
 
414
422
  <!-- Download -->
415
- <v-tooltip v-if="options.download" top>
416
- <template
417
- v-slot:activator="{ on, attrs }"
418
- >
423
+ <v-tooltip v-if="allowDownload" top>
424
+ <template #activator="{ on, attrs }">
419
425
  <v-btn
420
426
  small
421
427
  text
@@ -438,117 +444,18 @@
438
444
  </v-tooltip>
439
445
 
440
446
  <!-- Settings -->
441
- <v-menu
442
- top
443
- offset-y
444
- :close-on-content-click="false"
445
- nudge-left="100"
446
- >
447
- <template
448
- v-slot:activator="{ on, attrs }"
449
- >
450
- <v-btn
451
- small
452
- text
453
- v-bind="attrs"
454
- v-on="on"
455
- >
456
- <v-icon>mdi-cog</v-icon>
457
- <span class="d-sr-only">{{
458
- t(
459
- language,
460
- 'player.toggle_settings'
461
- )
462
- }}</span>
463
- </v-btn>
464
- </template>
465
-
466
- <v-list>
467
- <v-list-item>
468
- <v-list-item-title>
469
- <v-icon
470
- >mdi-play-speed</v-icon
471
- >
472
- {{
473
- t(
474
- language,
475
- 'player.playback_speed'
476
- )
477
- }}
478
- </v-list-item-title>
479
- </v-list-item>
480
- <v-list-item>
481
- <v-list-item-title
482
- class="text-center"
483
- >
484
- <v-btn
485
- small
486
- :disabled="
487
- options.playbackRateIndex ===
488
- 0
489
- "
490
- @click="
491
- onPlaybackSpeed(
492
- options.playbackRateIndex -
493
- 1
494
- )
495
- "
496
- >
497
- <v-icon>
498
- mdi-clock-minus-outline
499
- </v-icon>
500
- <span
501
- class="d-sr-only"
502
- >{{
503
- t(
504
- language,
505
- 'player.playback_decrease'
506
- )
507
- }}</span
508
- >
509
- </v-btn>
510
- <span class="pl-2 pr-2"
511
- >{{
512
- attributes
513
- .playbackrates[
514
- options
515
- .playbackRateIndex
516
- ]
517
- }}x</span
518
- >
519
- <v-btn
520
- small
521
- :disabled="
522
- options.playbackRateIndex >=
523
- attributes
524
- .playbackrates
525
- .length -
526
- 1
527
- "
528
- @click="
529
- onPlaybackSpeed(
530
- options.playbackRateIndex +
531
- 1
532
- )
533
- "
534
- >
535
- <v-icon>
536
- mdi-clock-plus-outline
537
- </v-icon>
538
- <span
539
- class="d-sr-only"
540
- >{{
541
- t(
542
- language,
543
- 'player.playback_increase'
544
- )
545
- }}</span
546
- >
547
- </v-btn>
548
- </v-list-item-title>
549
- </v-list-item>
550
- </v-list>
551
- </v-menu>
447
+ <SettingsMenu
448
+ :attach="$refs.controlsContainer"
449
+ :state="state"
450
+ :attributes="attributes"
451
+ :language="language"
452
+ :captions-visible.sync="
453
+ captionsVisibleState
454
+ "
455
+ @change:playback-rate-index="
456
+ onPlaybackSpeedChange
457
+ "
458
+ ></SettingsMenu>
552
459
  </template>
553
460
  </v-slider>
554
461
  </div>
@@ -564,14 +471,32 @@
564
471
  captions.cues &&
565
472
  Object.keys(captions.cues).length
566
473
  "
567
- :cols="!options.expandedCaptions ? 12 : 6"
474
+ :cols="!state.expandedCaptions ? 12 : 6"
475
+ class="pt-0 mt-0"
568
476
  >
569
477
  <CaptionsMenu
570
478
  v-model="captions"
571
479
  :language="language"
480
+ :expanded.sync="captionsExpandedState"
481
+ :hide-expand="captionsHideExpand"
482
+ :paragraph-view="captionsParagraphView"
483
+ :hide-paragraph-view="captionsHideParagraphView"
484
+ :autoscroll="captionsAutoscroll"
485
+ :visible.sync="captionsVisibleState"
486
+ :hide-autoscroll="captionsHideAutoscroll"
487
+ :hide-close="captionsHideClose"
488
+ @update:paragraph-view="
489
+ $emit('update:captions-paragraph-view', $event)
490
+ "
491
+ @update:autoscroll="
492
+ $emit('update:captions-autoscroll', $event)
493
+ "
494
+ @update:close="$emit('update:captions-visible', $event)"
572
495
  @click:cue="onCueClick"
573
496
  @click:expand="onClickExpandCaptions"
574
- @click:paragraph="onClickParagraph"
497
+ @click:paragraph-view="onClickParagraph"
498
+ @click:autoscroll="onClickAutoscroll"
499
+ @click:close="onClickCaptionsClose"
575
500
  ></CaptionsMenu>
576
501
  </v-col>
577
502
  </v-row>
@@ -580,12 +505,14 @@
580
505
 
581
506
  <script>
582
507
  import filters from '../filters'
508
+ import SettingsMenu from './SettingsMenu.vue'
583
509
  import CaptionsMenu from './CaptionsMenu.vue'
584
510
  import { t } from '../../i18n/i18n'
585
511
 
586
512
  export default {
587
513
  name: 'Html5Player',
588
514
  components: {
515
+ SettingsMenu,
589
516
  CaptionsMenu,
590
517
  },
591
518
  props: {
@@ -593,7 +520,7 @@ export default {
593
520
  type: {
594
521
  type: String,
595
522
  required: false,
596
- default: 'video',
523
+ default: 'auto',
597
524
  },
598
525
  attributes: {
599
526
  type: Object,
@@ -603,8 +530,84 @@ export default {
603
530
  type: Object,
604
531
  required: true,
605
532
  },
533
+ captionsExpanded: {
534
+ type: Boolean,
535
+ required: false,
536
+ default: undefined,
537
+ },
538
+ captionsHideExpand: { type: Boolean, required: false, default: true },
539
+ captionsParagraphView: {
540
+ type: Boolean,
541
+ required: false,
542
+ default: undefined,
543
+ },
544
+ captionsHideParagraphView: {
545
+ type: Boolean,
546
+ required: false,
547
+ default: false,
548
+ },
549
+ captionsAutoscroll: {
550
+ type: Boolean,
551
+ required: false,
552
+ default: undefined,
553
+ },
554
+ captionsVisible: {
555
+ type: Boolean,
556
+ required: false,
557
+ default: undefined,
558
+ },
559
+ captionsHideAutoscroll: {
560
+ type: Boolean,
561
+ required: false,
562
+ default: false,
563
+ },
564
+ captionsHideClose: {
565
+ type: Boolean,
566
+ required: false,
567
+ default: false,
568
+ },
569
+ },
570
+ emits: [
571
+ 'error',
572
+ 'canplaythrough',
573
+ 'emptied',
574
+ 'stalled',
575
+ 'abort',
576
+ 'canplay',
577
+ 'waiting',
578
+ 'play',
579
+ 'pause',
580
+ 'load',
581
+ 'mouseover',
582
+ 'mouseout',
583
+ 'ended',
584
+ 'trackchange',
585
+ 'ratechange',
586
+ 'timeupdate',
587
+ 'seeking',
588
+ 'progress',
589
+ 'volumechange',
590
+ 'cuechange',
591
+ 'loadeddata',
592
+ 'loadedmetadata',
593
+ 'click:fullscreen',
594
+ 'click:pictureinpicture',
595
+ 'click:remoteplayback',
596
+ 'click:captions-expand',
597
+ 'click:captions-paragraph-view',
598
+ 'click:captions-autoscroll',
599
+ 'click:captions-close',
600
+ 'click:captions-cue',
601
+ 'update:captions-expanded',
602
+ 'update:captions-paragraph-view',
603
+ 'update:captions-autoscroll',
604
+ 'update:captions-visible',
605
+ ],
606
+ watch: {
607
+ 'state.controls': function () {
608
+ this.setCuePosition()
609
+ },
606
610
  },
607
- watch: {},
608
611
  computed: {
609
612
  current() {
610
613
  // We're playing an ad currently
@@ -616,9 +619,98 @@ export default {
616
619
  }
617
620
  },
618
621
  playerClass() {
619
- let classList = 'player-' + this.type
622
+ let classList = 'player-' + this.resolvedType
620
623
  return classList
621
624
  },
625
+ resolvedType() {
626
+ // Default to video if the type can't be resolved
627
+ let type = 'video'
628
+
629
+ // Make sure current is set and valid and has sources
630
+ if (
631
+ this.current &&
632
+ this.current.sources &&
633
+ this.current.sources.length > 0
634
+ ) {
635
+ const source = this.current.sources[0]
636
+
637
+ // Determine off the type / mime field first, then check the extensions
638
+ if (source.type && source.type.match(/^video\//i)) {
639
+ type = 'video'
640
+ } else if (source.type && source.type.match(/^audio\//i)) {
641
+ type = 'audio'
642
+ } else if (
643
+ source.src &&
644
+ source.src.match(/(?:mp4|webm|ogg)$/)
645
+ ) {
646
+ type = 'video'
647
+ } else if (source.src && source.src.match(/(?:mp3|wav)$/)) {
648
+ type = 'audio'
649
+ }
650
+ }
651
+
652
+ return type
653
+ },
654
+ captionsVisibleState: {
655
+ get() {
656
+ if (typeof this.captionsVisible !== 'undefined') {
657
+ return this.captionsVisible
658
+ } else {
659
+ return this.state.captionsVisible
660
+ }
661
+ },
662
+ set(v) {
663
+ this.$emit('update:captions-visible', v)
664
+ this.state.captionsVisible = v
665
+ },
666
+ },
667
+ captionsExpandedState: {
668
+ get() {
669
+ if (typeof this.captionsExpanded !== 'undefined') {
670
+ return this.captionsExpanded
671
+ } else {
672
+ return this.state.expandedCaptions
673
+ }
674
+ },
675
+ set(v) {
676
+ this.$emit('update:captions-expanded', v)
677
+ this.state.expandedCaptions = v
678
+ },
679
+ },
680
+ allowFullscreen() {
681
+ // Determine fullscreen settings
682
+ // If we explicitly disabled fullscreen in the attributes
683
+ // Or the browser doesn't support fullscreen
684
+ // Or we passed the HTML nofullscreen attribute
685
+ if (
686
+ this.attributes.playsinline ||
687
+ !document.fullscreenEnabled ||
688
+ this.state.controlslist.indexOf('nofullscreen') !== -1
689
+ ) {
690
+ return false
691
+ } else {
692
+ return true
693
+ }
694
+ },
695
+ allowRemotePlayback() {
696
+ // Determine remote playback settings
697
+ if (
698
+ this.attributes.disableremoteplayback ||
699
+ this.state.controlslist.indexOf('noremoteplayback') !== -1
700
+ ) {
701
+ return false
702
+ } else {
703
+ return true
704
+ }
705
+ },
706
+ allowDownload() {
707
+ // Determine download settings
708
+ if (this.state.controlslist.indexOf('nodownload') !== -1) {
709
+ return false
710
+ } else {
711
+ return true
712
+ }
713
+ },
622
714
  },
623
715
  data() {
624
716
  return {
@@ -629,8 +721,8 @@ export default {
629
721
  currentPercent: 0,
630
722
  player: {},
631
723
  captions: { nonce: 0 },
632
- fullscreenEnabled: false,
633
- options: {
724
+ state: {
725
+ replay: false,
634
726
  cc: true,
635
727
  ccLang: this.language,
636
728
  controls: true,
@@ -641,24 +733,22 @@ export default {
641
733
  playbackRateIndex: 0,
642
734
  fullscreen: false,
643
735
  expandedCaptions: false,
644
- download: false,
645
- remoteplayback: false,
736
+ captionsVisible: true,
646
737
  controlslist: [],
647
738
  },
648
739
  watchPlayer: 0,
649
740
  scrub: { max: 100 },
650
741
  buffering: false,
651
- showReplay: false,
652
742
  }
653
743
  },
654
744
  beforeMount() {
655
- // Parse the controlslist string
745
+ // Parse the html controlslist attribute string
656
746
  if (
657
747
  this.attributes.controlslist &&
658
748
  typeof this.attributes.controlslist === 'string' &&
659
749
  this.attributes.controlslist !== ''
660
750
  ) {
661
- this.options.controlslist = this.attributes.controlslist.split(' ')
751
+ this.state.controlslist = this.attributes.controlslist.split(' ')
662
752
  }
663
753
 
664
754
  if (
@@ -672,12 +762,12 @@ export default {
672
762
 
673
763
  // Adjust the playback speed to 1 by default
674
764
  if (this.attributes.playbackrates.indexOf(1) !== -1) {
675
- this.options.playbackRateIndex =
765
+ this.state.playbackRateIndex =
676
766
  this.attributes.playbackrates.indexOf(1)
677
767
  } else {
678
768
  // 1 aka normal playback not enabled (What monster would do this?!)
679
769
  // Set the playback rate to "middle of the road" for whatever is available
680
- this.options.playbackRateIndex = Math.floor(
770
+ this.state.playbackRateIndex = Math.floor(
681
771
  this.attributes.playbackrates.length / 2
682
772
  )
683
773
  }
@@ -690,34 +780,6 @@ export default {
690
780
  this.ads[ad.play_at_percent].complete = false
691
781
  }
692
782
  }
693
-
694
- // Determine fullscreen settings
695
- if (
696
- this.attributes.playsinline ||
697
- !document.fullscreenEnabled ||
698
- this.options.controlslist.indexOf('nofullscreen') !== -1
699
- ) {
700
- this.fullscreenEnabled = false
701
- } else {
702
- this.fullscreenEnabled = true
703
- }
704
-
705
- // Determine remote playback settings
706
- if (
707
- this.attributes.disableremoteplayback ||
708
- this.options.controlslist.indexOf('noremoteplayback') !== -1
709
- ) {
710
- this.options.remoteplayback = false
711
- } else {
712
- this.options.remoteplayback = true
713
- }
714
-
715
- // Determine download settings
716
- if (this.options.controlslist.indexOf('nodownload') !== -1) {
717
- this.options.download = false
718
- } else {
719
- this.options.download = true
720
- }
721
783
  },
722
784
  mounted() {
723
785
  if (
@@ -753,34 +815,45 @@ export default {
753
815
  },
754
816
  onCueClick(time) {
755
817
  this.setTime(time)
818
+ this.$emit('click:captions-cue', time)
756
819
  },
757
820
  onClickExpandCaptions(expanded) {
758
- this.options.expandedCaptions = expanded
759
821
  this.$emit('click:captions-expand', expanded)
760
822
  },
761
823
  onClickParagraph(isParagraph) {
762
- this.$emit('click:captions-paragraph', isParagraph)
824
+ this.$emit('click:captions-paragraph-view', isParagraph)
825
+ },
826
+ onClickAutoscroll(autoscroll) {
827
+ this.$emit('click:captions-autoscroll', autoscroll)
828
+ },
829
+ onClickCaptionsClose() {
830
+ this.state.captionsVisible = false
831
+ this.$emit('click:captions-close')
763
832
  },
764
833
  onDownload() {
765
834
  window.open(this.src.sources[0].src, '_blank')
766
835
  },
767
- onRewind() {
768
- // Rewind in seconds
769
- const seconds = 10
770
-
836
+ rewind(seconds = 10) {
771
837
  if (this.player.currentTime <= seconds) {
772
838
  this.setTime(0)
773
839
  } else {
774
840
  this.setTime(this.player.currentTime - seconds)
775
841
  }
776
842
  },
777
- onFullscreen() {
778
- this.options.fullscreen = !document.fullscreenElement
843
+ fastForward(seconds = 10) {
844
+ if (this.player.currentTime + seconds >= this.player.duration) {
845
+ this.setTime(this.player.duration)
846
+ } else {
847
+ this.setTime(this.player.currentTime + seconds)
848
+ }
849
+ },
850
+ fullscreenToggle() {
851
+ this.state.fullscreen = !document.fullscreenElement
779
852
  // Return the whole element to be fullscreened so the controls come with it
780
- this.$emit('click:fullscreen', this.$el)
853
+ this.$emit('click:fullscreen', this.$refs.playerContainer)
781
854
  },
782
855
  onPictureInPicture() {
783
- //this.options.pip = !document.fullscreenElement;
856
+ //this.state.pip = !document.fullscreenElement;
784
857
  // Return the player aka HTMLVideoElement
785
858
  this.$emit('click:pictureinpicture', this.$refs.player)
786
859
  },
@@ -788,16 +861,16 @@ export default {
788
861
  this.$emit('click:remoteplayback', this.$refs.player)
789
862
  },
790
863
  onVideoHover(e) {
791
- this.options.controls = true
792
- clearTimeout(this.options.controlsDebounce)
864
+ this.state.controls = true
865
+ clearTimeout(this.state.controlsDebounce)
793
866
  this.$emit('mouseover', e)
794
867
  },
795
868
  onVideoLeave(e) {
796
869
  const self = this
797
870
  // Clear any existing timeouts before we create one
798
- clearTimeout(this.options.controlsDebounce)
799
- this.options.controlsDebounce = setTimeout(() => {
800
- self.options.controls = false
871
+ clearTimeout(this.state.controlsDebounce)
872
+ this.state.controlsDebounce = setTimeout(() => {
873
+ self.state.controls = false
801
874
  }, 50)
802
875
  this.$emit('mouseout', e)
803
876
  },
@@ -828,25 +901,25 @@ export default {
828
901
  this.activeAd !== null &&
829
902
  this.activeAd.play_at_percent === 100
830
903
  ) {
831
- this.showReplay = true
904
+ this.state.replay = true
832
905
  // Ended but this ad was a postroll
833
906
  this.$emit('ended', e)
834
907
  } else {
835
- this.showReplay = true
908
+ this.state.replay = true
836
909
  // Ended without an ad
837
910
  this.$emit('ended', e)
838
911
  }
839
912
  },
840
913
  onControlsHover() {
841
- clearTimeout(this.options.controlsDebounce)
842
- this.options.controls = true
914
+ clearTimeout(this.state.controlsDebounce)
915
+ this.state.controls = true
843
916
  },
844
917
  onControlsLeave() {
845
918
  const self = this
846
919
  // Clear any existing timeouts before we create one
847
- clearTimeout(this.options.controlsDebounce)
848
- this.options.controlsDebounce = setTimeout(() => {
849
- self.options.controls = false
920
+ clearTimeout(this.state.controlsDebounce)
921
+ this.state.controlsDebounce = setTimeout(() => {
922
+ self.state.controls = false
850
923
  }, 50)
851
924
  },
852
925
  /**
@@ -857,25 +930,25 @@ export default {
857
930
  onSelectTrack(lang = null) {
858
931
  if (this.player.textTracks && this.player.textTracks.length > 0) {
859
932
  for (let i = 0; i < this.player.textTracks.length; i++) {
860
- const tt = this.player.textTracks[i]
933
+ // Disable all tracks by default
934
+ // We only want to enable the correct active track otherwise track switches / replays will overlay tracks
935
+ this.player.textTracks[i].mode = 'disabled'
861
936
 
862
- if (tt.language === lang) {
863
- this.options.ccLang = lang
937
+ if (this.player.textTracks[i].language === lang) {
938
+ this.state.ccLang = lang
864
939
  this.player.textTracks[i].mode = 'showing'
865
940
 
866
- this.setCues(tt)
941
+ this.setCues(this.player.textTracks[i])
867
942
 
868
943
  // Emit the current track
869
- this.$emit('trackchange', tt)
870
- } else {
871
- this.player.textTracks[i].mode = 'disabled'
944
+ this.$emit('trackchange', this.player.textTracks[i])
872
945
  }
873
946
  }
874
947
  }
875
948
  },
876
- onPlaybackSpeed(index) {
949
+ onPlaybackSpeedChange(index) {
877
950
  this.player.playbackRate = this.attributes.playbackrates[index]
878
- this.options.playbackRateIndex = index
951
+ this.state.playbackRateIndex = index
879
952
  this.$emit('ratechange', this.player.playbackRate)
880
953
  },
881
954
  onTimeupdate(e) {
@@ -905,31 +978,15 @@ export default {
905
978
  onMediaProgress(e) {
906
979
  this.$emit('progress', e)
907
980
  },
908
- onCCToggle() {
909
- this.options.cc = !this.options.cc
981
+ CCToggle() {
982
+ this.state.cc = !this.state.cc
910
983
 
911
- if (this.options.cc) {
912
- this.onSelectTrack(this.options.ccLang)
984
+ if (this.state.cc) {
985
+ this.onSelectTrack(this.state.ccLang)
913
986
  } else {
914
987
  this.onSelectTrack()
915
988
  }
916
989
  },
917
- onPlayToggle(e) {
918
- const self = this
919
- this.options.controls = true
920
-
921
- // Clear any existing timeouts and close the controls in 5 seconds
922
- clearTimeout(this.options.controlsDebounce)
923
- this.options.controlsDebounce = setTimeout(() => {
924
- self.options.controls = false
925
- }, 5000)
926
-
927
- if (this.player.paused) {
928
- this.play(e)
929
- } else {
930
- this.pause(e)
931
- }
932
- },
933
990
  onClickReplay(e) {
934
991
  // Re-initialize the ads aka pre/post/midroll
935
992
  if (this.src.ads && this.src.ads.length) {
@@ -955,13 +1012,13 @@ export default {
955
1012
  // Restart from the beginning
956
1013
  this.setTime(0)
957
1014
  },
958
- onMuteToggle() {
1015
+ muteToggle() {
959
1016
  if (this.player.muted) {
960
- this.options.muted = false
1017
+ this.state.muted = false
961
1018
  this.player.muted = false
962
- this.$emit('volumechange', this.options.volume)
1019
+ this.$emit('volumechange', this.state.volume)
963
1020
  } else {
964
- this.options.muted = true
1021
+ this.state.muted = true
965
1022
  this.player.muted = true
966
1023
  this.$emit('volumechange', 0)
967
1024
  }
@@ -994,6 +1051,15 @@ export default {
994
1051
  if (typeof track.activeCues[0].rawText === 'undefined') {
995
1052
  track.activeCues[0].rawText = track.activeCues[0].text
996
1053
  }
1054
+ // Retain the original cue display values
1055
+ // This way we can swap between a modified display when the controls are visible
1056
+ if (typeof track.activeCues[0].defaults === 'undefined') {
1057
+ track.activeCues[0].defaults = {
1058
+ line: track.activeCues[0].line,
1059
+ size: track.activeCues[0].size,
1060
+ snapToLines: track.activeCues[0].snapToLines,
1061
+ }
1062
+ }
997
1063
 
998
1064
  // Now remove `<c.transcript>` tags
999
1065
  const transcriptTagRegex = /<c.transcript>.*?<\/c>/gi
@@ -1003,6 +1069,8 @@ export default {
1003
1069
  transcriptTagRegex,
1004
1070
  ''
1005
1071
  )
1072
+
1073
+ this.setCuePosition()
1006
1074
  }
1007
1075
 
1008
1076
  this.setCues(track)
@@ -1032,19 +1100,31 @@ export default {
1032
1100
  //this.player.media = this.$refs.player;
1033
1101
  this.$emit('loadedmetadata', e)
1034
1102
  this.player = this.$refs.player
1035
- this.player.volume = this.options.volume
1036
- this.$emit('volumechange', this.options.volume)
1103
+ this.player.volume = this.state.volume
1104
+ this.$emit('volumechange', this.state.volume)
1037
1105
  },
1038
- onVolumeChange(value) {
1039
- this.options.volume = value
1106
+ volumeChange(value) {
1107
+ // Value needs to be a decimal value between 0 and 1
1108
+ if (value > 1) {
1109
+ value = 1
1110
+ } else if (value < 0) {
1111
+ value = 0
1112
+ }
1113
+ this.state.volume = value
1040
1114
  this.player.volume = value
1041
1115
  this.$emit('volumechange', value)
1042
1116
  },
1117
+ volumeAdjust(value) {
1118
+ const newVolume = this.state.volume + value
1119
+ this.volumeChange(newVolume)
1120
+ },
1043
1121
  onDurationChange() {
1044
1122
  // console.log('onDurationChange');
1045
1123
  // console.log(e);
1046
1124
  },
1047
1125
  setTime(time) {
1126
+ // Scrubbing / manually setting the time should remove the replay button
1127
+ this.state.replay = false
1048
1128
  this.player.currentTime = time
1049
1129
  },
1050
1130
  setCues(track) {
@@ -1056,19 +1136,76 @@ export default {
1056
1136
  // Required so the v-model will actually update.
1057
1137
  this.captions.nonce = Math.random()
1058
1138
  },
1139
+ setCuePosition() {
1140
+ if (
1141
+ this.player &&
1142
+ this.player.textTracks &&
1143
+ this.player.textTracks.length > 0
1144
+ ) {
1145
+ for (let i = 0; i < this.player.textTracks.length; i++) {
1146
+ // Only alter the currently showing text track
1147
+ if (this.player.textTracks[i].mode === 'showing') {
1148
+ // If the controls are showing then bump the alignment to the start
1149
+ if (
1150
+ this.state.controls &&
1151
+ this.player.textTracks[i].activeCues &&
1152
+ this.player.textTracks[i].activeCues.length > 0
1153
+ ) {
1154
+ // Count the number of line breaks in the cue to figure out our offset from the bottom
1155
+ // VTTCue doesn't have a "margin from bottom" by default
1156
+ const numLines = (
1157
+ this.player.textTracks[
1158
+ i
1159
+ ].activeCues[0].text.match(/\n/g) || []
1160
+ ).length
1161
+
1162
+ // Limit the cues to 90% of the screen width
1163
+ // If this is left default / set to 100 then the above line
1164
+ // Also set snapToLines to true otherwise if there's a line % in the vtt file the display will be relative and make the lines not aligned properly
1165
+ this.player.textTracks[i].activeCues[0].line =
1166
+ -3 - numLines
1167
+ this.player.textTracks[i].activeCues[0].size = 99
1168
+ this.player.textTracks[
1169
+ i
1170
+ ].activeCues[0].snapToLines = true
1171
+ } else if (
1172
+ this.player.textTracks[i].activeCues &&
1173
+ this.player.textTracks[i].activeCues.length > 0 &&
1174
+ typeof this.player.textTracks[i].activeCues[0]
1175
+ .defaults !== 'undefined'
1176
+ ) {
1177
+ this.player.textTracks[i].activeCues[0].line =
1178
+ this.player.textTracks[
1179
+ i
1180
+ ].activeCues[0].defaults.line
1181
+ this.player.textTracks[i].activeCues[0].size =
1182
+ this.player.textTracks[
1183
+ i
1184
+ ].activeCues[0].defaults.size
1185
+ this.player.textTracks[
1186
+ i
1187
+ ].activeCues[0].snapToLines =
1188
+ this.player.textTracks[
1189
+ i
1190
+ ].activeCues[0].defaults.snapToLines
1191
+ }
1192
+ }
1193
+ }
1194
+ }
1195
+ },
1059
1196
  load(e = null) {
1060
1197
  if (this.player.load) {
1061
1198
  // Reload the player to refresh all the sources / tracks
1062
1199
  this.player.load()
1063
1200
  this.$emit('load', e)
1064
1201
  } else {
1065
- console.log('Cannot load player')
1202
+ console.error('Cannot load player')
1066
1203
  }
1067
1204
  },
1068
1205
  pause(e = null) {
1069
1206
  if (this.player.pause) {
1070
1207
  this.player.pause()
1071
- this.options.paused = true
1208
+ this.state.paused = true
1072
1209
  this.$emit('pause', e)
1073
1210
  } else {
1074
1211
  console.log('Cannot pause player')
@@ -1078,12 +1215,35 @@ export default {
1078
1215
  if (this.player.play) {
1079
1216
  // Start playing the main video
1080
1217
  this.player.play()
1081
- this.options.paused = false
1218
+ this.state.paused = false
1219
+ this.state.replay = false
1082
1220
  this.$emit('play', e)
1083
1221
  } else {
1084
1222
  console.log('Cannot play player')
1085
1223
  }
1086
1224
  },
1225
+ playToggle(e) {
1226
+ // If the replay button is active then we actually need to call the onClickReplay method instead
1227
+ // Otherwise we'd just end up replaying any postroll ad
1228
+ if (this.state.replay) {
1229
+ this.onClickReplay(e)
1230
+ } else {
1231
+ const self = this
1232
+ this.state.controls = true
1233
+
1234
+ // Clear any existing timeouts and close the controls in 5 seconds
1235
+ clearTimeout(this.state.controlsDebounce)
1236
+ this.state.controlsDebounce = setTimeout(() => {
1237
+ self.state.controls = false
1238
+ }, 5000)
1239
+
1240
+ if (this.player.paused) {
1241
+ this.play(e)
1242
+ } else {
1243
+ this.pause(e)
1244
+ }
1245
+ }
1246
+ },
1087
1247
  },
1088
1248
  }
1089
1249
  </script>
@@ -1094,29 +1254,11 @@ export default {
1094
1254
  position: relative;
1095
1255
  top: -50px;
1096
1256
  margin-bottom: -40px;
1097
- overflow: hidden;
1098
1257
  }
1099
1258
  .controls {
1100
1259
  height: 40px;
1101
1260
  background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.7));
1102
1261
  }
1103
- .volume-slider {
1104
- position: relative;
1105
- right: -50px;
1106
- top: -180px;
1107
- height: 180px;
1108
- width: 50px;
1109
- margin-left: -50px;
1110
- padding-bottom: 10px;
1111
- }
1112
- .slider-active-area {
1113
- width: 50px;
1114
- height: 200px;
1115
- margin-right: -50px;
1116
- margin-bottom: -200px;
1117
- position: relative;
1118
- top: -160px; /* height of this - controls height */
1119
- }
1120
1262
  .player-audio {
1121
1263
  height: 40px;
1122
1264
  }
@@ -1134,12 +1276,18 @@ export default {
1134
1276
  color: #fff;
1135
1277
  left: 25%;
1136
1278
  width: 50%;
1137
- top: 100px;
1279
+ top: 35%;
1138
1280
  height: 0;
1139
1281
  text-align: center;
1140
1282
  }
1141
- .player-overlay > div {
1283
+ .player-overlay--replay-icon {
1284
+ color: #fff;
1285
+ font-size: 5rem;
1286
+ }
1287
+ .player-overlay > .player-overlay--icon {
1288
+ display: inline-block;
1142
1289
  background: rgba(0, 0, 0, 0.25);
1143
1290
  border-radius: 100%;
1291
+ padding: 1rem;
1144
1292
  }
1145
1293
  </style>