From 74652e2e54a93e9ed775058c2ae6174eebabc620 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 29 Apr 2023 14:57:51 -0500 Subject: [PATCH] API route to generate waveform images --- client/pages/audiobook/_id/chapters.vue | 16 ++++++++++++++- server/controllers/ToolsController.js | 27 +++++++++++++++++++++++++ server/routers/ApiRouter.js | 1 + server/utils/ffmpegHelpers.js | 23 +++++++++++++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/client/pages/audiobook/_id/chapters.vue b/client/pages/audiobook/_id/chapters.vue index 52cb1bbc..f0d83c0b 100644 --- a/client/pages/audiobook/_id/chapters.vue +++ b/client/pages/audiobook/_id/chapters.vue @@ -94,9 +94,16 @@ error_outline + + +
+ +
@@ -246,7 +253,8 @@ export default { chapterData: null, showSecondInputs: false, audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'], - hasChanges: false + hasChanges: false, + showWaveform: {} } }, computed: { @@ -256,6 +264,9 @@ export default { userToken() { return this.$store.getters['user/getToken'] }, + baseUrl() { + return process.env.serverUrl + }, media() { return this.libraryItem.media || {} }, @@ -288,6 +299,9 @@ export default { } }, methods: { + setShowWaveform(chapterId) { + this.$set(this.showWaveform, chapterId, true) + }, setChaptersFromTracks() { let currentStartTime = 0 let index = 0 diff --git a/server/controllers/ToolsController.js b/server/controllers/ToolsController.js index 3f21c5dd..7302f01a 100644 --- a/server/controllers/ToolsController.js +++ b/server/controllers/ToolsController.js @@ -1,4 +1,5 @@ const Logger = require('../Logger') +const ffmpegHelpers = require('../utils/ffmpegHelpers') class ToolsController { constructor() { } @@ -98,6 +99,32 @@ class ToolsController { res.sendStatus(200) } + getAudioFileWaveform(req, res) { + let start = Number(req.query.start || 0) + let end = Number(req.query.end || 0) + if (isNaN(start) || isNaN(end) || start < 0 || end > req.libraryItem.media.duration || end <= start || end - start < 5) { + return res.status(400).send('Invalid start/end query params') + } + + const paths = [] + let currentTime = 0 + let startOffset = 0 + for (const track of req.libraryItem.media.tracks) { + currentTime += track.duration + if (currentTime > start) { + if (!paths.length) startOffset = track.startOffset + paths.push(track.metadata.path) + } + if (currentTime > end) { + break + } + } + start -= startOffset + end -= startOffset + + ffmpegHelpers.generateWaveform(paths, start, end, res) + } + middleware(req, res, next) { if (!req.user.isAdminOrUp) { Logger.error(`[LibraryItemController] Non-root user attempted to access tools route`, req.user) diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index 1b2df155..6e34c2bd 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -275,6 +275,7 @@ class ApiRouter { this.router.delete('/tools/item/:id/encode-m4b', ToolsController.middleware.bind(this), ToolsController.cancelM4bEncode.bind(this)) this.router.post('/tools/item/:id/embed-metadata', ToolsController.middleware.bind(this), ToolsController.embedAudioFileMetadata.bind(this)) this.router.post('/tools/batch/embed-metadata', ToolsController.middleware.bind(this), ToolsController.batchEmbedMetadata.bind(this)) + this.router.get('/tools/item/:id/waveform', ToolsController.middleware.bind(this), ToolsController.getAudioFileWaveform.bind(this)) // // RSS Feed Routes (Admin and up) diff --git a/server/utils/ffmpegHelpers.js b/server/utils/ffmpegHelpers.js index b82f9784..4190ffd9 100644 --- a/server/utils/ffmpegHelpers.js +++ b/server/utils/ffmpegHelpers.js @@ -152,3 +152,26 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => { ffmpeg.run() }) } + +module.exports.generateWaveform = (filepaths, start, end, res) => { + let ffmpeg = null + if (filepaths.length === 1) ffmpeg = Ffmpeg(filepaths[0]) + else { + ffmpeg = Ffmpeg(`concat:${filepaths.join('|')}`) + } + ffmpeg.inputOptions('-ss', start) + ffmpeg.inputOptions('-to', end) + ffmpeg.complexFilter('aformat=channel_layouts=mono,showwavespic=s=1280x240') + ffmpeg.frames(1) + ffmpeg.format('image2pipe') + ffmpeg.on('start', (cmd) => { + Logger.debug(`[FfmpegHelpers] generateWaveform: Cmd: ${cmd}`) + }) + ffmpeg.on('error', (error) => { + Logger.error(`[FfmpegHelpers] generateWaveform: Error`, error) + }).on('end', () => { + Logger.debug(`[FfmpegHelpers] generateWaveform finished`) + }).pipe(res, { + end: true + }) +} \ No newline at end of file