aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/helpers/ffmpeg/ffmpeg-edition.ts
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2022-02-11 10:51:33 +0100
committerChocobozzz <chocobozzz@cpy.re>2022-02-28 10:42:19 +0100
commitc729caf6cc34630877a0e5a1bda1719384cd0c8a (patch)
tree1d2e13722e518c73d2c9e6f0969615e29d51cf8c /server/helpers/ffmpeg/ffmpeg-edition.ts
parenta24bf4dc659cebb65d887862bf21d7a35e9ec791 (diff)
downloadPeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.gz
PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.zst
PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.zip
Add basic video editor support
Diffstat (limited to 'server/helpers/ffmpeg/ffmpeg-edition.ts')
-rw-r--r--server/helpers/ffmpeg/ffmpeg-edition.ts242
1 files changed, 242 insertions, 0 deletions
diff --git a/server/helpers/ffmpeg/ffmpeg-edition.ts b/server/helpers/ffmpeg/ffmpeg-edition.ts
new file mode 100644
index 000000000..a5baa7ef1
--- /dev/null
+++ b/server/helpers/ffmpeg/ffmpeg-edition.ts
@@ -0,0 +1,242 @@
1import { FilterSpecification } from 'fluent-ffmpeg'
2import { VIDEO_FILTERS } from '@server/initializers/constants'
3import { AvailableEncoders } from '@shared/models'
4import { logger, loggerTagsFactory } from '../logger'
5import { getFFmpeg, runCommand } from './ffmpeg-commons'
6import { presetCopy, presetVOD } from './ffmpeg-presets'
7import { ffprobePromise, getVideoStreamDimensionsInfo, getVideoStreamDuration, getVideoStreamFPS, hasAudioStream } from './ffprobe-utils'
8
9const lTags = loggerTagsFactory('ffmpeg')
10
11async function cutVideo (options: {
12 inputPath: string
13 outputPath: string
14 start?: number
15 end?: number
16}) {
17 const { inputPath, outputPath } = options
18
19 logger.debug('Will cut the video.', { options, ...lTags() })
20
21 let command = getFFmpeg(inputPath, 'vod')
22 .output(outputPath)
23
24 command = presetCopy(command)
25
26 if (options.start) command.inputOption('-ss ' + options.start)
27
28 if (options.end) {
29 const endSeeking = options.end - (options.start || 0)
30
31 command.outputOption('-to ' + endSeeking)
32 }
33
34 await runCommand({ command })
35}
36
37async function addWatermark (options: {
38 inputPath: string
39 watermarkPath: string
40 outputPath: string
41
42 availableEncoders: AvailableEncoders
43 profile: string
44}) {
45 const { watermarkPath, inputPath, outputPath, availableEncoders, profile } = options
46
47 logger.debug('Will add watermark to the video.', { options, ...lTags() })
48
49 const videoProbe = await ffprobePromise(inputPath)
50 const fps = await getVideoStreamFPS(inputPath, videoProbe)
51 const { resolution } = await getVideoStreamDimensionsInfo(inputPath, videoProbe)
52
53 let command = getFFmpeg(inputPath, 'vod')
54 .output(outputPath)
55 command.input(watermarkPath)
56
57 command = await presetVOD({
58 command,
59 input: inputPath,
60 availableEncoders,
61 profile,
62 resolution,
63 fps,
64 canCopyAudio: true,
65 canCopyVideo: false
66 })
67
68 const complexFilter: FilterSpecification[] = [
69 // Scale watermark
70 {
71 inputs: [ '[1]', '[0]' ],
72 filter: 'scale2ref',
73 options: {
74 w: 'oh*mdar',
75 h: `ih*${VIDEO_FILTERS.WATERMARK.SIZE_RATIO}`
76 },
77 outputs: [ '[watermark]', '[video]' ]
78 },
79
80 {
81 inputs: [ '[video]', '[watermark]' ],
82 filter: 'overlay',
83 options: {
84 x: `main_w - overlay_w - (main_h * ${VIDEO_FILTERS.WATERMARK.HORIZONTAL_MARGIN_RATIO})`,
85 y: `main_h * ${VIDEO_FILTERS.WATERMARK.VERTICAL_MARGIN_RATIO}`
86 }
87 }
88 ]
89
90 command.complexFilter(complexFilter)
91
92 await runCommand({ command })
93}
94
95async function addIntroOutro (options: {
96 inputPath: string
97 introOutroPath: string
98 outputPath: string
99 type: 'intro' | 'outro'
100
101 availableEncoders: AvailableEncoders
102 profile: string
103}) {
104 const { introOutroPath, inputPath, outputPath, availableEncoders, profile, type } = options
105
106 logger.debug('Will add intro/outro to the video.', { options, ...lTags() })
107
108 const mainProbe = await ffprobePromise(inputPath)
109 const fps = await getVideoStreamFPS(inputPath, mainProbe)
110 const { resolution } = await getVideoStreamDimensionsInfo(inputPath, mainProbe)
111 const mainHasAudio = await hasAudioStream(inputPath, mainProbe)
112
113 const introOutroProbe = await ffprobePromise(introOutroPath)
114 const introOutroHasAudio = await hasAudioStream(introOutroPath, introOutroProbe)
115
116 let command = getFFmpeg(inputPath, 'vod')
117 .output(outputPath)
118
119 command.input(introOutroPath)
120
121 if (!introOutroHasAudio && mainHasAudio) {
122 const duration = await getVideoStreamDuration(introOutroPath, introOutroProbe)
123
124 command.input('anullsrc')
125 command.withInputFormat('lavfi')
126 command.withInputOption('-t ' + duration)
127 }
128
129 command = await presetVOD({
130 command,
131 input: inputPath,
132 availableEncoders,
133 profile,
134 resolution,
135 fps,
136 canCopyAudio: false,
137 canCopyVideo: false
138 })
139
140 // Add black background to correctly scale intro/outro with padding
141 const complexFilter: FilterSpecification[] = [
142 {
143 inputs: [ '1', '0' ],
144 filter: 'scale2ref',
145 options: {
146 w: 'iw',
147 h: `ih`
148 },
149 outputs: [ 'intro-outro', 'main' ]
150 },
151 {
152 inputs: [ 'intro-outro', 'main' ],
153 filter: 'scale2ref',
154 options: {
155 w: 'iw',
156 h: `ih`
157 },
158 outputs: [ 'to-scale', 'main' ]
159 },
160 {
161 inputs: 'to-scale',
162 filter: 'drawbox',
163 options: {
164 t: 'fill'
165 },
166 outputs: [ 'to-scale-bg' ]
167 },
168 {
169 inputs: [ '1', 'to-scale-bg' ],
170 filter: 'scale2ref',
171 options: {
172 w: 'iw',
173 h: 'ih',
174 force_original_aspect_ratio: 'decrease',
175 flags: 'spline'
176 },
177 outputs: [ 'to-scale', 'to-scale-bg' ]
178 },
179 {
180 inputs: [ 'to-scale-bg', 'to-scale' ],
181 filter: 'overlay',
182 options: {
183 x: '(main_w - overlay_w)/2',
184 y: '(main_h - overlay_h)/2'
185 },
186 outputs: 'intro-outro-resized'
187 }
188 ]
189
190 const concatFilter = {
191 inputs: [],
192 filter: 'concat',
193 options: {
194 n: 2,
195 v: 1,
196 unsafe: 1
197 },
198 outputs: [ 'v' ]
199 }
200
201 const introOutroFilterInputs = [ 'intro-outro-resized' ]
202 const mainFilterInputs = [ 'main' ]
203
204 if (mainHasAudio) {
205 mainFilterInputs.push('0:a')
206
207 if (introOutroHasAudio) {
208 introOutroFilterInputs.push('1:a')
209 } else {
210 // Silent input
211 introOutroFilterInputs.push('2:a')
212 }
213 }
214
215 if (type === 'intro') {
216 concatFilter.inputs = [ ...introOutroFilterInputs, ...mainFilterInputs ]
217 } else {
218 concatFilter.inputs = [ ...mainFilterInputs, ...introOutroFilterInputs ]
219 }
220
221 if (mainHasAudio) {
222 concatFilter.options['a'] = 1
223 concatFilter.outputs.push('a')
224
225 command.outputOption('-map [a]')
226 }
227
228 command.outputOption('-map [v]')
229
230 complexFilter.push(concatFilter)
231 command.complexFilter(complexFilter)
232
233 await runCommand({ command })
234}
235
236// ---------------------------------------------------------------------------
237
238export {
239 cutVideo,
240 addIntroOutro,
241 addWatermark
242}