]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - shared/extra-utils/ffprobe.ts
Add basic video editor support
[github/Chocobozzz/PeerTube.git] / shared / extra-utils / ffprobe.ts
CommitLineData
06aad801 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
c729caf6
C
20// ---------------------------------------------------------------------------
21// Audio
22// ---------------------------------------------------------------------------
23
482b2623 24async function isAudioFile (path: string, existingProbe?: FfprobeData) {
c729caf6 25 const videoStream = await getVideoStream(path, existingProbe)
482b2623
C
26
27 return !videoStream
28}
29
c729caf6
C
30async function hasAudioStream (path: string, existingProbe?: FfprobeData) {
31 const { audioStream } = await getAudioStream(path, existingProbe)
32
33 return !!audioStream
34}
35
06aad801 36async function getAudioStream (videoPath: string, existingProbe?: FfprobeData) {
37 // without position, ffprobe considers the last input only
38 // we make it consider the first input only
39 // if you pass a file path to pos, then ffprobe acts on that file directly
40 const data = existingProbe || await ffprobePromise(videoPath)
41
42 if (Array.isArray(data.streams)) {
43 const audioStream = data.streams.find(stream => stream['codec_type'] === 'audio')
44
45 if (audioStream) {
46 return {
47 absolutePath: data.format.filename,
48 audioStream,
49 bitrate: parseInt(audioStream['bit_rate'] + '', 10)
50 }
51 }
52 }
53
54 return { absolutePath: data.format.filename }
55}
56
57function getMaxAudioBitrate (type: 'aac' | 'mp3' | string, bitrate: number) {
58 const maxKBitrate = 384
59 const kToBits = (kbits: number) => kbits * 1000
60
61 // If we did not manage to get the bitrate, use an average value
62 if (!bitrate) return 256
63
64 if (type === 'aac') {
65 switch (true) {
66 case bitrate > kToBits(maxKBitrate):
67 return maxKBitrate
68
69 default:
70 return -1 // we interpret it as a signal to copy the audio stream as is
71 }
72 }
73
74 /*
75 a 192kbit/sec mp3 doesn't hold as much information as a 192kbit/sec aac.
76 That's why, when using aac, we can go to lower kbit/sec. The equivalences
77 made here are not made to be accurate, especially with good mp3 encoders.
78 */
79 switch (true) {
80 case bitrate <= kToBits(192):
81 return 128
82
83 case bitrate <= kToBits(384):
84 return 256
85
86 default:
87 return maxKBitrate
88 }
89}
90
c729caf6
C
91// ---------------------------------------------------------------------------
92// Video
93// ---------------------------------------------------------------------------
06aad801 94
c729caf6
C
95async function getVideoStreamDimensionsInfo (path: string, existingProbe?: FfprobeData) {
96 const videoStream = await getVideoStream(path, existingProbe)
97 if (!videoStream) return undefined
06aad801 98
99 return {
c729caf6
C
100 width: videoStream.width,
101 height: videoStream.height,
102 ratio: Math.max(videoStream.height, videoStream.width) / Math.min(videoStream.height, videoStream.width),
103 resolution: Math.min(videoStream.height, videoStream.width),
104 isPortraitMode: videoStream.height > videoStream.width
06aad801 105 }
106}
107
c729caf6
C
108async function getVideoStreamFPS (path: string, existingProbe?: FfprobeData) {
109 const videoStream = await getVideoStream(path, existingProbe)
110 if (!videoStream) return 0
06aad801 111
112 for (const key of [ 'avg_frame_rate', 'r_frame_rate' ]) {
113 const valuesText: string = videoStream[key]
114 if (!valuesText) continue
115
116 const [ frames, seconds ] = valuesText.split('/')
117 if (!frames || !seconds) continue
118
119 const result = parseInt(frames, 10) / parseInt(seconds, 10)
120 if (result > 0) return Math.round(result)
121 }
122
123 return 0
124}
125
c729caf6 126async function buildFileMetadata (path: string, existingProbe?: FfprobeData) {
06aad801 127 const metadata = existingProbe || await ffprobePromise(path)
128
129 return new VideoFileMetadata(metadata)
130}
131
c729caf6
C
132async function getVideoStreamBitrate (path: string, existingProbe?: FfprobeData): Promise<number> {
133 const metadata = await buildFileMetadata(path, existingProbe)
06aad801 134
135 let bitrate = metadata.format.bit_rate as number
136 if (bitrate && !isNaN(bitrate)) return bitrate
137
c729caf6 138 const videoStream = await getVideoStream(path, existingProbe)
06aad801 139 if (!videoStream) return undefined
140
141 bitrate = videoStream?.bit_rate
142 if (bitrate && !isNaN(bitrate)) return bitrate
143
144 return undefined
145}
146
c729caf6
C
147async function getVideoStreamDuration (path: string, existingProbe?: FfprobeData) {
148 const metadata = await buildFileMetadata(path, existingProbe)
06aad801 149
150 return Math.round(metadata.format.duration)
151}
152
c729caf6
C
153async function getVideoStream (path: string, existingProbe?: FfprobeData) {
154 const metadata = await buildFileMetadata(path, existingProbe)
06aad801 155
c729caf6 156 return metadata.streams.find(s => s.codec_type === 'video')
06aad801 157}
158
159// ---------------------------------------------------------------------------
160
161export {
c729caf6
C
162 getVideoStreamDimensionsInfo,
163 buildFileMetadata,
06aad801 164 getMaxAudioBitrate,
c729caf6
C
165 getVideoStream,
166 getVideoStreamDuration,
06aad801 167 getAudioStream,
c729caf6 168 getVideoStreamFPS,
482b2623 169 isAudioFile,
06aad801 170 ffprobePromise,
c729caf6
C
171 getVideoStreamBitrate,
172 hasAudioStream
06aad801 173}