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
135 try {
136 track.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, file));
137 } catch (IOException e) {
138 throw new MediaInspectionException("Unable to read " + file, e);
139 }
140
141
142 track.setMimeType(metadata.getMimeType());
143 track.setMaster(metadata.getAdaptiveMaster());
144
145
146 try {
147 addAudioStreamMetadata(track, metadata);
148 } catch (Exception e) {
149 throw new MediaInspectionException("Unable to extract audio metadata from " + file, e);
150 }
151
152
153 try {
154 addVideoStreamMetadata(track, metadata);
155 } catch (Exception e) {
156 throw new MediaInspectionException("Unable to extract video metadata from " + file, e);
157 }
158
159
160 try {
161 addSubtitleStreamMetadata(track, metadata);
162 } catch (Exception e) {
163 throw new MediaInspectionException("Unable to extract subtitle metadata from " + file, e);
164 }
165
166
167 track.setSize(file.length());
168
169 return track;
170 }
171 } catch (Exception e) {
172 logger.warn("Error inspecting " + trackURI, e);
173 if (e instanceof MediaInspectionException) {
174 throw (MediaInspectionException) e;
175 } else {
176 throw new MediaInspectionException(e);
177 }
178 }
179 }
180
181
182
183
184
185
186
187
188
189
190
191
192 public MediaPackageElement enrich(MediaPackageElement element, boolean override, final Map<String, String> options)
193 throws MediaInspectionException {
194 throwExceptionIfInvalid(options);
195 if (element instanceof Track) {
196 final Track originalTrack = (Track) element;
197 return enrichTrack(originalTrack, override, options);
198 } else {
199 return enrichElement(element, override, options);
200 }
201 }
202
203
204
205
206
207
208
209
210
211
212
213 private MediaPackageElement enrichTrack(final Track originalTrack, final boolean override,
214 final Map<String, String> options) throws MediaInspectionException {
215 try {
216 URI originalTrackUrl = originalTrack.getURI();
217 MediaPackageElementFlavor flavor = originalTrack.getFlavor();
218 logger.debug("enrich(" + originalTrackUrl + ") called");
219
220
221 File file = null;
222 try {
223 file = workspace.get(originalTrackUrl);
224 } catch (NotFoundException e) {
225 throw new MediaInspectionException("File " + originalTrackUrl + " was not found and can therefore not be "
226 + "inspected", e);
227 } catch (IOException e) {
228 throw new MediaInspectionException("Error accessing " + originalTrackUrl, e);
229 }
230
231
232
233 if ("".equals(FilenameUtils.getExtension(file.getName()))) {
234 throw new MediaInspectionException("Can not inspect files without a filename extension");
235 }
236
237 MediaContainerMetadata metadata = getFileMetadata(file, getAccurateFrameCount(options));
238 if (metadata == null) {
239 throw new MediaInspectionException("Unable to acquire media metadata for " + originalTrackUrl);
240 } else {
241 TrackImpl track = null;
242 try {
243 track = (TrackImpl) MediaPackageElementBuilderFactory.newInstance().newElementBuilder()
244 .elementFromURI(originalTrackUrl, MediaPackageElement.Type.Track, flavor);
245 } catch (UnsupportedElementException e) {
246 throw new MediaInspectionException("Unable to create track element from " + file, e);
247 }
248
249
250 track.setChecksum(originalTrack.getChecksum());
251 track.setDuration(originalTrack.getDuration());
252 track.setElementDescription(originalTrack.getElementDescription());
253 track.setFlavor(flavor);
254 track.setIdentifier(originalTrack.getIdentifier());
255
256 if (!originalTrack.hasMaster() || override) {
257 track.setMaster(metadata.getAdaptiveMaster());
258 } else {
259 track.setMaster(originalTrack.isMaster());
260 }
261 track.setMimeType(originalTrack.getMimeType());
262 track.setReference(originalTrack.getReference());
263 track.setSize(file.length());
264 track.setURI(originalTrackUrl);
265 for (String tag : originalTrack.getTags()) {
266 track.addTag(tag);
267 }
268
269
270 if (track.getDuration() == null || override) {
271 track.setDuration(metadata.getDuration());
272 }
273 if (track.getChecksum() == null || override) {
274 try {
275 track.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, file));
276 } catch (IOException e) {
277 throw new MediaInspectionException("Unable to read " + file, e);
278 }
279 }
280
281
282 if (track.getMimeType() == null || override) {
283 track.setMimeType(metadata.getMimeType());
284 }
285
286
287 Dictionary<String, Stream> streamsId2Stream = new Hashtable<String, Stream>();
288 for (Stream stream : originalTrack.getStreams()) {
289 streamsId2Stream.put(stream.getIdentifier(), stream);
290 }
291
292
293 try {
294 addAudioStreamMetadata(track, metadata);
295 } catch (Exception e) {
296 throw new MediaInspectionException("Unable to extract audio metadata from " + file, e);
297 }
298
299
300 try {
301 addVideoStreamMetadata(track, metadata);
302 } catch (Exception e) {
303 throw new MediaInspectionException("Unable to extract video metadata from " + file, e);
304 }
305
306
307 try {
308 addSubtitleStreamMetadata(track, metadata);
309 } catch (Exception e) {
310 throw new MediaInspectionException("Unable to extract subtitle metadata from " + file, e);
311 }
312
313 logger.info("Successfully inspected track {}", track);
314 return track;
315 }
316 } catch (Exception e) {
317 logger.warn("Error enriching track " + originalTrack, e);
318 if (e instanceof MediaInspectionException) {
319 throw (MediaInspectionException) e;
320 } else {
321 throw new MediaInspectionException(e);
322 }
323 }
324 }
325
326
327
328
329
330
331
332
333
334
335
336
337
338 private MediaPackageElement enrichElement(final MediaPackageElement element, final boolean override,
339 final Map<String, String> options) throws MediaInspectionException {
340 try {
341 File file;
342 try {
343 file = workspace.get(element.getURI());
344 } catch (NotFoundException e) {
345 throw new MediaInspectionException("Unable to find " + element.getURI() + " in the workspace", e);
346 } catch (IOException e) {
347 throw new MediaInspectionException("Error accessing " + element.getURI() + " in the workspace", e);
348 }
349
350
351 if (element.getChecksum() == null || override) {
352 try {
353 element.setChecksum(Checksum.create(ChecksumType.DEFAULT_TYPE, file));
354 } catch (IOException e) {
355 throw new MediaInspectionException("Error generating checksum for " + element.getURI(), e);
356 }
357 }
358
359
360 if (element.getMimeType() == null || override) {
361 try {
362 element.setMimeType(MimeTypes.fromString(file.getPath()));
363 } catch (UnknownFileTypeException e) {
364 logger.info("unable to determine the mime type for {}", file.getName());
365 }
366 }
367
368 logger.info("Successfully inspected element {}", element);
369
370 return element;
371 } catch (Exception e) {
372 logger.warn("Error enriching element " + element, e);
373 if (e instanceof MediaInspectionException) {
374 throw (MediaInspectionException) e;
375 } else {
376 throw new MediaInspectionException(e);
377 }
378 }
379 }
380
381
382
383
384
385
386
387
388
389
390 private MediaContainerMetadata getFileMetadata(File file, boolean accurateFrameCount)
391 throws MediaInspectionException {
392 if (file == null) {
393 throw new IllegalArgumentException("file to analyze cannot be null");
394 }
395 try {
396 MediaAnalyzer analyzer = new FFmpegAnalyzer(accurateFrameCount);
397 analyzer.setConfig(map(Tuple.<String, Object> tuple(FFmpegAnalyzer.FFPROBE_BINARY_CONFIG, ffprobePath)));
398
399 MediaContainerMetadata metadata = analyzer.analyze(file);
400
401 try {
402
403 MimeType mimeType = MimeTypes.fromString(file.getName());
404
405
406
407 if (metadata.hasVideoStreamMetadata()) {
408 if (!"video".equals(mimeType.getType()) && !"application".equals(mimeType.getType())) {
409 mimeType = MimeTypes.parseMimeType("video/" + mimeType.getSubtype());
410 }
411 } else if (metadata.hasAudioStreamMetadata()) {
412 if (!"audio".equals(mimeType.getType()) && !"application".equals(mimeType.getType())) {
413 mimeType = MimeTypes.parseMimeType("audio/" + mimeType.getSubtype());
414 }
415 } else if (metadata.hasSubtitleStreamMetadata()) {
416 if (!"text".equals(mimeType.getType()) && !"application".equals(mimeType.getType())) {
417 mimeType = MimeTypes.parseMimeType("text/" + mimeType.getSubtype());
418 }
419 }
420 metadata.setMimeType(mimeType);
421 } catch (UnknownFileTypeException e) {
422 logger.error("parsing mimeType failed for {} : {}", file, e.getMessage());
423 throw new MediaAnalyzerException("parsing mimetype failed for file" + file);
424 }
425
426
427
428 try {
429 metadata.setAdaptiveMaster(AdaptivePlaylist.checkForMaster(file));
430 } catch (IOException e) {
431 logger.error("parsing adaptive playlist failed for {} : {}", file, e.getMessage());
432 throw new MediaAnalyzerException("parsing for adaptive playlist master failed for file" + file);
433 }
434 return metadata;
435 } catch (MediaAnalyzerException e) {
436 throw new MediaInspectionException(e);
437 }
438 }
439
440
441
442
443
444
445
446
447
448
449
450
451 private Track addVideoStreamMetadata(TrackImpl track, MediaContainerMetadata metadata) throws Exception {
452 List<VideoStreamMetadata> videoList = metadata.getVideoStreamMetadata();
453 if (videoList != null && !videoList.isEmpty()) {
454 for (int i = 0; i < videoList.size(); i++) {
455 VideoStreamImpl video = new VideoStreamImpl("video-" + (i + 1));
456 VideoStreamMetadata v = videoList.get(i);
457 video.setBitRate(v.getBitRate());
458 video.setFormat(v.getFormat());
459 video.setFormatVersion(v.getFormatVersion());
460 video.setFrameCount(v.getFrames());
461 video.setFrameHeight(v.getFrameHeight());
462 video.setFrameRate(v.getFrameRate());
463 video.setFrameWidth(v.getFrameWidth());
464 video.setScanOrder(v.getScanOrder());
465 video.setScanType(v.getScanType());
466
467 track.addStream(video);
468 }
469 }
470 return track;
471 }
472
473
474
475
476
477
478
479
480
481
482
483
484 private Track addAudioStreamMetadata(TrackImpl track, MediaContainerMetadata metadata) throws Exception {
485 List<AudioStreamMetadata> audioList = metadata.getAudioStreamMetadata();
486 if (audioList != null && !audioList.isEmpty()) {
487 for (int i = 0; i < audioList.size(); i++) {
488 AudioStreamImpl audio = new AudioStreamImpl("audio-" + (i + 1));
489 AudioStreamMetadata a = audioList.get(i);
490 audio.setBitRate(a.getBitRate());
491 audio.setChannels(a.getChannels());
492 audio.setFormat(a.getFormat());
493 audio.setFormatVersion(a.getFormatVersion());
494 audio.setFrameCount(a.getFrames());
495 audio.setBitDepth(a.getResolution());
496 audio.setSamplingRate(a.getSamplingRate());
497
498 track.addStream(audio);
499 }
500 }
501 return track;
502 }
503
504
505
506
507
508
509
510
511
512
513
514
515
516 private Track addSubtitleStreamMetadata(TrackImpl track, MediaContainerMetadata metadata) throws Exception {
517 List<SubtitleStreamMetadata> subtitleList = metadata.getSubtitleStreamMetadata();
518 if (subtitleList != null && !subtitleList.isEmpty()) {
519 for (int i = 0; i < subtitleList.size(); i++) {
520 SubtitleStreamImpl subtitle = new SubtitleStreamImpl("subtitle-" + (i + 1));
521 SubtitleStreamMetadata s = subtitleList.get(i);
522 subtitle.setFormat(s.getFormat());
523 subtitle.setFormatVersion(s.getFormatVersion());
524 subtitle.setFrameCount(s.getFrames());
525
526 track.addStream(subtitle);
527 }
528 }
529 return track;
530 }
531
532
533 private boolean getAccurateFrameCount(final Map<String, String> options) {
534 return BooleanUtils.toBoolean(options.get(OPTION_ACCURATE_FRAME_COUNT));
535 }
536
537
538 private void throwExceptionIfInvalid(final Map<String, String> options) throws MediaInspectionException {
539 if (options != null) {
540 for (Entry e : options.entrySet()) {
541 if (e.getKey().equals(OPTION_ACCURATE_FRAME_COUNT)) {
542
543 } else {
544 throw new MediaInspectionException("Unsupported option " + e.getKey());
545 }
546 }
547 } else {
548 throw new MediaInspectionException("Options must not be null");
549 }
550 }
551 }