]>
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' | |
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 | } |