Add support for WMA and AIFF audio files #449, add remove orphan streams, clean up audio mime type logic

This commit is contained in:
advplyr 2022-04-16 12:37:10 -05:00
parent 6d823f4e42
commit 5d305c96ad
11 changed files with 75 additions and 29 deletions

View File

@ -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) {

View File

@ -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'

View File

@ -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() {

View File

@ -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

View File

@ -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'],

View File

@ -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

View File

@ -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

View File

@ -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}`,

View File

@ -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

View File

@ -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'
}

View File

@ -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']