aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-08-05 10:36:19 +0200
committerChocobozzz <me@florianbigard.com>2022-08-09 09:18:07 +0200
commit84cae54e7a2595bea0c3ea106a4d111fd11a4ec6 (patch)
tree03fe73edf049ce60df6bbc34dcfb2031c07ea59c /server
parent7e0f50d6e0c7dc583d40e196c283eb20dc386ae6 (diff)
downloadPeerTube-84cae54e7a2595bea0c3ea106a4d111fd11a4ec6.tar.gz
PeerTube-84cae54e7a2595bea0c3ea106a4d111fd11a4ec6.tar.zst
PeerTube-84cae54e7a2595bea0c3ea106a4d111fd11a4ec6.zip
Add option to not transcode original resolution
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/config.ts4
-rw-r--r--server/controllers/api/videos/transcoding.ts10
-rw-r--r--server/helpers/ffmpeg/ffmpeg-vod.ts12
-rw-r--r--server/helpers/ffmpeg/ffprobe-utils.ts26
-rw-r--r--server/initializers/checker-before-init.ts4
-rw-r--r--server/initializers/config.ts3
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts3
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts26
-rw-r--r--server/lib/live/live-manager.ts14
-rw-r--r--server/lib/transcoding/transcoding.ts55
-rw-r--r--server/middlewares/validators/config.ts5
-rw-r--r--server/tests/api/check-params/config.ts4
-rw-r--r--server/tests/api/live/live.ts73
-rw-r--r--server/tests/api/server/config.ts8
-rw-r--r--server/tests/api/transcoding/transcoder.ts80
-rw-r--r--server/tests/shared/streaming-playlists.ts3
16 files changed, 259 insertions, 71 deletions
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 {
227 '1440p': CONFIG.TRANSCODING.RESOLUTIONS['1440p'], 227 '1440p': CONFIG.TRANSCODING.RESOLUTIONS['1440p'],
228 '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p'] 228 '2160p': CONFIG.TRANSCODING.RESOLUTIONS['2160p']
229 }, 229 },
230 alwaysTranscodeOriginalResolution: CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION,
230 webtorrent: { 231 webtorrent: {
231 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED 232 enabled: CONFIG.TRANSCODING.WEBTORRENT.ENABLED
232 }, 233 },
@@ -256,7 +257,8 @@ function customConfig (): CustomConfig {
256 '1080p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1080p'], 257 '1080p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1080p'],
257 '1440p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1440p'], 258 '1440p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['1440p'],
258 '2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p'] 259 '2160p': CONFIG.LIVE.TRANSCODING.RESOLUTIONS['2160p']
259 } 260 },
261 alwaysTranscodeOriginalResolution: CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION
260 } 262 }
261 }, 263 },
262 videoStudio: { 264 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 @@
1import express from 'express' 1import express from 'express'
2import { computeLowerResolutionsToTranscode } from '@server/helpers/ffmpeg' 2import { computeResolutionsToTranscode } from '@server/helpers/ffmpeg'
3import { logger, loggerTagsFactory } from '@server/helpers/logger' 3import { logger, loggerTagsFactory } from '@server/helpers/logger'
4import { addTranscodingJob } from '@server/lib/video' 4import { addTranscodingJob } from '@server/lib/video'
5import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' 5import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models'
@@ -30,9 +30,9 @@ async function createTranscoding (req: express.Request, res: express.Response) {
30 30
31 const body: VideoTranscodingCreate = req.body 31 const body: VideoTranscodingCreate = req.body
32 32
33 const { resolution: maxResolution, isPortraitMode, audioStream } = await video.probeMaxQualityFile() 33 const { resolution: maxResolution, audioStream } = await video.probeMaxQualityFile()
34 const resolutions = await Hooks.wrapObject( 34 const resolutions = await Hooks.wrapObject(
35 computeLowerResolutionsToTranscode(maxResolution, 'vod').concat([ maxResolution ]), 35 computeResolutionsToTranscode({ inputResolution: maxResolution, type: 'vod', includeInputResolution: true }),
36 'filter:transcoding.manual.lower-resolutions-to-transcode.result', 36 'filter:transcoding.manual.lower-resolutions-to-transcode.result',
37 body 37 body
38 ) 38 )
@@ -50,7 +50,6 @@ async function createTranscoding (req: express.Request, res: express.Response) {
50 type: 'new-resolution-to-hls', 50 type: 'new-resolution-to-hls',
51 videoUUID: video.uuid, 51 videoUUID: video.uuid,
52 resolution, 52 resolution,
53 isPortraitMode,
54 hasAudio: !!audioStream, 53 hasAudio: !!audioStream,
55 copyCodecs: false, 54 copyCodecs: false,
56 isNewVideo: false, 55 isNewVideo: false,
@@ -64,8 +63,7 @@ async function createTranscoding (req: express.Request, res: express.Response) {
64 isNewVideo: false, 63 isNewVideo: false,
65 resolution, 64 resolution,
66 hasAudio: !!audioStream, 65 hasAudio: !!audioStream,
67 createHLSIfNeeded: false, 66 createHLSIfNeeded: false
68 isPortraitMode
69 }) 67 })
70 } 68 }
71 } 69 }
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'
7import { logger, loggerTagsFactory } from '../logger' 7import { logger, loggerTagsFactory } from '../logger'
8import { getFFmpeg, runCommand } from './ffmpeg-commons' 8import { getFFmpeg, runCommand } from './ffmpeg-commons'
9import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets' 9import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets'
10import { computeFPS, getVideoStreamFPS } from './ffprobe-utils' 10import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils'
11import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' 11import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants'
12 12
13const lTags = loggerTagsFactory('ffmpeg') 13const lTags = loggerTagsFactory('ffmpeg')
@@ -27,8 +27,6 @@ interface BaseTranscodeVODOptions {
27 27
28 resolution: number 28 resolution: number
29 29
30 isPortraitMode?: boolean
31
32 job?: Job 30 job?: Job
33} 31}
34 32
@@ -115,13 +113,17 @@ export {
115// --------------------------------------------------------------------------- 113// ---------------------------------------------------------------------------
116 114
117async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) { 115async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) {
118 let fps = await getVideoStreamFPS(options.inputPath) 116 const probe = await ffprobePromise(options.inputPath)
117
118 let fps = await getVideoStreamFPS(options.inputPath, probe)
119 fps = computeFPS(fps, options.resolution) 119 fps = computeFPS(fps, options.resolution)
120 120
121 let scaleFilterValue: string 121 let scaleFilterValue: string
122 122
123 if (options.resolution !== undefined) { 123 if (options.resolution !== undefined) {
124 scaleFilterValue = options.isPortraitMode === true 124 const videoStreamInfo = await getVideoStreamDimensionsInfo(options.inputPath, probe)
125
126 scaleFilterValue = videoStreamInfo?.isPortraitMode === true
125 ? `w=${options.resolution}:h=-2` 127 ? `w=${options.resolution}:h=-2`
126 : `w=-2:h=${options.resolution}` 128 : `w=-2:h=${options.resolution}`
127 } 129 }
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) {
90// Resolutions 90// Resolutions
91// --------------------------------------------------------------------------- 91// ---------------------------------------------------------------------------
92 92
93function computeLowerResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') { 93function computeResolutionsToTranscode (options: {
94 inputResolution: number
95 type: 'vod' | 'live'
96 includeInputResolution: boolean
97}) {
98 const { inputResolution, type, includeInputResolution } = options
99
94 const configResolutions = type === 'vod' 100 const configResolutions = type === 'vod'
95 ? CONFIG.TRANSCODING.RESOLUTIONS 101 ? CONFIG.TRANSCODING.RESOLUTIONS
96 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS 102 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
97 103
98 const resolutionsEnabled: number[] = [] 104 const resolutionsEnabled = new Set<number>()
99 105
100 // Put in the order we want to proceed jobs 106 // Put in the order we want to proceed jobs
101 const resolutions: VideoResolution[] = [ 107 const availableResolutions: VideoResolution[] = [
102 VideoResolution.H_NOVIDEO, 108 VideoResolution.H_NOVIDEO,
103 VideoResolution.H_480P, 109 VideoResolution.H_480P,
104 VideoResolution.H_360P, 110 VideoResolution.H_360P,
@@ -110,13 +116,17 @@ function computeLowerResolutionsToTranscode (videoFileResolution: number, type:
110 VideoResolution.H_4K 116 VideoResolution.H_4K
111 ] 117 ]
112 118
113 for (const resolution of resolutions) { 119 for (const resolution of availableResolutions) {
114 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) { 120 if (configResolutions[resolution + 'p'] === true && inputResolution > resolution) {
115 resolutionsEnabled.push(resolution) 121 resolutionsEnabled.add(resolution)
116 } 122 }
117 } 123 }
118 124
119 return resolutionsEnabled 125 if (includeInputResolution) {
126 resolutionsEnabled.add(inputResolution)
127 }
128
129 return Array.from(resolutionsEnabled)
120} 130}
121 131
122// --------------------------------------------------------------------------- 132// ---------------------------------------------------------------------------
@@ -224,7 +234,7 @@ export {
224 computeFPS, 234 computeFPS,
225 getClosestFramerateStandard, 235 getClosestFramerateStandard,
226 236
227 computeLowerResolutionsToTranscode, 237 computeResolutionsToTranscode,
228 238
229 canDoQuickTranscode, 239 canDoQuickTranscode,
230 canDoQuickVideoTranscode, 240 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 () {
30 'transcoding.profile', 'transcoding.concurrency', 30 'transcoding.profile', 'transcoding.concurrency',
31 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p', 31 'transcoding.resolutions.0p', 'transcoding.resolutions.144p', 'transcoding.resolutions.240p', 'transcoding.resolutions.360p',
32 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p', 32 'transcoding.resolutions.480p', 'transcoding.resolutions.720p', 'transcoding.resolutions.1080p', 'transcoding.resolutions.1440p',
33 'transcoding.resolutions.2160p', 'video_studio.enabled', 33 'transcoding.resolutions.2160p', 'transcoding.always_transcode_original_resolution', 'video_studio.enabled',
34 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout', 34 'import.videos.http.enabled', 'import.videos.torrent.enabled', 'import.videos.concurrency', 'import.videos.timeout',
35 'auto_blacklist.videos.of_users.enabled', 'trending.videos.interval_days', 35 'auto_blacklist.videos.of_users.enabled', 'trending.videos.interval_days',
36 'client.videos.miniature.display_author_avatar', 36 'client.videos.miniature.display_author_avatar',
@@ -59,7 +59,7 @@ function checkMissedConfig () {
59 'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile', 59 'live.transcoding.enabled', 'live.transcoding.threads', 'live.transcoding.profile',
60 'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p', 60 'live.transcoding.resolutions.144p', 'live.transcoding.resolutions.240p', 'live.transcoding.resolutions.360p',
61 'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p', 61 'live.transcoding.resolutions.480p', 'live.transcoding.resolutions.720p', 'live.transcoding.resolutions.1080p',
62 'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p' 62 'live.transcoding.resolutions.1440p', 'live.transcoding.resolutions.2160p', 'live.transcoding.always_transcode_original_resolution'
63 ] 63 ]
64 64
65 const requiredAlternatives = [ 65 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 = {
309 get THREADS () { return config.get<number>('transcoding.threads') }, 309 get THREADS () { return config.get<number>('transcoding.threads') },
310 get CONCURRENCY () { return config.get<number>('transcoding.concurrency') }, 310 get CONCURRENCY () { return config.get<number>('transcoding.concurrency') },
311 get PROFILE () { return config.get<string>('transcoding.profile') }, 311 get PROFILE () { return config.get<string>('transcoding.profile') },
312 get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get<boolean>('transcoding.always_transcode_original_resolution') },
312 RESOLUTIONS: { 313 RESOLUTIONS: {
313 get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') }, 314 get '0p' () { return config.get<boolean>('transcoding.resolutions.0p') },
314 get '144p' () { return config.get<boolean>('transcoding.resolutions.144p') }, 315 get '144p' () { return config.get<boolean>('transcoding.resolutions.144p') },
@@ -361,6 +362,8 @@ const CONFIG = {
361 get THREADS () { return config.get<number>('live.transcoding.threads') }, 362 get THREADS () { return config.get<number>('live.transcoding.threads') },
362 get PROFILE () { return config.get<string>('live.transcoding.profile') }, 363 get PROFILE () { return config.get<string>('live.transcoding.profile') },
363 364
365 get ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION () { return config.get<boolean>('live.transcoding.always_transcode_original_resolution') },
366
364 RESOLUTIONS: { 367 RESOLUTIONS: {
365 get '144p' () { return config.get<boolean>('live.transcoding.resolutions.144p') }, 368 get '144p' () { return config.get<boolean>('live.transcoding.resolutions.144p') },
366 get '240p' () { return config.get<boolean>('live.transcoding.resolutions.240p') }, 369 get '240p' () { return config.get<boolean>('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: {
213 const probe = await ffprobePromise(concatenatedTsFilePath) 213 const probe = await ffprobePromise(concatenatedTsFilePath)
214 const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) 214 const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe)
215 215
216 const { resolution, isPortraitMode } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe) 216 const { resolution } = await getVideoStreamDimensionsInfo(concatenatedTsFilePath, probe)
217 217
218 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({ 218 const { resolutionPlaylistPath: outputPath } = await generateHlsPlaylistResolutionFromTS({
219 video, 219 video,
220 concatenatedTsFilePath, 220 concatenatedTsFilePath,
221 resolution, 221 resolution,
222 isPortraitMode,
223 isAAC: audioStream?.codec_name === 'aac' 222 isAAC: audioStream?.codec_name === 'aac'
224 }) 223 })
225 224
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 @@
1import { Job } from 'bull' 1import { Job } from 'bull'
2import { TranscodeVODOptionsType } from '@server/helpers/ffmpeg' 2import { TranscodeVODOptionsType } from '@server/helpers/ffmpeg'
3import { Hooks } from '@server/lib/plugins/hooks'
3import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video' 4import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video'
4import { VideoPathManager } from '@server/lib/video-path-manager' 5import { VideoPathManager } from '@server/lib/video-path-manager'
5import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state' 6import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state'
@@ -16,7 +17,7 @@ import {
16 VideoTranscodingPayload 17 VideoTranscodingPayload
17} from '@shared/models' 18} from '@shared/models'
18import { retryTransactionWrapper } from '../../../helpers/database-utils' 19import { retryTransactionWrapper } from '../../../helpers/database-utils'
19import { computeLowerResolutionsToTranscode } from '../../../helpers/ffmpeg' 20import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg'
20import { logger, loggerTagsFactory } from '../../../helpers/logger' 21import { logger, loggerTagsFactory } from '../../../helpers/logger'
21import { CONFIG } from '../../../initializers/config' 22import { CONFIG } from '../../../initializers/config'
22import { VideoModel } from '../../../models/video/video' 23import { VideoModel } from '../../../models/video/video'
@@ -26,7 +27,6 @@ import {
26 optimizeOriginalVideofile, 27 optimizeOriginalVideofile,
27 transcodeNewWebTorrentResolution 28 transcodeNewWebTorrentResolution
28} from '../../transcoding/transcoding' 29} from '../../transcoding/transcoding'
29import { Hooks } from '@server/lib/plugins/hooks'
30 30
31type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void> 31type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void>
32 32
@@ -99,7 +99,6 @@ async function handleHLSJob (job: Job, payload: HLSTranscodingPayload, video: MV
99 videoInputPath, 99 videoInputPath,
100 resolution: payload.resolution, 100 resolution: payload.resolution,
101 copyCodecs: payload.copyCodecs, 101 copyCodecs: payload.copyCodecs,
102 isPortraitMode: payload.isPortraitMode || false,
103 job 102 job
104 }) 103 })
105 }) 104 })
@@ -117,7 +116,7 @@ async function handleNewWebTorrentResolutionJob (
117) { 116) {
118 logger.info('Handling WebTorrent transcoding job for %s.', video.uuid, lTags(video.uuid)) 117 logger.info('Handling WebTorrent transcoding job for %s.', video.uuid, lTags(video.uuid))
119 118
120 await transcodeNewWebTorrentResolution(video, payload.resolution, payload.isPortraitMode || false, job) 119 await transcodeNewWebTorrentResolution({ video, resolution: payload.resolution, job })
121 120
122 logger.info('WebTorrent transcoding job for %s ended.', video.uuid, lTags(video.uuid)) 121 logger.info('WebTorrent transcoding job for %s ended.', video.uuid, lTags(video.uuid))
123 122
@@ -127,7 +126,7 @@ async function handleNewWebTorrentResolutionJob (
127async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) { 126async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) {
128 logger.info('Handling merge audio transcoding job for %s.', video.uuid, lTags(video.uuid)) 127 logger.info('Handling merge audio transcoding job for %s.', video.uuid, lTags(video.uuid))
129 128
130 await mergeAudioVideofile(video, payload.resolution, job) 129 await mergeAudioVideofile({ video, resolution: payload.resolution, job })
131 130
132 logger.info('Merge audio transcoding job for %s ended.', video.uuid, lTags(video.uuid)) 131 logger.info('Merge audio transcoding job for %s ended.', video.uuid, lTags(video.uuid))
133 132
@@ -137,7 +136,7 @@ async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTrans
137async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { 136async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) {
138 logger.info('Handling optimize transcoding job for %s.', video.uuid, lTags(video.uuid)) 137 logger.info('Handling optimize transcoding job for %s.', video.uuid, lTags(video.uuid))
139 138
140 const { transcodeType } = await optimizeOriginalVideofile(video, video.getMaxQualityFile(), job) 139 const { transcodeType } = await optimizeOriginalVideofile({ video, inputVideoFile: video.getMaxQualityFile(), job })
141 140
142 logger.info('Optimize transcoding job for %s ended.', video.uuid, lTags(video.uuid)) 141 logger.info('Optimize transcoding job for %s ended.', video.uuid, lTags(video.uuid))
143 142
@@ -161,7 +160,6 @@ async function onHlsPlaylistGeneration (video: MVideoFullLight, user: MUser, pay
161 video, 160 video,
162 user, 161 user,
163 videoFileResolution: payload.resolution, 162 videoFileResolution: payload.resolution,
164 isPortraitMode: payload.isPortraitMode,
165 hasAudio: payload.hasAudio, 163 hasAudio: payload.hasAudio,
166 isNewVideo: payload.isNewVideo ?? true, 164 isNewVideo: payload.isNewVideo ?? true,
167 type: 'hls' 165 type: 'hls'
@@ -178,7 +176,7 @@ async function onVideoFirstWebTorrentTranscoding (
178 transcodeType: TranscodeVODOptionsType, 176 transcodeType: TranscodeVODOptionsType,
179 user: MUserId 177 user: MUserId
180) { 178) {
181 const { resolution, isPortraitMode, audioStream } = await videoArg.probeMaxQualityFile() 179 const { resolution, audioStream } = await videoArg.probeMaxQualityFile()
182 180
183 // Maybe the video changed in database, refresh it 181 // Maybe the video changed in database, refresh it
184 const videoDatabase = await VideoModel.loadFull(videoArg.uuid) 182 const videoDatabase = await VideoModel.loadFull(videoArg.uuid)
@@ -189,7 +187,6 @@ async function onVideoFirstWebTorrentTranscoding (
189 const originalFileHLSPayload = { 187 const originalFileHLSPayload = {
190 ...payload, 188 ...payload,
191 189
192 isPortraitMode,
193 hasAudio: !!audioStream, 190 hasAudio: !!audioStream,
194 resolution: videoDatabase.getMaxQualityFile().resolution, 191 resolution: videoDatabase.getMaxQualityFile().resolution,
195 // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues 192 // If we quick transcoded original file, force transcoding for HLS to avoid some weird playback issues
@@ -202,7 +199,6 @@ async function onVideoFirstWebTorrentTranscoding (
202 user, 199 user,
203 videoFileResolution: resolution, 200 videoFileResolution: resolution,
204 hasAudio: !!audioStream, 201 hasAudio: !!audioStream,
205 isPortraitMode,
206 type: 'webtorrent', 202 type: 'webtorrent',
207 isNewVideo: payload.isNewVideo ?? true 203 isNewVideo: payload.isNewVideo ?? true
208 }) 204 })
@@ -235,7 +231,6 @@ async function createHlsJobIfEnabled (user: MUserId, payload: {
235 videoUUID: string 231 videoUUID: string
236 resolution: number 232 resolution: number
237 hasAudio: boolean 233 hasAudio: boolean
238 isPortraitMode?: boolean
239 copyCodecs: boolean 234 copyCodecs: boolean
240 isMaxQuality: boolean 235 isMaxQuality: boolean
241 isNewVideo?: boolean 236 isNewVideo?: boolean
@@ -250,7 +245,7 @@ async function createHlsJobIfEnabled (user: MUserId, payload: {
250 type: 'new-resolution-to-hls', 245 type: 'new-resolution-to-hls',
251 autoDeleteWebTorrentIfNeeded: true, 246 autoDeleteWebTorrentIfNeeded: true,
252 247
253 ...pick(payload, [ 'videoUUID', 'resolution', 'isPortraitMode', 'copyCodecs', 'isMaxQuality', 'isNewVideo', 'hasAudio' ]) 248 ...pick(payload, [ 'videoUUID', 'resolution', 'copyCodecs', 'isMaxQuality', 'isNewVideo', 'hasAudio' ])
254 } 249 }
255 250
256 await addTranscodingJob(hlsTranscodingPayload, jobOptions) 251 await addTranscodingJob(hlsTranscodingPayload, jobOptions)
@@ -262,16 +257,15 @@ async function createLowerResolutionsJobs (options: {
262 video: MVideoFullLight 257 video: MVideoFullLight
263 user: MUserId 258 user: MUserId
264 videoFileResolution: number 259 videoFileResolution: number
265 isPortraitMode: boolean
266 hasAudio: boolean 260 hasAudio: boolean
267 isNewVideo: boolean 261 isNewVideo: boolean
268 type: 'hls' | 'webtorrent' 262 type: 'hls' | 'webtorrent'
269}) { 263}) {
270 const { video, user, videoFileResolution, isPortraitMode, isNewVideo, hasAudio, type } = options 264 const { video, user, videoFileResolution, isNewVideo, hasAudio, type } = options
271 265
272 // Create transcoding jobs if there are enabled resolutions 266 // Create transcoding jobs if there are enabled resolutions
273 const resolutionsEnabled = await Hooks.wrapObject( 267 const resolutionsEnabled = await Hooks.wrapObject(
274 computeLowerResolutionsToTranscode(videoFileResolution, 'vod'), 268 computeResolutionsToTranscode({ inputResolution: videoFileResolution, type: 'vod', includeInputResolution: false }),
275 'filter:transcoding.auto.lower-resolutions-to-transcode.result', 269 'filter:transcoding.auto.lower-resolutions-to-transcode.result',
276 options 270 options
277 ) 271 )
@@ -289,7 +283,6 @@ async function createLowerResolutionsJobs (options: {
289 type: 'new-resolution-to-webtorrent', 283 type: 'new-resolution-to-webtorrent',
290 videoUUID: video.uuid, 284 videoUUID: video.uuid,
291 resolution, 285 resolution,
292 isPortraitMode,
293 hasAudio, 286 hasAudio,
294 createHLSIfNeeded: true, 287 createHLSIfNeeded: true,
295 isNewVideo 288 isNewVideo
@@ -303,7 +296,6 @@ async function createLowerResolutionsJobs (options: {
303 type: 'new-resolution-to-hls', 296 type: 'new-resolution-to-hls',
304 videoUUID: video.uuid, 297 videoUUID: video.uuid,
305 resolution, 298 resolution,
306 isPortraitMode,
307 hasAudio, 299 hasAudio,
308 copyCodecs: false, 300 copyCodecs: false,
309 isMaxQuality: false, 301 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'
4import { join } from 'path' 4import { join } from 'path'
5import { createServer as createServerTLS, Server as ServerTLS } from 'tls' 5import { createServer as createServerTLS, Server as ServerTLS } from 'tls'
6import { 6import {
7 computeLowerResolutionsToTranscode, 7 computeResolutionsToTranscode,
8 ffprobePromise, 8 ffprobePromise,
9 getLiveSegmentTime, 9 getLiveSegmentTime,
10 getVideoStreamBitrate, 10 getVideoStreamBitrate,
@@ -26,10 +26,10 @@ import { federateVideoIfNeeded } from '../activitypub/videos'
26import { JobQueue } from '../job-queue' 26import { JobQueue } from '../job-queue'
27import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '../paths' 27import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '../paths'
28import { PeerTubeSocket } from '../peertube-socket' 28import { PeerTubeSocket } from '../peertube-socket'
29import { Hooks } from '../plugins/hooks'
29import { LiveQuotaStore } from './live-quota-store' 30import { LiveQuotaStore } from './live-quota-store'
30import { cleanupPermanentLive } from './live-utils' 31import { cleanupPermanentLive } from './live-utils'
31import { MuxingSession } from './shared' 32import { MuxingSession } from './shared'
32import { Hooks } from '../plugins/hooks'
33 33
34const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') 34const NodeRtmpSession = require('node-media-server/src/node_rtmp_session')
35const context = require('node-media-server/src/node_core_ctx') 35const context = require('node-media-server/src/node_core_ctx')
@@ -456,11 +456,17 @@ class LiveManager {
456 } 456 }
457 457
458 private buildAllResolutionsToTranscode (originResolution: number) { 458 private buildAllResolutionsToTranscode (originResolution: number) {
459 const includeInputResolution = CONFIG.LIVE.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION
460
459 const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED 461 const resolutionsEnabled = CONFIG.LIVE.TRANSCODING.ENABLED
460 ? computeLowerResolutionsToTranscode(originResolution, 'live') 462 ? computeResolutionsToTranscode({ inputResolution: originResolution, type: 'live', includeInputResolution })
461 : [] 463 : []
462 464
463 return resolutionsEnabled.concat([ originResolution ]) 465 if (resolutionsEnabled.length === 0) {
466 return [ originResolution ]
467 }
468
469 return resolutionsEnabled
464 } 470 }
465 471
466 private async createLivePlaylist (video: MVideo, allResolutions: number[]): Promise<MStreamingPlaylistVideo> { 472 private async createLivePlaylist (video: MVideo, allResolutions: number[]): Promise<MStreamingPlaylistVideo> {
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'
10import { 10import {
11 buildFileMetadata, 11 buildFileMetadata,
12 canDoQuickTranscode, 12 canDoQuickTranscode,
13 computeResolutionsToTranscode,
13 getVideoStreamDuration, 14 getVideoStreamDuration,
14 getVideoStreamFPS, 15 getVideoStreamFPS,
15 transcodeVOD, 16 transcodeVOD,
@@ -32,7 +33,13 @@ import { VideoTranscodingProfilesManager } from './default-transcoding-profiles'
32 */ 33 */
33 34
34// Optimize the original video file and replace it. The resolution is not changed. 35// Optimize the original video file and replace it. The resolution is not changed.
35function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) { 36function optimizeOriginalVideofile (options: {
37 video: MVideoFullLight
38 inputVideoFile: MVideoFile
39 job: Job
40}) {
41 const { video, inputVideoFile, job } = options
42
36 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 43 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
37 const newExtname = '.mp4' 44 const newExtname = '.mp4'
38 45
@@ -43,7 +50,7 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid
43 ? 'quick-transcode' 50 ? 'quick-transcode'
44 : 'video' 51 : 'video'
45 52
46 const resolution = toEven(inputVideoFile.resolution) 53 const resolution = buildOriginalFileResolution(inputVideoFile.resolution)
47 54
48 const transcodeOptions: TranscodeVODOptions = { 55 const transcodeOptions: TranscodeVODOptions = {
49 type: transcodeType, 56 type: transcodeType,
@@ -63,6 +70,7 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid
63 await transcodeVOD(transcodeOptions) 70 await transcodeVOD(transcodeOptions)
64 71
65 // Important to do this before getVideoFilename() to take in account the new filename 72 // Important to do this before getVideoFilename() to take in account the new filename
73 inputVideoFile.resolution = resolution
66 inputVideoFile.extname = newExtname 74 inputVideoFile.extname = newExtname
67 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname) 75 inputVideoFile.filename = generateWebTorrentVideoFilename(resolution, newExtname)
68 inputVideoFile.storage = VideoStorage.FILE_SYSTEM 76 inputVideoFile.storage = VideoStorage.FILE_SYSTEM
@@ -76,17 +84,22 @@ function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVid
76 }) 84 })
77} 85}
78 86
79// Transcode the original video file to a lower resolution 87// Transcode the original video file to a lower resolution compatible with WebTorrent
80// We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed 88function transcodeNewWebTorrentResolution (options: {
81function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: VideoResolution, isPortrait: boolean, job: Job) { 89 video: MVideoFullLight
90 resolution: VideoResolution
91 job: Job
92}) {
93 const { video, resolution, job } = options
94
82 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 95 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
83 const extname = '.mp4' 96 const newExtname = '.mp4'
84 97
85 return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => { 98 return VideoPathManager.Instance.makeAvailableVideoFile(video.getMaxQualityFile().withVideoOrPlaylist(video), async videoInputPath => {
86 const newVideoFile = new VideoFileModel({ 99 const newVideoFile = new VideoFileModel({
87 resolution, 100 resolution,
88 extname, 101 extname: newExtname,
89 filename: generateWebTorrentVideoFilename(resolution, extname), 102 filename: generateWebTorrentVideoFilename(resolution, newExtname),
90 size: 0, 103 size: 0,
91 videoId: video.id 104 videoId: video.id
92 }) 105 })
@@ -117,7 +130,6 @@ function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: V
117 profile: CONFIG.TRANSCODING.PROFILE, 130 profile: CONFIG.TRANSCODING.PROFILE,
118 131
119 resolution, 132 resolution,
120 isPortraitMode: isPortrait,
121 133
122 job 134 job
123 } 135 }
@@ -129,7 +141,13 @@ function transcodeNewWebTorrentResolution (video: MVideoFullLight, resolution: V
129} 141}
130 142
131// Merge an image with an audio file to create a video 143// Merge an image with an audio file to create a video
132function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) { 144function mergeAudioVideofile (options: {
145 video: MVideoFullLight
146 resolution: VideoResolution
147 job: Job
148}) {
149 const { video, resolution, job } = options
150
133 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 151 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
134 const newExtname = '.mp4' 152 const newExtname = '.mp4'
135 153
@@ -188,13 +206,11 @@ async function generateHlsPlaylistResolutionFromTS (options: {
188 video: MVideo 206 video: MVideo
189 concatenatedTsFilePath: string 207 concatenatedTsFilePath: string
190 resolution: VideoResolution 208 resolution: VideoResolution
191 isPortraitMode: boolean
192 isAAC: boolean 209 isAAC: boolean
193}) { 210}) {
194 return generateHlsPlaylistCommon({ 211 return generateHlsPlaylistCommon({
195 video: options.video, 212 video: options.video,
196 resolution: options.resolution, 213 resolution: options.resolution,
197 isPortraitMode: options.isPortraitMode,
198 inputPath: options.concatenatedTsFilePath, 214 inputPath: options.concatenatedTsFilePath,
199 type: 'hls-from-ts' as 'hls-from-ts', 215 type: 'hls-from-ts' as 'hls-from-ts',
200 isAAC: options.isAAC 216 isAAC: options.isAAC
@@ -207,14 +223,12 @@ function generateHlsPlaylistResolution (options: {
207 videoInputPath: string 223 videoInputPath: string
208 resolution: VideoResolution 224 resolution: VideoResolution
209 copyCodecs: boolean 225 copyCodecs: boolean
210 isPortraitMode: boolean
211 job?: Job 226 job?: Job
212}) { 227}) {
213 return generateHlsPlaylistCommon({ 228 return generateHlsPlaylistCommon({
214 video: options.video, 229 video: options.video,
215 resolution: options.resolution, 230 resolution: options.resolution,
216 copyCodecs: options.copyCodecs, 231 copyCodecs: options.copyCodecs,
217 isPortraitMode: options.isPortraitMode,
218 inputPath: options.videoInputPath, 232 inputPath: options.videoInputPath,
219 type: 'hls' as 'hls', 233 type: 'hls' as 'hls',
220 job: options.job 234 job: options.job
@@ -267,11 +281,10 @@ async function generateHlsPlaylistCommon (options: {
267 resolution: VideoResolution 281 resolution: VideoResolution
268 copyCodecs?: boolean 282 copyCodecs?: boolean
269 isAAC?: boolean 283 isAAC?: boolean
270 isPortraitMode: boolean
271 284
272 job?: Job 285 job?: Job
273}) { 286}) {
274 const { type, video, inputPath, resolution, copyCodecs, isPortraitMode, isAAC, job } = options 287 const { type, video, inputPath, resolution, copyCodecs, isAAC, job } = options
275 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR 288 const transcodeDirectory = CONFIG.STORAGE.TMP_DIR
276 289
277 const videoTranscodedBasePath = join(transcodeDirectory, type) 290 const videoTranscodedBasePath = join(transcodeDirectory, type)
@@ -292,7 +305,6 @@ async function generateHlsPlaylistCommon (options: {
292 305
293 resolution, 306 resolution,
294 copyCodecs, 307 copyCodecs,
295 isPortraitMode,
296 308
297 isAAC, 309 isAAC,
298 310
@@ -350,3 +362,12 @@ async function generateHlsPlaylistCommon (options: {
350 362
351 return { resolutionPlaylistPath, videoFile: savedVideoFile } 363 return { resolutionPlaylistPath, videoFile: savedVideoFile }
352} 364}
365
366function buildOriginalFileResolution (inputResolution: number) {
367 if (CONFIG.TRANSCODING.ALWAYS_TRANSCODE_ORIGINAL_RESOLUTION === true) return toEven(inputResolution)
368
369 const resolutions = computeResolutionsToTranscode({ inputResolution, type: 'vod', includeInputResolution: false })
370 if (resolutions.length === 0) return toEven(inputResolution)
371
372 return Math.max(...resolutions)
373}
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 = [
54 body('transcoding.resolutions.1440p').isBoolean().withMessage('Should have a valid transcoding 1440p resolution enabled boolean'), 54 body('transcoding.resolutions.1440p').isBoolean().withMessage('Should have a valid transcoding 1440p resolution enabled boolean'),
55 body('transcoding.resolutions.2160p').isBoolean().withMessage('Should have a valid transcoding 2160p resolution enabled boolean'), 55 body('transcoding.resolutions.2160p').isBoolean().withMessage('Should have a valid transcoding 2160p resolution enabled boolean'),
56 56
57 body('transcoding.alwaysTranscodeOriginalResolution').isBoolean()
58 .withMessage('Should have a valid always transcode original resolution boolean'),
59
57 body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'), 60 body('transcoding.webtorrent.enabled').isBoolean().withMessage('Should have a valid webtorrent transcoding enabled boolean'),
58 body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid hls transcoding enabled boolean'), 61 body('transcoding.hls.enabled').isBoolean().withMessage('Should have a valid hls transcoding enabled boolean'),
59 62
@@ -91,6 +94,8 @@ const customConfigUpdateValidator = [
91 body('live.transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'), 94 body('live.transcoding.resolutions.1080p').isBoolean().withMessage('Should have a valid transcoding 1080p resolution enabled boolean'),
92 body('live.transcoding.resolutions.1440p').isBoolean().withMessage('Should have a valid transcoding 1440p resolution enabled boolean'), 95 body('live.transcoding.resolutions.1440p').isBoolean().withMessage('Should have a valid transcoding 1440p resolution enabled boolean'),
93 body('live.transcoding.resolutions.2160p').isBoolean().withMessage('Should have a valid transcoding 2160p resolution enabled boolean'), 96 body('live.transcoding.resolutions.2160p').isBoolean().withMessage('Should have a valid transcoding 2160p resolution enabled boolean'),
97 body('live.transcoding.alwaysTranscodeOriginalResolution').isBoolean()
98 .withMessage('Should have a valid always transcode live original resolution boolean'),
94 99
95 body('search.remoteUri.users').isBoolean().withMessage('Should have a remote URI search for users boolean'), 100 body('search.remoteUri.users').isBoolean().withMessage('Should have a remote URI search for users boolean'),
96 body('search.remoteUri.anonymous').isBoolean().withMessage('Should have a valid remote URI search for anonymous boolean'), 101 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 () {
114 '1440p': false, 114 '1440p': false,
115 '2160p': false 115 '2160p': false
116 }, 116 },
117 alwaysTranscodeOriginalResolution: false,
117 webtorrent: { 118 webtorrent: {
118 enabled: true 119 enabled: true
119 }, 120 },
@@ -145,7 +146,8 @@ describe('Test config API validators', function () {
145 '1080p': true, 146 '1080p': true,
146 '1440p': true, 147 '1440p': true,
147 '2160p': true 148 '2160p': true
148 } 149 },
150 alwaysTranscodeOriginalResolution: false
149 } 151 }
150 }, 152 },
151 videoStudio: { 153 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'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { basename, join } from 'path' 5import { basename, join } from 'path'
6import { ffprobePromise, getVideoStream } from '@server/helpers/ffmpeg' 6import { ffprobePromise, getVideoStream } from '@server/helpers/ffmpeg'
7import { checkLiveCleanup, checkLiveSegmentHash, checkResolutionsInMasterPlaylist, testImage } from '@server/tests/shared' 7import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist, getAllFiles, testImage } from '@server/tests/shared'
8import { wait } from '@shared/core-utils' 8import { wait } from '@shared/core-utils'
9import { 9import {
10 HttpStatusCode, 10 HttpStatusCode,
@@ -468,7 +468,7 @@ describe('Test live', function () {
468 await waitUntilLivePublishedOnAllServers(servers, liveVideoId) 468 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
469 await waitJobs(servers) 469 await waitJobs(servers)
470 470
471 await testVideoResolutions(liveVideoId, resolutions) 471 await testVideoResolutions(liveVideoId, resolutions.concat([ 720 ]))
472 472
473 await stopFfmpeg(ffmpegCommand) 473 await stopFfmpeg(ffmpegCommand)
474 }) 474 })
@@ -580,10 +580,73 @@ describe('Test live', function () {
580 } 580 }
581 }) 581 })
582 582
583 it('Should correctly have cleaned up the live files', async function () { 583 it('Should not generate an upper resolution than original file', async function () {
584 this.timeout(30000) 584 this.timeout(400_000)
585
586 const resolutions = [ 240, 480 ]
587 await updateConf(resolutions)
588
589 await servers[0].config.updateExistingSubConfig({
590 newConfig: {
591 live: {
592 transcoding: {
593 alwaysTranscodeOriginalResolution: false
594 }
595 }
596 }
597 })
598
599 liveVideoId = await createLiveWrapper(true)
600
601 const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
602 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
603 await waitJobs(servers)
604
605 await testVideoResolutions(liveVideoId, resolutions)
606
607 await stopFfmpeg(ffmpegCommand)
608 await commands[0].waitUntilEnded({ videoId: liveVideoId })
609
610 await waitJobs(servers)
611
612 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
613
614 const video = await servers[0].videos.get({ id: liveVideoId })
615 const hlsFiles = video.streamingPlaylists[0].files
616
617 expect(video.files).to.have.lengthOf(0)
618 expect(hlsFiles).to.have.lengthOf(resolutions.length)
619
620 // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
621 expect(getAllFiles(video).map(f => f.resolution.id).sort()).to.deep.equal(resolutions)
622 })
623
624 it('Should only keep the original resolution if all resolutions are disabled', async function () {
625 this.timeout(400_000)
626
627 await updateConf([])
628 liveVideoId = await createLiveWrapper(true)
629
630 const ffmpegCommand = await commands[0].sendRTMPStreamInVideo({ videoId: liveVideoId, fixtureName: 'video_short2.webm' })
631 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
632 await waitJobs(servers)
633
634 await testVideoResolutions(liveVideoId, [ 720 ])
635
636 await stopFfmpeg(ffmpegCommand)
637 await commands[0].waitUntilEnded({ videoId: liveVideoId })
638
639 await waitJobs(servers)
640
641 await waitUntilLivePublishedOnAllServers(servers, liveVideoId)
642
643 const video = await servers[0].videos.get({ id: liveVideoId })
644 const hlsFiles = video.streamingPlaylists[0].files
645
646 expect(video.files).to.have.lengthOf(0)
647 expect(hlsFiles).to.have.lengthOf(1)
585 648
586 await checkLiveCleanup(servers[0], liveVideoId, [ 240, 360, 720 ]) 649 expect(hlsFiles[0].resolution.id).to.equal(720)
587 }) 650 })
588 }) 651 })
589 652
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) {
77 expect(data.transcoding.resolutions['1080p']).to.be.true 77 expect(data.transcoding.resolutions['1080p']).to.be.true
78 expect(data.transcoding.resolutions['1440p']).to.be.true 78 expect(data.transcoding.resolutions['1440p']).to.be.true
79 expect(data.transcoding.resolutions['2160p']).to.be.true 79 expect(data.transcoding.resolutions['2160p']).to.be.true
80 expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.true
80 expect(data.transcoding.webtorrent.enabled).to.be.true 81 expect(data.transcoding.webtorrent.enabled).to.be.true
81 expect(data.transcoding.hls.enabled).to.be.true 82 expect(data.transcoding.hls.enabled).to.be.true
82 83
@@ -97,6 +98,7 @@ function checkInitialConfig (server: PeerTubeServer, data: CustomConfig) {
97 expect(data.live.transcoding.resolutions['1080p']).to.be.false 98 expect(data.live.transcoding.resolutions['1080p']).to.be.false
98 expect(data.live.transcoding.resolutions['1440p']).to.be.false 99 expect(data.live.transcoding.resolutions['1440p']).to.be.false
99 expect(data.live.transcoding.resolutions['2160p']).to.be.false 100 expect(data.live.transcoding.resolutions['2160p']).to.be.false
101 expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.true
100 102
101 expect(data.videoStudio.enabled).to.be.false 103 expect(data.videoStudio.enabled).to.be.false
102 104
@@ -181,6 +183,7 @@ function checkUpdatedConfig (data: CustomConfig) {
181 expect(data.transcoding.resolutions['720p']).to.be.false 183 expect(data.transcoding.resolutions['720p']).to.be.false
182 expect(data.transcoding.resolutions['1080p']).to.be.false 184 expect(data.transcoding.resolutions['1080p']).to.be.false
183 expect(data.transcoding.resolutions['2160p']).to.be.false 185 expect(data.transcoding.resolutions['2160p']).to.be.false
186 expect(data.transcoding.alwaysTranscodeOriginalResolution).to.be.false
184 expect(data.transcoding.hls.enabled).to.be.false 187 expect(data.transcoding.hls.enabled).to.be.false
185 expect(data.transcoding.webtorrent.enabled).to.be.true 188 expect(data.transcoding.webtorrent.enabled).to.be.true
186 189
@@ -200,6 +203,7 @@ function checkUpdatedConfig (data: CustomConfig) {
200 expect(data.live.transcoding.resolutions['720p']).to.be.true 203 expect(data.live.transcoding.resolutions['720p']).to.be.true
201 expect(data.live.transcoding.resolutions['1080p']).to.be.true 204 expect(data.live.transcoding.resolutions['1080p']).to.be.true
202 expect(data.live.transcoding.resolutions['2160p']).to.be.true 205 expect(data.live.transcoding.resolutions['2160p']).to.be.true
206 expect(data.live.transcoding.alwaysTranscodeOriginalResolution).to.be.false
203 207
204 expect(data.videoStudio.enabled).to.be.true 208 expect(data.videoStudio.enabled).to.be.true
205 209
@@ -318,6 +322,7 @@ const newCustomConfig: CustomConfig = {
318 '1440p': false, 322 '1440p': false,
319 '2160p': false 323 '2160p': false
320 }, 324 },
325 alwaysTranscodeOriginalResolution: false,
321 webtorrent: { 326 webtorrent: {
322 enabled: true 327 enabled: true
323 }, 328 },
@@ -347,7 +352,8 @@ const newCustomConfig: CustomConfig = {
347 '1080p': true, 352 '1080p': true,
348 '1440p': true, 353 '1440p': true,
349 '2160p': true 354 '2160p': true
350 } 355 },
356 alwaysTranscodeOriginalResolution: false
351 } 357 }
352 }, 358 },
353 videoStudio: { 359 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'
7import { generateHighBitrateVideo, generateVideoWithFramerate, getAllFiles } from '@server/tests/shared' 7import { generateHighBitrateVideo, generateVideoWithFramerate, getAllFiles } from '@server/tests/shared'
8import { buildAbsoluteFixturePath, getMaxBitrate, getMinLimitBitrate } from '@shared/core-utils' 8import { buildAbsoluteFixturePath, getMaxBitrate, getMinLimitBitrate } from '@shared/core-utils'
9import { 9import {
10 getAudioStream,
11 buildFileMetadata, 10 buildFileMetadata,
11 getAudioStream,
12 getVideoStreamBitrate, 12 getVideoStreamBitrate,
13 getVideoStreamFPS,
14 getVideoStreamDimensionsInfo, 13 getVideoStreamDimensionsInfo,
14 getVideoStreamFPS,
15 hasAudioStream 15 hasAudioStream
16} from '@shared/extra-utils' 16} from '@shared/extra-utils'
17import { HttpStatusCode, VideoState } from '@shared/models' 17import { HttpStatusCode, VideoState } from '@shared/models'
@@ -727,6 +727,82 @@ describe('Test video transcoding', function () {
727 }) 727 })
728 }) 728 })
729 729
730 describe('Bounded transcoding', function () {
731
732 it('Should not generate an upper resolution than original file', async function () {
733 this.timeout(120_000)
734
735 await servers[0].config.updateExistingSubConfig({
736 newConfig: {
737 transcoding: {
738 enabled: true,
739 hls: { enabled: true },
740 webtorrent: { enabled: true },
741 resolutions: {
742 '0p': false,
743 '144p': false,
744 '240p': true,
745 '360p': false,
746 '480p': true,
747 '720p': false,
748 '1080p': false,
749 '1440p': false,
750 '2160p': false
751 },
752 alwaysTranscodeOriginalResolution: false
753 }
754 }
755 })
756
757 const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
758 await waitJobs(servers)
759
760 const video = await servers[0].videos.get({ id: uuid })
761 const hlsFiles = video.streamingPlaylists[0].files
762
763 expect(video.files).to.have.lengthOf(2)
764 expect(hlsFiles).to.have.lengthOf(2)
765
766 // eslint-disable-next-line @typescript-eslint/require-array-sort-compare
767 const resolutions = getAllFiles(video).map(f => f.resolution.id).sort()
768 expect(resolutions).to.deep.equal([ 240, 240, 480, 480 ])
769 })
770
771 it('Should only keep the original resolution if all resolutions are disabled', async function () {
772 this.timeout(120_000)
773
774 await servers[0].config.updateExistingSubConfig({
775 newConfig: {
776 transcoding: {
777 resolutions: {
778 '0p': false,
779 '144p': false,
780 '240p': false,
781 '360p': false,
782 '480p': false,
783 '720p': false,
784 '1080p': false,
785 '1440p': false,
786 '2160p': false
787 }
788 }
789 }
790 })
791
792 const { uuid } = await servers[0].videos.quickUpload({ name: 'video', fixture: 'video_short.webm' })
793 await waitJobs(servers)
794
795 const video = await servers[0].videos.get({ id: uuid })
796 const hlsFiles = video.streamingPlaylists[0].files
797
798 expect(video.files).to.have.lengthOf(1)
799 expect(hlsFiles).to.have.lengthOf(1)
800
801 expect(video.files[0].resolution.id).to.equal(720)
802 expect(hlsFiles[0].resolution.id).to.equal(720)
803 })
804 })
805
730 after(async function () { 806 after(async function () {
731 await cleanupTests(servers) 807 await cleanupTests(servers)
732 }) 808 })
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: {
68 68
69 expect(masterPlaylist).to.match(reg) 69 expect(masterPlaylist).to.match(reg)
70 } 70 }
71
72 const playlistsLength = masterPlaylist.split('\n').filter(line => line.startsWith('#EXT-X-STREAM-INF:BANDWIDTH='))
73 expect(playlistsLength).to.have.lengthOf(resolutions.length)
71} 74}
72 75
73export { 76export {