diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index 47a1abc7..e5890e11 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -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) { diff --git a/client/players/CastPlayer.js b/client/players/CastPlayer.js index 41326669..8937a108 100644 --- a/client/players/CastPlayer.js +++ b/client/players/CastPlayer.js @@ -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' diff --git a/client/players/LocalPlayer.js b/client/players/LocalPlayer.js index 7c9086c4..db0798c0 100644 --- a/client/players/LocalPlayer.js +++ b/client/players/LocalPlayer.js @@ -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() { diff --git a/client/players/PlayerHandler.js b/client/players/PlayerHandler.js index 78ded80a..26cb26bb 100644 --- a/client/players/PlayerHandler.js +++ b/client/players/PlayerHandler.js @@ -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 diff --git a/client/plugins/constants.js b/client/plugins/constants.js index 4dd85ca5..e7081e99 100644 --- a/client/plugins/constants.js +++ b/client/plugins/constants.js @@ -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'], diff --git a/server/Server.js b/server/Server.js index 6efdba34..5d4180ac 100644 --- a/server/Server.js +++ b/server/Server.js @@ -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 diff --git a/server/managers/PlaybackSessionManager.js b/server/managers/PlaybackSessionManager.js index faa941ca..45f010fa 100644 --- a/server/managers/PlaybackSessionManager.js +++ b/server/managers/PlaybackSessionManager.js @@ -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 \ No newline at end of file diff --git a/server/objects/Stream.js b/server/objects/Stream.js index abe61fde..f60c744a 100644 --- a/server/objects/Stream.js +++ b/server/objects/Stream.js @@ -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}`, diff --git a/server/objects/files/AudioFile.js b/server/objects/files/AudioFile.js index 0ecc5fb1..d20dc367 100644 --- a/server/objects/files/AudioFile.js +++ b/server/objects/files/AudioFile.js @@ -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 diff --git a/server/utils/constants.js b/server/utils/constants.js index 51f7adb5..6a5283f7 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -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' } \ No newline at end of file diff --git a/server/utils/globals.js b/server/utils/globals.js index 781a0804..9b70d555 100644 --- a/server/utils/globals.js +++ b/server/utils/globals.js @@ -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']