mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +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;  | ||||
| } | ||||
| 
 | ||||
| .no-scroll::-webkit-scrollbar { | ||||
|   display: none; | ||||
|   opacity: 0; | ||||
| } | ||||
| 
 | ||||
| .tracksTable { | ||||
|   border-collapse: collapse; | ||||
| @ -49,4 +53,12 @@ | ||||
| .tracksTable th { | ||||
|   padding: 4px; | ||||
|   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-leave-active { | ||||
|   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"> | ||||
|             <span class="material-icons text-3xl">forward_10</span> | ||||
|           </div> | ||||
|           <div class="flex items-center justify-center text-gray-300 ml-8" @mousedown.prevent @mouseup.prevent> | ||||
|             <span class="font-mono text-lg uppercase text-gray-500">1x</span> | ||||
|           </div> | ||||
|           <controls-playback-speed-control v-model="playbackRate" @change="updatePlaybackRate" /> | ||||
|         </template> | ||||
|         <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"> | ||||
| @ -75,6 +73,7 @@ export default { | ||||
|       hlsInstance: null, | ||||
|       staleHlsInstance: null, | ||||
|       volume: 0.5, | ||||
|       playbackRate: 1, | ||||
|       trackWidth: 0, | ||||
|       isPaused: true, | ||||
|       url: null, | ||||
| @ -126,7 +125,15 @@ export default { | ||||
|     }, | ||||
|     updateVolume(volume) { | ||||
|       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) { | ||||
| @ -173,7 +180,6 @@ export default { | ||||
|     setStreamReady() { | ||||
|       this.readyTrackWidth = this.trackWidth | ||||
|       this.$refs.readyTrack.style.width = this.trackWidth + 'px' | ||||
|       console.warn('SET STREAM READY', this.readyTrackWidth) | ||||
|     }, | ||||
|     setChunksReady(chunks, numSegments) { | ||||
|       var largestSeg = 0 | ||||
| @ -349,6 +355,7 @@ export default { | ||||
|       this.hlsInstance = new Hls(hlsOptions) | ||||
|       var audio = this.$refs.audio | ||||
|       audio.volume = this.volume | ||||
|       audio.playbackRate = this.playbackRate | ||||
|       this.hlsInstance.attachMedia(audio) | ||||
|       this.hlsInstance.on(Hls.Events.MEDIA_ATTACHED, () => { | ||||
|         // console.log('[HLS] MEDIA ATTACHED') | ||||
| @ -367,13 +374,6 @@ export default { | ||||
|             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, () => { | ||||
|           console.warn('[HLS] Destroying HLS Instance') | ||||
|         }) | ||||
| @ -425,14 +425,7 @@ export default { | ||||
| } | ||||
| </script> | ||||
| 
 | ||||
| <style scoped> | ||||
| .arrow-down { | ||||
|   width: 0; | ||||
|   height: 0; | ||||
|   border-left: 6px solid transparent; | ||||
|   border-right: 6px solid transparent; | ||||
|   border-top: 6px solid white; | ||||
| } | ||||
| <style> | ||||
| .loadingTrack { | ||||
|   animation-name: loadingTrack; | ||||
|   animation-duration: 1s; | ||||
|  | ||||
| @ -92,8 +92,11 @@ export default { | ||||
|     streamProgress(data) { | ||||
|       if (!data.numSegments) return | ||||
|       var chunks = data.chunks | ||||
|       console.log(`[STREAM-CONTAINER] Stream Progress ${data.percent}`) | ||||
|       if (this.$refs.audioPlayer) { | ||||
|         this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments) | ||||
|       } else { | ||||
|         console.error('No Audio Ref') | ||||
|       } | ||||
|     }, | ||||
|     streamOpen(stream) { | ||||
| @ -101,6 +104,8 @@ export default { | ||||
|       if (this.$refs.audioPlayer) { | ||||
|         console.log('[STREAM-CONTAINER] streamOpen', stream) | ||||
|         this.openStream() | ||||
|       } else { | ||||
|         console.error('No Audio Ref') | ||||
|       } | ||||
|     }, | ||||
|     streamClosed(streamId) { | ||||
| @ -111,8 +116,11 @@ export default { | ||||
|       } | ||||
|     }, | ||||
|     streamReady() { | ||||
|       console.log(`[STREAM-CONTAINER] Stream Ready`) | ||||
|       if (this.$refs.audioPlayer) { | ||||
|         this.$refs.audioPlayer.setStreamReady() | ||||
|       } else { | ||||
|         console.error('No Audio Ref') | ||||
|       } | ||||
|     }, | ||||
|     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> | ||||
|   <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"> | ||||
|       <span class="material-icons text-3xl">volume_up</span> | ||||
|       <span class="material-icons text-3xl">{{ volumeIcon }}</span> | ||||
|     </div> | ||||
|     <div v-show="isOpen" class="absolute bottom-10 left-0 h-28 py-2 bg-white shadow-sm rounded-lg"> | ||||
|       <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 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' }" /> | ||||
|     <transition name="menux"> | ||||
|       <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 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> | ||||
|     </transition> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| @ -20,8 +23,12 @@ export default { | ||||
|     return { | ||||
|       isOpen: false, | ||||
|       isDragging: false, | ||||
|       posY: 0, | ||||
|       trackHeight: 112 - 16 | ||||
|       isHovering: false, | ||||
|       posX: 0, | ||||
|       lastValue: 0.5, | ||||
|       isMute: false, | ||||
|       trackWidth: 112 - 20, | ||||
|       openTimeout: null | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
| @ -33,22 +40,45 @@ export default { | ||||
|         this.$emit('input', val) | ||||
|       } | ||||
|     }, | ||||
|     cursorTop() { | ||||
|       var top = this.trackHeight * this.volume | ||||
|       return top - 6 | ||||
|     cursorLeft() { | ||||
|       var left = this.trackWidth * this.volume | ||||
|       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: { | ||||
|     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) { | ||||
|       var diff = this.posY - e.y | ||||
|       this.posY = e.y | ||||
|       var diff = this.posX - e.x | ||||
|       this.posX = e.x | ||||
|       var volShift = 0 | ||||
|       if (diff < 0) { | ||||
|         // Volume up | ||||
|         volShift = diff / this.trackHeight | ||||
|         volShift = diff / this.trackWidth | ||||
|       } else { | ||||
|         // volume down | ||||
|         volShift = diff / this.trackHeight | ||||
|         volShift = diff / this.trackWidth | ||||
|       } | ||||
|       var newVol = this.volume - volShift | ||||
|       newVol = Math.min(Math.max(0, newVol), 1) | ||||
| @ -64,8 +94,8 @@ export default { | ||||
|     }, | ||||
|     mousedownTrack(e) { | ||||
|       this.isDragging = true | ||||
|       this.posY = e.y | ||||
|       var vol = e.offsetY / e.target.clientHeight | ||||
|       this.posX = e.x | ||||
|       var vol = e.offsetX / this.trackWidth | ||||
|       vol = Math.min(Math.max(vol, 0), 1) | ||||
|       this.volume = vol | ||||
|       document.body.addEventListener('mousemove', this.mousemove) | ||||
| @ -76,14 +106,24 @@ export default { | ||||
|       this.isOpen = false | ||||
|     }, | ||||
|     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) { | ||||
|       var vol = e.offsetY / e.target.clientHeight | ||||
|       var vol = e.offsetX / this.trackWidth | ||||
|       vol = Math.min(Math.max(vol, 0), 1) | ||||
|       this.volume = vol | ||||
|     } | ||||
|   }, | ||||
|   mounted() {} | ||||
|   mounted() { | ||||
|     if (this.value === 0) { | ||||
|       this.isMute = true | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
| @ -85,7 +85,7 @@ export default { | ||||
|       console.log('Search', this.lastSearch, this.search) | ||||
| 
 | ||||
|       this.searchResults = [] | ||||
|       this.processing = true | ||||
|       this.isProcessing = true | ||||
|       this.lastSearch = this.search | ||||
|       var results = await this.$axios.$get(`/api/find/search?title=${this.search}`).catch((error) => { | ||||
|         console.error('Failed', error) | ||||
| @ -96,7 +96,7 @@ export default { | ||||
|       }) | ||||
|       console.log('Got results', results) | ||||
|       this.searchResults = results | ||||
|       this.processing = false | ||||
|       this.isProcessing = false | ||||
|     }, | ||||
|     init() { | ||||
|       if (!this.audiobook.book || !this.audiobook.book.title) { | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf-client", | ||||
|   "version": "0.9.52", | ||||
|   "version": "0.9.54", | ||||
|   "description": "Audiobook manager and player", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -3,18 +3,27 @@ | ||||
|     <div class="w-full max-w-4xl mx-auto"> | ||||
|       <h1 class="text-2xl mb-2">Config</h1> | ||||
|       <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> | ||||
|       </div> | ||||
|       <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> | ||||
|         <div class="flex-grow" /> | ||||
|         <ui-btn color="success" @click="scan">Scan</ui-btn> | ||||
|       </div> | ||||
|       <div class="h-0.5 bg-primary bg-opacity-50 w-full" /> | ||||
|       <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> | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "audiobookshelf", | ||||
|   "version": "0.9.53", | ||||
|   "version": "0.9.54", | ||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks.", | ||||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|  | ||||
| @ -28,14 +28,11 @@ class HlsController { | ||||
| 
 | ||||
|   async streamFileRequest(req, res) { | ||||
|     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 exists = await fs.pathExists(fullFilePath) | ||||
|     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) | ||||
|       if (fileExt === '.ts') { | ||||
| @ -52,36 +49,16 @@ class HlsController { | ||||
|         } else { | ||||
|           var startTimeForReset = await stream.checkSegmentNumberRequest(segNum) | ||||
|           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`) | ||||
|             this.emitter('stream_reset', { | ||||
|               startTime: startTimeForReset, | ||||
|               streamId: stream.id | ||||
|             }) | ||||
|             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)
 | ||||
|     res.sendFile(fullFilePath) | ||||
|  | ||||
| @ -130,7 +130,6 @@ class Server { | ||||
|       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('/logout', this.logout.bind(this)) | ||||
|     app.get('/ping', (req, res) => { | ||||
| @ -165,7 +164,6 @@ class Server { | ||||
|       socket.on('close_stream', () => this.streamManager.closeStreamRequest(socket)) | ||||
|       socket.on('stream_update', (payload) => this.streamManager.streamUpdate(socket, payload)) | ||||
|       socket.on('test', () => { | ||||
|         console.log('Test Request from', 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) => { | ||||
|       Logger.info('[FFMPEG] Transcoding ended') | ||||
|       this.isTranscodeComplete = true | ||||
|       this.ffmpeg = null | ||||
|     }) | ||||
| 
 | ||||
|     this.ffmpeg.run() | ||||
|   } | ||||
| 
 | ||||
|   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) | ||||
|       // For very small fast load
 | ||||
|       if (!this.isClientInitialized) { | ||||
|         this.isClientInitialized = true | ||||
|         Logger.info(`[STREAM] ${this.id} notifying client that stream is ready`) | ||||
|         this.socket.emit('stream_open', this.toJSON()) | ||||
|       } | ||||
|       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.ffmpeg = null | ||||
|     }) | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user