mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Add support for WMA and AIFF audio files #449, add remove orphan streams, clean up audio mime type logic
This commit is contained in:
		
							parent
							
								
									6d823f4e42
								
							
						
					
					
						commit
						5d305c96ad
					
				| @ -255,6 +255,9 @@ export default { | ||||
|       }) | ||||
|       this.playerHandler.prepareOpenSession(session) | ||||
|     }, | ||||
|     streamOpen(session) { | ||||
|       console.log(`[StreamContainer] Stream session open`, session) | ||||
|     }, | ||||
|     streamClosed(streamId) { | ||||
|       // Stream was closed from the server | ||||
|       if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) { | ||||
|  | ||||
| @ -18,7 +18,7 @@ export default class CastPlayer extends EventEmitter { | ||||
|     this.defaultPlaybackRate = 1 | ||||
| 
 | ||||
|     // TODO: Use canDisplayType on receiver to check mime types
 | ||||
|     this.playableMimeTypes = {} | ||||
|     this.playableMimeTypes = [] | ||||
| 
 | ||||
|     this.coverUrl = '' | ||||
|     this.castPlayerState = 'IDLE' | ||||
|  | ||||
| @ -19,7 +19,7 @@ export default class LocalPlayer extends EventEmitter { | ||||
|     this.playWhenReady = false | ||||
|     this.defaultPlaybackRate = 1 | ||||
| 
 | ||||
|     this.playableMimeTypes = {} | ||||
|     this.playableMimeTypes = [] | ||||
| 
 | ||||
|     this.initialize() | ||||
|   } | ||||
| @ -46,11 +46,14 @@ export default class LocalPlayer extends EventEmitter { | ||||
|     this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this)) | ||||
|     this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this)) | ||||
| 
 | ||||
|     var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac'] | ||||
|     var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff'] | ||||
|     var mimeTypeCanPlayMap = {} | ||||
|     mimeTypes.forEach((mt) => { | ||||
|       this.playableMimeTypes[mt] = this.player.canPlayType(mt) | ||||
|       var canPlay = this.player.canPlayType(mt) | ||||
|       mimeTypeCanPlayMap[mt] = canPlay | ||||
|       if (canPlay) this.playableMimeTypes.push(mt) | ||||
|     }) | ||||
|     console.log(`[LocalPlayer] Supported mime types`, this.playableMimeTypes) | ||||
|     console.log(`[LocalPlayer] Supported mime types`, mimeTypeCanPlayMap, this.playableMimeTypes) | ||||
|   } | ||||
| 
 | ||||
