]>
Commit | Line | Data |
---|---|---|
c729caf6 C |
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 | } |