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 command.add("-of");
97 command.add("json");
98 command.add(media.getAbsolutePath().replaceAll(" ", "\\ "));
99
100
101 logger.debug("Running {} {}", binary, command);
102
103 MediaContainerMetadata metadata = new MediaContainerMetadata();
104
105 final StringBuilder sb = new StringBuilder();
106 Process encoderProcess = null;
107 try {
108 encoderProcess = new ProcessBuilder(command)
109 .redirectError(ProcessBuilder.Redirect.DISCARD)
110 .start();
111
112
113 try (var in = new BufferedReader(new InputStreamReader(encoderProcess.getInputStream()))) {
114 String line;
115 while ((line = in.readLine()) != null) {
116 logger.debug(line);
117 sb.append(line).append(System.getProperty("line.separator"));
118 }
119 }
120
121 int exitCode = encoderProcess.waitFor();
122 if (exitCode != 0) {
123 throw new MediaAnalyzerException("Frame analyzer " + binary + " exited with code " + exitCode);
124 }
125 } catch (IOException | InterruptedException e) {
126 logger.error("Error executing ffprobe", e);
127 throw new MediaAnalyzerException("Error while running " + binary, e);
128 } finally {
129 IoSupport.closeQuietly(encoderProcess);
130 }
131
132 JSONParser parser = new JSONParser();
133
134 try {
135 JSONObject jsonObject = (JSONObject) parser.parse(sb.toString());
136 Object obj;
137 Double duration;
138
139
140 JSONObject jsonFormat = (JSONObject) jsonObject.get("format");
141
142
143 obj = jsonFormat.get("filename");
144 if (obj != null) {
145 metadata.setFileName((String) obj);
146 }
147
148
149 obj = jsonFormat.get("format_long_name");
150 if (obj != null) {
151 metadata.setFormat((String) obj);
152 }
153
154
155
156
157
158 obj = jsonFormat.get("nb_streams");
159 if (obj != null && (Long) obj > 0) {
160 obj = jsonFormat.get("duration");
161 if (obj != null) {
162 duration = Double.parseDouble((String) obj) * 1000;
163 metadata.setDuration(duration.longValue());
164 }
165 }
166
167
168 obj = jsonFormat.get("size");
169 if (obj != null) {
170 metadata.setSize(Long.parseLong((String) obj));
171 }
172
173
174 obj = jsonFormat.get("bit_rate");
175 if (obj != null) {
176 metadata.setBitRate(Float.parseFloat((String) obj));
177 }
178
179
180
181
182
183 JSONArray streams = (JSONArray) jsonObject.get("streams");
184 for (JSONObject stream : (Iterable<JSONObject>) streams) {
185
186 String codecType = (String) stream.get("codec_type");
187
188
189
190 if ("audio".equals(codecType)) {
191
192 AudioStreamMetadata aMetadata = new AudioStreamMetadata();
193
194
195 obj = stream.get("codec_long_name");
196 if (obj != null) {
197 aMetadata.setFormat((String) obj);
198 }
199
200
201 obj = stream.get("duration");
202 if (obj != null) {
203 duration = new Double((String) obj) * 1000;
204 aMetadata.setDuration(duration.longValue());
205 } else {
206
207
208
209 aMetadata.setDuration(metadata.getDuration());
210 }
211
212
213 obj = stream.get("bit_rate");
214 if (obj != null) {
215 aMetadata.setBitRate(new Float((String) obj));
216 }
217
218
219 obj = stream.get("channels");
220 if (obj != null) {
221 aMetadata.setChannels(((Long) obj).intValue());
222 }
223
224
225 obj = stream.get("sample_rate");
226 if (obj != null) {
227 aMetadata.setSamplingRate(Integer.parseInt((String) obj));
228 }
229
230
231 obj = stream.get("nb_read_frames");
232 if (obj != null) {
233 aMetadata.setFrames(Long.parseLong((String) obj));
234 } else {
235
236
237 obj = stream.get("nb_frames");
238 if (obj != null) {
239 aMetadata.setFrames(Long.parseLong((String) obj));
240 }
241 }
242
243
244 metadata.getAudioStreamMetadata().add(aMetadata);
245
246
247
248 } else if ("video".equals(codecType)) {
249
250 VideoStreamMetadata vMetadata = new VideoStreamMetadata();
251
252
253 obj = stream.get("codec_long_name");
254 if (obj != null) {
255 vMetadata.setFormat((String) obj);
256 }
257
258
259 obj = stream.get("duration");
260 if (obj != null) {
261 duration = new Double((String) obj) * 1000;
262 vMetadata.setDuration(duration.longValue());
263 } else {
264
265
266
267 vMetadata.setDuration(metadata.getDuration());
268 }
269
270
271 obj = stream.get("bit_rate");
272 if (obj != null) {
273 vMetadata.setBitRate(new Float((String) obj));
274 }
275
276
277 obj = stream.get("width");
278 if (obj != null) {
279 vMetadata.setFrameWidth(((Long) obj).intValue());
280 }
281
282
283 obj = stream.get("height");
284 if (obj != null) {
285 vMetadata.setFrameHeight(((Long) obj).intValue());
286 }
287
288
289 obj = stream.get("profile");
290 if (obj != null) {
291 vMetadata.setFormatProfile((String) obj);
292 }
293
294
295 obj = stream.get("sample_aspect_ratio");
296 if (obj != null) {
297 vMetadata.setPixelAspectRatio(parseFloat((String) obj));
298 }
299
300
301 obj = stream.get("avg_frame_rate");
302 if (obj != null) {
303 vMetadata.setFrameRate(parseFloat((String) obj));
304 }
305
306
307 obj = stream.get("nb_read_frames");
308 if (obj != null) {
309 vMetadata.setFrames(Long.parseLong((String) obj));
310 } else {
311
312
313 obj = stream.get("nb_frames");
314 if (obj != null) {
315 vMetadata.setFrames(Long.parseLong((String) obj));
316 } else if (vMetadata.getDuration() != null && vMetadata.getFrameRate() != null) {
317 long framesEstimation = Double.valueOf(vMetadata.getDuration() / 1000.0 * vMetadata.getFrameRate())
318 .longValue();
319 if (framesEstimation >= 1) {
320 vMetadata.setFrames(framesEstimation);
321 }
322 }
323 }
324
325
326 metadata.getVideoStreamMetadata().add(vMetadata);
327
328
329
330 } else if ("subtitle".equals(codecType)) {
331
332 SubtitleStreamMetadata sMetadata = new SubtitleStreamMetadata();
333
334
335 obj = stream.get("codec_long_name");
336 if (obj != null) {
337 sMetadata.setFormat((String) obj);
338 }
339
340 metadata.getSubtitleStreamMetadata().add(sMetadata);
341 }
342 }
343
344 } catch (ParseException e) {
345 logger.error("Error parsing ffprobe output: {}", e.getMessage());
346 }
347
348 return metadata;
349 }
350
351
352
353
354
355
356 @Override
357 public void setConfig(Map<String, Object> config) {
358 if (config != null) {
359 if (config.containsKey(FFPROBE_BINARY_CONFIG)) {
360 String binary = (String) config.get(FFPROBE_BINARY_CONFIG);
361 setBinary(binary);
362 logger.debug("FFmpegAnalyzer config binary: " + binary);
363 }
364 }
365 }
366
367 private float parseFloat(String val) {
368 if (val.contains("/")) {
369 String[] v = val.split("/");
370 if (Float.parseFloat(v[1]) == 0) {
371 return 0;
372 } else {
373 return Float.parseFloat(v[0]) / Float.parseFloat(v[1]);
374 }
375 } else if (val.contains(":")) {
376 String[] v = val.split(":");
377 if (Float.parseFloat(v[1]) == 0) {
378 return 0;
379 } else {
380 return Float.parseFloat(v[0]) / Float.parseFloat(v[1]);
381 }
382 } else {
383 return Float.parseFloat(val);
384 }
385 }
386
387 }