Respect "transcode original resolution" for runner
This commit is contained in:
parent
1682b0bab0
commit
d4f21493e1
7 changed files with 83 additions and 15 deletions
|
@ -15,11 +15,13 @@ export type RunnerJobPrivatePayload =
|
||||||
export interface RunnerJobVODWebVideoTranscodingPrivatePayload {
|
export interface RunnerJobVODWebVideoTranscodingPrivatePayload {
|
||||||
videoUUID: string
|
videoUUID: string
|
||||||
isNewVideo: boolean
|
isNewVideo: boolean
|
||||||
|
deleteInputFileId: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunnerJobVODAudioMergeTranscodingPrivatePayload {
|
export interface RunnerJobVODAudioMergeTranscodingPrivatePayload {
|
||||||
videoUUID: string
|
videoUUID: string
|
||||||
isNewVideo: boolean
|
isNewVideo: boolean
|
||||||
|
deleteInputFileId: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RunnerJobVODHLSTranscodingPrivatePayload {
|
export interface RunnerJobVODHLSTranscodingPrivatePayload {
|
||||||
|
|
|
@ -232,6 +232,45 @@ describe('Test VOD transcoding in peertube-runner program', function () {
|
||||||
resolutions: [ 720, 480, 360, 240, 144, 0 ]
|
resolutions: [ 720, 480, 360, 240, 144, 0 ]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Should not generate an upper resolution than original file', async function () {
|
||||||
|
this.timeout(120_000)
|
||||||
|
|
||||||
|
await servers[0].config.updateExistingSubConfig({
|
||||||
|
newConfig: {
|
||||||
|
transcoding: {
|
||||||
|
enabled: true,
|
||||||
|
hls: { enabled: true },
|
||||||
|
webVideos: { enabled: true },
|
||||||
|
resolutions: {
|
||||||
|
'0p': false,
|
||||||
|
'144p': false,
|
||||||
|
'240p': true,
|
||||||
|
'360p': false,
|
||||||
|
'480p': true,
|
||||||
|
'720p': false,
|
||||||
|
'1080p': false,
|
||||||
|
'1440p': false,
|
||||||
|
'2160p': false
|
||||||
|
},
|
||||||
|
alwaysTranscodeOriginalResolution: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
|
||||||
|
await waitJobs(servers, { runnerJobs: true })
|
||||||
|
|
||||||
|
const video = await servers[0].videos.get({ id: uuid })
|
||||||
|
const hlsFiles = video.streamingPlaylists[0].files
|
||||||
|
|
||||||
|
expect(video.files).to.have.lengthOf(2)
|
||||||
|
expect(hlsFiles).to.have.lengthOf(2)
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-array-sort-compare
|
||||||
|
const resolutions = getAllFiles(video).map(f => f.resolution.id).sort()
|
||||||
|
expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ])
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
before(async function () {
|
before(async function () {
|
||||||
|
|
|
@ -36,6 +36,8 @@ async function completeWebVideoFilesCheck (options: {
|
||||||
|
|
||||||
const transcodingEnabled = serverConfig.transcoding.web_videos.enabled
|
const transcodingEnabled = serverConfig.transcoding.web_videos.enabled
|
||||||
|
|
||||||
|
expect(files).to.have.lengthOf(files.length)
|
||||||
|
|
||||||
for (const attributeFile of files) {
|
for (const attributeFile of files) {
|
||||||
const file = video.files.find(f => f.resolution.id === attributeFile.resolution)
|
const file = video.files.find(f => f.resolution.id === attributeFile.resolution)
|
||||||
expect(file, `resolution ${attributeFile.resolution} does not exist`).not.to.be.undefined
|
expect(file, `resolution ${attributeFile.resolution} does not exist`).not.to.be.undefined
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { VideoModel } from '@server/models/video/video.js'
|
||||||
import { MVideoFullLight } from '@server/types/models/index.js'
|
import { MVideoFullLight } from '@server/types/models/index.js'
|
||||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||||
import { RunnerJobVODAudioMergeTranscodingPrivatePayload, RunnerJobVODWebVideoTranscodingPrivatePayload } from '@peertube/peertube-models'
|
import { RunnerJobVODAudioMergeTranscodingPrivatePayload, RunnerJobVODWebVideoTranscodingPrivatePayload } from '@peertube/peertube-models'
|
||||||
|
import { lTags } from '@server/lib/object-storage/shared/logger.js'
|
||||||
|
|
||||||
export async function onVODWebVideoOrAudioMergeTranscodingJob (options: {
|
export async function onVODWebVideoOrAudioMergeTranscodingJob (options: {
|
||||||
video: MVideoFullLight
|
video: MVideoFullLight
|
||||||
|
@ -28,6 +29,23 @@ export async function onVODWebVideoOrAudioMergeTranscodingJob (options: {
|
||||||
videoOutputPath: newVideoFilePath
|
videoOutputPath: newVideoFilePath
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (privatePayload.deleteInputFileId) {
|
||||||
|
const inputFile = video.VideoFiles.find(f => f.id === privatePayload.deleteInputFileId)
|
||||||
|
|
||||||
|
if (inputFile) {
|
||||||
|
await video.removeWebVideoFile(inputFile)
|
||||||
|
await inputFile.destroy()
|
||||||
|
|
||||||
|
video.VideoFiles = video.VideoFiles.filter(f => f.id !== inputFile.id)
|
||||||
|
} else {
|
||||||
|
logger.error(
|
||||||
|
'Cannot delete input file %d of video %s: does not exist anymore',
|
||||||
|
privatePayload.deleteInputFileId, video.uuid,
|
||||||
|
{ ...lTags(video.uuid), privatePayload }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
|
await onTranscodingEnded({ isNewVideo: privatePayload.isNewVideo, moveVideoToNextState: true, video })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ type CreateOptions = {
|
||||||
resolution: number
|
resolution: number
|
||||||
fps: number
|
fps: number
|
||||||
priority: number
|
priority: number
|
||||||
|
deleteInputFileId: number | null
|
||||||
dependsOnRunnerJob?: MRunnerJob
|
dependsOnRunnerJob?: MRunnerJob
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJo
|
||||||
}
|
}
|
||||||
|
|
||||||
const privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload = {
|
const privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload = {
|
||||||
...pick(options, [ 'isNewVideo' ]),
|
...pick(options, [ 'isNewVideo', 'deleteInputFileId' ]),
|
||||||
|
|
||||||
videoUUID: video.uuid
|
videoUUID: video.uuid
|
||||||
}
|
}
|
||||||
|
@ -81,12 +82,6 @@ export class VODAudioMergeTranscodingJobHandler extends AbstractVODTranscodingJo
|
||||||
video.duration = await getVideoStreamDuration(videoFilePath)
|
video.duration = await getVideoStreamDuration(videoFilePath)
|
||||||
await video.save()
|
await video.save()
|
||||||
|
|
||||||
// We can remove the old audio file
|
|
||||||
const oldAudioFile = video.VideoFiles[0]
|
|
||||||
await video.removeWebVideoFile(oldAudioFile)
|
|
||||||
await oldAudioFile.destroy()
|
|
||||||
video.VideoFiles = []
|
|
||||||
|
|
||||||
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload })
|
await onVODWebVideoOrAudioMergeTranscodingJob({ video, videoFilePath, privatePayload })
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
@ -20,6 +20,7 @@ type CreateOptions = {
|
||||||
resolution: number
|
resolution: number
|
||||||
fps: number
|
fps: number
|
||||||
priority: number
|
priority: number
|
||||||
|
deleteInputFileId: number | null
|
||||||
dependsOnRunnerJob?: MRunnerJob
|
dependsOnRunnerJob?: MRunnerJob
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,7 +42,7 @@ export class VODWebVideoTranscodingJobHandler extends AbstractVODTranscodingJobH
|
||||||
}
|
}
|
||||||
|
|
||||||
const privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload = {
|
const privatePayload: RunnerJobVODWebVideoTranscodingPrivatePayload = {
|
||||||
...pick(options, [ 'isNewVideo' ]),
|
...pick(options, [ 'isNewVideo', 'deleteInputFileId' ]),
|
||||||
|
|
||||||
videoUUID: video.uuid
|
videoUUID: video.uuid
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { MUserId, MVideoFile, MVideoFullLight, MVideoWithFileThumbnail } from '@
|
||||||
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
import { MRunnerJob } from '@server/types/models/runners/index.js'
|
||||||
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@peertube/peertube-ffmpeg'
|
import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream, isAudioFile } from '@peertube/peertube-ffmpeg'
|
||||||
import { getTranscodingJobPriority } from '../../transcoding-priority.js'
|
import { getTranscodingJobPriority } from '../../transcoding-priority.js'
|
||||||
import { computeResolutionsToTranscode } from '../../transcoding-resolutions.js'
|
import { buildOriginalFileResolution, computeResolutionsToTranscode } from '../../transcoding-resolutions.js'
|
||||||
import { AbstractJobBuilder } from './abstract-job-builder.js'
|
import { AbstractJobBuilder } from './abstract-job-builder.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -52,16 +52,23 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
|
? VIDEO_TRANSCODING_FPS.AUDIO_MERGE // The first transcoding job will transcode to this FPS value
|
||||||
: await getVideoStreamFPS(videoFilePath, probe)
|
: await getVideoStreamFPS(videoFilePath, probe)
|
||||||
|
|
||||||
const maxResolution = await isAudioFile(videoFilePath, probe)
|
const isAudioInput = await isAudioFile(videoFilePath, probe)
|
||||||
|
const maxResolution = isAudioInput
|
||||||
? DEFAULT_AUDIO_RESOLUTION
|
? DEFAULT_AUDIO_RESOLUTION
|
||||||
: resolution
|
: buildOriginalFileResolution(resolution)
|
||||||
|
|
||||||
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
const fps = computeOutputFPS({ inputFPS, resolution: maxResolution })
|
||||||
const priority = await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
const priority = await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
|
|
||||||
|
const deleteInputFileId = isAudioInput || maxResolution !== resolution
|
||||||
|
? videoFile.id
|
||||||
|
: null
|
||||||
|
|
||||||
|
const jobPayload = { video, resolution: maxResolution, fps, isNewVideo, priority, deleteInputFileId }
|
||||||
|
|
||||||
const mainRunnerJob = videoFile.isAudio()
|
const mainRunnerJob = videoFile.isAudio()
|
||||||
? await new VODAudioMergeTranscodingJobHandler().create({ video, resolution: maxResolution, fps, isNewVideo, priority })
|
? await new VODAudioMergeTranscodingJobHandler().create(jobPayload)
|
||||||
: await new VODWebVideoTranscodingJobHandler().create({ video, resolution: maxResolution, fps, isNewVideo, priority })
|
: await new VODWebVideoTranscodingJobHandler().create(jobPayload)
|
||||||
|
|
||||||
if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
|
if (CONFIG.TRANSCODING.HLS.ENABLED === true) {
|
||||||
await new VODHLSTranscodingJobHandler().create({
|
await new VODHLSTranscodingJobHandler().create({
|
||||||
|
@ -110,12 +117,14 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
|
|
||||||
logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
|
logger.info('Manually creating transcoding jobs for %s.', transcodingType, { childrenResolutions, maxResolution })
|
||||||
|
|
||||||
|
const jobPayload = { video, resolution: maxResolution, fps: maxFPS, isNewVideo, priority, deleteInputFileId: null }
|
||||||
|
|
||||||
// Process the last resolution before the other ones to prevent concurrency issue
|
// Process the last resolution before the other ones to prevent concurrency issue
|
||||||
// Because low resolutions use the biggest one as ffmpeg input
|
// Because low resolutions use the biggest one as ffmpeg input
|
||||||
const mainJob = transcodingType === 'hls'
|
const mainJob = transcodingType === 'hls'
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
? await new VODHLSTranscodingJobHandler().create({ video, resolution: maxResolution, fps: maxFPS, isNewVideo, deleteWebVideoFiles: false, priority })
|
? await new VODHLSTranscodingJobHandler().create({ ...jobPayload, deleteWebVideoFiles: false })
|
||||||
: await new VODWebVideoTranscodingJobHandler().create({ video, resolution: maxResolution, fps: maxFPS, isNewVideo, priority })
|
: await new VODWebVideoTranscodingJobHandler().create(jobPayload)
|
||||||
|
|
||||||
for (const resolution of childrenResolutions) {
|
for (const resolution of childrenResolutions) {
|
||||||
const dependsOnRunnerJob = mainJob
|
const dependsOnRunnerJob = mainJob
|
||||||
|
@ -141,6 +150,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
fps,
|
fps,
|
||||||
isNewVideo,
|
isNewVideo,
|
||||||
dependsOnRunnerJob,
|
dependsOnRunnerJob,
|
||||||
|
deleteInputFileId: null,
|
||||||
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
|
@ -180,6 +190,7 @@ export class TranscodingRunnerJobBuilder extends AbstractJobBuilder {
|
||||||
fps,
|
fps,
|
||||||
isNewVideo,
|
isNewVideo,
|
||||||
dependsOnRunnerJob: mainRunnerJob,
|
dependsOnRunnerJob: mainRunnerJob,
|
||||||
|
deleteInputFileId: null,
|
||||||
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
priority: await getTranscodingJobPriority({ user, type: 'vod', fallback: 0 })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue