@mindedge/vuetify-player 0.3.0 → 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 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,23 +141,50 @@
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
 
150
+ <!-- Replay button -->
151
+ <v-tooltip v-if="state.replay" top>
152
+ <template #activator="{ on, attrs }">
153
+ <v-btn
154
+ small
155
+ text
156
+ v-bind="attrs"
157
+ v-on="on"
158
+ @click="onClickReplay"
159
+ >
160
+ <v-icon>mdi-replay</v-icon>
161
+ <span class="d-sr-only">
162
+ {{
163
+ t(
164
+ language,
165
+ 'player.replay'
166
+ )
167
+ }}
168
+ </span>
169
+ </v-btn>
170
+ </template>
171
+ <span>{{
172
+ t(language, 'player.replay')
173
+ }}</span>
174
+ </v-tooltip>
175
+
130
176
  <!-- Rewind Button-->
131
- <v-tooltip v-if="attributes.rewind" top>
132
- <template
133
- v-slot:activator="{ on, attrs }"
134
- >
177
+ <v-tooltip
178
+ v-if="attributes.rewind && !activeAd"
179
+ top
180
+ >
181
+ <template #activator="{ on, attrs }">
135
182
  <v-btn
136
183
  small
137
184
  text
138
185
  v-bind="attrs"
139
186
  v-on="on"
140
- @click="onRewind"
187
+ @click="rewind"
141
188
  >
142
189
  <v-icon>mdi-rewind-10</v-icon>
143
190
  <span class="sr-only">{{
@@ -161,22 +208,21 @@
161
208
  current.tracks &&
162
209
  current.tracks.length > 0
163
210
  "
211
+ :attach="$refs.controlsContainer"
164
212
  open-on-hover
165
- top
166
213
  offset-y
214
+ top
167
215
  >
168
- <template
169
- v-slot:activator="{ on, attrs }"
170
- >
216
+ <template #activator="{ on, attrs }">
171
217
  <v-btn
172
218
  small
173
219
  text
174
220
  v-bind="attrs"
175
221
  v-on="on"
176
- @click="onCCToggle"
222
+ @click="CCToggle"
177
223
  >
178
224
  <v-icon>{{
179
- options.cc
225
+ state.cc
180
226
  ? 'mdi-closed-caption'
181
227
  : 'mdi-closed-caption-outline'
182
228
  }}</v-icon>
@@ -209,45 +255,47 @@
209
255
  </v-menu>
210
256
 
211
257
  <!-- Volume -->
212
- <v-menu open-on-hover top offset-y>
213
- <template
214
- v-slot:activator="{ on, attrs }"
215
- >
258
+ <v-menu
259
+ :attach="$refs.controlsContainer"
260
+ open-on-hover
261
+ offset-y
262
+ top
263
+ >
264
+ <template #activator="{ on, attrs }">
216
265
  <v-btn
217
266
  small
218
267
  text
219
268
  v-bind="attrs"
220
269
  v-on="on"
221
- @click="onMuteToggle"
270
+ @click="muteToggle"
222
271
  >
223
272
  <v-icon
224
273
  v-if="
225
- !options.muted &&
226
- options.volume > 0.75
274
+ !state.muted &&
275
+ state.volume > 0.75
227
276
  "
228
277
  >mdi-volume-high</v-icon
229
278
  >
230
279
  <v-icon
231
280
  v-if="
232
- !options.muted &&
233
- options.volume >=
234
- 0.25 &&
235
- options.volume <= 0.75
281
+ !state.muted &&
282
+ state.volume >= 0.25 &&
283
+ state.volume <= 0.75
236
284
  "
237
285
  >mdi-volume-medium</v-icon
238
286
  >
239
287
  <v-icon
240
288
  v-if="
241
- !options.muted &&
242
- options.volume > 0 &&
243
- options.volume < 0.25
289
+ !state.muted &&
290
+ state.volume > 0 &&
291
+ state.volume < 0.25
244
292
  "
245
293
  >mdi-volume-low</v-icon
246
294
  >
247
295
  <v-icon
248
296
  v-if="
249
- options.muted ||
250
- options.volume === 0
297
+ state.muted ||
298
+ state.volume === 0
251
299
  "
252
300
  >mdi-volume-off</v-icon
253
301
  >
@@ -268,31 +316,29 @@
268
316
  )
269
317
  }}</span>
