aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2020-11-20 17:16:55 +0100
committerChocobozzz <chocobozzz@cpy.re>2020-11-25 10:07:51 +0100
commitdaf6e4801052d3ca6be2fafd20bae2323b1ce175 (patch)
treea136af611c2543c461ce3fd126ddb7cb1e37a0c2
parent123f61933611f326ea5a5e8c2ea253ee8720e4f0 (diff)
downloadPeerTube-daf6e4801052d3ca6be2fafd20bae2323b1ce175.tar.gz
PeerTube-daf6e4801052d3ca6be2fafd20bae2323b1ce175.tar.zst
PeerTube-daf6e4801052d3ca6be2fafd20bae2323b1ce175.zip
Split ffmpeg utils with ffprobe utils
-rwxr-xr-xscripts/create-transcoding-job.ts2
-rw-r--r--scripts/optimize-old-videos.ts2
-rw-r--r--server/controllers/api/videos/index.ts4
-rw-r--r--server/helpers/ffmpeg-utils.ts310
-rw-r--r--server/helpers/ffprobe-utils.ts249
-rw-r--r--server/initializers/migrations/0075-video-resolutions.ts2
-rw-r--r--server/lib/hls.ts14
-rw-r--r--server/lib/job-queue/handlers/video-file-import.ts12
-rw-r--r--server/lib/job-queue/handlers/video-import.ts2
-rw-r--r--server/lib/job-queue/handlers/video-live-ending.ts5
-rw-r--r--server/lib/job-queue/handlers/video-transcoding.ts2
-rw-r--r--server/lib/live-manager.ts9
-rw-r--r--server/lib/video-transcoding.ts11
-rw-r--r--server/middlewares/validators/videos/videos.ts2
-rw-r--r--server/models/video/video.ts2
-rw-r--r--server/tests/api/videos/audio-only.ts8
-rw-r--r--server/tests/api/videos/video-transcoder.ts41
-rw-r--r--server/tests/cli/optimize-old-videos.ts6
18 files changed, 339 insertions, 344 deletions
diff --git a/scripts/create-transcoding-job.ts b/scripts/create-transcoding-job.ts
index 78eb455aa..2eed53f42 100755
--- a/scripts/create-transcoding-job.ts
+++ b/scripts/create-transcoding-job.ts
@@ -5,7 +5,7 @@ import * as program from 'commander'
5import { VideoModel } from '../server/models/video/video' 5import { VideoModel } from '../server/models/video/video'
6import { initDatabaseModels } from '../server/initializers/database' 6import { initDatabaseModels } from '../server/initializers/database'
7import { JobQueue } from '../server/lib/job-queue' 7import { JobQueue } from '../server/lib/job-queue'
8import { computeResolutionsToTranscode } from '@server/helpers/ffmpeg-utils' 8import { computeResolutionsToTranscode } from '@server/helpers/ffprobe-utils'
9import { VideoTranscodingPayload } from '@shared/models' 9import { VideoTranscodingPayload } from '@shared/models'
10 10
11program 11program
diff --git a/scripts/optimize-old-videos.ts b/scripts/optimize-old-videos.ts
index 9595efd9c..d5696de67 100644
--- a/scripts/optimize-old-videos.ts
+++ b/scripts/optimize-old-videos.ts
@@ -2,7 +2,7 @@ import { registerTSPaths } from '../server/helpers/register-ts-paths'
2registerTSPaths() 2registerTSPaths()
3 3
4import { VIDEO_TRANSCODING_FPS } from '../server/initializers/constants' 4import { VIDEO_TRANSCODING_FPS } from '../server/initializers/constants'
5import { getDurationFromVideoFile, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../server/helpers/ffmpeg-utils' 5import { getDurationFromVideoFile, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../server/helpers/ffprobe-utils'
6import { getMaxBitrate } from '../shared/models/videos' 6import { getMaxBitrate } from '../shared/models/videos'
7import { VideoModel } from '../server/models/video/video' 7import { VideoModel } from '../server/models/video/video'
8import { optimizeOriginalVideofile } from '../server/lib/video-transcoding' 8import { optimizeOriginalVideofile } from '../server/lib/video-transcoding'
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts
index b5ff2e72e..e8480d749 100644
--- a/server/controllers/api/videos/index.ts
+++ b/server/controllers/api/videos/index.ts
@@ -16,7 +16,7 @@ import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
16import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' 16import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
17import { resetSequelizeInstance } from '../../../helpers/database-utils' 17import { resetSequelizeInstance } from '../../../helpers/database-utils'
18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' 18import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 19import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
20import { logger } from '../../../helpers/logger' 20import { logger } from '../../../helpers/logger'
21import { getFormattedObjects } from '../../../helpers/utils' 21import { getFormattedObjects } from '../../../helpers/utils'
22import { CONFIG } from '../../../initializers/config' 22import { CONFIG } from '../../../initializers/config'
@@ -195,7 +195,7 @@ async function addVideo (req: express.Request, res: express.Response) {
195 extname: extname(videoPhysicalFile.filename), 195 extname: extname(videoPhysicalFile.filename),
196 size: videoPhysicalFile.size, 196 size: videoPhysicalFile.size,
197 videoStreamingPlaylistId: null, 197 videoStreamingPlaylistId: null,
198 metadata: await getMetadataFromFile<any>(videoPhysicalFile.path) 198 metadata: await getMetadataFromFile(videoPhysicalFile.path)
199 }) 199 })
200 200
201 if (videoFile.isAudio()) { 201 if (videoFile.isAudio()) {
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 66b9d2e44..df3926658 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,201 +1,14 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { readFile, remove, writeFile } from 'fs-extra' 2import { readFile, remove, writeFile } from 'fs-extra'
3import { dirname, join } from 'path' 3import { dirname, join } from 'path'
4import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata' 4import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
5import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
6import { checkFFmpegEncoders } from '../initializers/checker-before-init' 5import { checkFFmpegEncoders } from '../initializers/checker-before-init'
7import { CONFIG } from '../initializers/config' 6import { CONFIG } from '../initializers/config'
8import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' 7import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
8import { getAudioStream, getClosestFramerateStandard, getMaxAudioBitrate, getVideoFileFPS } from './ffprobe-utils'
9import { processImage } from './image-utils' 9import { processImage } from './image-utils'
10import { logger } from './logger' 10import { logger } from './logger'
11 11
12/**
13 * A toolbox to play with audio
14 */
15namespace audio {
16 export const get = (videoPath: string) => {
17 // without position, ffprobe considers the last input only
18 // we make it consider the first input only
19 // if you pass a file path to pos, then ffprobe acts on that file directly
20 return new Promise<{ absolutePath: string, audioStream?: any }>((res, rej) => {
21
22 function parseFfprobe (err: any, data: ffmpeg.FfprobeData) {
23 if (err) return rej(err)
24
25 if ('streams' in data) {
26 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
27 if (audioStream) {
28 return res({
29 absolutePath: data.format.filename,
30 audioStream
31 })
32 }
33 }
34
35 return res({ absolutePath: data.format.filename })
36 }
37
38 return ffmpeg.ffprobe(videoPath, parseFfprobe)
39 })
40 }
41
42 export namespace bitrate {
43 const baseKbitrate = 384
44
45 const toBits = (kbits: number) => kbits * 8000
46
47 export const aac = (bitrate: number): number => {
48 switch (true) {
49 case bitrate > toBits(baseKbitrate):
50 return baseKbitrate
51
52 default:
53 return -1 // we interpret it as a signal to copy the audio stream as is
54 }
55 }
56
57 export const mp3 = (bitrate: number): number => {
58 /*
59 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
60 That's why, when using aac, we can go to lower kbit/sec. The equivalences
61 made here are not made to be accurate, especially with good mp3 encoders.
62 */
63 switch (true) {
64 case bitrate <= toBits(192):
65 return 128
66
67 case bitrate <= toBits(384):
68 return 256
69
70 default:
71 return baseKbitrate
72 }
73 }
74 }
75}
76
77function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
78 const configResolutions = type === 'vod'
79 ? CONFIG.TRANSCODING.RESOLUTIONS
80 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
81
82 const resolutionsEnabled: number[] = []
83
84 // Put in the order we want to proceed jobs
85 const resolutions = [
86 VideoResolution.H_NOVIDEO,
87 VideoResolution.H_480P,
88 VideoResolution.H_360P,
89 VideoResolution.H_720P,
90 VideoResolution.H_240P,
91 VideoResolution.H_1080P,
92 VideoResolution.H_4K
93 ]
94
95 for (const resolution of resolutions) {
96 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
97 resolutionsEnabled.push(resolution)
98 }
99 }
100
101 return resolutionsEnabled
102}
103
104async function getVideoStreamSize (path: string) {
105 const videoStream = await getVideoStreamFromFile(path)
106
107 return videoStream === null
108 ? { width: 0, height: 0 }
109 : { width: videoStream.width, height: videoStream.height }
110}
111
112async function getVideoStreamCodec (path: string) {
113 const videoStream = await getVideoStreamFromFile(path)
114
115 if (!videoStream) return ''
116
117 const videoCodec = videoStream.codec_tag_string
118
119 const baseProfileMatrix = {
120 High: '6400',
121 Main: '4D40',
122 Baseline: '42E0'
123 }
124
125 let baseProfile = baseProfileMatrix[videoStream.profile]
126 if (!baseProfile) {
127 logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
128 baseProfile = baseProfileMatrix['High'] // Fallback
129 }
130
131 let level = videoStream.level.toString(16)
132 if (level.length === 1) level = `0${level}`
133
134 return `${videoCodec}.${baseProfile}${level}`
135}
136
137async function getAudioStreamCodec (path: string) {
138 const { audioStream } = await audio.get(path)
139
140 if (!audioStream) return ''
141
142 const audioCodec = audioStream.codec_name
143 if (audioCodec === 'aac') return 'mp4a.40.2'
144
145 logger.warn('Cannot get audio codec of %s.', path, { audioStream })
146
147 return 'mp4a.40.2' // Fallback
148}
149
150async function getVideoFileResolution (path: string) {
151 const size = await getVideoStreamSize(path)
152
153 return {
154 videoFileResolution: Math.min(size.height, size.width),
155 isPortraitMode: size.height > size.width
156 }
157}
158
159async function getVideoFileFPS (path: string) {
160 const videoStream = await getVideoStreamFromFile(path)
161 if (videoStream === null) return 0
162
163 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
164 const valuesText: string = videoStream[key]
165 if (!valuesText) continue
166
167 const [ frames, seconds ] = valuesText.split('/')
168 if (!frames || !seconds) continue
169
170 const result = parseInt(frames, 10) / parseInt(seconds, 10)
171 if (result > 0) return Math.round(result)
172 }
173
174 return 0
175}
176
177async function getMetadataFromFile <T> (path: string, cb = metadata => metadata) {
178 return new Promise<T>((res, rej) => {
179 ffmpeg.ffprobe(path, (err, metadata) => {
180 if (err) return rej(err)
181
182 return res(cb(new VideoFileMetadata(metadata)))
183 })
184 })
185}
186
187async function getVideoFileBitrate (path: string) {
188 return getMetadataFromFile<number>(path, metadata => metadata.format.bit_rate)
189}
190
191function getDurationFromVideoFile (path: string) {
192 return getMetadataFromFile<number>(path, metadata => Math.floor(metadata.format.duration))
193}
194
195function getVideoStreamFromFile (path: string) {
196 return getMetadataFromFile<any>(path, metadata => metadata.streams.find(s => s.codec_type === 'video') || null)
197}
198
199async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) { 12async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) {
200 const pendingImageName = 'pending-' + imageName 13 const pendingImageName = 'pending-' + imageName
201 14
@@ -228,6 +41,10 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
228 } 41 }
229} 42}
230 43
44// ---------------------------------------------------------------------------
45// Transcode meta function
46// ---------------------------------------------------------------------------
47
231type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio' 48type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio'
232 49
233interface BaseTranscodeOptions { 50interface BaseTranscodeOptions {
@@ -270,72 +87,27 @@ type TranscodeOptions =
270 | OnlyAudioTranscodeOptions 87 | OnlyAudioTranscodeOptions
271 | QuickTranscodeOptions 88 | QuickTranscodeOptions
272 89
273function transcode (options: TranscodeOptions) { 90const builders: {
91 [ type in TranscodeOptionsType ]: (c: ffmpeg.FfmpegCommand, o?: TranscodeOptions) => Promise<ffmpeg.FfmpegCommand> | ffmpeg.FfmpegCommand
92} = {
93 'quick-transcode': buildQuickTranscodeCommand,
94 'hls': buildHLSVODCommand,
95 'merge-audio': buildAudioMergeCommand,
96 'only-audio': buildOnlyAudioCommand,
97 'video': buildx264Command
98}
99
100async function transcode (options: TranscodeOptions) {
274 logger.debug('Will run transcode.', { options }) 101 logger.debug('Will run transcode.', { options })
275 102
276 return new Promise<void>(async (res, rej) => { 103 let command = getFFmpeg(options.inputPath)
277 try { 104 .output(options.outputPath)
278 let command = getFFmpeg(options.inputPath)
279 .output(options.outputPath)
280
281 if (options.type === 'quick-transcode') {
282 command = buildQuickTranscodeCommand(command)
283 } else if (options.type === 'hls') {
284 command = await buildHLSVODCommand(command, options)
285 } else if (options.type === 'merge-audio') {
286 command = await buildAudioMergeCommand(command, options)
287 } else if (options.type === 'only-audio') {
288 command = buildOnlyAudioCommand(command, options)
289 } else {
290 command = await buildx264Command(command, options)
291 }
292
293 command
294 .on('error', (err, stdout, stderr) => {
295 logger.error('Error in transcoding job.', { stdout, stderr })
296 return rej(err)
297 })
298 .on('end', () => {
299 return fixHLSPlaylistIfNeeded(options)
300 .then(() => res())
301 .catch(err => rej(err))
302 })
303 .run()
304 } catch (err) {
305 return rej(err)
306 }
307 })
308}
309 105
310async function canDoQuickTranscode (path: string): Promise<boolean> { 106 command = await builders[options.type](command, options)
311 // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway)
312 const videoStream = await getVideoStreamFromFile(path)
313 const parsedAudio = await audio.get(path)
314 const fps = await getVideoFileFPS(path)
315 const bitRate = await getVideoFileBitrate(path)
316 const resolution = await getVideoFileResolution(path)
317
318 // check video params
319 if (videoStream == null) return false
320 if (videoStream['codec_name'] !== 'h264') return false
321 if (videoStream['pix_fmt'] !== 'yuv420p') return false
322 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
323 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
324
325 // check audio params (if audio stream exists)
326 if (parsedAudio.audioStream) {
327 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
328
329 const maxAudioBitrate = audio.bitrate['aac'](parsedAudio.audioStream['bit_rate'])
330 if (maxAudioBitrate !== -1 && parsedAudio.audioStream['bit_rate'] > maxAudioBitrate) return false
331 }
332 107
333 return true 108 await runCommand(command)
334}
335 109
336function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number { 110 await fixHLSPlaylistIfNeeded(options)
337 return VIDEO_TRANSCODING_FPS[type].slice(0)
338 .sort((a, b) => fps % a - fps % b)[0]
339} 111}
340 112
341function convertWebPToJPG (path: string, destination: string): Promise<void> { 113function convertWebPToJPG (path: string, destination: string): Promise<void> {
@@ -484,12 +256,11 @@ async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: s
484} 256}
485 257
486async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) { 258async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) {
487 command.run()
488
489 return new Promise<string>((res, rej) => { 259 return new Promise<string>((res, rej) => {
490 command.on('error', err => { 260 command.on('error', (err, stdout, stderr) => {
491 if (onEnd) onEnd() 261 if (onEnd) onEnd()
492 262
263 logger.error('Error in transcoding job.', { stdout, stderr })
493 rej(err) 264 rej(err)
494 }) 265 })
495 266
@@ -498,32 +269,23 @@ async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) {
498 269
499 res() 270 res()
500 }) 271 })
272
273 command.run()
501 }) 274 })
502} 275}
503 276
504// --------------------------------------------------------------------------- 277// ---------------------------------------------------------------------------
505 278
506export { 279export {
507 getVideoStreamCodec,
508 getAudioStreamCodec,
509 runLiveMuxing, 280 runLiveMuxing,
510 convertWebPToJPG, 281 convertWebPToJPG,
511 processGIF, 282 processGIF,
512 getVideoStreamSize,
513 getVideoFileResolution,
514 getMetadataFromFile,
515 getDurationFromVideoFile,
516 runLiveTranscoding, 283 runLiveTranscoding,
517 generateImageFromVideoFile, 284 generateImageFromVideoFile,
518 TranscodeOptions, 285 TranscodeOptions,
519 TranscodeOptionsType, 286 TranscodeOptionsType,
520 transcode, 287 transcode,
521 getVideoFileFPS, 288 hlsPlaylistToFragmentedMP4
522 computeResolutionsToTranscode,
523 audio,
524 hlsPlaylistToFragmentedMP4,
525 getVideoFileBitrate,
526 canDoQuickTranscode
527} 289}
528 290
529// --------------------------------------------------------------------------- 291// ---------------------------------------------------------------------------
@@ -595,7 +357,7 @@ async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: M
595 return command 357 return command
596} 358}
597 359
598function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) { 360function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, _options: OnlyAudioTranscodeOptions) {
599 command = presetOnlyAudio(command) 361 command = presetOnlyAudio(command)
600 362
601 return command 363 return command
@@ -684,7 +446,7 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolut
684 446
685 addDefaultX264Params(localCommand) 447 addDefaultX264Params(localCommand)
686 448
687 const parsedAudio = await audio.get(input) 449 const parsedAudio = await getAudioStream(input)
688 450
689 if (!parsedAudio.audioStream) { 451 if (!parsedAudio.audioStream) {
690 localCommand = localCommand.noAudio() 452 localCommand = localCommand.noAudio()
@@ -699,22 +461,16 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, input: string, resolut
699 461
700 const audioCodecName = parsedAudio.audioStream['codec_name'] 462 const audioCodecName = parsedAudio.audioStream['codec_name']
701 463
702 if (audio.bitrate[audioCodecName]) { 464 const bitrate = getMaxAudioBitrate(audioCodecName, parsedAudio.bitrate)
703 const bitrate = audio.bitrate[audioCodecName](parsedAudio.audioStream['bit_rate']) 465
704 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate) 466 if (bitrate !== undefined && bitrate !== -1) localCommand = localCommand.audioBitrate(bitrate)
705 }
706 } 467 }
707 468
708 if (fps) { 469 if (fps) {
709 // Constrained Encoding (VBV) 470 // Constrained Encoding (VBV)
710 // https://slhck.info/video/2017/03/01/rate-control.html 471 // https://slhck.info/video/2017/03/01/rate-control.html
711 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate 472 // https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate
712 let targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS) 473 const targetBitrate = getTargetBitrate(resolution, fps, VIDEO_TRANSCODING_FPS)
713
714 // Don't transcode to an higher bitrate than the original file
715 const fileBitrate = await getVideoFileBitrate(input)
716 targetBitrate = Math.min(targetBitrate, fileBitrate)
717
718 localCommand = localCommand.outputOptions([ `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ]) 474 localCommand = localCommand.outputOptions([ `-maxrate ${targetBitrate}`, `-bufsize ${targetBitrate * 2}` ])
719 475
720 // Keyframe interval of 2 seconds for faster seeking and resolution switching. 476 // Keyframe interval of 2 seconds for faster seeking and resolution switching.
diff --git a/server/helpers/ffprobe-utils.ts b/server/helpers/ffprobe-utils.ts
new file mode 100644
index 000000000..6159d3963
--- /dev/null
+++ b/server/helpers/ffprobe-utils.ts
@@ -0,0 +1,249 @@
1import * as ffmpeg from 'fluent-ffmpeg'
2import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
3import { getMaxBitrate, VideoResolution } from '../../shared/models/videos'
4import { CONFIG } from '../initializers/config'
5import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
6import { logger } from './logger'
7
8function ffprobePromise (path: string) {
9 return new Promise<ffmpeg.FfprobeData>((res, rej) => {
10 ffmpeg.ffprobe(path, (err, data) => {
11 if (err) return rej(err)
12
13 return res(data)
14 })
15 })
16}
17
18async function getAudioStream (videoPath: string, existingProbe?: ffmpeg.FfprobeData) {
19 // without position, ffprobe considers the last input only
20 // we make it consider the first input only
21 // if you pass a file path to pos, then ffprobe acts on that file directly
22 const data = existingProbe || await ffprobePromise(videoPath)
23
24 if (Array.isArray(data.streams)) {
25 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
26
27 if (audioStream) {
28 return {
29 absolutePath: data.format.filename,
30 audioStream,
31 bitrate: parseInt(audioStream['bit_rate'] + '', 10)
32 }
33 }
34 }
35
36 return { absolutePath: data.format.filename }
37}
38
39function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) {
40 const baseKbitrate = 384
41 const toBits = (kbits: number) => kbits * 8000
42
43 if (type === 'aac') {
44 switch (true) {
45 case bitrate > toBits(baseKbitrate):
46 return baseKbitrate
47
48 default:
49 return -1 // we interpret it as a signal to copy the audio stream as is
50 }
51 }
52
53 if (type === 'mp3') {
54 /*
55 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
56 That's why, when using aac, we can go to lower kbit/sec. The equivalences
57 made here are not made to be accurate, especially with good mp3 encoders.
58 */
59 switch (true) {
60 case bitrate <= toBits(192):
61 return 128
62
63 case bitrate <= toBits(384):
64 return 256
65
66 default:
67 return baseKbitrate
68 }
69 }
70
71 return undefined
72}
73
74async function getVideoStreamSize (path: string, existingProbe?: ffmpeg.FfprobeData) {
75 const videoStream = await getVideoStreamFromFile(path, existingProbe)
76
77 return videoStream === null
78 ? { width: 0, height: 0 }
79 : { width: videoStream.width, height: videoStream.height }
80}
81
82async function getVideoStreamCodec (path: string) {
83 const videoStream = await getVideoStreamFromFile(path)
84
85 if (!videoStream) return ''
86
87 const videoCodec = videoStream.codec_tag_string
88
89 const baseProfileMatrix = {
90 High: '6400',
91 Main: '4D40',
92 Baseline: '42E0'
93 }
94
95 let baseProfile = baseProfileMatrix[videoStream.profile]
96 if (!baseProfile) {
97 logger.warn('Cannot get video profile codec of %s.', path, { videoStream })
98 baseProfile = baseProfileMatrix['High'] // Fallback
99 }
100
101 let level = videoStream.level.toString(16)
102 if (level.length === 1) level = `0${level}`
103
104 return `${videoCodec}.${baseProfile}${level}`
105}
106
107async function getAudioStreamCodec (path: string, existingProbe?: ffmpeg.FfprobeData) {
108 const { audioStream } = await getAudioStream(path, existingProbe)
109
110 if (!audioStream) return ''
111
112 const audioCodec = audioStream.codec_name
113 if (audioCodec === 'aac') return 'mp4a.40.2'
114
115 logger.warn('Cannot get audio codec of %s.', path, { audioStream })
116
117 return 'mp4a.40.2' // Fallback
118}
119
120async function getVideoFileResolution (path: string, existingProbe?: ffmpeg.FfprobeData) {
121 const size = await getVideoStreamSize(path, existingProbe)
122
123 return {
124 videoFileResolution: Math.min(size.height, size.width),
125 isPortraitMode: size.height > size.width
126 }
127}
128
129async function getVideoFileFPS (path: string, existingProbe?: ffmpeg.FfprobeData) {
130 const videoStream = await getVideoStreamFromFile(path, existingProbe)
131 if (videoStream === null) return 0
132
133 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
134 const valuesText: string = videoStream[key]
135 if (!valuesText) continue
136
137 const [ frames, seconds ] = valuesText.split('/')
138 if (!frames || !seconds) continue
139
140 const result = parseInt(frames, 10) / parseInt(seconds, 10)
141 if (result > 0) return Math.round(result)
142 }
143
144 return 0
145}
146
147async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
148 const metadata = existingProbe || await ffprobePromise(path)
149
150 return new VideoFileMetadata(metadata)
151}
152
153async function getVideoFileBitrate (path: string, existingProbe?: ffmpeg.FfprobeData) {
154 const metadata = await getMetadataFromFile(path, existingProbe)
155
156 return metadata.format.bit_rate as number
157}
158
159async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
160 const metadata = await getMetadataFromFile(path, existingProbe)
161
162 return Math.floor(metadata.format.duration)
163}
164
165async function getVideoStreamFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) {
166 const metadata = await getMetadataFromFile(path, existingProbe)
167
168 return metadata.streams.find(s => s.codec_type === 'video') || null
169}
170
171function computeResolutionsToTranscode (videoFileResolution: number, type: 'vod' | 'live') {
172 const configResolutions = type === 'vod'
173 ? CONFIG.TRANSCODING.RESOLUTIONS
174 : CONFIG.LIVE.TRANSCODING.RESOLUTIONS
175
176 const resolutionsEnabled: number[] = []
177
178 // Put in the order we want to proceed jobs
179 const resolutions = [
180 VideoResolution.H_NOVIDEO,
181 VideoResolution.H_480P,
182 VideoResolution.H_360P,
183 VideoResolution.H_720P,
184 VideoResolution.H_240P,
185 VideoResolution.H_1080P,
186 VideoResolution.H_4K
187 ]
188
189 for (const resolution of resolutions) {
190 if (configResolutions[resolution + 'p'] === true && videoFileResolution > resolution) {
191 resolutionsEnabled.push(resolution)
192 }
193 }
194
195 return resolutionsEnabled
196}
197
198async function canDoQuickTranscode (path: string): Promise<boolean> {
199 const probe = await ffprobePromise(path)
200
201 // NOTE: This could be optimized by running ffprobe only once (but it runs fast anyway)
202 const videoStream = await getVideoStreamFromFile(path, probe)
203 const parsedAudio = await getAudioStream(path, probe)
204 const fps = await getVideoFileFPS(path, probe)
205 const bitRate = await getVideoFileBitrate(path, probe)
206 const resolution = await getVideoFileResolution(path, probe)
207
208 // check video params
209 if (videoStream == null) return false
210 if (videoStream['codec_name'] !== 'h264') return false
211 if (videoStream['pix_fmt'] !== 'yuv420p') return false
212 if (fps < VIDEO_TRANSCODING_FPS.MIN || fps > VIDEO_TRANSCODING_FPS.MAX) return false
213 if (bitRate > getMaxBitrate(resolution.videoFileResolution, fps, VIDEO_TRANSCODING_FPS)) return false
214
215 // check audio params (if audio stream exists)
216 if (parsedAudio.audioStream) {
217 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
218
219 const audioBitrate = parsedAudio.bitrate
220
221 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
222 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
223 }
224
225 return true
226}
227
228function getClosestFramerateStandard (fps: number, type: 'HD_STANDARD' | 'STANDARD'): number {
229 return VIDEO_TRANSCODING_FPS[type].slice(0)
230 .sort((a, b) => fps % a - fps % b)[0]
231}
232
233// ---------------------------------------------------------------------------
234
235export {
236 getVideoStreamCodec,
237 getAudioStreamCodec,
238 getVideoStreamSize,
239 getVideoFileResolution,
240 getMetadataFromFile,
241 getMaxAudioBitrate,
242 getDurationFromVideoFile,
243 getAudioStream,
244 getVideoFileFPS,
245 getClosestFramerateStandard,
246 computeResolutionsToTranscode,
247 getVideoFileBitrate,
248 canDoQuickTranscode
249}
diff --git a/server/initializers/migrations/0075-video-resolutions.ts b/server/initializers/migrations/0075-video-resolutions.ts
index f56c1b2c3..496125adb 100644
--- a/server/initializers/migrations/0075-video-resolutions.ts
+++ b/server/initializers/migrations/0075-video-resolutions.ts
@@ -1,7 +1,7 @@
1import * as Sequelize from 'sequelize' 1import * as Sequelize from 'sequelize'
2import { join } from 'path' 2import { join } from 'path'
3import { CONFIG } from '../../initializers/config' 3import { CONFIG } from '../../initializers/config'
4import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' 4import { getVideoFileResolution } from '../../helpers/ffprobe-utils'
5import { readdir, rename } from 'fs-extra' 5import { readdir, rename } from 'fs-extra'
6 6
7function up (utils: { 7function up (utils: {
diff --git a/server/lib/hls.ts b/server/lib/hls.ts
index 7aa152638..9ea83f337 100644
--- a/server/lib/hls.ts
+++ b/server/lib/hls.ts
@@ -1,17 +1,17 @@
1import { basename, dirname, join } from 'path'
2import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../initializers/constants'
3import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' 1import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
4import { getVideoStreamSize, getAudioStreamCodec, getVideoStreamCodec } from '../helpers/ffmpeg-utils' 2import { flatten, uniq } from 'lodash'
3import { basename, dirname, join } from 'path'
4import { MVideoWithFile } from '@server/types/models'
5import { sha256 } from '../helpers/core-utils' 5import { sha256 } from '../helpers/core-utils'
6import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' 6import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils'
7import { logger } from '../helpers/logger' 7import { logger } from '../helpers/logger'
8import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' 8import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
9import { generateRandomString } from '../helpers/utils' 9import { generateRandomString } from '../helpers/utils'
10import { flatten, uniq } from 'lodash'
11import { VideoFileModel } from '../models/video/video-file'
12import { CONFIG } from '../initializers/config' 10import { CONFIG } from '../initializers/config'
11import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../initializers/constants'
13import { sequelizeTypescript } from '../initializers/database' 12import { sequelizeTypescript } from '../initializers/database'
14import { MVideoWithFile } from '@server/types/models' 13import { VideoFileModel } from '../models/video/video-file'
14import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
15import { getVideoFilename, getVideoFilePath } from './video-paths' 15import { getVideoFilename, getVideoFilePath } from './video-paths'
16 16
17async function updateStreamingPlaylistsInfohashesIfNeeded () { 17async function updateStreamingPlaylistsInfohashesIfNeeded () {
diff --git a/server/lib/job-queue/handlers/video-file-import.ts b/server/lib/job-queue/handlers/video-file-import.ts
index f9bc3137c..18823ee9c 100644
--- a/server/lib/job-queue/handlers/video-file-import.ts
+++ b/server/lib/job-queue/handlers/video-file-import.ts
@@ -1,15 +1,15 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { logger } from '../../../helpers/logger'
3import { VideoModel } from '../../../models/video/video'
4import { publishNewResolutionIfNeeded } from './video-transcoding'
5import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils'
6import { copy, stat } from 'fs-extra' 2import { copy, stat } from 'fs-extra'
7import { VideoFileModel } from '../../../models/video/video-file'
8import { extname } from 'path' 3import { extname } from 'path'
9import { MVideoFile, MVideoWithFile } from '@server/types/models'
10import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' 4import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
11import { getVideoFilePath } from '@server/lib/video-paths' 5import { getVideoFilePath } from '@server/lib/video-paths'
6import { MVideoFile, MVideoWithFile } from '@server/types/models'
12import { VideoFileImportPayload } from '@shared/models' 7import { VideoFileImportPayload } from '@shared/models'
8import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
9import { logger } from '../../../helpers/logger'
10import { VideoModel } from '../../../models/video/video'
11import { VideoFileModel } from '../../../models/video/video-file'
12import { publishNewResolutionIfNeeded } from './video-transcoding'
13 13
14async function processVideoFileImport (job: Bull.Job) { 14async function processVideoFileImport (job: Bull.Job) {
15 const payload = job.data as VideoFileImportPayload 15 const payload = job.data as VideoFileImportPayload
diff --git a/server/lib/job-queue/handlers/video-import.ts b/server/lib/job-queue/handlers/video-import.ts
index 9210aec54..5a82a8d2b 100644
--- a/server/lib/job-queue/handlers/video-import.ts
+++ b/server/lib/job-queue/handlers/video-import.ts
@@ -17,7 +17,7 @@ import {
17} from '../../../../shared' 17} from '../../../../shared'
18import { VideoImportState } from '../../../../shared/models/videos' 18import { VideoImportState } from '../../../../shared/models/videos'
19import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' 19import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
20import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' 20import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
21import { logger } from '../../../helpers/logger' 21import { logger } from '../../../helpers/logger'
22import { getSecureTorrentName } from '../../../helpers/utils' 22import { getSecureTorrentName } from '../../../helpers/utils'
23import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' 23import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
diff --git a/server/lib/job-queue/handlers/video-live-ending.ts b/server/lib/job-queue/handlers/video-live-ending.ts
index 599aabf80..447744224 100644
--- a/server/lib/job-queue/handlers/video-live-ending.ts
+++ b/server/lib/job-queue/handlers/video-live-ending.ts
@@ -1,7 +1,9 @@
1import * as Bull from 'bull' 1import * as Bull from 'bull'
2import { readdir, remove } from 'fs-extra' 2import { readdir, remove } from 'fs-extra'
3import { join } from 'path' 3import { join } from 'path'
4import { getDurationFromVideoFile, getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' 4import { hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils'
5import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
6import { generateVideoMiniature } from '@server/lib/thumbnail'
5import { publishAndFederateIfNeeded } from '@server/lib/video' 7import { publishAndFederateIfNeeded } from '@server/lib/video'
6import { getHLSDirectory } from '@server/lib/video-paths' 8import { getHLSDirectory } from '@server/lib/video-paths'
7import { generateHlsPlaylist } from '@server/lib/video-transcoding' 9import { generateHlsPlaylist } from '@server/lib/video-transcoding'
@@ -12,7 +14,6 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin
12import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' 14import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models'
13import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' 15import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models'
14import { logger } from '../../../helpers/logger' 16import { logger } from '../../../helpers/logger'
15import { generateVideoMiniature } from '@server/lib/thumbnail'
16 17
17async function processVideoLiveEnding (job: Bull.Job) { 18async function processVideoLiveEnding (job: Bull.Job) {
18 const payload = job.data as VideoLiveEndingPayload 19 const payload = job.data as VideoLiveEndingPayload
diff --git a/server/lib/job-queue/handlers/video-transcoding.ts b/server/lib/job-queue/handlers/video-transcoding.ts
index 843a9f1b5..b6b8d9071 100644
--- a/server/lib/job-queue/handlers/video-transcoding.ts
+++ b/server/lib/job-queue/handlers/video-transcoding.ts
@@ -9,7 +9,7 @@ import {
9 VideoTranscodingPayload 9 VideoTranscodingPayload
10} from '../../../../shared' 10} from '../../../../shared'
11import { retryTransactionWrapper } from '../../../helpers/database-utils' 11import { retryTransactionWrapper } from '../../../helpers/database-utils'
12import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' 12import { computeResolutionsToTranscode } from '../../../helpers/ffprobe-utils'
13import { logger } from '../../../helpers/logger' 13import { logger } from '../../../helpers/logger'
14import { CONFIG } from '../../../initializers/config' 14import { CONFIG } from '../../../initializers/config'
15import { sequelizeTypescript } from '../../../initializers/database' 15import { sequelizeTypescript } from '../../../initializers/database'
diff --git a/server/lib/live-manager.ts b/server/lib/live-manager.ts
index feb6c5275..4d2e9b1b3 100644
--- a/server/lib/live-manager.ts
+++ b/server/lib/live-manager.ts
@@ -4,13 +4,8 @@ import { FfmpegCommand } from 'fluent-ffmpeg'
4import { ensureDir, stat } from 'fs-extra' 4import { ensureDir, stat } from 'fs-extra'
5import { basename } from 'path' 5import { basename } from 'path'
6import { isTestInstance } from '@server/helpers/core-utils' 6import { isTestInstance } from '@server/helpers/core-utils'
7import { 7import { runLiveMuxing, runLiveTranscoding } from '@server/helpers/ffmpeg-utils'
8 computeResolutionsToTranscode, 8import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils'
9 getVideoFileFPS,
10 getVideoFileResolution,
11 runLiveMuxing,
12 runLiveTranscoding
13} from '@server/helpers/ffmpeg-utils'
14import { logger } from '@server/helpers/logger' 9import { logger } from '@server/helpers/logger'
15import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' 10import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
16import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants' 11import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants'
diff --git a/server/lib/video-transcoding.ts b/server/lib/video-transcoding.ts
index 9882a14db..ca969b235 100644
--- a/server/lib/video-transcoding.ts
+++ b/server/lib/video-transcoding.ts
@@ -4,15 +4,8 @@ import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
4import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' 4import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models'
5import { VideoResolution } from '../../shared/models/videos' 5import { VideoResolution } from '../../shared/models/videos'
6import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' 6import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
7import { 7import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
8 canDoQuickTranscode, 8import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../helpers/ffprobe-utils'
9 getDurationFromVideoFile,
10 getMetadataFromFile,
11 getVideoFileFPS,
12 transcode,
13 TranscodeOptions,
14 TranscodeOptionsType
15} from '../helpers/ffmpeg-utils'
16import { logger } from '../helpers/logger' 9import { logger } from '../helpers/logger'
17import { CONFIG } from '../initializers/config' 10import { CONFIG } from '../initializers/config'
18import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' 11import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index efab67a01..af0072d73 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -34,7 +34,7 @@ import {
34 isVideoTagsValid 34 isVideoTagsValid
35} from '../../../helpers/custom-validators/videos' 35} from '../../../helpers/custom-validators/videos'
36import { cleanUpReqFiles } from '../../../helpers/express-utils' 36import { cleanUpReqFiles } from '../../../helpers/express-utils'
37import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' 37import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils'
38import { logger } from '../../../helpers/logger' 38import { logger } from '../../../helpers/logger'
39import { 39import {
40 checkUserCanManageVideo, 40 checkUserCanManageVideo,
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index f365d3d51..d33ae8a5a 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -51,7 +51,7 @@ import {
51 isVideoStateValid, 51 isVideoStateValid,
52 isVideoSupportValid 52 isVideoSupportValid
53} from '../../helpers/custom-validators/videos' 53} from '../../helpers/custom-validators/videos'
54import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' 54import { getVideoFileResolution } from '../../helpers/ffprobe-utils'
55import { logger } from '../../helpers/logger' 55import { logger } from '../../helpers/logger'
56import { CONFIG } from '../../initializers/config' 56import { CONFIG } from '../../initializers/config'
57import { 57import {
diff --git a/server/tests/api/videos/audio-only.ts b/server/tests/api/videos/audio-only.ts
index ac7a0b89c..053b29ca1 100644
--- a/server/tests/api/videos/audio-only.ts
+++ b/server/tests/api/videos/audio-only.ts
@@ -1,7 +1,9 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
4import * as chai from 'chai'
5import { join } from 'path'
6import { getAudioStream, getVideoStreamSize } from '@server/helpers/ffprobe-utils'
5import { 7import {
6 cleanupTests, 8 cleanupTests,
7 doubleFollow, 9 doubleFollow,
@@ -14,8 +16,6 @@ import {
14 waitJobs 16 waitJobs
15} from '../../../../shared/extra-utils' 17} from '../../../../shared/extra-utils'
16import { VideoDetails } from '../../../../shared/models/videos' 18import { VideoDetails } from '../../../../shared/models/videos'
17import { join } from 'path'
18import { audio, getVideoStreamSize } from '@server/helpers/ffmpeg-utils'
19 19
20const expect = chai.expect 20const expect = chai.expect
21 21
@@ -85,7 +85,7 @@ describe('Test audio only video transcoding', function () {
85 ] 85 ]
86 86
87 for (const path of paths) { 87 for (const path of paths) {
88 const { audioStream } = await audio.get(path) 88 const { audioStream } = await getAudioStream(path)
89 expect(audioStream['codec_name']).to.be.equal('aac') 89 expect(audioStream['codec_name']).to.be.equal('aac')
90 expect(audioStream['bit_rate']).to.be.at.most(384 * 8000) 90 expect(audioStream['bit_rate']).to.be.at.most(384 * 8000)
91 91
diff --git a/server/tests/api/videos/video-transcoder.ts b/server/tests/api/videos/video-transcoder.ts
index ae21c3716..3e336e786 100644
--- a/server/tests/api/videos/video-transcoder.ts
+++ b/server/tests/api/videos/video-transcoder.ts
@@ -1,17 +1,12 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ 1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2 2
3import * as chai from 'chai'
4import 'mocha' 3import 'mocha'
4import * as chai from 'chai'
5import { FfprobeData } from 'fluent-ffmpeg'
5import { omit } from 'lodash' 6import { omit } from 'lodash'
6import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' 7import { join } from 'path'
7import { 8
8 audio, 9import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants'
9 canDoQuickTranscode,
10 getVideoFileBitrate,
11 getVideoFileFPS,
12 getVideoFileResolution,
13 getMetadataFromFile
14} from '../../../helpers/ffmpeg-utils'
15import { 10import {
16 buildAbsoluteFixturePath, 11 buildAbsoluteFixturePath,
17 cleanupTests, 12 cleanupTests,
@@ -29,14 +24,20 @@ import {
29 ServerInfo, 24 ServerInfo,
30 setAccessTokensToServers, 25 setAccessTokensToServers,
31 updateCustomSubConfig, 26 updateCustomSubConfig,
32 uploadVideo, uploadVideoAndGetId, 27 uploadVideo,
28 uploadVideoAndGetId,
33 waitJobs, 29 waitJobs,
34 webtorrentAdd 30 webtorrentAdd
35} from '../../../../shared/extra-utils' 31} from '../../../../shared/extra-utils'
36import { join } from 'path' 32import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos'
37import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' 33import {
38import { FfprobeData } from 'fluent-ffmpeg' 34 canDoQuickTranscode,
39import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata' 35 getAudioStream,
36 getMetadataFromFile,
37 getVideoFileBitrate,
38 getVideoFileFPS,
39 getVideoFileResolution
40} from '../../../helpers/ffprobe-utils'
40 41
41const expect = chai.expect 42const expect = chai.expect
42 43
@@ -136,7 +137,7 @@ describe('Test video transcoding', function () {
136 expect(videoDetails.files).to.have.lengthOf(4) 137 expect(videoDetails.files).to.have.lengthOf(4)
137 138
138 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4') 139 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
139 const probe = await audio.get(path) 140 const probe = await getAudioStream(path)
140 141
141 if (probe.audioStream) { 142 if (probe.audioStream) {
142 expect(probe.audioStream['codec_name']).to.be.equal('aac') 143 expect(probe.audioStream['codec_name']).to.be.equal('aac')
@@ -167,7 +168,7 @@ describe('Test video transcoding', function () {
167 168
168 expect(videoDetails.files).to.have.lengthOf(4) 169 expect(videoDetails.files).to.have.lengthOf(4)
169 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4') 170 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
170 const probe = await audio.get(path) 171 const probe = await getAudioStream(path)
171 expect(probe).to.not.have.property('audioStream') 172 expect(probe).to.not.have.property('audioStream')
172 } 173 }
173 }) 174 })
@@ -192,9 +193,9 @@ describe('Test video transcoding', function () {
192 193
193 expect(videoDetails.files).to.have.lengthOf(4) 194 expect(videoDetails.files).to.have.lengthOf(4)
194 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture) 195 const fixturePath = buildAbsoluteFixturePath(videoAttributes.fixture)
195 const fixtureVideoProbe = await audio.get(fixturePath) 196 const fixtureVideoProbe = await getAudioStream(fixturePath)
196 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4') 197 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', video.uuid + '-240.mp4')
197 const videoProbe = await audio.get(path) 198 const videoProbe = await getAudioStream(path)
198 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) { 199 if (videoProbe.audioStream && fixtureVideoProbe.audioStream) {
199 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ] 200 const toOmit = [ 'max_bit_rate', 'duration', 'duration_ts', 'nb_frames', 'start_time', 'start_pts' ]
200 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit)) 201 expect(omit(videoProbe.audioStream, toOmit)).to.be.deep.equal(omit(fixtureVideoProbe.audioStream, toOmit))
@@ -513,7 +514,7 @@ describe('Test video transcoding', function () {
513 514
514 { 515 {
515 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', videoUUID + '-240.mp4') 516 const path = join(root(), 'test' + servers[1].internalServerNumber, 'videos', videoUUID + '-240.mp4')
516 const metadata = await getMetadataFromFile<VideoFileMetadata>(path) 517 const metadata = await getMetadataFromFile(path)
517 518
518 // expected format properties 519 // expected format properties
519 for (const p of [ 520 for (const p of [
diff --git a/server/tests/cli/optimize-old-videos.ts b/server/tests/cli/optimize-old-videos.ts
index 43f9b7f55..420fb8049 100644
--- a/server/tests/cli/optimize-old-videos.ts
+++ b/server/tests/cli/optimize-old-videos.ts
@@ -2,7 +2,7 @@
2 2
3import 'mocha' 3import 'mocha'
4import * as chai from 'chai' 4import * as chai from 'chai'
5import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos' 5import { join } from 'path'
6import { 6import {
7 cleanupTests, 7 cleanupTests,
8 doubleFollow, 8 doubleFollow,
@@ -20,9 +20,9 @@ import {
20 wait 20 wait
21} from '../../../shared/extra-utils' 21} from '../../../shared/extra-utils'
22import { waitJobs } from '../../../shared/extra-utils/server/jobs' 22import { waitJobs } from '../../../shared/extra-utils/server/jobs'
23import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils' 23import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos'
24import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffprobe-utils'
24import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' 25import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
25import { join } from 'path'
26 26
27const expect = chai.expect 27const expect = chai.expect
28 28