Skip to content
Snippets Groups Projects
Commit 8974881f authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch '392-new-player' into 'develop'

Resolve "Investigate Howler.js as a way to manage low-level audio and get cross-browser compatibility"

Closes #392 and #390

See merge request !351
parents 73757ff9 50fb5543
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 @@ ...@@ -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",
......
<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="renderAudio && currentTrack" v-if="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 @@ ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
} }
} }
......
<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 { ...@@ -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 { ...@@ -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 { ...@@ -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) {
......
...@@ -56,7 +56,8 @@ export default { ...@@ -56,7 +56,8 @@ export default {
fetch ({dispatch, state, commit, rootState}, url) { fetch ({dispatch, state, commit, rootState}, url) {
// will fetch favorites by batches from API to have them locally // will fetch favorites by batches from API to have them locally
let params = { let params = {
user: rootState.auth.profile.id user: rootState.auth.profile.id,
page_size: 50
} }
url = url || 'favorites/tracks/' url = url || 'favorites/tracks/'
return axios.get(url, {params: params}).then((response) => { return axios.get(url, {params: params}).then((response) => {
......
...@@ -3493,6 +3493,10 @@ hosted-git-info@^2.1.4: ...@@ -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"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment