1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.inspection.ffmpeg;
22
23 import org.opencastproject.inspection.ffmpeg.api.AudioStreamMetadata;
24 import org.opencastproject.inspection.ffmpeg.api.MediaAnalyzer;
25 import org.opencastproject.inspection.ffmpeg.api.MediaAnalyzerException;
26 import org.opencastproject.inspection.ffmpeg.api.MediaContainerMetadata;
27 import org.opencastproject.inspection.ffmpeg.api.SubtitleStreamMetadata;
28 import org.opencastproject.inspection.ffmpeg.api.VideoStreamMetadata;
29 import org.opencastproject.util.IoSupport;
30
31 import org.json.simple.JSONArray;
32 import org.json.simple.JSONObject;
33 import org.json.simple.parser.JSONParser;
34 import org.json.simple.parser.ParseException;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37
38 import java.io.BufferedReader;
39 import java.io.File;
40 import java.io.IOException;
41 import java.io.InputStreamReader;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.Map;
45
46
47
48
49
50
51 public class FFmpegAnalyzer implements MediaAnalyzer {
52
53
54 protected String binary;
55
56 public static final String FFPROBE_BINARY_CONFIG = "org.opencastproject.inspection.ffprobe.path";
57 public static final String FFPROBE_BINARY_DEFAULT = "ffprobe";
58
59
60 private static final Logger logger = LoggerFactory.getLogger(FFmpegAnalyzer.class);
61
62
63 private boolean accurateFrameCount;
64
65 public FFmpegAnalyzer(boolean accurateFrameCount) {
66 this.accurateFrameCount = accurateFrameCount;
67
68 this.binary = FFPROBE_BINARY_DEFAULT;
69 }
70
71
72
73
74
75
76 protected String getBinary() {
77 return binary;
78 }
79
80 public void setBinary(String binary) {
81 this.binary = binary;
82 }
83
84 @Override
85 public MediaContainerMetadata analyze(File media) throws MediaAnalyzerException {
86 if (binary == null) {
87 throw new IllegalStateException("Binary is not set");
88 }
89
90 List<String> command = new ArrayList<>();
91 command.add(binary);
92 command.add("-show_format");
93 command.add("-show_streams");
94 if (accurateFrameCount) {
95 command.add("-count_frames");
96 }
97 command.add("-of");
98 command.add("json");
99 command.add(media.getAbsolutePath().replaceAll(" ", "\\ "));
100
101
102 logger.debug("Running {} {}", binary, command);
103
104 MediaContainerMetadata metadata = new MediaContainerMetadata();
105
106 final StringBuilder sb = new StringBuilder();
107 Process encoderProcess = null;
108 try {
109 encoderProcess = new ProcessBuilder(command)
110 .redirectError(ProcessBuilder.Redirect.DISCARD)
111 .start();
112
113
114 try (var in = new BufferedReader(new InputStreamReader(encoderProcess.getInputStream()))) {
115 String line;
116 while ((line = in.readLine()) != null) {
117 logger.debug(line);
118 sb.append(line).append(System.getProperty("line.separator"));
119 }
120 }
121
122 int exitCode = encoderProcess.waitFor();
123 if (exitCode != 0) {
124 throw new MediaAnalyzerException("Frame analyzer " + binary + " exited with code " + exitCode);
125 }
126 } catch (IOException | InterruptedException e) {
127 logger.error("Error executing ffprobe", e);
128 throw new MediaAnalyzerException("Error while running " + binary, e);
129 } finally {
130 IoSupport.closeQuietly(encoderProcess);
131 }
132
133 JSONParser parser = new JSONParser();
134
135 try {
136 JSONObject jsonObject = (JSONObject) parser.parse(sb.toString());
137 Object obj;
138 Double duration;
139
140
141 JSONObject jsonFormat = (JSONObject) jsonObject.get("format");
142
143
144 obj = jsonFormat.get("filename");
145 if (obj != null) {
146 metadata.setFileName((String) obj);
147 }
148
149
150 obj = jsonFormat.get("format_long_name");
151 if (obj != null) {
152 metadata.setFormat((String) obj);
153 }
154
155
156
157
158
159 obj = jsonFormat.get("nb_streams");
160 if (obj != null && (Long) obj > 0) {
161 obj = jsonFormat.get("duration");
162 if (obj != null) {
163 duration = Double.parseDouble((String) obj) * 1000;
164 metadata.setDuration(duration.longValue());
165 }
166 }
167
168
169 obj = jsonFormat.get("size");
170 if (obj != null) {
171 metadata.setSize(Long.parseLong((String) obj));
172 }
173
174
175 obj = jsonFormat.get("bit_rate");
176 if (obj != null) {
177 metadata.setBitRate(Float.parseFloat((String) obj));
178 }
179
180
181
182
183
184 JSONArray streams = (JSONArray) jsonObject.get("streams");
185 for (JSONObject stream : (Iterable<JSONObject>) streams) {
186
187 String codecType = (String) stream.get("codec_type");
188
189
190
191 if ("audio".equals(codecType)) {
192
193 AudioStreamMetadata aMetadata = new AudioStreamMetadata();
194
195
196 obj = stream.get("codec_long_name");
197 if (obj != null) {
198 aMetadata.setFormat((String) obj);
199 }
200
201
202 obj = stream.get("duration");
203 if (obj != null) {
204 duration = new Double((String) obj) * 1000;
205 aMetadata.setDuration(duration.longValue());
206 } else {
207
208
209
210 aMetadata.setDuration(metadata.getDuration());
211 }
212
213
214 obj = stream.get("bit_rate");
215 if (obj != null) {
216 aMetadata.setBitRate(new Float((String) obj));
217 }
218
219
220 obj = stream.get("channels");
221 if (obj != null) {
222 aMetadata.setChannels(((Long) obj).intValue());
223 }
224
225
226 obj = stream.get("sample_rate");
227 if (obj != null) {
228 aMetadata.setSamplingRate(Integer.parseInt((String) obj));
229 }
230
231
232 obj = stream.get("nb_read_frames");
233 if (obj != null) {
234 aMetadata.setFrames(Long.parseLong((String) obj));
235 } else {
236
237
238 obj = stream.get("nb_frames");
239 if (obj != null) {
240 aMetadata.setFrames(Long.parseLong((String) obj));
241 }
242 }
243
244
245 metadata.getAudioStreamMetadata().add(aMetadata);
246
247
248
249 } else if ("video".equals(codecType)) {
250
251 VideoStreamMetadata vMetadata = new VideoStreamMetadata();
252
253
254 obj = stream.get("codec_long_name");
255 if (obj != null) {
256 vMetadata.setFormat((String) obj);
257 }
258
259
260 obj = stream.get("duration");
261 if (obj != null) {
262 duration = new Double((String) obj) * 1000;
263 vMetadata.setDuration(duration.longValue());
264 } else {
265
266
267
268 vMetadata.setDuration(metadata.getDuration());
269 }
270
271
272 obj = stream.get("bit_rate");
273 if (obj != null) {
274 vMetadata.setBitRate(new Float((String) obj));
275 }
276
277
278 obj = stream.get("width");
279 if (obj != null) {
280 vMetadata.setFrameWidth(((Long) obj).intValue());
281 }
282
283
284 obj = stream.get("height");
285 if (obj != null) {
286 vMetadata.setFrameHeight(((Long) obj).intValue());
287 }
288
289
290 obj = stream.get("profile");
291 if (obj != null) {
292 vMetadata.setFormatProfile((String) obj);
293 }
294
295
296 obj = stream.get("sample_aspect_ratio");
297 if (obj != null) {
298 vMetadata.setPixelAspectRatio(parseFloat((String) obj));
299 }
300
301
302 obj = stream.get("avg_frame_rate");
303 if (obj != null) {
304 vMetadata.setFrameRate(parseFloat((String) obj));
305 }
306
307
308 obj = stream.get("nb_read_frames");
309 if (obj != null) {
310 vMetadata.setFrames(Long.parseLong((String) obj));
311 } else {
312
313
314 obj = stream.get("nb_frames");
315 if (obj != null) {
316 vMetadata.setFrames(Long.parseLong((String) obj));
317 } else if (vMetadata.getDuration() != null && vMetadata.getFrameRate() != null) {
318 long framesEstimation = Double.valueOf(vMetadata.getDuration() / 1000.0 * vMetadata.getFrameRate())
319 .longValue();
320 if (framesEstimation >= 1) {
321 vMetadata.setFrames(framesEstimation);
322 }
323 }
324 }
325
326
327 metadata.getVideoStreamMetadata().add(vMetadata);
328
329
330
331 } else if ("subtitle".equals(codecType)) {
332
333 SubtitleStreamMetadata sMetadata = new SubtitleStreamMetadata();
334
335
336 obj = stream.get("codec_long_name");
337 if (obj != null) {
338 sMetadata.setFormat((String) obj);
339 }
340
341 metadata.getSubtitleStreamMetadata().add(sMetadata);
342 }
343 }
344
345 } catch (ParseException e) {
346 logger.error("Error parsing ffprobe output: {}", e.getMessage());
347 }
348
349 return metadata;
350 }
351
352
353
354
355
356
357 @Override
358 public void setConfig(Map<String, Object> config) {
359 if (config != null) {
360 if (config.containsKey(FFPROBE_BINARY_CONFIG)) {
361 String binary = (String) config.get(FFPROBE_BINARY_CONFIG);
362 setBinary(binary);
363 logger.debug("FFmpegAnalyzer config binary: " + binary);
364 }
365 }
366 }
367
368 private float parseFloat(String val) {
369 if (val.contains("/")) {
370 String[] v = val.split("/");
371 if (Float.parseFloat(v[1]) == 0) {
372 return 0;
373 } else {
374 return Float.parseFloat(v[0]) / Float.parseFloat(v[1]);
375 }
376 } else if (val.contains(":")) {
377 String[] v = val.split(":");
378 if (Float.parseFloat(v[1]) == 0) {
379 return 0;
380 } else {
381 return Float.parseFloat(v[0]) / Float.parseFloat(v[1]);
382 }
383 } else {
384 return Float.parseFloat(val);
385 }
386 }
387
388 }