Skip to content
Snippets Groups Projects
Verified Commit 1f084750 authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Fix #390 and #392: rewritten audio player internal logic, broken volume control under Chrome

parent 3862034d
No related branches found
No related tags found
No related merge requests found
Fixed broken audio playback on Chrome and invisible volume control (#390)
Use Howler to manage audio instead of our own dirty/untested code (#392)
......@@ -21,6 +21,7 @@
"axios": "^0.17.1",
"dateformat": "^2.0.0",
"django-channels": "^1.1.6",
"howler": "^2.0.14",
"js-logger": "^1.3.0",
"jwt-decode": "^2.2.0",
"lodash": "^4.17.4",
......
<template>
<div class="ui inverted segment player-wrapper" :style="style">
<div class="player">
<keep-alive>
<audio-track
ref="currentAudio"
v-if="renderAudio && currentTrack"
:is-current="true"
:start-time="$store.state.player.currentTime"
:autoplay="$store.state.player.playing"
:track="currentTrack">
</audio-track>
</keep-alive>
<audio-track
ref="currentAudio"
v-if="currentTrack"
:is-current="true"
:start-time="$store.state.player.currentTime"
:autoplay="$store.state.player.playing"
:key="audioKey"
:track="currentTrack">
</audio-track>
<div v-if="currentTrack" class="track-area ui unstackable items">
<div class="ui inverted item">
<div class="ui tiny image">
......@@ -160,13 +159,13 @@
import {mapState, mapGetters, mapActions} from 'vuex'
import GlobalEvents from '@/components/utils/global-events'
import ColorThief from '@/vendor/color-thief'
import {Howl} from 'howler'
import AudioTrack from '@/components/audio/Track'
import TrackFavoriteIcon from '@/components/favorites/TrackFavoriteIcon'
import TrackPlaylistIcon from '@/components/playlists/TrackPlaylistIcon'
export default {
name: 'player',
components: {
TrackFavoriteIcon,
TrackPlaylistIcon,
......@@ -177,16 +176,28 @@ export default {
let defaultAmbiantColors = [[46, 46, 46], [46, 46, 46], [46, 46, 46], [46, 46, 46]]
return {
isShuffling: false,
renderAudio: true,
sliderVolume: this.volume,
defaultAmbiantColors: defaultAmbiantColors,
showVolume: false,
ambiantColors: defaultAmbiantColors
ambiantColors: defaultAmbiantColors,
audioKey: String(new Date()),
dummyAudio: null
}
},
mounted () {
// we trigger the watcher explicitely it does not work otherwise
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: {
...mapActions({
......@@ -305,21 +316,13 @@ export default {
},
watch: {
currentTrack (newValue) {
if (!this.isShuffling) {
this.audioKey = String(new Date())
}
if (!newValue || !newValue.album.cover) {
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) {
this.sliderVolume = newValue
},
......@@ -385,9 +388,6 @@ export default {
.volume-control {
position: relative;
width: 12.5% !important;
.icon {
// margin: 0;
}
[type="range"] {
max-width: 70%;
position: absolute;
......@@ -395,16 +395,11 @@ export default {
left: 25%;
cursor: pointer;
}
input[type=range] {
-webkit-appearance: none;
}
input[type=range]:focus {
outline: none;
}
input[type=range]::-webkit-slider-runnable-track {
cursor: pointer;
background: white;
opacity: 0.3;
}
input[type=range]::-webkit-slider-thumb {
background: white;
......@@ -413,10 +408,6 @@ export default {
border-radius: 3px;
width: 10px;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: #white;
opacity: 0.3;
}
input[type=range]::-moz-range-track {
cursor: pointer;
background: white;
......@@ -455,7 +446,7 @@ export default {
background: white;
}
input[type=range]:focus::-ms-fill-upper {
background: #white;
background: white;
}
}
......
<template>
<audio
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>
<i />
</template>
<script>
import {mapState} from 'vuex'
import url from '@/utils/url'
import _ from 'lodash'
import url from '@/utils/url'
import {Howl} from 'howler'
// import logger from '@/logging'
export default {
......@@ -30,11 +19,44 @@ export default {
},
data () {
return {
realTrack: this.track,
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: {
...mapState({
playing: state => state.player.playing,
......@@ -44,7 +66,7 @@ export default {
looping: state => state.player.looping
}),
srcs: function () {
let file = this.realTrack.files[0]
let file = this.track.files[0]
if (!file) {
this.$store.dispatch('player/trackErrored')
return []
......@@ -68,90 +90,58 @@ export default {
}
},
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 () {
this.isUpdatingTime = true
if (this.$refs.audio) {
this.$store.dispatch('player/updateProgress', this.$refs.audio.currentTime)
if (this.sound && this.sound.state() === 'loaded') {
this.$store.dispatch('player/updateProgress', this.sound.seek())
}
},
ended: function () {
let onlyTrack = this.$store.state.queue.tracks.length === 1
if (this.looping === 1 || (onlyTrack && this.looping === 2)) {
this.setCurrentTime(0)
this.$refs.audio.play()
observeProgress: function (enable) {
let self = this
if (enable) {
if (self.progressInterval) {
clearInterval(self.progressInterval)
}
self.progressInterval = setInterval(() => {
self.updateProgress()
}, 1000)
} else {
this.$store.dispatch('player/trackEnded', this.realTrack)
clearInterval(self.progressInterval)
}
},
setCurrentTime (t) {
if (t < 0 | t > this.duration) {
return
}
if (t === this.$refs.audio.currentTime) {
if (t === this.sound.seek()) {
return
}
if (t === 0) {
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: {
track: _.debounce(function (newValue) {
this.realTrack = newValue
this.setCurrentTime(0)
this.$refs.audio.load()
}, 1000, {leading: true, trailing: true}),
playing: function (newValue) {
if (newValue === true) {
this.$refs.audio.play()
this.sound.play()
} else {
this.$refs.audio.pause()
}
},
'$store.state.queue.currentIndex' () {
if (this.$store.state.player.playing) {
this.$refs.audio.play()
this.sound.pause()
}
this.observeProgress(newValue)
},
volume: function (newValue) {
this.$refs.audio.volume = newValue
this.sound.volume(newValue)
},
currentTime (newValue) {
if (!this.isUpdatingTime) {
......
......@@ -3493,6 +3493,10 @@ hosted-git-info@^2.1.4:
version "2.6.0"
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:
version "1.1.1"
resolved "https://registry.yarnpkg.com/html-comment-regex/-/html-comment-regex-1.1.1.tgz#668b93776eaae55ebde8f3ad464b307a4963625e"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment