diff options
author | Chocobozzz <me@florianbigard.com> | 2022-02-11 10:51:33 +0100 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2022-02-28 10:42:19 +0100 |
commit | c729caf6cc34630877a0e5a1bda1719384cd0c8a (patch) | |
tree | 1d2e13722e518c73d2c9e6f0969615e29d51cf8c /server/helpers/ffmpeg/ffmpeg-presets.ts | |
parent | a24bf4dc659cebb65d887862bf21d7a35e9ec791 (diff) | |
download | PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.gz PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.tar.zst PeerTube-c729caf6cc34630877a0e5a1bda1719384cd0c8a.zip |
Add basic video editor support
Diffstat (limited to 'server/helpers/ffmpeg/ffmpeg-presets.ts')
-rw-r--r-- | server/helpers/ffmpeg/ffmpeg-presets.ts | 156 |
1 files changed, 156 insertions, 0 deletions
diff --git a/server/helpers/ffmpeg/ffmpeg-presets.ts b/server/helpers/ffmpeg/ffmpeg-presets.ts new file mode 100644 index 000000000..99b39f79a --- /dev/null +++ b/server/helpers/ffmpeg/ffmpeg-presets.ts | |||
@@ -0,0 +1,156 @@ | |||
1 | import { FfmpegCommand } from 'fluent-ffmpeg' | ||
2 | import { pick } from 'lodash' | ||
3 | import { logger, loggerTagsFactory } from '@server/helpers/logger' | ||
4 | import { AvailableEncoders, EncoderOptions } from '@shared/models' | ||
5 | import { buildStreamSuffix, getScaleFilter, StreamType } from './ffmpeg-commons' | ||
6 | import { getEncoderBuilderResult } from './ffmpeg-encoders' | ||
7 | import { ffprobePromise, getVideoStreamBitrate, getVideoStreamDimensionsInfo, hasAudioStream } from './ffprobe-utils' | ||
8 | |||
9 | const lTags = loggerTagsFactory('ffmpeg') | ||
10 | |||
11 | // --------------------------------------------------------------------------- | ||
12 | |||
13 | function addDefaultEncoderGlobalParams (command: FfmpegCommand) { | ||
14 | // avoid issues when transcoding some files: https://trac.ffmpeg.org/ticket/6375 | ||
15 | command.outputOption('-max_muxing_queue_size 1024') | ||
16 | // strip all metadata | ||
17 | .outputOption('-map_metadata -1') | ||
18 | // allows import of source material with incompatible pixel formats (e.g. MJPEG video) | ||
19 | .outputOption('-pix_fmt yuv420p') | ||
20 | } | ||
21 | |||
22 | function addDefaultEncoderParams (options: { | ||
23 | command: FfmpegCommand | ||
24 | encoder: 'libx264' | string | ||
25 | fps: number | ||
26 | |||
27 | streamNum?: number | ||
28 | }) { | ||
29 | const { command, encoder, fps, streamNum } = options | ||
30 | |||
31 | if (encoder === 'libx264') { | ||
32 | // 3.1 is the minimal resource allocation for our highest supported resolution | ||
33 | command.outputOption(buildStreamSuffix('-level:v', streamNum) + ' 3.1') | ||
34 | |||
35 | if (fps) { | ||
36 | // Keyframe interval of 2 seconds for faster seeking and resolution switching. | ||
37 | // https://streaminglearningcenter.com/blogs/whats-the-right-keyframe-interval.html | ||
38 | // https://superuser.com/a/908325 | ||
39 | command.outputOption(buildStreamSuffix('-g:v', streamNum) + ' ' + (fps * 2)) | ||
40 | } | ||
41 | } | ||
42 | } | ||
43 | |||
44 | // --------------------------------------------------------------------------- | ||
45 | |||
46 | async function presetVOD (options: { | ||
47 | command: FfmpegCommand | ||
48 | input: string | ||
49 | |||
50 | availableEncoders: AvailableEncoders | ||
51 | profile: string | ||
52 | |||
53 | canCopyAudio: boolean | ||
54 | canCopyVideo: boolean | ||
55 | |||
56 | resolution: number | ||
57 | fps: number | ||
58 | |||
59 | scaleFilterValue?: string | ||
60 | }) { | ||
61 | const { command, input, profile, resolution, fps, scaleFilterValue } = options | ||
62 | |||
63 | let localCommand = command | ||
64 | .format('mp4') | ||
65 | .outputOption('-movflags faststart') | ||
66 | |||
67 | addDefaultEncoderGlobalParams(command) | ||
68 | |||
69 | const probe = await ffprobePromise(input) | ||
70 | |||
71 | // Audio encoder | ||
72 | const bitrate = await getVideoStreamBitrate(input, probe) | ||
73 | const videoStreamDimensions = await getVideoStreamDimensionsInfo(input, probe) | ||
74 | |||
75 | let streamsToProcess: StreamType[] = [ 'audio', 'video' ] | ||
76 | |||
77 | if (!await hasAudioStream(input, probe)) { | ||
78 | localCommand = localCommand.noAudio() | ||
79 | streamsToProcess = [ 'video' ] | ||
80 | } | ||
81 | |||
82 | for (const streamType of streamsToProcess) { | ||
83 | const builderResult = await getEncoderBuilderResult({ | ||
84 | ...pick(options, [ 'availableEncoders', 'canCopyAudio', 'canCopyVideo' ]), | ||
85 | |||
86 | input, | ||
87 | inputBitrate: bitrate, | ||
88 | inputRatio: videoStreamDimensions?.ratio || 0, | ||
89 | |||
90 | profile, | ||
91 | resolution, | ||
92 | fps, | ||
93 | streamType, | ||
94 | |||
95 | videoType: 'vod' as 'vod' | ||
96 | }) | ||
97 | |||
98 | if (!builderResult) { | ||
99 | throw new Error('No available encoder found for stream ' + streamType) | ||
100 | } | ||
101 | |||
102 | logger.debug( | ||
103 | 'Apply ffmpeg params from %s for %s stream of input %s using %s profile.', | ||
104 | builderResult.encoder, streamType, input, profile, | ||
105 | { builderResult, resolution, fps, ...lTags() } | ||
106 | ) | ||
107 | |||
108 | if (streamType === 'video') { | ||
109 | localCommand.videoCodec(builderResult.encoder) | ||
110 | |||
111 | if (scaleFilterValue) { | ||
112 | localCommand.outputOption(`-vf ${getScaleFilter(builderResult.result)}=${scaleFilterValue}`) | ||
113 | } | ||
114 | } else if (streamType === 'audio') { | ||
115 | localCommand.audioCodec(builderResult.encoder) | ||
116 | } | ||
117 | |||
118 | applyEncoderOptions(localCommand, builderResult.result) | ||
119 | addDefaultEncoderParams({ command: localCommand, encoder: builderResult.encoder, fps }) | ||
120 | } | ||
121 | |||
122 | return localCommand | ||
123 | } | ||
124 | |||
125 | function presetCopy (command: FfmpegCommand): FfmpegCommand { | ||
126 | return command | ||
127 | .format('mp4') | ||
128 | .videoCodec('copy') | ||
129 | .audioCodec('copy') | ||
130 | } | ||
131 | |||
132 | function presetOnlyAudio (command: FfmpegCommand): FfmpegCommand { | ||
133 | return command | ||
134 | .format('mp4') | ||
135 | .audioCodec('copy') | ||
136 | .noVideo() | ||
137 | } | ||
138 | |||
139 | function applyEncoderOptions (command: FfmpegCommand, options: EncoderOptions): FfmpegCommand { | ||
140 | return command | ||
141 | .inputOptions(options.inputOptions ?? []) | ||
142 | .outputOptions(options.outputOptions ?? []) | ||
143 | } | ||
144 | |||
145 | // --------------------------------------------------------------------------- | ||
146 | |||
147 | export { | ||
148 | presetVOD, | ||
149 | presetCopy, | ||
150 | presetOnlyAudio, | ||
151 | |||
152 | addDefaultEncoderGlobalParams, | ||
153 | addDefaultEncoderParams, | ||
154 | |||
155 | applyEncoderOptions | ||
156 | } | ||