diff options
Diffstat (limited to 'server/helpers/ffmpeg/ffmpeg-live.ts')
-rw-r--r-- | server/helpers/ffmpeg/ffmpeg-live.ts | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/server/helpers/ffmpeg/ffmpeg-live.ts b/server/helpers/ffmpeg/ffmpeg-live.ts new file mode 100644 index 000000000..ff571626c --- /dev/null +++ b/server/helpers/ffmpeg/ffmpeg-live.ts | |||
@@ -0,0 +1,161 @@ | |||
1 | import { FfmpegCommand, FilterSpecification } from 'fluent-ffmpeg' | ||
2 | import { join } from 'path' | ||
3 | import { VIDEO_LIVE } from '@server/initializers/constants' | ||
4 | import { AvailableEncoders } from '@shared/models' | ||
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 | ||
18 | |||
19 | resolutions: number[] | ||
20 | |||
21 | // Input information | ||
22 | fps: number | ||
23 | bitrate: number | ||
24 | ratio: number | ||
25 | |||
26 | availableEncoders: AvailableEncoders | ||
27 | profile: string | ||
28 | }) { | ||
29 | const { inputUrl, outPath, resolutions, fps, bitrate, availableEncoders, profile, masterPlaylistName, ratio } = options | ||
30 | |||
31 | const command = getFFmpeg(inputUrl, 'live') | ||
32 | |||
33 | const varStreamMap: string[] = [] | ||
34 | |||
35 | const complexFilter: FilterSpecification[] = [ | ||
36 | { | ||
37 | inputs: '[v:0]', | ||
38 | filter: 'split', | ||
39 | options: resolutions.length, | ||
40 | outputs: resolutions.map(r => `vtemp${r}`) | ||
41 | } | ||
42 | ] | ||
43 | |||
44 | command.outputOption('-sc_threshold 0') | ||
45 | |||
46 | addDefaultEncoderGlobalParams(command) | ||
47 | |||
48 | for (let i = 0; i < resolutions.length; i++) { | ||
49 | const resolution = resolutions[i] | ||
50 | const resolutionFPS = computeFPS(fps, resolution) | ||
51 | |||
52 | const baseEncoderBuilderParams = { | ||
53 | input: inputUrl, | ||
54 | |||
55 | availableEncoders, | ||
56 | profile, | ||
57 | |||
58 | canCopyAudio: true, | ||
59 | canCopyVideo: true, | ||
60 | |||
61 | inputBitrate: bitrate, | ||
62 | inputRatio: ratio, | ||
63 | |||
64 | resolution, | ||
65 | fps: resolutionFPS, | ||
66 | |||
67 | streamNum: i, | ||
68 | videoType: 'live' as 'live' | ||
69 | } | ||
70 | |||
71 | { | ||
72 | const streamType: StreamType = 'video' | ||
73 | const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType }) | ||
74 | if (!builderResult) { | ||
75 | throw new Error('No available live video encoder found') | ||
76 | } | ||
77 | |||
78 | command.outputOption(`-map [vout${resolution}]`) | ||
79 | |||
80 | addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) | ||
81 | |||
82 | logger.debug( | ||
83 | 'Apply ffmpeg live video params from %s using %s profile.', builderResult.encoder, profile, | ||
84 | { builderResult, fps: resolutionFPS, resolution, ...lTags() } | ||
85 | ) | ||
86 | |||
87 | command.outputOption(`${buildStreamSuffix('-c:v', i)} ${builderResult.encoder}`) | ||
88 | applyEncoderOptions(command, builderResult.result) | ||
89 | |||
90 | complexFilter.push({ | ||
91 | inputs: `vtemp${resolution}`, | ||
92 | filter: getScaleFilter(builderResult.result), | ||
93 | options: `w=-2:h=${resolution}`, | ||
94 | outputs: `vout${resolution}` | ||
95 | }) | ||
96 | } | ||
97 | |||
98 | { | ||
99 | const streamType: StreamType = 'audio' | ||
100 | const builderResult = await getEncoderBuilderResult({ ...baseEncoderBuilderParams, streamType }) | ||
101 | if (!builderResult) { | ||
102 | throw new Error('No available live audio encoder found') | ||
103 | } | ||
104 | |||
105 | command.outputOption('-map a:0') | ||
106 | |||
107 | addDefaultEncoderParams({ command, encoder: builderResult.encoder, fps: resolutionFPS, streamNum: i }) | ||
108 | |||
109 | logger.debug( | ||
110 | 'Apply ffmpeg live audio params from %s using %s profile.', builderResult.encoder, profile, | ||
111 | { builderResult, fps: resolutionFPS, resolution, ...lTags() } | ||
112 | ) | ||
113 | |||
114 | command.outputOption(`${buildStreamSuffix('-c:a', i)} ${builderResult.encoder}`) | ||
115 | applyEncoderOptions(command, builderResult.result) | ||
116 | } | ||
117 | |||
118 | varStreamMap.push(`v:${i},a:${i}`) | ||
119 | } | ||
120 | |||
121 | command.complexFilter(complexFilter) | ||
122 | |||
123 | addDefaultLiveHLSParams(command, outPath, masterPlaylistName) | ||
124 | |||
125 | command.outputOption('-var_stream_map', varStreamMap.join(' ')) | ||
126 | |||
127 | return command | ||
128 | } | ||
129 | |||
130 | function getLiveMuxingCommand (inputUrl: string, outPath: string, masterPlaylistName: string) { | ||
131 | const command = getFFmpeg(inputUrl, 'live') | ||
132 | |||
133 | command.outputOption('-c:v copy') | ||
134 | command.outputOption('-c:a copy') | ||
135 | command.outputOption('-map 0:a?') | ||
136 | command.outputOption('-map 0:v?') | ||
137 | |||
138 | addDefaultLiveHLSParams(command, outPath, masterPlaylistName) | ||
139 | |||
140 | return command | ||
141 | } | ||
142 | |||
143 | // --------------------------------------------------------------------------- | ||
144 | |||
145 | export { | ||
146 | getLiveTranscodingCommand, | ||
147 | getLiveMuxingCommand | ||
148 | } | ||
149 | |||
150 | // --------------------------------------------------------------------------- | ||
151 | |||
152 | function addDefaultLiveHLSParams (command: FfmpegCommand, outPath: string, masterPlaylistName: string) { | ||
153 | command.outputOption('-hls_time ' + VIDEO_LIVE.SEGMENT_TIME_SECONDS) | ||
154 | command.outputOption('-hls_list_size ' + VIDEO_LIVE.SEGMENTS_LIST_SIZE) | ||
155 | command.outputOption('-hls_flags delete_segments+independent_segments') | ||
156 | command.outputOption(`-hls_segment_filename ${join(outPath, '%v-%06d.ts')}`) | ||
157 | command.outputOption('-master_pl_name ' + masterPlaylistName) | ||
158 | command.outputOption(`-f hls`) | ||
159 | |||
160 | command.output(join(outPath, '%v.m3u8')) | ||
161 | } | ||