diff options
author | Chocobozzz <me@florianbigard.com> | 2023-04-21 14:55:10 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2023-05-09 08:57:34 +0200 |
commit | 0c9668f77901e7540e2c7045eb0f2974a4842a69 (patch) | |
tree | 226d3dd1565b0bb56588897af3b8530e6216e96b /shared/ffmpeg/ffprobe.ts | |
parent | 6bcb854cdea8688a32240bc5719c7d139806e00b (diff) | |
download | PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.gz PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.tar.zst PeerTube-0c9668f77901e7540e2c7045eb0f2974a4842a69.zip |
Implement remote runner jobs in server
Move ffmpeg functions to @shared
Diffstat (limited to 'shared/ffmpeg/ffprobe.ts')
-rw-r--r-- | shared/ffmpeg/ffprobe.ts | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/shared/ffmpeg/ffprobe.ts b/shared/ffmpeg/ffprobe.ts new file mode 100644 index 000000000..fda08c28e --- /dev/null +++ b/shared/ffmpeg/ffprobe.ts | |||
@@ -0,0 +1,184 @@ | |||
1 | import { ffprobe, FfprobeData } from 'fluent-ffmpeg' | ||
2 | import { forceNumber } from '@shared/core-utils' | ||
3 | import { VideoResolution } from '@shared/models/videos' | ||
4 | |||
5 | /** | ||
6 | * | ||
7 | * Helpers to run ffprobe and extract data from the JSON output | ||
8 | * | ||
9 | */ | ||
10 | |||
11 | function ffprobePromise (path: string) { | ||
12 | return new Promise<FfprobeData>((res, rej) => { | ||
13 | ffprobe(path, (err, data) => { | ||
14 | if (err) return rej(err) | ||
15 | |||
16 | return res(data) | ||
17 | }) | ||
18 | }) | ||
19 | } | ||
20 | |||
21 | // --------------------------------------------------------------------------- | ||
22 | // Audio | ||
23 | // --------------------------------------------------------------------------- | ||
24 | |||
25 | const imageCodecs = new Set([ | ||
26 | 'ansi', 'apng', 'bintext', 'bmp', 'brender_pix', 'dpx', 'exr', 'fits', 'gem', 'gif', 'jpeg2000', 'jpgls', 'mjpeg', 'mjpegb', 'msp2', | ||
27 | 'pam', 'pbm', 'pcx', 'pfm', 'pgm', 'pgmyuv', 'pgx', 'photocd', 'pictor', 'png', 'ppm', 'psd', 'sgi', 'sunrast', 'svg', 'targa', 'tiff', | ||
28 | 'txd', 'webp', 'xbin', 'xbm', 'xface', 'xpm', 'xwd' | ||
29 | ]) | ||
30 | |||
31 | async function isAudioFile (path: string, existingProbe?: FfprobeData) { | ||
32 | const videoStream = await getVideoStream(path, existingProbe) | ||
33 | if (!videoStream) return true | ||
34 | |||
35 | if (imageCodecs.has(videoStream.codec_name)) return true | ||
36 | |||
37 | return false | ||
38 | } | ||
39 | |||
40 | async function hasAudioStream (path: string, existingProbe?: FfprobeData) { | ||
41 | const { audioStream } = await getAudioStream(path, existingProbe) | ||
42 | |||
43 | return !!audioStream | ||
44 | } | ||
45 | |||
46 | async function getAudioStream (videoPath: string, existingProbe?: FfprobeData) { | ||
47 | // without position, ffprobe considers the last input only | ||
48 | // we make it consider the first input only | ||
49 | // if you pass a file path to pos, then ffprobe acts on that file directly | ||
50 | const data = existingProbe || await ffprobePromise(videoPath) | ||
51 | |||
52 | if (Array.isArray(data.streams)) { | ||
53 | const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio') | ||
54 | |||
55 | if (audioStream) { | ||
56 | return { | ||
57 | absolutePath: data.format.filename, | ||
58 | audioStream, | ||
59 | bitrate: forceNumber(audioStream['bit_rate']) | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | |||
64 | return { absolutePath: data.format.filename } | ||
65 | } | ||
66 | |||
67 | function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) { | ||
68 | const maxKBitrate = 384 | ||
69 | const kToBits = (kbits: number) => kbits * 1000 | ||
70 | |||
71 | // If we did not manage to get the bitrate, use an average value | ||
72 | if (!bitrate) return 256 | ||
73 | |||
74 | if (type === 'aac') { | ||
75 | switch (true) { | ||
76 | case bitrate > kToBits(maxKBitrate): | ||
77 | return maxKBitrate | ||
78 | |||
79 | default: | ||
80 | return -1 // we interpret it as a signal to copy the audio stream as is | ||
81 | } | ||
82 | } | ||
83 | |||
84 | /* | ||
85 | a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac. | ||
86 | That's why, when using aac, we can go to lower kbit/sec. The equivalences | ||
87 | made here are not made to be accurate, especially with good mp3 encoders. | ||
88 | */ | ||
89 | switch (true) { | ||
90 | case bitrate <= kToBits(192): | ||
91 | return 128 | ||
92 | |||
93 | case bitrate <= kToBits(384): | ||
94 | return 256 | ||
95 | |||
96 | default: | ||
97 | return maxKBitrate | ||
98 | } | ||
99 | } | ||
100 | |||
101 | // --------------------------------------------------------------------------- | ||
102 | // Video | ||
103 | // --------------------------------------------------------------------------- | ||
104 | |||
105 | async function getVideoStreamDimensionsInfo (path: string, existingProbe?: FfprobeData) { | ||
106 | const videoStream = await getVideoStream(path, existingProbe) | ||
107 | if (!videoStream) { | ||
108 | return { | ||
109 | width: 0, | ||
110 | height: 0, | ||
111 | ratio: 0, | ||
112 | resolution: VideoResolution.H_NOVIDEO, | ||
113 | isPortraitMode: false | ||
114 | } | ||
115 | } | ||
116 | |||
117 | return { | ||
118 | width: videoStream.width, | ||
119 | height: videoStream.height, | ||
120 | ratio: Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width), | ||
121 | resolution: Math.min(videoStream.height, videoStream.width), | ||
122 | isPortraitMode: videoStream.height > videoStream.width | ||
123 | } | ||
124 | } | ||
125 | |||
126 | async function getVideoStreamFPS (path: string, existingProbe?: FfprobeData) { | ||
127 | const videoStream = await getVideoStream(path, existingProbe) | ||
128 | if (!videoStream) return 0 | ||
129 | |||
130 | for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) { | ||
131 | const valuesText: string = videoStream[key] | ||
132 | if (!valuesText) continue | ||
133 | |||
134 | const [ frames, seconds ] = valuesText.split('/') | ||
135 | if (!frames || !seconds) continue | ||
136 | |||
137 | const result = parseInt(frames, 10) / parseInt(seconds, 10) | ||
138 | if (result > 0) return Math.round(result) | ||
139 | } | ||
140 | |||
141 | return 0 | ||
142 | } | ||
143 | |||
144 | async function getVideoStreamBitrate (path: string, existingProbe?: FfprobeData): Promise<number> { | ||
145 | const metadata = existingProbe || await ffprobePromise(path) | ||
146 | |||
147 | let bitrate = metadata.format.bit_rate | ||
148 | if (bitrate && !isNaN(bitrate)) return bitrate | ||
149 | |||
150 | const videoStream = await getVideoStream(path, existingProbe) | ||
151 | if (!videoStream) return undefined | ||
152 | |||
153 | bitrate = forceNumber(videoStream?.bit_rate) | ||
154 | if (bitrate && !isNaN(bitrate)) return bitrate | ||
155 | |||
156 | return undefined | ||
157 | } | ||
158 | |||
159 | async function getVideoStreamDuration (path: string, existingProbe?: FfprobeData) { | ||
160 | const metadata = existingProbe || await ffprobePromise(path) | ||
161 | |||
162 | return Math.round(metadata.format.duration) | ||
163 | } | ||
164 | |||
165 | async function getVideoStream (path: string, existingProbe?: FfprobeData) { | ||
166 | const metadata = existingProbe || await ffprobePromise(path) | ||
167 | |||
168 | return metadata.streams.find(s => s.codec_type === 'video') | ||
169 | } | ||
170 | |||
171 | // --------------------------------------------------------------------------- | ||
172 | |||
173 | export { | ||
174 | getVideoStreamDimensionsInfo, | ||
175 | getMaxAudioBitrate, | ||
176 | getVideoStream, | ||
177 | getVideoStreamDuration, | ||
178 | getAudioStream, | ||
179 | getVideoStreamFPS, | ||
180 | isAudioFile, | ||
181 | ffprobePromise, | ||
182 | getVideoStreamBitrate, | ||
183 | hasAudioStream | ||
184 | } | ||