mirror of https://github.com/Chocobozzz/PeerTube
Support cover when downloading audio
parent
6d28305582
commit
658241d8c6
|
@ -12,8 +12,10 @@ export class FFmpegContainer {
|
||||||
inputs: (Readable | string)[]
|
inputs: (Readable | string)[]
|
||||||
output: Writable
|
output: Writable
|
||||||
logError: boolean
|
logError: boolean
|
||||||
|
|
||||||
|
coverPath?: string
|
||||||
}) {
|
}) {
|
||||||
const { inputs, output, logError } = options
|
const { inputs, output, logError, coverPath } = options
|
||||||
|
|
||||||
this.commandWrapper.buildCommand(inputs)
|
this.commandWrapper.buildCommand(inputs)
|
||||||
.outputOption('-c copy')
|
.outputOption('-c copy')
|
||||||
|
@ -21,6 +23,11 @@ export class FFmpegContainer {
|
||||||
.format('mp4')
|
.format('mp4')
|
||||||
.output(output)
|
.output(output)
|
||||||
|
|
||||||
|
if (coverPath) {
|
||||||
|
this.commandWrapper.getCommand()
|
||||||
|
.addInput(coverPath)
|
||||||
|
}
|
||||||
|
|
||||||
return this.commandWrapper.runCommand({ silent: !logError })
|
return this.commandWrapper.runCommand({ silent: !logError })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,45 +83,46 @@ describe('Test generate download', function () {
|
||||||
return probeResBody(body)
|
return probeResBody(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkProbe (probe: FfprobeData, options: { hasVideo: boolean, hasAudio: boolean }) {
|
function checkProbe (probe: FfprobeData, options: { hasVideo: boolean, hasAudio: boolean, hasImage: boolean }) {
|
||||||
expect(probe.streams.some(s => s.codec_type === 'video')).to.equal(options.hasVideo)
|
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_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 () {
|
it('Should generate a classic web video file', async function () {
|
||||||
const probe = await getProbe('common', video => [ getVideoFile(video.files).id ])
|
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 () {
|
it('Should generate a classic HLS file', async function () {
|
||||||
const probe = await getProbe('common', video => [ getVideoFile(getHLS(video).files).id ])
|
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 () {
|
it('Should generate an audio only web video file', async function () {
|
||||||
const probe = await getProbe('common', video => [ getAudioOnlyFile(video.files).id ])
|
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 () {
|
it('Should generate an audio only HLS file', async function () {
|
||||||
const probe = await getProbe('common', video => [ getAudioOnlyFile(getHLS(video).files).id ])
|
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 () {
|
it('Should generate a video only file', async function () {
|
||||||
const probe = await getProbe('splitted', video => [ getVideoFile(getHLS(video).files).id ])
|
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 () {
|
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 ])
|
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 () {
|
it('Should have cleaned the TMP directory', async function () {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { forceNumber, maxBy } from '@peertube/peertube-core-utils'
|
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 { exists } from '@server/helpers/custom-validators/misc.js'
|
||||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||||
import { CONFIG } from '@server/initializers/config.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
|
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)}`)
|
res.setHeader('Content-disposition', `attachment; filename="${encodeURI(downloadFilename)}`)
|
||||||
|
|
||||||
await muxToMergeVideoFiles({ video, videoFiles, output: res })
|
await muxToMergeVideoFiles({ video, videoFiles, output: res })
|
||||||
|
|
|
@ -191,7 +191,7 @@ export function isBinaryResponse (result: Response<any>) {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
function buildGotOptions (options: PeerTubeRequestOptions): OptionsOfUnknownResponseBody {
|
function buildGotOptions (options: PeerTubeRequestOptions): OptionsOfUnknownResponseBody {
|
||||||
const { activityPub, bodyKBLimit = 1000 } = options
|
const { activityPub, bodyKBLimit = 3000 } = options
|
||||||
|
|
||||||
const context = { bodyKBLimit, httpSignature: options.httpSignature }
|
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 { MIMETYPES, REQUEST_TIMEOUTS } from '@server/initializers/constants.js'
|
||||||
import { VideoFileModel } from '@server/models/video/video-file.js'
|
import { VideoFileModel } from '@server/models/video/video-file.js'
|
||||||
import { VideoSourceModel } from '@server/models/video/video-source.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 { FfprobeData } from 'fluent-ffmpeg'
|
||||||
import { move, remove } from 'fs-extra/esm'
|
import { move, remove } from 'fs-extra/esm'
|
||||||
import { Readable, Writable } from 'stream'
|
import { Readable, Writable } from 'stream'
|
||||||
|
@ -264,7 +264,7 @@ export async function saveNewOriginalFileIfNeeded (video: MVideo, videoFile: MVi
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
export async function muxToMergeVideoFiles (options: {
|
export async function muxToMergeVideoFiles (options: {
|
||||||
video: MVideo
|
video: MVideoThumbnail
|
||||||
videoFiles: MVideoFile[]
|
videoFiles: MVideoFile[]
|
||||||
output: Writable
|
output: Writable
|
||||||
}) {
|
}) {
|
||||||
|
@ -274,9 +274,13 @@ export async function muxToMergeVideoFiles (options: {
|
||||||
const tmpDestinations: string[] = []
|
const tmpDestinations: string[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
let maxResolution = 0
|
||||||
|
|
||||||
for (const videoFile of videoFiles) {
|
for (const videoFile of videoFiles) {
|
||||||
if (!videoFile) continue
|
if (!videoFile) continue
|
||||||
|
|
||||||
|
maxResolution = Math.max(maxResolution, videoFile.resolution)
|
||||||
|
|
||||||
const { input, isTmpDestination } = await buildMuxInput(video, videoFile)
|
const { input, isTmpDestination } = await buildMuxInput(video, videoFile)
|
||||||
|
|
||||||
inputs.push(input)
|
inputs.push(input)
|
||||||
|
@ -284,6 +288,13 @@ export async function muxToMergeVideoFiles (options: {
|
||||||
if (isTmpDestination === true) tmpDestinations.push(input)
|
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 => {
|
const inputsToLog = inputs.map(i => {
|
||||||
if (typeof i === 'string') return 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) })
|
logger.info(`Muxing files for video ${video.url}`, { inputs: inputsToLog, ...lTags(video.uuid) })
|
||||||
|
|
||||||
try {
|
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) })
|
logger.info(`Mux ended for video ${video.url}`, { inputs: inputsToLog, ...lTags(video.uuid) })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -391,3 +409,19 @@ async function buildMuxInput (
|
||||||
|
|
||||||
return { input: stream, isTmpDestination: false }
|
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