diff options
24 files changed, 263 insertions, 204 deletions
diff --git a/scripts/create-transcoding-job.ts b/scripts/create-transcoding-job.ts index 65e65b616..3a552c19a 100755 --- a/scripts/create-transcoding-job.ts +++ b/scripts/create-transcoding-job.ts | |||
@@ -47,13 +47,13 @@ async function run () { | |||
47 | if (!video) throw new Error('Video not found.') | 47 | if (!video) throw new Error('Video not found.') |
48 | 48 | ||
49 | const dataInput: VideoTranscodingPayload[] = [] | 49 | const dataInput: VideoTranscodingPayload[] = [] |
50 | const { videoFileResolution } = await video.getMaxQualityResolution() | 50 | const { resolution } = await video.getMaxQualityResolution() |
51 | 51 | ||
52 | // Generate HLS files | 52 | // Generate HLS files |
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 | ? [ options.resolution ] | 55 | ? [ options.resolution ] |
56 | : computeResolutionsToTranscode(videoFileResolution, 'vod').concat([ videoFileResolution ]) | 56 | : computeResolutionsToTranscode(resolution, 'vod').concat([ resolution ]) |
57 | 57 | ||
58 | for (const resolution of resolutionsEnabled) { | 58 | for (const resolution of resolutionsEnabled) { |
59 | dataInput.push({ | 59 | dataInput.push({ |
diff --git a/scripts/optimize-old-videos.ts b/scripts/optimize-old-videos.ts index bde9d1e01..9e66105dd 100644 --- a/scripts/optimize-old-videos.ts +++ b/scripts/optimize-old-videos.ts | |||
@@ -1,9 +1,7 @@ | |||
1 | import { registerTSPaths } from '../server/helpers/register-ts-paths' | 1 | import { registerTSPaths } from '../server/helpers/register-ts-paths' |
2 | registerTSPaths() | 2 | registerTSPaths() |
3 | 3 | ||
4 | import { VIDEO_TRANSCODING_FPS } from '../server/initializers/constants' | ||
5 | import { getDurationFromVideoFile, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../server/helpers/ffprobe-utils' | 4 | import { getDurationFromVideoFile, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../server/helpers/ffprobe-utils' |
6 | import { getMaxBitrate } from '../shared/models/videos' | ||
7 | import { VideoModel } from '../server/models/video/video' | 5 | import { VideoModel } from '../server/models/video/video' |
8 | import { optimizeOriginalVideofile } from '../server/lib/transcoding/video-transcoding' | 6 | import { optimizeOriginalVideofile } from '../server/lib/transcoding/video-transcoding' |
9 | import { initDatabaseModels } from '../server/initializers/database' | 7 | import { initDatabaseModels } from '../server/initializers/database' |
@@ -11,6 +9,7 @@ import { basename, dirname } from 'path' | |||
11 | import { copy, move, remove } from 'fs-extra' | 9 | import { copy, move, remove } from 'fs-extra' |
12 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 10 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
13 | import { getVideoFilePath } from '@server/lib/video-paths' | 11 | import { getVideoFilePath } from '@server/lib/video-paths' |
12 | import { getMaxBitrate } from '@shared/core-utils' | ||
14 | 13 | ||
15 | run() | 14 | run() |
16 | .then(() => process.exit(0)) | 15 | .then(() => process.exit(0)) |
@@ -42,13 +41,13 @@ async function run () { | |||
42 | for (const file of video.VideoFiles) { | 41 | for (const file of video.VideoFiles) { |
43 | currentFilePath = getVideoFilePath(video, file) | 42 | currentFilePath = getVideoFilePath(video, file) |
44 | 43 | ||
45 | const [ videoBitrate, fps, resolution ] = await Promise.all([ | 44 | const [ videoBitrate, fps, dataResolution ] = await Promise.all([ |
46 | getVideoFileBitrate(currentFilePath), | 45 | getVideoFileBitrate(currentFilePath), |
47 | getVideoFileFPS(currentFilePath), | 46 | getVideoFileFPS(currentFilePath), |
48 | getVideoFileResolution(currentFilePath) | 47 | getVideoFileResolution(currentFilePath) |
49 | ]) | 48 | ]) |
50 | 49 | ||
51 | const maxBitrate = getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS) | 50 | const maxBitrate = getMaxBitrate({ ...dataResolution, fps }) |
52 | const isMaxBitrateExceeded = videoBitrate > maxBitrate | 51 | const isMaxBitrateExceeded = videoBitrate > maxBitrate |
53 | if (isMaxBitrateExceeded) { | 52 | if (isMaxBitrateExceeded) { |
54 | console.log( | 53 | console.log( |
diff --git a/server/controllers/api/videos/upload.ts b/server/controllers/api/videos/upload.ts index 408f677ff..89f50714d 100644 --- a/server/controllers/api/videos/upload.ts +++ b/server/controllers/api/videos/upload.ts | |||
@@ -239,7 +239,7 @@ async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUplo | |||
239 | videoFile.resolution = DEFAULT_AUDIO_RESOLUTION | 239 | videoFile.resolution = DEFAULT_AUDIO_RESOLUTION |
240 | } else { | 240 | } else { |
241 | videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path) | 241 | videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path) |
242 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution | 242 | videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).resolution |
243 | } | 243 | } |
244 | 244 | ||
245 | videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname) | 245 | videoFile.filename = generateWebTorrentVideoFilename(videoFile.resolution, videoFile.extname) |
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts index 7f84a049f..830625cc6 100644 --- a/server/helpers/ffmpeg-utils.ts +++ b/server/helpers/ffmpeg-utils.ts | |||
@@ -3,10 +3,18 @@ import * as ffmpeg from 'fluent-ffmpeg' | |||
3 | import { readFile, remove, writeFile } from 'fs-extra' | 3 | import { readFile, remove, writeFile } from 'fs-extra' |
4 | import { dirname, join } from 'path' | 4 | import { dirname, join } from 'path' |
5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' | 5 | import { FFMPEG_NICE, VIDEO_LIVE } from '@server/initializers/constants' |
6 | import { AvailableEncoders, EncoderOptions, EncoderOptionsBuilder, EncoderProfile, VideoResolution } from '../../shared/models/videos' | 6 | import { pick } from '@shared/core-utils' |
7 | import { | ||
8 | AvailableEncoders, | ||
9 | EncoderOptions, | ||
10 | EncoderOptionsBuilder, | ||
11 | EncoderOptionsBuilderParams, | ||
12 | EncoderProfile, | ||
13 | VideoResolution | ||
14 | } from '../../shared/models/videos' | ||
7 | import { CONFIG } from '../initializers/config' | 15 | import { CONFIG } from '../initializers/config' |
8 | import { execPromise, promisify0 } from './core-utils' | 16 | import { execPromise, promisify0 } from './core-utils' |
9 | import { computeFPS, ffprobePromise, getAudioStream, getVideoFileBitrate, getVideoFileFPS } from './ffprobe-utils' | 17 | import { computeFPS, ffprobePromise, getAudioStream, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from './ffprobe-utils' |
10 | import { processImage } from './image-utils' | 18 | import { processImage } from './image-utils' |
11 | import { logger } from './logger' | 19 | import { logger } from './logger' |
12 | 20 | ||
@@ -217,13 +225,16 @@ async function getLiveTranscodingCommand (options: { | |||
217 | masterPlaylistName: string | 225 | masterPlaylistName: string |
218 | 226 | ||
219 | resolutions: number[] | 227 | resolutions: number[] |
228 | |||
229 | // Input information | ||
220 | fps: number | 230 | fps: number |
221 | bitrate: number | 231 | bitrate: number |
232 | ratio: number | ||
222 | 233 | ||
223 | availableEncoders: AvailableEncoders | 234 | availableEncoders: AvailableEncoders |
224 | profile: string | 235 | profile: string |
225 | }) { | 236 | }) { |
226 | const { rtmpUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName } = options | 237 | const { rtmpUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options |
227 | const input = rtmpUrl | 238 | const input = rtmpUrl |
228 | 239 | ||
229 | const command = getFFmpeg(input, 'live') | 240 | const command = getFFmpeg(input, 'live') |
@@ -253,9 +264,12 @@ async function getLiveTranscodingCommand (options: { | |||
253 | availableEncoders, | 264 | availableEncoders, |
254 | profile, | 265 | profile, |
255 | 266 | ||
256 | fps: resolutionFPS, | ||
257 | inputBitrate: bitrate, | 267 | inputBitrate: bitrate, |
268 | inputRatio: ratio, | ||
269 | |||
258 | resolution, | 270 | resolution, |
271 | fps: resolutionFPS, | ||
272 | |||
259 | streamNum: i, | 273 | streamNum: i, |
260 | videoType: 'live' as 'live' | 274 | videoType: 'live' as 'live' |
261 | } | 275 | } |
@@ -502,7 +516,7 @@ function getHLSVideoPath (options: HLSTranscodeOptions | HLSFromTSTranscodeOptio | |||
502 | // Run encoder builder depending on available encoders | 516 | // Run encoder builder depending on available encoders |
503 | // Try encoders by priority: if the encoder is available, run the chosen profile or fallback to the default one | 517 | // Try encoders by priority: if the encoder is available, run the chosen profile or fallback to the default one |
504 | // If the default one does not exist, check the next encoder | 518 | // If the default one does not exist, check the next encoder |
505 | async function getEncoderBuilderResult (options: { | 519 | async function getEncoderBuilderResult (options: EncoderOptionsBuilderParams & { |
506 | streamType: 'video' | 'audio' | 520 | streamType: 'video' | 'audio' |
507 | input: string | 521 | input: string |
508 | 522 | ||
@@ -510,13 +524,8 @@ async function getEncoderBuilderResult (options: { | |||
510 | profile: string | 524 | profile: string |
511 | 525 | ||
512 | videoType: 'vod' | 'live' | 526 | videoType: 'vod' | 'live' |
513 | |||
514 | resolution: number | ||
515 | inputBitrate: number | ||
516 | fps?: number | ||
517 | streamNum?: number | ||
518 | }) { | 527 | }) { |
519 | const { availableEncoders, input, profile, resolution, streamType, fps, inputBitrate, streamNum, videoType } = options | 528 | const { availableEncoders, profile, streamType, videoType } = options |
520 | 529 | ||
521 | const encodersToTry = availableEncoders.encodersToTry[videoType][streamType] | 530 | const encodersToTry = availableEncoders.encodersToTry[videoType][streamType] |
522 | const encoders = availableEncoders.available[videoType] | 531 | const encoders = availableEncoders.available[videoType] |
@@ -546,7 +555,7 @@ async function getEncoderBuilderResult (options: { | |||
546 | } | 555 | } |
547 | } | 556 | } |
548 | 557 | ||
549 | const result = await builder({ input, resolution, inputBitrate, fps, streamNum }) | 558 | const result = await builder(pick(options, [ 'input', 'resolution', 'inputBitrate', 'fps', 'inputRatio', 'streamNum' ])) |
550 | 559 | ||
551 | return { | 560 | return { |
552 | result, | 561 | result, |
@@ -581,6 +590,7 @@ async function presetVideo (options: { | |||
581 | // Audio encoder | 590 | // Audio encoder |
582 | const parsedAudio = await getAudioStream(input, probe) | 591 | const parsedAudio = await getAudioStream(input, probe) |
583 | const bitrate = await getVideoFileBitrate(input, probe) | 592 | const bitrate = await getVideoFileBitrate(input, probe) |
593 | const { ratio } = await getVideoFileResolution(input, probe) | ||
584 | 594 | ||
585 | let streamsToProcess: StreamType[] = [ 'audio', 'video' ] | 595 | let streamsToProcess: StreamType[] = [ 'audio', 'video' ] |
586 | 596 | ||
@@ -600,6 +610,7 @@ async function presetVideo (options: { | |||
600 | profile, | 610 | profile, |
601 | fps, | 611 | fps, |
602 | inputBitrate: bitrate, | 612 | inputBitrate: bitrate, |
613 | inputRatio: ratio, | ||
603 | videoType: 'vod' as 'vod' | 614 | videoType: 'vod' as 'vod' |
604 | }) | 615 | }) |
605 | 616 | ||
diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts index bc87e49b1..e58444b07 100644 --- a/server/helpers/ffprobe-utils.ts +++ b/server/helpers/ffprobe-utils.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { getMaxBitrate, VideoFileMetadata, VideoResolution } from '../../shared/models/videos' | 2 | import { getMaxBitrate } from '@shared/core-utils' |
3 | import { VideoFileMetadata, VideoResolution, VideoTranscodingFPS } from '../../shared/models/videos' | ||
3 | import { CONFIG } from '../initializers/config' | 4 | import { CONFIG } from '../initializers/config' |
4 | import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 5 | import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
5 | import { logger } from './logger' | 6 | import { logger } from './logger' |
@@ -75,7 +76,7 @@ function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) { | |||
75 | } | 76 | } |
76 | } | 77 | } |
77 | 78 | ||
78 | async function getVideoStreamSize (path: string, existingProbe?: ffmpeg.FfprobeData) { | 79 | async function getVideoStreamSize (path: string, existingProbe?: ffmpeg.FfprobeData): Promise<{ width: number, height: number }> { |
79 | const videoStream = await getVideoStreamFromFile(path, existingProbe) | 80 | const videoStream = await getVideoStreamFromFile(path, existingProbe) |
80 | 81 | ||
81 | return videoStream === null | 82 | return videoStream === null |
@@ -146,7 +147,10 @@ async function getVideoFileResolution (path: string, existingProbe?: ffmpeg.Ffpr | |||
146 | const size = await getVideoStreamSize(path, existingProbe) | 147 | const size = await getVideoStreamSize(path, existingProbe) |
147 | 148 | ||
148 | return { | 149 | return { |
149 | videoFileResolution: Math.min(size.height, size.width), | 150 | width: size.width, |
151 | height: size.height, | ||
152 | ratio: Math.max(size.height, size.width) / Math.min(size.height, size.width), | ||
153 | resolution: Math.min(size.height, size.width), | ||
150 | isPortraitMode: size.height > size.width | 154 | isPortraitMode: size.height > size.width |
151 | } | 155 | } |
152 | } | 156 | } |
@@ -243,7 +247,7 @@ async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeDat | |||
243 | const videoStream = await getVideoStreamFromFile(path, probe) | 247 | const videoStream = await getVideoStreamFromFile(path, probe) |
244 | const fps = await getVideoFileFPS(path, probe) | 248 | const fps = await getVideoFileFPS(path, probe) |
245 | const bitRate = await getVideoFileBitrate(path, probe) | 249 | const bitRate = await getVideoFileBitrate(path, probe) |
246 | const resolution = await getVideoFileResolution(path, probe) | 250 | const resolutionData = await getVideoFileResolution(path, probe) |
247 | 251 | ||
248 | // If ffprobe did not manage to guess the bitrate | 252 | // If ffprobe did not manage to guess the bitrate |
249 | if (!bitRate) return false | 253 | if (!bitRate) return false |
@@ -253,7 +257,7 @@ async function canDoQuickVideoTranscode (path: string, probe?: ffmpeg.FfprobeDat | |||
253 | if (videoStream['codec_name'] !== 'h264') return false | 257 | if (videoStream['codec_name'] !== 'h264') return false |
254 | if (videoStream['pix_fmt'] !== 'yuv420p') return false | 258 | if (videoStream['pix_fmt'] !== 'yuv420p') return false |
255 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false | 259 | if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false |
256 | if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false | 260 | if (bitRate > getMaxBitrate({ ...resolutionData, fps })) return false |
257 | 261 | ||
258 | return true | 262 | return true |
259 | } | 263 | } |
@@ -278,7 +282,7 @@ async function canDoQuickAudioTranscode (path: string, probe?: ffmpeg.FfprobeDat | |||
278 | return true | 282 | return true |
279 | } | 283 | } |
280 | 284 | ||
281 | function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number { | 285 | function getClosestFramerateStandard <K extends keyof Pick<VideoTranscodingFPS, 'HD_STANDARD' | 'STANDARD'>> (fps: number, type: K) { |
282 | return VIDEO_TRANSCODING_FPS[type].slice(0) | 286 | return VIDEO_TRANSCODING_FPS[type].slice(0) |
283 | .sort((a, b) => fps % a - fps % b)[0] | 287 | .sort((a, b) => fps % a - fps % b)[0] |
284 | } | 288 | } |
diff --git a/server/initializers/migrations/0075-video-resolutions.ts b/server/initializers/migrations/0075-video-resolutions.ts index 496125adb..6e8e47acb 100644 --- a/server/initializers/migrations/0075-video-resolutions.ts +++ b/server/initializers/migrations/0075-video-resolutions.ts | |||
@@ -27,17 +27,15 @@ function up (utils: { | |||
27 | const ext = matches[2] | 27 | const ext = matches[2] |
28 | 28 | ||
29 | const p = getVideoFileResolution(join(videoFileDir, videoFile)) | 29 | const p = getVideoFileResolution(join(videoFileDir, videoFile)) |
30 | .then(height => { | 30 | .then(async ({ resolution }) => { |
31 | const oldTorrentName = uuid + '.torrent' | 31 | const oldTorrentName = uuid + '.torrent' |
32 | const newTorrentName = uuid + '-' + height + '.torrent' | 32 | const newTorrentName = uuid + '-' + resolution + '.torrent' |
33 | return rename(join(torrentDir, oldTorrentName), join(torrentDir, newTorrentName)).then(() => height) | 33 | await rename(join(torrentDir, oldTorrentName), join(torrentDir, newTorrentName)).then(() => resolution) |
34 | }) | 34 | |
35 | .then(height => { | 35 | const newVideoFileName = uuid + '-' + resolution + '.' + ext |
36 | const newVideoFileName = uuid + '-' + height + '.' + ext | 36 | await rename(join(videoFileDir, videoFile), join(videoFileDir, newVideoFileName)).then(() => resolution) |
37 | return rename(join(videoFileDir, videoFile), join(videoFileDir, newVideoFileName)).then(() => height) | 37 | |
38 | }) | 38 | const query = 'UPDATE "VideoFiles" SET "resolution" = ' + resolution + |
39 | .then(height => { | ||
40 | const query = 'UPDATE "VideoFiles" SET "resolution" = ' + height + | ||
41 | ' WHERE "videoId" = (SELECT "id" FROM "Videos" WHERE "uuid" = \'' + uuid + '\')' | 39 | ' WHERE "videoId" = (SELECT "id" FROM "Videos" WHERE "uuid" = \'' + uuid + '\')' |
42 | return utils.sequelize.query(query) | 40 | return utils.sequelize.query(query) |
43 | }) | 41 | }) |
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts index 4d199f247..2f4abf730 100644 --- a/server/lib/job-queue/handlers/video-file-import.ts +++ b/server/lib/job-queue/handlers/video-file-import.ts | |||
@@ -32,7 +32,7 @@ async function processVideoFileImport (job: Bull.Job) { | |||
32 | const newResolutionPayload = { | 32 | const newResolutionPayload = { |
33 | type: 'new-resolution-to-webtorrent' as 'new-resolution-to-webtorrent', | 33 | type: 'new-resolution-to-webtorrent' as 'new-resolution-to-webtorrent', |
34 | videoUUID: video.uuid, | 34 | videoUUID: video.uuid, |
35 | resolution: data.videoFileResolution, | 35 | resolution: data.resolution, |
36 | isPortraitMode: data.isPortraitMode, | 36 | isPortraitMode: data.isPortraitMode, |
37 | copyCodecs: false, | 37 | copyCodecs: false, |
38 | isNewVideo: false | 38 | isNewVideo: false |
@@ -51,13 +51,13 @@ export { | |||
51 | // --------------------------------------------------------------------------- | 51 | // --------------------------------------------------------------------------- |
52 | 52 | ||
53 | async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | 53 | async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { |
54 | const { videoFileResolution } = await getVideoFileResolution(inputFilePath) | 54 | const { resolution } = await getVideoFileResolution(inputFilePath) |
55 | const { size } = await stat(inputFilePath) | 55 | const { size } = await stat(inputFilePath) |
56 | const fps = await getVideoFileFPS(inputFilePath) | 56 | const fps = await getVideoFileFPS(inputFilePath) |
57 | 57 | ||
58 | const fileExt = getLowercaseExtension(inputFilePath) | 58 | const fileExt = getLowercaseExtension(inputFilePath) |
59 | 59 | ||
60 | const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === videoFileResolution) | 60 | const currentVideoFile = video.VideoFiles.find(videoFile => videoFile.resolution === resolution) |
61 | 61 | ||
62 | if (currentVideoFile) { | 62 | if (currentVideoFile) { |
63 | // Remove old file and old torrent | 63 | // Remove old file and old torrent |
@@ -69,9 +69,9 @@ async function updateVideoFile (video: MVideoFullLight, inputFilePath: string) { | |||
69 | } | 69 | } |
70 | 70 | ||
71 | const newVideoFile = new VideoFileModel({ | 71 | const newVideoFile = new VideoFileModel({ |
72 | resolution: videoFileResolution, | 72 | resolution, |
73 | extname: fileExt, | 73 | extname: fileExt, |
74 | filename: generateWebTorrentVideoFilename(videoFileResolution, fileExt), | 74 | filename: generateWebTorrentVideoFilename(resolution, fileExt), |
75 | size, | 75 | size, |
76 | fps, | 76 | fps, |
77 | videoId: video.id | 77 | videoId: video.id |
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts index 5fd2039b1..fec553f2b 100644 --- a/server/lib/job-queue/handlers/video-import.ts +++ b/server/lib/job-queue/handlers/video-import.ts | |||
@@ -114,7 +114,7 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
114 | throw new Error('The user video quota is exceeded with this video to import.') | 114 | throw new Error('The user video quota is exceeded with this video to import.') |
115 | } | 115 | } |
116 | 116 | ||
117 | const { videoFileResolution } = await getVideoFileResolution(tempVideoPath) | 117 | const { resolution } = await getVideoFileResolution(tempVideoPath) |
118 | const fps = await getVideoFileFPS(tempVideoPath) | 118 | const fps = await getVideoFileFPS(tempVideoPath) |
119 | const duration = await getDurationFromVideoFile(tempVideoPath) | 119 | const duration = await getDurationFromVideoFile(tempVideoPath) |
120 | 120 | ||
@@ -122,9 +122,9 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid | |||
122 | const fileExt = getLowercaseExtension(tempVideoPath) | 122 | const fileExt = getLowercaseExtension(tempVideoPath) |
123 | const videoFileData = { | 123 | const videoFileData = { |
124 | extname: fileExt, | 124 | extname: fileExt, |
125 | resolution: videoFileResolution, | 125 | resolution, |
126 | size: stats.size, | 126 | size: stats.size, |
127 | filename: generateWebTorrentVideoFilename(videoFileResolution, fileExt), | 127 | filename: generateWebTorrentVideoFilename(resolution, fileExt), |
128 | fps, | 128 | fps, |
129 | videoId: videoImport.videoId | 129 | videoId: videoImport.videoId |
130 | } | 130 | } |
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts index 386ccdc7b..aa5bd573a 100644 --- a/server/lib/job-queue/handlers/video-live-ending.ts +++ b/server/lib/job-queue/handlers/video-live-ending.ts | |||
@@ -96,12 +96,12 @@ async function saveLive (video: MVideo, live: MVideoLive, streamingPlaylist: MSt | |||
96 | const probe = await ffprobePromise(concatenatedTsFilePath) | 96 | const probe = await ffprobePromise(concatenatedTsFilePath) |
97 | const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) | 97 | const { audioStream } = await getAudioStream(concatenatedTsFilePath, probe) |
98 | 98 | ||
99 | const { videoFileResolution, isPortraitMode } = await getVideoFileResolution(concatenatedTsFilePath, probe) | 99 | const { resolution, isPortraitMode } = await getVideoFileResolution(concatenatedTsFilePath, probe) |
100 | 100 | ||
101 | const outputPath = await generateHlsPlaylistResolutionFromTS({ | 101 | const outputPath = await generateHlsPlaylistResolutionFromTS({ |
102 | video: videoWithFiles, | 102 | video: videoWithFiles, |
103 | concatenatedTsFilePath, | 103 | concatenatedTsFilePath, |
104 | resolution: videoFileResolution, | 104 | resolution, |
105 | isPortraitMode, | 105 | isPortraitMode, |
106 | isAAC: audioStream?.codec_name === 'aac' | 106 | isAAC: audioStream?.codec_name === 'aac' |
107 | }) | 107 | }) |
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts index 2abb351ce..876d1460c 100644 --- a/server/lib/job-queue/handlers/video-transcoding.ts +++ b/server/lib/job-queue/handlers/video-transcoding.ts | |||
@@ -136,7 +136,7 @@ async function onVideoFileOptimizer ( | |||
136 | if (videoArg === undefined) return undefined | 136 | if (videoArg === undefined) return undefined |
137 | 137 | ||
138 | // Outside the transaction (IO on disk) | 138 | // Outside the transaction (IO on disk) |
139 | const { videoFileResolution, isPortraitMode } = await videoArg.getMaxQualityResolution() | 139 | const { resolution, isPortraitMode } = await videoArg.getMaxQualityResolution() |
140 | 140 | ||
141 | // Maybe the video changed in database, refresh it | 141 | // Maybe the video changed in database, refresh it |
142 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid) | 142 | const videoDatabase = await VideoModel.loadAndPopulateAccountAndServerAndTags(videoArg.uuid) |
@@ -155,7 +155,7 @@ async function onVideoFileOptimizer ( | |||
155 | }) | 155 | }) |
156 | const hasHls = await createHlsJobIfEnabled(user, originalFileHLSPayload) | 156 | const hasHls = await createHlsJobIfEnabled(user, originalFileHLSPayload) |
157 | 157 | ||
158 | const hasNewResolutions = await createLowerResolutionsJobs(videoDatabase, user, videoFileResolution, isPortraitMode, 'webtorrent') | 158 | const hasNewResolutions = await createLowerResolutionsJobs(videoDatabase, user, resolution, isPortraitMode, 'webtorrent') |
159 | 159 | ||
160 | if (!hasHls && !hasNewResolutions) { | 160 | if (!hasHls && !hasNewResolutions) { |
161 | // No transcoding to do, it's now published | 161 | // No transcoding to do, it's now published |
diff --git a/server/lib/live/live-manager.ts b/server/lib/live/live-manager.ts index b19ecef6f..2a429fb33 100644 --- a/server/lib/live/live-manager.ts +++ b/server/lib/live/live-manager.ts | |||
@@ -202,7 +202,7 @@ class LiveManager { | |||
202 | const now = Date.now() | 202 | const now = Date.now() |
203 | const probe = await ffprobePromise(rtmpUrl) | 203 | const probe = await ffprobePromise(rtmpUrl) |
204 | 204 | ||
205 | const [ { videoFileResolution }, fps, bitrate ] = await Promise.all([ | 205 | const [ { resolution, ratio }, fps, bitrate ] = await Promise.all([ |
206 | getVideoFileResolution(rtmpUrl, probe), | 206 | getVideoFileResolution(rtmpUrl, probe), |
207 | getVideoFileFPS(rtmpUrl, probe), | 207 | getVideoFileFPS(rtmpUrl, probe), |
208 | getVideoFileBitrate(rtmpUrl, probe) | 208 | getVideoFileBitrate(rtmpUrl, probe) |
@@ -210,13 +210,13 @@ class LiveManager { | |||
210 | 210 | ||
211 | logger.info( | 211 | logger.info( |
212 | '%s probing took %d ms (bitrate: %d, fps: %d, resolution: %d)', | 212 | '%s probing took %d ms (bitrate: %d, fps: %d, resolution: %d)', |
213 | rtmpUrl, Date.now() - now, bitrate, fps, videoFileResolution, lTags(sessionId, video.uuid) | 213 | rtmpUrl, Date.now() - now, bitrate, fps, resolution, lTags(sessionId, video.uuid) |
214 | ) | 214 | ) |
215 | 215 | ||
216 | const allResolutions = this.buildAllResolutionsToTranscode(videoFileResolution) | 216 | const allResolutions = this.buildAllResolutionsToTranscode(resolution) |
217 | 217 | ||
218 | logger.info( | 218 | logger.info( |
219 | 'Will mux/transcode live video of original resolution %d.', videoFileResolution, | 219 | 'Will mux/transcode live video of original resolution %d.', resolution, |
220 | { allResolutions, ...lTags(sessionId, video.uuid) } | 220 | { allResolutions, ...lTags(sessionId, video.uuid) } |
221 | ) | 221 | ) |
222 | 222 | ||
@@ -229,6 +229,7 @@ class LiveManager { | |||
229 | rtmpUrl, | 229 | rtmpUrl, |
230 | fps, | 230 | fps, |
231 | bitrate, | 231 | bitrate, |
232 | ratio, | ||
232 | allResolutions | 233 | allResolutions |
233 | }) | 234 | }) |
234 | } | 235 | } |
@@ -240,9 +241,10 @@ class LiveManager { | |||
240 | rtmpUrl: string | 241 | rtmpUrl: string |
241 | fps: number | 242 | fps: number |
242 | bitrate: number | 243 | bitrate: number |
244 | ratio: number | ||
243 | allResolutions: number[] | 245 | allResolutions: number[] |
244 | }) { | 246 | }) { |
245 | const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, bitrate, rtmpUrl } = options | 247 | const { sessionId, videoLive, streamingPlaylist, allResolutions, fps, bitrate, ratio, rtmpUrl } = options |
246 | const videoUUID = videoLive.Video.uuid | 248 | const videoUUID = videoLive.Video.uuid |
247 | const localLTags = lTags(sessionId, videoUUID) | 249 | const localLTags = lTags(sessionId, videoUUID) |
248 | 250 | ||
@@ -257,6 +259,7 @@ class LiveManager { | |||
257 | streamingPlaylist, | 259 | streamingPlaylist, |
258 | rtmpUrl, | 260 | rtmpUrl, |
259 | bitrate, | 261 | bitrate, |
262 | ratio, | ||
260 | fps, | 263 | fps, |
261 | allResolutions | 264 | allResolutions |
262 | }) | 265 | }) |
diff --git a/server/lib/live/shared/muxing-session.ts b/server/lib/live/shared/muxing-session.ts index 62708b14b..a80abc843 100644 --- a/server/lib/live/shared/muxing-session.ts +++ b/server/lib/live/shared/muxing-session.ts | |||
@@ -54,9 +54,11 @@ class MuxingSession extends EventEmitter { | |||
54 | private readonly streamingPlaylist: MStreamingPlaylistVideo | 54 | private readonly streamingPlaylist: MStreamingPlaylistVideo |
55 | private readonly rtmpUrl: string | 55 | private readonly rtmpUrl: string |
56 | private readonly fps: number | 56 | private readonly fps: number |
57 | private readonly bitrate: number | ||
58 | private readonly allResolutions: number[] | 57 | private readonly allResolutions: number[] |
59 | 58 | ||
59 | private readonly bitrate: number | ||
60 | private readonly ratio: number | ||
61 | |||
60 | private readonly videoId: number | 62 | private readonly videoId: number |
61 | private readonly videoUUID: string | 63 | private readonly videoUUID: string |
62 | private readonly saveReplay: boolean | 64 | private readonly saveReplay: boolean |
@@ -85,6 +87,7 @@ class MuxingSession extends EventEmitter { | |||
85 | rtmpUrl: string | 87 | rtmpUrl: string |
86 | fps: number | 88 | fps: number |
87 | bitrate: number | 89 | bitrate: number |
90 | ratio: number | ||
88 | allResolutions: number[] | 91 | allResolutions: number[] |
89 | }) { | 92 | }) { |
90 | super() | 93 | super() |
@@ -96,7 +99,10 @@ class MuxingSession extends EventEmitter { | |||
96 | this.streamingPlaylist = options.streamingPlaylist | 99 | this.streamingPlaylist = options.streamingPlaylist |
97 | this.rtmpUrl = options.rtmpUrl | 100 | this.rtmpUrl = options.rtmpUrl |
98 | this.fps = options.fps | 101 | this.fps = options.fps |
102 | |||
99 | this.bitrate = options.bitrate | 103 | this.bitrate = options.bitrate |
104 | this.ratio = options.bitrate | ||
105 | |||
100 | this.allResolutions = options.allResolutions | 106 | this.allResolutions = options.allResolutions |
101 | 107 | ||
102 | this.videoId = this.videoLive.Video.id | 108 | this.videoId = this.videoLive.Video.id |
@@ -122,6 +128,7 @@ class MuxingSession extends EventEmitter { | |||
122 | resolutions: this.allResolutions, | 128 | resolutions: this.allResolutions, |
123 | fps: this.fps, | 129 | fps: this.fps, |
124 | bitrate: this.bitrate, | 130 | bitrate: this.bitrate, |
131 | ratio: this.ratio, | ||
125 | 132 | ||
126 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), | 133 | availableEncoders: VideoTranscodingProfilesManager.Instance.getAvailableEncoders(), |
127 | profile: CONFIG.LIVE.TRANSCODING.PROFILE | 134 | profile: CONFIG.LIVE.TRANSCODING.PROFILE |
diff --git a/server/lib/transcoding/video-transcoding-profiles.ts b/server/lib/transcoding/video-transcoding-profiles.ts index 2309f38d4..bca6dfccd 100644 --- a/server/lib/transcoding/video-transcoding-profiles.ts +++ b/server/lib/transcoding/video-transcoding-profiles.ts | |||
@@ -1,23 +1,24 @@ | |||
1 | import { logger } from '@server/helpers/logger' | 1 | import { logger } from '@server/helpers/logger' |
2 | import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos' | 2 | import { getAverageBitrate } from '@shared/core-utils' |
3 | import { AvailableEncoders, EncoderOptionsBuilder, EncoderOptionsBuilderParams } from '../../../shared/models/videos' | ||
3 | import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils' | 4 | import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils' |
4 | import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils' | 5 | import { canDoQuickAudioTranscode, ffprobePromise, getAudioStream, getMaxAudioBitrate } from '../../helpers/ffprobe-utils' |
5 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' | ||
6 | 6 | ||
7 | /** | 7 | /** |
8 | * | 8 | * |
9 | * Available encoders and profiles for the transcoding jobs | 9 | * Available encoders and profiles for the transcoding jobs |
10 | * These functions are used by ffmpeg-utils that will get the encoders and options depending on the chosen profile | 10 | * These functions are used by ffmpeg-utils that will get the encoders and options depending on the chosen profile |
11 | * | 11 | * |
12 | * Resources: | ||
13 | * * https://slhck.info/video/2017/03/01/rate-control.html | ||
14 | * * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate | ||
12 | */ | 15 | */ |
13 | 16 | ||
14 | // Resources: | 17 | const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async (options: EncoderOptionsBuilderParams) => { |
15 | // * https://slhck.info/video/2017/03/01/rate-control.html | 18 | const { fps, inputRatio, inputBitrate } = options |
16 | // * https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate | 19 | if (!fps) return { outputOptions: [ ] } |
17 | 20 | ||
18 | const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ inputBitrate, resolution, fps }) => { | 21 | const targetBitrate = capBitrate(inputBitrate, getAverageBitrate({ ...options, fps, ratio: inputRatio })) |
19 | const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps }) | ||
20 | if (!targetBitrate) return { outputOptions: [ ] } | ||
21 | 22 | ||
22 | return { | 23 | return { |
23 | outputOptions: [ | 24 | outputOptions: [ |
@@ -29,8 +30,10 @@ const defaultX264VODOptionsBuilder: EncoderOptionsBuilder = async ({ inputBitrat | |||
29 | } | 30 | } |
30 | } | 31 | } |
31 | 32 | ||
32 | const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async ({ resolution, fps, inputBitrate, streamNum }) => { | 33 | const defaultX264LiveOptionsBuilder: EncoderOptionsBuilder = async (options: EncoderOptionsBuilderParams) => { |
33 | const targetBitrate = buildTargetBitrate({ inputBitrate, resolution, fps }) | 34 | const { streamNum, fps, inputBitrate, inputRatio } = options |
35 | |||
36 | const targetBitrate = capBitrate(inputBitrate, getAverageBitrate({ ...options, fps, ratio: inputRatio })) | ||
34 | 37 | ||
35 | return { | 38 | return { |
36 | outputOptions: [ | 39 | outputOptions: [ |
@@ -231,14 +234,7 @@ export { | |||
231 | 234 | ||
232 | // --------------------------------------------------------------------------- | 235 | // --------------------------------------------------------------------------- |
233 | 236 | ||
234 | function buildTargetBitrate (options: { | 237 | function capBitrate (inputBitrate: number, targetBitrate: number) { |
235 | inputBitrate: number | ||
236 | resolution: VideoResolution | ||
237 | fps: number | ||
238 | }) { | ||
239 | const { inputBitrate, resolution, fps } = options | ||
240 | |||
241 | const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) | ||
242 | if (!inputBitrate) return targetBitrate | 238 | if (!inputBitrate) return targetBitrate |
243 | 239 | ||
244 | return Math.min(targetBitrate, inputBitrate) | 240 | return Math.min(targetBitrate, inputBitrate) |
diff --git a/server/tests/api/live/live.ts b/server/tests/api/live/live.ts index 4095cdb1c..ba952aff5 100644 --- a/server/tests/api/live/live.ts +++ b/server/tests/api/live/live.ts | |||
@@ -3,7 +3,7 @@ | |||
3 | import 'mocha' | 3 | 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, getVideoFileBitrate, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' | 6 | import { ffprobePromise, getVideoStreamFromFile } from '@server/helpers/ffprobe-utils' |
7 | import { | 7 | import { |
8 | checkLiveCleanupAfterSave, | 8 | checkLiveCleanupAfterSave, |
9 | checkLiveSegmentHash, | 9 | checkLiveSegmentHash, |
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts index 2a09e95bf..f67752d69 100644 --- a/server/tests/api/videos/video-transcoder.ts +++ b/server/tests/api/videos/video-transcoder.ts | |||
@@ -3,6 +3,7 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { omit } from 'lodash' | 5 | import { omit } from 'lodash' |
6 | import { getMaxBitrate } from '@shared/core-utils' | ||
6 | import { | 7 | import { |
7 | buildAbsoluteFixturePath, | 8 | buildAbsoluteFixturePath, |
8 | cleanupTests, | 9 | cleanupTests, |
@@ -17,8 +18,7 @@ import { | |||
17 | waitJobs, | 18 | waitJobs, |
18 | webtorrentAdd | 19 | webtorrentAdd |
19 | } from '@shared/extra-utils' | 20 | } from '@shared/extra-utils' |
20 | import { getMaxBitrate, HttpStatusCode, VideoResolution, VideoState } from '@shared/models' | 21 | import { HttpStatusCode, VideoState } from '@shared/models' |
21 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' | ||
22 | import { | 22 | import { |
23 | canDoQuickTranscode, | 23 | canDoQuickTranscode, |
24 | getAudioStream, | 24 | getAudioStream, |
@@ -191,15 +191,6 @@ describe('Test video transcoding', function () { | |||
191 | it('Should accept and transcode additional extensions', async function () { | 191 | it('Should accept and transcode additional extensions', async function () { |
192 | this.timeout(300_000) | 192 | this.timeout(300_000) |
193 | 193 | ||
194 | let tempFixturePath: string | ||
195 | |||
196 | { | ||
197 | tempFixturePath = await generateHighBitrateVideo() | ||
198 | |||
199 | const bitrate = await getVideoFileBitrate(tempFixturePath) | ||
200 | expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS)) | ||
201 | } | ||
202 | |||
203 | for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { | 194 | for (const fixture of [ 'video_short.mkv', 'video_short.avi' ]) { |
204 | const attributes = { | 195 | const attributes = { |
205 | name: fixture, | 196 | name: fixture, |
@@ -555,14 +546,7 @@ describe('Test video transcoding', function () { | |||
555 | it('Should respect maximum bitrate values', async function () { | 546 | it('Should respect maximum bitrate values', async function () { |
556 | this.timeout(160_000) | 547 | this.timeout(160_000) |
557 | 548 | ||
558 | let tempFixturePath: string | 549 | const tempFixturePath = await generateHighBitrateVideo() |
559 | |||
560 | { | ||
561 | tempFixturePath = await generateHighBitrateVideo() | ||
562 | |||
563 | const bitrate = await getVideoFileBitrate(tempFixturePath) | ||
564 | expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS)) | ||
565 | } | ||
566 | 550 | ||
567 | const attributes = { | 551 | const attributes = { |
568 | name: 'high bitrate video', | 552 | name: 'high bitrate video', |
@@ -586,10 +570,12 @@ describe('Test video transcoding', function () { | |||
586 | 570 | ||
587 | const bitrate = await getVideoFileBitrate(path) | 571 | const bitrate = await getVideoFileBitrate(path) |
588 | const fps = await getVideoFileFPS(path) | 572 | const fps = await getVideoFileFPS(path) |
589 | const { videoFileResolution } = await getVideoFileResolution(path) | 573 | const dataResolution = await getVideoFileResolution(path) |
574 | |||
575 | expect(resolution).to.equal(resolution) | ||
590 | 576 | ||
591 | expect(videoFileResolution).to.equal(resolution) | 577 | const maxBitrate = getMaxBitrate({ ...dataResolution, fps }) |
592 | expect(bitrate).to.be.below(getMaxBitrate(videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) | 578 | expect(bitrate).to.be.below(maxBitrate) |
593 | } | 579 | } |
594 | } | 580 | } |
595 | }) | 581 | }) |
diff --git a/server/tests/cli/optimize-old-videos.ts b/server/tests/cli/optimize-old-videos.ts index 579b2e7d8..9b75ae164 100644 --- a/server/tests/cli/optimize-old-videos.ts +++ b/server/tests/cli/optimize-old-videos.ts | |||
@@ -2,6 +2,7 @@ | |||
2 | 2 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { getMaxBitrate } from '@shared/core-utils' | ||
5 | import { | 6 | import { |
6 | cleanupTests, | 7 | cleanupTests, |
7 | createMultipleServers, | 8 | createMultipleServers, |
@@ -12,9 +13,7 @@ import { | |||
12 | wait, | 13 | wait, |
13 | waitJobs | 14 | waitJobs |
14 | } from '@shared/extra-utils' | 15 | } from '@shared/extra-utils' |
15 | import { getMaxBitrate, VideoResolution } from '@shared/models' | ||
16 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffprobe-utils' | 16 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffprobe-utils' |
17 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' | ||
18 | 17 | ||
19 | const expect = chai.expect | 18 | const expect = chai.expect |
20 | 19 | ||
@@ -30,14 +29,7 @@ describe('Test optimize old videos', function () { | |||
30 | 29 | ||
31 | await doubleFollow(servers[0], servers[1]) | 30 | await doubleFollow(servers[0], servers[1]) |
32 | 31 | ||
33 | let tempFixturePath: string | 32 | const tempFixturePath = await generateHighBitrateVideo() |
34 | |||
35 | { | ||
36 | tempFixturePath = await generateHighBitrateVideo() | ||
37 | |||
38 | const bitrate = await getVideoFileBitrate(tempFixturePath) | ||
39 | expect(bitrate).to.be.above(getMaxBitrate(VideoResolution.H_1080P, 25, VIDEO_TRANSCODING_FPS)) | ||
40 | } | ||
41 | 33 | ||
42 | // Upload two videos for our needs | 34 | // Upload two videos for our needs |
43 | await servers[0].videos.upload({ attributes: { name: 'video1', fixture: tempFixturePath } }) | 35 | await servers[0].videos.upload({ attributes: { name: 'video1', fixture: tempFixturePath } }) |
@@ -88,10 +80,12 @@ describe('Test optimize old videos', function () { | |||
88 | const path = servers[0].servers.buildWebTorrentFilePath(file.fileUrl) | 80 | const path = servers[0].servers.buildWebTorrentFilePath(file.fileUrl) |
89 | const bitrate = await getVideoFileBitrate(path) | 81 | const bitrate = await getVideoFileBitrate(path) |
90 | const fps = await getVideoFileFPS(path) | 82 | const fps = await getVideoFileFPS(path) |
91 | const resolution = await getVideoFileResolution(path) | 83 | const data = await getVideoFileResolution(path) |
84 | |||
85 | expect(data.resolution).to.equal(file.resolution.id) | ||
92 | 86 | ||
93 | expect(resolution.videoFileResolution).to.equal(file.resolution.id) | 87 | const maxBitrate = getMaxBitrate({ ...data, fps }) |
94 | expect(bitrate).to.be.below(getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) | 88 | expect(bitrate).to.be.below(maxBitrate) |
95 | } | 89 | } |
96 | } | 90 | } |
97 | }) | 91 | }) |
diff --git a/server/tests/cli/print-transcode-command.ts b/server/tests/cli/print-transcode-command.ts index 3a7969e68..e328a6072 100644 --- a/server/tests/cli/print-transcode-command.ts +++ b/server/tests/cli/print-transcode-command.ts | |||
@@ -3,16 +3,16 @@ | |||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { getVideoFileBitrate, getVideoFileFPS } from '@server/helpers/ffprobe-utils' | 5 | import { getVideoFileBitrate, getVideoFileFPS } from '@server/helpers/ffprobe-utils' |
6 | import { CLICommand } from '@shared/extra-utils' | 6 | import { getMaxBitrate } from '@shared/core-utils' |
7 | import { getTargetBitrate, VideoResolution } from '../../../shared/models/videos' | 7 | import { buildAbsoluteFixturePath, CLICommand } from '@shared/extra-utils' |
8 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' | 8 | import { VideoResolution } from '../../../shared/models/videos' |
9 | 9 | ||
10 | const expect = chai.expect | 10 | const expect = chai.expect |
11 | 11 | ||
12 | describe('Test create transcoding jobs', function () { | 12 | describe('Test create transcoding jobs', function () { |
13 | 13 | ||
14 | it('Should print the correct command for each resolution', async function () { | 14 | it('Should print the correct command for each resolution', async function () { |
15 | const fixturePath = 'server/tests/fixtures/video_short.webm' | 15 | const fixturePath = buildAbsoluteFixturePath('video_short.webm') |
16 | const fps = await getVideoFileFPS(fixturePath) | 16 | const fps = await getVideoFileFPS(fixturePath) |
17 | const bitrate = await getVideoFileBitrate(fixturePath) | 17 | const bitrate = await getVideoFileBitrate(fixturePath) |
18 | 18 | ||
@@ -21,7 +21,7 @@ describe('Test create transcoding jobs', function () { | |||
21 | VideoResolution.H_1080P | 21 | VideoResolution.H_1080P |
22 | ]) { | 22 | ]) { |
23 | const command = await CLICommand.exec(`npm run print-transcode-command -- ${fixturePath} -r ${resolution}`) | 23 | const command = await CLICommand.exec(`npm run print-transcode-command -- ${fixturePath} -r ${resolution}`) |
24 | const targetBitrate = Math.min(getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS), bitrate) | 24 | const targetBitrate = Math.min(getMaxBitrate({ resolution, fps, ratio: 16 / 9 }), bitrate) |
25 | 25 | ||
26 | expect(command).to.includes(`-vf scale=w=-2:h=${resolution}`) | 26 | expect(command).to.includes(`-vf scale=w=-2:h=${resolution}`) |
27 | expect(command).to.includes(`-y -acodec aac -vcodec libx264`) | 27 | expect(command).to.includes(`-y -acodec aac -vcodec libx264`) |
diff --git a/server/tests/helpers/core-utils.ts b/server/tests/helpers/core-utils.ts index d5cac51a3..a6bf5b4c5 100644 --- a/server/tests/helpers/core-utils.ts +++ b/server/tests/helpers/core-utils.ts | |||
@@ -4,6 +4,8 @@ import 'mocha' | |||
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { snakeCase } from 'lodash' | 5 | import { snakeCase } from 'lodash' |
6 | import validator from 'validator' | 6 | import validator from 'validator' |
7 | import { getAverageBitrate, getMaxBitrate } from '@shared/core-utils' | ||
8 | import { VideoResolution } from '@shared/models' | ||
7 | import { objectConverter, parseBytes } from '../../helpers/core-utils' | 9 | import { objectConverter, parseBytes } from '../../helpers/core-utils' |
8 | 10 | ||
9 | const expect = chai.expect | 11 | const expect = chai.expect |
@@ -46,6 +48,9 @@ describe('Parse Bytes', function () { | |||
46 | it('Should be invalid when given invalid value', async function () { | 48 | it('Should be invalid when given invalid value', async function () { |
47 | expect(parseBytes('6GB 1GB')).to.be.eq(6) | 49 | expect(parseBytes('6GB 1GB')).to.be.eq(6) |
48 | }) | 50 | }) |
51 | }) | ||
52 | |||
53 | describe('Object', function () { | ||
49 | 54 | ||
50 | it('Should convert an object', async function () { | 55 | it('Should convert an object', async function () { |
51 | function keyConverter (k: string) { | 56 | function keyConverter (k: string) { |
@@ -94,3 +99,36 @@ describe('Parse Bytes', function () { | |||
94 | expect(obj['my_super_key']).to.be.undefined | 99 | expect(obj['my_super_key']).to.be.undefined |
95 | }) | 100 | }) |
96 | }) | 101 | }) |
102 | |||
103 | describe('Bitrate', function () { | ||
104 | |||
105 | it('Should get appropriate max bitrate', function () { | ||
106 | const tests = [ | ||
107 | { resolution: VideoResolution.H_240P, ratio: 16 / 9, fps: 24, min: 600, max: 800 }, | ||
108 | { resolution: VideoResolution.H_360P, ratio: 16 / 9, fps: 24, min: 1200, max: 1600 }, | ||
109 | { resolution: VideoResolution.H_480P, ratio: 16 / 9, fps: 24, min: 2000, max: 2300 }, | ||
110 | { resolution: VideoResolution.H_720P, ratio: 16 / 9, fps: 24, min: 4000, max: 4400 }, | ||
111 | { resolution: VideoResolution.H_1080P, ratio: 16 / 9, fps: 24, min: 8000, max: 10000 }, | ||
112 | { resolution: VideoResolution.H_4K, ratio: 16 / 9, fps: 24, min: 25000, max: 30000 } | ||
113 | ] | ||
114 | |||
115 | for (const test of tests) { | ||
116 | expect(getMaxBitrate(test)).to.be.above(test.min * 1000).and.below(test.max * 1000) | ||
117 | } | ||
118 | }) | ||
119 | |||
120 | it('Should get appropriate average bitrate', function () { | ||
121 | const tests = [ | ||
122 | { resolution: VideoResolution.H_240P, ratio: 16 / 9, fps: 24, min: 350, max: 450 }, | ||
123 | { resolution: VideoResolution.H_360P, ratio: 16 / 9, fps: 24, min: 700, max: 900 }, | ||
124 | { resolution: VideoResolution.H_480P, ratio: 16 / 9, fps: 24, min: 1100, max: 1300 }, | ||
125 | { resolution: VideoResolution.H_720P, ratio: 16 / 9, fps: 24, min: 2300, max: 2500 }, | ||
126 | { resolution: VideoResolution.H_1080P, ratio: 16 / 9, fps: 24, min: 4700, max: 5000 }, | ||
127 | { resolution: VideoResolution.H_4K, ratio: 16 / 9, fps: 24, min: 15000, max: 17000 } | ||
128 | ] | ||
129 | |||
130 | for (const test of tests) { | ||
131 | expect(getAverageBitrate(test)).to.be.above(test.min * 1000).and.below(test.max * 1000) | ||
132 | } | ||
133 | }) | ||
134 | }) | ||
diff --git a/shared/core-utils/index.ts b/shared/core-utils/index.ts index 2a7d4d982..e0a6a8087 100644 --- a/shared/core-utils/index.ts +++ b/shared/core-utils/index.ts | |||
@@ -5,3 +5,4 @@ export * from './plugins' | |||
5 | export * from './renderer' | 5 | export * from './renderer' |
6 | export * from './users' | 6 | export * from './users' |
7 | export * from './utils' | 7 | export * from './utils' |
8 | export * from './videos' | ||
diff --git a/shared/core-utils/videos/bitrate.ts b/shared/core-utils/videos/bitrate.ts new file mode 100644 index 000000000..3d4e47906 --- /dev/null +++ b/shared/core-utils/videos/bitrate.ts | |||
@@ -0,0 +1,86 @@ | |||
1 | import { VideoResolution } from "@shared/models" | ||
2 | |||
3 | type BitPerPixel = { [ id in VideoResolution ]: number } | ||
4 | |||
5 | // https://bitmovin.com/video-bitrate-streaming-hls-dash/ | ||
6 | |||
7 | const averageBitPerPixel: BitPerPixel = { | ||
8 | [VideoResolution.H_NOVIDEO]: 0, | ||
9 | [VideoResolution.H_240P]: 0.17, | ||
10 | [VideoResolution.H_360P]: 0.15, | ||
11 | [VideoResolution.H_480P]: 0.12, | ||
12 | [VideoResolution.H_720P]: 0.11, | ||
13 | [VideoResolution.H_1080P]: 0.10, | ||
14 | [VideoResolution.H_1440P]: 0.09, | ||
15 | [VideoResolution.H_4K]: 0.08 | ||
16 | } | ||
17 | |||
18 | const maxBitPerPixel: BitPerPixel = { | ||
19 | [VideoResolution.H_NOVIDEO]: 0, | ||
20 | [VideoResolution.H_240P]: 0.29, | ||
21 | [VideoResolution.H_360P]: 0.26, | ||
22 | [VideoResolution.H_480P]: 0.22, | ||
23 | [VideoResolution.H_720P]: 0.19, | ||
24 | [VideoResolution.H_1080P]: 0.17, | ||
25 | [VideoResolution.H_1440P]: 0.16, | ||
26 | [VideoResolution.H_4K]: 0.14 | ||
27 | } | ||
28 | |||
29 | function getAverageBitrate (options: { | ||
30 | resolution: VideoResolution | ||
31 | ratio: number | ||
32 | fps: number | ||
33 | }) { | ||
34 | const targetBitrate = calculateBitrate({ ...options, bitPerPixel: averageBitPerPixel }) | ||
35 | if (!targetBitrate) return 192 * 1000 | ||
36 | |||
37 | return targetBitrate | ||
38 | } | ||
39 | |||
40 | function getMaxBitrate (options: { | ||
41 | resolution: VideoResolution | ||
42 | ratio: number | ||
43 | fps: number | ||
44 | }) { | ||
45 | const targetBitrate = calculateBitrate({ ...options, bitPerPixel: maxBitPerPixel }) | ||
46 | if (!targetBitrate) return 256 * 1000 | ||
47 | |||
48 | return targetBitrate | ||
49 | } | ||
50 | |||
51 | // --------------------------------------------------------------------------- | ||
52 | |||
53 | export { | ||
54 | getAverageBitrate, | ||
55 | getMaxBitrate | ||
56 | } | ||
57 | |||
58 | // --------------------------------------------------------------------------- | ||
59 | |||
60 | function calculateBitrate (options: { | ||
61 | bitPerPixel: BitPerPixel | ||
62 | resolution: VideoResolution | ||
63 | ratio: number | ||
64 | fps: number | ||
65 | }) { | ||
66 | const { bitPerPixel, resolution, ratio, fps } = options | ||
67 | |||
68 | const resolutionsOrder = [ | ||
69 | VideoResolution.H_4K, | ||
70 | VideoResolution.H_1440P, | ||
71 | VideoResolution.H_1080P, | ||
72 | VideoResolution.H_720P, | ||
73 | VideoResolution.H_480P, | ||
74 | VideoResolution.H_360P, | ||
75 | VideoResolution.H_240P, | ||
76 | VideoResolution.H_NOVIDEO | ||
77 | ] | ||
78 | |||
79 | for (const toTestResolution of resolutionsOrder) { | ||
80 | if (toTestResolution <= resolution) { | ||
81 | return resolution * resolution * ratio * fps * bitPerPixel[toTestResolution] | ||
82 | } | ||
83 | } | ||
84 | |||
85 | throw new Error('Unknown resolution ' + resolution) | ||
86 | } | ||
diff --git a/shared/core-utils/videos/index.ts b/shared/core-utils/videos/index.ts new file mode 100644 index 000000000..5a1145f1a --- /dev/null +++ b/shared/core-utils/videos/index.ts | |||
@@ -0,0 +1 @@ | |||
export * from './bitrate' | |||
diff --git a/shared/extra-utils/miscs/generate.ts b/shared/extra-utils/miscs/generate.ts index 8d6435481..a03a20049 100644 --- a/shared/extra-utils/miscs/generate.ts +++ b/shared/extra-utils/miscs/generate.ts | |||
@@ -1,8 +1,20 @@ | |||
1 | import { expect } from 'chai' | ||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 2 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { ensureDir, pathExists } from 'fs-extra' | 3 | import { ensureDir, pathExists } from 'fs-extra' |
3 | import { dirname } from 'path' | 4 | import { dirname } from 'path' |
5 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | ||
6 | import { getMaxBitrate } from '@shared/core-utils' | ||
4 | import { buildAbsoluteFixturePath } from './tests' | 7 | import { buildAbsoluteFixturePath } from './tests' |
5 | 8 | ||
9 | async function ensureHasTooBigBitrate (fixturePath: string) { | ||
10 | const bitrate = await getVideoFileBitrate(fixturePath) | ||
11 | const dataResolution = await getVideoFileResolution(fixturePath) | ||
12 | const fps = await getVideoFileFPS(fixturePath) | ||
13 | |||
14 | const maxBitrate = getMaxBitrate({ ...dataResolution, fps }) | ||
15 | expect(bitrate).to.be.above(maxBitrate) | ||
16 | } | ||
17 | |||
6 | async function generateHighBitrateVideo () { | 18 | async function generateHighBitrateVideo () { |
7 | const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) | 19 | const tempFixturePath = buildAbsoluteFixturePath('video_high_bitrate_1080p.mp4', true) |
8 | 20 | ||
@@ -28,6 +40,8 @@ async function generateHighBitrateVideo () { | |||
28 | }) | 40 | }) |
29 | } | 41 | } |
30 | 42 | ||
43 | await ensureHasTooBigBitrate(tempFixturePath) | ||
44 | |||
31 | return tempFixturePath | 45 | return tempFixturePath |
32 | } | 46 | } |
33 | 47 | ||
diff --git a/shared/models/videos/video-resolution.enum.ts b/shared/models/videos/video-resolution.enum.ts index a5d2ac7fa..24cd2d04d 100644 --- a/shared/models/videos/video-resolution.enum.ts +++ b/shared/models/videos/video-resolution.enum.ts | |||
@@ -1,5 +1,3 @@ | |||
1 | import { VideoTranscodingFPS } from './video-transcoding-fps.model' | ||
2 | |||
3 | export const enum VideoResolution { | 1 | export const enum VideoResolution { |
4 | H_NOVIDEO = 0, | 2 | H_NOVIDEO = 0, |
5 | H_240P = 240, | 3 | H_240P = 240, |
@@ -10,90 +8,3 @@ export const enum VideoResolution { | |||
10 | H_1440P = 1440, | 8 | H_1440P = 1440, |
11 | H_4K = 2160 | 9 | H_4K = 2160 |
12 | } | 10 | } |
13 | |||
14 | /** | ||
15 | * Bitrate targets for different resolutions, at VideoTranscodingFPS.AVERAGE. | ||
16 | * | ||
17 | * Sources for individual quality levels: | ||
18 | * Google Live Encoder: https://support.google.com/youtube/answer/2853702?hl=en | ||
19 | * YouTube Video Info: youtube-dl --list-formats, with sample videos | ||
20 | */ | ||
21 | function getBaseBitrate (resolution: number) { | ||
22 | if (resolution === VideoResolution.H_NOVIDEO) { | ||
23 | // audio-only | ||
24 | return 64 * 1000 | ||
25 | } | ||
26 | |||
27 | if (resolution <= VideoResolution.H_240P) { | ||
28 | // quality according to Google Live Encoder: 300 - 700 Kbps | ||
29 | // Quality according to YouTube Video Info: 285 Kbps | ||
30 | return 320 * 1000 | ||
31 | } | ||
32 | |||
33 | if (resolution <= VideoResolution.H_360P) { | ||
34 | // quality according to Google Live Encoder: 400 - 1,000 Kbps | ||
35 | // Quality according to YouTube Video Info: 700 Kbps | ||
36 | return 780 * 1000 | ||
37 | } | ||
38 | |||
39 | if (resolution <= VideoResolution.H_480P) { | ||
40 | // quality according to Google Live Encoder: 500 - 2,000 Kbps | ||
41 | // Quality according to YouTube Video Info: 1300 Kbps | ||
42 | return 1500 * 1000 | ||
43 | } | ||
44 | |||
45 | if (resolution <= VideoResolution.H_720P) { | ||
46 | // quality according to Google Live Encoder: 1,500 - 4,000 Kbps | ||
47 | // Quality according to YouTube Video Info: 2680 Kbps | ||
48 | return 2800 * 1000 | ||
49 | } | ||
50 | |||
51 | if (resolution <= VideoResolution.H_1080P) { | ||
52 | // quality according to Google Live Encoder: 3000 - 6000 Kbps | ||
53 | // Quality according to YouTube Video Info: 5081 Kbps | ||
54 | return 5200 * 1000 | ||
55 | } | ||
56 | |||
57 | if (resolution <= VideoResolution.H_1440P) { | ||
58 | // quality according to Google Live Encoder: 6000 - 13000 Kbps | ||
59 | // Quality according to YouTube Video Info: 8600 (av01) - 17000 (vp9.2) Kbps | ||
60 | return 10_000 * 1000 | ||
61 | } | ||
62 | |||
63 | // 4K | ||
64 | // quality according to Google Live Encoder: 13000 - 34000 Kbps | ||
65 | return 22_000 * 1000 | ||
66 | } | ||
67 | |||
68 | /** | ||
69 | * Calculate the target bitrate based on video resolution and FPS. | ||
70 | * | ||
71 | * The calculation is based on two values: | ||
72 | * Bitrate at VideoTranscodingFPS.AVERAGE is always the same as | ||
73 | * getBaseBitrate(). Bitrate at VideoTranscodingFPS.MAX is always | ||
74 | * getBaseBitrate() * 1.4. All other values are calculated linearly | ||
75 | * between these two points. | ||
76 | */ | ||
77 | export function getTargetBitrate (resolution: number, fps: number, fpsTranscodingConstants: VideoTranscodingFPS) { | ||
78 | const baseBitrate = getBaseBitrate(resolution) | ||
79 | // The maximum bitrate, used when fps === VideoTranscodingFPS.MAX | ||
80 | // Based on numbers from Youtube, 60 fps bitrate divided by 30 fps bitrate: | ||
81 | // 720p: 2600 / 1750 = 1.49 | ||
82 | // 1080p: 4400 / 3300 = 1.33 | ||
83 | const maxBitrate = baseBitrate * 1.4 | ||
84 | const maxBitrateDifference = maxBitrate - baseBitrate | ||
85 | const maxFpsDifference = fpsTranscodingConstants.MAX - fpsTranscodingConstants.AVERAGE | ||
86 | // For 1080p video with default settings, this results in the following formula: | ||
87 | // 3300 + (x - 30) * (1320/30) | ||
88 | // Example outputs: | ||
89 | // 1080p10: 2420 kbps, 1080p30: 3300 kbps, 1080p60: 4620 kbps | ||
90 | // 720p10: 1283 kbps, 720p30: 1750 kbps, 720p60: 2450 kbps | ||
91 | return Math.floor(baseBitrate + (fps - fpsTranscodingConstants.AVERAGE) * (maxBitrateDifference / maxFpsDifference)) | ||
92 | } | ||
93 | |||
94 | /** | ||
95 | * The maximum bitrate we expect to see on a transcoded video in bytes per second. | ||
96 | */ | ||
97 | export function getMaxBitrate (resolution: VideoResolution, fps: number, fpsTranscodingConstants: VideoTranscodingFPS) { | ||
98 | return getTargetBitrate(resolution, fps, fpsTranscodingConstants) * 2 | ||
99 | } | ||
diff --git a/shared/models/videos/video-transcoding.model.ts b/shared/models/videos/video-transcoding.model.ts index f1fe4609b..83b8e98a0 100644 --- a/shared/models/videos/video-transcoding.model.ts +++ b/shared/models/videos/video-transcoding.model.ts | |||
@@ -2,13 +2,23 @@ import { VideoResolution } from './video-resolution.enum' | |||
2 | 2 | ||
3 | // Types used by plugins and ffmpeg-utils | 3 | // Types used by plugins and ffmpeg-utils |
4 | 4 | ||
5 | export type EncoderOptionsBuilder = (params: { | 5 | export type EncoderOptionsBuilderParams = { |
6 | input: string | 6 | input: string |
7 | |||
7 | resolution: VideoResolution | 8 | resolution: VideoResolution |
8 | inputBitrate: number | 9 | |
10 | // Could be null for "merge audio" transcoding | ||
9 | fps?: number | 11 | fps?: number |
12 | |||
13 | // Could be undefined if we could not get input bitrate (some RTMP streams for example) | ||
14 | inputBitrate: number | ||
15 | inputRatio: number | ||
16 | |||
17 | // For lives | ||
10 | streamNum?: number | 18 | streamNum?: number |
11 | }) => Promise<EncoderOptions> | EncoderOptions | 19 | } |
20 | |||
21 | export type EncoderOptionsBuilder = (params: EncoderOptionsBuilderParams) => Promise<EncoderOptions> | EncoderOptions | ||
12 | 22 | ||
13 | export interface EncoderOptions { | 23 | export interface EncoderOptions { |
14 | copy?: boolean // Copy stream? Default to false | 24 | copy?: boolean // Copy stream? Default to false |