mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-20 19:06:06 +01:00
Adding audio playback speed control, updating volume control UI, fix stream play for small streams
This commit is contained in:
parent
4bcb346365
commit
be7e2576f1
@ -28,6 +28,10 @@
|
|||||||
background: #704922;
|
background: #704922;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.no-scroll::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.tracksTable {
|
.tracksTable {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
@ -49,4 +53,12 @@
|
|||||||
.tracksTable th {
|
.tracksTable th {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow-down {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 6px solid transparent;
|
||||||
|
border-right: 6px solid transparent;
|
||||||
|
border-top: 6px solid white;
|
||||||
}
|
}
|
@ -44,4 +44,18 @@
|
|||||||
.menu-enter,
|
.menu-enter,
|
||||||
.menu-leave-active {
|
.menu-leave-active {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menux-enter, .menux-leave-active {
|
||||||
|
transform: translateX(15px);
|
||||||
|
}
|
||||||
|
.menux-enter-active {
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.menux-leave-active {
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
||||||
|
.menux-enter,
|
||||||
|
.menux-leave-active {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
@ -27,9 +27,7 @@
|
|||||||
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="forward10">
|
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="forward10">
|
||||||
<span class="material-icons text-3xl">forward_10</span>
|
<span class="material-icons text-3xl">forward_10</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-center text-gray-300 ml-8" @mousedown.prevent @mouseup.prevent>
|
<controls-playback-speed-control v-model="playbackRate" @change="updatePlaybackRate" />
|
||||||
<span class="font-mono text-lg uppercase text-gray-500">1x</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||||
@ -75,6 +73,7 @@ export default {
|
|||||||
hlsInstance: null,
|
hlsInstance: null,
|
||||||
staleHlsInstance: null,
|
staleHlsInstance: null,
|
||||||
volume: 0.5,
|
volume: 0.5,
|
||||||
|
playbackRate: 1,
|
||||||
trackWidth: 0,
|
trackWidth: 0,
|
||||||
isPaused: true,
|
isPaused: true,
|
||||||
url: null,
|
url: null,
|
||||||
@ -126,7 +125,15 @@ export default {
|
|||||||
},
|
},
|
||||||
updateVolume(volume) {
|
updateVolume(volume) {
|
||||||
if (this.audioEl) {
|
if (this.audioEl) {
|
||||||
this.audioEl.volume = 1 - volume
|
this.audioEl.volume = volume
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updatePlaybackRate(playbackRate) {
|
||||||
|
if (this.audioEl) {
|
||||||
|
console.log('UpdatePlaybackRate', playbackRate)
|
||||||
|
this.audioEl.playbackRate = playbackRate
|
||||||
|
} else {
|
||||||
|
console.error('No Audio El updatePlaybackRate')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mousemoveTrack(e) {
|
mousemoveTrack(e) {
|
||||||
@ -173,7 +180,6 @@ export default {
|
|||||||
setStreamReady() {
|
setStreamReady() {
|
||||||
this.readyTrackWidth = this.trackWidth
|
this.readyTrackWidth = this.trackWidth
|
||||||
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
this.$refs.readyTrack.style.width = this.trackWidth + 'px'
|
||||||
console.warn('SET STREAM READY', this.readyTrackWidth)
|
|
||||||
},
|
},
|
||||||
setChunksReady(chunks, numSegments) {
|
setChunksReady(chunks, numSegments) {
|
||||||
var largestSeg = 0
|
var largestSeg = 0
|
||||||
@ -349,6 +355,7 @@ export default {
|
|||||||
this.hlsInstance = new Hls(hlsOptions)
|
this.hlsInstance = new Hls(hlsOptions)
|
||||||
var audio = this.$refs.audio
|
var audio = this.$refs.audio
|
||||||
audio.volume = this.volume
|
audio.volume = this.volume
|
||||||
|
audio.playbackRate = this.playbackRate
|
||||||
this.hlsInstance.attachMedia(audio)
|
this.hlsInstance.attachMedia(audio)
|
||||||
this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
|
this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||||
// console.log('[HLS] MEDIA ATTACHED')
|
// console.log('[HLS] MEDIA ATTACHED')
|
||||||
@ -367,13 +374,6 @@ export default {
|
|||||||
console.error('[HLS] BUFFER STALLED ERROR')
|
console.error('[HLS] BUFFER STALLED ERROR')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.hlsInstance.on(Hls.Events.FRAG_LOADED, (e, data) => {
|
|
||||||
var frag = data.frag
|
|
||||||
// console.log('[HLS] Frag Loaded', frag.sn, this.$secondsToTimestamp(frag.start), frag)
|
|
||||||
})
|
|
||||||
this.hlsInstance.on(Hls.Events.BUFFER_APPENDED, (e, data) => {
|
|
||||||
// console.log('[HLS] BUFFER', data)
|
|
||||||
})
|
|
||||||
this.hlsInstance.on(Hls.Events.DESTROYING, () => {
|
this.hlsInstance.on(Hls.Events.DESTROYING, () => {
|
||||||
console.warn('[HLS] Destroying HLS Instance')
|
console.warn('[HLS] Destroying HLS Instance')
|
||||||
})
|
})
|
||||||
@ -425,14 +425,7 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.arrow-down {
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
border-left: 6px solid transparent;
|
|
||||||
border-right: 6px solid transparent;
|
|
||||||
border-top: 6px solid white;
|
|
||||||
}
|
|
||||||
.loadingTrack {
|
.loadingTrack {
|
||||||
animation-name: loadingTrack;
|
animation-name: loadingTrack;
|
||||||
animation-duration: 1s;
|
animation-duration: 1s;
|
||||||
|
@ -92,8 +92,11 @@ export default {
|
|||||||
streamProgress(data) {
|
streamProgress(data) {
|
||||||
if (!data.numSegments) return
|
if (!data.numSegments) return
|
||||||
var chunks = data.chunks
|
var chunks = data.chunks
|
||||||
|
console.log(`[STREAM-CONTAINER] Stream Progress ${data.percent}`)
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
||||||
|
} else {
|
||||||
|
console.error('No Audio Ref')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamOpen(stream) {
|
streamOpen(stream) {
|
||||||
@ -101,6 +104,8 @@ export default {
|
|||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
console.log('[STREAM-CONTAINER] streamOpen', stream)
|
console.log('[STREAM-CONTAINER] streamOpen', stream)
|
||||||
this.openStream()
|
this.openStream()
|
||||||
|
} else {
|
||||||
|
console.error('No Audio Ref')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamClosed(streamId) {
|
streamClosed(streamId) {
|
||||||
@ -111,8 +116,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamReady() {
|
streamReady() {
|
||||||
|
console.log(`[STREAM-CONTAINER] Stream Ready`)
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.setStreamReady()
|
this.$refs.audioPlayer.setStreamReady()
|
||||||
|
} else {
|
||||||
|
console.error('No Audio Ref')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateTime(currentTime) {
|
updateTime(currentTime) {
|
||||||
|
62
client/components/controls/PlaybackSpeedControl.vue
Normal file
62
client/components/controls/PlaybackSpeedControl.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative ml-8" v-click-outside="clickOutside">
|
||||||
|
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="showMenu = !showMenu">
|
||||||
|
<span class="font-mono uppercase text-gray-200">{{ playbackRate.toFixed(1) }}<span class="text-lg">⨯</span></span>
|
||||||
|
</div>
|
||||||
|
<div v-show="showMenu" class="absolute -top-10 left-0 z-20 h-9 bg-bg border-black-200 border shadow-xl rounded-lg" style="left: -114px">
|
||||||
|
<div class="absolute -bottom-2 left-0 right-0 w-full flex justify-center">
|
||||||
|
<div class="arrow-down" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full h-full no-scroll flex">
|
||||||
|
<template v-for="(rate, index) in rates">
|
||||||
|
<div :key="rate" class="flex items-center justify-center border-black-300 w-11 hover:bg-black hover:bg-opacity-10 cursor-pointer" :class="index < rates.length - 1 ? 'border-r' : ''" style="min-width: 44px; max-width: 44px" @click="set(rate)">
|
||||||
|
<p class="text-xs text-center font-mono">{{ rate.toFixed(1) }}<span class="text-sm">⨯</span></p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: [String, Number],
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
showMenu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
playbackRate: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rates() {
|
||||||
|
return [0.5, 0.8, 1.0, 1.3, 1.5, 2.0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickOutside() {
|
||||||
|
this.showMenu = false
|
||||||
|
},
|
||||||
|
set(rate) {
|
||||||
|
var newPlaybackRate = Number(rate)
|
||||||
|
var hasChanged = this.playbackRate !== newPlaybackRate
|
||||||
|
this.playbackRate = newPlaybackRate
|
||||||
|
if (hasChanged) this.$emit('change', newPlaybackRate)
|
||||||
|
this.showMenu = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,13 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative" v-click-outside="clickOutside">
|
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div class="cursor-pointer" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
|
<div class="cursor-pointer" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
|
||||||
<span class="material-icons text-3xl">volume_up</span>
|
<span class="material-icons text-3xl">{{ volumeIcon }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="isOpen" class="absolute bottom-10 left-0 h-28 py-2 bg-white shadow-sm rounded-lg">
|
<transition name="menux">
|
||||||
<div ref="volumeTrack" class="w-2 border-2 border-white h-full bg-gray-400 mx-4 relative cursor-pointer" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
|
||||||
<div class="w-3 h-3 bg-gray-500 shadow-sm rounded-full absolute -left-1 bottom-0 pointer-events-none" :class="isDragging ? 'transform scale-150' : ''" :style="{ top: cursorTop + 'px' }" />
|
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
|
||||||
|
<div class="bg-gray-100 h-full absolute left-0 top-0 pointer-events-none rounded-full" :style="{ width: volume * trackWidth + 'px' }" />
|
||||||
|
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ left: cursorLeft + 'px', top: '-3px' }" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -20,8 +23,12 @@ export default {
|
|||||||
return {
|
return {
|
||||||
isOpen: false,
|
isOpen: false,
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
posY: 0,
|
isHovering: false,
|
||||||
trackHeight: 112 - 16
|
posX: 0,
|
||||||
|
lastValue: 0.5,
|
||||||
|
isMute: false,
|
||||||
|
trackWidth: 112 - 20,
|
||||||
|
openTimeout: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -33,22 +40,45 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cursorTop() {
|
cursorLeft() {
|
||||||
var top = this.trackHeight * this.volume
|
var left = this.trackWidth * this.volume
|
||||||
return top - 6
|
return left - 3
|
||||||
|
},
|
||||||
|
volumeIcon() {
|
||||||
|
if (this.volume <= 0) return 'volume_mute'
|
||||||
|
else if (this.volume <= 0.5) return 'volume_down'
|
||||||
|
else return 'volume_up'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
this.setOpen()
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
setOpen() {
|
||||||
|
this.isOpen = true
|
||||||
|
clearTimeout(this.openTimeout)
|
||||||
|
this.openTimeout = setTimeout(() => {
|
||||||
|
if (!this.isHovering && !this.isDragging) {
|
||||||
|
this.isOpen = false
|
||||||
|
} else {
|
||||||
|
this.setOpen()
|
||||||
|
}
|
||||||
|
}, 600)
|
||||||
|
},
|
||||||
mousemove(e) {
|
mousemove(e) {
|
||||||
var diff = this.posY - e.y
|
var diff = this.posX - e.x
|
||||||
this.posY = e.y
|
this.posX = e.x
|
||||||
var volShift = 0
|
var volShift = 0
|
||||||
if (diff < 0) {
|
if (diff < 0) {
|
||||||
// Volume up
|
// Volume up
|
||||||
volShift = diff / this.trackHeight
|
volShift = diff / this.trackWidth
|
||||||
} else {
|
} else {
|
||||||
// volume down
|
// volume down
|
||||||
volShift = diff / this.trackHeight
|
volShift = diff / this.trackWidth
|
||||||
}
|
}
|
||||||
var newVol = this.volume - volShift
|
var newVol = this.volume - volShift
|
||||||
newVol = Math.min(Math.max(0, newVol), 1)
|
newVol = Math.min(Math.max(0, newVol), 1)
|
||||||
@ -64,8 +94,8 @@ export default {
|
|||||||
},
|
},
|
||||||
mousedownTrack(e) {
|
mousedownTrack(e) {
|
||||||
this.isDragging = true
|
this.isDragging = true
|
||||||
this.posY = e.y
|
this.posX = e.x
|
||||||
var vol = e.offsetY / e.target.clientHeight
|
var vol = e.offsetX / this.trackWidth
|
||||||
vol = Math.min(Math.max(vol, 0), 1)
|
vol = Math.min(Math.max(vol, 0), 1)
|
||||||
this.volume = vol
|
this.volume = vol
|
||||||
document.body.addEventListener('mousemove', this.mousemove)
|
document.body.addEventListener('mousemove', this.mousemove)
|
||||||
@ -76,14 +106,24 @@ export default {
|
|||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
},
|
},
|
||||||
clickVolumeIcon() {
|
clickVolumeIcon() {
|
||||||
this.isOpen = !this.isOpen
|
this.isMute = !this.isMute
|
||||||
|
if (this.isMute) {
|
||||||
|
this.lastValue = this.volume
|
||||||
|
this.volume = 0
|
||||||
|
} else {
|
||||||
|
this.volume = this.lastValue || 0.5
|
||||||
|
}
|
||||||
},
|
},
|
||||||
clickVolumeTrack(e) {
|
clickVolumeTrack(e) {
|
||||||
var vol = e.offsetY / e.target.clientHeight
|
var vol = e.offsetX / this.trackWidth
|
||||||
vol = Math.min(Math.max(vol, 0), 1)
|
vol = Math.min(Math.max(vol, 0), 1)
|
||||||
this.volume = vol
|
this.volume = vol
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {
|
||||||
|
if (this.value === 0) {
|
||||||
|
this.isMute = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
@ -85,7 +85,7 @@ export default {
|
|||||||
console.log('Search', this.lastSearch, this.search)
|
console.log('Search', this.lastSearch, this.search)
|
||||||
|
|
||||||
this.searchResults = []
|
this.searchResults = []
|
||||||
this.processing = true
|
this.isProcessing = true
|
||||||
this.lastSearch = this.search
|
this.lastSearch = this.search
|
||||||
var results = await this.$axios.$get(`/api/find/search?title=${this.search}`).catch((error) => {
|
var results = await this.$axios.$get(`/api/find/search?title=${this.search}`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
@ -96,7 +96,7 @@ export default {
|
|||||||
})
|
})
|
||||||
console.log('Got results', results)
|
console.log('Got results', results)
|
||||||
this.searchResults = results
|
this.searchResults = results
|
||||||
this.processing = false
|
this.isProcessing = false
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
if (!this.audiobook.book || !this.audiobook.book.title) {
|
if (!this.audiobook.book || !this.audiobook.book.title) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "0.9.52",
|
"version": "0.9.54",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -3,18 +3,27 @@
|
|||||||
<div class="w-full max-w-4xl mx-auto">
|
<div class="w-full max-w-4xl mx-auto">
|
||||||
<h1 class="text-2xl mb-2">Config</h1>
|
<h1 class="text-2xl mb-2">Config</h1>
|
||||||
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
||||||
<div class="p-4 text-center h-40">
|
<div class="p-4 text-center h-20">
|
||||||
<p>Nothing much here yet...</p>
|
<p>Nothing much here yet...</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4 mb-8">
|
||||||
<p class="text-2xl">Scanner</p>
|
<p class="text-2xl">Scanner</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn color="success" @click="scan">Scan</ui-btn>
|
<ui-btn color="success" @click="scan">Scan</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
<div class="h-0.5 bg-primary bg-opacity-50 w-full" />
|
||||||
<div class="flex items-center py-4">
|
<div class="flex items-center py-4">
|
||||||
<p class="font-mono">v{{ $config.version }}</p>
|
<p class="font-mono">Beta v{{ $config.version }}</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p class="pr-2 text-sm font-book text-yellow-400">Report bugs, request features, provide feedback, and contribute on <a class="underline" href="https://github.com/advplyr/audiobookshelf" target="_blank">github</a>.</p>
|
||||||
|
<a href="https://github.com/advplyr/audiobookshelf" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width="24" height="24" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "0.9.53",
|
"version": "0.9.54",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -28,14 +28,11 @@ class HlsController {
|
|||||||
|
|
||||||
async streamFileRequest(req, res) {
|
async streamFileRequest(req, res) {
|
||||||
var streamId = req.params.stream
|
var streamId = req.params.stream
|
||||||
|
|
||||||
// Logger.info('Got hls request', streamId, req.params.file)
|
|
||||||
|
|
||||||
var fullFilePath = Path.join(this.MetadataPath, streamId, req.params.file)
|
var fullFilePath = Path.join(this.MetadataPath, streamId, req.params.file)
|
||||||
|
|
||||||
var exists = await fs.pathExists(fullFilePath)
|
var exists = await fs.pathExists(fullFilePath)
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
Logger.error('File path does not exist', fullFilePath)
|
Logger.warn('File path does not exist', fullFilePath)
|
||||||
|
|
||||||
var fileExt = Path.extname(req.params.file)
|
var fileExt = Path.extname(req.params.file)
|
||||||
if (fileExt === '.ts') {
|
if (fileExt === '.ts') {
|
||||||
@ -52,36 +49,16 @@ class HlsController {
|
|||||||
} else {
|
} else {
|
||||||
var startTimeForReset = await stream.checkSegmentNumberRequest(segNum)
|
var startTimeForReset = await stream.checkSegmentNumberRequest(segNum)
|
||||||
if (startTimeForReset) {
|
if (startTimeForReset) {
|
||||||
// HLS.js should request the file again]
|
// HLS.js will restart the stream at the new time
|
||||||
Logger.info(`[HLS-CONTROLLER] Resetting Stream - notify client @${startTimeForReset}s`)
|
Logger.info(`[HLS-CONTROLLER] Resetting Stream - notify client @${startTimeForReset}s`)
|
||||||
this.emitter('stream_reset', {
|
this.emitter('stream_reset', {
|
||||||
startTime: startTimeForReset,
|
startTime: startTimeForReset,
|
||||||
streamId: stream.id
|
streamId: stream.id
|
||||||
})
|
})
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
// await new Promise((resolve) => {
|
|
||||||
// setTimeout(() => {
|
|
||||||
// console.log('Waited 4 seconds')
|
|
||||||
// resolve()
|
|
||||||
// }, 4000)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// exists = await fs.pathExists(fullFilePath)
|
|
||||||
// if (!exists) {
|
|
||||||
// console.error('Still does not exist')
|
|
||||||
// return res.sendStatus(404)
|
|
||||||
// }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// await new Promise(resolve => setTimeout(resolve, 500))
|
|
||||||
// exists = await fs.pathExists(fullFilePath)
|
|
||||||
// Logger.info('Waited', exists)
|
|
||||||
// if (!exists) {
|
|
||||||
// Logger.error('still does not exist', fullFilePath)
|
|
||||||
// return res.sendStatus(404)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
// Logger.info('Sending file', fullFilePath)
|
// Logger.info('Sending file', fullFilePath)
|
||||||
res.sendFile(fullFilePath)
|
res.sendFile(fullFilePath)
|
||||||
|
@ -130,7 +130,6 @@ class Server {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
app.post('/stream', (req, res) => this.streamManager.openStreamRequest(req, res))
|
|
||||||
app.post('/login', (req, res) => this.auth.login(req, res))
|
app.post('/login', (req, res) => this.auth.login(req, res))
|
||||||
app.post('/logout', this.logout.bind(this))
|
app.post('/logout', this.logout.bind(this))
|
||||||
app.get('/ping', (req, res) => {
|
app.get('/ping', (req, res) => {
|
||||||
@ -165,7 +164,6 @@ class Server {
|
|||||||
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket))
|
||||||
socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload))
|
socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload))
|
||||||
socket.on('test', () => {
|
socket.on('test', () => {
|
||||||
console.log('Test Request from', socket.id)
|
|
||||||
socket.emit('test_received', socket.id)
|
socket.emit('test_received', socket.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
142
server/Stream.js
142
server/Stream.js
@ -296,144 +296,12 @@ class Stream extends EventEmitter {
|
|||||||
|
|
||||||
this.ffmpeg.on('end', (stdout, stderr) => {
|
this.ffmpeg.on('end', (stdout, stderr) => {
|
||||||
Logger.info('[FFMPEG] Transcoding ended')
|
Logger.info('[FFMPEG] Transcoding ended')
|
||||||
this.isTranscodeComplete = true
|
// For very small fast load
|
||||||
this.ffmpeg = null
|
if (!this.isClientInitialized) {
|
||||||
})
|
this.isClientInitialized = true
|
||||||
|
Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`)
|
||||||
this.ffmpeg.run()
|
this.socket.emit('stream_open', this.toJSON())
|
||||||
}
|
|
||||||
|
|
||||||
async startConcat() {
|
|
||||||
Logger.info(`[STREAM] START STREAM - Num Segments: ${this.numSegments}`)
|
|
||||||
|
|
||||||
|
|
||||||
var concatOutput = null
|
|
||||||
if (this.tracks.length > 1) {
|
|
||||||
var start = Date.now()
|
|
||||||
await new Promise(async (resolve) => {
|
|
||||||
Logger.info('Concatenating here', this.tracks.length)
|
|
||||||
|
|
||||||
this.ffmpeg = Ffmpeg()
|
|
||||||
var trackExt = this.tracks[0].ext
|
|
||||||
concatOutput = Path.join(this.streamPath, `concat${trackExt}`)
|
|
||||||
Logger.info('Concat OUTPUT', concatOutput)
|
|
||||||
var trackPaths = this.tracks.map(t => {
|
|
||||||
var line = 'file ' + this.escapeSingleQuotes(t.fullPath) + '\n' + `duration ${t.duration}`
|
|
||||||
return line
|
|
||||||
})
|
|
||||||
var inputstr = trackPaths.join('\n\n')
|
|
||||||
await fs.writeFile(this.concatFilesPath, inputstr)
|
|
||||||
this.ffmpeg.addInput(this.concatFilesPath)
|
|
||||||
this.ffmpeg.inputFormat('concat')
|
|
||||||
this.ffmpeg.inputOption('-safe 0')
|
|
||||||
this.ffmpeg.addOption([
|
|
||||||
'-loglevel warning',
|
|
||||||
'-map 0:a',
|
|
||||||
'-c:a copy'
|
|
||||||
])
|
|
||||||
this.ffmpeg.output(concatOutput)
|
|
||||||
|
|
||||||
this.ffmpeg.on('start', (command) => {
|
|
||||||
Logger.info('[CONCAT] FFMPEG transcoding started with command: ' + command)
|
|
||||||
})
|
|
||||||
this.ffmpeg.on('error', (err, stdout, stderr) => {
|
|
||||||
Logger.info('[CONCAT] ERROR', err, stderr)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.ffmpeg.on('end', (stdout, stderr) => {
|
|
||||||
Logger.info('[CONCAT] Concat is done')
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
this.ffmpeg.run()
|
|
||||||
})
|
|
||||||
var elapsed = ((Date.now() - start) / 1000).toFixed(1)
|
|
||||||
Logger.info(`[CONCAT] Final elapsed is ${elapsed}s`)
|
|
||||||
} else {
|
|
||||||
concatOutput = this.tracks[0].fullPath
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
this.ffmpeg = Ffmpeg()
|
|
||||||
|
|
||||||
// var currTrackEnd = 0
|
|
||||||
// var startingTrack = this.tracks.find(t => {
|
|
||||||
// currTrackEnd += t.duration
|
|
||||||
// return this.startTime < currTrackEnd
|
|
||||||
// })
|
|
||||||
// var trackStartTime = currTrackEnd - startingTrack.duration
|
|
||||||
// var currInpoint = this.startTime - trackStartTime
|
|
||||||
|
|
||||||
// var tracksToInclude = this.tracks.filter(t => t.index >= startingTrack.index)
|
|
||||||
|
|
||||||
// var trackPaths = tracksToInclude.map(t => {
|
|
||||||
// var line = 'file ' + this.escapeSingleQuotes(t.fullPath) + '\n' + `duration ${t.duration}`
|
|
||||||
// if (t.index === startingTrack.index) {
|
|
||||||
// line += `\ninpoint ${currInpoint}`
|
|
||||||
// }
|
|
||||||
// return line
|
|
||||||
// })
|
|
||||||
// var inputstr = trackPaths.join('\n\n')
|
|
||||||
// await fs.writeFile(this.concatFilesPath, inputstr)
|
|
||||||
|
|
||||||
this.ffmpeg.addInput(concatOutput)
|
|
||||||
// this.ffmpeg.inputFormat('concat')
|
|
||||||
// this.ffmpeg.inputOption('-safe 0')
|
|
||||||
|
|
||||||
if (this.startTime > 0) {
|
|
||||||
Logger.info(`[STREAM] Starting Stream at startTime ${secondsToTimestamp(this.startTime)} and Segment #${this.segmentStartNumber}`)
|
|
||||||
this.ffmpeg.inputOption(`-ss ${this.startTime}`)
|
|
||||||
this.ffmpeg.inputOption('-noaccurate_seek')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ffmpeg.addOption([
|
|
||||||
'-loglevel warning',
|
|
||||||
'-map 0:a',
|
|
||||||
'-c:a copy'
|
|
||||||
])
|
|
||||||
this.ffmpeg.addOption([
|
|
||||||
'-f hls',
|
|
||||||
"-copyts",
|
|
||||||
"-avoid_negative_ts disabled",
|
|
||||||
"-max_delay 5000000",
|
|
||||||
"-max_muxing_queue_size 2048",
|
|
||||||
`-hls_time 6`,
|
|
||||||
"-hls_segment_type mpegts",
|
|
||||||
`-start_number ${this.segmentStartNumber}`,
|
|
||||||
"-hls_playlist_type vod",
|
|
||||||
"-hls_list_size 0",
|
|
||||||
"-hls_allow_cache 0"
|
|
||||||
])
|
|
||||||
var segmentFilename = Path.join(this.streamPath, this.segmentBasename)
|
|
||||||
this.ffmpeg.addOption(`-hls_segment_filename ${segmentFilename}`)
|
|
||||||
this.ffmpeg.output(this.playlistPath)
|
|
||||||
|
|
||||||
this.ffmpeg.on('start', (command) => {
|
|
||||||
Logger.info('[INFO] FFMPEG transcoding started with command: ' + command)
|
|
||||||
if (this.isResetting) {
|
|
||||||
setTimeout(() => {
|
|
||||||
Logger.info('[STREAM] Clearing isResetting')
|
|
||||||
this.isResetting = false
|
|
||||||
}, 500)
|
|
||||||
}
|
}
|
||||||
this.startLoop()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.ffmpeg.on('stderr', (stdErrline) => {
|
|
||||||
Logger.info(stdErrline)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.ffmpeg.on('error', (err, stdout, stderr) => {
|
|
||||||
if (err.message && err.message.includes('SIGKILL')) {
|
|
||||||
// This is an intentional SIGKILL
|
|
||||||
Logger.info('[FFMPEG] Transcode Killed')
|
|
||||||
this.ffmpeg = null
|
|
||||||
} else {
|
|
||||||
Logger.error('Ffmpeg Err', err.message)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
this.ffmpeg.on('end', (stdout, stderr) => {
|
|
||||||
Logger.info('[FFMPEG] Transcoding ended')
|
|
||||||
this.isTranscodeComplete = true
|
this.isTranscodeComplete = true
|
||||||
this.ffmpeg = null
|
this.ffmpeg = null
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user