diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/config.ts | 4 | ||||
-rw-r--r-- | server/controllers/api/videos/transcoding.ts | 10 | ||||
-rw-r--r-- | server/helpers/ffmpeg/ffmpeg-vod.ts | 12 | ||||
-rw-r--r-- | server/helpers/ffmpeg/ffprobe-utils.ts | 26 | ||||
-rw-r--r-- | server/initializers/checker-before-init.ts | 4 | ||||
-rw-r--r-- | server/initializers/config.ts | 3 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 3 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-transcoding.ts | 26 | ||||
-rw-r--r-- | server/lib/live/live-manager.ts | 14 | ||||
-rw-r--r-- | server/lib/transcoding/transcoding.ts | 55 | ||||
-rw-r--r-- | server/middlewares/validators/config.ts | 5 | ||||
-rw-r--r-- | server/tests/api/check-params/config.ts | 4 | ||||
-rw-r--r-- | server/tests/api/live/live.ts | 73 | ||||
-rw-r--r-- | server/tests/api/server/config.ts | 8 | ||||
-rw-r--r-- | server/tests/api/transcoding/transcoder.ts | 80 | ||||
-rw-r--r-- | server/tests/shared/streaming-playlists.ts | 3 |
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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { computeLowerResolutionsToTranscode } from '@server/helpers/ffmpeg' | 2 | import { computeResolutionsToTranscode } from '@server/helpers/ffmpeg' |
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | 3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' |
4 | import { addTranscodingJob } from '@server/lib/video' | 4 | import { addTranscodingJob } from '@server/lib/video' |
5 | import { HttpStatusCode, UserRight, VideoState, VideoTranscodingCreate } from '@shared/models' | 5 | import { 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' | |||
7 | import { logger, loggerTagsFactory } from '../logger' | 7 | import { logger, loggerTagsFactory } from '../logger' |
8 | import { getFFmpeg, runCommand } from './ffmpeg-commons' | 8 | import { getFFmpeg, runCommand } from './ffmpeg-commons' |
9 | import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets' | 9 | import { presetCopy, presetOnlyAudio, presetVOD } from './ffmpeg-presets' |
10 | import { computeFPS, getVideoStreamFPS } from './ffprobe-utils' | 10 | import { computeFPS, ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamFPS } from './ffprobe-utils' |
11 | import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' | 11 | import { VIDEO_TRANSCODING_FPS } from '@server/initializers/constants' |
12 | 12 | ||
13 | const lTags = loggerTagsFactory('ffmpeg') | 13 | const 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 | ||
117 | async function buildVODCommand (command: FfmpegCommand, options: TranscodeVODOptions) { | 115 | async 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 | ||
93 | function computeLowerResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') { | 93 | function 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 @@ | |||
1 | import { Job } from 'bull' | 1 | import { Job } from 'bull' |
2 | import { TranscodeVODOptionsType } from '@server/helpers/ffmpeg' | 2 | import { TranscodeVODOptionsType } from '@server/helpers/ffmpeg' |
3 | import { Hooks } from '@server/lib/plugins/hooks' | ||
3 | import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video' | 4 | import { addTranscodingJob, getTranscodingJobPriority } from '@server/lib/video' |
4 | import { VideoPathManager } from '@server/lib/video-path-manager' | 5 | import { VideoPathManager } from '@server/lib/video-path-manager' |
5 | import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state' | 6 | import { moveToFailedTranscodingState, moveToNextState } from '@server/lib/video-state' |
@@ -16,7 +17,7 @@ import { | |||
16 | VideoTranscodingPayload | 17 | VideoTranscodingPayload |
17 | } from '@shared/models' | 18 | } from '@shared/models' |
18 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 19 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
19 | import { computeLowerResolutionsToTranscode } from '../../../helpers/ffmpeg' | 20 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg' |
20 | import { logger, loggerTagsFactory } from '../../../helpers/logger' | 21 | import { logger, loggerTagsFactory } from '../../../helpers/logger' |
21 | import { CONFIG } from '../../../initializers/config' | 22 | import { CONFIG } from '../../../initializers/config' |
22 | import { VideoModel } from '../../../models/video/video' | 23 | import { 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' |
29 | import { Hooks } from '@server/lib/plugins/hooks' | ||
30 | 30 | ||
31 | type HandlerFunction = (job: Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<void> | 31 | type 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 ( | |||
127 | async function handleWebTorrentMergeAudioJob (job: Job, payload: MergeAudioTranscodingPayload, video: MVideoFullLight, user: MUserId) { | 126 | async 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 | |||
137 | async function handleWebTorrentOptimizeJob (job: Job, payload: OptimizeTranscodingPayload, video: MVideoFullLight, user: MUserId) { | 136 | async 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' | |||
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { createServer as createServerTLS, Server as ServerTLS } from 'tls' | 5 | import { createServer as createServerTLS, Server as ServerTLS } from 'tls' |
6 | import { | 6 | import { |
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' | |||
26 | import { JobQueue } from '../job-queue' | 26 | import { JobQueue } from '../job-queue' |
27 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '../paths' | 27 | import { generateHLSMasterPlaylistFilename, generateHlsSha256SegmentsFilename, getLiveReplayBaseDirectory } from '../paths' |
28 | import { PeerTubeSocket } from '../peertube-socket' | 28 | import { PeerTubeSocket } from '../peertube-socket' |
29 | import { Hooks } from '../plugins/hooks' | ||
29 | import { LiveQuotaStore } from './live-quota-store' | 30 | import { LiveQuotaStore } from './live-quota-store' |
30 | import { cleanupPermanentLive } from './live-utils' | 31 | import { cleanupPermanentLive } from './live-utils' |
31 | import { MuxingSession } from './shared' | 32 | import { MuxingSession } from './shared' |
32 | import { Hooks } from '../plugins/hooks' | ||
33 | 33 | ||
34 | const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') | 34 | const NodeRtmpSession = require('node-media-server/src/node_rtmp_session') |
35 | const context = require('node-media-server/src/node_core_ctx') | 35 | const 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' | |||
10 | import { | 10 | import { |
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. |
35 | function optimizeOriginalVideofile (video: MVideoFullLight, inputVideoFile: MVideoFile, job?: Job) { | 36 | function 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 | 88 | function transcodeNewWebTorrentResolution (options: { |
81 | function 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 |
132 | function mergeAudioVideofile (video: MVideoFullLight, resolution: VideoResolution, job: Job) { | 144 | function 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 | |||
366 | function 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' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { basename, join } from 'path' | 5 | import { basename, join } from 'path' |
6 | import { ffprobePromise, getVideoStream } from '@server/helpers/ffmpeg' | 6 | import { ffprobePromise, getVideoStream } from '@server/helpers/ffmpeg' |
7 | import { checkLiveCleanup, checkLiveSegmentHash, checkResolutionsInMasterPlaylist, testImage } from '@server/tests/shared' | 7 | import { checkLiveSegmentHash, checkResolutionsInMasterPlaylist, getAllFiles, testImage } from '@server/tests/shared' |
8 | import { wait } from '@shared/core-utils' | 8 | import { wait } from '@shared/core-utils' |
9 | import { | 9 | import { |
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' | |||
7 | import { generateHighBitrateVideo, generateVideoWithFramerate, getAllFiles } from '@server/tests/shared' | 7 | import { generateHighBitrateVideo, generateVideoWithFramerate, getAllFiles } from '@server/tests/shared' |
8 | import { buildAbsoluteFixturePath, getMaxBitrate, getMinLimitBitrate } from '@shared/core-utils' | 8 | import { buildAbsoluteFixturePath, getMaxBitrate, getMinLimitBitrate } from '@shared/core-utils' |
9 | import { | 9 | import { |
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' |
17 | import { HttpStatusCode, VideoState } from '@shared/models' | 17 | import { 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 | ||
73 | export { | 76 | export { |