Prefer video studio instead of video edition

Clearer and easier to find in the project
pull/5593/head
Chocobozzz 2023-05-04 15:55:51 +02:00 committed by Chocobozzz
parent 5e47f6ab98
commit ab14f0e0dc
26 changed files with 151 additions and 276 deletions

View File

@ -1,7 +1,7 @@
import { logger } from 'packages/peertube-runner/shared/logger' import { logger } from 'packages/peertube-runner/shared/logger'
import { import {
RunnerJobLiveRTMPHLSTranscodingPayload, RunnerJobLiveRTMPHLSTranscodingPayload,
RunnerJobVideoEditionTranscodingPayload, RunnerJobStudioTranscodingPayload,
RunnerJobVODAudioMergeTranscodingPayload, RunnerJobVODAudioMergeTranscodingPayload,
RunnerJobVODHLSTranscodingPayload, RunnerJobVODHLSTranscodingPayload,
RunnerJobVODWebVideoTranscodingPayload RunnerJobVODWebVideoTranscodingPayload
@ -23,8 +23,8 @@ export async function processJob (options: ProcessOptions) {
await processHLSTranscoding(options as ProcessOptions<RunnerJobVODHLSTranscodingPayload>) await processHLSTranscoding(options as ProcessOptions<RunnerJobVODHLSTranscodingPayload>)
} else if (job.type === 'live-rtmp-hls-transcoding') { } else if (job.type === 'live-rtmp-hls-transcoding') {
await new ProcessLiveRTMPHLSTranscoding(options as ProcessOptions<RunnerJobLiveRTMPHLSTranscodingPayload>).process() await new ProcessLiveRTMPHLSTranscoding(options as ProcessOptions<RunnerJobLiveRTMPHLSTranscodingPayload>).process()
} else if (job.type === 'video-edition-transcoding') { } else if (job.type === 'video-studio-transcoding') {
await processStudioTranscoding(options as ProcessOptions<RunnerJobVideoEditionTranscodingPayload>) await processStudioTranscoding(options as ProcessOptions<RunnerJobStudioTranscodingPayload>)
} else { } else {
logger.error(`Unknown job ${job.type} to process`) logger.error(`Unknown job ${job.type} to process`)
return return

View File

@ -1,13 +1,12 @@
import { remove } from 'fs-extra'
import { throttle } from 'lodash' import { throttle } from 'lodash'
import { ConfigManager, downloadFile, logger } from 'packages/peertube-runner/shared' import { ConfigManager, downloadFile, logger } from 'packages/peertube-runner/shared'
import { join } from 'path' import { join } from 'path'
import { buildUUID } from '@shared/extra-utils' import { buildUUID } from '@shared/extra-utils'
import { FFmpegEdition, FFmpegLive, FFmpegVOD } from '@shared/ffmpeg' import { FFmpegEdition, FFmpegLive, FFmpegVOD, getDefaultAvailableEncoders, getDefaultEncodersToTry } from '@shared/ffmpeg'
import { RunnerJob, RunnerJobPayload } from '@shared/models' import { RunnerJob, RunnerJobPayload } from '@shared/models'
import { PeerTubeServer } from '@shared/server-commands' import { PeerTubeServer } from '@shared/server-commands'
import { getTranscodingLogger } from './transcoding-logger' import { getTranscodingLogger } from './transcoding-logger'
import { getAvailableEncoders, getEncodersToTry } from './transcoding-profiles'
import { remove } from 'fs-extra'
export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string } export type JobWithToken <T extends RunnerJobPayload = RunnerJobPayload> = RunnerJob<T> & { jobToken: string }
@ -92,8 +91,8 @@ function getCommonFFmpegOptions () {
tmpDirectory: ConfigManager.Instance.getTranscodingDirectory(), tmpDirectory: ConfigManager.Instance.getTranscodingDirectory(),
profile: 'default', profile: 'default',
availableEncoders: { availableEncoders: {
available: getAvailableEncoders(), available: getDefaultAvailableEncoders(),
encodersToTry: getEncodersToTry() encodersToTry: getDefaultEncodersToTry()
}, },
logger: getTranscodingLogger() logger: getTranscodingLogger()
} }

View File

@ -1,4 +1,3 @@
export * from './common' export * from './common'
export * from './process-vod' export * from './process-vod'
export * from './transcoding-logger' export * from './transcoding-logger'
export * from './transcoding-profiles'

View File

@ -1,11 +1,11 @@
import { remove } from 'fs-extra' import { remove } from 'fs-extra'
import { pick } from 'lodash' import { pick } from 'lodash'
import { logger } from 'packages/peertube-runner/shared' import { logger } from 'packages/peertube-runner/shared'
import { extname, join } from 'path' import { join } from 'path'
import { buildUUID } from '@shared/extra-utils' import { buildUUID } from '@shared/extra-utils'
import { import {
RunnerJobVideoEditionTranscodingPayload, RunnerJobStudioTranscodingPayload,
VideoEditionTranscodingSuccess, VideoStudioTranscodingSuccess,
VideoStudioTask, VideoStudioTask,
VideoStudioTaskCutPayload, VideoStudioTaskCutPayload,
VideoStudioTaskIntroPayload, VideoStudioTaskIntroPayload,
@ -16,7 +16,7 @@ import {
import { ConfigManager } from '../../../shared/config-manager' import { ConfigManager } from '../../../shared/config-manager'
import { buildFFmpegEdition, downloadInputFile, JobWithToken, ProcessOptions } from './common' import { buildFFmpegEdition, downloadInputFile, JobWithToken, ProcessOptions } from './common'
export async function processStudioTranscoding (options: ProcessOptions<RunnerJobVideoEditionTranscodingPayload>) { export async function processStudioTranscoding (options: ProcessOptions<RunnerJobStudioTranscodingPayload>) {
const { server, job, runnerToken } = options const { server, job, runnerToken } = options
const payload = job.payload const payload = job.payload
@ -43,7 +43,7 @@ export async function processStudioTranscoding (options: ProcessOptions<RunnerJo
tmpInputFilePath = outputPath tmpInputFilePath = outputPath
} }
const successBody: VideoEditionTranscodingSuccess = { const successBody: VideoStudioTranscodingSuccess = {
videoFile: outputPath videoFile: outputPath
} }
@ -94,7 +94,8 @@ async function processAddIntroOutro (options: TaskProcessorOptions<VideoStudioTa
const introOutroPath = await downloadInputFile({ url: task.options.file, runnerToken, job }) const introOutroPath = await downloadInputFile({ url: task.options.file, runnerToken, job })
return buildFFmpegEdition().addIntroOutro({ try {
await buildFFmpegEdition().addIntroOutro({
...pick(options, [ 'inputPath', 'outputPath' ]), ...pick(options, [ 'inputPath', 'outputPath' ]),
introOutroPath, introOutroPath,
@ -102,6 +103,9 @@ async function processAddIntroOutro (options: TaskProcessorOptions<VideoStudioTa
? 'intro' ? 'intro'
: 'outro' : 'outro'
}) })
} finally {
await remove(introOutroPath)
}
} }
function processCut (options: TaskProcessorOptions<VideoStudioTaskCutPayload>) { function processCut (options: TaskProcessorOptions<VideoStudioTaskCutPayload>) {
@ -124,7 +128,8 @@ async function processAddWatermark (options: TaskProcessorOptions<VideoStudioTas
const watermarkPath = await downloadInputFile({ url: task.options.file, runnerToken, job }) const watermarkPath = await downloadInputFile({ url: task.options.file, runnerToken, job })
return buildFFmpegEdition().addWatermark({ try {
await buildFFmpegEdition().addWatermark({
...pick(options, [ 'inputPath', 'outputPath' ]), ...pick(options, [ 'inputPath', 'outputPath' ]),
watermarkPath, watermarkPath,
@ -135,4 +140,7 @@ async function processAddWatermark (options: TaskProcessorOptions<VideoStudioTas
verticalMarginRatio: task.options.verticalMarginRatio verticalMarginRatio: task.options.verticalMarginRatio
} }
}) })
} finally {
await remove(watermarkPath)
}
} }

View File

@ -22,6 +22,7 @@ export async function processWebVideoTranscoding (options: ProcessOptions<Runner
const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`) const outputPath = join(ConfigManager.Instance.getTranscodingDirectory(), `output-${buildUUID()}.mp4`)
try {
await ffmpegVod.transcode({ await ffmpegVod.transcode({
type: 'video', type: 'video',
@ -45,9 +46,11 @@ export async function processWebVideoTranscoding (options: ProcessOptions<Runner
runnerToken, runnerToken,
payload: successBody payload: successBody
}) })
} finally {
await remove(inputPath)
await remove(outputPath) await remove(outputPath)
} }
}
export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVODHLSTranscodingPayload>) { export async function processHLSTranscoding (options: ProcessOptions<RunnerJobVODHLSTranscodingPayload>) {
const { server, job, runnerToken } = options const { server, job, runnerToken } = options
@ -105,6 +108,7 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken }) const ffmpegVod = buildFFmpegVOD({ job, server, runnerToken })
try {
await ffmpegVod.transcode({ await ffmpegVod.transcode({
type: 'merge-audio', type: 'merge-audio',
@ -129,6 +133,9 @@ export async function processAudioMergeTranscoding (options: ProcessOptions<Runn
runnerToken, runnerToken,
payload: successBody payload: successBody
}) })
} finally {
await remove(audioPath)
await remove(inputPath)
await remove(outputPath) await remove(outputPath)
} }
}

View File

@ -2,7 +2,7 @@ import {
RunnerJobLiveRTMPHLSTranscodingPayload, RunnerJobLiveRTMPHLSTranscodingPayload,
RunnerJobPayload, RunnerJobPayload,
RunnerJobType, RunnerJobType,
RunnerJobVideoEditionTranscodingPayload, RunnerJobStudioTranscodingPayload,
RunnerJobVODAudioMergeTranscodingPayload, RunnerJobVODAudioMergeTranscodingPayload,
RunnerJobVODHLSTranscodingPayload, RunnerJobVODHLSTranscodingPayload,
RunnerJobVODWebVideoTranscodingPayload, RunnerJobVODWebVideoTranscodingPayload,
@ -22,7 +22,7 @@ const supportedMatrix = {
'live-rtmp-hls-transcoding': (_payload: RunnerJobLiveRTMPHLSTranscodingPayload) => { 'live-rtmp-hls-transcoding': (_payload: RunnerJobLiveRTMPHLSTranscodingPayload) => {
return true return true
}, },
'video-edition-transcoding': (payload: RunnerJobVideoEditionTranscodingPayload) => { 'video-studio-transcoding': (payload: RunnerJobStudioTranscodingPayload) => {
const tasks = payload?.tasks const tasks = payload?.tasks
const supported = new Set<VideoStudioTaskPayload['name']>([ 'add-intro', 'add-outro', 'add-watermark', 'cut' ]) const supported = new Set<VideoStudioTaskPayload['name']>([ 'add-intro', 'add-outro', 'add-watermark', 'cut' ])

View File

@ -31,7 +31,7 @@ runnerJobFilesRouter.post('/jobs/:jobUUID/files/videos/:videoId/studio/task-file
asyncMiddleware(jobOfRunnerGetValidator), asyncMiddleware(jobOfRunnerGetValidator),
asyncMiddleware(runnerJobGetVideoTranscodingFileValidator), asyncMiddleware(runnerJobGetVideoTranscodingFileValidator),
runnerJobGetVideoStudioTaskFileValidator, runnerJobGetVideoStudioTaskFileValidator,
getVideoEditionTaskFile getVideoStudioTaskFile
) )
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -94,14 +94,14 @@ function getMaxQualityVideoPreview (req: express.Request, res: express.Response)
return res.sendFile(file.getPath()) return res.sendFile(file.getPath())
} }
function getVideoEditionTaskFile (req: express.Request, res: express.Response) { function getVideoStudioTaskFile (req: express.Request, res: express.Response) {
const runnerJob = res.locals.runnerJob const runnerJob = res.locals.runnerJob
const runner = runnerJob.Runner const runner = runnerJob.Runner
const video = res.locals.videoAll const video = res.locals.videoAll
const filename = req.params.filename const filename = req.params.filename
logger.info( logger.info(
'Get video edition task file %s of video %s of job %s for runner %s', filename, video.uuid, runnerJob.uuid, runner.name, 'Get video studio task file %s of video %s of job %s for runner %s', filename, video.uuid, runnerJob.uuid, runner.name,
lTags(runner.name, runnerJob.id, runnerJob.type) lTags(runner.name, runnerJob.id, runnerJob.type)
) )

View File

@ -42,7 +42,7 @@ import {
RunnerJobUpdateBody, RunnerJobUpdateBody,
RunnerJobUpdatePayload, RunnerJobUpdatePayload,
UserRight, UserRight,
VideoEditionTranscodingSuccess, VideoStudioTranscodingSuccess,
VODAudioMergeTranscodingSuccess, VODAudioMergeTranscodingSuccess,
VODHLSTranscodingSuccess, VODHLSTranscodingSuccess,
VODWebVideoTranscodingSuccess VODWebVideoTranscodingSuccess
@ -300,7 +300,7 @@ const jobSuccessPayloadBuilders: {
} }
}, },
'video-edition-transcoding': (payload: VideoEditionTranscodingSuccess, files) => { 'video-studio-transcoding': (payload: VideoStudioTranscodingSuccess, files) => {
return { return {
...payload, ...payload,

View File

@ -6,7 +6,7 @@ import {
RunnerJobSuccessPayload, RunnerJobSuccessPayload,
RunnerJobType, RunnerJobType,
RunnerJobUpdatePayload, RunnerJobUpdatePayload,
VideoEditionTranscodingSuccess, VideoStudioTranscodingSuccess,
VODAudioMergeTranscodingSuccess, VODAudioMergeTranscodingSuccess,
VODHLSTranscodingSuccess, VODHLSTranscodingSuccess,
VODWebVideoTranscodingSuccess VODWebVideoTranscodingSuccess
@ -25,7 +25,7 @@ function isRunnerJobSuccessPayloadValid (value: RunnerJobSuccessPayload, type: R
isRunnerJobVODHLSResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) || isRunnerJobVODHLSResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
isRunnerJobVODAudioMergeResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) || isRunnerJobVODAudioMergeResultPayloadValid(value as VODHLSTranscodingSuccess, type, files) ||
isRunnerJobLiveRTMPHLSResultPayloadValid(value as LiveRTMPHLSTranscodingSuccess, type) || isRunnerJobLiveRTMPHLSResultPayloadValid(value as LiveRTMPHLSTranscodingSuccess, type) ||
isRunnerJobVideoEditionResultPayloadValid(value as VideoEditionTranscodingSuccess, type, files) isRunnerJobVideoStudioResultPayloadValid(value as VideoStudioTranscodingSuccess, type, files)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -37,7 +37,7 @@ function isRunnerJobProgressValid (value: string) {
function isRunnerJobUpdatePayloadValid (value: RunnerJobUpdatePayload, type: RunnerJobType, files: UploadFilesForCheck) { function isRunnerJobUpdatePayloadValid (value: RunnerJobUpdatePayload, type: RunnerJobType, files: UploadFilesForCheck) {
return isRunnerJobVODWebVideoUpdatePayloadValid(value, type, files) || return isRunnerJobVODWebVideoUpdatePayloadValid(value, type, files) ||
isRunnerJobVODHLSUpdatePayloadValid(value, type, files) || isRunnerJobVODHLSUpdatePayloadValid(value, type, files) ||
isRunnerJobVideoEditionUpdatePayloadValid(value, type, files) || isRunnerJobVideoStudioUpdatePayloadValid(value, type, files) ||
isRunnerJobVODAudioMergeUpdatePayloadValid(value, type, files) || isRunnerJobVODAudioMergeUpdatePayloadValid(value, type, files) ||
isRunnerJobLiveRTMPHLSUpdatePayloadValid(value, type, files) isRunnerJobLiveRTMPHLSUpdatePayloadValid(value, type, files)
} }
@ -105,12 +105,12 @@ function isRunnerJobLiveRTMPHLSResultPayloadValid (
return type === 'live-rtmp-hls-transcoding' && (!value || (typeof value === 'object' && Object.keys(value).length === 0)) return type === 'live-rtmp-hls-transcoding' && (!value || (typeof value === 'object' && Object.keys(value).length === 0))
} }
function isRunnerJobVideoEditionResultPayloadValid ( function isRunnerJobVideoStudioResultPayloadValid (
_value: VideoEditionTranscodingSuccess, _value: VideoStudioTranscodingSuccess,
type: RunnerJobType, type: RunnerJobType,
files: UploadFilesForCheck files: UploadFilesForCheck
) { ) {
return type === 'video-edition-transcoding' && return type === 'video-studio-transcoding' &&
isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null }) isFileValid({ files, field: 'payload[videoFile]', mimeTypeRegex: null, maxSize: null })
} }
@ -177,11 +177,11 @@ function isRunnerJobLiveRTMPHLSUpdatePayloadValid (
) )
} }
function isRunnerJobVideoEditionUpdatePayloadValid ( function isRunnerJobVideoStudioUpdatePayloadValid (
value: RunnerJobUpdatePayload, value: RunnerJobUpdatePayload,
type: RunnerJobType, type: RunnerJobType,
_files: UploadFilesForCheck _files: UploadFilesForCheck
) { ) {
return type === 'video-edition-transcoding' && return type === 'video-studio-transcoding' &&
(!value || (typeof value === 'object' && Object.keys(value).length === 0)) (!value || (typeof value === 'object' && Object.keys(value).length === 0))
} }

View File

@ -6,7 +6,7 @@ import { CONFIG } from '@server/initializers/config'
import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles' import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/default-transcoding-profiles'
import { isAbleToUploadVideo } from '@server/lib/user' import { isAbleToUploadVideo } from '@server/lib/user'
import { VideoPathManager } from '@server/lib/video-path-manager' import { VideoPathManager } from '@server/lib/video-path-manager'
import { approximateIntroOutroAdditionalSize, onVideoEditionEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio' import { approximateIntroOutroAdditionalSize, onVideoStudioEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio'
import { UserModel } from '@server/models/user/user' import { UserModel } from '@server/models/user/user'
import { VideoModel } from '@server/models/video/video' import { VideoModel } from '@server/models/video/video'
import { MVideo, MVideoFullLight } from '@server/types/models' import { MVideo, MVideoFullLight } from '@server/types/models'
@ -24,7 +24,7 @@ import {
} from '@shared/models' } from '@shared/models'
import { logger, loggerTagsFactory } from '../../../helpers/logger' import { logger, loggerTagsFactory } from '../../../helpers/logger'
const lTagsBase = loggerTagsFactory('video-edition') const lTagsBase = loggerTagsFactory('video-studio')
async function processVideoStudioEdition (job: Job) { async function processVideoStudioEdition (job: Job) {
const payload = job.data as VideoStudioEditionPayload const payload = job.data as VideoStudioEditionPayload
@ -74,7 +74,7 @@ async function processVideoStudioEdition (job: Job) {
logger.info('Video edition ended for video %s.', video.uuid, lTags) logger.info('Video edition ended for video %s.', video.uuid, lTags)
await onVideoEditionEnded({ video, editionResultPath, tasks: payload.tasks }) await onVideoStudioEnded({ video, editionResultPath, tasks: payload.tasks })
} catch (err) { } catch (err) {
await safeCleanupStudioTMPFiles(payload.tasks) await safeCleanupStudioTMPFiles(payload.tasks)

View File

@ -15,8 +15,8 @@ import {
RunnerJobSuccessPayload, RunnerJobSuccessPayload,
RunnerJobType, RunnerJobType,
RunnerJobUpdatePayload, RunnerJobUpdatePayload,
RunnerJobVideoEditionTranscodingPayload, RunnerJobStudioTranscodingPayload,
RunnerJobVideoEditionTranscodingPrivatePayload, RunnerJobVideoStudioTranscodingPrivatePayload,
RunnerJobVODAudioMergeTranscodingPayload, RunnerJobVODAudioMergeTranscodingPayload,
RunnerJobVODAudioMergeTranscodingPrivatePayload, RunnerJobVODAudioMergeTranscodingPrivatePayload,
RunnerJobVODHLSTranscodingPayload, RunnerJobVODHLSTranscodingPayload,
@ -47,9 +47,9 @@ type CreateRunnerJobArg =
privatePayload: RunnerJobLiveRTMPHLSTranscodingPrivatePayload privatePayload: RunnerJobLiveRTMPHLSTranscodingPrivatePayload
} | } |
{ {
type: Extract<RunnerJobType, 'video-edition-transcoding'> type: Extract<RunnerJobType, 'video-studio-transcoding'>
payload: RunnerJobVideoEditionTranscodingPayload payload: RunnerJobStudioTranscodingPayload
privatePayload: RunnerJobVideoEditionTranscodingPrivatePayload privatePayload: RunnerJobVideoStudioTranscodingPrivatePayload
} }
export abstract class AbstractJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> { export abstract class AbstractJobHandler <C, U extends RunnerJobUpdatePayload, S extends RunnerJobSuccessPayload> {

View File

@ -1,7 +1,7 @@
export * from './abstract-job-handler' export * from './abstract-job-handler'
export * from './live-rtmp-hls-transcoding-job-handler' export * from './live-rtmp-hls-transcoding-job-handler'
export * from './runner-job-handlers' export * from './runner-job-handlers'
export * from './video-edition-transcoding-job-handler' export * from './video-studio-transcoding-job-handler'
export * from './vod-audio-merge-transcoding-job-handler' export * from './vod-audio-merge-transcoding-job-handler'
export * from './vod-hls-transcoding-job-handler' export * from './vod-hls-transcoding-job-handler'
export * from './vod-web-video-transcoding-job-handler' export * from './vod-web-video-transcoding-job-handler'

View File

@ -2,7 +2,7 @@ import { MRunnerJob } from '@server/types/models/runners'
import { RunnerJobSuccessPayload, RunnerJobType, RunnerJobUpdatePayload } from '@shared/models' import { RunnerJobSuccessPayload, RunnerJobType, RunnerJobUpdatePayload } from '@shared/models'
import { AbstractJobHandler } from './abstract-job-handler' import { AbstractJobHandler } from './abstract-job-handler'
import { LiveRTMPHLSTranscodingJobHandler } from './live-rtmp-hls-transcoding-job-handler' import { LiveRTMPHLSTranscodingJobHandler } from './live-rtmp-hls-transcoding-job-handler'
import { VideoEditionTranscodingJobHandler } from './video-edition-transcoding-job-handler' import { VideoStudioTranscodingJobHandler } from './video-studio-transcoding-job-handler'
import { VODAudioMergeTranscodingJobHandler } from './vod-audio-merge-transcoding-job-handler' import { VODAudioMergeTranscodingJobHandler } from './vod-audio-merge-transcoding-job-handler'
import { VODHLSTranscodingJobHandler } from './vod-hls-transcoding-job-handler' import { VODHLSTranscodingJobHandler } from './vod-hls-transcoding-job-handler'
import { VODWebVideoTranscodingJobHandler } from './vod-web-video-transcoding-job-handler' import { VODWebVideoTranscodingJobHandler } from './vod-web-video-transcoding-job-handler'
@ -12,7 +12,7 @@ const processors: Record<RunnerJobType, new() => AbstractJobHandler<unknown, Run
'vod-hls-transcoding': VODHLSTranscodingJobHandler, 'vod-hls-transcoding': VODHLSTranscodingJobHandler,
'vod-audio-merge-transcoding': VODAudioMergeTranscodingJobHandler, 'vod-audio-merge-transcoding': VODAudioMergeTranscodingJobHandler,
'live-rtmp-hls-transcoding': LiveRTMPHLSTranscodingJobHandler, 'live-rtmp-hls-transcoding': LiveRTMPHLSTranscodingJobHandler,
'video-edition-transcoding': VideoEditionTranscodingJobHandler 'video-studio-transcoding': VideoStudioTranscodingJobHandler
} }
export function getRunnerJobHandlerClass (job: MRunnerJob) { export function getRunnerJobHandlerClass (job: MRunnerJob) {

View File

@ -1,7 +1,7 @@
import { basename } from 'path' import { basename } from 'path'
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { onVideoEditionEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio' import { onVideoStudioEnded, safeCleanupStudioTMPFiles } from '@server/lib/video-studio'
import { MVideo } from '@server/types/models' import { MVideo } from '@server/types/models'
import { MRunnerJob } from '@server/types/models/runners' import { MRunnerJob } from '@server/types/models/runners'
import { buildUUID } from '@shared/extra-utils' import { buildUUID } from '@shared/extra-utils'
@ -11,9 +11,9 @@ import {
isVideoStudioTaskWatermark, isVideoStudioTaskWatermark,
RunnerJobState, RunnerJobState,
RunnerJobUpdatePayload, RunnerJobUpdatePayload,
RunnerJobVideoEditionTranscodingPayload, RunnerJobStudioTranscodingPayload,
RunnerJobVideoEditionTranscodingPrivatePayload, RunnerJobVideoStudioTranscodingPrivatePayload,
VideoEditionTranscodingSuccess, VideoStudioTranscodingSuccess,
VideoState, VideoState,
VideoStudioTaskPayload VideoStudioTaskPayload
} from '@shared/models' } from '@shared/models'
@ -28,13 +28,13 @@ type CreateOptions = {
} }
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
export class VideoEditionTranscodingJobHandler extends AbstractJobHandler<CreateOptions, RunnerJobUpdatePayload, VideoEditionTranscodingSuccess> { export class VideoStudioTranscodingJobHandler extends AbstractJobHandler<CreateOptions, RunnerJobUpdatePayload, VideoStudioTranscodingSuccess> {
async create (options: CreateOptions) { async create (options: CreateOptions) {
const { video, priority, tasks } = options const { video, priority, tasks } = options
const jobUUID = buildUUID() const jobUUID = buildUUID()
const payload: RunnerJobVideoEditionTranscodingPayload = { const payload: RunnerJobStudioTranscodingPayload = {
input: { input: {
videoFileUrl: generateRunnerTranscodingVideoInputFileUrl(jobUUID, video.uuid) videoFileUrl: generateRunnerTranscodingVideoInputFileUrl(jobUUID, video.uuid)
}, },
@ -67,13 +67,13 @@ export class VideoEditionTranscodingJobHandler extends AbstractJobHandler<Create
}) })
} }
const privatePayload: RunnerJobVideoEditionTranscodingPrivatePayload = { const privatePayload: RunnerJobVideoStudioTranscodingPrivatePayload = {
videoUUID: video.uuid, videoUUID: video.uuid,
originalTasks: tasks originalTasks: tasks
} }
const job = await this.createRunnerJob({ const job = await this.createRunnerJob({
type: 'video-edition-transcoding', type: 'video-studio-transcoding',
jobUUID, jobUUID,
payload, payload,
privatePayload, privatePayload,
@ -103,10 +103,10 @@ export class VideoEditionTranscodingJobHandler extends AbstractJobHandler<Create
protected async specificComplete (options: { protected async specificComplete (options: {
runnerJob: MRunnerJob runnerJob: MRunnerJob
resultPayload: VideoEditionTranscodingSuccess resultPayload: VideoStudioTranscodingSuccess
}) { }) {
const { runnerJob, resultPayload } = options const { runnerJob, resultPayload } = options
const privatePayload = runnerJob.privatePayload as RunnerJobVideoEditionTranscodingPrivatePayload const privatePayload = runnerJob.privatePayload as RunnerJobVideoStudioTranscodingPrivatePayload
const video = await loadTranscodingRunnerVideo(runnerJob, this.lTags) const video = await loadTranscodingRunnerVideo(runnerJob, this.lTags)
if (!video) { if (!video) {
@ -116,7 +116,7 @@ export class VideoEditionTranscodingJobHandler extends AbstractJobHandler<Create
const videoFilePath = resultPayload.videoFile as string const videoFilePath = resultPayload.videoFile as string
await onVideoEditionEnded({ video, editionResultPath: videoFilePath, tasks: privatePayload.originalTasks }) await onVideoStudioEnded({ video, editionResultPath: videoFilePath, tasks: privatePayload.originalTasks })
logger.info( logger.info(
'Runner video edition transcoding job %s for %s ended.', 'Runner video edition transcoding job %s for %s ended.',
@ -146,7 +146,7 @@ export class VideoEditionTranscodingJobHandler extends AbstractJobHandler<Create
}) { }) {
const { runnerJob } = options const { runnerJob } = options
const payload = runnerJob.privatePayload as RunnerJobVideoEditionTranscodingPrivatePayload const payload = runnerJob.privatePayload as RunnerJobVideoStudioTranscodingPrivatePayload
await safeCleanupStudioTMPFiles(payload.originalTasks) await safeCleanupStudioTMPFiles(payload.originalTasks)
const video = await loadTranscodingRunnerVideo(options.runnerJob, this.lTags) const video = await loadTranscodingRunnerVideo(options.runnerJob, this.lTags)

View File

@ -1,88 +1,7 @@
import { logger } from '@server/helpers/logger' import { logger } from '@server/helpers/logger'
import { getAverageBitrate, getMinLimitBitrate } from '@shared/core-utils' import { FFmpegCommandWrapper, getDefaultAvailableEncoders } from '@shared/ffmpeg'
import { buildStreamSuffix, FFmpegCommandWrapper, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '@shared/ffmpeg' import { AvailableEncoders, EncoderOptionsBuilder } from '@shared/models'
import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptionsBuilderParams, VideoResolution } from '@shared/models'
import { canDoQuickAudioTranscode } from './transcoding-quick-transcode'
/**
*
* Available encoders and profiles for the transcoding jobs
* These functions are used by ffmpeg-utils that will get the encoders and options depending on the chosen profile
*
* Resources:
* * https://slhck.info/video/2017/03/01/rate-control.html
* * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
*/
// ---------------------------------------------------------------------------
// Default builders
// ---------------------------------------------------------------------------
const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => {
const { fps, inputRatio, inputBitrate, resolution } = options
// TODO: remove in 4.2, fps is not optional anymore
if (!fps) return { outputOptions: [ ] }
const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution })
return {
outputOptions: [
...getCommonOutputOptions(targetBitrate),
`-r ${fps}`
]
}
}
const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = (options: EncoderOptionsBuilderParams) => {
const { streamNum, fps, inputBitrate, inputRatio, resolution } = options
const targetBitrate = getTargetBitrate({ inputBitrate, ratio: inputRatio, fps, resolution })
return {
outputOptions: [
...getCommonOutputOptions(targetBitrate, streamNum),
`${buildStreamSuffix('-r:v', streamNum)} ${fps}`,
`${buildStreamSuffix('-b:v', streamNum)} ${targetBitrate}`
]
}
}
const defaultAACOptionsBuilder: EncoderOptionsBuilder = async ({ input, streamNum, canCopyAudio }) => {
const probe = await ffprobePromise(input)
if (canCopyAudio && await canDoQuickAudioTranscode(input, probe)) {
logger.debug('Copy audio stream %s by AAC encoder.', input)
return { copy: true, outputOptions: [ ] }
}
const parsedAudio = await getAudioStream(input, probe)
// We try to reduce the ceiling bitrate by making rough matches of bitrates
// Of course this is far from perfect, but it might save some space in the end
const audioCodecName = parsedAudio.audioStream['codec_name']
const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate)
logger.debug('Calculating audio bitrate of %s by AAC encoder.', input, { bitrate: parsedAudio.bitrate, audioCodecName })
// Force stereo as it causes some issues with HLS playback in Chrome
const base = [ '-channel_layout', 'stereo' ]
if (bitrate !== -1) {
return { outputOptions: base.concat([ buildStreamSuffix('-b:a', streamNum), bitrate + 'k' ]) }
}
return { outputOptions: base }
}
const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum }) => {
return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] }
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Profile manager to get and change default profiles // Profile manager to get and change default profiles
@ -97,27 +16,7 @@ class VideoTranscodingProfilesManager {
live: this.buildDefaultEncodersPriorities() live: this.buildDefaultEncodersPriorities()
} }
private readonly availableEncoders = { private readonly availableEncoders = getDefaultAvailableEncoders()
vod: {
libx264: {
default: defaultX264VODOptionsBuilder
},
aac: {
default: defaultAACOptionsBuilder
},
libfdk_aac: {
default: defaultLibFDKAACVODOptionsBuilder
}
},
live: {
libx264: {
default: defaultX264LiveOptionsBuilder
},
aac: {
default: defaultAACOptionsBuilder
}
}
}
private availableProfiles = { private availableProfiles = {
vod: [] as string[], vod: [] as string[],
@ -242,41 +141,3 @@ class VideoTranscodingProfilesManager {
export { export {
VideoTranscodingProfilesManager VideoTranscodingProfilesManager
} }
// ---------------------------------------------------------------------------
function getTargetBitrate (options: {
inputBitrate: number
resolution: VideoResolution
ratio: number
fps: number
}) {
const { inputBitrate, resolution, ratio, fps } = options
const capped = capBitrate(inputBitrate, getAverageBitrate({ resolution, fps, ratio }))
const limit = getMinLimitBitrate({ resolution, fps, ratio })
return Math.max(limit, capped)
}
function capBitrate (inputBitrate: number, targetBitrate: number) {
if (!inputBitrate) return targetBitrate
// Add 30% margin to input bitrate
const inputBitrateWithMargin = inputBitrate + (inputBitrate * 0.3)
return Math.min(targetBitrate, inputBitrateWithMargin)
}
function getCommonOutputOptions (targetBitrate: number, streamNum?: number) {
return [
`-preset veryfast`,
`${buildStreamSuffix('-maxrate:v', streamNum)} ${targetBitrate}`,
`${buildStreamSuffix('-bufsize:v', streamNum)} ${targetBitrate * 2}`,
// NOTE: b-strategy 1 - heuristic algorithm, 16 is optimal B-frames for it
`-b_strategy 1`,
// NOTE: Why 16: https://github.com/Chocobozzz/PeerTube/pull/774. b-strategy 2 -> B-frames<16
`-bf 16`
]
}

View File

@ -9,13 +9,13 @@ import { getVideoStreamDuration } from '@shared/ffmpeg'
import { VideoStudioEditionPayload, VideoStudioTask, VideoStudioTaskPayload } from '@shared/models' import { VideoStudioEditionPayload, VideoStudioTask, VideoStudioTaskPayload } from '@shared/models'
import { federateVideoIfNeeded } from './activitypub/videos' import { federateVideoIfNeeded } from './activitypub/videos'
import { JobQueue } from './job-queue' import { JobQueue } from './job-queue'
import { VideoEditionTranscodingJobHandler } from './runners' import { VideoStudioTranscodingJobHandler } from './runners'
import { createOptimizeOrMergeAudioJobs } from './transcoding/create-transcoding-job' import { createOptimizeOrMergeAudioJobs } from './transcoding/create-transcoding-job'
import { getTranscodingJobPriority } from './transcoding/transcoding-priority' import { getTranscodingJobPriority } from './transcoding/transcoding-priority'
import { buildNewFile, removeHLSPlaylist, removeWebTorrentFile } from './video-file' import { buildNewFile, removeHLSPlaylist, removeWebTorrentFile } from './video-file'
import { VideoPathManager } from './video-path-manager' import { VideoPathManager } from './video-path-manager'
const lTags = loggerTagsFactory('video-edition') const lTags = loggerTagsFactory('video-studio')
export function buildTaskFileFieldname (indice: number, fieldName = 'file') { export function buildTaskFileFieldname (indice: number, fieldName = 'file') {
return `tasks[${indice}][options][${fieldName}]` return `tasks[${indice}][options][${fieldName}]`
@ -78,14 +78,14 @@ export async function createVideoStudioJob (options: {
const priority = await getTranscodingJobPriority({ user, type: 'studio', fallback: 0 }) const priority = await getTranscodingJobPriority({ user, type: 'studio', fallback: 0 })
if (CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED) { if (CONFIG.VIDEO_STUDIO.REMOTE_RUNNERS.ENABLED) {
await new VideoEditionTranscodingJobHandler().create({ video, tasks: payload.tasks, priority }) await new VideoStudioTranscodingJobHandler().create({ video, tasks: payload.tasks, priority })
return return
} }
await JobQueue.Instance.createJob({ type: 'video-studio-edition', payload, priority }) await JobQueue.Instance.createJob({ type: 'video-studio-edition', payload, priority })
} }
export async function onVideoEditionEnded (options: { export async function onVideoStudioEnded (options: {
editionResultPath: string editionResultPath: string
tasks: VideoStudioTaskPayload[] tasks: VideoStudioTaskPayload[]
video: MVideoFullLight video: MVideoFullLight

View File

@ -2,7 +2,7 @@ import express from 'express'
import { param } from 'express-validator' import { param } from 'express-validator'
import { basename } from 'path' import { basename } from 'path'
import { isSafeFilename } from '@server/helpers/custom-validators/misc' import { isSafeFilename } from '@server/helpers/custom-validators/misc'
import { hasVideoStudioTaskFile, HttpStatusCode, RunnerJobVideoEditionTranscodingPayload } from '@shared/models' import { hasVideoStudioTaskFile, HttpStatusCode, RunnerJobStudioTranscodingPayload } from '@shared/models'
import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared' import { areValidationErrors, doesVideoExist, isValidVideoIdParam } from '../shared'
const tags = [ 'runner' ] const tags = [ 'runner' ]
@ -37,7 +37,7 @@ export const runnerJobGetVideoStudioTaskFileValidator = [
const filename = req.params.filename const filename = req.params.filename
const payload = res.locals.runnerJob.payload as RunnerJobVideoEditionTranscodingPayload const payload = res.locals.runnerJob.payload as RunnerJobStudioTranscodingPayload
const found = Array.isArray(payload?.tasks) && payload.tasks.some(t => { const found = Array.isArray(payload?.tasks) && payload.tasks.some(t => {
if (hasVideoStudioTaskFile(t)) { if (hasVideoStudioTaskFile(t)) {

View File

@ -8,7 +8,7 @@ import {
RunnerJobState, RunnerJobState,
RunnerJobSuccessPayload, RunnerJobSuccessPayload,
RunnerJobUpdatePayload, RunnerJobUpdatePayload,
RunnerJobVideoEditionTranscodingPayload, RunnerJobStudioTranscodingPayload,
VideoPrivacy, VideoPrivacy,
VideoStudioTaskIntro VideoStudioTaskIntro
} from '@shared/models' } from '@shared/models'
@ -404,10 +404,10 @@ describe('Test managing runners', function () {
tasks: VideoStudioCommand.getComplexTask() tasks: VideoStudioCommand.getComplexTask()
}) })
const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'video-edition-transcoding' }) const { job } = await server.runnerJobs.autoAccept({ runnerToken, type: 'video-studio-transcoding' })
studioAcceptedJob = job studioAcceptedJob = job
const tasks = (job.payload as RunnerJobVideoEditionTranscodingPayload).tasks const tasks = (job.payload as RunnerJobStudioTranscodingPayload).tasks
const fileUrl = (tasks.find(t => isVideoStudioTaskIntro(t)) as VideoStudioTaskIntro).options.file as string const fileUrl = (tasks.find(t => isVideoStudioTaskIntro(t)) as VideoStudioTaskIntro).options.file as string
studioFile = basename(fileUrl) studioFile = basename(fileUrl)
} }
@ -787,7 +787,7 @@ describe('Test managing runners', function () {
describe('Video studio', function () { describe('Video studio', function () {
it('Should fail with an invalid video edition transcoding payload', async function () { it('Should fail with an invalid video studio transcoding payload', async function () {
await server.runnerJobs.success({ await server.runnerJobs.success({
jobUUID: studioAcceptedJob.uuid, jobUUID: studioAcceptedJob.uuid,
jobToken: studioAcceptedJob.jobToken, jobToken: studioAcceptedJob.jobToken,
@ -849,7 +849,7 @@ describe('Test managing runners', function () {
}) })
}) })
describe('Video edition tasks file routes', function () { describe('Video studio tasks file routes', function () {
it('Should fail with an invalid studio filename', async function () { it('Should fail with an invalid studio filename', async function () {
await fetchStudioFiles({ await fetchStudioFiles({

View File

@ -5,8 +5,8 @@ import { readFile } from 'fs-extra'
import { checkPersistentTmpIsEmpty, checkVideoDuration } from '@server/tests/shared' import { checkPersistentTmpIsEmpty, checkVideoDuration } from '@server/tests/shared'
import { buildAbsoluteFixturePath } from '@shared/core-utils' import { buildAbsoluteFixturePath } from '@shared/core-utils'
import { import {
RunnerJobVideoEditionTranscodingPayload, RunnerJobStudioTranscodingPayload,
VideoEditionTranscodingSuccess, VideoStudioTranscodingSuccess,
VideoState, VideoState,
VideoStudioTask, VideoStudioTask,
VideoStudioTaskIntro VideoStudioTaskIntro
@ -121,10 +121,10 @@ describe('Test runner video studio transcoding', function () {
await checkVideoDuration(server, videoUUID, 5) await checkVideoDuration(server, videoUUID, 5)
} }
const { job } = await servers[0].runnerJobs.accept<RunnerJobVideoEditionTranscodingPayload>({ runnerToken, jobUUID }) const { job } = await servers[0].runnerJobs.accept<RunnerJobStudioTranscodingPayload>({ runnerToken, jobUUID })
const jobToken = job.jobToken const jobToken = job.jobToken
expect(job.type === 'video-edition-transcoding') expect(job.type === 'video-studio-transcoding')
expect(job.payload.input.videoFileUrl).to.exist expect(job.payload.input.videoFileUrl).to.exist
// Check video input file // Check video input file
@ -150,7 +150,7 @@ describe('Test runner video studio transcoding', function () {
expect(body).to.deep.equal(inputFile) expect(body).to.deep.equal(inputFile)
} }
const payload: VideoEditionTranscodingSuccess = { videoFile: 'video_very_short_240p.mp4' } const payload: VideoStudioTranscodingSuccess = { videoFile: 'video_very_short_240p.mp4' }
await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload }) await servers[0].runnerJobs.success({ runnerToken, jobUUID, jobToken, payload })
await waitJobs(servers) await waitJobs(servers)

View File

@ -270,7 +270,7 @@ describe('Test video studio', function () {
}) })
}) })
describe('HLS only video edition', function () { describe('HLS only studio edition', function () {
before(async function () { before(async function () {
// Disable webtorrent // Disable webtorrent
@ -300,7 +300,7 @@ describe('Test video studio', function () {
}) })
}) })
describe('Object storage video edition', function () { describe('Object storage studio edition', function () {
if (areMockObjectStorageTestsDisabled()) return if (areMockObjectStorageTestsDisabled()) return
before(async function () { before(async function () {

View File

@ -57,7 +57,7 @@ const defaultLibFDKAACVODOptionsBuilder: EncoderOptionsBuilder = ({ streamNum })
return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] } return { outputOptions: [ buildStreamSuffix('-q:a', streamNum), '5' ] }
} }
export function getAvailableEncoders () { export function getDefaultAvailableEncoders () {
return { return {
vod: { vod: {
libx264: { libx264: {
@ -81,7 +81,7 @@ export function getAvailableEncoders () {
} }
} }
export function getEncodersToTry () { export function getDefaultEncodersToTry () {
return { return {
vod: { vod: {
video: [ 'libx264' ], video: [ 'libx264' ],

View File

@ -1,4 +1,5 @@
export * from './ffmpeg-command-wrapper' export * from './ffmpeg-command-wrapper'
export * from './ffmpeg-default-transcoding-profile'
export * from './ffmpeg-edition' export * from './ffmpeg-edition'
export * from './ffmpeg-images' export * from './ffmpeg-images'
export * from './ffmpeg-live' export * from './ffmpeg-live'

View File

@ -8,7 +8,7 @@ export type RunnerJobVODPayload =
export type RunnerJobPayload = export type RunnerJobPayload =
RunnerJobVODPayload | RunnerJobVODPayload |
RunnerJobLiveRTMPHLSTranscodingPayload | RunnerJobLiveRTMPHLSTranscodingPayload |
RunnerJobVideoEditionTranscodingPayload RunnerJobStudioTranscodingPayload
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -46,7 +46,7 @@ export interface RunnerJobVODAudioMergeTranscodingPayload {
} }
} }
export interface RunnerJobVideoEditionTranscodingPayload { export interface RunnerJobStudioTranscodingPayload {
input: { input: {
videoFileUrl: string videoFileUrl: string
} }

View File

@ -8,7 +8,7 @@ export type RunnerJobVODPrivatePayload =
export type RunnerJobPrivatePayload = export type RunnerJobPrivatePayload =
RunnerJobVODPrivatePayload | RunnerJobVODPrivatePayload |
RunnerJobLiveRTMPHLSTranscodingPrivatePayload | RunnerJobLiveRTMPHLSTranscodingPrivatePayload |
RunnerJobVideoEditionTranscodingPrivatePayload RunnerJobVideoStudioTranscodingPrivatePayload
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -38,7 +38,7 @@ export interface RunnerJobLiveRTMPHLSTranscodingPrivatePayload {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export interface RunnerJobVideoEditionTranscodingPrivatePayload { export interface RunnerJobVideoStudioTranscodingPrivatePayload {
videoUUID: string videoUUID: string
originalTasks: VideoStudioTaskPayload[] originalTasks: VideoStudioTaskPayload[]
} }

View File

@ -12,7 +12,7 @@ export type RunnerJobSuccessPayload =
VODHLSTranscodingSuccess | VODHLSTranscodingSuccess |
VODAudioMergeTranscodingSuccess | VODAudioMergeTranscodingSuccess |
LiveRTMPHLSTranscodingSuccess | LiveRTMPHLSTranscodingSuccess |
VideoEditionTranscodingSuccess VideoStudioTranscodingSuccess
export interface VODWebVideoTranscodingSuccess { export interface VODWebVideoTranscodingSuccess {
videoFile: Blob | string videoFile: Blob | string
@ -31,7 +31,7 @@ export interface LiveRTMPHLSTranscodingSuccess {
} }
export interface VideoEditionTranscodingSuccess { export interface VideoStudioTranscodingSuccess {
videoFile: Blob | string videoFile: Blob | string
} }

View File

@ -3,4 +3,4 @@ export type RunnerJobType =
'vod-hls-transcoding' | 'vod-hls-transcoding' |
'vod-audio-merge-transcoding' | 'vod-audio-merge-transcoding' |
'live-rtmp-hls-transcoding' | 'live-rtmp-hls-transcoding' |
'video-edition-transcoding' 'video-studio-transcoding'