270
318
  <v-slider
271
- v-model="options.volume"
319
+ v-model="state.volume"
272
320
  inverse-label
273
321
  :min="0"
274
322
  :max="1"
275
323
  :step="0.1"
276
324
  vertical
277
- @change="onVolumeChange"
325
+ @change="volumeChange"
278
326
  ></v-slider>
279
327
  </v-sheet>
280
328
  </v-menu>
281
329
 
282
330
  <!-- Fullscreen -->
283
- <v-tooltip v-if="fullscreenEnabled" top>
284
- <template
285
- v-slot:activator="{ on, attrs }"
286
- >
331
+ <v-tooltip v-if="allowFullscreen" top>
332
+ <template #activator="{ on, attrs }">
287
333
  <v-btn
288
334
  small
289
335
  text
290
336
  v-bind="attrs"
291
337
  v-on="on"
292
- @click="onFullscreen"
338
+ @click="fullscreenToggle"
293
339
  >
294
340
  <v-icon>{{
295
- !options.fullscreen
341
+ !state.fullscreen
296
342
  ? 'mdi-fullscreen'
297
343
  : 'mdi-fullscreen-exit'
298
344
  }}</v-icon>
@@ -319,9 +365,7 @@
319
365
  "
320
366
  top
321
367
  >
322
- <template
323
- v-slot:activator="{ on, attrs }"
324
- >
368
+ <template #activator="{ on, attrs }">
325
369
  <v-btn
326
370
  small
327
371
  text
@@ -349,13 +393,8 @@
349
393
  </v-tooltip>
350
394
 
351
395
  <!-- Remote playback -->
352
- <v-tooltip
353
- v-if="options.remoteplayback"
354
- top
355
- >
356
- <template
357
- v-slot:activator="{ on, attrs }"
358
- >
396
+ <v-tooltip v-if="allowRemotePlayback" top>
397
+ <template #activator="{ on, attrs }">
359
398
  <v-btn
360
399
  small
361
400
  text
@@ -381,10 +420,8 @@
381
420
  </v-tooltip>
382
421
 
383
422
  <!-- Download -->
384
- <v-tooltip v-if="options.download" top>
385
- <template
386
- v-slot:activator="{ on, attrs }"
387
- >
423
+ <v-tooltip v-if="allowDownload" top>
424
+ <template #activator="{ on, attrs }">
388
425
  <v-btn
389
426
  small
390
427
  text
@@ -407,117 +444,18 @@
407
444
  </v-tooltip>
408
445
 
409
446
  <!-- Settings -->
