mirror of
https://github.com/owntone/owntone-server.git
synced 2024-12-24 22:25:56 -05:00
[web-src] Add option to stream audio from player web interface
The audio implementation is based on https://github.com/rainner/soma-fm-player and makes use of the HTML5 Web Audio Context API.
This commit is contained in:
parent
7a2ba572f8
commit
3b718ffb16
51
web-src/src/audio.js
Normal file
51
web-src/src/audio.js
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Audio handler object
|
||||
* Taken from https://github.com/rainner/soma-fm-player (released under MIT licence)
|
||||
*/
|
||||
export default {
|
||||
_audio: new Audio(),
|
||||
_context: new AudioContext(),
|
||||
_source: null,
|
||||
_gain: null,
|
||||
_analyser: null,
|
||||
|
||||
// setup audio routing
|
||||
setupAudio () {
|
||||
this._source = this._context.createMediaElementSource(this._audio)
|
||||
this._analyser = this._context.createAnalyser()
|
||||
this._gain = this._context.createGain()
|
||||
|
||||
this._source.connect(this._gain)
|
||||
this._source.connect(this._analyser)
|
||||
this._gain.connect(this._context.destination)
|
||||
|
||||
this._audio.addEventListener('canplaythrough', e => {
|
||||
this._audio.play()
|
||||
})
|
||||
return this._audio
|
||||
},
|
||||
|
||||
// set audio volume
|
||||
setVolume (volume) {
|
||||
if (!this._gain) return
|
||||
volume = parseFloat(volume) || 0.0
|
||||
volume = (volume < 0) ? 0 : volume
|
||||
volume = (volume > 1) ? 1 : volume
|
||||
this._gain.gain.value = volume
|
||||
},
|
||||
|
||||
// play audio source url
|
||||
playSource (source) {
|
||||
this.stopAudio()
|
||||
this._audio.src = String(source || '') + '?x=' + Date.now()
|
||||
this._audio.crossOrigin = 'anonymous'
|
||||
this._audio.load()
|
||||
},
|
||||
|
||||
// stop playing audio
|
||||
stopAudio () {
|
||||
try { this._audio.pause() } catch (e) {}
|
||||
try { this._audio.stop() } catch (e) {}
|
||||
try { this._audio.close() } catch (e) {}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<span class="icon fd-has-action" :class="{ 'has-text-grey-light': !output.selected }" v-on:click="set_enabled"><i class="mdi mdi-18px" v-bind:class="type_class"></i></span>
|
||||
<a class="button is-white is-small"><span class="icon fd-has-action" :class="{ 'has-text-grey-light': !output.selected }" v-on:click="set_enabled"><i class="mdi mdi-18px" v-bind:class="type_class"></i></span></a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
|
@ -60,6 +60,31 @@
|
||||
<hr class="navbar-divider">
|
||||
<nav-bar-item-output v-for="output in outputs" :key="output.id" :output="output"></nav-bar-item-output>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
<div class="navbar-item">
|
||||
<div class="level is-mobile">
|
||||
<div class="level-left fd-expanded">
|
||||
<div class="level-item" style="flex-grow: 0;">
|
||||
<a class="button is-white is-small" :class="{ 'is-loading': loading }"><span class="icon fd-has-action" :class="{ 'has-text-grey-light': !playing && !loading, 'is-loading': loading }" @click="togglePlay"><i class="mdi mdi-18px mdi-radio-tower"></i></span></a>
|
||||
</div>
|
||||
<div class="level-item fd-expanded">
|
||||
<div class="fd-expanded">
|
||||
<p class="heading" :class="{ 'has-text-grey-light': !playing }">HTTP stream</p>
|
||||
<range-slider
|
||||
class="slider fd-has-action"
|
||||
min="0"
|
||||
max="100"
|
||||
step="1"
|
||||
:disabled="!playing"
|
||||
:value="stream_volume"
|
||||
@change="set_stream_volume">
|
||||
</range-slider>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="navbar-divider">
|
||||
<div class="navbar-item">
|
||||
<div class="level is-mobile">
|
||||
@ -104,6 +129,7 @@
|
||||
|
||||
<script>
|
||||
import webapi from '@/webapi'
|
||||
import _audio from '@/audio'
|
||||
import NavBarItemOutput from './NavBarItemOutput'
|
||||
import PlayerButtonPlayPause from './PlayerButtonPlayPause'
|
||||
import PlayerButtonNext from './PlayerButtonNext'
|
||||
@ -120,7 +146,11 @@ export default {
|
||||
|
||||
data () {
|
||||
return {
|
||||
search_query: ''
|
||||
search_query: '',
|
||||
|
||||
playing: false,
|
||||
loading: false,
|
||||
stream_volume: 10
|
||||
}
|
||||
},
|
||||
|
||||
@ -166,7 +196,72 @@ export default {
|
||||
open_about: function () {
|
||||
this.$store.commit(types.SHOW_BURGER_MENU, false)
|
||||
this.$router.push({ path: '/about' })
|
||||
},
|
||||
|
||||
setupAudio: function () {
|
||||
const a = _audio.setupAudio()
|
||||
|
||||
a.addEventListener('waiting', e => {
|
||||
this.playing = false
|
||||
this.loading = true
|
||||
})
|
||||
a.addEventListener('playing', e => {
|
||||
this.playing = true
|
||||
this.loading = false
|
||||
})
|
||||
a.addEventListener('ended', e => {
|
||||
this.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
a.addEventListener('error', e => {
|
||||
this.closeAudio()
|
||||
this.$store.dispatch('add_notification', { text: 'HTTP stream error: failed to load stream or stopped loading due to network problem', type: 'danger' })
|
||||
this.playing = false
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
|
||||
// close active audio
|
||||
closeAudio: function () {
|
||||
_audio.stopAudio()
|
||||
this.playing = false
|
||||
},
|
||||
|
||||
playChannel: function () {
|
||||
if (this.playing) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = '/stream.mp3'
|
||||
this.loading = true
|
||||
_audio.playSource(channel)
|
||||
_audio.setVolume(this.stream_volume / 100)
|
||||
},
|
||||
|
||||
togglePlay: function () {
|
||||
if (this.loading) {
|
||||
return
|
||||
}
|
||||
if (this.playing) {
|
||||
return this.closeAudio()
|
||||
}
|
||||
return this.playChannel()
|
||||
},
|
||||
|
||||
set_stream_volume: function (newVolume) {
|
||||
this.stream_volume = newVolume
|
||||
_audio.setVolume(this.stream_volume / 100)
|
||||
}
|
||||
},
|
||||
|
||||
// on app mounted
|
||||
mounted () {
|
||||
this.setupAudio()
|
||||
},
|
||||
|
||||
// on app destroyed
|
||||
destroyed () {
|
||||
this.closeAudio()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -24,10 +24,13 @@ module.exports = {
|
||||
// localhost:3689
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:3689',
|
||||
target: 'http://localhost:3689'
|
||||
},
|
||||
'/artwork': {
|
||||
target: 'http://localhost:3689',
|
||||
target: 'http://localhost:3689'
|
||||
},
|
||||
'/stream.mp3': {
|
||||
target: 'http://localhost:3689'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user