From c729caf6cc34630877a0e5a1bda1719384cd0c8a Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 11 Feb 2022 10:51:33 +0100 Subject: Add basic video editor support --- server/controllers/api/config.ts | 3 + server/controllers/api/videos/editor.ts | 120 +++++++++++++++++++++++++++ server/controllers/api/videos/index.ts | 2 + server/controllers/api/videos/transcoding.ts | 4 +- server/controllers/api/videos/upload.ts | 8 +- 5 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 server/controllers/api/videos/editor.ts (limited to 'server/controllers/api') diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 4e3dd4d80..821ed4ad3 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -256,6 +256,9 @@ function customConfig (): CustomConfig { } } }, + videoEditor: { + enabled: CONFIG.VIDEO_EDITOR.ENABLED + }, import: { videos: { concurrency: CONFIG.IMPORT.VIDEOS.CONCURRENCY, diff --git a/server/controllers/api/videos/editor.ts b/server/controllers/api/videos/editor.ts new file mode 100644 index 000000000..61e2eb5da --- /dev/null +++ b/server/controllers/api/videos/editor.ts @@ -0,0 +1,120 @@ +import express from 'express' +import { createAnyReqFiles } from '@server/helpers/express-utils' +import { CONFIG } from '@server/initializers/config' +import { MIMETYPES } from '@server/initializers/constants' +import { JobQueue } from '@server/lib/job-queue' +import { buildTaskFileFieldname, getTaskFile } from '@server/lib/video-editor' +import { + HttpStatusCode, + VideoEditionTaskPayload, + VideoEditorCreateEdition, + VideoEditorTask, + VideoEditorTaskCut, + VideoEditorTaskIntro, + VideoEditorTaskOutro, + VideoEditorTaskWatermark, + VideoState +} from '@shared/models' +import { asyncMiddleware, authenticate, videosEditorAddEditionValidator } from '../../../middlewares' + +const editorRouter = express.Router() + +const tasksFiles = createAnyReqFiles( + MIMETYPES.VIDEO.MIMETYPE_EXT, + CONFIG.STORAGE.TMP_DIR, + (req: express.Request, file: Express.Multer.File, cb: (err: Error, result?: boolean) => void) => { + const body = req.body as VideoEditorCreateEdition + + // Fetch array element + const matches = file.fieldname.match(/tasks\[(\d+)\]/) + if (!matches) return cb(new Error('Cannot find array element indice for ' + file.fieldname)) + + const indice = parseInt(matches[1]) + const task = body.tasks[indice] + + if (!task) return cb(new Error('Cannot find array element of indice ' + indice + ' for ' + file.fieldname)) + + if ( + [ 'add-intro', 'add-outro', 'add-watermark' ].includes(task.name) && + file.fieldname === buildTaskFileFieldname(indice) + ) { + return cb(null, true) + } + + return cb(null, false) + } +) + +editorRouter.post('/:videoId/editor/edit', + authenticate, + tasksFiles, + asyncMiddleware(videosEditorAddEditionValidator), + asyncMiddleware(createEditionTasks) +) + +// --------------------------------------------------------------------------- + +export { + editorRouter +} + +// --------------------------------------------------------------------------- + +async function createEditionTasks (req: express.Request, res: express.Response) { + const files = req.files as Express.Multer.File[] + const body = req.body as VideoEditorCreateEdition + const video = res.locals.videoAll + + video.state = VideoState.TO_EDIT + await video.save() + + const payload = { + videoUUID: video.uuid, + tasks: body.tasks.map((t, i) => buildTaskPayload(t, i, files)) + } + + JobQueue.Instance.createJob({ type: 'video-edition', payload }) + + return res.sendStatus(HttpStatusCode.NO_CONTENT_204) +} + +const taskPayloadBuilders: { + [id in VideoEditorTask['name']]: (task: VideoEditorTask, indice?: number, files?: Express.Multer.File[]) => VideoEditionTaskPayload +} = { + 'add-intro': buildIntroOutroTask, + 'add-outro': buildIntroOutroTask, + 'cut': buildCutTask, + 'add-watermark': buildWatermarkTask +} + +function buildTaskPayload (task: VideoEditorTask, indice: number, files: Express.Multer.File[]): VideoEditionTaskPayload { + return taskPayloadBuilders[task.name](task, indice, files) +} + +function buildIntroOutroTask (task: VideoEditorTaskIntro | VideoEditorTaskOutro, indice: number, files: Express.Multer.File[]) { + return { + name: task.name, + options: { + file: getTaskFile(files, indice).path + } + } +} + +function buildCutTask (task: VideoEditorTaskCut) { + return { + name: task.name, + options: { + start: task.options.start, + end: task.options.end + } + } +} + +function buildWatermarkTask (task: VideoEditorTaskWatermark, indice: number, files: Express.Multer.File[]) { + return { + name: task.name, + options: { + file: getTaskFile(files, indice).path + } + } +} diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 61a030ba1..a5ae07d95 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -35,6 +35,7 @@ import { VideoModel } from '../../../models/video/video' import { blacklistRouter } from './blacklist' import { videoCaptionsRouter } from './captions' import { videoCommentRouter } from './comment' +import { editorRouter } from './editor' import { filesRouter } from './files' import { videoImportsRouter } from './import' import { liveRouter } from './live' @@ -51,6 +52,7 @@ const videosRouter = express.Router() videosRouter.use('/', blacklistRouter) videosRouter.use('/', rateVideoRouter) videosRouter.use('/', videoCommentRouter) +videosRouter.use('/', editorRouter) videosRouter.use('/', videoCaptionsRouter) videosRouter.use('/', videoImportsRouter) videosRouter.use('/', ownershipVideoRouter) diff --git a/server/controllers/api/videos/transcoding.ts b/server/controllers/api/videos/transcoding.ts index fba4545c2..da3ea3c9c 100644 --- a/server/controllers/api/videos/transcoding.ts +++ b/server/controllers/api/videos/transcoding.ts @@ -1,5 +1,5 @@ import express from 'express' -import { computeLowerResolutionsToTranscode } from '@server/helpers/ffprobe-utils' +import { computeLowerResolutionsToTranscode } from '@server/helpers/ffmpeg' import { logger, loggerTagsFactory } from '@server/helpers/logger' import { addTranscodingJob } from '@server/lib/video' import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' @@ -29,7 +29,7 @@ async function createTranscoding (req: express.Request, res: express.Response) { const body: VideoTranscodingCreate = req.body - const { resolution: maxResolution, isPortraitMode, audioStream } = await video.getMaxQualityFileInfo() + const { resolution: maxResolution, isPortraitMode, audioStream } = await video.probeMaxQualityFile() const resolutions = computeLowerResolutionsToTranscode(maxResolution, 'vod').concat([ maxResolution ]) video.state = VideoState.TO_TRANSCODE diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index fd90d9915..3c026ad1f 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts @@ -24,7 +24,7 @@ import { HttpStatusCode, VideoCreate, VideoResolution, VideoState } from '@share import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' import { retryTransactionWrapper } from '../../../helpers/database-utils' import { createReqFiles } from '../../../helpers/express-utils' -import { ffprobePromise, getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' +import { ffprobePromise, buildFileMetadata, getVideoStreamFPS, getVideoStreamDimensionsInfo } from '../../../helpers/ffmpeg' import { logger, loggerTagsFactory } from '../../../helpers/logger' import { CONFIG } from '../../../initializers/config' import { MIMETYPES } from '../../../initializers/constants' @@ -246,7 +246,7 @@ async function buildNewFile (videoPhysicalFile: express.VideoUploadFile) { extname: getLowercaseExtension(videoPhysicalFile.filename), size: videoPhysicalFile.size, videoStreamingPlaylistId: null, - metadata: await getMetadataFromFile(videoPhysicalFile.path) + metadata: await buildFileMetadata(videoPhysicalFile.path) }) const probe = await ffprobePromise(videoPhysicalFile.path) @@ -254,8 +254,8 @@ async function buildNewFile (videoPhysicalFile: express.VideoUploadFile) { if (await isAudioFile(videoPhysicalFile.path, probe)) { videoFile.resolution = VideoResolution.H_NOVIDEO } else { - videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path, probe) - videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path, probe)).resolution + videoFile.fps = await getVideoStreamFPS(videoPhysicalFile.path, probe) + videoFile.resolution = (await getVideoStreamDimensionsInfo(videoPhysicalFile.path, probe)).resolution } videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname) -- cgit v1.2.3