diff --git a/changes/changelog.d/390.bugfix b/changes/changelog.d/390.bugfix new file mode 100644 index 0000000000000000000000000000000000000000..df80a8b1db18c040ca4897cb73dabaf143e3808c --- /dev/null +++ b/changes/changelog.d/390.bugfix @@ -0,0 +1 @@ +Fixed broken audio playback on Chrome and invisible volume control (#390) diff --git a/changes/changelog.d/392.enhancement b/changes/changelog.d/392.enhancement new file mode 100644 index 0000000000000000000000000000000000000000..11ca09ac37cd8e60eba59fcaa6bcd561fdd00adf --- /dev/null +++ b/changes/changelog.d/392.enhancement @@ -0,0 +1 @@ +Use Howler to manage audio instead of our own dirty/untested code (#392) diff --git a/front/package.json b/front/package.json index 9837479badac5b9a1897c10ad2d375cf71d5e8a0..80d9d81a501671bd02b30f82c1e320c24b741334 100644 --- a/front/package.json +++ b/front/package.json @@ -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", diff --git a/front/src/components/audio/Player.vue b/front/src/components/audio/Player.vue index 704121d92e2fb98f0688634bbd184bb768258fd8..8e4185c0c6f285346b1101377386f08fe9555a68 100644 --- a/front/src/components/audio/Player.vue +++ b/front/src/components/audio/Player.vue @@ -1,16 +1,15 @@ <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; } } diff --git a/front/src/components/audio/Track.vue b/front/src/components/audio/Track.vue index 9be38337717e8d024e3f711e019a960762d7c355..e22cb62c79995a799c2d02b85f2b1a7fe029173e 100644 --- a/front/src/components/audio/Track.vue +++ b/front/src/components/audio/Track.vue @@ -1,24 +1,13 @@ <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) { diff --git a/front/yarn.lock b/front/yarn.lock index 5c69d78893cdf414feba6c5412acbba6edb4f380..6bc14175f69bdeb38f3be3c7be4f67c6de7d6362 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -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"