aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/helpers/ffmpeg-utils.ts66
-rw-r--r--server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js97
-rw-r--r--server/tests/plugins/plugin-transcoding.ts34
3 files changed, 126 insertions, 71 deletions
diff --git a/server/helpers/ffmpeg-utils.ts b/server/helpers/ffmpeg-utils.ts
index 685a35886..aa4223cda 100644
--- a/server/helpers/ffmpeg-utils.ts
+++ b/server/helpers/ffmpeg-utils.ts
@@ -9,6 +9,7 @@ import { execPromise, promisify0 } from './core-utils'
9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils' 9import { computeFPS, getAudioStream, getVideoFileFPS } from './ffprobe-utils'
10import { processImage } from './image-utils' 10import { processImage } from './image-utils'
11import { logger } from './logger' 11import { logger } from './logger'
12import { FilterSpecification } from 'fluent-ffmpeg'
12 13
13/** 14/**
14 * 15 *
@@ -226,21 +227,14 @@ async function getLiveTranscodingCommand (options: {
226 227
227 const varStreamMap: string[] = [] 228 const varStreamMap: string[] = []
228 229
229 command.complexFilter([ 230 const complexFilter: FilterSpecification[] = [
230 { 231 {
231 inputs: '[v:0]', 232 inputs: '[v:0]',
232 filter: 'split', 233 filter: 'split',
233 options: resolutions.length, 234 options: resolutions.length,
234 outputs: resolutions.map(r => `vtemp${r}`) 235 outputs: resolutions.map(r => `vtemp${r}`)
235 }, 236 }
236 237 ]
237 ...resolutions.map(r => ({
238 inputs: `vtemp${r}`,
239 filter: 'scale',
240 options: `w=-2:h=${r}`,
241 outputs: `vout${r}`
242 }))
243 ])
244 238
245 command.outputOption('-preset superfast') 239 command.outputOption('-preset superfast')
246 command.outputOption('-sc_threshold 0') 240 command.outputOption('-sc_threshold 0')
@@ -278,6 +272,13 @@ async function getLiveTranscodingCommand (options: {
278 272
279 command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) 273 command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`)
280 applyEncoderOptions(command, builderResult.result) 274 applyEncoderOptions(command, builderResult.result)
275
276 complexFilter.push({
277 inputs: `vtemp${resolution}`,
278 filter: getScaleFilter(builderResult.result),
279 options: `w=-2:h=${resolution}`,
280 outputs: `vout${resolution}`
281 })
281 } 282 }
282 283
283 { 284 {
@@ -300,6 +301,8 @@ async function getLiveTranscodingCommand (options: {
300 varStreamMap.push(`v:${i},a:${i}`) 301 varStreamMap.push(`v:${i},a:${i}`)
301 } 302 }
302 303
304 command.complexFilter(complexFilter)
305
303 addDefaultLiveHLSParams(command, outPath) 306 addDefaultLiveHLSParams(command, outPath)
304 307
305 command.outputOption('-var_stream_map', varStreamMap.join(' ')) 308 command.outputOption('-var_stream_map', varStreamMap.join(' '))
@@ -389,29 +392,29 @@ async function buildx264VODCommand (command: ffmpeg.FfmpegCommand, options: Tran
389 let fps = await getVideoFileFPS(options.inputPath) 392 let fps = await getVideoFileFPS(options.inputPath)
390 fps = computeFPS(fps, options.resolution) 393 fps = computeFPS(fps, options.resolution)
391 394
392 command = await presetVideo(command, options.inputPath, options, fps) 395 let scaleFilterValue: string
393 396
394 if (options.resolution !== undefined) { 397 if (options.resolution !== undefined) {
395 // '?x720' or '720x?' for example 398 scaleFilterValue = options.isPortraitMode === true
396 const size = options.isPortraitMode === true 399 ? `${options.resolution}:-2`
397 ? `${options.resolution}x?` 400 : `-2:${options.resolution}`
398 : `?x${options.resolution}`
399
400 command = command.size(size)
401 } 401 }
402 402
403 command = await presetVideo({ command, input: options.inputPath, transcodeOptions: options, fps, scaleFilterValue })
404
403 return command 405 return command
404} 406}
405 407
406async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) { 408async function buildAudioMergeCommand (command: ffmpeg.FfmpegCommand, options: MergeAudioTranscodeOptions) {
407 command = command.loop(undefined) 409 command = command.loop(undefined)
408 410
409 command = await presetVideo(command, options.audioPath, options) 411 // Avoid "height not divisible by 2" error
412 const scaleFilterValue = 'trunc(iw/2)*2:trunc(ih/2)*2'
413 command = await presetVideo({ command, input: options.audioPath, transcodeOptions: options, scaleFilterValue })
410 414
411 command.outputOption('-preset:v veryfast') 415 command.outputOption('-preset:v veryfast')
412 416
413 command = command.input(options.audioPath) 417 command = command.input(options.audioPath)
414 .videoFilter('scale=trunc(iw/2)*2:trunc(ih/2)*2') // Avoid "height not divisible by 2" error
415 .outputOption('-tune stillimage') 418 .outputOption('-tune stillimage')
416 .outputOption('-shortest') 419 .outputOption('-shortest')
417 420
@@ -555,12 +558,15 @@ async function getEncoderBuilderResult (options: {
555 return null 558 return null
556} 559}
557 560
558async function presetVideo ( 561async function presetVideo (options: {
559 command: ffmpeg.FfmpegCommand, 562 command: ffmpeg.FfmpegCommand
560 input: string, 563 input: string
561 transcodeOptions: TranscodeOptions, 564 transcodeOptions: TranscodeOptions
562 fps?: number 565 fps?: number
563) { 566 scaleFilterValue?: string
567}) {
568 const { command, input, transcodeOptions, fps, scaleFilterValue } = options
569
564 let localCommand = command 570 let localCommand = command
565 .format('mp4') 571 .format('mp4')
566 .outputOption('-movflags faststart') 572 .outputOption('-movflags faststart')
@@ -601,9 +607,14 @@ async function presetVideo (
601 607
602 if (streamType === 'video') { 608 if (streamType === 'video') {
603 localCommand.videoCodec(builderResult.encoder) 609 localCommand.videoCodec(builderResult.encoder)
610
611 if (scaleFilterValue) {
612 localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`)
613 }
604 } else if (streamType === 'audio') { 614 } else if (streamType === 'audio') {
605 localCommand.audioCodec(builderResult.encoder) 615 localCommand.audioCodec(builderResult.encoder)
606 } 616 }
617
607 applyEncoderOptions(localCommand, builderResult.result) 618 applyEncoderOptions(localCommand, builderResult.result)
608 addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps }) 619 addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps })
609 } 620 }
@@ -628,10 +639,15 @@ function presetOnlyAudio (command: ffmpeg.FfmpegCommand): ffmpeg.FfmpegCommand {
628function applyEncoderOptions (command: ffmpeg.FfmpegCommand, options: EncoderOptions): ffmpeg.FfmpegCommand { 639function applyEncoderOptions (command: ffmpeg.FfmpegCommand, options: EncoderOptions): ffmpeg.FfmpegCommand {
629 return command 640 return command
630 .inputOptions(options.inputOptions ?? []) 641 .inputOptions(options.inputOptions ?? [])
631 .videoFilters(options.videoFilters ?? [])
632 .outputOptions(options.outputOptions ?? []) 642 .outputOptions(options.outputOptions ?? [])
633} 643}
634 644
645function getScaleFilter (options: EncoderOptions): string {
646 if (options.scaleFilter) return options.scaleFilter.name
647
648 return 'scale'
649}
650
635// --------------------------------------------------------------------------- 651// ---------------------------------------------------------------------------
636// Utils 652// Utils
637// --------------------------------------------------------------------------- 653// ---------------------------------------------------------------------------
diff --git a/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js
index 366b827a9..59b136947 100644
--- a/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js
+++ b/server/tests/fixtures/peertube-plugin-test-transcoding-one/main.js
@@ -1,63 +1,84 @@
1async function register ({ transcodingManager }) { 1async function register ({ transcodingManager }) {
2 2
3 // Output options
3 { 4 {
4 const builder = () => { 5 {
5 return { 6 const builder = () => {
6 outputOptions: [ 7 return {
7 '-r 10' 8 outputOptions: [
8 ] 9 '-r 10'
10 ]
11 }
9 } 12 }
10 }
11 13
12 transcodingManager.addVODProfile('libx264', 'low-vod', builder) 14 transcodingManager.addVODProfile('libx264', 'low-vod', builder)
13 } 15 }
14 16
15 { 17 {
16 const builder = () => { 18 const builder = (options) => {
17 return { 19 return {
18 videoFilters: [ 20 outputOptions: [
19 'fps=10' 21 '-r:' + options.streamNum + ' 5'
20 ] 22 ]
23 }
21 } 24 }
22 }
23 25
24 transcodingManager.addVODProfile('libx264', 'video-filters-vod', builder) 26 transcodingManager.addLiveProfile('libx264', 'low-live', builder)
27 }
25 } 28 }
26 29
30 // Input options
27 { 31 {
28 const builder = () => { 32 {
29 return { 33 const builder = () => {
30 inputOptions: [ 34 return {
31 '-r 5' 35 inputOptions: [
32 ] 36 '-r 5'
37 ]
38 }
33 } 39 }
34 }
35 40
36 transcodingManager.addVODProfile('libx264', 'input-options-vod', builder) 41 transcodingManager.addVODProfile('libx264', 'input-options-vod', builder)
37 } 42 }
38 43
39 { 44 {
40 const builder = (options) => { 45 const builder = () => {
41 return { 46 return {
42 outputOptions: [ 47 inputOptions: [
43 '-r:' + options.streamNum + ' 5' 48 '-r 5'
44 ] 49 ]
50 }
45 } 51 }
46 }
47 52
48 transcodingManager.addLiveProfile('libx264', 'low-live', builder) 53 transcodingManager.addLiveProfile('libx264', 'input-options-live', builder)
54 }
49 } 55 }
50 56
57 // Scale filters
51 { 58 {
52 const builder = () => { 59 {
53 return { 60 const builder = () => {
54 inputOptions: [ 61 return {
55 '-r 5' 62 scaleFilter: {
56 ] 63 name: 'Glomgold'
64 }
65 }
57 } 66 }
67
68 transcodingManager.addVODProfile('libx264', 'bad-scale-vod', builder)
58 } 69 }
59 70
60 transcodingManager.addLiveProfile('libx264', 'input-options-live', builder) 71 {
72 const builder = () => {
73 return {
74 scaleFilter: {
75 name: 'Flintheart'
76 }
77 }
78 }
79
80 transcodingManager.addLiveProfile('libx264', 'bad-scale-live', builder)
81 }
61 } 82 }
62} 83}
63 84
diff --git a/server/tests/plugins/plugin-transcoding.ts b/server/tests/plugins/plugin-transcoding.ts
index 415705ca1..b6dff930e 100644
--- a/server/tests/plugins/plugin-transcoding.ts
+++ b/server/tests/plugins/plugin-transcoding.ts
@@ -15,9 +15,11 @@ import {
15 sendRTMPStreamInVideo, 15 sendRTMPStreamInVideo,
16 setAccessTokensToServers, 16 setAccessTokensToServers,
17 setDefaultVideoChannel, 17 setDefaultVideoChannel,
18 testFfmpegStreamError,
18 uninstallPlugin, 19 uninstallPlugin,
19 updateCustomSubConfig, 20 updateCustomSubConfig,
20 uploadVideoAndGetId, 21 uploadVideoAndGetId,
22 waitFfmpegUntilError,
21 waitJobs, 23 waitJobs,
22 waitUntilLivePublished 24 waitUntilLivePublished
23} from '../../../shared/extra-utils' 25} from '../../../shared/extra-utils'
@@ -119,8 +121,8 @@ describe('Test transcoding plugins', function () {
119 const res = await getConfig(server.url) 121 const res = await getConfig(server.url)
120 const config = res.body as ServerConfig 122 const config = res.body as ServerConfig
121 123
122 expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'video-filters-vod', 'input-options-vod' ]) 124 expect(config.transcoding.availableProfiles).to.have.members([ 'default', 'low-vod', 'input-options-vod', 'bad-scale-vod' ])
123 expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live', 'input-options-live' ]) 125 expect(config.live.transcoding.availableProfiles).to.have.members([ 'default', 'low-live', 'input-options-live', 'bad-scale-live' ])
124 }) 126 })
125 127
126 it('Should not use the plugin profile if not chosen by the admin', async function () { 128 it('Should not use the plugin profile if not chosen by the admin', async function () {
@@ -143,26 +145,31 @@ describe('Test transcoding plugins', function () {
143 await checkVideoFPS(videoUUID, 'below', 12) 145 await checkVideoFPS(videoUUID, 'below', 12)
144 }) 146 })
145 147
146 it('Should apply video filters in vod profile', async function () { 148 it('Should apply input options in vod profile', async function () {
147 this.timeout(120000) 149 this.timeout(120000)
148 150
149 await updateConf(server, 'video-filters-vod', 'default') 151 await updateConf(server, 'input-options-vod', 'default')
150 152
151 const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid 153 const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
152 await waitJobs([ server ]) 154 await waitJobs([ server ])
153 155
154 await checkVideoFPS(videoUUID, 'below', 12) 156 await checkVideoFPS(videoUUID, 'below', 6)
155 }) 157 })
156 158
157 it('Should apply input options in vod profile', async function () { 159 it('Should apply the scale filter in vod profile', async function () {
158 this.timeout(120000) 160 this.timeout(120000)
159 161
160 await updateConf(server, 'input-options-vod', 'default') 162 await updateConf(server, 'bad-scale-vod', 'default')
161 163
162 const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid 164 const videoUUID = (await uploadVideoAndGetId({ server, videoName: 'video' })).uuid
163 await waitJobs([ server ]) 165 await waitJobs([ server ])
164 166
165 await checkVideoFPS(videoUUID, 'below', 6) 167 // Transcoding failed
168 const res = await getVideo(server.url, videoUUID)
169 const video: VideoDetails = res.body
170
171 expect(video.files).to.have.lengthOf(1)
172 expect(video.streamingPlaylists).to.have.lengthOf(0)
166 }) 173 })
167 174
168 it('Should not use the plugin profile if not chosen by the admin', async function () { 175 it('Should not use the plugin profile if not chosen by the admin', async function () {
@@ -205,6 +212,17 @@ describe('Test transcoding plugins', function () {
205 await checkLiveFPS(liveVideoId, 'below', 6) 212 await checkLiveFPS(liveVideoId, 'below', 6)
206 }) 213 })
207 214
215 it('Should apply the scale filter name on live profile', async function () {
216 this.timeout(120000)
217
218 await updateConf(server, 'low-vod', 'bad-scale-live')
219
220 const liveVideoId = await createLiveWrapper(server)
221
222 const command = await sendRTMPStreamInVideo(server.url, server.accessToken, liveVideoId, 'video_short2.webm')
223 await testFfmpegStreamError(command, true)
224 })
225
208 it('Should default to the default profile if the specified profile does not exist', async function () { 226 it('Should default to the default profile if the specified profile does not exist', async function () {
209 this.timeout(120000) 227 this.timeout(120000)
210 228