410
- <v-menu
411
- top
412
- offset-y
413
- :close-on-content-click="false"
414
- nudge-left="100"
415
- >
416
- <template
417
- v-slot:activator="{ on, attrs }"
418
- >
419
- <v-btn
420
- small
421
- text
422
- v-bind="attrs"
423
- v-on="on"
424
- >
425
- <v-icon>mdi-cog</v-icon>
426
- <span class="d-sr-only">{{
427
- t(
428
- language,
429
- 'player.toggle_settings'
430
- )
431
- }}</span>
432
- </v-btn>
433
- </template>
434
-
435
- <v-list>
436
- <v-list-item>
437
- <v-list-item-title>
438
- <v-icon
439
- >mdi-play-speed</v-icon
440
- >
441
- {{
442
- t(
443
- language,
444
- 'player.playback_speed'
445
- )
446
- }}
447
- </v-list-item-title>
448
- </v-list-item>
449
- <v-list-item>
450
- <v-list-item-title
451
- class="text-center"
452
- >
453
- <v-btn
454
- small
455
- :disabled="
456
- options.playbackRateIndex ===
457
- 0
458
- "
459
- @click="
460
- onPlaybackSpeed(
461
- options.playbackRateIndex -
462
- 1
463
- )
464
- "
465
- >
466
- <v-icon>
467
- mdi-clock-minus-outline
468
- </v-icon>
469
- <span
470
- class="d-sr-only"
471
- >{{
472
- t(
473
- language,
474
- 'player.playback_decrease'
475
- )
476
- }}</span
477
- >
478
- </v-btn>
479
- <span class="pl-2 pr-2"
480
- >{{
481
- attributes
482
- .playbackrates[
483
- options
484
- .playbackRateIndex
485
- ]
486
- }}x</span
487
- >
488
- <v-btn
489
- small
490
- :disabled="
491
- options.playbackRateIndex >=
492
- attributes
493
- .playbackrates
494
- .length -
495
- 1
496
- "
497
- @click="
498
- onPlaybackSpeed(
499
- options.playbackRateIndex +
500
- 1
501
- )
502
- "
503
- >
504
- <v-icon>
505
- mdi-clock-plus-outline
506
- </v-icon>
507
- <span
508
- class="d-sr-only"
509
- >{{
510
- t(
511
- language,
512
- 'player.playback_increase'
513
- )
514
- }}</span
515
- >
516
- </v-btn>
517
- </v-list-item-title>
518
- </v-list-item>
519
- </v-list>
520
- </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>
521
459
  </template>
522
460
  </v-slider>
523
461
  </div>
@@ -533,14 +471,32 @@
533
471
  captions.cues &&
534
472
  Object.keys(captions.cues).length
535
473
  "
536
- :cols="!options.expandedCaptions ? 12 : 6"
474
+ :cols="!state.expandedCaptions ? 12 : 6"
475
+ class="pt-0 mt-0"
537
476
  >
538
477
  <CaptionsMenu
539
478
  v-model="captions"
540
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)"
541
495
  @click:cue="onCueClick"
542
496
  @click:expand="onClickExpandCaptions"
543
- @click:paragraph="onClickParagraph"
497
+ @click:paragraph-view="onClickParagraph"
498
+ @click:autoscroll="onClickAutoscroll"
499
+ @click:close="onClickCaptionsClose"
544
500
  ></CaptionsMenu>
545
501
  </v-col>
546
502
  </v-row>
@@ -549,12 +505,14 @@
549
505
 
550
506
  <script>
551
507
  import filters from '../filters'
508
+ import SettingsMenu from './SettingsMenu.vue'
552
509
  import CaptionsMenu from './CaptionsMenu.vue'
553
510
  import { t } from '../../i18n/i18n'
554
511
 
