aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers/ffmpeg-utils.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/helpers/ffmpeg-utils.ts')
-rw-r--r--server/helpers/ffmpeg-utils.ts140
1 files changed, 106 insertions, 34 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 132f4690e..76b744de8 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -1,11 +1,12 @@
1import * as ffmpeg from 'fluent-ffmpeg' 1import * as ffmpeg from 'fluent-ffmpeg'
2import { join } from 'path' 2import { dirname, join } from 'path'
3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos' 3import { getTargetBitrate, VideoResolution } from '../../shared/models/videos'
4import { CONFIG, FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants' 4import { FFMPEG_NICE, VIDEO_TRANSCODING_FPS } from '../initializers/constants'
5import { processImage } from './image-utils' 5import { processImage } from './image-utils'
6import { logger } from './logger' 6import { logger } from './logger'
7import { checkFFmpegEncoders } from '../initializers/checker-before-init' 7import { checkFFmpegEncoders } from '../initializers/checker-before-init'
8import { remove } from 'fs-extra' 8import { readFile, remove, writeFile } from 'fs-extra'
9import { CONFIG } from '../initializers/config'
9 10
10function computeResolutionsToTranscode (videoFileHeight: number) { 11function computeResolutionsToTranscode (videoFileHeight: number) {
11 const resolutionsEnabled: number[] = [] 12 const resolutionsEnabled: number[] = []
@@ -29,12 +30,21 @@ function computeResolutionsToTranscode (videoFileHeight: number) {
29 return resolutionsEnabled 30 return resolutionsEnabled
30} 31}
31 32
32async function getVideoFileResolution (path: string) { 33async function getVideoFileSize (path: string) {
33 const videoStream = await getVideoFileStream(path) 34 const videoStream = await getVideoFileStream(path)
34 35
35 return { 36 return {
36 videoFileResolution: Math.min(videoStream.height, videoStream.width), 37 width: videoStream.width,
37 isPortraitMode: videoStream.height > videoStream.width 38 height: videoStream.height
39 }
40}
41
42async function getVideoFileResolution (path: string) {
43 const size = await getVideoFileSize(path)
44
45 return {
46 videoFileResolution: Math.min(size.height, size.width),
47 isPortraitMode: size.height > size.width
38 } 48 }
39} 49}
40 50
@@ -95,7 +105,7 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
95 }) 105 })
96 106
97 const destination = join(folder, imageName) 107 const destination = join(folder, imageName)
98 await processImage({ path: pendingImagePath }, destination, size) 108 await processImage(pendingImagePath, destination, size)
99 } catch (err) { 109 } catch (err) {
100 logger.error('Cannot generate image from video %s.', fromPath, { err }) 110 logger.error('Cannot generate image from video %s.', fromPath, { err })
101 111
@@ -110,52 +120,41 @@ async function generateImageFromVideoFile (fromPath: string, folder: string, ima
110type TranscodeOptions = { 120type TranscodeOptions = {
111 inputPath: string 121 inputPath: string
112 outputPath: string 122 outputPath: string
113 resolution?: VideoResolution 123 resolution: VideoResolution
114 isPortraitMode?: boolean 124 isPortraitMode?: boolean
125
126 hlsPlaylist?: {
127 videoFilename: string
128 }
115} 129}
116 130
117function transcode (options: TranscodeOptions) { 131function transcode (options: TranscodeOptions) {
118 return new Promise<void>(async (res, rej) => { 132 return new Promise<void>(async (res, rej) => {
119 try { 133 try {
120 let fps = await getVideoFileFPS(options.inputPath)
121 // On small/medium resolutions, limit FPS
122 if (
123 options.resolution !== undefined &&
124 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
125 fps > VIDEO_TRANSCODING_FPS.AVERAGE
126 ) {
127 fps = VIDEO_TRANSCODING_FPS.AVERAGE
128 }
129
130 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING }) 134 let command = ffmpeg(options.inputPath, { niceness: FFMPEG_NICE.TRANSCODING })
131 .output(options.outputPath) 135 .output(options.outputPath)
132 command = await presetH264(command, options.resolution, fps) 136
137 if (options.hlsPlaylist) {
138 command = await buildHLSCommand(command, options)
139 } else {
140 command = await buildx264Command(command, options)
141 }
133 142
134 if (CONFIG.TRANSCODING.THREADS > 0) { 143 if (CONFIG.TRANSCODING.THREADS > 0) {
135 // if we don't set any threads ffmpeg will chose automatically 144 // if we don't set any threads ffmpeg will chose automatically
136 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS) 145 command = command.outputOption('-threads ' + CONFIG.TRANSCODING.THREADS)
137 } 146 }
138 147
139 if (options.resolution !== undefined) {
140 // '?x720' or '720x?' for example
141 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
142 command = command.size(size)
143 }
144
145 if (fps) {
146 // Hard FPS limits
147 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
148 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
149
150 command = command.withFPS(fps)
151 }
152
153 command 148 command
154 .on('error', (err, stdout, stderr) => { 149 .on('error', (err, stdout, stderr) => {
155 logger.error('Error in transcoding job.', { stdout, stderr }) 150 logger.error('Error in transcoding job.', { stdout, stderr })
156 return rej(err) 151 return rej(err)
157 }) 152 })
158 .on('end', res) 153 .on('end', () => {
154 return onTranscodingSuccess(options)
155 .then(() => res())
156 .catch(err => rej(err))
157 })
159 .run() 158 .run()
160 } catch (err) { 159 } catch (err) {
161 return rej(err) 160 return rej(err)
@@ -166,6 +165,7 @@ function transcode (options: TranscodeOptions) {
166// --------------------------------------------------------------------------- 165// ---------------------------------------------------------------------------
167 166
168export { 167export {
168 getVideoFileSize,
169 getVideoFileResolution, 169 getVideoFileResolution,
170 getDurationFromVideoFile, 170 getDurationFromVideoFile,
171 generateImageFromVideoFile, 171 generateImageFromVideoFile,
@@ -178,6 +178,71 @@ export {
178 178
179// --------------------------------------------------------------------------- 179// ---------------------------------------------------------------------------
180 180
181async function buildx264Command (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
182 let fps = await getVideoFileFPS(options.inputPath)
183 // On small/medium resolutions, limit FPS
184 if (
185 options.resolution !== undefined &&
186 options.resolution < VIDEO_TRANSCODING_FPS.KEEP_ORIGIN_FPS_RESOLUTION_MIN &&
187 fps > VIDEO_TRANSCODING_FPS.AVERAGE
188 ) {
189 fps = VIDEO_TRANSCODING_FPS.AVERAGE
190 }
191
192 command = await presetH264(command, options.resolution, fps)
193
194 if (options.resolution !== undefined) {
195 // '?x720' or '720x?' for example
196 const size = options.isPortraitMode === true ? `${options.resolution}x?` : `?x${options.resolution}`
197 command = command.size(size)
198 }
199
200 if (fps) {
201 // Hard FPS limits
202 if (fps > VIDEO_TRANSCODING_FPS.MAX) fps = VIDEO_TRANSCODING_FPS.MAX
203 else if (fps < VIDEO_TRANSCODING_FPS.MIN) fps = VIDEO_TRANSCODING_FPS.MIN
204
205 command = command.withFPS(fps)
206 }
207
208 return command
209}
210
211async function buildHLSCommand (command: ffmpeg.FfmpegCommand, options: TranscodeOptions) {
212 const videoPath = getHLSVideoPath(options)
213
214 command = await presetCopy(command)
215
216 command = command.outputOption('-hls_time 4')
217 .outputOption('-hls_list_size 0')
218 .outputOption('-hls_playlist_type vod')
219 .outputOption('-hls_segment_filename ' + videoPath)
220 .outputOption('-hls_segment_type fmp4')
221 .outputOption('-f hls')
222 .outputOption('-hls_flags single_file')
223
224 return command
225}
226
227function getHLSVideoPath (options: TranscodeOptions) {
228 return `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
229}
230
231async function onTranscodingSuccess (options: TranscodeOptions) {
232 if (!options.hlsPlaylist) return
233
234 // Fix wrong mapping with some ffmpeg versions
235 const fileContent = await readFile(options.outputPath)
236
237 const videoFileName = options.hlsPlaylist.videoFilename
238 const videoFilePath = getHLSVideoPath(options)
239
240 const newContent = fileContent.toString()
241 .replace(`#EXT-X-MAP:URI="${videoFilePath}",`, `#EXT-X-MAP:URI="${videoFileName}",`)
242
243 await writeFile(options.outputPath, newContent)
244}
245
181function getVideoFileStream (path: string) { 246function getVideoFileStream (path: string) {
182 return new Promise<any>((res, rej) => { 247 return new Promise<any>((res, rej) => {
183 ffmpeg.ffprobe(path, (err, metadata) => { 248 ffmpeg.ffprobe(path, (err, metadata) => {
@@ -348,3 +413,10 @@ async function presetH264 (command: ffmpeg.FfmpegCommand, resolution: VideoResol
348 413
349 return localCommand 414 return localCommand
350} 415}
416
417async function presetCopy (command: ffmpeg.FfmpegCommand): Promise<ffmpeg.FfmpegCommand> {
418 return command
419 .format('mp4')
420 .videoCodec('copy')
421 .audioCodec('copy')
422}