aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts4
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-live-configuration.component.html15
-rw-r--r--client/src/app/+admin/config/edit-custom-config/edit-vod-transcoding.component.html8
-rw-r--r--config/default.yaml6
-rw-r--r--config/production.yaml.example6
-rwxr-xr-xscripts/create-transcoding-job.ts6
-rw-r--r--scripts/print-transcode-command.ts3
-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
-rw-r--r--shared/models/server/custom-config.model.ts3
-rw-r--r--shared/models/server/job.model.ts5
-rw-r--r--shared/server-commands/server/config-command.ts4
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 {
175 profile: null, 175 profile: null,
176 concurrency: CONCURRENCY_VALIDATOR, 176 concurrency: CONCURRENCY_VALIDATOR,
177 resolutions: {}, 177 resolutions: {},
178 alwaysTranscodeOriginalResolution: null,
178 hls: { 179 hls: {
179 enabled: null 180 enabled: null
180 }, 181 },
@@ -197,7 +198,8 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
197 enabled: null, 198 enabled: null,
198 threads: TRANSCODING_THREADS_VALIDATOR, 199 threads: TRANSCODING_THREADS_VALIDATOR,
199 profile: null, 200 profile: null,
200 resolutions: {} 201 resolutions: {},
202 alwaysTranscodeOriginalResolution: null
201 } 203 }
202 }, 204 },
203 videoStudio: { 205 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 @@
41 <ng-container ngProjectAs="description" i18n> 41 <ng-container ngProjectAs="description" i18n>
42 Small latency disables P2P and high latency can increase P2P ratio 42 Small latency disables P2P and high latency can increase P2P ratio
43 </ng-container> 43 </ng-container>
44
45 </my-peertube-checkbox> 44 </my-peertube-checkbox>
46 </div> 45 </div>
47 46
@@ -115,8 +114,8 @@
115 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label> 114 <label i18n for="liveTranscodingThreads">Live resolutions to generate</label>
116 115
117 <div class="ms-2 mt-2 d-flex flex-column"> 116 <div class="ms-2 mt-2 d-flex flex-column">
118 <ng-container formGroupName="resolutions">
119 117
118 <ng-container formGroupName="resolutions">
120 <div class="form-group" *ngFor="let resolution of liveResolutions"> 119 <div class="form-group" *ngFor="let resolution of liveResolutions">
121 <my-peertube-checkbox 120 <my-peertube-checkbox
122 [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id" 121 [inputName]="getResolutionKey(resolution.id)" [formControlName]="resolution.id"
@@ -127,8 +126,18 @@
127 </ng-template> 126 </ng-template>
128 </my-peertube-checkbox> 127 </my-peertube-checkbox>
129 </div> 128 </div>
130
131 </ng-container> 129 </ng-container>
130
131 <div class="form-group">
132 <my-peertube-checkbox
133 inputName="transcodingAlwaysTranscodeOriginalResolution" formControlName="alwaysTranscodeOriginalResolution"
134 i18n-labelText labelText="Also transcode original resolution"
135 >
136 <ng-container i18n ngProjectAs="description">
137 Even if it's above your maximum enabled resolution
138 </ng-container>
139 </my-peertube-checkbox>
140 </div>
132 </div> 141 </div>
133 </div> 142 </div>
134 143
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 @@
111 <label i18n>Resolutions to generate per enabled format</label> 111 <label i18n>Resolutions to generate per enabled format</label>
112 112
113 <div class="ms-2 d-flex flex-column"> 113 <div class="ms-2 d-flex flex-column">
114 <span class="mb-3 small muted" i18n> 114 <my-peertube-checkbox
115 inputName="transcodingAlwaysTranscodeOriginalResolution" formControlName="alwaysTranscodeOriginalResolution"
116 i18n-labelText labelText="Always transcode original resolution"
117 >
118 </my-peertube-checkbox>
119
120 <span class="mt-3 mb-2 small muted" i18n>
115 The original file resolution will be the default target if no option is selected. 121 The original file resolution will be the default target if no option is selected.
116 </span> 122 </span>
117 123
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:
403 1440p: false 403 1440p: false
404 2160p: false 404 2160p: false
405 405
406 # Transcode and keep original resolution, even if it's above your maximum enabled resolution
407 always_transcode_original_resolution: true
408
406 # Generate videos in a WebTorrent format (what we do since the first PeerTube release) 409 # Generate videos in a WebTorrent format (what we do since the first PeerTube release)
407 # If you also enabled the hls format, it will multiply videos storage by 2 410 # If you also enabled the hls format, it will multiply videos storage by 2
408 # If disabled, breaks federation with PeerTube instances < 2.1 411 # If disabled, breaks federation with PeerTube instances < 2.1
@@ -496,6 +499,9 @@ live:
496 1440p: false 499 1440p: false
497 2160p: false 500 2160p: false
498 501
502 # Also transcode original resolution, even if it's above your maximum enabled resolution
503 always_transcode_original_resolution: true
504
499video_studio: 505video_studio:
500 # Enable video edition by users (cut, add intro/outro, add watermark etc) 506 # Enable video edition by users (cut, add intro/outro, add watermark etc)
501 # If enabled, users can create transcoding tasks as they wish 507 # 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:
413 1440p: false 413 1440p: false
414 2160p: false 414 2160p: false
415 415
416 # Transcode and keep original resolution, even if it's above your maximum enabled resolution
417 always_transcode_original_resolution: true
418
416 # Generate videos in a WebTorrent format (what we do since the first PeerTube release) 419 # Generate videos in a WebTorrent format (what we do since the first PeerTube release)
417 # If you also enabled the hls format, it will multiply videos storage by 2 420 # If you also enabled the hls format, it will multiply videos storage by 2
418 # If disabled, breaks federation with PeerTube instances < 2.1 421 # If disabled, breaks federation with PeerTube instances < 2.1
@@ -506,6 +509,9 @@ live:
506 1440p: false 509 1440p: false
507 2160p: false 510 2160p: false
508 511
512 # Also transcode original resolution, even if it's above your maximum enabled resolution
513 always_transcode_original_resolution: true
514
509video_studio: 515video_studio:
510 # Enable video edition by users (cut, add intro/outro, add watermark etc) 516 # Enable video edition by users (cut, add intro/outro, add watermark etc)
511 # If enabled, users can create transcoding tasks as they wish 517 # 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 @@
1import { program } from 'commander' 1import { program } from 'commander'
2import { isUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc' 2import { isUUIDValid, toCompleteUUID } from '@server/helpers/custom-validators/misc'
3import { computeLowerResolutionsToTranscode } from '@server/helpers/ffmpeg' 3import { computeResolutionsToTranscode } from '@server/helpers/ffmpeg'
4import { CONFIG } from '@server/initializers/config' 4import { CONFIG } from '@server/initializers/config'
5import { addTranscodingJob } from '@server/lib/video' 5import { addTranscodingJob } from '@server/lib/video'
6import { VideoState, VideoTranscodingPayload } from '@shared/models' 6import { VideoState, VideoTranscodingPayload } from '@shared/models'
@@ -53,7 +53,7 @@ async function run () {
53 if (options.generateHls || CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) { 53 if (options.generateHls || CONFIG.TRANSCODING.WEBTORRENT.ENABLED === false) {
54 const resolutionsEnabled = options.resolution 54 const resolutionsEnabled = options.resolution
55 ? [ parseInt(options.resolution) ] 55 ? [ parseInt(options.resolution) ]
56 : computeLowerResolutionsToTranscode(maxResolution, 'vod').concat([ maxResolution ]) 56 : computeResolutionsToTranscode({ inputResolution: maxResolution, type: 'vod', includeInputResolution: true })
57 57
58 for (const resolution of resolutionsEnabled) { 58 for (const resolution of resolutionsEnabled) {
59 dataInput.push({ 59 dataInput.push({
@@ -61,8 +61,6 @@ async function run () {
61 videoUUID: video.uuid, 61 videoUUID: video.uuid,
62 resolution, 62 resolution,
63 63
64 // FIXME: check the file has audio and is not in portrait mode
65 isPortraitMode: false,
66 hasAudio: true, 64 hasAudio: true,
67 65
68 copyCodecs: false, 66 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) {
31 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), 31 availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(),
32 profile: 'default', 32 profile: 'default',
33 33
34 resolution: +cmd.resolution, 34 resolution: +cmd.resolution
35 isPortraitMode: false
36 } as TranscodeVODOptions 35 } as TranscodeVODOptions
37 36
38 let command = ffmpeg(options.inputPath) 37 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 {
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 {
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 {
117 117
118 resolutions: ConfigResolutions & { '0p': boolean } 118 resolutions: ConfigResolutions & { '0p': boolean }
119 119
120 alwaysTranscodeOriginalResolution: boolean
121
120 webtorrent: { 122 webtorrent: {
121 enabled: boolean 123 enabled: boolean
122 } 124 }
@@ -144,6 +146,7 @@ export interface CustomConfig {
144 threads: number 146 threads: number
145 profile: string 147 profile: string
146 resolutions: ConfigResolutions 148 resolutions: ConfigResolutions
149 alwaysTranscodeOriginalResolution: boolean
147 } 150 }
148 } 151 }
149 152
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 @@
1import { ContextType } from '../activitypub/context' 1import { ContextType } from '../activitypub/context'
2import { VideoState } from '../videos' 2import { VideoState } from '../videos'
3import { VideoStudioTaskCut } from '../videos/studio'
4import { VideoResolution } from '../videos/file/video-resolution.enum' 3import { VideoResolution } from '../videos/file/video-resolution.enum'
4import { VideoStudioTaskCut } from '../videos/studio'
5import { SendEmailOptions } from './emailer.model' 5import { SendEmailOptions } from './emailer.model'
6 6
7export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused' 7export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused'
@@ -126,7 +126,6 @@ export interface HLSTranscodingPayload extends BaseTranscodingPayload {
126 copyCodecs: boolean 126 copyCodecs: boolean
127 127
128 hasAudio: boolean 128 hasAudio: boolean
129 isPortraitMode?: boolean
130 129
131 autoDeleteWebTorrentIfNeeded: boolean 130 autoDeleteWebTorrentIfNeeded: boolean
132 isMaxQuality: boolean 131 isMaxQuality: boolean
@@ -138,8 +137,6 @@ export interface NewWebTorrentResolutionTranscodingPayload extends BaseTranscodi
138 137
139 hasAudio: boolean 138 hasAudio: boolean
140 createHLSIfNeeded: boolean 139 createHLSIfNeeded: boolean
141
142 isPortraitMode?: boolean
143} 140}
144 141
145export interface MergeAudioTranscodingPayload extends BaseTranscodingPayload { 142export 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 {
310 '1440p': false, 310 '1440p': false,
311 '2160p': false 311 '2160p': false
312 }, 312 },
313 alwaysTranscodeOriginalResolution: true,
313 webtorrent: { 314 webtorrent: {
314 enabled: true 315 enabled: true
315 }, 316 },
@@ -339,7 +340,8 @@ export class ConfigCommand extends AbstractCommand {
339 '1080p': true, 340 '1080p': true,
340 '1440p': true, 341 '1440p': true,
341 '2160p': true 342 '2160p': true
342 } 343 },
344 alwaysTranscodeOriginalResolution: true
343 } 345 }
344 }, 346 },
345 videoStudio: { 347 videoStudio: {