mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-11-04 03:17:00 -05: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
 | 
			
		||||
 | 
			
		||||
@ -32,3 +32,16 @@ module.exports.PlayMethod = {
 | 
			
		||||
  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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user