]>
Commit | Line | Data |
---|---|---|
c729caf6 C |
1 | import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg' |
2 | import { join } from 'path' | |
3 | import { VIDEO_LIVE } from '@server/initializers/constants' | |
f443a746 | 4 | import { AvailableEncoders, LiveVideoLatencyMode } from '@shared/models' |
c729caf6 C |
5 | import { logger, loggerTagsFactory } from '../logger' |
6 | import { buildStreamSuffix, getFFmpeg, getScaleFilter, StreamType } from './ffmpeg-commons' | |
7 | import { getEncoderBuilderResult } from './ffmpeg-encoders' | |
8 | import { addDefaultEncoderGlobalParams, addDefaultEncoderParams, applyEncoderOptions } from './ffmpeg-presets' | |
9 | import { computeFPS } from './ffprobe-utils' | |
10 | ||
11 | const lTags = loggerTagsFactory('ffmpeg') | |
12 | ||
13 | async function getLiveTranscodingCommand (options: { | |
14 | inputUrl: string | |
15 | ||
16 | outPath: string | |
17 | masterPlaylistName: string | |
f443a746 | 18 | latencyMode: LiveVideoLatencyMode |
c729caf6 C |
19 | |
20 | resolutions: number[] | |
21 | ||
22 | // Input information | |
23 | fps: number | |
24 | bitrate: number | |
25 | ratio: number | |
1ce4256a | 26 | hasAudio: boolean |
c729caf6 C |
27 | |
28 | availableEncoders: AvailableEncoders | |
29 | profile: string | |
30 | }) { | |
1ce4256a C |
31 | const { |
32 | inputUrl, | |
33 | outPath, | |
34 | resolutions, | |
35 | fps, | |
36 | bitrate, | |
37 | availableEncoders, | |
38 | profile, | |
39 | masterPlaylistName, | |
40 | ratio, | |
41 | latencyMode, | |
42 | hasAudio | |
43 | } = options | |
c729caf6 C |
44 | |
45 | const command = getFFmpeg(inputUrl, 'live') | |
46 | ||
47 | const varStreamMap: string[] = [] | |
48 | ||
49 | const complexFilter: FilterSpecification[] = [ | |
50 | { | |
51 | inputs: '[v:0]', | |
52 | filter: 'split', | |
53 | options: resolutions.length, | |
54 | outputs: resolutions.map(r => `vtemp${r}`) | |
55 | } | |
56 | ] | |
57 | ||
58 | command.outputOption('-sc_threshold 0') | |
59 | ||
60 | addDefaultEncoderGlobalParams(command) | |
61 | ||
62 | for (let i = 0; i < resolutions.length; i++) { | |
1ce4256a | 63 | const streamMap: string[] = [] |
c729caf6 C |
64 | const resolution = resolutions[i] |
65 | const resolutionFPS = computeFPS(fps, resolution) | |
66 | ||
67 | const baseEncoderBuilderParams = { | |
68 | input: inputUrl, | |
69 | ||
70 | availableEncoders, | |
71 | profile, | |
72 | ||
73 | canCopyAudio: true, | |
74 | canCopyVideo: true, | |
75 | ||
76 | inputBitrate: bitrate, | |
77 | inputRatio: ratio, | |
78 | ||
79 | resolution, | |
80 | fps: resolutionFPS, | |
81 | ||
82 | streamNum: i, | |
83 | videoType: 'live' as 'live' | |
84 | } | |
85 | ||
86 | { | |
87 | const streamType: StreamType = 'video' | |
88 | const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType }) | |
89 | if (!builderResult) { | |
90 | throw new Error('No available live video encoder found') | |
91 | } | |
92 | ||
93 | command.outputOption(`-map [vout${resolution}]`) | |
94 | ||
95 | addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) | |
96 | ||
97 | logger.debug( | |
98 | 'Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, | |
99 | { builderResult, fps: resolutionFPS, resolution, ...lTags() } | |
100 | ) | |
101 | ||
102 | command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) | |
103 | applyEncoderOptions(command, builderResult.result) | |
104 | ||
105 | complexFilter.push({ | |
106 | inputs: `vtemp${resolution}`, | |
107 | filter: getScaleFilter(builderResult.result), | |
108 | options: `w=-2:h=${resolution}`, | |
109 | outputs: `vout${resolution}` | |
110 | }) | |
1ce4256a C |
111 | |
112 | streamMap.push(`v:${i}`) | |
c729caf6 C |
113 | } |
114 | ||
1ce4256a | 115 | if (hasAudio) { |
c729caf6 C |
116 | const streamType: StreamType = 'audio' |
117 | const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType }) | |
118 | if (!builderResult) { | |
119 | throw new Error('No available live audio encoder found') | |
120 | } | |
121 | ||
122 | command.outputOption('-map a:0') | |
123 | ||
124 | addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) | |
125 | ||
126 | logger.debug( | |
127 | 'Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, | |
128 | { builderResult, fps: resolutionFPS, resolution, ...lTags() } | |
129 | ) | |
130 | ||
131 | command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`) | |
132 | applyEncoderOptions(command, builderResult.result) | |
1ce4256a C |
133 | |
134 | streamMap.push(`a:${i}`) | |
c729caf6 C |
135 | } |
136 | ||
1ce4256a | 137 | varStreamMap.push(streamMap.join(',')) |
c729caf6 C |
138 | } |
139 | ||
140 | command.complexFilter(complexFilter) | |
141 | ||
f443a746 | 142 | addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode }) |
c729caf6 C |
143 | |
144 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) | |
145 | ||
146 | return command | |
147 | } | |
148 | ||
f443a746 C |
149 | function getLiveMuxingCommand (options: { |
150 | inputUrl: string | |
151 | outPath: string | |
152 | masterPlaylistName: string | |
153 | latencyMode: LiveVideoLatencyMode | |
154 | }) { | |
155 | const { inputUrl, outPath, masterPlaylistName, latencyMode } = options | |
156 | ||
c729caf6 C |
157 | const command = getFFmpeg(inputUrl, 'live') |
158 | ||
159 | command.outputOption('-c:v copy') | |
160 | command.outputOption('-c:a copy') | |
161 | command.outputOption('-map 0:a?') | |
162 | command.outputOption('-map 0:v?') | |
163 | ||
f443a746 | 164 | addDefaultLiveHLSParams({ command, outPath, masterPlaylistName, latencyMode }) |
c729caf6 C |
165 | |
166 | return command | |
167 | } | |
168 | ||
f443a746 C |
169 | function getLiveSegmentTime (latencyMode: LiveVideoLatencyMode) { |
170 | if (latencyMode === LiveVideoLatencyMode.SMALL_LATENCY) { | |
171 | return VIDEO_LIVE.SEGMENT_TIME_SECONDS.SMALL_LATENCY | |
172 | } | |
173 | ||
174 | return VIDEO_LIVE.SEGMENT_TIME_SECONDS.DEFAULT_LATENCY | |
175 | } | |
176 | ||
c729caf6 C |
177 | // --------------------------------------------------------------------------- |
178 | ||
179 | export { | |
f443a746 C |
180 | getLiveSegmentTime, |
181 | ||
c729caf6 C |
182 | getLiveTranscodingCommand, |
183 | getLiveMuxingCommand | |
184 | } | |
185 | ||
186 | // --------------------------------------------------------------------------- | |
187 | ||
f443a746 C |
188 | function addDefaultLiveHLSParams (options: { |
189 | command: FfmpegCommand | |
190 | outPath: string | |
191 | masterPlaylistName: string | |
192 | latencyMode: LiveVideoLatencyMode | |
193 | }) { | |
194 | const { command, outPath, masterPlaylistName, latencyMode } = options | |
195 | ||
196 | command.outputOption('-hls_time ' + getLiveSegmentTime(latencyMode)) | |
c729caf6 | 197 | command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) |
ffcfb32b | 198 | command.outputOption('-hls_flags delete_segments+independent_segments+program_date_time') |
c729caf6 C |
199 | command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`) |
200 | command.outputOption('-master_pl_name ' + masterPlaylistName) | |
201 | command.outputOption(`-f hls`) | |
202 | ||
203 | command.output(join(outPath, '%v.m3u8')) | |
204 | } |