This commit is contained in:
parent
3862034dbc
commit
1f08475017
|
@ -0,0 +1 @@
|
||||||
|
Fixed broken audio playback on Chrome and invisible volume control (#390)
|
|
@ -0,0 +1 @@
|
||||||
|
Use Howler to manage audio instead of our own dirty/untested code (#392)
|
|
@ -21,6 +21,7 @@
|
||||||
"axios": "^0.17.1",
|
"axios": "^0.17.1",
|
||||||
"dateformat": "^2.0.0",
|
"dateformat": "^2.0.0",
|
||||||
"django-channels": "^1.1.6",
|
"django-channels": "^1.1.6",
|
||||||
|
"howler": "^2.0.14",
|
||||||
"js-logger": "^1.3.0",
|
"js-logger": "^1.3.0",
|
||||||
"jwt-decode": "^2.2.0",
|
"jwt-decode": "^2.2.0",
|
||||||
"lodash": "^4.17.4",
|
"lodash": "^4.17.4",
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="ui inverted segment player-wrapper" :style="style">
|
<div class="ui inverted segment player-wrapper" :style="style">
|
||||||
<div class="player">
|
<div class="player">
|
||||||
<keep-alive>
|
<audio-track
|
||||||
<audio-track
|
ref="currentAudio"
|
||||||
ref="currentAudio"
|
v-if="currentTrack"
|
||||||
v-if="renderAudio && currentTrack"
|
:is-current="true"
|
||||||
:is-current="true"
|
:start-time="$store.state.player.currentTime"
|
||||||
:start-time="$store.state.player.currentTime"
|
:autoplay="$store.state.player.playing"
|
||||||
:autoplay="$store.state.player.playing"
|
:key="audioKey"
|
||||||
:track="currentTrack">
|
:track="currentTrack">
|
||||||
</audio-track>
|
</audio-track>
|
||||||
</keep-alive>
|
|
||||||
<div v-if="currentTrack" class="track-area ui unstackable items">
|
<div v-if="currentTrack" class="track-area ui unstackable items">
|
||||||
<div class="ui inverted item">
|
<div class="ui inverted item">
|
||||||
<div class="ui tiny image">
|
<div class="ui tiny image">
|
||||||
|
@ -160,13 +159,13 @@
|
||||||
import {mapState, mapGetters, mapActions} from 'vuex'
|
import {mapState, mapGetters, mapActions} from 'vuex'
|
||||||
import GlobalEvents from '@/components/utils/global-events'
|
import GlobalEvents from '@/components/utils/global-events'
|
||||||
import ColorThief from '@/vendor/color-thief'
|
import ColorThief from '@/vendor/color-thief'
|
||||||
|
import {Howl} from 'howler'
|
||||||
|
|
||||||
import AudioTrack from '@/components/audio/Track'
|
import AudioTrack from '@/components/audio/Track'
|
||||||
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
|
||||||
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
|
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'player',
|
|
||||||
components: {
|
components: {
|
||||||
TrackFavoriteIcon,
|
TrackFavoriteIcon,
|
||||||
TrackPlaylistIcon,
|
TrackPlaylistIcon,
|
||||||
|
@ -177,16 +176,28 @@ export default {
|
||||||
let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]]
|
let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]]
|
||||||
return {
|
return {
|
||||||
isShuffling: false,
|
isShuffling: false,
|
||||||
renderAudio: true,
|
|
||||||
sliderVolume: this.volume,
|
sliderVolume: this.volume,
|
||||||
defaultAmbiantColors: defaultAmbiantColors,
|
defaultAmbiantColors: defaultAmbiantColors,
|
||||||
showVolume: false,
|
showVolume: false,
|
||||||
ambiantColors: defaultAmbiantColors
|
ambiantColors: defaultAmbiantColors,
|
||||||
|
audioKey: String(new Date()),
|
||||||
|
dummyAudio: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
// we trigger the watcher explicitely it does not work otherwise
|
// we trigger the watcher explicitely it does not work otherwise
|
||||||
this.sliderVolume = this.volume
|
this.sliderVolume = this.volume
|
||||||
|
// this is needed to unlock audio playing under some browsers,
|
||||||
|
// cf https://github.com/goldfire/howler.js#mobilechrome-playback
|
||||||
|
// but we never actually load those audio files
|
||||||
|
this.dummyAudio = new Howl({
|
||||||
|
preload: false,
|
||||||
|
autoplay: false,
|
||||||
|
src: ['noop.webm', 'noop.mp3']
|
||||||
|
})
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.dummyAudio.unload()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
|
@ -305,21 +316,13 @@ export default {
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentTrack (newValue) {
|
currentTrack (newValue) {
|
||||||
|
if (!this.isShuffling) {
|
||||||
|
this.audioKey = String(new Date())
|
||||||
|
}
|
||||||
if (!newValue || !newValue.album.cover) {
|
if (!newValue || !newValue.album.cover) {
|
||||||
this.ambiantColors = this.defaultAmbiantColors
|
this.ambiantColors = this.defaultAmbiantColors
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
currentIndex (newValue, oldValue) {
|
|
||||||
if (newValue !== oldValue) {
|
|
||||||
// why this? to ensure the audio tag is deleted and fully
|
|
||||||
// rerendered, so we don't have any issues with cached position
|
|
||||||
// or whatever
|
|
||||||
this.renderAudio = false
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.renderAudio = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
volume (newValue) {
|
volume (newValue) {
|
||||||
this.sliderVolume = newValue
|
this.sliderVolume = newValue
|
||||||
},
|
},
|
||||||
|
@ -385,9 +388,6 @@ export default {
|
||||||
.volume-control {
|
.volume-control {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 12.5% !important;
|
width: 12.5% !important;
|
||||||
.icon {
|
|
||||||
// margin: 0;
|
|
||||||
}
|
|
||||||
[type="range"] {
|
[type="range"] {
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -395,16 +395,11 @@ export default {
|
||||||
left: 25%;
|
left: 25%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
input[type=range] {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
}
|
|
||||||
input[type=range]:focus {
|
input[type=range]:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
input[type=range]::-webkit-slider-runnable-track {
|
input[type=range]::-webkit-slider-runnable-track {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: white;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
}
|
||||||
input[type=range]::-webkit-slider-thumb {
|
input[type=range]::-webkit-slider-thumb {
|
||||||
background: white;
|
background: white;
|
||||||
|
@ -413,10 +408,6 @@ export default {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
input[type=range]:focus::-webkit-slider-runnable-track {
|
|
||||||
background: #white;
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
input[type=range]::-moz-range-track {
|
input[type=range]::-moz-range-track {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: white;
|
background: white;
|
||||||
|
@ -455,7 +446,7 @@ export default {
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
input[type=range]:focus::-ms-fill-upper {
|
input[type=range]:focus::-ms-fill-upper {
|
||||||
background: #white;
|
background: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<audio
|
<i />
|
||||||
ref="audio"
|
|
||||||
@error="errored"
|
|
||||||
@loadeddata="loaded"
|
|
||||||
@durationchange="updateDuration"
|
|
||||||
@timeupdate="updateProgressThrottled"
|
|
||||||
@ended="ended"
|
|
||||||
preload>
|
|
||||||
<source
|
|
||||||
@error="sourceErrored"
|
|
||||||
v-for="src in srcs"
|
|
||||||
:src="src.url"
|
|
||||||
:type="src.type">
|
|
||||||
</audio>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import {mapState} from 'vuex'
|
import {mapState} from 'vuex'
|
||||||
import url from '@/utils/url'
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import url from '@/utils/url'
|
||||||
|
import {Howl} from 'howler'
|
||||||
|
|
||||||
// import logger from '@/logging'
|
// import logger from '@/logging'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -30,11 +19,44 @@ export default {
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
realTrack: this.track,
|
|
||||||
sourceErrors: 0,
|
sourceErrors: 0,
|
||||||
isUpdatingTime: false
|
sound: null,
|
||||||
|
isUpdatingTime: false,
|
||||||
|
progressInterval: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
let self = this
|
||||||
|
this.sound = new Howl({
|
||||||
|
src: this.srcs.map((s) => { return s.url }),
|
||||||
|
autoplay: false,
|
||||||
|
loop: false,
|
||||||
|
html5: true,
|
||||||
|
preload: true,
|
||||||
|
volume: this.volume,
|
||||||
|
onend: function () {
|
||||||
|
self.ended()
|
||||||
|
},
|
||||||
|
onunlock: function () {
|
||||||
|
if (this.$store.state.player.playing) {
|
||||||
|
self.sound.play()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onload: function () {
|
||||||
|
self.$store.commit('player/resetErrorCount')
|
||||||
|
self.$store.commit('player/duration', self.sound.duration())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (this.autoplay) {
|
||||||
|
this.sound.play()
|
||||||
|
this.$store.commit('player/playing', true)
|
||||||
|
this.observeProgress(true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
destroyed () {
|
||||||
|
this.observeProgress(false)
|
||||||
|
this.sound.unload()
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
playing: state => state.player.playing,
|
playing: state => state.player.playing,
|
||||||
|
@ -44,7 +66,7 @@ export default {
|
||||||
looping: state => state.player.looping
|
looping: state => state.player.looping
|
||||||
}),
|
}),
|
||||||
srcs: function () {
|
srcs: function () {
|
||||||
let file = this.realTrack.files[0]
|
let file = this.track.files[0]
|
||||||
if (!file) {
|
if (!file) {
|
||||||
this.$store.dispatch('player/trackErrored')
|
this.$store.dispatch('player/trackErrored')
|
||||||
return []
|
return []
|
||||||
|
@ -68,90 +90,58 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
errored: function () {
|
|
||||||
let self = this
|
|
||||||
setTimeout(
|
|
||||||
() => { self.$store.dispatch('player/trackErrored') }
|
|
||||||
, 1000)
|
|
||||||
},
|
|
||||||
sourceErrored: function () {
|
|
||||||
this.sourceErrors += 1
|
|
||||||
if (this.sourceErrors >= this.srcs.length) {
|
|
||||||
// all sources failed
|
|
||||||
this.errored()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateDuration: function (e) {
|
|
||||||
if (!this.$refs.audio) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$store.commit('player/duration', this.$refs.audio.duration)
|
|
||||||
},
|
|
||||||
loaded: function () {
|
|
||||||
if (!this.$refs.audio) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.$refs.audio.volume = this.volume
|
|
||||||
this.$store.commit('player/resetErrorCount')
|
|
||||||
if (this.isCurrent) {
|
|
||||||
this.$store.commit('player/duration', this.$refs.audio.duration)
|
|
||||||
if (this.startTime) {
|
|
||||||
this.setCurrentTime(this.startTime)
|
|
||||||
}
|
|
||||||
if (this.autoplay) {
|
|
||||||
this.$store.commit('player/playing', true)
|
|
||||||
this.$refs.audio.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updateProgress: function () {
|
updateProgress: function () {
|
||||||
this.isUpdatingTime = true
|
this.isUpdatingTime = true
|
||||||
if (this.$refs.audio) {
|
if (this.sound && this.sound.state() === 'loaded') {
|
||||||
this.$store.dispatch('player/updateProgress', this.$refs.audio.currentTime)
|
this.$store.dispatch('player/updateProgress', this.sound.seek())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
ended: function () {
|
observeProgress: function (enable) {
|
||||||
let onlyTrack = this.$store.state.queue.tracks.length === 1
|
let self = this
|
||||||
if (this.looping === 1 || (onlyTrack && this.looping === 2)) {
|
if (enable) {
|
||||||
this.setCurrentTime(0)
|
if (self.progressInterval) {
|
||||||
this.$refs.audio.play()
|
clearInterval(self.progressInterval)
|
||||||
|
}
|
||||||
|
self.progressInterval = setInterval(() => {
|
||||||
|
self.updateProgress()
|
||||||
|
}, 1000)
|
||||||
} else {
|
} else {
|
||||||
this.$store.dispatch('player/trackEnded', this.realTrack)
|
clearInterval(self.progressInterval)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setCurrentTime (t) {
|
setCurrentTime (t) {
|
||||||
if (t < 0 | t > this.duration) {
|
if (t < 0 | t > this.duration) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (t === this.$refs.audio.currentTime) {
|
if (t === this.sound.seek()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (t === 0) {
|
if (t === 0) {
|
||||||
this.updateProgressThrottled.cancel()
|
this.updateProgressThrottled.cancel()
|
||||||
}
|
}
|
||||||
this.$refs.audio.currentTime = t
|
this.sound.seek(t)
|
||||||
|
},
|
||||||
|
ended: function () {
|
||||||
|
let onlyTrack = this.$store.state.queue.tracks.length === 1
|
||||||
|
if (this.looping === 1 || (onlyTrack && this.looping === 2)) {
|
||||||
|
this.sound.seek(0)
|
||||||
|
this.sound.play()
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('player/trackEnded', this.track)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
track: _.debounce(function (newValue) {
|
|
||||||
this.realTrack = newValue
|
|
||||||
this.setCurrentTime(0)
|
|
||||||
this.$refs.audio.load()
|
|
||||||
}, 1000, {leading: true, trailing: true}),
|
|
||||||
playing: function (newValue) {
|
playing: function (newValue) {
|
||||||
if (newValue === true) {
|
if (newValue === true) {
|
||||||
this.$refs.audio.play()
|
this.sound.play()
|
||||||
} else {
|
} else {
|
||||||
this.$refs.audio.pause()
|
this.sound.pause()
|
||||||
}
|
|
||||||
},
|
|
||||||
'$store.state.queue.currentIndex' () {
|
|
||||||
if (this.$store.state.player.playing) {
|
|
||||||
this.$refs.audio.play()
|
|
||||||
}
|
}
|
||||||
|
this.observeProgress(newValue)
|
||||||
},
|
},
|
||||||
volume: function (newValue) {
|
volume: function (newValue) {
|
||||||
this.$refs.audio.volume = newValue
|
this.sound.volume(newValue)
|
||||||
},
|
},
|
||||||
currentTime (newValue) {
|
currentTime (newValue) {
|
||||||
if (!this.isUpdatingTime) {
|
if (!this.isUpdatingTime) {
|
||||||
|
|
|
@ -3493,6 +3493,10 @@ hosted-git-info@^2.1.4:
|
||||||
version "2.6.0"
|
version "2.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222"
|
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222"
|
||||||
|
|
||||||
|
howler@^2.0.14:
|
||||||
|
version "2.0.14"
|
||||||
|
resolved "https://registry.yarnpkg.com/howler/-/howler-2.0.14.tgz#28e37800fea002fea147a3ca033660c4f1288a99"
|
||||||
|
|
||||||
html-comment-regex@^1.1.0:
|
html-comment-regex@^1.1.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
|
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
|
||||||
|
|
Loading…
Reference in New Issue