diff options
author | Chocobozzz <me@florianbigard.com> | 2020-11-20 17:16:55 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2020-11-25 10:07:51 +0100 |
commit | daf6e4801052d3ca6be2fafd20bae2323b1ce175 (patch) | |
tree | a136af611c2543c461ce3fd126ddb7cb1e37a0c2 /server | |
parent | 123f61933611f326ea5a5e8c2ea253ee8720e4f0 (diff) | |
download | PeerTube-daf6e4801052d3ca6be2fafd20bae2323b1ce175.tar.gz PeerTube-daf6e4801052d3ca6be2fafd20bae2323b1ce175.tar.zst PeerTube-daf6e4801052d3ca6be2fafd20bae2323b1ce175.zip |
Split ffmpeg utils with ffprobe utils
Diffstat (limited to 'server')
-rw-r--r-- | server/controllers/api/videos/index.ts | 4 | ||||
-rw-r--r-- | server/helpers/ffmpeg-utils.ts | 310 | ||||
-rw-r--r-- | server/helpers/ffprobe-utils.ts | 249 | ||||
-rw-r--r-- | server/initializers/migrations/0075-video-resolutions.ts | 2 | ||||
-rw-r--r-- | server/lib/hls.ts | 14 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-file-import.ts | 12 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-import.ts | 2 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-live-ending.ts | 5 | ||||
-rw-r--r-- | server/lib/job-queue/handlers/video-transcoding.ts | 2 | ||||
-rw-r--r-- | server/lib/live-manager.ts | 9 | ||||
-rw-r--r-- | server/lib/video-transcoding.ts | 11 | ||||
-rw-r--r-- | server/middlewares/validators/videos/videos.ts | 2 | ||||
-rw-r--r-- | server/models/video/video.ts | 2 | ||||
-rw-r--r-- | server/tests/api/videos/audio-only.ts | 8 | ||||
-rw-r--r-- | server/tests/api/videos/video-transcoder.ts | 41 | ||||
-rw-r--r-- | server/tests/cli/optimize-old-videos.ts | 6 |
16 files changed, 337 insertions, 342 deletions
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' | |||
16 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' | 16 | import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger' |
17 | import { resetSequelizeInstance } from '../../../helpers/database-utils' | 17 | import { resetSequelizeInstance } from '../../../helpers/database-utils' |
18 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' | 18 | import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils' |
19 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 19 | import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' |
20 | import { logger } from '../../../helpers/logger' | 20 | import { logger } from '../../../helpers/logger' |
21 | import { getFormattedObjects } from '../../../helpers/utils' | 21 | import { getFormattedObjects } from '../../../helpers/utils' |
22 | import { CONFIG } from '../../../initializers/config' | 22 | import { 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 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | 1 | import * as ffmpeg from 'fluent-ffmpeg' |
2 | import { readFile, remove, writeFile } from 'fs-extra' | 2 | import { readFile, remove, writeFile } from 'fs-extra' |
3 | import { dirname, join } from 'path' | 3 | import { dirname, join } from 'path' |
4 | import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata' | 4 | import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' |
5 | import { getMaxBitrate, getTargetBitrate, VideoResolution } from '../../shared/models/videos' | ||
6 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' | 5 | import { checkFFmpegEncoders } from '../initializers/checker-before-init' |
7 | import { CONFIG } from '../initializers/config' | 6 | import { CONFIG } from '../initializers/config' |
8 | import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' | 7 | import { FFMPEG_NICE, VIDEO_LIVE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' |
8 | import { getAudioStream, getClosestFramerateStandard, getMaxAudioBitrate, getVideoFileFPS } from './ffprobe-utils' | ||
9 | import { processImage } from './image-utils' | 9 | import { processImage } from './image-utils' |
10 | import { logger } from './logger' | 10 | import { logger } from './logger' |
11 | 11 | ||
12 | /** | ||
13 | * A toolbox to play with audio | ||
14 | */ | ||
15 | namespace 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 | |||
77 | function 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 | |||
104 | async 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 | |||
112 | async 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 | |||
137 | async 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 | |||
150 | async 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 | |||
159 | async 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 | |||
177 | async 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 | |||
187 | async function getVideoFileBitrate (path: string) { | ||
188 | return getMetadataFromFile<number>(path, metadata => metadata.format.bit_rate) | ||
189 | } | ||
190 | |||
191 | function getDurationFromVideoFile (path: string) { | ||
192 | return getMetadataFromFile<number>(path, metadata => Math.floor(metadata.format.duration)) | ||
193 | } | ||
194 | |||
195 | function getVideoStreamFromFile (path: string) { | ||
196 | return getMetadataFromFile<any>(path, metadata => metadata.streams.find(s => s.codec_type === 'video') || null) | ||
197 | } | ||
198 | |||
199 | async function generateImageFromVideoFile (fromPath: string, folder: string, imageName: string, size: { width: number, height: number }) { | 12 | async 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 | |||
231 | type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio' | 48 | type TranscodeOptionsType = 'hls' | 'quick-transcode' | 'video' | 'merge-audio' | 'only-audio' |
232 | 49 | ||
233 | interface BaseTranscodeOptions { | 50 | interface BaseTranscodeOptions { |
@@ -270,72 +87,27 @@ type TranscodeOptions = | |||
270 | | OnlyAudioTranscodeOptions | 87 | | OnlyAudioTranscodeOptions |
271 | | QuickTranscodeOptions | 88 | | QuickTranscodeOptions |
272 | 89 | ||
273 | function transcode (options: TranscodeOptions) { | 90 | const 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 | |||
100 | async 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 | ||
310 | async 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 | ||
336 | function 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 | ||
341 | function convertWebPToJPG (path: string, destination: string): Promise<void> { | 113 | function convertWebPToJPG (path: string, destination: string): Promise<void> { |
@@ -484,12 +256,11 @@ async function hlsPlaylistToFragmentedMP4 (hlsDirectory: string, segmentFiles: s | |||
484 | } | 256 | } |
485 | 257 | ||
486 | async function runCommand (command: ffmpeg.FfmpegCommand, onEnd?: Function) { | 258 | async 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 | ||
506 | export { | 279 | export { |
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 | ||
598 | function buildOnlyAudioCommand (command: ffmpeg.FfmpegCommand, options: OnlyAudioTranscodeOptions) { | 360 | function 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 @@ | |||
1 | import * as ffmpeg from 'fluent-ffmpeg' | ||
2 | import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata' | ||
3 | import { getMaxBitrate, VideoResolution } from '../../shared/models/videos' | ||
4 | import { CONFIG } from '../initializers/config' | ||
5 | import { VIDEO_TRANSCODING_FPS } from '../initializers/constants' | ||
6 | import { logger } from './logger' | ||
7 | |||
8 | function 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 | |||
18 | async 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 | |||
39 | function 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 | |||
74 | async 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 | |||
82 | async 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 | |||
107 | async 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 | |||
120 | async 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 | |||
129 | async 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 | |||
147 | async function getMetadataFromFile (path: string, existingProbe?: ffmpeg.FfprobeData) { | ||
148 | const metadata = existingProbe || await ffprobePromise(path) | ||
149 | |||
150 | return new VideoFileMetadata(metadata) | ||
151 | } | ||
152 | |||
153 | async 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 | |||
159 | async function getDurationFromVideoFile (path: string, existingProbe?: ffmpeg.FfprobeData) { | ||
160 | const metadata = await getMetadataFromFile(path, existingProbe) | ||
161 | |||
162 | return Math.floor(metadata.format.duration) | ||
163 | } | ||
164 | |||
165 | async 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 | |||
171 | function 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 | |||
198 | async 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 | |||
228 | function 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 | |||
235 | export { | ||
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 @@ | |||
1 | import * as Sequelize from 'sequelize' | 1 | import * as Sequelize from 'sequelize' |
2 | import { join } from 'path' | 2 | import { join } from 'path' |
3 | import { CONFIG } from '../../initializers/config' | 3 | import { CONFIG } from '../../initializers/config' |
4 | import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' | 4 | import { getVideoFileResolution } from '../../helpers/ffprobe-utils' |
5 | import { readdir, rename } from 'fs-extra' | 5 | import { readdir, rename } from 'fs-extra' |
6 | 6 | ||
7 | function up (utils: { | 7 | function 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 @@ | |||
1 | import { basename, dirname, join } from 'path' | ||
2 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../initializers/constants' | ||
3 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' | 1 | import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra' |
4 | import { getVideoStreamSize, getAudioStreamCodec, getVideoStreamCodec } from '../helpers/ffmpeg-utils' | 2 | import { flatten, uniq } from 'lodash' |
3 | import { basename, dirname, join } from 'path' | ||
4 | import { MVideoWithFile } from '@server/types/models' | ||
5 | import { sha256 } from '../helpers/core-utils' | 5 | import { sha256 } from '../helpers/core-utils' |
6 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | 6 | import { getAudioStreamCodec, getVideoStreamCodec, getVideoStreamSize } from '../helpers/ffprobe-utils' |
7 | import { logger } from '../helpers/logger' | 7 | import { logger } from '../helpers/logger' |
8 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' | 8 | import { doRequest, doRequestAndSaveToFile } from '../helpers/requests' |
9 | import { generateRandomString } from '../helpers/utils' | 9 | import { generateRandomString } from '../helpers/utils' |
10 | import { flatten, uniq } from 'lodash' | ||
11 | import { VideoFileModel } from '../models/video/video-file' | ||
12 | import { CONFIG } from '../initializers/config' | 10 | import { CONFIG } from '../initializers/config' |
11 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION } from '../initializers/constants' | ||
13 | import { sequelizeTypescript } from '../initializers/database' | 12 | import { sequelizeTypescript } from '../initializers/database' |
14 | import { MVideoWithFile } from '@server/types/models' | 13 | import { VideoFileModel } from '../models/video/video-file' |
14 | import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist' | ||
15 | import { getVideoFilename, getVideoFilePath } from './video-paths' | 15 | import { getVideoFilename, getVideoFilePath } from './video-paths' |
16 | 16 | ||
17 | async function updateStreamingPlaylistsInfohashesIfNeeded () { | 17 | async 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 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { logger } from '../../../helpers/logger' | ||
3 | import { VideoModel } from '../../../models/video/video' | ||
4 | import { publishNewResolutionIfNeeded } from './video-transcoding' | ||
5 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | ||
6 | import { copy, stat } from 'fs-extra' | 2 | import { copy, stat } from 'fs-extra' |
7 | import { VideoFileModel } from '../../../models/video/video-file' | ||
8 | import { extname } from 'path' | 3 | import { extname } from 'path' |
9 | import { MVideoFile, MVideoWithFile } from '@server/types/models' | ||
10 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' | 4 | import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent' |
11 | import { getVideoFilePath } from '@server/lib/video-paths' | 5 | import { getVideoFilePath } from '@server/lib/video-paths' |
6 | import { MVideoFile, MVideoWithFile } from '@server/types/models' | ||
12 | import { VideoFileImportPayload } from '@shared/models' | 7 | import { VideoFileImportPayload } from '@shared/models' |
8 | import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' | ||
9 | import { logger } from '../../../helpers/logger' | ||
10 | import { VideoModel } from '../../../models/video/video' | ||
11 | import { VideoFileModel } from '../../../models/video/video-file' | ||
12 | import { publishNewResolutionIfNeeded } from './video-transcoding' | ||
13 | 13 | ||
14 | async function processVideoFileImport (job: Bull.Job) { | 14 | async 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' |
18 | import { VideoImportState } from '../../../../shared/models/videos' | 18 | import { VideoImportState } from '../../../../shared/models/videos' |
19 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' | 19 | import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type' |
20 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffmpeg-utils' | 20 | import { getDurationFromVideoFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils' |
21 | import { logger } from '../../../helpers/logger' | 21 | import { logger } from '../../../helpers/logger' |
22 | import { getSecureTorrentName } from '../../../helpers/utils' | 22 | import { getSecureTorrentName } from '../../../helpers/utils' |
23 | import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent' | 23 | import { 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 @@ | |||
1 | import * as Bull from 'bull' | 1 | import * as Bull from 'bull' |
2 | import { readdir, remove } from 'fs-extra' | 2 | import { readdir, remove } from 'fs-extra' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { getDurationFromVideoFile, getVideoFileResolution, hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' | 4 | import { hlsPlaylistToFragmentedMP4 } from '@server/helpers/ffmpeg-utils' |
5 | import { getDurationFromVideoFile, getVideoFileResolution } from '@server/helpers/ffprobe-utils' | ||
6 | import { generateVideoMiniature } from '@server/lib/thumbnail' | ||
5 | import { publishAndFederateIfNeeded } from '@server/lib/video' | 7 | import { publishAndFederateIfNeeded } from '@server/lib/video' |
6 | import { getHLSDirectory } from '@server/lib/video-paths' | 8 | import { getHLSDirectory } from '@server/lib/video-paths' |
7 | import { generateHlsPlaylist } from '@server/lib/video-transcoding' | 9 | import { generateHlsPlaylist } from '@server/lib/video-transcoding' |
@@ -12,7 +14,6 @@ import { VideoStreamingPlaylistModel } from '@server/models/video/video-streamin | |||
12 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' | 14 | import { MStreamingPlaylist, MVideo, MVideoLive } from '@server/types/models' |
13 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' | 15 | import { ThumbnailType, VideoLiveEndingPayload, VideoState } from '@shared/models' |
14 | import { logger } from '../../../helpers/logger' | 16 | import { logger } from '../../../helpers/logger' |
15 | import { generateVideoMiniature } from '@server/lib/thumbnail' | ||
16 | 17 | ||
17 | async function processVideoLiveEnding (job: Bull.Job) { | 18 | async 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' |
11 | import { retryTransactionWrapper } from '../../../helpers/database-utils' | 11 | import { retryTransactionWrapper } from '../../../helpers/database-utils' |
12 | import { computeResolutionsToTranscode } from '../../../helpers/ffmpeg-utils' | 12 | import { computeResolutionsToTranscode } from '../../../helpers/ffprobe-utils' |
13 | import { logger } from '../../../helpers/logger' | 13 | import { logger } from '../../../helpers/logger' |
14 | import { CONFIG } from '../../../initializers/config' | 14 | import { CONFIG } from '../../../initializers/config' |
15 | import { sequelizeTypescript } from '../../../initializers/database' | 15 | import { 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' | |||
4 | import { ensureDir, stat } from 'fs-extra' | 4 | import { ensureDir, stat } from 'fs-extra' |
5 | import { basename } from 'path' | 5 | import { basename } from 'path' |
6 | import { isTestInstance } from '@server/helpers/core-utils' | 6 | import { isTestInstance } from '@server/helpers/core-utils' |
7 | import { | 7 | import { runLiveMuxing, runLiveTranscoding } from '@server/helpers/ffmpeg-utils' |
8 | computeResolutionsToTranscode, | 8 | import { computeResolutionsToTranscode, getVideoFileFPS, getVideoFileResolution } from '@server/helpers/ffprobe-utils' |
9 | getVideoFileFPS, | ||
10 | getVideoFileResolution, | ||
11 | runLiveMuxing, | ||
12 | runLiveTranscoding | ||
13 | } from '@server/helpers/ffmpeg-utils' | ||
14 | import { logger } from '@server/helpers/logger' | 9 | import { logger } from '@server/helpers/logger' |
15 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' | 10 | import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config' |
16 | import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants' | 11 | import { 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' | |||
4 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' | 4 | import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoWithAllFiles, MVideoWithFile } from '@server/types/models' |
5 | import { VideoResolution } from '../../shared/models/videos' | 5 | import { VideoResolution } from '../../shared/models/videos' |
6 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' | 6 | import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type' |
7 | import { | 7 | import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils' |
8 | canDoQuickTranscode, | 8 | import { 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' | ||
16 | import { logger } from '../helpers/logger' | 9 | import { logger } from '../helpers/logger' |
17 | import { CONFIG } from '../initializers/config' | 10 | import { CONFIG } from '../initializers/config' |
18 | import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants' | 11 | import { 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' |
36 | import { cleanUpReqFiles } from '../../../helpers/express-utils' | 36 | import { cleanUpReqFiles } from '../../../helpers/express-utils' |
37 | import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils' | 37 | import { getDurationFromVideoFile } from '../../../helpers/ffprobe-utils' |
38 | import { logger } from '../../../helpers/logger' | 38 | import { logger } from '../../../helpers/logger' |
39 | import { | 39 | import { |
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' |
54 | import { getVideoFileResolution } from '../../helpers/ffmpeg-utils' | 54 | import { getVideoFileResolution } from '../../helpers/ffprobe-utils' |
55 | import { logger } from '../../helpers/logger' | 55 | import { logger } from '../../helpers/logger' |
56 | import { CONFIG } from '../../initializers/config' | 56 | import { CONFIG } from '../../initializers/config' |
57 | import { | 57 | import { |
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 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { join } from 'path' | ||
6 | import { getAudioStream, getVideoStreamSize } from '@server/helpers/ffprobe-utils' | ||
5 | import { | 7 | import { |
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' |
16 | import { VideoDetails } from '../../../../shared/models/videos' | 18 | import { VideoDetails } from '../../../../shared/models/videos' |
17 | import { join } from 'path' | ||
18 | import { audio, getVideoStreamSize } from '@server/helpers/ffmpeg-utils' | ||
19 | 19 | ||
20 | const expect = chai.expect | 20 | const 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 | ||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | ||
5 | import { FfprobeData } from 'fluent-ffmpeg' | ||
5 | import { omit } from 'lodash' | 6 | import { omit } from 'lodash' |
6 | import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' | 7 | import { join } from 'path' |
7 | import { | 8 | |
8 | audio, | 9 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' |
9 | canDoQuickTranscode, | ||
10 | getVideoFileBitrate, | ||
11 | getVideoFileFPS, | ||
12 | getVideoFileResolution, | ||
13 | getMetadataFromFile | ||
14 | } from '../../../helpers/ffmpeg-utils' | ||
15 | import { | 10 | import { |
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' |
36 | import { join } from 'path' | 32 | import { getMaxBitrate, VideoDetails, VideoResolution, VideoState } from '../../../../shared/models/videos' |
37 | import { VIDEO_TRANSCODING_FPS } from '../../../../server/initializers/constants' | 33 | import { |
38 | import { FfprobeData } from 'fluent-ffmpeg' | 34 | canDoQuickTranscode, |
39 | import { 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 | ||
41 | const expect = chai.expect | 42 | const 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 | ||
3 | import 'mocha' | 3 | import 'mocha' |
4 | import * as chai from 'chai' | 4 | import * as chai from 'chai' |
5 | import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos' | 5 | import { join } from 'path' |
6 | import { | 6 | import { |
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' |
22 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' | 22 | import { waitJobs } from '../../../shared/extra-utils/server/jobs' |
23 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffmpeg-utils' | 23 | import { getMaxBitrate, Video, VideoDetails, VideoResolution } from '../../../shared/models/videos' |
24 | import { getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../../helpers/ffprobe-utils' | ||
24 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' | 25 | import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants' |
25 | import { join } from 'path' | ||
26 | 26 | ||
27 | const expect = chai.expect | 27 | const expect = chai.expect |
28 | 28 | ||