UNPKG

39.8 kBJavaScriptView Raw
1import Vue from 'vue'
2
3import {
4 AppFullscreen,
5 QSlider,
6 QBtn,
7 QTooltip,
8 QMenu,
9 QExpansionItem,
10 QList,
11 QItem,
12 QItemSection,
13 QIcon,
14 QSpinner,
15 CloseMenu,
16 Ripple
17} from 'quasar'
18
19import slot from 'quasar/src/utils/slot.js'
20
21const getMousePosition = function (e, type = 'x') {
22 if (type === 'x') {
23 return e.pageX
24 }
25 return e.pageY
26}
27const padTime = (val) => {
28 val = Math.floor(val)
29 if (val < 10) {
30 return '0' + val
31 }
32 return val + ''
33}
34const timeParse = (sec) => {
35 let min = 0
36 min = Math.floor(sec / 60)
37 sec = sec - min * 60
38 return padTime(min) + ':' + padTime(sec)
39}
40
41export default Vue.extend({
42 name: 'QMediaPlayer',
43
44 directives: {
45 CloseMenu,
46 Ripple
47 },
48
49 props: {
50 type: {
51 type: String,
52 required: false,
53 default: 'video',
54 validator: v => ['video', 'audio'].includes(v)
55 },
56 mobileMode: Boolean,
57 sources: {
58 type: Array,
59 default: () => []
60 },
61 poster: {
62 type: String,
63 default: ''
64 },
65 tracks: {
66 type: Array,
67 default: () => []
68 },
69 dense: Boolean,
70 autoplay: Boolean,
71 crossOrigin: {
72 type: [String, null],
73 default: null,
74 validator: v => [null, 'anonymous', 'use-credentials'].includes(v)
75 },
76 volume: {
77 type: Number,
78 default: 60,
79 validator: v => v >= 0 && v <= 100
80 },
81 preload: {
82 type: String,
83 default: 'metadata',
84 validator: v => ['none', 'metadata', 'auto'].includes(v)
85 },
86 muted: Boolean,
87 trackLanguage: {
88 type: String,
89 default: 'off' // value for 'Off'
90 },
91 showTooltips: Boolean,
92 showBigPlayButton: {
93 type: Boolean,
94 default: true
95 },
96 showSpinner: {
97 type: Boolean,
98 default: true
99 },
100 noControls: Boolean,
101 controlsDisplayTime: {
102 type: Number,
103 default: 2000
104 },
105 playbackRates: Array,
106 // initial playback rate
107 playbackRate: {
108 type: Number,
109 default: 1
110 },
111 color: {
112 type: String,
113 default: 'white'
114 },
115 backgroundColor: {
116 type: String,
117 default: 'black'
118 },
119 dark: Boolean,
120 radius: {
121 type: [Number, String],
122 default: 0
123 }
124 },
125
126 data () {
127 return {
128 lang: {
129 mediaPlayer: {}
130 },
131 iconSet: {
132 mediaPlayer: {}
133 },
134 $media: null, // the actual video/audio player
135 timer: {
136 // timer used to hide control during mouse inactivity
137 hideControlsTimer: null
138 },
139 state: {
140 errorText: null,
141 controls: false,
142 showControls: true,
143 volume: 60,
144 muted: false,
145 currentTime: 0.01,
146 duration: 1,
147 durationTime: '00:00',
148 remainingTime: '00:00',
149 displayTime: '00:00',
150 inFullscreen: false,
151 loading: true,
152 playReady: false,
153 playing: false,
154 playbackRates: [
155 { label: '.5x', value: 0.5 },
156 { label: 'Normal', value: 1 },
157 { label: '1.5x', value: 1.5 },
158 { label: '2x', value: 2 }
159 ],
160 playbackRate: 1,
161 trackLanguage: 'Off',
162 showBigPlayButton: true,
163 metadataLoaded: false
164 },
165 allEvents: [
166 'abort',
167 'canplay',
168 'canplaythrough',
169 'durationchange',
170 'emptied',
171 'ended',
172 'error',
173 'interruptbegin',
174 'interruptend',
175 'loadeddata',
176 'loadedmetadata',
177 'loadedstart',
178 'pause',
179 'play',
180 'playing',
181 'progress',
182 'ratechange',
183 'seeked',
184 'timeupdate',
185 'volumechange',
186 'waiting'
187 ],
188 settingsMenuVisible: false
189 }
190 },
191
192 beforeMount () {
193 this.__setupLang()
194 this.__setupIcons()
195
196 // if playbackRates has not been passed in
197 if (!this.playbackRates) {
198 this.state.playbackRates.splice(0, this.state.playbackRates.length)
199 this.state.playbackRates.push({ label: this.lang.mediaPlayer.ratePoint5, value: 0.5 })
200 this.state.playbackRates.push({ label: this.lang.mediaPlayer.rateNormal, value: 1 })
201 this.state.playbackRates.push({ label: this.lang.mediaPlayer.rate1Point5, value: 1.5 })
202 this.state.playbackRates.push({ label: this.lang.mediaPlayer.rate2, value: 2 })
203 }
204
205 this.state.trackLanguage = this.lang.mediaPlayer.trackLanguageOff
206
207 document.body.addEventListener('mousemove', this.__mouseMoveAction, false)
208 },
209
210 mounted () {
211 this.__init()
212 },
213
214 beforeDestroy () {
215 this.exitFullscreen()
216
217 // TODO: clear all timers
218
219 document.body.removeEventListener('mousemove', this.__mouseMoveAction)
220
221 this.__removeSourceEventListeners()
222 this.__removeMediaEventListeners()
223
224 // make sure no memory leaks
225 this.__removeTracks()
226 this.__removeSources()
227 this.$media = null
228 },
229
230 computed: {
231 classes () {
232 return {
233 'q-media--fullscreen': this.state.inFullscreen
234 }
235 },
236 videoControlsClasses () {
237 return {
238 'q-media__controls--dense': (this.state.showControls || this.mobileMode) && this.dense,
239 'q-media__controls--standard': (this.state.showControls || this.mobileMode) && !this.dense,
240 'q-media__controls--hidden': !this.state.showControls
241 }
242 },
243 audioControlsClasses () {
244 return {
245 'q-media__controls--dense': this.dense,
246 'q-media__controls--standard': !this.dense
247 }
248 },
249 videoWidth () {
250 if (this.$el) {
251 return this.$el.getBoundingClientRect().width
252 }
253 return 0
254 },
255 volumeIcon () {
256 if (this.state.volume > 1 && this.state.volume < 70 && !this.state.muted) {
257 return this.iconSet.mediaPlayer.volumeDown
258 } else if (this.state.volume >= 70 && !this.state.muted) {
259 return this.iconSet.mediaPlayer.volumeUp
260 } else {
261 return this.iconSet.mediaPlayer.volumeOff
262 }
263 },
264 selectTracksLanguageList () {
265 let tracksList = []
266 // provide option to turn subtitles/captions/chapters off
267 let track = {}
268 track.label = this.lang.mediaPlayer.trackLanguageOff
269 track.value = 'off'
270 tracksList.push(track)
271 for (let index = 0; index < this.tracks.length; ++index) {
272 let track = {}
273 track.label = track.value = this.tracks[index].label
274 tracksList.push(track)
275 }
276 return tracksList
277 },
278 isAudio () {
279 return this.type === 'audio'
280 },
281 isVideo () {
282 return this.type === 'video'
283 },
284 settingsPlaybackCaption () {
285 let caption = ''
286 this.state.playbackRates.forEach((rate) => {
287 if (rate.value === this.state.playbackRate) {
288 caption = rate.label
289 return
290 }
291 })
292 return caption
293 }
294 },
295
296 watch: {
297 poster () {
298 this.__updatePoster()
299 },
300 sources: {
301 handler () {
302 this.__updateSources()
303 },
304 deep: true
305 },
306 tracks: {
307 handler () {
308 this.__updateTracks()
309 },
310 deep: true
311 },
312 volume () {
313 this.__updateVolume()
314 },
315 muted () {
316 this.__updateMuted()
317 },
318 trackLanguage () {
319 this.__updateTrackLanguage()
320 },
321 showBigPlayButton () {
322 this.__updateBigPlayButton()
323 },
324 playbackRates () {
325 this.__updatePlaybackRates()
326 },
327 playbackRate () {
328 this.__updatePlaybackRate()
329 },
330 $route (val) {
331 this.exitFullscreen()
332 },
333 '$q.lang.isoName' (val) {
334 this.__setupLang()
335 },
336 '$q.iconSet.name' (val) {
337 this.__setupIcons()
338 },
339 'AppFullscreen.isActive' (val) {
340 // user pressed F11 to exit fullscreen
341 if (!val && this.isVideo && this.state.inFullscreen) {
342 this.exitFullscreen()
343 }
344 },
345 'state.playbackRate' (val) {
346 if (val && this.$media) {
347 this.$media.playbackRate = parseFloat(val)
348 this.$emit('playbackRate', val)
349 }
350 },
351 'state.trackLanguage' (val) {
352 this.__toggleCaptions()
353 this.$emit('trackLanguage', val)
354 },
355 'state.showControls' (val) {
356 if (this.isVideo && !this.noControls) {
357 this.$emit('showControls', val)
358 }
359 },
360 'state.volume' (val) {
361 if (this.$media) {
362 let volume = parseFloat(val / 100.0)
363 if (this.$media.volume !== volume) {
364 this.$media.volume = volume
365 this.$emit('volume', val)
366 }
367 }
368 },
369 'state.muted' (val) {
370 this.$emit('muted', val)
371 },
372 'state.currentTime' (val) {
373 if (this.$media && this.state.playReady) {
374 this.state.remainingTime = timeParse(this.$media.duration - this.$media.currentTime)
375 this.state.displayTime = timeParse(this.$media.currentTime)
376 }
377 }
378 },
379
380 methods: {
381 showControls () {
382 if (this.timer.hideControlsTimer) {
383 clearTimeout(this.timer.hideControlsTimer)
384 this.timer.hideControlsTimer = null
385 }
386 if (this.noControls) {
387 return
388 }
389 this.state.showControls = true
390 this.__checkCursor()
391 if (this.controlsDisplayTime !== -1 && !this.mobileMode && this.isVideo) {
392 this.timer.hideControlsTimer = setTimeout(() => {
393 if (!this.__showingMenu()) {
394 this.state.showControls = false
395 this.timer.hideControlsTimer = null
396 this.__checkCursor()
397 } else {
398 this.showControls()
399 }
400 }, this.controlsDisplayTime)
401 }
402 },
403 hideControls () {
404 if (this.timer.hideControlsTimer) {
405 clearTimeout(this.timer.hideControlsTimer)
406 }
407 if (this.controlsDisplayTime !== -1) {
408 this.state.showControls = false
409 this.__checkCursor()
410 }
411 this.timer.hideControlsTimer = null
412 },
413 toggleControls () {
414 if (this.state.showControls) {
415 this.hideControls()
416 } else {
417 this.showControls()
418 }
419 },
420 __reset () {
421 if (this.timer.hideControlsTimer) {
422 clearTimeout(this.timer.hideControlsTimer)
423 }
424 this.timer.hideControlsTimer = null
425 this.state.errorText = null
426 this.state.currentTime = 0.01
427 this.state.durationTime = '00:00'
428 this.state.remainingTime = '00:00'
429 this.state.displayTime = '00:00'
430 this.state.duration = 1
431 this.state.playReady = false
432 this.state.playing = false
433 this.state.loading = true
434 this.state.metadataLoaded = false
435 this.__updateTrackLanguage()
436 this.showControls()
437 },
438 __toggleCaptions () {
439 this.__showCaptions(this.state.trackLanguage)
440 },
441 __showCaptions (lang) {
442 if (this.$media && this.isVideo) {
443 for (let index = 0; index < this.$media.textTracks.length; ++index) {
444 if (this.$media.textTracks[index].label === lang) {
445 this.$media.textTracks[index].mode = 'showing'
446 this.$media.textTracks[index].oncuechange = this.__cueChanged
447 } else {
448 this.$media.textTracks[index].mode = 'hidden'
449 this.$media.textTracks[index].oncuechange = null
450 }
451 }
452 }
453 },
454 togglePlay () {
455 if (this.state.playReady) {
456 this.state.playing = !this.state.playing
457 if (this.state.playing) {
458 this.state.showBigPlayButton = false
459 this.$media.play()
460 this.__mouseLeaveVideo()
461 } else {
462 this.$media.pause()
463 this.state.showBigPlayButton = true
464 }
465 }
466 },
467 toggleMuted () {
468 this.state.muted = !this.state.muted
469 if (this.$media) {
470 this.$media.muted = this.state.muted
471 }
472 },
473 toggleFullscreen () {
474 if (this.isVideo) {
475 if (this.state.inFullscreen) {
476 this.exitFullscreen()
477 } else {
478 this.setFullscreen()
479 }
480 this.$emit('fullscreen', this.state.inFullscreen)
481 }
482 },
483 setFullscreen () {
484 if (!this.isVideo || this.state.inFullscreen) {
485 return
486 }
487 this.state.inFullscreen = true
488 AppFullscreen.request(this.$el)
489 document.body.classList.add('q-body--fullscreen-mixin')
490 },
491 exitFullscreen () {
492 if (!this.isVideo || !this.state.inFullscreen) {
493 return
494 }
495 this.state.inFullscreen = false
496 AppFullscreen.exit()
497 document.body.classList.remove('q-body--fullscreen-mixin')
498 },
499 setCurrentTime (seconds) {
500 if (this.state.playReady) {
501 if (seconds >= 0 && seconds <= this.$media.duration) {
502 this.state.currentTime = Math.floor(seconds)
503 this.$media.currentTime = Math.floor(seconds)
504 }
505 }
506 },
507 setVolume (volume) {
508 if (volume >= 100 && volume <= 100) {
509 this.state.volume = volume
510 }
511 },
512 __setupLang () {
513 let isoName = this.$q.lang.isoName || 'en-us'
514 let lang
515 try {
516 lang = require(`./lang/${isoName}`)
517 } catch (e) {}
518 lang && (this.lang['mediaPlayer'] = { ...lang.default.mediaPlayer })
519 },
520
521 __setupIcons () {
522 let iconName = this.$q.iconSet.name || 'material-icons'
523 let iconSet
524 try {
525 iconSet = require(`./iconSet/${iconName}`)
526 } catch (e) {}
527 iconSet && (this.iconSet['mediaPlayer'] = { ...iconSet.default.mediaPlayer })
528 },
529
530 __init () {
531 this.$media = this.$refs['media']
532 // set default track language
533 this.__updateTrackLanguage()
534 this.__updateSources()
535 this.__updateTracks()
536 // set big play button
537 this.__updateBigPlayButton()
538 // set the volume
539 this.__updateVolume()
540 // set muted
541 this.__updateMuted()
542 // set playback rates
543 this.__updatePlaybackRates()
544 // set playback rate default
545 this.__updatePlaybackRate()
546 // does user want cors?
547 if (this.crossOrigin) {
548 this.$media.setAttribute('crossorigin', this.crossOrigin)
549 }
550 // make sure "controls" is turned off
551 this.$media.controls = false
552 // set up event listeners on video
553 this.__addMediaEventListeners()
554 this.__addSourceEventListeners()
555 this.__toggleCaptions()
556 // send player to parent, if they are interested
557 },
558
559 __addMediaEventListeners () {
560 if (this.$media) {
561 this.allEvents.forEach((event) => {
562 this.$media.addEventListener(event, this.__mediaEventHandler)
563 })
564 }
565 },
566 __removeMediaEventListeners () {
567 if (this.$media) {
568 this.allEvents.forEach((event) => {
569 this.$media.removeEventListener(event, this.__mediaEventHandler)
570 })
571 }
572 },
573 __addSourceEventListeners () {
574 if (this.$media) {
575 let sources = this.$media.querySelectorAll('source')
576 for (let index = 0; index < sources.length; ++index) {
577 sources[index].addEventListener('error', this.__sourceEventHandler)
578 }
579 }
580 },
581 __removeSourceEventListeners () {
582 if (this.$media) {
583 let sources = this.$media.querySelectorAll('source')
584 for (let index = 0; index < sources.length; ++index) {
585 sources[index].removeEventListener('error', this.__sourceEventHandler)
586 }
587 }
588 },
589 __sourceEventHandler (event) {
590 const NETWORK_NO_SOURCE = 3
591 if (this.$media && this.$media.networkState === NETWORK_NO_SOURCE) {
592 this.state.errorText = this.isVideo ? this.lang.mediaPlayer.noLoadVideo : this.lang.mediaPlayer.noLoadAudio
593 }
594 },
595 __mediaEventHandler (event) {
596 if (event.type === 'abort') {
597
598 } else if (event.type === 'canplay') {
599 this.state.playReady = true
600 this.__mouseEnterVideo()
601 this.$emit('ready')
602 // autoplay if set (we manage this)
603 if (this.autoplay) {
604 this.togglePlay()
605 }
606 } else if (event.type === 'canplaythrough') {
607 // console.log('canplaythrough')
608 } else if (event.type === 'durationchange') {
609 this.state.duration = Math.floor(this.$media.duration)
610 this.state.durationTime = timeParse(this.$media.duration)
611 this.$emit('duration', this.$media.duration)
612 } else if (event.type === 'emptied') {
613 } else if (event.type === 'ended') {
614 this.state.playing = false
615 this.$emit('ended')
616 } else if (event.type === 'error') {
617 let error = this.$media.error
618 this.$emit('error', error)
619 } else if (event.type === 'interruptbegin') {
620 console.log('interruptbegin')
621 } else if (event.type === 'interruptend') {
622 console.log('interruptend')
623 } else if (event.type === 'loadeddata') {
624 this.state.loading = false
625 this.$emit('loaded')
626 } else if (event.type === 'loadedmetadata') {
627 // tracks can only be programatically added after 'loadedmetadata' event
628 this.state.metadataLoaded = true
629 this.__updateTracks()
630 // set default track language
631 this.__updateTrackLanguage()
632 this.__toggleCaptions()
633 } else if (event.type === 'loadedstart') {
634 } else if (event.type === 'pause') {
635 this.state.playing = false
636 this.$emit('paused')
637 } else if (event.type === 'play') {
638 // console.log('play')
639 } else if (event.type === 'playing') {
640 this.state.playing = true
641 this.$emit('playing')
642 } else if (event.type === 'progress') {
643 } else if (event.type === 'ratechange') {
644 } else if (event.type === 'seeked') {
645 } else if (event.type === 'timeupdate') {
646 this.state.currentTime = Math.floor(this.$media.currentTime)
647 this.$emit('timeupdate', this.$media.currentTime, this.state.remainingTime)
648 } else if (event.type === 'volumechange') {
649 } else if (event.type === 'waiting') {
650 this.$emit('waiting')
651 }
652 },
653
654 // for future functionality
655 __cueChanged (data) {
656 },
657
658 __checkCursor () {
659 if (this.state.inFullscreen && this.state.playing && !this.state.showControls) {
660 this.$el.classList.remove('cursor-inherit')
661 this.$el.classList.add('cursor-none')
662 } else {
663 this.$el.classList.remove('cursor-none')
664 this.$el.classList.add('cursor-inherit')
665 }
666 },
667
668 __adjustMenu () {
669 const qmenu = this.$refs['menu']
670 if (qmenu) {
671 setTimeout(() => {
672 qmenu.updatePosition()
673 }, 350)
674 }
675 },
676
677 __videoClick () {
678 if (this.mobileMode) {
679 this.toggleControls()
680 } else {
681 this.togglePlay()
682 }
683 },
684 __bigButtonClick () {
685 if (this.mobileMode) {
686 this.hideControls()
687 }
688 this.togglePlay()
689 },
690 __settingsMenuShowing (val) {
691 this.settingsMenuVisible = val
692 },
693 __mouseEnterVideo (e) {
694 if (!this.mobileMode && !this.isAudio) {
695 this.showControls()
696 }
697 },
698 __mouseLeaveVideo (e) {
699 if (!this.mobileMode && !this.isAudio) {
700 this.hideControls()
701 }
702 },
703 __mouseMoveAction (e) {
704 if (!this.mobileMode && !this.isAudio) {
705 this.__showControlsIfValid(e)
706 }
707 },
708 __showControlsIfValid (e) {
709 if (this.__showingMenu()) {
710 this.showControls()
711 return true
712 }
713 const x = getMousePosition(e, 'x')
714 const y = getMousePosition(e, 'y')
715 const pos = this.$el.getBoundingClientRect()
716 if (!pos) return false
717 if (x > pos.left && x < pos.left + pos.width) {
718 if (y > pos.top + pos.height - (this.dense ? 40 : 80) && y < pos.top + pos.height) {
719 this.showControls()
720 return true
721 }
722 }
723 return false
724 },
725 __videoCurrentTimeChanged (val) {
726 this.showControls()
727 if (this.$media && this.$media.duration && val && val > 0 && val <= this.state.duration) {
728 if (this.$media.currentTime !== val) {
729 this.state.currentTime = val
730 this.$media.currentTime = val
731 }
732 }
733 },
734 __volumePercentChanged (val) {
735 this.showControls()
736 this.state.volume = val
737 },
738 __trackLanguageChanged (language) {
739 if (this.state.trackLanguage !== language) {
740 this.state.trackLanguage = language
741 }
742 },
743 __playbackRateChanged (rate) {
744 if (this.state.playbackRate !== rate) {
745 this.state.playbackRate = rate
746 }
747 },
748 __showingMenu () {
749 return this.settingsMenuVisible
750 },
751 __updateBigPlayButton () {
752 if (this.state.showBigPlayButton !== this.showBigPlayButton) {
753 this.state.showBigPlayButton = this.showBigPlayButton
754 }
755 },
756 __updateVolume () {
757 if (this.state.volume !== this.volume) {
758 this.state.volume = this.volume
759 }
760 },
761 __updateMuted () {
762 if (this.state.muted !== this.muted) {
763 this.state.muted = this.muted
764 if (this.$media) {
765 this.$media.muted = this.state.muted
766 }
767 }
768 },
769 __updateTrackLanguage () {
770 if (this.state.trackLanguage !== this.trackLanguage || this.lang.mediaPlayer.trackLanguageOff) {
771 this.state.trackLanguage = this.trackLanguage || this.lang.mediaPlayer.trackLanguageOff
772 }
773 },
774 __updatePlaybackRates () {
775 if (this.playbackRates && this.playbackRates.length > 0) {
776 this.state.playbackRates = [...this.playbackRates]
777 }
778 },
779 __updatePlaybackRate () {
780 if (this.state.playbackRate !== this.playbackRate) {
781 this.state.playbackRate = this.playbackRate
782 }
783 },
784
785 __updateSources () {
786 this.__removeSources()
787 this.__addSources()
788 },
789
790 __removeSources () {
791 if (this.$media) {
792 this.__removeSourceEventListeners()
793 // player must not be running
794 this.$media.pause()
795 this.$media.src = ''
796 this.$media.currentTime = 0
797 let childNodes = this.$media.childNodes
798 for (let index = childNodes.length - 1; index >= 0; --index) {
799 if (childNodes[index].tagName === 'SOURCE') {
800 this.$media.removeChild(childNodes[index])
801 }
802 }
803 this.$media.load()
804 }
805 },
806
807 __addSources () {
808 if (this.$media) {
809 let loaded = false
810 this.sources.forEach((source) => {
811 let s = document.createElement('SOURCE')
812 s.src = source.src ? source.src : ''
813 s.type = source.type ? source.type : ''
814 this.$media.appendChild(s)
815 if (!loaded && source.src) {
816 this.$media.src = source.src
817 loaded = true
818 }
819 })
820 this.__reset()
821 this.__addSourceEventListeners()
822 this.$media.load()
823 }
824 },
825
826 __updateTracks () {
827 this.__removeTracks()
828 this.__addTracks()
829 },
830
831 __removeTracks () {
832 if (this.$media) {
833 let childNodes = this.$media.childNodes
834 for (let index = childNodes.length - 1; index >= 0; --index) {
835 if (childNodes[index].tagName === 'TRACK') {
836 this.$media.removeChild(childNodes[index])
837 }
838 }
839 }
840 },
841
842 __addTracks () {
843 // only add tracks to video
844 if (this.isVideo && this.$media) {
845 this.tracks.forEach((track) => {
846 let t = document.createElement('TRACK')
847 t.kind = track.kind ? track.kind : ''
848 t.label = track.label ? track.label : ''
849 t.src = track.src ? track.src : ''
850 t.srclang = track.srclang ? track.srclang : ''
851 this.$media.appendChild(t)
852 })
853 this.$nextTick(() => {
854 this.__toggleCaptions()
855 })
856 }
857 },
858
859 __updatePoster () {
860 if (this.$media) {
861 this.$media.poster = this.poster
862 }
863 },
864
865 __renderVideo (h) {
866 return h('video', {
867 ref: 'media',
868 staticClass: 'q-media--player',
869 attrs: {
870 poster: this.poster,
871 preload: this.preload
872 }
873 }, [
874 // this.sources.length && this.__renderSources(h),
875 // this.tracks.length && this.__renderTracks(h),
876 this.isVideo && h('p', this.lang.mediaPlayer.oldBrowserVideo, slot(this, 'oldbrowser'))
877 ])
878 },
879 __renderAudio (h) {
880 // This is on purpose (not using audio tag).
881 // The video tag can also play audio and works if dynamically
882 // switching between video and audo on the same component.
883 return h('video', {
884 ref: 'media',
885 staticClass: 'q-media--player',
886 attrs: {
887 preload: this.preload
888 }
889 }, [
890 // this.sources.length && this.__renderSources(h),
891 this.isAudio && h('p', this.lang.mediaPlayer.oldBrowserAudio, slot(this, 'oldbrowser'))
892 ])
893 },
894 __renderSources (h) {
895 return this.sources.map((source) => {
896 return h('source', {
897 attrs: {
898 key: source.src + ':' + source.type,
899 src: source.src,
900 type: source.type
901 }
902 })
903 })
904 },
905 __renderTracks (h) {
906 return this.tracks.map((track) => {
907 return h('track', {
908 attrs: {
909 key: track.src + ':' + track.kind,
910 src: track.src,
911 kind: track.kind,
912 label: track.label,
913 srclang: track.srclang
914 }
915 })
916 })
917 },
918 __renderOverlayWindow (h) {
919 return h('div', {
920 staticClass: 'q-media__overlay-window',
921 on: {
922 click: this.__videoClick
923 }
924 }, [
925 h('div', slot(this, 'overlay'))
926 ])
927 },
928 __renderErrorWindow (h) {
929 return h('div', {
930 staticClass: 'q-media__error-window'
931 }, [
932 h('div', this.state.errorText)
933 ], slot(this, 'errorWindow'))
934 },
935 __renderPlayButton (h) {
936 return h(QBtn, {
937 staticClass: 'q-media__controls--button',
938 props: {
939 icon: this.state.playing ? this.iconSet.mediaPlayer.pause : this.iconSet.mediaPlayer.play,
940 textColor: this.color,
941 size: '1rem',
942 disable: !this.state.playReady,
943 flat: true
944 },
945 on: {
946 click: this.togglePlay
947 }
948 }, [
949 this.showTooltips && this.state.playing && h(QTooltip, this.lang.mediaPlayer.pause),
950 this.showTooltips && !this.state.playing && this.state.playReady && h(QTooltip, this.lang.mediaPlayer.play)
951 ], slot(this, 'play'))
952 },
953 __renderVideoControls (h) {
954 return h('div', {
955 ref: 'controls',
956 staticClass: 'q-media__controls',
957 class: this.videoControlsClasses
958 }, [
959 // dense
960 this.dense && h('div', {
961 staticClass: 'q-media__controls--row row col content-start items-center'
962 }, [
963 h('div', [
964 this.__renderPlayButton(h),
965 this.showTooltips && !this.state.playReady && h(QTooltip, this.lang.mediaPlayer.waitingVideo)
966 ]),
967 this.__renderVolumeButton(h),
968 this.__renderVolumeSlider(h),
969 this.__renderDisplayTime(h),
970 this.__renderCurrentTimeSlider(h),
971 this.__renderDurationTime(h),
972 this.__renderSettingsButton(h),
973 this.__renderFullscreenButton(h)
974 ]),
975 // sparse
976 !this.dense && h('div', {
977 staticClass: 'q-media__controls--row row col items-center justify-between'
978 }, [
979 this.__renderDisplayTime(h),
980 this.__renderCurrentTimeSlider(h),
981 this.__renderDurationTime(h)
982 ]),
983 !this.dense && h('div', {
984 staticClass: 'q-media__controls--row row col content-start items-center'
985 }, [
986 h('div', {
987 staticClass: 'row col'
988 }, [
989 h('div', [
990 this.__renderPlayButton(h),
991 this.showTooltips && !this.state.playReady && h(QTooltip, this.lang.mediaPlayer.waitingVideo)
992 ]),
993 this.__renderVolumeButton(h),
994 this.__renderVolumeSlider(h)
995 ]),
996 h('div', [
997 this.__renderSettingsButton(h),
998 this.__renderFullscreenButton(h)
999 ])
1000 ])
1001 ], slot(this, 'controls'))
1002 },
1003 __renderAudioControls (h) {
1004 return h('div', {
1005 ref: 'controls',
1006 staticClass: 'q-media__controls',
1007 class: this.audioControlsClasses
1008 }, [
1009 this.dense && h('div', {
1010 staticClass: 'q-media__controls--row row col content-start items-center'
1011 }, [
1012 // dense
1013 h('div', [
1014 this.__renderPlayButton(h),
1015 this.showTooltips && !this.state.playReady && h(QTooltip, this.lang.mediaPlayer.waitingAudio)
1016 ]),
1017 this.__renderVolumeButton(h),
1018 this.__renderVolumeSlider(h),
1019 this.__renderDisplayTime(h),
1020 this.__renderCurrentTimeSlider(h),
1021 this.__renderDurationTime(h)
1022 ]),
1023 // sparse
1024 !this.dense && h('div', {
1025 staticClass: 'q-media__controls--row row col items-center justify-between'
1026 }, [
1027 this.__renderDisplayTime(h),
1028 this.__renderCurrentTimeSlider(h),
1029 this.__renderDurationTime(h)
1030 ]),
1031 !this.dense && h('div', {
1032 staticClass: 'q-media__controls--row row col content-start items-center'
1033 }, [
1034 h('div', [
1035 this.__renderPlayButton(h),
1036 this.showTooltips && !this.state.playReady && h(QTooltip, this.lang.mediaPlayer.waitingAudio)
1037 ]),
1038 this.__renderVolumeButton(h),
1039 this.__renderVolumeSlider(h)
1040 ])
1041 ], slot(this, 'controls'))
1042 },
1043 __renderVolumeButton (h) {
1044 return h(QBtn, {
1045 staticClass: 'q-media__controls--button',
1046 props: {
1047 icon: this.volumeIcon,
1048 textColor: this.color,
1049 size: '1rem',
1050 disable: !this.state.playReady,
1051 flat: true
1052 },
1053 on: {
1054 click: this.toggleMuted
1055 }
1056 }, [
1057 this.showTooltips && !this.state.muted && h(QTooltip, this.lang.mediaPlayer.mute),
1058 this.showTooltips && this.state.muted && h(QTooltip, this.lang.mediaPlayer.unmute)
1059 ], slot(this, 'volume'))
1060 },
1061 __renderVolumeSlider (h) {
1062 return h(QSlider, {
1063 staticClass: 'col',
1064 style: {
1065 width: '20%',
1066 margin: '0 0.5rem',
1067 minWidth: this.dense ? '20px' : '50px',
1068 maxWidth: this.dense ? '50px' : '200px'
1069 },
1070 props: {
1071 value: this.state.volume,
1072 color: this.color,
1073 dark: this.dark,
1074 min: 0,
1075 max: 100,
1076 disable: !this.state.playReady || this.state.muted
1077 },
1078 on: {
1079 input: this.__volumePercentChanged
1080 }
1081 }, slot(this, 'volumeSlider'))
1082 },
1083 __renderSettingsButton (h) {
1084 return h(QBtn, {
1085 staticClass: 'q-media__controls--button',
1086 props: {
1087 icon: this.iconSet.mediaPlayer.settings,
1088 textColor: this.color,
1089 size: '1rem',
1090 disable: !this.state.playReady,
1091 flat: true
1092 }
1093 }, [
1094 this.showTooltips && !this.settingsMenuVisible && h(QTooltip, this.lang.mediaPlayer.settings),
1095 this.__renderSettingsMenu(h)
1096 ], slot(this, 'settings'))
1097 },
1098 __renderFullscreenButton (h) {
1099 return h(QBtn, {
1100 staticClass: 'q-media__controls--button',
1101 props: {
1102 icon: this.state.inFullscreen ? this.iconSet.mediaPlayer.fullscreenExit : this.iconSet.mediaPlayer.fullscreen,
1103 textColor: this.color,
1104 size: '1rem',
1105 disable: !this.state.playReady,
1106 flat: true
1107 },
1108 on: {
1109 click: this.toggleFullscreen
1110 }
1111 }, [
1112 this.showTooltips && h(QTooltip, this.lang.mediaPlayer.toggleFullscreen)
1113 ], slot(this, 'fullscreen'))
1114 },
1115 __renderLoader (h) {
1116 return h('div', {
1117 staticClass: this.isVideo ? 'q-media__loading--video' : 'q-media__loading--audio'
1118 }, [
1119 h(QSpinner, {
1120 props: {
1121 size: this.isVideo ? '3rem' : '1rem'
1122 }
1123 })
1124 ], slot(this, 'spinner'))
1125 },
1126 __renderBigPlayButton (h) {
1127 return h('div', {
1128 staticClass: 'q-media--big-button'
1129 }, [
1130 h(QIcon, {
1131 props: {
1132 name: this.iconSet.mediaPlayer.bigPlayButton,
1133 size: '3rem'
1134 },
1135 on: {
1136 click: this.__bigButtonClick
1137 },
1138 directives: [
1139 {
1140 name: 'ripple',
1141 value: true
1142 }
1143 ]
1144 })
1145 ], slot(this, 'bigPlayButton'))
1146 },
1147 __renderCurrentTimeSlider (h) {
1148 return h(QSlider, {
1149 staticClass: 'col',
1150 style: {
1151 width: '100%',
1152 margin: '0 0.5rem'
1153 },
1154 props: {
1155 value: this.state.currentTime,
1156 color: this.color,
1157 dark: this.dark,
1158 min: 0.01,
1159 max: this.state.duration ? this.state.duration : 1,
1160 disable: !this.state.playReady
1161 },
1162 on: {
1163 input: this.__videoCurrentTimeChanged
1164 }
1165 }, slot(this, 'positionSlider'))
1166 },
1167 __renderDisplayTime (h) {
1168 return h('span', {
1169 staticClass: 'q-media__controls--video-time-text' + ' text-' + this.color
1170 }, this.state.displayTime, slot(this, 'displayTime'))
1171 // TODO: syntax on above line??
1172 },
1173 __renderDurationTime (h) {
1174 return h('span', {
1175 staticClass: 'q-media__controls--video-time-text' + ' text-' + this.color
1176 }, this.state.durationTime, slot(this, 'durationTime'))
1177 // TODO: syntax on above line??
1178 },
1179 __renderSettingsMenu (h) {
1180 return h(QMenu, {
1181 ref: 'menu',
1182 props: {
1183 anchor: 'top right',
1184 self: 'bottom right'
1185 },
1186 on: {
1187 show: () => {
1188 this.__settingsMenuShowing(true)
1189 },
1190 hide: () => {
1191 this.__settingsMenuShowing(false)
1192 }
1193 }
1194 }, [
1195 this.state.playbackRates.length && h(QExpansionItem, {
1196 props: {
1197 group: 'settings-menu',
1198 expandSeparator: true,
1199 icon: this.iconSet.mediaPlayer.speed,
1200 label: this.lang.mediaPlayer.speed,
1201 caption: this.settingsPlaybackCaption
1202 },
1203 on: {
1204 show: this.__adjustMenu,
1205 hide: this.__adjustMenu
1206 }
1207 }, [
1208 h(QList, {
1209 props: {
1210 highlight: true
1211 }
1212 }, [
1213 this.state.playbackRates.map((rate) => {
1214 return h(QItem, {
1215 attrs: {
1216 key: rate.value
1217 },
1218 props: {
1219 clickable: true
1220 },
1221 on: {
1222 click: () => {
1223 this.__playbackRateChanged(rate.value)
1224 }
1225 },
1226 directives: [
1227 {
1228 name: 'ripple',
1229 value: true
1230 },
1231 {
1232 name: 'close-menu',
1233 value: true
1234 }
1235 ]
1236 }, [
1237 h(QItemSection, {
1238 props: {
1239 avatar: true
1240 }
1241 }, [
1242 rate.value === this.state.playbackRate && h(QIcon, {
1243 props: {
1244 name: this.iconSet.mediaPlayer.selected
1245 }
1246 })
1247 ]),
1248 h(QItemSection, rate.label)
1249 ])
1250 })
1251 ])
1252 ]),
1253 // first item is 'Off' and doesn't count unless more are added
1254 this.selectTracksLanguageList.length > 1 && h(QExpansionItem, {
1255 props: {
1256 group: 'settings-menu',
1257 expandSeparator: true,
1258 icon: this.iconSet.mediaPlayer.language,
1259 label: this.lang.mediaPlayer.language,
1260 caption: this.state.trackLanguage
1261 },
1262 on: {
1263 show: this.__adjustMenu,
1264 hide: this.__adjustMenu
1265 }
1266 }, [
1267 h(QList, {
1268 props: {
1269 highlight: true
1270 }
1271 }, [
1272 this.selectTracksLanguageList.map((language) => {
1273 return h(QItem, {
1274 attrs: {
1275 key: language.value
1276 },
1277 props: {
1278 clickable: true
1279 },
1280 on: {
1281 click: (event) => {
1282 this.__trackLanguageChanged(language.value)
1283 }
1284 },
1285 directives: [
1286 {
1287 name: 'ripple',
1288 value: true
1289 },
1290 {
1291 name: 'close-menu',
1292 value: true
1293 }
1294 ]
1295 }, [
1296 h(QItemSection, {
1297 props: {
1298 avatar: true
1299 }
1300 }, [
1301 language.value === this.state.trackLanguage && h(QIcon, {
1302 props: {
1303 name: this.iconSet.mediaPlayer.selected
1304 }
1305 })
1306 ]),
1307 h(QItemSection, language.label)
1308 ])
1309 })
1310 ])
1311 ])
1312 ], slot(this, 'settingsMenu'))
1313 }
1314 },
1315
1316 render (h) {
1317 return h('div', {
1318 staticClass: 'q-media col-12 bg-' + this.backgroundColor,
1319 class: this.classes,
1320 style: {
1321 borderRadius: !this.state.inFullscreen ? this.radius : 0,
1322 height: this.isVideo ? 'auto' : this.dense ? '40px' : '80px'
1323 },
1324 on: {
1325 mousemove: this.__mouseEnterVideo,
1326 mouseenter: this.__mouseEnterVideo,
1327 mouseleave: this.__mouseLeaveVideo
1328 }
1329 }, [
1330 this.isVideo && this.__renderVideo(h),
1331 this.isAudio && this.__renderAudio(h),
1332 this.__renderOverlayWindow(h),
1333 this.state.errorText && this.__renderErrorWindow(h),
1334 this.isVideo && !this.noControls && !this.state.errorText && this.__renderVideoControls(h),
1335 this.isAudio && !this.noControls && !this.state.errorText && this.__renderAudioControls(h),
1336 this.showSpinner && this.state.loading && !this.state.playReady && !this.state.errorText && this.__renderLoader(h),
1337 this.isVideo && this.showBigPlayButton && this.state.playReady && !this.state.playing && this.__renderBigPlayButton(h)
1338 ])
1339 }
1340})