1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.inspection.ffmpeg;
23
24 import static org.opencastproject.inspection.api.MediaInspectionOptions.OPTION_ACCURATE_FRAME_COUNT;
25 import static org.opencastproject.util.data.Collections.map;
26
27 import org.opencastproject.inspection.api.MediaInspectionException;
28 import org.opencastproject.inspection.ffmpeg.api.AudioStreamMetadata;
29 import org.opencastproject.inspection.ffmpeg.api.MediaAnalyzer;
30 import org.opencastproject.inspection.ffmpeg.api.MediaAnalyzerException;
31 import org.opencastproject.inspection.ffmpeg.api.MediaContainerMetadata;
32 import org.opencastproject.inspection.ffmpeg.api.SubtitleStreamMetadata;
33 import org.opencastproject.inspection.ffmpeg.api.VideoStreamMetadata;
34 import org.opencastproject.mediapackage.AdaptivePlaylist;
35 import org.opencastproject.mediapackage.MediaPackageElement;
36 import org.opencastproject.mediapackage.MediaPackageElementBuilder;
37 import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
38 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
39 import org.opencastproject.mediapackage.Stream;
40 import org.opencastproject.mediapackage.Track;
41 import org.opencastproject.mediapackage.UnsupportedElementException;
42 import org.opencastproject.mediapackage.track.AudioStreamImpl;
43 import org.opencastproject.mediapackage.track.SubtitleStreamImpl;
44 import org.opencastproject.mediapackage.track.TrackImpl;
45 import org.opencastproject.mediapackage.track.VideoStreamImpl;
46 import org.opencastproject.util.Checksum;
47 import org.opencastproject.util.ChecksumType;
48 import org.opencastproject.util.MimeType;
49 import org.opencastproject.util.MimeTypes;
50 import org.opencastproject.util.NotFoundException;
51 import org.opencastproject.util.UnknownFileTypeException;
52 import org.opencastproject.util.data.Tuple;
53 import org.opencastproject.workspace.api.Workspace;
54
55 import org.apache.commons.io.FilenameUtils;
56 import org.apache.commons.lang3.BooleanUtils;
57 import org.slf4j.Logger;
58 import org.slf4j.LoggerFactory;
59
60 import java.io.File;
61 import java.io.IOException;
62 import java.net.URI;
63 import java.util.Dictionary;
64 import java.util.Hashtable;
65 import java.util.List;
66 import java.util.Map;
67 import java.util.Map.Entry;
68
69
70
71
72
73 public class MediaInspector {
74
75 private static final Logger logger = LoggerFactory.getLogger(MediaInspector.class);
76
77 private final Workspace workspace;
78 private final String ffprobePath;
79
80 public MediaInspector(Workspace workspace, String ffprobePath) {
81 this.workspace = workspace;
82 this.ffprobePath = ffprobePath;
83 }
84
85
86
87
88
89
90
91
92
93
94 public Track inspectTrack(URI trackURI, Map<String, String> options) throws MediaInspectionException {
95 logger.debug("inspect(" + trackURI + ") called, using workspace " + workspace);
96 throwExceptionIfInvalid(options);
97
98 try {
99
100 File file = null;
101 try {
102 file = workspace.get(trackURI);
103 } catch (NotFoundException notFound) {
104 throw new MediaInspectionException("Unable to find resource " + trackURI, notFound);
105 } catch (IOException ioe) {
106 throw new MediaInspectionException("Error reading " + trackURI + " from workspace", ioe);
107 }
108
109
110
111 if ("".equals(FilenameUtils.getExtension(file.getName()))) {
112 throw new MediaInspectionException("Can not inspect files without a filename extension");
113 }
114
115 MediaContainerMetadata metadata = getFileMetadata(file, getAccurateFrameCount(options));
116 if (metadata == null) {
117 throw new MediaInspectionException("Media analyzer returned no metadata from " + file);
118 } else {
119 MediaPackageElementBuilder elementBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
120 TrackImpl track;
121 MediaPackageElement element;
122 try {
123 element = elementBuilder.elementFromURI(trackURI, MediaPackageElement.Type.Track, null);
124 } catch (UnsupportedElementException e) {
125 throw new MediaInspectionException("Unable to create track element from " + file, e);
126 }
127 track = (TrackImpl) element;
128
129
130 if (metadata.getDuration() != null && metadata.getDuration() > 0)
131 track.setDuration(metadata.getDuration());
132
133
134 try {
135 track.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, file));
136 } catch (IOException e) {
137 throw new MediaInspectionException("Unable to read " + file, e);
138 }
139
140
141 track.setMimeType(metadata.getMimeType());
142 track.setMaster(metadata.getAdaptiveMaster());
143
144
145 try {
146 addAudioStreamMetadata(track, metadata);
147 } catch (Exception e) {
148 throw new MediaInspectionException("Unable to extract audio metadata from " + file, e);
149 }
150
151
152 try {
153 addVideoStreamMetadata(track, metadata);
154 } catch (Exception e) {
155 throw new MediaInspectionException("Unable to extract video metadata from " + file, e);
156 }
157
158
159 try {
160 addSubtitleStreamMetadata(track, metadata);
161 } catch (Exception e) {
162 throw new MediaInspectionException("Unable to extract subtitle metadata from " + file, e);
163 }
164
165
166 track.setSize(file.length());
167
168 return track;
169 }
170 } catch (Exception e) {
171 logger.warn("Error inspecting " + trackURI, e);
172 if (e instanceof MediaInspectionException) {
173 throw (MediaInspectionException) e;
174 } else {
175 throw new MediaInspectionException(e);
176 }
177 }
178 }
179
180
181
182
183
184
185
186
187
188
189
190
191 public MediaPackageElement enrich(MediaPackageElement element, boolean override, final Map<String, String> options)
192 throws MediaInspectionException {
193 throwExceptionIfInvalid(options);
194 if (element instanceof Track) {
195 final Track originalTrack = (Track) element;
196 return enrichTrack(originalTrack, override, options);
197 } else {
198 return enrichElement(element, override, options);
199 }
200 }
201
202
203
204
205
206
207
208
209
210
211
212 private MediaPackageElement enrichTrack(final Track originalTrack, final boolean override, final Map<String, String> options)
213 throws MediaInspectionException {
214 try {
215 URI originalTrackUrl = originalTrack.getURI();
216 MediaPackageElementFlavor flavor = originalTrack.getFlavor();
217 logger.debug("enrich(" + originalTrackUrl + ") called");
218
219
220 File file = null;
221 try {
222 file = workspace.get(originalTrackUrl);
223 } catch (NotFoundException e) {
224 throw new MediaInspectionException("File " + originalTrackUrl + " was not found and can therefore not be "
225 + "inspected", e);
226 } catch (IOException e) {
227 throw new MediaInspectionException("Error accessing " + originalTrackUrl, e);
228 }
229
230
231
232 if ("".equals(FilenameUtils.getExtension(file.getName()))) {
233 throw new MediaInspectionException("Can not inspect files without a filename extension");
234 }
235
236 MediaContainerMetadata metadata = getFileMetadata(file, getAccurateFrameCount(options));
237 if (metadata == null) {
238 throw new MediaInspectionException("Unable to acquire media metadata for " + originalTrackUrl);
239 } else {
240 TrackImpl track = null;
241 try {
242 track = (TrackImpl) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
243 .elementFromURI(originalTrackUrl, MediaPackageElement.Type.Track, flavor);
244 } catch (UnsupportedElementException e) {
245 throw new MediaInspectionException("Unable to create track element from " + file, e);
246 }
247
248
249 track.setChecksum(originalTrack.getChecksum());
250 track.setDuration(originalTrack.getDuration());
251 track.setElementDescription(originalTrack.getElementDescription());
252 track.setFlavor(flavor);
253 track.setIdentifier(originalTrack.getIdentifier());
254
255 if (!originalTrack.hasMaster() || override)
256 track.setMaster(metadata.getAdaptiveMaster());
257 else
258 track.setMaster(originalTrack.isMaster());
259 track.setMimeType(originalTrack.getMimeType());
260 track.setReference(originalTrack.getReference());
261 track.setSize(file.length());
262 track.setURI(originalTrackUrl);
263 for (String tag : originalTrack.getTags()) {
264 track.addTag(tag);
265 }
266
267
268 if (track.getDuration() == null || override)
269 track.setDuration(metadata.getDuration());
270 if (track.getChecksum() == null || override) {
271 try {
272 track.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, file));
273 } catch (IOException e) {
274 throw new MediaInspectionException("Unable to read " + file, e);
275 }
276 }
277
278
279 if (track.getMimeType() == null || override) {
280 track.setMimeType(metadata.getMimeType());
281 }
282
283
284 Dictionary<String, Stream> streamsId2Stream = new Hashtable<String, Stream>();
285 for (Stream stream : originalTrack.getStreams()) {
286 streamsId2Stream.put(stream.getIdentifier(), stream);
287 }
288
289
290 try {
291 addAudioStreamMetadata(track, metadata);
292 } catch (Exception e) {
293 throw new MediaInspectionException("Unable to extract audio metadata from " + file, e);
294 }
295
296
297 try {
298 addVideoStreamMetadata(track, metadata);
299 } catch (Exception e) {
300 throw new MediaInspectionException("Unable to extract video metadata from " + file, e);
301 }
302
303
304 try {
305 addSubtitleStreamMetadata(track, metadata);
306 } catch (Exception e) {
307 throw new MediaInspectionException("Unable to extract subtitle metadata from " + file, e);
308 }
309
310 logger.info("Successfully inspected track {}", track);
311 return track;
312 }
313 } catch (Exception e) {
314 logger.warn("Error enriching track " + originalTrack, e);
315 if (e instanceof MediaInspectionException) {
316 throw (MediaInspectionException) e;
317 } else {
318 throw new MediaInspectionException(e);
319 }
320 }
321 }
322
323
324
325
326
327
328
329
330
331
332
333
334
335 private MediaPackageElement enrichElement(final MediaPackageElement element, final boolean override,
336 final Map<String, String> options) throws MediaInspectionException {
337 try {
338 File file;
339 try {
340 file = workspace.get(element.getURI());
341 } catch (NotFoundException e) {
342 throw new MediaInspectionException("Unable to find " + element.getURI() + " in the workspace", e);
343 } catch (IOException e) {
344 throw new MediaInspectionException("Error accessing " + element.getURI() + " in the workspace", e);
345 }
346
347
348 if (element.getChecksum() == null || override) {
349 try {
350 element.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, file));
351 } catch (IOException e) {
352 throw new MediaInspectionException("Error generating checksum for " + element.getURI(), e);
353 }
354 }
355
356
357 if (element.getMimeType() == null || override) {
358 try {
359 element.setMimeType(MimeTypes.fromString(file.getPath()));
360 } catch (UnknownFileTypeException e) {
361 logger.info("unable to determine the mime type for {}", file.getName());
362 }
363 }
364
365 logger.info("Successfully inspected element {}", element);
366
367 return element;
368 } catch (Exception e) {
369 logger.warn("Error enriching element " + element, e);
370 if (e instanceof MediaInspectionException) {
371 throw (MediaInspectionException) e;
372 } else {
373 throw new MediaInspectionException(e);
374 }
375 }
376 }
377
378
379
380
381
382
383
384
385
386
387 private MediaContainerMetadata getFileMetadata(File file, boolean accurateFrameCount) throws MediaInspectionException {
388 if (file == null)
389 throw new IllegalArgumentException("file to analyze cannot be null");
390 try {
391 MediaAnalyzer analyzer = new FFmpegAnalyzer(accurateFrameCount);
392 analyzer.setConfig(map(Tuple.<String, Object> tuple(FFmpegAnalyzer.FFPROBE_BINARY_CONFIG, ffprobePath)));
393
394 MediaContainerMetadata metadata = analyzer.analyze(file);
395
396 try {
397
398 MimeType mimeType = MimeTypes.fromString(file.getName());
399
400
401
402 if (metadata.hasVideoStreamMetadata()) {
403 if (!"video".equals(mimeType.getType()) && !"application".equals(mimeType.getType())) {
404 mimeType = MimeTypes.parseMimeType("video/" + mimeType.getSubtype());
405 }
406 } else if (metadata.hasAudioStreamMetadata()) {
407 if (!"audio".equals(mimeType.getType()) && !"application".equals(mimeType.getType())) {
408 mimeType = MimeTypes.parseMimeType("audio/" + mimeType.getSubtype());
409 }
410 } else if (metadata.hasSubtitleStreamMetadata()) {
411 if (!"text".equals(mimeType.getType()) && !"application".equals(mimeType.getType())) {
412 mimeType = MimeTypes.parseMimeType("text/" + mimeType.getSubtype());
413 }
414 }
415 metadata.setMimeType(mimeType);
416 } catch (UnknownFileTypeException e) {
417 logger.error("parsing mimeType failed for {} : {}", file, e.getMessage());
418 throw new MediaAnalyzerException("parsing mimetype failed for file" + file);
419 }
420
421
422
423 try {
424 metadata.setAdaptiveMaster(AdaptivePlaylist.checkForMaster(file));
425 } catch (IOException e) {
426 logger.error("parsing adaptive playlist failed for {} : {}", file, e.getMessage());
427 throw new MediaAnalyzerException("parsing for adaptive playlist master failed for file" + file);
428 }
429 return metadata;
430 } catch (MediaAnalyzerException e) {
431 throw new MediaInspectionException(e);
432 }
433 }
434
435
436
437
438
439
440
441
442
443
444
445
446 private Track addVideoStreamMetadata(TrackImpl track, MediaContainerMetadata metadata) throws Exception {
447 List<VideoStreamMetadata> videoList = metadata.getVideoStreamMetadata();
448 if (videoList != null && !videoList.isEmpty()) {
449 for (int i = 0; i < videoList.size(); i++) {
450 VideoStreamImpl video = new VideoStreamImpl("video-" + (i + 1));
451 VideoStreamMetadata v = videoList.get(i);
452 video.setBitRate(v.getBitRate());
453 video.setFormat(v.getFormat());
454 video.setFormatVersion(v.getFormatVersion());
455 video.setFrameCount(v.getFrames());
456 video.setFrameHeight(v.getFrameHeight());
457 video.setFrameRate(v.getFrameRate());
458 video.setFrameWidth(v.getFrameWidth());
459 video.setScanOrder(v.getScanOrder());
460 video.setScanType(v.getScanType());
461
462 track.addStream(video);
463 }
464 }
465 return track;
466 }
467
468
469
470
471
472
473
474
475
476
477
478
479 private Track addAudioStreamMetadata(TrackImpl track, MediaContainerMetadata metadata) throws Exception {
480 List<AudioStreamMetadata> audioList = metadata.getAudioStreamMetadata();
481 if (audioList != null && !audioList.isEmpty()) {
482 for (int i = 0; i < audioList.size(); i++) {
483 AudioStreamImpl audio = new AudioStreamImpl("audio-" + (i + 1));
484 AudioStreamMetadata a = audioList.get(i);
485 audio.setBitRate(a.getBitRate());
486 audio.setChannels(a.getChannels());
487 audio.setFormat(a.getFormat());
488 audio.setFormatVersion(a.getFormatVersion());
489 audio.setFrameCount(a.getFrames());
490 audio.setBitDepth(a.getResolution());
491 audio.setSamplingRate(a.getSamplingRate());
492
493 track.addStream(audio);
494 }
495 }
496 return track;
497 }
498
499
500
501
502
503
504
505
506
507
508
509
510
511 private Track addSubtitleStreamMetadata(TrackImpl track, MediaContainerMetadata metadata) throws Exception {
512 List<SubtitleStreamMetadata> subtitleList = metadata.getSubtitleStreamMetadata();
513 if (subtitleList != null && !subtitleList.isEmpty()) {
514 for (int i = 0; i < subtitleList.size(); i++) {
515 SubtitleStreamImpl subtitle = new SubtitleStreamImpl("subtitle-" + (i + 1));
516 SubtitleStreamMetadata s = subtitleList.get(i);
517 subtitle.setFormat(s.getFormat());
518 subtitle.setFormatVersion(s.getFormatVersion());
519 subtitle.setFrameCount(s.getFrames());
520
521 track.addStream(subtitle);
522 }
523 }
524 return track;
525 }
526
527
528 private boolean getAccurateFrameCount(final Map<String, String> options) {
529 return BooleanUtils.toBoolean(options.get(OPTION_ACCURATE_FRAME_COUNT));
530 }
531
532
533 private void throwExceptionIfInvalid(final Map<String, String> options) throws MediaInspectionException {
534 if (options != null) {
535 for (Entry e : options.entrySet()) {
536 if (e.getKey().equals(OPTION_ACCURATE_FRAME_COUNT)) {
537
538 } else {
539 throw new MediaInspectionException("Unsupported option " + e.getKey());
540 }
541 }
542 } else {
543 throw new MediaInspectionException("Options must not be null");
544 }
545 }
546 }