|   evtPlay() { | ||||
|  | ||||
| @ -138,7 +138,7 @@ export default class PlayerHandler { | ||||
| 
 | ||||
|   async prepare(forceTranscode = false) { | ||||
|     var payload = { | ||||
|       supportedMimeTypes: Object.keys(this.player.playableMimeTypes), | ||||
|       supportedMimeTypes: this.player.playableMimeTypes, | ||||
|       mediaPlayer: this.isCasting ? 'chromecast' : 'html5', | ||||
|       forceTranscode, | ||||
|       forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
 | ||||
|  | ||||
| @ -1,6 +1,6 @@ | ||||
| const SupportedFileTypes = { | ||||
|   image: ['png', 'jpg', 'jpeg', 'webp'], | ||||
|   audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4', 'aac'], | ||||
|   audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'mp4', 'aac', 'wma', 'aiff'], | ||||
|   ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'], | ||||
|   info: ['nfo'], | ||||
|   text: ['txt'], | ||||
|  | ||||
| @ -114,7 +114,7 @@ class Server { | ||||
|     Logger.info('[Server] Init v' + version) | ||||
|     // TODO: Remove orphan streams from playback session manager
 | ||||
|     // await this.streamManager.ensureStreamsDir()
 | ||||
|     // await this.streamManager.removeOrphanStreams()
 | ||||
|     await this.playbackSessionManager.removeOrphanStreams() | ||||
|     await this.downloadManager.removeOrphanDownloads() | ||||
| 
 | ||||
|     if (version.localeCompare('2.0.0') < 0) { // Old version data model migration
 | ||||
|  | ||||
| @ -3,6 +3,7 @@ const { PlayMethod } = require('../utils/constants') | ||||
| const PlaybackSession = require('../objects/PlaybackSession') | ||||
| const Stream = require('../objects/Stream') | ||||
| const Logger = require('../Logger') | ||||
| const fs = require('fs-extra') | ||||
| 
 | ||||
| class PlaybackSessionManager { | ||||
|   constructor(db, emitter, clientEmitter) { | ||||
| @ -95,9 +96,12 @@ class PlaybackSessionManager { | ||||
|       Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`) | ||||
|       var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this)) | ||||
|       await stream.generatePlaylist() | ||||
|       stream.start() // Start transcode
 | ||||
| 
 | ||||
|       audioTracks = [stream.getAudioTrack()] | ||||
|       newPlaybackSession.stream = stream | ||||
|       newPlaybackSession.playMethod = PlayMethod.TRANSCODE | ||||
| 
 | ||||
|       stream.on('closed', () => { | ||||
|         Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}"`) | ||||
|         newPlaybackSession.stream = null | ||||
| @ -179,5 +183,26 @@ class PlaybackSessionManager { | ||||
|     this.sessions = this.sessions.filter(s => s.id !== sessionId) | ||||
|     Logger.debug(`[PlaybackSessionManager] Removed session "${sessionId}"`) | ||||
|   } | ||||
| 
 | ||||
|   // Check for streams that are not in memory and remove
 | ||||
|   async removeOrphanStreams() { | ||||
|     await fs.ensureDir(this.StreamsPath) | ||||
|     try { | ||||
|       var streamsInPath = await fs.readdir(this.StreamsPath) | ||||
|       for (let i = 0; i < streamsInPath.length; i++) { | ||||
|         var streamId = streamsInPath[i] | ||||
|         if (streamId.startsWith('play_')) { // Make sure to only remove folders that are a stream
 | ||||
|           var session = this.sessions.find(se => se.id === streamId) | ||||
|           if (!session) { | ||||
|             var streamPath = Path.join(this.StreamsPath, streamId) | ||||
|             Logger.debug(`[PlaybackSessionManager] Removing orphan stream "${streamPath}"`) | ||||
|             await fs.remove(streamPath) | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } catch (error) { | ||||
|       Logger.error(`[PlaybackSessionManager] cleanOrphanStreams failed`, error) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| module.exports = PlaybackSessionManager | ||||
| @ -3,8 +3,9 @@ const EventEmitter = require('events') | ||||
| const Path = require('path') | ||||
| const fs = require('fs-extra') | ||||
| const Logger = require('../Logger') | ||||
| const { getId, secondsToTimestamp } = require('../utils/index') | ||||
| const { secondsToTimestamp } = require('../utils/index') | ||||
| const { writeConcatFile } = require('../utils/ffmpegHelpers') | ||||
| const { AudioMimeType } = require('../utils/constants') | ||||
| const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator') | ||||
| const AudioTrack = require('./files/AudioTrack') | ||||
| 
 | ||||
| @ -63,6 +64,18 @@ class Stream extends EventEmitter { | ||||
|     if (!this.tracks.length) return null | ||||
|     return this.tracks[0].metadata.format | ||||
|   } | ||||
|   get tracksMimeType() { | ||||
|     if (!this.tracks.length) return null | ||||
|     return this.tracks[0].mimeType | ||||
|   } | ||||
|   get mimeTypesToForceAAC() { | ||||
|     return [ | ||||
|       AudioMimeType.FLAC, | ||||
|       AudioMimeType.OPUS, | ||||
|       AudioMimeType.WMA, | ||||
|       AudioMimeType.AIFF | ||||
|     ] | ||||
|   } | ||||
|   get userToken() { | ||||
|     return this.user.token | ||||
|   } | ||||
| @ -89,11 +102,6 @@ class Stream extends EventEmitter { | ||||
|   get clientPlaylistUri() { | ||||
|     return `/hls/${this.id}/output.m3u8` | ||||
|   } | ||||
|   // get clientProgress() {
 | ||||
|   //   if (!this.clientCurrentTime) return 0
 | ||||
|   //   var prog = Math.min(1, this.clientCurrentTime / this.totalDuration)
 | ||||
|   //   return Number(prog.toFixed(3))
 | ||||
|   // }
 | ||||
|   get isAACEncodable() { | ||||
|     return ['mp4', 'm4a', 'm4b'].includes(this.tracksAudioFileType) | ||||
|   } | ||||
| @ -137,7 +145,7 @@ class Stream extends EventEmitter { | ||||
|   } | ||||
| 
 | ||||
|   async generatePlaylist() { | ||||
|     fs.ensureDirSync(this.streamPath) | ||||
|     await fs.ensureDir(this.streamPath) | ||||
|     await hlsPlaylistGenerator(this.playlistPath, 'output', this.totalDuration, this.segmentLength, this.hlsSegmentType, this.userToken) | ||||
|     return this.clientPlaylistUri | ||||
|   } | ||||
| @ -251,7 +259,7 @@ class Stream extends EventEmitter { | ||||
| 
 | ||||
|     const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning' | ||||
| 
 | ||||
|     const audioCodec = (this.tracksAudioFileType === 'flac' || this.tracksAudioFileType === 'opus' || this.transcodeForceAAC) ? 'aac' : 'copy' | ||||
|     const audioCodec = (this.mimeTypesToForceAAC.includes(this.tracksMimeType) || this.transcodeForceAAC) ? 'aac' : 'copy' | ||||
| 
 | ||||
|     this.ffmpeg.addOption([ | ||||
|       `-loglevel ${logLevel}`, | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| const { isNullOrNaN } = require('../../utils/index') | ||||
| const { AudioMimeType } = require('../../utils/constants') | ||||
| const AudioMetaTags = require('../metadata/AudioMetaTags') | ||||
| const FileMetadata = require('../metadata/FileMetadata') | ||||
| 
 | ||||
| @ -102,19 +103,12 @@ class AudioFile { | ||||
|   } | ||||
| 
 | ||||
|   get mimeType() { | ||||
|     var ext = this.metadata.ext | ||||
|     if (ext === '.mp3' || ext === '.m4b' || ext === '.m4a') { | ||||
|       return 'audio/mpeg' | ||||
|     } else if (ext === '.mp4') { | ||||
|       return 'audio/mp4' | ||||
|     } else if (ext === '.ogg') { | ||||
|       return 'audio/ogg' | ||||
|     } else if (ext === '.aac' || ext === '.m4p') { | ||||
|       return 'audio/aac' | ||||
|     } else if (ext === '.flac') { | ||||
|       return 'audio/flac' | ||||
|     var format = this.metadata.format.toUpperCase() | ||||
|     if (AudioMimeType[format]) { | ||||
|       return AudioMimeType[format] | ||||
|     } else { | ||||
|       return AudioMimeType.MP3 | ||||
|     } | ||||
|     return 'audio/mpeg' | ||||
|   } | ||||
| 
 | ||||
|   // New scanner creates AudioFile from AudioFileScanner
 | ||||
|  | ||||
| @ -31,4 +31,17 @@ module.exports.PlayMethod = { | ||||
|   DIRECTSTREAM: 1, | ||||
|   TRANSCODE: 2, | ||||
|   LOCAL: 3 | ||||
| } | ||||
| 
 | ||||
| module.exports.AudioMimeType = { | ||||
|   MP3: 'audio/mpeg', | ||||
|   M4B: 'audio/mpeg', | ||||
|   M4A: 'audio/mpeg', | ||||
|   MP4: 'audio/mp4', | ||||
|   OGG: 'audio/ogg', | ||||
|   OPUS: 'audio/ogg', | ||||
|   AAC: 'audio/aac', | ||||
|   FLAC: 'audio/flac', | ||||
|   WMA: 'audio/x-ms-wma', | ||||
|   AIFF: 'audio/x-aiff' | ||||
| } | ||||
| @ -1,6 +1,6 @@ | ||||
| const globals = { | ||||
|   SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'], | ||||
|   SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'mp4', 'aac'], | ||||
|   SupportedAudioTypes: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'mp4', 'aac', 'wma', 'aiff'], | ||||
|   SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'], | ||||
|   TextFileTypes: ['txt', 'nfo'], | ||||
|   MetadataFileTypes: ['opf', 'abs', 'xml'] | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user