Support cover when downloading audio

This commit is contained in:
Chocobozzz 2024-08-08 10:33:41 +02:00
parent 6d28305582
commit 658241d8c6
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
5 changed files with 64 additions and 15 deletions

View File

@ -12,8 +12,10 @@ export class FFmpegContainer {
inputs: (Readable | string)[]
output: Writable
logError: boolean
coverPath?: string
}) {
const { inputs, output, logError } = options
const { inputs, output, logError, coverPath } = options
this.commandWrapper.buildCommand(inputs)
.outputOption('-c copy')
@ -21,6 +23,11 @@ export class FFmpegContainer {
.format('mp4')
.output(output)
if (coverPath) {
this.commandWrapper.getCommand()
.addInput(coverPath)
}
return this.commandWrapper.runCommand({ silent: !logError })
}
}

View File

@ -83,45 +83,46 @@ describe('Test generate download', function () {
return probeResBody(body)
}
function checkProbe (probe: FfprobeData, options: { hasVideo: boolean, hasAudio: boolean }) {
expect(probe.streams.some(s => s.codec_type === 'video')).to.equal(options.hasVideo)
function checkProbe (probe: FfprobeData, options: { hasVideo: boolean, hasAudio: boolean, hasImage: boolean }) {
expect(probe.streams.some(s => s.codec_type === 'video' && s.codec_name !== 'mjpeg')).to.equal(options.hasVideo)
expect(probe.streams.some(s => s.codec_type === 'audio')).to.equal(options.hasAudio)
expect(probe.streams.some(s => s.codec_name === 'mjpeg')).to.equal(options.hasImage)
}
it('Should generate a classic web video file', async function () {
const probe = await getProbe('common', video => [ getVideoFile(video.files).id ])
checkProbe(probe, { hasAudio: true, hasVideo: true })
checkProbe(probe, { hasAudio: true, hasVideo: true, hasImage: false })
})
it('Should generate a classic HLS file', async function () {
const probe = await getProbe('common', video => [ getVideoFile(getHLS(video).files).id ])
checkProbe(probe, { hasAudio: true, hasVideo: true })
checkProbe(probe, { hasAudio: true, hasVideo: true, hasImage: false })
})
it('Should generate an audio only web video file', async function () {
const probe = await getProbe('common', video => [ getAudioOnlyFile(video.files).id ])
checkProbe(probe, { hasAudio: true, hasVideo: false })
checkProbe(probe, { hasAudio: true, hasVideo: false, hasImage: true })
})
it('Should generate an audio only HLS file', async function () {
const probe = await getProbe('common', video => [ getAudioOnlyFile(getHLS(video).files).id ])
checkProbe(probe, { hasAudio: true, hasVideo: false })
checkProbe(probe, { hasAudio: true, hasVideo: false, hasImage: true })
})
it('Should generate a video only file', async function () {
const probe = await getProbe('splitted', video => [ getVideoFile(getHLS(video).files).id ])
checkProbe(probe, { hasAudio: false, hasVideo: true })
checkProbe(probe, { hasAudio: false, hasVideo: true, hasImage: false })
})
it('Should merge audio and video files', async function () {
const probe = await getProbe('splitted', video => [ getVideoFile(getHLS(video).files).id, getAudioFile(getHLS(video).files).id ])
checkProbe(probe, { hasAudio: true, hasVideo: true })
checkProbe(probe, { hasAudio: true, hasVideo: true, hasImage: false })
})
it('Should have cleaned the TMP directory', async function () {

View File

@ -1,5 +1,5 @@
import { forceNumber, maxBy } from '@peertube/peertube-core-utils'
import { FileStorage, HttpStatusCode, VideoStreamingPlaylistType } from '@peertube/peertube-models'
import { FileStorage, HttpStatusCode, VideoResolution, VideoStreamingPlaylistType } from '@peertube/peertube-models'
import { exists } from '@server/helpers/custom-validators/misc.js'
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
import { CONFIG } from '@server/initializers/config.js'
@ -244,7 +244,14 @@ async function downloadGeneratedVideoFile (req: express.Request, res: express.Re
if (!checkAllowResult(res, allowParameters, allowedResult)) return
const downloadFilename = buildDownloadFilename({ video, extname: maxBy(videoFiles, 'resolution').extname })
const maxResolutionFile = maxBy(videoFiles, 'resolution')
// Prefer m4a extension for the user if this is a mp4 audio file only
const extname = maxResolutionFile.resolution === VideoResolution.H_NOVIDEO && maxResolutionFile.extname === '.mp4'
? '.m4a'
: maxResolutionFile.extname
const downloadFilename = buildDownloadFilename({ video, extname })
res.setHeader('Content-disposition', `attachment; filename="${encodeURI(downloadFilename)}`)
await muxToMergeVideoFiles({ video, videoFiles, output: res })

View File

@ -191,7 +191,7 @@ export function isBinaryResponse (result: Response<any>) {
// ---------------------------------------------------------------------------
function buildGotOptions (options: PeerTubeRequestOptions): OptionsOfUnknownResponseBody {
const { activityPub, bodyKBLimit = 1000 } = options
const { activityPub, bodyKBLimit = 3000 } = options
const context = { bodyKBLimit, httpSignature: options.httpSignature }

View File

@ -16,7 +16,7 @@ import { CONFIG } from '@server/initializers/config.js'
import { MIMETYPES, REQUEST_TIMEOUTS } from '@server/initializers/constants.js'
import { VideoFileModel } from '@server/models/video/video-file.js'
import { VideoSourceModel } from '@server/models/video/video-source.js'
import { MVideo, MVideoFile, MVideoId, MVideoWithAllFiles } from '@server/types/models/index.js'
import { MVideo, MVideoFile, MVideoId, MVideoThumbnail, MVideoWithAllFiles } from '@server/types/models/index.js'
import { FfprobeData } from 'fluent-ffmpeg'
import { move, remove } from 'fs-extra/esm'
import { Readable, Writable } from 'stream'
@ -264,7 +264,7 @@ export async function saveNewOriginalFileIfNeeded (video: MVideo, videoFile: MVi
// ---------------------------------------------------------------------------
export async function muxToMergeVideoFiles (options: {
video: MVideo
video: MVideoThumbnail
videoFiles: MVideoFile[]
output: Writable
}) {
@ -274,9 +274,13 @@ export async function muxToMergeVideoFiles (options: {
const tmpDestinations: string[] = []
try {
let maxResolution = 0
for (const videoFile of videoFiles) {
if (!videoFile) continue
maxResolution = Math.max(maxResolution, videoFile.resolution)
const { input, isTmpDestination } = await buildMuxInput(video, videoFile)
inputs.push(input)
@ -284,6 +288,13 @@ export async function muxToMergeVideoFiles (options: {
if (isTmpDestination === true) tmpDestinations.push(input)
}
// Include cover to audio file?
const { coverPath, isTmpDestination } = maxResolution === 0
? await buildCoverInput(video)
: { coverPath: undefined, isTmpDestination: false }
if (coverPath && isTmpDestination) tmpDestinations.push(coverPath)
const inputsToLog = inputs.map(i => {
if (typeof i === 'string') return i
@ -293,7 +304,14 @@ export async function muxToMergeVideoFiles (options: {
logger.info(`Muxing files for video ${video.url}`, { inputs: inputsToLog, ...lTags(video.uuid) })
try {
await new FFmpegContainer(getFFmpegCommandWrapperOptions('vod')).mergeInputs({ inputs, output, logError: true })
await new FFmpegContainer(getFFmpegCommandWrapperOptions('vod')).mergeInputs({
inputs,
output,
logError: true,
// Include a cover if this is an audio file
coverPath
})
logger.info(`Mux ended for video ${video.url}`, { inputs: inputsToLog, ...lTags(video.uuid) })
} catch (err) {
@ -391,3 +409,19 @@ async function buildMuxInput (
return { input: stream, isTmpDestination: false }
}
async function buildCoverInput (video: MVideoThumbnail) {
const preview = video.getPreview()
if (video.isOwned()) return { coverPath: preview?.getPath() }
if (preview.fileUrl) {
const destination = VideoPathManager.Instance.buildTMPDestination(preview.filename)
await doRequestAndSaveToFile(preview.fileUrl, destination)
return { coverPath: destination, isTmpDestination: true }
}
return { coverPath: undefined }
}