aboutsummaryrefslogtreecommitdiffhomepage
path: root/shared/extra-utils/ffprobe.ts
diff options
context:
space:
mode:
Diffstat (limited to 'shared/extra-utils/ffprobe.ts')
-rw-r--r--shared/extra-utils/ffprobe.ts180
1 files changed, 180 insertions, 0 deletions
diff --git a/shared/extra-utils/ffprobe.ts b/shared/extra-utils/ffprobe.ts
new file mode 100644
index 000000000..9257bbd5f
--- /dev/null
+++ b/shared/extra-utils/ffprobe.ts
@@ -0,0 +1,180 @@
1import { ffprobe, FfprobeData } from 'fluent-ffmpeg'
2import { VideoFileMetadata } from '@shared/models/videos'
3
4/**
5 *
6 * Helpers to run ffprobe and extract data from the JSON output
7 *
8 */
9
10function ffprobePromise (path: string) {
11 return new Promise<FfprobeData>((res, rej) => {
12 ffprobe(path, (err, data) => {
13 if (err) return rej(err)
14
15 return res(data)
16 })
17 })
18}
19
20async function getAudioStream (videoPath: string, existingProbe?: FfprobeData) {
21 // without position, ffprobe considers the last input only
22 // we make it consider the first input only
23 // if you pass a file path to pos, then ffprobe acts on that file directly
24 const data = existingProbe || await ffprobePromise(videoPath)
25
26 if (Array.isArray(data.streams)) {
27 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
28
29 if (audioStream) {
30 return {
31 absolutePath: data.format.filename,
32 audioStream,
33 bitrate: parseInt(audioStream['bit_rate'] + '', 10)
34 }
35 }
36 }
37
38 return { absolutePath: data.format.filename }
39}
40
41function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) {
42 const maxKBitrate = 384
43 const kToBits = (kbits: number) => kbits * 1000
44
45 // If we did not manage to get the bitrate, use an average value
46 if (!bitrate) return 256
47
48 if (type === 'aac') {
49 switch (true) {
50 case bitrate > kToBits(maxKBitrate):
51 return maxKBitrate
52
53 default:
54 return -1 // we interpret it as a signal to copy the audio stream as is
55 }
56 }
57
58 /*
59 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
60 That's why, when using aac, we can go to lower kbit/sec. The equivalences
61 made here are not made to be accurate, especially with good mp3 encoders.
62 */
63 switch (true) {
64 case bitrate <= kToBits(192):
65 return 128
66
67 case bitrate <= kToBits(384):
68 return 256
69
70 default:
71 return maxKBitrate
72 }
73}
74
75async function getVideoStreamSize (path: string, existingProbe?: FfprobeData): Promise<{ width: number, height: number }> {
76 const videoStream = await getVideoStreamFromFile(path, existingProbe)
77
78 return videoStream === null
79 ? { width: 0, height: 0 }
80 : { width: videoStream.width, height: videoStream.height }
81}
82
83async function getVideoFileResolution (path: string, existingProbe?: FfprobeData) {
84 const size = await getVideoStreamSize(path, existingProbe)
85
86 return {
87 width: size.width,
88 height: size.height,
89 ratio: Math.max(size.height, size.width) / Math.min(size.height, size.width),
90 resolution: Math.min(size.height, size.width),
91 isPortraitMode: size.height > size.width
92 }
93}
94
95async function getVideoFileFPS (path: string, existingProbe?: FfprobeData) {
96 const videoStream = await getVideoStreamFromFile(path, existingProbe)
97 if (videoStream === null) return 0
98
99 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
100 const valuesText: string = videoStream[key]
101 if (!valuesText) continue
102
103 const [ frames, seconds ] = valuesText.split('/')
104 if (!frames || !seconds) continue
105
106 const result = parseInt(frames, 10) / parseInt(seconds, 10)
107 if (result > 0) return Math.round(result)
108 }
109
110 return 0
111}
112
113async function getMetadataFromFile (path: string, existingProbe?: FfprobeData) {
114 const metadata = existingProbe || await ffprobePromise(path)
115
116 return new VideoFileMetadata(metadata)
117}
118
119async function getVideoFileBitrate (path: string, existingProbe?: FfprobeData): Promise<number> {
120 const metadata = await getMetadataFromFile(path, existingProbe)
121
122 let bitrate = metadata.format.bit_rate as number
123 if (bitrate && !isNaN(bitrate)) return bitrate
124
125 const videoStream = await getVideoStreamFromFile(path, existingProbe)
126 if (!videoStream) return undefined
127
128 bitrate = videoStream?.bit_rate
129 if (bitrate && !isNaN(bitrate)) return bitrate
130
131 return undefined
132}
133
134async function getDurationFromVideoFile (path: string, existingProbe?: FfprobeData) {
135 const metadata = await getMetadataFromFile(path, existingProbe)
136
137 return Math.round(metadata.format.duration)
138}
139
140async function getVideoStreamFromFile (path: string, existingProbe?: FfprobeData) {
141 const metadata = await getMetadataFromFile(path, existingProbe)
142
143 return metadata.streams.find(s => s.codec_type === 'video') || null
144}
145
146async function canDoQuickAudioTranscode (path: string, probe?: FfprobeData): Promise<boolean> {
147 const parsedAudio = await getAudioStream(path, probe)
148
149 if (!parsedAudio.audioStream) return true
150
151 if (parsedAudio.audioStream['codec_name'] !== 'aac') return false
152
153 const audioBitrate = parsedAudio.bitrate
154 if (!audioBitrate) return false
155
156 const maxAudioBitrate = getMaxAudioBitrate('aac', audioBitrate)
157 if (maxAudioBitrate !== -1 && audioBitrate > maxAudioBitrate) return false
158
159 const channelLayout = parsedAudio.audioStream['channel_layout']
160 // Causes playback issues with Chrome
161 if (!channelLayout || channelLayout === 'unknown') return false
162
163 return true
164}
165
166// ---------------------------------------------------------------------------
167
168export {
169 getVideoStreamSize,
170 getVideoFileResolution,
171 getMetadataFromFile,
172 getMaxAudioBitrate,
173 getVideoStreamFromFile,
174 getDurationFromVideoFile,
175 getAudioStream,
176 getVideoFileFPS,
177 ffprobePromise,
178 getVideoFileBitrate,
179 canDoQuickAudioTranscode
180}