From 84cae54e7a2595bea0c3ea106a4d111fd11a4ec6 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 5 Aug 2022 10:36:19 +0200 Subject: [PATCH] Add option to not transcode original resolution --- .../edit-custom-config.component.ts | 4 +- .../edit-live-configuration.component.html | 15 +++- .../edit-vod-transcoding.component.html | 8 +- config/default.yaml | 6 ++ config/production.yaml.example | 6 ++ scripts/create-transcoding-job.ts | 6 +- scripts/print-transcode-command.ts | 3 +- server/controllers/api/config.ts | 4 +- server/controllers/api/videos/transcoding.ts | 10 +-- server/helpers/ffmpeg/ffmpeg-vod.ts | 12 +-- server/helpers/ffmpeg/ffprobe-utils.ts | 26 ++++-- server/initializers/checker-before-init.ts | 4 +- server/initializers/config.ts | 3 + .../job-queue/handlers/video-live-ending.ts | 3 +- .../job-queue/handlers/video-transcoding.ts | 26 +++--- server/lib/live/live-manager.ts | 14 +++- server/lib/transcoding/transcoding.ts | 55 +++++++++---- server/middlewares/validators/config.ts | 5 ++ server/tests/api/check-params/config.ts | 4 +- server/tests/api/live/live.ts | 73 +++++++++++++++-- server/tests/api/server/config.ts | 8 +- server/tests/api/transcoding/transcoder.ts | 80 ++++++++++++++++++- server/tests/shared/streaming-playlists.ts | 3 + shared/models/server/custom-config.model.ts | 3 + shared/models/server/job.model.ts | 5 +- .../server-commands/server/config-command.ts | 4 +- 26 files changed, 303 insertions(+), 87 deletions(-) diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index eb892bbfd..ce01f8b59 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts @@ -175,6 +175,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { profile: null, concurrency: CONCURRENCY_VALIDATOR, resolutions: {}, + alwaysTranscodeOriginalResolution: null, hls: { enabled: null }, @@ -197,7 +198,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { enabled: null, threads: TRANSCODING_THREADS_VALIDATOR, profile: null, - resolutions: {} + resolutions: {}, + alwaysTranscodeOriginalResolution: null } }, videoStudio: { diff --git a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html index ae79e54fc..c90c34c80 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html @@ -41,7 +41,6 @@ Small latency disables P2P and high latency can increase P2P ratio - @@ -115,8 +114,8 @@
- +
-
+ +
+ + + Even if it's above your maximum enabled resolution + + +
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html index 66e421b16..5a67b8e3b 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html @@ -111,7 +111,13 @@
- + + + + The original file resolution will be the default target if no option is selected. diff --git a/config/default.yaml b/config/default.yaml index 7e07165b9..3a577d31d 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -403,6 +403,9 @@ transcoding: 1440p: false 2160p: false + # Transcode and keep original resolution, even if it's above your maximum enabled resolution + always_transcode_original_resolution: true + # Generate videos in a WebTorrent format (what we do since the first PeerTube release) # If you also enabled the hls format, it will multiply videos storage by 2 # If disabled, breaks federation with PeerTube instances < 2.1 @@ -496,6 +499,9 @@ live: 1440p: false 2160p: false + # Also transcode original resolution, even if it's above your maximum enabled resolution + always_transcode_original_resolution: true + video_studio: # Enable video edition by users (cut, add intro/outro, add watermark etc) # If enabled, users can create transcoding tasks as they wish diff --git a/config/production.yaml.example b/config/production.yaml.example index 042f5a641..b5ea7fec5 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -413,6 +413,9 @@ transcoding: 1440p: false 2160p: false + # Transcode and keep original resolution, even if it's above your maximum enabled resolution + always_transcode_original_resolution: true + # Generate videos in a WebTorrent format (what we do since the first PeerTube release) # If you also enabled the hls format, it will multiply videos storage by 2 # If disabled, breaks federation with PeerTube instances < 2.1 @@ -506,6 +509,9 @@ live: 1440p: false 2160p: false + # Also transcode original resolution, even if it's above your maximum enabled resolution + always_transcode_original_resolution: true + video_studio: # Enable video edition by users (cut, add intro/outro, add watermark etc) # If enabled, users can create transcoding tasks as they wish diff --git a/scripts/create-transcoding-job.ts b/scripts/create-transcoding-job.ts index 8f4d64290..b7761597e 100755 --- a/scripts/create-transcoding-job.ts +++ b/scripts/create-transcoding-job.ts @@ -1,6 +1,6 @@ import { program } from 'commander' import { isUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc' -import { computeLowerResolutionsToTranscode } from '@server/helpers/ffmpeg' +import { computeResolutionsToTranscode } from '@server/helpers/ffmpeg' import { CONFIG } from '@server/initializers/config' import { addTranscodingJob } from '@server/lib/video' import { VideoState, VideoTranscodingPayload } from '@shared/models' @@ -53,7 +53,7 @@ async function run () { if (options.generateHls || CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { const resolutionsEnabled = options.resolution ? [ parseInt(options.resolution) ] - : computeLowerResolutionsToTranscode(maxResolution, 'vod').concat([ maxResolution ]) + : computeResolutionsToTranscode({ inputResolution: maxResolution, type: 'vod', includeInputResolution: true }) for (const resolution of resolutionsEnabled) { dataInput.push({ @@ -61,8 +61,6 @@ async function run () { videoUUID: video.uuid, resolution, - // FIXME: check the file has audio and is not in portrait mode - isPortraitMode: false, hasAudio: true, copyCodecs: false, diff --git a/scripts/print-transcode-command.ts b/scripts/print-transcode-command.ts index ef671c0aa..ac60ff8a5 100644 --- a/scripts/print-transcode-command.ts +++ b/scripts/print-transcode-command.ts @@ -31,8 +31,7 @@ async function run (path: string, cmd: any) { availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), profile: 'default', - resolution: +cmd.resolution, - isPortraitMode: false + resolution: +cmd.resolution } as TranscodeVODOptions let command = ffmpeg(options.inputPath) diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index cfb750bc9..ff2fa9d86 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts @@ -227,6 +227,7 @@ function customConfig (): CustomConfig { '1440p': CONFIG.TRANSCODING.RESOLUTIONS['1440p'], '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p'] }, + alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION, webtorrent: { enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED }, @@ -256,7 +257,8 @@ function customConfig (): CustomConfig { '1080p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1080p'], '1440p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1440p'], '2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p'] - } + }, + alwaysTranscodeOriginalResolution: CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION } }, videoStudio: { diff --git a/server/controllers/api/videos/transcoding.ts b/server/controllers/api/videos/transcoding.ts index a360a8b6a..09ab7dc0f 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/ffmpeg' +import { computeResolutionsToTranscode } 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' @@ -30,9 +30,9 @@ async function createTranscoding (req: express.Request, res: express.Response) { const body: VideoTranscodingCreate = req.body - const { resolution: maxResolution, isPortraitMode, audioStream } = await video.probeMaxQualityFile() + const { resolution: maxResolution, audioStream } = await video.probeMaxQualityFile() const resolutions = await Hooks.wrapObject( - computeLowerResolutionsToTranscode(maxResolution, 'vod').concat([ maxResolution ]), + computeResolutionsToTranscode({ inputResolution: maxResolution, type: 'vod', includeInputResolution: true }), 'filter:transcoding.manual.lower-resolutions-to-transcode.result', body ) @@ -50,7 +50,6 @@ async function createTranscoding (req: express.Request, res: express.Response) { type: 'new-resolution-to-hls', videoUUID: video.uuid, resolution, - isPortraitMode, hasAudio: !!audioStream, copyCodecs: false, isNewVideo: false, @@ -64,8 +63,7 @@ async function createTranscoding (req: express.Request, res: express.Response) { isNewVideo: false, resolution, hasAudio: !!audioStream, - createHLSIfNeeded: false, - isPortraitMode + createHLSIfNeeded: false }) } } diff --git a/server/helpers/ffmpeg/ffmpeg-vod.ts b/server/helpers/ffmpeg/ffmpeg-vod.ts index c3622ceb1..f84157e0f 100644 --- a/server/helpers/ffmpeg/ffmpeg-vod.ts +++ b/server/helpers/ffmpeg/ffmpeg-vod.ts @@ -7,7 +7,7 @@ import { AvailableEncoders, VideoResolution } from '@shared/models' import { logger, loggerTagsFactory } from '../logger' import { getFFmpeg, runCommand } from './ffmpeg-commons' import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets' -import { computeFPS, getVideoStreamFPS } from './ffprobe-utils' +import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils' import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' const lTags = loggerTagsFactory('ffmpeg') @@ -27,8 +27,6 @@ interface BaseTranscodeVODOptions { resolution: number - isPortraitMode?: boolean - job?: Job } @@ -115,13 +113,17 @@ export { // --------------------------------------------------------------------------- async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) { - let fps = await getVideoStreamFPS(options.inputPath) + const probe = await ffprobePromise(options.inputPath) + + let fps = await getVideoStreamFPS(options.inputPath, probe) fps = computeFPS(fps, options.resolution) let scaleFilterValue: string if (options.resolution !== undefined) { - scaleFilterValue = options.isPortraitMode === true + const videoStreamInfo = await getVideoStreamDimensionsInfo(options.inputPath, probe) + + scaleFilterValue = videoStreamInfo?.isPortraitMode === true ? `w=${options.resolution}:h=-2` : `w=-2:h=${options.resolution}` } diff --git a/server/helpers/ffmpeg/ffprobe-utils.ts b/server/helpers/ffmpeg/ffprobe-utils.ts index 9529162eb..7bcd27665 100644 --- a/server/helpers/ffmpeg/ffprobe-utils.ts +++ b/server/helpers/ffmpeg/ffprobe-utils.ts @@ -90,15 +90,21 @@ async function getAudioStreamCodec (path: string, existingProbe?: FfprobeData) { // Resolutions // --------------------------------------------------------------------------- -function computeLowerResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') { +function computeResolutionsToTranscode (options: { + inputResolution: number + type: 'vod' | 'live' + includeInputResolution: boolean +}) { + const { inputResolution, type, includeInputResolution } = options + const configResolutions = type === 'vod' ? CONFIG.TRANSCODING.RESOLUTIONS : CONFIG.LIVE.TRANSCODING.RESOLUTIONS - const resolutionsEnabled: number[] = [] + const resolutionsEnabled = new Set() // Put in the order we want to proceed jobs - const resolutions: VideoResolution[] = [ + const availableResolutions: VideoResolution[] = [ VideoResolution.H_NOVIDEO, VideoResolution.H_480P, VideoResolution.H_360P, @@ -110,13 +116,17 @@ function computeLowerResolutionsToTranscode (videoFileResolution: number, type: VideoResolution.H_4K ] - for (const resolution of resolutions) { - if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) { - resolutionsEnabled.push(resolution) + for (const resolution of availableResolutions) { + if (configResolutions[resolution + 'p'] === true && inputResolution > resolution) { + resolutionsEnabled.add(resolution) } } - return resolutionsEnabled + if (includeInputResolution) { + resolutionsEnabled.add(inputResolution) + } + + return Array.from(resolutionsEnabled) } // --------------------------------------------------------------------------- @@ -224,7 +234,7 @@ export { computeFPS, getClosestFramerateStandard, - computeLowerResolutionsToTranscode, + computeResolutionsToTranscode, canDoQuickTranscode, canDoQuickVideoTranscode, diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts index 359f0c31d..f4057b81b 100644 --- a/server/initializers/checker-before-init.ts +++ b/server/initializers/checker-before-init.ts @@ -30,7 +30,7 @@ function checkMissedConfig () { 'transcoding.profile', 'transcoding.concurrency', 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', - 'transcoding.resolutions.2160p', 'video_studio.enabled', + 'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'video_studio.enabled', 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout', 'auto_blacklist.videos.of_users.enabled', 'trending.videos.interval_days', 'client.videos.miniature.display_author_avatar', @@ -59,7 +59,7 @@ function checkMissedConfig () { 'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile', 'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p', 'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p', - 'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p' + 'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p', 'live.transcoding.always_transcode_original_resolution' ] const requiredAlternatives = [ diff --git a/server/initializers/config.ts b/server/initializers/config.ts index ba0f756ef..1a0b8942c 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -309,6 +309,7 @@ const CONFIG = { get THREADS () { return config.get('transcoding.threads') }, get CONCURRENCY () { return config.get('transcoding.concurrency') }, get PROFILE () { return config.get('transcoding.profile') }, + get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get('transcoding.always_transcode_original_resolution') }, RESOLUTIONS: { get '0p' () { return config.get('transcoding.resolutions.0p') }, get '144p' () { return config.get('transcoding.resolutions.144p') }, @@ -361,6 +362,8 @@ const CONFIG = { get THREADS () { return config.get('live.transcoding.threads') }, get PROFILE () { return config.get('live.transcoding.profile') }, + get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get('live.transcoding.always_transcode_original_resolution') }, + RESOLUTIONS: { get '144p' () { return config.get('live.transcoding.resolutions.144p') }, get '240p' () { return config.get('live.transcoding.resolutions.240p') }, diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 10507fb83..78d0b2192 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts @@ -213,13 +213,12 @@ async function assignReplayFilesToVideo (options: { const probe = await ffprobePromise(concatenatedTsFilePath) const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) - const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) + const { resolution } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ video, concatenatedTsFilePath, resolution, - isPortraitMode, isAAC: audioStream?.codec_name === 'aac' }) diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index d3fb7778b..b07876a1c 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts @@ -1,5 +1,6 @@ import { Job } from 'bull' import { TranscodeVODOptionsType } from '@server/helpers/ffmpeg' +import { Hooks } from '@server/lib/plugins/hooks' import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video' import { VideoPathManager } from '@server/lib/video-path-manager' import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state' @@ -16,7 +17,7 @@ import { VideoTranscodingPayload } from '@shared/models' import { retryTransactionWrapper } from '../../../helpers/database-utils' -import { computeLowerResolutionsToTranscode } from '../../../helpers/ffmpeg' +import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg' import { logger, loggerTagsFactory } from '../../../helpers/logger' import { CONFIG } from '../../../initializers/config' import { VideoModel } from '../../../models/video/video' @@ -26,7 +27,6 @@ import { optimizeOriginalVideofile, transcodeNewWebTorrentResolution } from '../../transcoding/transcoding' -import { Hooks } from '@server/lib/plugins/hooks' type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise @@ -99,7 +99,6 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, video: MV videoInputPath, resolution: payload.resolution, copyCodecs: payload.copyCodecs, - isPortraitMode: payload.isPortraitMode || false, job }) }) @@ -117,7 +116,7 @@ async function handleNewWebTorrentResolutionJob ( ) { logger.info('Handling WebTorrent transcoding job for %s.', video.uuid, lTags(video.uuid)) - await transcodeNewWebTorrentResolution(video, payload.resolution, payload.isPortraitMode || false, job) + await transcodeNewWebTorrentResolution({ video, resolution: payload.resolution, job }) logger.info('WebTorrent transcoding job for %s ended.', video.uuid, lTags(video.uuid)) @@ -127,7 +126,7 @@ async function handleNewWebTorrentResolutionJob ( async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) { logger.info('Handling merge audio transcoding job for %s.', video.uuid, lTags(video.uuid)) - await mergeAudioVideofile(video, payload.resolution, job) + await mergeAudioVideofile({ video, resolution: payload.resolution, job }) logger.info('Merge audio transcoding job for %s ended.', video.uuid, lTags(video.uuid)) @@ -137,7 +136,7 @@ async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTrans async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { logger.info('Handling optimize transcoding job for %s.', video.uuid, lTags(video.uuid)) - const { transcodeType } = await optimizeOriginalVideofile(video, video.getMaxQualityFile(), job) + const { transcodeType } = await optimizeOriginalVideofile({ video, inputVideoFile: video.getMaxQualityFile(), job }) logger.info('Optimize transcoding job for %s ended.', video.uuid, lTags(video.uuid)) @@ -161,7 +160,6 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay video, user, videoFileResolution: payload.resolution, - isPortraitMode: payload.isPortraitMode, hasAudio: payload.hasAudio, isNewVideo: payload.isNewVideo ?? true, type: 'hls' @@ -178,7 +176,7 @@ async function onVideoFirstWebTorrentTranscoding ( transcodeType: TranscodeVODOptionsType, user: MUserId ) { - const { resolution, isPortraitMode, audioStream } = await videoArg.probeMaxQualityFile() + const { resolution, audioStream } = await videoArg.probeMaxQualityFile() // Maybe the video changed in database, refresh it const videoDatabase = await VideoModel.loadFull(videoArg.uuid) @@ -189,7 +187,6 @@ async function onVideoFirstWebTorrentTranscoding ( const originalFileHLSPayload = { ...payload, - isPortraitMode, hasAudio: !!audioStream, resolution: videoDatabase.getMaxQualityFile().resolution, // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues @@ -202,7 +199,6 @@ async function onVideoFirstWebTorrentTranscoding ( user, videoFileResolution: resolution, hasAudio: !!audioStream, - isPortraitMode, type: 'webtorrent', isNewVideo: payload.isNewVideo ?? true }) @@ -235,7 +231,6 @@ async function createHlsJobIfEnabled (user: MUserId, payload: { videoUUID: string resolution: number hasAudio: boolean - isPortraitMode?: boolean copyCodecs: boolean isMaxQuality: boolean isNewVideo?: boolean @@ -250,7 +245,7 @@ async function createHlsJobIfEnabled (user: MUserId, payload: { type: 'new-resolution-to-hls', autoDeleteWebTorrentIfNeeded: true, - ...pick(payload, [ 'videoUUID', 'resolution', 'isPortraitMode', 'copyCodecs', 'isMaxQuality', 'isNewVideo', 'hasAudio' ]) + ...pick(payload, [ 'videoUUID', 'resolution', 'copyCodecs', 'isMaxQuality', 'isNewVideo', 'hasAudio' ]) } await addTranscodingJob(hlsTranscodingPayload, jobOptions) @@ -262,16 +257,15 @@ async function createLowerResolutionsJobs (options: { video: MVideoFullLight user: MUserId videoFileResolution: number - isPortraitMode: boolean hasAudio: boolean isNewVideo: boolean type: 'hls' | 'webtorrent' }) { - const { video, user, videoFileResolution, isPortraitMode, isNewVideo, hasAudio, type } = options + const { video, user, videoFileResolution, isNewVideo, hasAudio, type } = options // Create transcoding jobs if there are enabled resolutions const resolutionsEnabled = await Hooks.wrapObject( - computeLowerResolutionsToTranscode(videoFileResolution, 'vod'), + computeResolutionsToTranscode({ inputResolution: videoFileResolution, type: 'vod', includeInputResolution: false }), 'filter:transcoding.auto.lower-resolutions-to-transcode.result', options ) @@ -289,7 +283,6 @@ async function createLowerResolutionsJobs (options: { type: 'new-resolution-to-webtorrent', videoUUID: video.uuid, resolution, - isPortraitMode, hasAudio, createHLSIfNeeded: true, isNewVideo @@ -303,7 +296,6 @@ async function createLowerResolutionsJobs (options: { type: 'new-resolution-to-hls', videoUUID: video.uuid, resolution, - isPortraitMode, hasAudio, copyCodecs: false, isMaxQuality: false, diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index bd47b01f9..1d1ecd935 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts @@ -4,7 +4,7 @@ import { createServer, Server } from 'net' import { join } from 'path' import { createServer as createServerTLS, Server as ServerTLS } from 'tls' import { - computeLowerResolutionsToTranscode, + computeResolutionsToTranscode, ffprobePromise, getLiveSegmentTime, getVideoStreamBitrate, @@ -26,10 +26,10 @@ import { federateVideoIfNeeded } from '../activitypub/videos' import { JobQueue } from '../job-queue' import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '../paths' import { PeerTubeSocket } from '../peertube-socket' +import { Hooks } from '../plugins/hooks' import { LiveQuotaStore } from './live-quota-store' import { cleanupPermanentLive } from './live-utils' import { MuxingSession } from './shared' -import { Hooks } from '../plugins/hooks' const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') const context = require('node-media-server/src/node_core_ctx') @@ -456,11 +456,17 @@ class LiveManager { } private buildAllResolutionsToTranscode (originResolution: number) { + const includeInputResolution = CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION + const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED - ? computeLowerResolutionsToTranscode(originResolution, 'live') + ? computeResolutionsToTranscode({ inputResolution: originResolution, type: 'live', includeInputResolution }) : [] - return resolutionsEnabled.concat([ originResolution ]) + if (resolutionsEnabled.length === 0) { + return [ originResolution ] + } + + return resolutionsEnabled } private async createLivePlaylist (video: MVideo, allResolutions: number[]): Promise { diff --git a/server/lib/transcoding/transcoding.ts b/server/lib/transcoding/transcoding.ts index 924141d1c..3681de994 100644 --- a/server/lib/transcoding/transcoding.ts +++ b/server/lib/transcoding/transcoding.ts @@ -10,6 +10,7 @@ import { VideoResolution, VideoStorage } from '../../../shared/models/videos' import { buildFileMetadata, canDoQuickTranscode, + computeResolutionsToTranscode, getVideoStreamDuration, getVideoStreamFPS, transcodeVOD, @@ -32,7 +33,13 @@ import { VideoTranscodingProfilesManager } from './default-transcoding-profiles' */ // Optimize the original video file and replace it. The resolution is not changed. -function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) { +function optimizeOriginalVideofile (options: { + video: MVideoFullLight + inputVideoFile: MVideoFile + job: Job +}) { + const { video, inputVideoFile, job } = options + const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const newExtname = '.mp4' @@ -43,7 +50,7 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid ? 'quick-transcode' : 'video' - const resolution = toEven(inputVideoFile.resolution) + const resolution = buildOriginalFileResolution(inputVideoFile.resolution) const transcodeOptions: TranscodeVODOptions = { type: transcodeType, @@ -63,6 +70,7 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid await transcodeVOD(transcodeOptions) // Important to do this before getVideoFilename() to take in account the new filename + inputVideoFile.resolution = resolution inputVideoFile.extname = newExtname inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) inputVideoFile.storage = VideoStorage.FILE_SYSTEM @@ -76,17 +84,22 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid }) } -// Transcode the original video file to a lower resolution -// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed -function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) { +// Transcode the original video file to a lower resolution compatible with WebTorrent +function transcodeNewWebTorrentResolution (options: { + video: MVideoFullLight + resolution: VideoResolution + job: Job +}) { + const { video, resolution, job } = options + const transcodeDirectory = CONFIG.STORAGE.TMP_DIR - const extname = '.mp4' + const newExtname = '.mp4' return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => { const newVideoFile = new VideoFileModel({ resolution, - extname, - filename: generateWebTorrentVideoFilename(resolution, extname), + extname: newExtname, + filename: generateWebTorrentVideoFilename(resolution, newExtname), size: 0, videoId: video.id }) @@ -117,7 +130,6 @@ function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: V profile: CONFIG.TRANSCODING.PROFILE, resolution, - isPortraitMode: isPortrait, job } @@ -129,7 +141,13 @@ function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: V } // Merge an image with an audio file to create a video -function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) { +function mergeAudioVideofile (options: { + video: MVideoFullLight + resolution: VideoResolution + job: Job +}) { + const { video, resolution, job } = options + const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const newExtname = '.mp4' @@ -188,13 +206,11 @@ async function generateHlsPlaylistResolutionFromTS (options: { video: MVideo concatenatedTsFilePath: string resolution: VideoResolution - isPortraitMode: boolean isAAC: boolean }) { return generateHlsPlaylistCommon({ video: options.video, resolution: options.resolution, - isPortraitMode: options.isPortraitMode, inputPath: options.concatenatedTsFilePath, type: 'hls-from-ts' as 'hls-from-ts', isAAC: options.isAAC @@ -207,14 +223,12 @@ function generateHlsPlaylistResolution (options: { videoInputPath: string resolution: VideoResolution copyCodecs: boolean - isPortraitMode: boolean job?: Job }) { return generateHlsPlaylistCommon({ video: options.video, resolution: options.resolution, copyCodecs: options.copyCodecs, - isPortraitMode: options.isPortraitMode, inputPath: options.videoInputPath, type: 'hls' as 'hls', job: options.job @@ -267,11 +281,10 @@ async function generateHlsPlaylistCommon (options: { resolution: VideoResolution copyCodecs?: boolean isAAC?: boolean - isPortraitMode: boolean job?: Job }) { - const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options + const { type, video, inputPath, resolution, copyCodecs, isAAC, job } = options const transcodeDirectory = CONFIG.STORAGE.TMP_DIR const videoTranscodedBasePath = join(transcodeDirectory, type) @@ -292,7 +305,6 @@ async function generateHlsPlaylistCommon (options: { resolution, copyCodecs, - isPortraitMode, isAAC, @@ -350,3 +362,12 @@ async function generateHlsPlaylistCommon (options: { return { resolutionPlaylistPath, videoFile: savedVideoFile } } + +function buildOriginalFileResolution (inputResolution: number) { + if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) return toEven(inputResolution) + + const resolutions = computeResolutionsToTranscode({ inputResolution, type: 'vod', includeInputResolution: false }) + if (resolutions.length === 0) return toEven(inputResolution) + + return Math.max(...resolutions) +} diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index a44fcb854..9ce47c5aa 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts @@ -54,6 +54,9 @@ const customConfigUpdateValidator = [ body('transcoding.resolutions.1440p').isBoolean().withMessage('Should have a valid transcoding 1440p resolution enabled boolean'), body('transcoding.resolutions.2160p').isBoolean().withMessage('Should have a valid transcoding 2160p resolution enabled boolean'), + body('transcoding.alwaysTranscodeOriginalResolution').isBoolean() + .withMessage('Should have a valid always transcode original resolution boolean'), + body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'), body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid hls transcoding enabled boolean'), @@ -91,6 +94,8 @@ const customConfigUpdateValidator = [ body('live.transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), body('live.transcoding.resolutions.1440p').isBoolean().withMessage('Should have a valid transcoding 1440p resolution enabled boolean'), body('live.transcoding.resolutions.2160p').isBoolean().withMessage('Should have a valid transcoding 2160p resolution enabled boolean'), + body('live.transcoding.alwaysTranscodeOriginalResolution').isBoolean() + .withMessage('Should have a valid always transcode live original resolution boolean'), body('search.remoteUri.users').isBoolean().withMessage('Should have a remote URI search for users boolean'), body('search.remoteUri.anonymous').isBoolean().withMessage('Should have a valid remote URI search for anonymous boolean'), diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 99fb24a5b..2f9f553ab 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts @@ -114,6 +114,7 @@ describe('Test config API validators', function () { '1440p': false, '2160p': false }, + alwaysTranscodeOriginalResolution: false, webtorrent: { enabled: true }, @@ -145,7 +146,8 @@ describe('Test config API validators', function () { '1080p': true, '1440p': true, '2160p': true - } + }, + alwaysTranscodeOriginalResolution: false } }, videoStudio: { diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 2d47c131b..f6ad5c82e 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts @@ -4,7 +4,7 @@ import 'mocha' import * as chai from 'chai' import { basename, join } from 'path' import { ffprobePromise, getVideoStream } from '@server/helpers/ffmpeg' -import { checkLiveCleanup, checkLiveSegmentHash, checkResolutionsInMasterPlaylist, testImage } from '@server/tests/shared' +import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist, getAllFiles, testImage } from '@server/tests/shared' import { wait } from '@shared/core-utils' import { HttpStatusCode, @@ -468,7 +468,7 @@ describe('Test live', function () { await waitUntilLivePublishedOnAllServers(servers, liveVideoId) await waitJobs(servers) - await testVideoResolutions(liveVideoId, resolutions) + await testVideoResolutions(liveVideoId, resolutions.concat([ 720 ])) await stopFfmpeg(ffmpegCommand) }) @@ -580,10 +580,73 @@ describe('Test live', function () { } }) - it('Should correctly have cleaned up the live files', async function () { - this.timeout(30000) + it('Should not generate an upper resolution than original file', async function () { + this.timeout(400_000) + + const resolutions = [ 240, 480 ] + await updateConf(resolutions) + + await servers[0].config.updateExistingSubConfig({ + newConfig: { + live: { + transcoding: { + alwaysTranscodeOriginalResolution: false + } + } + } + }) + + liveVideoId = await createLiveWrapper(true) + + const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) + await waitUntilLivePublishedOnAllServers(servers, liveVideoId) + await waitJobs(servers) + + await testVideoResolutions(liveVideoId, resolutions) + + await stopFfmpeg(ffmpegCommand) + await commands[0].waitUntilEnded({ videoId: liveVideoId }) + + await waitJobs(servers) + + await waitUntilLivePublishedOnAllServers(servers, liveVideoId) + + const video = await servers[0].videos.get({ id: liveVideoId }) + const hlsFiles = video.streamingPlaylists[0].files + + expect(video.files).to.have.lengthOf(0) + expect(hlsFiles).to.have.lengthOf(resolutions.length) + + // eslint-disable-next-line @typescript-eslint/require-array-sort-compare + expect(getAllFiles(video).map(f => f.resolution.id).sort()).to.deep.equal(resolutions) + }) + + it('Should only keep the original resolution if all resolutions are disabled', async function () { + this.timeout(400_000) + + await updateConf([]) + liveVideoId = await createLiveWrapper(true) + + const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' }) + await waitUntilLivePublishedOnAllServers(servers, liveVideoId) + await waitJobs(servers) + + await testVideoResolutions(liveVideoId, [ 720 ]) + + await stopFfmpeg(ffmpegCommand) + await commands[0].waitUntilEnded({ videoId: liveVideoId }) + + await waitJobs(servers) + + await waitUntilLivePublishedOnAllServers(servers, liveVideoId) + + const video = await servers[0].videos.get({ id: liveVideoId }) + const hlsFiles = video.streamingPlaylists[0].files + + expect(video.files).to.have.lengthOf(0) + expect(hlsFiles).to.have.lengthOf(1) - await checkLiveCleanup(servers[0], liveVideoId, [ 240, 360, 720 ]) + expect(hlsFiles[0].resolution.id).to.equal(720) }) }) diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index 0f2fb5493..efc57b345 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts @@ -77,6 +77,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { expect(data.transcoding.resolutions['1080p']).to.be.true expect(data.transcoding.resolutions['1440p']).to.be.true expect(data.transcoding.resolutions['2160p']).to.be.true + expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true expect(data.transcoding.webtorrent.enabled).to.be.true expect(data.transcoding.hls.enabled).to.be.true @@ -97,6 +98,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) { expect(data.live.transcoding.resolutions['1080p']).to.be.false expect(data.live.transcoding.resolutions['1440p']).to.be.false expect(data.live.transcoding.resolutions['2160p']).to.be.false + expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true expect(data.videoStudio.enabled).to.be.false @@ -181,6 +183,7 @@ function checkUpdatedConfig (data: CustomConfig) { expect(data.transcoding.resolutions['720p']).to.be.false expect(data.transcoding.resolutions['1080p']).to.be.false expect(data.transcoding.resolutions['2160p']).to.be.false + expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false expect(data.transcoding.hls.enabled).to.be.false expect(data.transcoding.webtorrent.enabled).to.be.true @@ -200,6 +203,7 @@ function checkUpdatedConfig (data: CustomConfig) { expect(data.live.transcoding.resolutions['720p']).to.be.true expect(data.live.transcoding.resolutions['1080p']).to.be.true expect(data.live.transcoding.resolutions['2160p']).to.be.true + expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.false expect(data.videoStudio.enabled).to.be.true @@ -318,6 +322,7 @@ const newCustomConfig: CustomConfig = { '1440p': false, '2160p': false }, + alwaysTranscodeOriginalResolution: false, webtorrent: { enabled: true }, @@ -347,7 +352,8 @@ const newCustomConfig: CustomConfig = { '1080p': true, '1440p': true, '2160p': true - } + }, + alwaysTranscodeOriginalResolution: false } }, videoStudio: { diff --git a/server/tests/api/transcoding/transcoder.ts b/server/tests/api/transcoding/transcoder.ts index 245c4c012..48a20e1d5 100644 --- a/server/tests/api/transcoding/transcoder.ts +++ b/server/tests/api/transcoding/transcoder.ts @@ -7,11 +7,11 @@ import { canDoQuickTranscode } from '@server/helpers/ffmpeg' import { generateHighBitrateVideo, generateVideoWithFramerate, getAllFiles } from '@server/tests/shared' import { buildAbsoluteFixturePath, getMaxBitrate, getMinLimitBitrate } from '@shared/core-utils' import { - getAudioStream, buildFileMetadata, + getAudioStream, getVideoStreamBitrate, - getVideoStreamFPS, getVideoStreamDimensionsInfo, + getVideoStreamFPS, hasAudioStream } from '@shared/extra-utils' import { HttpStatusCode, VideoState } from '@shared/models' @@ -727,6 +727,82 @@ describe('Test video transcoding', function () { }) }) + describe('Bounded transcoding', function () { + + 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 }, + webtorrent: { 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) + + 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 ]) + }) + + it('Should only keep the original resolution if all resolutions are disabled', async function () { + this.timeout(120_000) + + await servers[0].config.updateExistingSubConfig({ + newConfig: { + transcoding: { + resolutions: { + '0p': false, + '144p': false, + '240p': false, + '360p': false, + '480p': false, + '720p': false, + '1080p': false, + '1440p': false, + '2160p': false + } + } + } + }) + + const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' }) + await waitJobs(servers) + + const video = await servers[0].videos.get({ id: uuid }) + const hlsFiles = video.streamingPlaylists[0].files + + expect(video.files).to.have.lengthOf(1) + expect(hlsFiles).to.have.lengthOf(1) + + expect(video.files[0].resolution.id).to.equal(720) + expect(hlsFiles[0].resolution.id).to.equal(720) + }) + }) + after(async function () { await cleanupTests(servers) }) diff --git a/server/tests/shared/streaming-playlists.ts b/server/tests/shared/streaming-playlists.ts index 7ca707f2e..4d82b3654 100644 --- a/server/tests/shared/streaming-playlists.ts +++ b/server/tests/shared/streaming-playlists.ts @@ -68,6 +68,9 @@ async function checkResolutionsInMasterPlaylist (options: { expect(masterPlaylist).to.match(reg) } + + const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH=')) + expect(playlistsLength).to.have.lengthOf(resolutions.length) } export { diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index ab83ed497..bb9c7cef1 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts @@ -117,6 +117,8 @@ export interface CustomConfig { resolutions: ConfigResolutions & { '0p': boolean } + alwaysTranscodeOriginalResolution: boolean + webtorrent: { enabled: boolean } @@ -144,6 +146,7 @@ export interface CustomConfig { threads: number profile: string resolutions: ConfigResolutions + alwaysTranscodeOriginalResolution: boolean } } diff --git a/shared/models/server/job.model.ts b/shared/models/server/job.model.ts index 4633ab769..ac10ea964 100644 --- a/shared/models/server/job.model.ts +++ b/shared/models/server/job.model.ts @@ -1,7 +1,7 @@ import { ContextType } from '../activitypub/context' import { VideoState } from '../videos' -import { VideoStudioTaskCut } from '../videos/studio' import { VideoResolution } from '../videos/file/video-resolution.enum' +import { VideoStudioTaskCut } from '../videos/studio' import { SendEmailOptions } from './emailer.model' export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused' @@ -126,7 +126,6 @@ export interface HLSTranscodingPayload extends BaseTranscodingPayload { copyCodecs: boolean hasAudio: boolean - isPortraitMode?: boolean autoDeleteWebTorrentIfNeeded: boolean isMaxQuality: boolean @@ -138,8 +137,6 @@ export interface NewWebTorrentResolutionTranscodingPayload extends BaseTranscodi hasAudio: boolean createHLSIfNeeded: boolean - - isPortraitMode?: boolean } export interface MergeAudioTranscodingPayload extends BaseTranscodingPayload { diff --git a/shared/server-commands/server/config-command.ts b/shared/server-commands/server/config-command.ts index 3803aaf95..8ab750983 100644 --- a/shared/server-commands/server/config-command.ts +++ b/shared/server-commands/server/config-command.ts @@ -310,6 +310,7 @@ export class ConfigCommand extends AbstractCommand { '1440p': false, '2160p': false }, + alwaysTranscodeOriginalResolution: true, webtorrent: { enabled: true }, @@ -339,7 +340,8 @@ export class ConfigCommand extends AbstractCommand { '1080p': true, '1440p': true, '2160p': true - } + }, + alwaysTranscodeOriginalResolution: true } }, videoStudio: { -- 2.41.0