Support cover when downloading audio
This commit is contained in:
parent
6d28305582
commit
658241d8c6
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 () {
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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 }
|
||||
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue