mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-02 01:16:54 +02: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)
|
this.playerHandler.prepareOpenSession(session)
|
||||||
},
|
},
|
||||||
|
streamOpen(session) {
|
||||||
|
console.log(`[StreamContainer] Stream session open`, session)
|
||||||
|
},
|
||||||
streamClosed(streamId) {
|
streamClosed(streamId) {
|
||||||
// Stream was closed from the server
|
// Stream was closed from the server
|
||||||
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
|
||||||
|
@ -18,7 +18,7 @@ export default class CastPlayer extends EventEmitter {
|
|||||||
this.defaultPlaybackRate = 1
|
this.defaultPlaybackRate = 1
|
||||||
|
|
||||||
// TODO: Use canDisplayType on receiver to check mime types
|
// TODO: Use canDisplayType on receiver to check mime types
|
||||||
this.playableMimeTypes = {}
|
this.playableMimeTypes = []
|
||||||
|
|
||||||
this.coverUrl = ''
|
this.coverUrl = ''
|
||||||
this.castPlayerState = 'IDLE'
|
this.castPlayerState = 'IDLE'
|
||||||
|
@ -19,7 +19,7 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
this.defaultPlaybackRate = 1
|
this.defaultPlaybackRate = 1
|
||||||
|
|
||||||
this.playableMimeTypes = {}
|
this.playableMimeTypes = []
|
||||||
|
|
||||||
this.initialize()
|
this.initialize()
|
||||||
}
|
}
|
||||||
@ -46,11 +46,14 @@ export default class LocalPlayer extends EventEmitter {
|
|||||||
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
|
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
|
||||||
this.player.addEventListener('timeupdate', this.evtTimeupdate.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) => {
|
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() {
|
evtPlay() {
|
||||||
|
@ -138,7 +138,7 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
async prepare(forceTranscode = false) {
|
async prepare(forceTranscode = false) {
|
||||||
var payload = {
|
var payload = {
|
||||||
supportedMimeTypes: Object.keys(this.player.playableMimeTypes),
|
supportedMimeTypes: this.player.playableMimeTypes,
|
||||||
mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
|
mediaPlayer: this.isCasting ? 'chromecast' : 'html5',
|
||||||
forceTranscode,
|
forceTranscode,
|
||||||
forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
|
forceDirectPlay: this.isCasting // TODO: add transcode support for chromecast
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
const SupportedFileTypes = {
|
const SupportedFileTypes = {
|
||||||
image: ['png', 'jpg', 'jpeg', 'webp'],
|
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'],
|
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
info: ['nfo'],
|
info: ['nfo'],
|
||||||
text: ['txt'],
|
text: ['txt'],
|
||||||
|
@ -114,7 +114,7 @@ class Server {
|
|||||||
Logger.info('[Server] Init v' + version)
|
Logger.info('[Server] Init v' + version)
|
||||||
// TODO: Remove orphan streams from playback session manager
|
// TODO: Remove orphan streams from playback session manager
|
||||||
// await this.streamManager.ensureStreamsDir()
|
// await this.streamManager.ensureStreamsDir()
|
||||||
// await this.streamManager.removeOrphanStreams()
|
await this.playbackSessionManager.removeOrphanStreams()
|
||||||
await this.downloadManager.removeOrphanDownloads()
|
await this.downloadManager.removeOrphanDownloads()
|
||||||
|
|
||||||
if (version.localeCompare('2.0.0') < 0) { // Old version data model migration
|
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 PlaybackSession = require('../objects/PlaybackSession')
|
||||||
const Stream = require('../objects/Stream')
|
const Stream = require('../objects/Stream')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const fs = require('fs-extra')
|
||||||
|
|
||||||
class PlaybackSessionManager {
|
class PlaybackSessionManager {
|
||||||
constructor(db, emitter, clientEmitter) {
|
constructor(db, emitter, clientEmitter) {
|
||||||
@ -95,9 +96,12 @@ class PlaybackSessionManager {
|
|||||||
Logger.debug(`[PlaybackSessionManager] "${user.username}" starting stream session for item "${libraryItem.id}"`)
|
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))
|
var stream = new Stream(newPlaybackSession.id, this.StreamsPath, user, libraryItem, episodeId, userStartTime, this.clientEmitter.bind(this))
|
||||||
await stream.generatePlaylist()
|
await stream.generatePlaylist()
|
||||||
|
stream.start() // Start transcode
|
||||||
|
|
||||||
audioTracks = [stream.getAudioTrack()]
|
audioTracks = [stream.getAudioTrack()]
|
||||||
newPlaybackSession.stream = stream
|
newPlaybackSession.stream = stream
|
||||||
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
|
newPlaybackSession.playMethod = PlayMethod.TRANSCODE
|
||||||
|
|
||||||
stream.on('closed', () => {
|
stream.on('closed', () => {
|
||||||
Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}"`)
|
Logger.debug(`[PlaybackSessionManager] Stream closed for session "${newPlaybackSession.id}"`)
|
||||||
newPlaybackSession.stream = null
|
newPlaybackSession.stream = null
|
||||||
@ -179,5 +183,26 @@ class PlaybackSessionManager {
|
|||||||
this.sessions = this.sessions.filter(s => s.id !== sessionId)
|
this.sessions = this.sessions.filter(s => s.id !== sessionId)
|
||||||
Logger.debug(`[PlaybackSessionManager] Removed session "${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
|
module.exports = PlaybackSessionManager
|
@ -3,8 +3,9 @@ const EventEmitter = require('events')
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { getId, secondsToTimestamp } = require('../utils/index')
|
const { secondsToTimestamp } = require('../utils/index')
|
||||||
const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
const { writeConcatFile } = require('../utils/ffmpegHelpers')
|
||||||
|
const { AudioMimeType } = require('../utils/constants')
|
||||||
const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
|
const hlsPlaylistGenerator = require('../utils/hlsPlaylistGenerator')
|
||||||
const AudioTrack = require('./files/AudioTrack')
|
const AudioTrack = require('./files/AudioTrack')
|
||||||
|
|
||||||
@ -63,6 +64,18 @@ class Stream extends EventEmitter {
|
|||||||
if (!this.tracks.length) return null
|
if (!this.tracks.length) return null
|
||||||
return this.tracks[0].metadata.format
|
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() {
|
get userToken() {
|
||||||
return this.user.token
|
return this.user.token
|
||||||
}
|
}
|
||||||
@ -89,11 +102,6 @@ class Stream extends EventEmitter {
|
|||||||
get clientPlaylistUri() {
|
get clientPlaylistUri() {
|
||||||
return `/hls/${this.id}/output.m3u8`
|
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() {
|
get isAACEncodable() {
|
||||||
return ['mp4', 'm4a', 'm4b'].includes(this.tracksAudioFileType)
|
return ['mp4', 'm4a', 'm4b'].includes(this.tracksAudioFileType)
|
||||||
}
|
}
|
||||||
@ -137,7 +145,7 @@ class Stream extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generatePlaylist() {
|
async generatePlaylist() {
|
||||||
fs.ensureDirSync(this.streamPath)
|
await fs.ensureDir(this.streamPath)
|
||||||
await hlsPlaylistGenerator(this.playlistPath, 'output', this.totalDuration, this.segmentLength, this.hlsSegmentType, this.userToken)
|
await hlsPlaylistGenerator(this.playlistPath, 'output', this.totalDuration, this.segmentLength, this.hlsSegmentType, this.userToken)
|
||||||
return this.clientPlaylistUri
|
return this.clientPlaylistUri
|
||||||
}
|
}
|
||||||
@ -251,7 +259,7 @@ class Stream extends EventEmitter {
|
|||||||
|
|
||||||
const logLevel = process.env.NODE_ENV === 'production' ? 'error' : 'warning'
|
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([
|
this.ffmpeg.addOption([
|
||||||
`-loglevel ${logLevel}`,
|
`-loglevel ${logLevel}`,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const { isNullOrNaN } = require('../../utils/index')
|
const { isNullOrNaN } = require('../../utils/index')
|
||||||
|
const { AudioMimeType } = require('../../utils/constants')
|
||||||
const AudioMetaTags = require('../metadata/AudioMetaTags')
|
const AudioMetaTags = require('../metadata/AudioMetaTags')
|
||||||
const FileMetadata = require('../metadata/FileMetadata')
|
const FileMetadata = require('../metadata/FileMetadata')
|
||||||
|
|
||||||
@ -102,19 +103,12 @@ class AudioFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get mimeType() {
|
get mimeType() {
|
||||||
var ext = this.metadata.ext
|
var format = this.metadata.format.toUpperCase()
|
||||||
if (ext === '.mp3' || ext === '.m4b' || ext === '.m4a') {
|
if (AudioMimeType[format]) {
|
||||||
return 'audio/mpeg'
|
return AudioMimeType[format]
|
||||||
} else if (ext === '.mp4') {
|
} else {
|
||||||
return 'audio/mp4'
|
return AudioMimeType.MP3
|
||||||
} else if (ext === '.ogg') {
|
|
||||||
return 'audio/ogg'
|
|
||||||
} else if (ext === '.aac' || ext === '.m4p') {
|
|
||||||
return 'audio/aac'
|
|
||||||
} else if (ext === '.flac') {
|
|
||||||
return 'audio/flac'
|
|
||||||
}
|
}
|
||||||
return 'audio/mpeg'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New scanner creates AudioFile from AudioFileScanner
|
// New scanner creates AudioFile from AudioFileScanner
|
||||||
|
@ -32,3 +32,16 @@ module.exports.PlayMethod = {
|
|||||||
TRANSCODE: 2,
|
TRANSCODE: 2,
|
||||||
LOCAL: 3
|
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 = {
|
const globals = {
|
||||||
SupportedImageTypes: ['png', 'jpg', 'jpeg', 'webp'],
|
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'],
|
SupportedEbookTypes: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
TextFileTypes: ['txt', 'nfo'],
|
TextFileTypes: ['txt', 'nfo'],
|
||||||
MetadataFileTypes: ['opf', 'abs', 'xml']
|
MetadataFileTypes: ['opf', 'abs', 'xml']
|
||||||
|
Loading…
Reference in New Issue
Block a user