@mindedge/vuetify-player 0.3.1 → 0.4.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.
@@ -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,33 @@
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
+ :elevation="elevation"
489
+ @update:paragraph-view="
490
+ $emit('update:captions-paragraph-view', $event)
491
+ "
492
+ @update:autoscroll="
493
+ $emit('update:captions-autoscroll', $event)
494
+ "
495
+ @update:close="$emit('update:captions-visible', $event)"
572
496
  @click:cue="onCueClick"
573
497
  @click:expand="onClickExpandCaptions"
574
- @click:paragraph="onClickParagraph"
498
+ @click:paragraph-view="onClickParagraph"
499
+ @click:autoscroll="onClickAutoscroll"
500
+ @click:close="onClickCaptionsClose"
575
501
  ></CaptionsMenu>
576
502
  </v-col>
577
503
  </v-row>
@@ -580,12 +506,14 @@
580
506
 
581
507
  <script>
582
508
  import filters from '../filters'
509
+ import SettingsMenu from './SettingsMenu.vue'
583
510
  import CaptionsMenu from './CaptionsMenu.vue'
584
511
  import { t } from '../../i18n/i18n'
585
512
 
586
513
  export default {
587
514
  name: 'Html5Player',
588
515
  components: {
516
+ SettingsMenu,
589
517
  CaptionsMenu,
590
518
  },
591
519
  props: {
@@ -593,7 +521,7 @@ export default {
593
521
  type: {
594
522
  type: String,
595
523
  required: false,
596
- default: 'video',
524
+ default: 'auto',
597
525
  },
598
526
  attributes: {
599
527
  type: Object,
@@ -603,8 +531,85 @@ export default {
603
531
  type: Object,
604
532
  required: true,
605
533
  },
534
+ captionsExpanded: {
535
+ type: Boolean,
536
+ required: false,
537
+ default: undefined,
538
+ },
539
+ captionsHideExpand: { type: Boolean, required: false, default: true },
540
+ captionsParagraphView: {
541
+ type: Boolean,
542
+ required: false,
543
+ default: undefined,
544
+ },
545
+ captionsHideParagraphView: {
546
+ type: Boolean,
547
+ required: false,
548
+ default: false,
549
+ },
550
+ captionsAutoscroll: {
551
+ type: Boolean,
552
+ required: false,
553
+ default: undefined,
554
+ },
555
+ captionsVisible: {
556
+ type: Boolean,
557
+ required: false,
558
+ default: undefined,
559
+ },
560
+ captionsHideAutoscroll: {
561
+ type: Boolean,
562
+ required: false,
563
+ default: false,
564
+ },
565
+ captionsHideClose: {
566
+ type: Boolean,
567
+ required: false,
568
+ default: false,
569
+ },
570
+ elevation: { type: [Number, String], required: false, default: 2 },
571
+ },
572
+ emits: [
573
+ 'error',
574
+ 'canplaythrough',
575
+ 'emptied',
576
+ 'stalled',
577
+ 'abort',
578
+ 'canplay',
579
+ 'waiting',
580
+ 'play',
581
+ 'pause',
582
+ 'load',
583
+ 'mouseover',
584
+ 'mouseout',
585
+ 'ended',
586
+ 'trackchange',
587
+ 'ratechange',
588
+ 'timeupdate',
589
+ 'seeking',
590
+ 'progress',
591
+ 'volumechange',
592
+ 'cuechange',
593
+ 'loadeddata',
594
+ 'loadedmetadata',
595
+ 'click:fullscreen',
596
+ 'click:pictureinpicture',
597
+ 'click:remoteplayback',
598
+ 'click:captions-expand',
599
+ 'click:captions-paragraph-view',
600
+ 'click:captions-autoscroll',
601
+ 'click:captions-close',
602
+ 'click:captions-cue',
603
+ 'update:captions-expanded',
604
+ 'update:captions-paragraph-view',
605
+ 'update:captions-autoscroll',
606
+ 'update:captions-visible',
607
+ ],
608
+ watch: {
609
+ 'state.controls': function () {
610
+ this.setCuePosition()
611
+ },
606
612
  },
607
- watch: {},
608
613
  computed: {
609
614
  current() {
610
615
  // We're playing an ad currently
@@ -616,9 +621,98 @@ export default {
616
621
  }
617
622
  },
618
623
  playerClass() {
619
- let classList = 'player-' + this.type
624
+ let classList = 'player-' + this.resolvedType
620
625
  return classList
621
626
  },
627
+ resolvedType() {
628
+ // Default to video if the type can't be resolved
629
+ let type = 'video'
630
+
631
+ // Make sure current is set and valid and has sources
632
+ if (
633
+ this.current &&
634
+ this.current.sources &&
635
+ this.current.sources.length > 0
636
+ ) {
637
+ const source = this.current.sources[0]
638
+
639
+ // Determine off the type / mime field first, then check the extensions
640
+ if (source.type && source.type.match(/^video\//i)) {
641
+ type = 'video'
642
+ } else if (source.type && source.type.match(/^audio\//i)) {
643
+ type = 'audio'
644
+ } else if (
645
+ source.src &&
646
+ source.src.match(/(?:mp4|webm|ogg)$/)
647
+ ) {
648
+ type = 'video'
649
+ } else if (source.src && source.src.match(/(?:mp3|wav)$/)) {
650
+ type = 'audio'
651
+ }
652
+ }
653
+
654
+ return type
655
+ },
656
+ captionsVisibleState: {
657
+ get() {
658
+ if (typeof this.captionsVisible !== 'undefined') {
659
+ return this.captionsVisible
660
+ } else {
661
+ return this.state.captionsVisible
662
+ }
663
+ },
664
+ set(v) {
665
+ this.$emit('update:captions-visible', v)
666
+ this.state.captionsVisible = v
667
+ },
668
+ },
669
+ captionsExpandedState: {
670
+ get() {
671
+ if (typeof this.captionsExpanded !== 'undefined') {
672
+ return this.captionsExpanded
673
+ } else {
674
+ return this.state.expandedCaptions
675
+ }
676
+ },
677
+ set(v) {
678
+ this.$emit('update:captions-expanded', v)
679
+ this.state.expandedCaptions = v
680
+ },
681
+ },
682
+ allowFullscreen() {
683
+ // Determine fullscreen settings
684
+ // If we explicitly disabled fullscreen in the attributes
685
+ // Or the browser doesn't support fullscreen
686
+ // Or we passed the HTML nofullscreen attribute
687
+ if (
688
+ this.attributes.playsinline ||
689
+ !document.fullscreenEnabled ||
690
+ this.state.controlslist.indexOf('nofullscreen') !== -1
691
+ ) {
692
+ return false
693
+ } else {
694
+ return true
695
+ }
696
+ },
697
+ allowRemotePlayback() {
698
+ // Determine remote playback settings
699
+ if (
700
+ this.attributes.disableremoteplayback ||
701
+ this.state.controlslist.indexOf('noremoteplayback') !== -1
702
+ ) {
703
+ return false
704
+ } else {
705
+ return true
706
+ }
707
+ },
708
+ allowDownload() {
709
+ // Determine download settings
710
+ if (this.state.controlslist.indexOf('nodownload') !== -1) {
711
+ return false
712
+ } else {
713
+ return true
714
+ }
715
+ },
622
716
  },
623
717
  data() {
624
718
  return {
@@ -629,8 +723,8 @@ export default {
629
723
  currentPercent: 0,
630
724
  player: {},
631
725
  captions: { nonce: 0 },
632
- fullscreenEnabled: false,
633
- options: {
726
+ state: {
727
+ replay: false,
634
728
  cc: true,
635
729
  ccLang: this.language,
636
730
  controls: true,
@@ -641,24 +735,22 @@ export default {
641
735
  playbackRateIndex: 0,
642
736
  fullscreen: false,
643
737
  expandedCaptions: false,
644
- download: false,
645
- remoteplayback: false,
738
+ captionsVisible: true,
646
739
  controlslist: [],
647
740
  },
648
741
  watchPlayer: 0,
649
742
  scrub: { max: 100 },
650
743
  buffering: false,
651
- showReplay: false,
652
744
  }
653
745
  },
654
746
  beforeMount() {
655
- // Parse the controlslist string
747
+ // Parse the html controlslist attribute string
656
748
  if (
657
749
  this.attributes.controlslist &&
658
750
  typeof this.attributes.controlslist === 'string' &&
659
751
  this.attributes.controlslist !== ''
660
752
  ) {
661
- this.options.controlslist = this.attributes.controlslist.split(' ')
753
+ this.state.controlslist = this.attributes.controlslist.split(' ')
662
754
  }
663
755
 
664
756
  if (
@@ -672,12 +764,12 @@ export default {
672
764
 
673
765
  // Adjust the playback speed to 1 by default
674
766
  if (this.attributes.playbackrates.indexOf(1) !== -1) {
675
- this.options.playbackRateIndex =
767
+ this.state.playbackRateIndex =
676
768
  this.attributes.playbackrates.indexOf(1)
677
769
  } else {
678
770
  // 1 aka normal playback not enabled (What monster would do this?!)
679
771
  // Set the playback rate to "middle of the road" for whatever is available
680
- this.options.playbackRateIndex = Math.floor(
772
+ this.state.playbackRateIndex = Math.floor(
681
773
  this.attributes.playbackrates.length / 2
682
774
  )
683
775
  }
@@ -690,34 +782,6 @@ export default {
690
782
  this.ads[ad.play_at_percent].complete = false
691
783
  }
692
784
  }
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
785
  },
722
786
  mounted() {
723
787
  if (
@@ -753,34 +817,45 @@ export default {
753
817
  },
754
818
  onCueClick(time) {
755
819
  this.setTime(time)
820
+ this.$emit('click:captions-cue', time)
756
821
  },
757
822
  onClickExpandCaptions(expanded) {
758
- this.options.expandedCaptions = expanded
759
823
  this.$emit('click:captions-expand', expanded)
760
824
  },
761
825
  onClickParagraph(isParagraph) {
762
- this.$emit('click:captions-paragraph', isParagraph)
826
+ this.$emit('click:captions-paragraph-view', isParagraph)
827
+ },
828
+ onClickAutoscroll(autoscroll) {
829
+ this.$emit('click:captions-autoscroll', autoscroll)
830
+ },
831
+ onClickCaptionsClose() {
832
+ this.state.captionsVisible = false
833
+ this.$emit('click:captions-close')
763
834
  },
764
835
  onDownload() {
765
836
  window.open(this.src.sources[0].src, '_blank')
766
837
  },
767
- onRewind() {
768
- // Rewind in seconds
769
- const seconds = 10
770
-
838
+ rewind(seconds = 10) {
771
839
  if (this.player.currentTime <= seconds) {
772
840
  this.setTime(0)
773
841
  } else {
774
842
  this.setTime(this.player.currentTime - seconds)
775
843
  }
776
844
  },
777
- onFullscreen() {
778
- this.options.fullscreen = !document.fullscreenElement
845
+ fastForward(seconds = 10) {
846
+ if (this.player.currentTime + seconds >= this.player.duration) {
847
+ this.setTime(this.player.duration)
848
+ } else {
849
+ this.setTime(this.player.currentTime + seconds)
850
+ }
851
+ },
852
+ fullscreenToggle() {
853
+ this.state.fullscreen = !document.fullscreenElement
779
854
  // Return the whole element to be fullscreened so the controls come with it
780
- this.$emit('click:fullscreen', this.$el)
855
+ this.$emit('click:fullscreen', this.$refs.playerContainer)
781
856
  },
782
857
  onPictureInPicture() {
783
- //this.options.pip = !document.fullscreenElement;
858
+ //this.state.pip = !document.fullscreenElement;
784
859
  // Return the player aka HTMLVideoElement
785
860
  this.$emit('click:pictureinpicture', this.$refs.player)
786
861
  },
@@ -788,16 +863,16 @@ export default {
788
863
  this.$emit('click:remoteplayback', this.$refs.player)
789
864
  },
790
865
  onVideoHover(e) {
791
- this.options.controls = true
792
- clearTimeout(this.options.controlsDebounce)
866
+ this.state.controls = true
867
+ clearTimeout(this.state.controlsDebounce)
793
868
  this.$emit('mouseover', e)
794
869
  },
795
870
  onVideoLeave(e) {
796
871
  const self = this
797
872
  // Clear any existing timeouts before we create one
798
- clearTimeout(this.options.controlsDebounce)
799
- this.options.controlsDebounce = setTimeout(() => {
800
- self.options.controls = false
873
+ clearTimeout(this.state.controlsDebounce)
874
+ this.state.controlsDebounce = setTimeout(() => {
875
+ self.state.controls = false
801
876
  }, 50)
802
877
  this.$emit('mouseout', e)
803
878
  },
@@ -828,25 +903,25 @@ export default {
828
903
  this.activeAd !== null &&
829
904
  this.activeAd.play_at_percent === 100
830
905
  ) {
831
- this.showReplay = true
906
+ this.state.replay = true
832
907
  // Ended but this ad was a postroll
833
908
  this.$emit('ended', e)
834
909
  } else {
835
- this.showReplay = true
910
+ this.state.replay = true
836
911
  // Ended without an ad
837
912
  this.$emit('ended', e)
838
913
  }
839
914
  },
840
915
  onControlsHover() {
841
- clearTimeout(this.options.controlsDebounce)
842
- this.options.controls = true
916
+ clearTimeout(this.state.controlsDebounce)
917
+ this.state.controls = true
843
918
  },
844
919
  onControlsLeave() {
845
920
  const self = this
846
921
  // Clear any existing timeouts before we create one
847
- clearTimeout(this.options.controlsDebounce)
848
- this.options.controlsDebounce = setTimeout(() => {
849
- self.options.controls = false
922
+ clearTimeout(this.state.controlsDebounce)
923
+ this.state.controlsDebounce = setTimeout(() => {
924
+ self.state.controls = false
850
925
  }, 50)
851
926
  },
852
927
  /**
@@ -857,25 +932,25 @@ export default {
857
932
  onSelectTrack(lang = null) {
858
933
  if (this.player.textTracks && this.player.textTracks.length > 0) {
859
934
  for (let i = 0; i < this.player.textTracks.length; i++) {
860
- const tt = this.player.textTracks[i]
935
+ // Disable all tracks by default
936
+ // We only want to enable the correct active track otherwise track switches / replays will overlay tracks
937
+ this.player.textTracks[i].mode = 'disabled'
861
938
 
862
- if (tt.language === lang) {
863
- this.options.ccLang = lang
939
+ if (this.player.textTracks[i].language === lang) {
940
+ this.state.ccLang = lang
864
941
  this.player.textTracks[i].mode = 'showing'
865
942
 
866
- this.setCues(tt)
943
+ this.setCues(this.player.textTracks[i])
867
944
 
868
945
  // Emit the current track
869
- this.$emit('trackchange', tt)
870
- } else {
871
- this.player.textTracks[i].mode = 'disabled'
946
+ this.$emit('trackchange', this.player.textTracks[i])
872
947
  }
873
948
  }
874
949
  }
875
950
  },
876
- onPlaybackSpeed(index) {
951
+ onPlaybackSpeedChange(index) {
877
952
  this.player.playbackRate = this.attributes.playbackrates[index]
878
- this.options.playbackRateIndex = index
953
+ this.state.playbackRateIndex = index
879
954
  this.$emit('ratechange', this.player.playbackRate)
880
955
  },
881
956
  onTimeupdate(e) {
@@ -905,31 +980,15 @@ export default {
905
980
  onMediaProgress(e) {
906
981
  this.$emit('progress', e)
907
982
  },
908
- onCCToggle() {
909
- this.options.cc = !this.options.cc
983
+ CCToggle() {
984
+ this.state.cc = !this.state.cc
910
985
 
911
- if (this.options.cc) {
912
- this.onSelectTrack(this.options.ccLang)
986
+ if (this.state.cc) {
987
+ this.onSelectTrack(this.state.ccLang)
913
988
  } else {
914
989
  this.onSelectTrack()
915
990
  }
916
991
  },
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
992
  onClickReplay(e) {
934
993
  // Re-initialize the ads aka pre/post/midroll
935
994
  if (this.src.ads && this.src.ads.length) {
@@ -955,13 +1014,13 @@ export default {
955
1014
  // Restart from the beginning
956
1015
  this.setTime(0)
957
1016
  },
958
- onMuteToggle() {
1017
+ muteToggle() {
959
1018
  if (this.player.muted) {
960
- this.options.muted = false
1019
+ this.state.muted = false
961
1020
  this.player.muted = false
962
- this.$emit('volumechange', this.options.volume)
1021
+ this.$emit('volumechange', this.state.volume)
963
1022
  } else {
964
- this.options.muted = true
1023
+ this.state.muted = true
965
1024
  this.player.muted = true
966
1025
  this.$emit('volumechange', 0)
967
1026
  }
@@ -994,6 +1053,15 @@ export default {
994
1053
  if (typeof track.activeCues[0].rawText === 'undefined') {
995
1054
  track.activeCues[0].rawText = track.activeCues[0].text
996
1055
  }
1056
+ // Retain the original cue display values
1057
+ // This way we can swap between a modified display when the controls are visible
1058
+ if (typeof track.activeCues[0].defaults === 'undefined') {
1059
+ track.activeCues[0].defaults = {
1060
+ line: track.activeCues[0].line,
1061
+ size: track.activeCues[0].size,
1062
+ snapToLines: track.activeCues[0].snapToLines,
1063
+ }
1064
+ }
997
1065
 
998
1066
  // Now remove `<c.transcript>` tags
999
1067
  const transcriptTagRegex = /<c.transcript>.*?<\/c>/gi
@@ -1003,6 +1071,8 @@ export default {
1003
1071
  transcriptTagRegex,
1004
1072
  ''
1005
1073
  )
1074
+
1075
+ this.setCuePosition()
1006
1076
  }
1007
1077
 
1008
1078
  this.setCues(track)
@@ -1032,19 +1102,31 @@ export default {
1032
1102
  //this.player.media = this.$refs.player;
1033
1103
  this.$emit('loadedmetadata', e)
1034
1104
  this.player = this.$refs.player
1035
- this.player.volume = this.options.volume
1036
- this.$emit('volumechange', this.options.volume)
1105
+ this.player.volume = this.state.volume
1106
+ this.$emit('volumechange', this.state.volume)
1037
1107
  },
1038
- onVolumeChange(value) {
1039
- this.options.volume = value
1108
+ volumeChange(value) {
1109
+ // Value needs to be a decimal value between 0 and 1
1110
+ if (value > 1) {
1111
+ value = 1
1112
+ } else if (value < 0) {
1113
+ value = 0
1114
+ }
1115
+ this.state.volume = value
1040
1116
  this.player.volume = value
1041
1117
  this.$emit('volumechange', value)
1042
1118
  },
1119
+ volumeAdjust(value) {
1120
+ const newVolume = this.state.volume + value
1121
+ this.volumeChange(newVolume)
1122
+ },
1043
1123
  onDurationChange() {
1044
1124
  // console.log('onDurationChange');
1045
1125
  // console.log(e);
1046
1126
  },
1047
1127
  setTime(time) {
1128
+ // Scrubbing / manually setting the time should remove the replay button
1129
+ this.state.replay = false
1048
1130
  this.player.currentTime = time
1049
1131
  },
1050
1132
  setCues(track) {
@@ -1056,19 +1138,76 @@ export default {
1056
1138
  // Required so the v-model will actually update.
1057
1139
  this.captions.nonce = Math.random()
1058
1140
  },
1141
+ setCuePosition() {
1142
+ if (
1143
+ this.player &&
1144
+ this.player.textTracks &&
1145
+ this.player.textTracks.length > 0
1146
+ ) {
1147
+ for (let i = 0; i < this.player.textTracks.length; i++) {
1148
+ // Only alter the currently showing text track
1149
+ if (this.player.textTracks[i].mode === 'showing') {
1150
+ // If the controls are showing then bump the alignment to the start
1151
+ if (
1152
+ this.state.controls &&
1153
+ this.player.textTracks[i].activeCues &&
1154
+ this.player.textTracks[i].activeCues.length > 0
1155
+ ) {
1156
+ // Count the number of line breaks in the cue to figure out our offset from the bottom
1157
+ // VTTCue doesn't have a "margin from bottom" by default
1158
+ const numLines = (
1159
+ this.player.textTracks[
1160
+ i
1161
+ ].activeCues[0].text.match(/\n/g) || []
1162
+ ).length
1163
+
1164
+ // Limit the cues to 90% of the screen width
1165
+ // If this is left default / set to 100 then the above line
1166
+ // 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
1167
+ this.player.textTracks[i].activeCues[0].line =
1168
+ -3 - numLines
1169
+ this.player.textTracks[i].activeCues[0].size = 99
1170
+ this.player.textTracks[
1171
+ i
1172
+ ].activeCues[0].snapToLines = true
1173
+ } else if (
1174
+ this.player.textTracks[i].activeCues &&
1175
+ this.player.textTracks[i].activeCues.length > 0 &&
1176
+ typeof this.player.textTracks[i].activeCues[0]
1177
+ .defaults !== 'undefined'
1178
+ ) {
1179
+ this.player.textTracks[i].activeCues[0].line =
1180
+ this.player.textTracks[
1181
+ i
1182
+ ].activeCues[0].defaults.line
1183
+ this.player.textTracks[i].activeCues[0].size =
1184
+ this.player.textTracks[
1185
+ i
1186
+ ].activeCues[0].defaults.size
1187
+ this.player.textTracks[
1188
+ i
1189
+ ].activeCues[0].snapToLines =
1190
+ this.player.textTracks[
1191
+ i
1192
+ ].activeCues[0].defaults.snapToLines
1193
+ }
1194
+ }
1195
+ }
1196
+ }
1197
+ },
1059
1198
  load(e = null) {
1060
1199
  if (this.player.load) {
1061
1200
  // Reload the player to refresh all the sources / tracks
1062
1201
  this.player.load()
1063
1202
  this.$emit('load', e)
1064
1203
  } else {
1065
- console.log('Cannot load player')
1204
+ console.error('Cannot load player')
1066
1205
  }
1067
1206
  },
1068
1207
  pause(e = null) {
1069
1208
  if (this.player.pause) {
1070
1209
  this.player.pause()
1071
- this.options.paused = true
1210
+ this.state.paused = true
1072
1211
  this.$emit('pause', e)
1073
1212
  } else {
1074
1213
  console.log('Cannot pause player')
@@ -1078,12 +1217,35 @@ export default {
1078
1217
  if (this.player.play) {
1079
1218
  // Start playing the main video
1080
1219
  this.player.play()
1081
- this.options.paused = false
1220
+ this.state.paused = false
1221
+ this.state.replay = false
1082
1222
  this.$emit('play', e)
1083
1223
  } else {
1084
1224
  console.log('Cannot play player')
1085
1225
  }
1086
1226
  },
1227
+ playToggle(e) {
1228
+ // If the replay button is active then we actually need to call the onClickReplay method instead
1229
+ // Otherwise we'd just end up replaying any postroll ad
1230
+ if (this.state.replay) {
1231
+ this.onClickReplay(e)
1232
+ } else {
1233
+ const self = this
1234
+ this.state.controls = true
1235
+
1236
+ // Clear any existing timeouts and close the controls in 5 seconds
1237
+ clearTimeout(this.state.controlsDebounce)
1238
+ this.state.controlsDebounce = setTimeout(() => {
1239
+ self.state.controls = false
1240
+ }, 5000)
1241
+
1242
+ if (this.player.paused) {
1243
+ this.play(e)
1244
+ } else {
1245
+ this.pause(e)
1246
+ }
1247
+ }
1248
+ },
1087
1249
  },
1088
1250
  }
1089
1251
  </script>
@@ -1094,29 +1256,11 @@ export default {
1094
1256
  position: relative;
1095
1257
  top: -50px;
1096
1258
  margin-bottom: -40px;
1097
- overflow: hidden;
1098
1259
  }
1099
1260
  .controls {
1100
1261
  height: 40px;
1101
1262
  background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.7));
1102
1263
  }
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
1264
  .player-audio {
1121
1265
  height: 40px;
1122
1266
  }
@@ -1134,12 +1278,18 @@ export default {
1134
1278
  color: #fff;
1135
1279
  left: 25%;
1136
1280
  width: 50%;
1137
- top: 100px;
1281
+ top: 35%;
1138
1282
  height: 0;
1139
1283
  text-align: center;
1140
1284
  }
1141
- .player-overlay > div {
1285
+ .player-overlay--replay-icon {
1286
+ color: #fff;
1287
+ font-size: 5rem;
1288
+ }
1289
+ .player-overlay > .player-overlay--icon {
1290
+ display: inline-block;
1142
1291
  background: rgba(0, 0, 0, 0.25);
1143
1292
  border-radius: 100%;
1293
+ padding: 1rem;
1144
1294
  }
1145
1295
  </style>