555
512
  export default {
556
513
  name: 'Html5Player',
557
514
  components: {
515
+ SettingsMenu,
558
516
  CaptionsMenu,
559
517
  },
560
518
  props: {
@@ -562,7 +520,7 @@ export default {
562
520
  type: {
563
521
  type: String,
564
522
  required: false,
565
- default: 'video',
523
+ default: 'auto',
566
524
  },
567
525
  attributes: {
568
526
  type: Object,
@@ -572,33 +530,187 @@ export default {
572
530
  type: Object,
573
531
  required: true,
574
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
+ },
575
610
  },
576
- watch: {},
577
611
  computed: {
578
612
  current() {
579
613
  // We're playing an ad currently
580
614
  if (this.activeAd) {
581
615
  return this.activeAd
582
-
583
- // We hit an ad spot~ play_at_percent
584
- } else if (
585
- !this.activeAd &&
586
- typeof this.ads[this.currentPercent] !== 'undefined' &&
587
- this.ads[this.currentPercent].sources &&
588
- this.ads[this.currentPercent].sources.length &&
589
- !this.ads[this.currentPercent].complete
590
- ) {
591
- this.setActiveAd(this.currentPercent)
592
- return this.ads[this.currentPercent]
593
616
  } else {
594
617
  // Only change sources if we're not watching an ad or pre/postroll
595
618
  return this.src
596
619
  }
597
620
  },
598
621
  playerClass() {
599
- let classList = 'player-' + this.type
622
+ let classList = 'player-' + this.resolvedType
600
623
  return classList
601
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
+ },
602
714
  },
603
715
  data() {
604
716
  return {
@@ -609,8 +721,8 @@ export default {
609
721
  currentPercent: 0,
610
722
  player: {},
611
723
  captions: { nonce: 0 },
612
- fullscreenEnabled: false,
613
- options: {
724
+ state: {
725
+ replay: false,
614
726
  cc: true,
615
727
  ccLang: this.language,
616
728
  controls: true,
@@ -621,8 +733,7 @@ export default {
621
733
  playbackRateIndex: 0,
622
734
  fullscreen: false,
623
735
  expandedCaptions: false,
624
- download: false,
625
- remoteplayback: false,
736
+ captionsVisible: true,
626
737
  controlslist: [],
627
738
  },
628
739
  watchPlayer: 0,
@@ -630,9 +741,65 @@ export default {
630
741
  buffering: false,
631
742
  }
632
743
  },
744
+ beforeMount() {
745
+ // Parse the html controlslist attribute string
746
+ if (
747
+ this.attributes.controlslist &&
748
+ typeof this.attributes.controlslist === 'string' &&
749
+ this.attributes.controlslist !== ''
750
+ ) {
751
+ this.state.controlslist = this.attributes.controlslist.split(' ')
752
+ }
753
+
754
+ if (
755
+ typeof this.attributes.playbackrates === 'undefined' ||
756
+ this.attributes.playbackrates.length === 0
757
+ ) {
758
+ throw new Error(
759
+ 'attributes.playbackrates must be defined and an array of numbers!'
760
+ )
761
+ }
762
+
763
+ // Adjust the playback speed to 1 by default
764
+ if (this.attributes.playbackrates.indexOf(1) !== -1) {
765
+ this.state.playbackRateIndex =
766
+ this.attributes.playbackrates.indexOf(1)
767
+ } else {
768
+ // 1 aka normal playback not enabled (What monster would do this?!)
769
+ // Set the playback rate to "middle of the road" for whatever is available
770
+ this.state.playbackRateIndex = Math.floor(
771
+ this.attributes.playbackrates.length / 2
772
+ )
773
+ }
774
+
775
+ // Initialize the ads aka pre/post/midroll
776
+ if (this.src.ads && this.src.ads.length) {
777
+ for (const ad of this.src.ads) {
778
+ // Map to a percent so we can avoid dupe timings and have easier lookups
779
+ this.ads[ad.play_at_percent] = ad
780
+ this.ads[ad.play_at_percent].complete = false
781
+ }
782
+ }
783
+ },
784
+ mounted() {
785
+ if (
786
+ !this.activeAd &&
787
+ typeof this.ads[this.currentPercent] !== 'undefined' &&
788
+ this.ads[this.currentPercent].sources &&
789
+ this.ads[this.currentPercent].sources.length &&
790
+ !this.ads[this.currentPercent].complete
791
+ ) {
792
+ this.activeAd = this.ads[this.currentPercent]
793
+ }
794
+ },
633
795
  methods: {
634
- setActiveAd(currentPercent) {
796
+ setActiveAd(currentPercent, e = null) {
635
797
  this.activeAd = this.ads[currentPercent]
798
+
799
+ // Reload the player to refresh all the sources / tracks
800
+ this.load(e)
801
+ // Start playing the main video
802
+ this.play(e)
636
803
  },
637
804
  percentToTimeSeconds(percent) {
638
805
  const scaleFactor = this.player.duration / this.scrub.max
@@ -648,34 +815,45 @@ export default {
648
815
  },
649
816
  onCueClick(time) {
650
817
  this.setTime(time)
818
+ this.$emit('click:captions-cue', time)
651
819
  },
652
820
  onClickExpandCaptions(expanded) {
653
- this.options.expandedCaptions = expanded
654
821
  this.$emit('click:captions-expand', expanded)
655
822
  },
656
823
  onClickParagraph(isParagraph) {
657
- 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')
658
832
  },
659
833
  onDownload() {
660
834
  window.open(this.src.sources[0].src, '_blank')
661
835
  },
662
- onRewind() {
663
- // Rewind in seconds
664
- const seconds = 10
665
-
836
+ rewind(seconds = 10) {
666
837
  if (this.player.currentTime <= seconds) {
667
838
  this.setTime(0)
668
839
  } else {
669
840
  this.setTime(this.player.currentTime - seconds)
670
841
  }
671
842
  },
672
- onFullscreen() {
673
- 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
674
852
  // Return the whole element to be fullscreened so the controls come with it
675
- this.$emit('click:fullscreen', this.$el)
853
+ this.$emit('click:fullscreen', this.$refs.playerContainer)
676
854
  },
677
855
  onPictureInPicture() {
678
- //this.options.pip = !document.fullscreenElement;
856
+ //this.state.pip = !document.fullscreenElement;
679
857
  // Return the player aka HTMLVideoElement
680
858
  this.$emit('click:pictureinpicture', this.$refs.player)
681
859
  },
@@ -683,51 +861,65 @@ export default {
683
861
  this.$emit('click:remoteplayback', this.$refs.player)
684
862
  },
685
863
  onVideoHover(e) {
686
- this.options.controls = true
687
- clearTimeout(this.options.controlsDebounce)
864
+ this.state.controls = true
865
+ clearTimeout(this.state.controlsDebounce)
688
866
  this.$emit('mouseover', e)
689
867
  },
690
868
  onVideoLeave(e) {
691
869
  const self = this
692
870
  // Clear any existing timeouts before we create one
693
- clearTimeout(this.options.controlsDebounce)
694
- this.options.controlsDebounce = setTimeout(() => {
695
- self.options.controls = false
871
+ clearTimeout(this.state.controlsDebounce)
872
+ this.state.controlsDebounce = setTimeout(() => {
873
+ self.state.controls = false
696
874
  }, 50)
697
875
  this.$emit('mouseout', e)
698
876
  },
699
877
  onEnded(e) {
700
- if (this.activeAd) {
878
+ // Active ad ended but only continue playing if the video didn't just end on a postroll
879
+ if (this.activeAd && this.activeAd.play_at_percent !== 100) {
701
880
  this.ads[this.activeAd.play_at_percent].complete = true
702
881
  // Go back to the play_at_percent for the main video
703
882
  this.currentPercent = this.activeAd.play_at_percent
883
+
704
884
  this.activeAd = null
705
885
 
706
886
  // Reload the player to refresh all the sources / tracks
707
887
  this.load(e)
888
+
708
889
  // Start playing the main video
709
890
  this.play(e)
891
+ } else if (
892
+ !this.activeAd &&
893
+ typeof this.ads[this.currentPercent] !== 'undefined' &&
894
+ this.ads[this.currentPercent].sources &&
895
+ this.ads[this.currentPercent].sources.length &&
896
+ !this.ads[this.currentPercent].complete
897
+ ) {
898
+ // Video ended but there's an ad (probably 100% ad)
899
+ this.setActiveAd(this.currentPercent, e)
710
900
  } else if (
711
901
  this.activeAd !== null &&
712
902
  this.activeAd.play_at_percent === 100
713
903
  ) {
904
+ this.state.replay = true
714
905
  // Ended but this ad was a postroll
715
906
  this.$emit('ended', e)
716
907
  } else {
908
+ this.state.replay = true
717
909
  // Ended without an ad
718
910
  this.$emit('ended', e)
719
911
  }
720
912
  },
721
913
  onControlsHover() {
722
- clearTimeout(this.options.controlsDebounce)
723
- this.options.controls = true
914
+ clearTimeout(this.state.controlsDebounce)
915
+ this.state.controls = true
724
916
  },
725
917
  onControlsLeave() {
726
918
  const self = this
727
919
  // Clear any existing timeouts before we create one
728
- clearTimeout(this.options.controlsDebounce)
729
- this.options.controlsDebounce = setTimeout(() => {
730
- self.options.controls = false
920
+ clearTimeout(this.state.controlsDebounce)
921
+ this.state.controlsDebounce = setTimeout(() => {
922
+ self.state.controls = false
731
923
  }, 50)
732
924
  },
733
925
  /**
@@ -738,25 +930,25 @@ export default {
738
930
  onSelectTrack(lang = null) {
739
931
  if (this.player.textTracks && this.player.textTracks.length > 0) {
740
932
  for (let i = 0; i < this.player.textTracks.length; i++) {
741
- 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'
742
936
 
743
- if (tt.language === lang) {
744
- this.options.ccLang = lang
937
+ if (this.player.textTracks[i].language === lang) {
938
+ this.state.ccLang = lang
745
939
  this.player.textTracks[i].mode = 'showing'
746
940
 
747
- this.setCues(tt)
941
+ this.setCues(this.player.textTracks[i])
748
942
 
749
943
  // Emit the current track
750
- this.$emit('trackchange', tt)
751
- } else {
752
- this.player.textTracks[i].mode = 'disabled'
944
+ this.$emit('trackchange', this.player.textTracks[i])
753
945
  }
754
946
  }
755
947
  }
756
948
  },
757
- onPlaybackSpeed(index) {
949
+ onPlaybackSpeedChange(index) {
758
950
  this.player.playbackRate = this.attributes.playbackrates[index]
759
- this.options.playbackRateIndex = index
951
+ this.state.playbackRateIndex = index
760
952
  this.$emit('ratechange', this.player.playbackRate)
761
953
  },
762
954
  onTimeupdate(e) {
@@ -764,6 +956,17 @@ export default {
764
956
  (this.player.currentTime / this.player.duration) * 100
765
957
  )
766
958
 
959
+ // Check if there's an ad that needs to be played
960
+ if (
961
+ !this.activeAd &&
962
+ typeof this.ads[this.currentPercent] !== 'undefined' &&
963
+ this.ads[this.currentPercent].sources &&
964
+ this.ads[this.currentPercent].sources.length &&
965
+ !this.ads[this.currentPercent].complete
966
+ ) {
967
+ this.setActiveAd(this.currentPercent, e)
968
+ }
969
+
767
970
  this.$emit('timeupdate', {
768
971
  event: e,
769
972
  current_percent: this.currentPercent,
@@ -775,38 +978,47 @@ export default {
775
978
  onMediaProgress(e) {
776
979
  this.$emit('progress', e)
777
980
  },
778
- onCCToggle() {
779
- this.options.cc = !this.options.cc
981
+ CCToggle() {
982
+ this.state.cc = !this.state.cc
780
983
 
781
- if (this.options.cc) {
782
- this.onSelectTrack(this.options.ccLang)
984
+ if (this.state.cc) {
985
+ this.onSelectTrack(this.state.ccLang)
783
986
  } else {
784
987
  this.onSelectTrack()
785
988
  }
786
989
  },
787
- onPlayToggle(e) {
788
- const self = this
789
- this.options.controls = true
790
-
791
- // Clear any existing timeouts and close the controls in 5 seconds
792
- clearTimeout(this.options.controlsDebounce)
793
- this.options.controlsDebounce = setTimeout(() => {
794
- self.options.controls = false
795
- }, 5000)
990
+ onClickReplay(e) {
991
+ // Re-initialize the ads aka pre/post/midroll
992
+ if (this.src.ads && this.src.ads.length) {
993
+ for (const ad of this.src.ads) {
994
+ // Map to a percent so we can avoid dupe timings and have easier lookups
995
+ this.ads[ad.play_at_percent] = ad
996
+ this.ads[ad.play_at_percent].complete = false
997
+ }
796
998
 
797
- if (this.player.paused) {
798
- this.play(e)
799
- } else {
800
- this.pause(e)
999
+ // There's a pre-roll / start ad. Reassign as the active
1000
+ if (typeof this.ads[0] !== 'undefined') {
1001
+ this.activeAd = this.ads[0]
1002
+ } else {
1003
+ // Clear the active ad otherwise
1004
+ this.activeAd = null
1005
+ }
801
1006
  }
1007
+
1008
+ // Reload the player to refresh all the sources / tracks
1009
+ this.load(e)
1010
+ // Start playing the main video
1011
+ this.play(e)
1012
+ // Restart from the beginning
1013
+ this.setTime(0)
802
1014
  },
803
- onMuteToggle() {
1015
+ muteToggle() {
804
1016
  if (this.player.muted) {
805
- this.options.muted = false
1017
+ this.state.muted = false
806
1018
  this.player.muted = false
807
- this.$emit('volumechange', this.options.volume)
1019
+ this.$emit('volumechange', this.state.volume)
808
1020
  } else {
809
- this.options.muted = true
1021
+ this.state.muted = true
810
1022
  this.player.muted = true
811
1023
  this.$emit('volumechange', 0)
812
1024
  }
@@ -839,6 +1051,15 @@ export default {
839
1051
  if (typeof track.activeCues[0].rawText === 'undefined') {
840
1052
  track.activeCues[0].rawText = track.activeCues[0].text
841
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
+ }
842
1063
 
843
1064
  // Now remove `<c.transcript>` tags
844
1065
  const transcriptTagRegex = /<c.transcript>.*?<\/c>/gi
@@ -848,6 +1069,8 @@ export default {
848
1069
  transcriptTagRegex,
849
1070
  ''
850
1071
  )
1072
+
1073
+ this.setCuePosition()
851
1074
  }
852
1075
 
853
1076
  this.setCues(track)
@@ -877,19 +1100,31 @@ export default {
877
1100
  //this.player.media = this.$refs.player;
878
1101
  this.$emit('loadedmetadata', e)
879
1102
  this.player = this.$refs.player
880
- this.player.volume = this.options.volume
881
- this.$emit('volumechange', this.options.volume)
1103
+ this.player.volume = this.state.volume
1104
+ this.$emit('volumechange', this.state.volume)
882
1105
  },
883
- onVolumeChange(value) {
884
- 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
885
1114
  this.player.volume = value
886
1115
  this.$emit('volumechange', value)
887
1116
  },
1117
+ volumeAdjust(value) {
1118
+ const newVolume = this.state.volume + value
1119
+ this.volumeChange(newVolume)
1120
+ },
888
1121
  onDurationChange() {
889
1122
  // console.log('onDurationChange');
890
1123
  // console.log(e);
891
1124
  },
892
1125
  setTime(time) {
1126
+ // Scrubbing / manually setting the time should remove the replay button
1127
+ this.state.replay = false
893
1128
  this.player.currentTime = time
894
1129
  },
895
1130
  setCues(track) {
@@ -901,92 +1136,115 @@ export default {
901
1136
  // Required so the v-model will actually update.
902
1137
  this.captions.nonce = Math.random()
903
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
+ },
904
1196
  load(e = null) {
905
- // Reload the player to refresh all the sources / tracks
906
- this.player.load()
907
- this.$emit('load', e)
1197
+ if (this.player.load) {
1198
+ // Reload the player to refresh all the sources / tracks
1199
+ this.player.load()
1200
+ this.$emit('load', e)
1201
+ } else {
1202
+ console.error('Cannot load player')
1203
+ }
908
1204
  },
909
1205
  pause(e = null) {
910
- this.player.pause()
911
- this.options.paused = true
912
- this.$emit('pause', e)
1206
+ if (this.player.pause) {
1207
+ this.player.pause()
1208
+ this.state.paused = true
1209
+ this.$emit('pause', e)
1210
+ } else {
1211
+ console.log('Cannot pause player')
1212
+ }
913
1213
  },
914
1214
  play(e = null) {
915
- // Start playing the main video
916
- this.player.play()
917
- this.options.paused = false
918
- this.$emit('play', e)
1215
+ if (this.player.play) {
1216
+ // Start playing the main video
1217
+ this.player.play()
1218
+ this.state.paused = false
1219
+ this.state.replay = false
1220
+ this.$emit('play', e)
1221
+ } else {
1222
+ console.log('Cannot play player')
1223
+ }
919
1224
  },
920
- },
921
- beforeMount() {
922
- // Parse the controlslist string
923
- if (
924
- this.attributes.controlslist &&
925
- typeof this.attributes.controlslist === 'string' &&
926
- this.attributes.controlslist !== ''
927
- ) {
928
- this.options.controlslist = this.attributes.controlslist.split(' ')
929
- }
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
930
1233
 
931
- if (
932
- typeof this.attributes.playbackrates === 'undefined' ||
933
- this.attributes.playbackrates.length === 0
934
- ) {
935
- throw new Error(
936
- 'attributes.playbackrates must be defined and an array of numbers!'
937
- )
938
- }
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)
939
1239
 
940
- // Adjust the playback speed to 1 by default
941
- if (this.attributes.playbackrates.indexOf(1) !== -1) {
942
- this.options.playbackRateIndex =
943
- this.attributes.playbackrates.indexOf(1)
944
- } else {
945
- // 1 aka normal playback not enabled (What monster would do this?!)
946
- // Set the playback rate to "middle of the road" for whatever is available
947
- this.options.playbackRateIndex = Math.floor(
948
- this.attributes.playbackrates.length / 2
949
- )
950
- }
951
-
952
- // Initialize the ads aka pre/post/midroll
953
- if (this.src.ads && this.src.ads.length) {
954
- for (const ad of this.src.ads) {
955
- // Map to a percent so we can avoid dupe timings and have easier lookups
956
- this.ads[ad.play_at_percent] = ad
957
- this.ads[ad.play_at_percent].complete = false
1240
+ if (this.player.paused) {
1241
+ this.play(e)
1242
+ } else {
1243
+ this.pause(e)
1244
+ }
958
1245
  }
959
- }
960
-
961
- // Determine fullscreen settings
962
- if (
963
- this.attributes.playsinline ||
964
- !document.fullscreenEnabled ||
965
- this.options.controlslist.indexOf('nofullscreen') !== -1
966
- ) {
967
- this.fullscreenEnabled = false
968
- } else {
969
- this.fullscreenEnabled = true
970
- }
971
-
972
- // Determine remote playback settings
973
- if (
974
- this.attributes.disableremoteplayback ||
975
- this.options.controlslist.indexOf('noremoteplayback') !== -1
976
- ) {
977
- this.options.remoteplayback = false
978
- } else {
979
- this.options.remoteplayback = true
980
- }
981
-
982
- // Determine download settings
983
- if (this.options.controlslist.indexOf('nodownload') !== -1) {
984
- this.options.download = false
985
- } else {
986
- this.options.download = true
987
- }
1246
+ },
988
1247
  },
989
- mounted() {},
990
1248
  }
991
1249
  </script>
992
1250
 
@@ -996,29 +1254,11 @@ export default {
996
1254
  position: relative;
997
1255
  top: -50px;
998
1256
  margin-bottom: -40px;
999
- overflow: hidden;
1000
1257
  }
1001
1258
  .controls {
1002
1259
  height: 40px;
1003
1260
  background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.7));
1004
1261
  }
1005
- .volume-slider {
1006
- position: relative;
1007
- right: -50px;
1008
- top: -180px;
1009
- height: 180px;
1010
- width: 50px;
1011
- margin-left: -50px;
1012
- padding-bottom: 10px;
1013
- }
1014
- .slider-active-area {
1015
- width: 50px;
1016
- height: 200px;
1017
- margin-right: -50px;
1018
- margin-bottom: -200px;
1019
- position: relative;
1020
- top: -160px; /* height of this - controls height */
1021
- }
1022
1262
  .player-audio {
1023
1263
  height: 40px;
1024
1264
  }
@@ -1036,12 +1276,18 @@ export default {
1036
1276
  color: #fff;
1037
1277
  left: 25%;
1038
1278
  width: 50%;
1039
- top: 100px;
1279
+ top: 35%;
1040
1280
  height: 0;
1041
1281
  text-align: center;
1042
1282
  }
1043
- .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;
1044
1289
  background: rgba(0, 0, 0, 0.25);
1045
1290
  border-radius: 100%;
1291
+ padding: 1rem;
1046
1292
  }
1047
1293
  </style>