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.workflow.handler.composer;
23
24 import static org.opencastproject.util.data.Collections.list;
25
26 import org.opencastproject.composer.api.ComposerService;
27 import org.opencastproject.composer.api.EncoderException;
28 import org.opencastproject.composer.api.EncodingProfile;
29 import org.opencastproject.composer.api.LaidOutElement;
30 import org.opencastproject.composer.layout.AbsolutePositionLayoutSpec;
31 import org.opencastproject.composer.layout.Dimension;
32 import org.opencastproject.composer.layout.HorizontalCoverageLayoutSpec;
33 import org.opencastproject.composer.layout.LayoutManager;
34 import org.opencastproject.composer.layout.MultiShapeLayout;
35 import org.opencastproject.composer.layout.Serializer;
36 import org.opencastproject.job.api.Job;
37 import org.opencastproject.job.api.JobContext;
38 import org.opencastproject.mediapackage.Attachment;
39 import org.opencastproject.mediapackage.MediaPackage;
40 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
41 import org.opencastproject.mediapackage.MediaPackageElementParser;
42 import org.opencastproject.mediapackage.MediaPackageException;
43 import org.opencastproject.mediapackage.Track;
44 import org.opencastproject.mediapackage.TrackSupport;
45 import org.opencastproject.mediapackage.VideoStream;
46 import org.opencastproject.mediapackage.attachment.AttachmentImpl;
47 import org.opencastproject.mediapackage.selector.AbstractMediaPackageElementSelector;
48 import org.opencastproject.mediapackage.selector.AttachmentSelector;
49 import org.opencastproject.mediapackage.selector.TrackSelector;
50 import org.opencastproject.serviceregistry.api.ServiceRegistry;
51 import org.opencastproject.util.JsonObj;
52 import org.opencastproject.util.NotFoundException;
53 import org.opencastproject.util.UrlSupport;
54 import org.opencastproject.util.data.Option;
55 import org.opencastproject.util.data.Tuple;
56 import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
57 import org.opencastproject.workflow.api.ConfiguredTagsAndFlavors;
58 import org.opencastproject.workflow.api.WorkflowInstance;
59 import org.opencastproject.workflow.api.WorkflowOperationException;
60 import org.opencastproject.workflow.api.WorkflowOperationHandler;
61 import org.opencastproject.workflow.api.WorkflowOperationInstance;
62 import org.opencastproject.workflow.api.WorkflowOperationResult;
63 import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
64 import org.opencastproject.workspace.api.Workspace;
65
66 import org.apache.commons.io.FilenameUtils;
67 import org.apache.commons.io.IOUtils;
68 import org.apache.commons.lang3.StringUtils;
69 import org.osgi.service.component.annotations.Component;
70 import org.osgi.service.component.annotations.Reference;
71 import org.slf4j.Logger;
72 import org.slf4j.LoggerFactory;
73
74 import java.awt.image.BufferedImage;
75 import java.io.File;
76 import java.io.IOException;
77 import java.io.InputStream;
78 import java.net.URI;
79 import java.util.ArrayList;
80 import java.util.Collection;
81 import java.util.List;
82 import java.util.UUID;
83 import java.util.regex.Pattern;
84
85 import javax.imageio.ImageIO;
86
87
88
89
90 @Component(
91 immediate = true,
92 service = WorkflowOperationHandler.class,
93 property = {
94 "service.description=Composite Workflow Operation Handler",
95 "workflow.operation=composite"
96 }
97 )
98 public class CompositeWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
99
100 private static final String COLLECTION = "composite";
101
102 private static final String SOURCE_AUDIO_NAME = "source-audio-name";
103 private static final String SOURCE_TAGS_UPPER = "source-tags-upper";
104 private static final String SOURCE_FLAVOR_UPPER = "source-flavor-upper";
105 private static final String SOURCE_TAGS_LOWER = "source-tags-lower";
106 private static final String SOURCE_FLAVOR_LOWER = "source-flavor-lower";
107 private static final String SOURCE_TAGS_WATERMARK = "source-tags-watermark";
108 private static final String SOURCE_FLAVOR_WATERMARK = "source-flavor-watermark";
109 private static final String SOURCE_URL_WATERMARK = "source-url-watermark";
110
111 private static final String ENCODING_PROFILE = "encoding-profile";
112
113 private static final String LAYOUT = "layout";
114 private static final String LAYOUT_MULTIPLE = "layout-multiple";
115 private static final String LAYOUT_SINGLE = "layout-single";
116 private static final String LAYOUT_PREFIX = "layout-";
117
118 private static final String OUTPUT_RESOLUTION = "output-resolution";
119 private static final String OUTPUT_BACKGROUND = "output-background";
120 private static final String DEFAULT_BG_COLOR = "black";
121
122
123 private static final Logger logger = LoggerFactory.getLogger(CompositeWorkflowOperationHandler.class);
124
125
126 private static final Pattern sourceAudioOption = Pattern.compile(
127 ComposerService.LOWER + "|" + ComposerService.UPPER + "|" + ComposerService.BOTH, Pattern.CASE_INSENSITIVE);
128
129
130 private ComposerService composerService = null;
131
132
133 private Workspace workspace = null;
134
135
136
137
138
139
140
141 @Reference
142 public void setComposerService(ComposerService composerService) {
143 this.composerService = composerService;
144 }
145
146
147
148
149
150
151
152
153 @Reference
154 public void setWorkspace(Workspace workspace) {
155 this.workspace = workspace;
156 }
157
158
159
160
161
162
163
164 @Override
165 public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
166 throws WorkflowOperationException {
167 logger.debug("Running composite workflow operation on workflow {}", workflowInstance.getId());
168
169 try {
170 return composite(workflowInstance);
171 } catch (Exception e) {
172 throw new WorkflowOperationException(e);
173 }
174 }
175
176 private WorkflowOperationResult composite(WorkflowInstance wi)
177 throws EncoderException, IOException, NotFoundException, MediaPackageException, WorkflowOperationException {
178 MediaPackage src = wi.getMediaPackage();
179 MediaPackage mediaPackage = (MediaPackage) src.clone();
180 CompositeSettings compositeSettings;
181 try {
182 compositeSettings = new CompositeSettings(wi);
183 } catch (IllegalArgumentException e) {
184 logger.warn("Unable to parse composite settings because", e);
185 return createResult(mediaPackage, Action.SKIP);
186 }
187 Option<Attachment> watermarkAttachment = Option.<Attachment> none();
188 Collection<Attachment> watermarkElements = compositeSettings.getWatermarkSelector().select(mediaPackage, false);
189 if (watermarkElements.size() > 1) {
190 logger.warn("More than one watermark attachment has been found for compositing, skipping compositing!: {}",
191 watermarkElements);
192 return createResult(mediaPackage, Action.SKIP);
193 } else if (watermarkElements.size() == 0 && compositeSettings.getSourceUrlWatermark() != null) {
194 logger.info("No watermark found from flavor and tags, take watermark from URL {}",
195 compositeSettings.getSourceUrlWatermark());
196 Attachment urlAttachment = new AttachmentImpl();
197 urlAttachment.setIdentifier(compositeSettings.getWatermarkIdentifier());
198
199 if (compositeSettings.getSourceUrlWatermark().startsWith("http")) {
200 urlAttachment.setURI(UrlSupport.uri(compositeSettings.getSourceUrlWatermark()));
201 } else {
202 InputStream in = null;
203 try {
204 in = UrlSupport.url(compositeSettings.getSourceUrlWatermark()).openStream();
205 URI imageUrl = workspace.putInCollection(COLLECTION, compositeSettings.getWatermarkIdentifier() + "."
206 + FilenameUtils.getExtension(compositeSettings.getSourceUrlWatermark()), in);
207 urlAttachment.setURI(imageUrl);
208 } catch (Exception e) {
209 logger.warn("Unable to read watermark source url {}", compositeSettings.getSourceUrlWatermark(), e);
210 throw new WorkflowOperationException("Unable to read watermark source url "
211 + compositeSettings.getSourceUrlWatermark(), e);
212 } finally {
213 IOUtils.closeQuietly(in);
214 }
215 }
216 watermarkAttachment = Option.option(urlAttachment);
217 } else if (watermarkElements.size() == 0 && compositeSettings.getSourceUrlWatermark() == null) {
218 logger.info("No watermark to composite");
219 } else {
220 for (Attachment a : watermarkElements)
221 watermarkAttachment = Option.option(a);
222 }
223
224 Collection<Track> upperElements = compositeSettings.getUpperTrackSelector().select(mediaPackage, false);
225 Collection<Track> lowerElements = compositeSettings.getLowerTrackSelector().select(mediaPackage, false);
226
227
228 if ((upperElements.size() == 1 && lowerElements.size() == 0)
229 || (upperElements.size() == 0 && lowerElements.size() == 1)) {
230 for (Track t : upperElements)
231 compositeSettings.setSingleTrack(t);
232 for (Track t : lowerElements)
233 compositeSettings.setSingleTrack(t);
234 return handleSingleTrack(mediaPackage, compositeSettings, watermarkAttachment);
235 } else {
236
237 if (upperElements.size() > 1) {
238 logger.warn("More than one upper track has been found for compositing, skipping compositing!: {}",
239 upperElements);
240 return createResult(mediaPackage, Action.SKIP);
241 } else if (upperElements.size() == 0) {
242 logger.warn("No upper track has been found for compositing, skipping compositing!");
243 return createResult(mediaPackage, Action.SKIP);
244 }
245
246 for (Track t : upperElements) {
247 compositeSettings.setUpperTrack(t);
248 }
249
250
251 if (lowerElements.size() > 1) {
252 logger.warn("More than one lower track has been found for compositing, skipping compositing!: {}",
253 lowerElements);
254 return createResult(mediaPackage, Action.SKIP);
255 } else if (lowerElements.size() == 0) {
256 logger.warn("No lower track has been found for compositing, skipping compositing!");
257 return createResult(mediaPackage, Action.SKIP);
258 }
259
260 for (Track t : lowerElements) {
261 compositeSettings.setLowerTrack(t);
262 }
263
264 return handleMultipleTracks(mediaPackage, compositeSettings, watermarkAttachment);
265 }
266 }
267
268
269
270
271
272 private class CompositeSettings {
273
274
275 public static final String OUTPUT_RESOLUTION_FIXED = "fixed";
276
277
278 public static final String OUTPUT_RESOLUTION_LOWER = "lower";
279
280
281 public static final String OUTPUT_RESOLUTION_UPPER = "upper";
282
283 private String sourceAudioName;
284 private String sourceTagsUpper;
285 private String sourceFlavorUpper;
286 private String sourceTagsLower;
287 private String sourceFlavorLower;
288 private String sourceTagsWatermark;
289 private String sourceFlavorWatermark;
290 private String sourceUrlWatermark;
291 private String encodingProfile;
292 private String layoutMultipleString;
293 private String layoutSingleString;
294 private String outputResolution;
295 private String outputBackground;
296
297 private AbstractMediaPackageElementSelector<Track> upperTrackSelector = new TrackSelector();
298 private AbstractMediaPackageElementSelector<Track> lowerTrackSelector = new TrackSelector();
299 private AbstractMediaPackageElementSelector<Attachment> watermarkSelector = new AttachmentSelector();
300
301 private String watermarkIdentifier;
302 private Option<AbsolutePositionLayoutSpec> watermarkLayout = Option.none();
303
304 private List<HorizontalCoverageLayoutSpec> multiSourceLayouts = new ArrayList<HorizontalCoverageLayoutSpec>();
305 private HorizontalCoverageLayoutSpec singleSourceLayout;
306
307 private Track upperTrack;
308 private Track lowerTrack;
309 private Track singleTrack;
310
311 private String outputResolutionSource;
312 private Dimension outputDimension;
313
314 private EncodingProfile profile;
315
316 private List<String> targetTags;
317
318 private MediaPackageElementFlavor targetFlavor = null;
319
320 CompositeSettings(WorkflowInstance wi) throws WorkflowOperationException {
321 WorkflowOperationInstance operation = wi.getCurrentOperation();
322 ConfiguredTagsAndFlavors tagsAndFlavors = getTagsAndFlavors(wi,
323 Configuration.none, Configuration.none, Configuration.many, Configuration.one);
324 sourceAudioName = StringUtils.trimToNull(operation.getConfiguration(SOURCE_AUDIO_NAME));
325 if (sourceAudioName == null) {
326 sourceAudioName = ComposerService.BOTH;
327 } else if (!sourceAudioOption.matcher(sourceAudioName).matches()) {
328 throw new WorkflowOperationException("sourceAudioName if used, must be either upper, lower or both!");
329 }
330
331 sourceTagsUpper = StringUtils.trimToNull(operation.getConfiguration(SOURCE_TAGS_UPPER));
332 sourceFlavorUpper = StringUtils.trimToNull(operation.getConfiguration(SOURCE_FLAVOR_UPPER));
333 sourceTagsLower = StringUtils.trimToNull(operation.getConfiguration(SOURCE_TAGS_LOWER));
334 sourceFlavorLower = StringUtils.trimToNull(operation.getConfiguration(SOURCE_FLAVOR_LOWER));
335 sourceTagsWatermark = StringUtils.trimToNull(operation.getConfiguration(SOURCE_TAGS_WATERMARK));
336 sourceFlavorWatermark = StringUtils.trimToNull(operation.getConfiguration(SOURCE_FLAVOR_WATERMARK));
337 sourceUrlWatermark = StringUtils.trimToNull(operation.getConfiguration(SOURCE_URL_WATERMARK));
338
339 targetTags = tagsAndFlavors.getTargetTags();
340 targetFlavor = tagsAndFlavors.getSingleTargetFlavor();
341
342 encodingProfile = StringUtils.trimToNull(operation.getConfiguration(ENCODING_PROFILE));
343
344 layoutMultipleString = StringUtils.trimToNull(operation.getConfiguration(LAYOUT_MULTIPLE));
345 if (layoutMultipleString == null) {
346 layoutMultipleString = StringUtils.trimToNull(operation.getConfiguration(LAYOUT));
347 }
348
349 if (layoutMultipleString != null && !layoutMultipleString.contains(";")) {
350 layoutMultipleString = StringUtils.trimToNull(operation.getConfiguration(LAYOUT_PREFIX + layoutMultipleString));
351 }
352
353 layoutSingleString = StringUtils.trimToNull(operation.getConfiguration(LAYOUT_SINGLE));
354
355 outputResolution = StringUtils.trimToNull(operation.getConfiguration(OUTPUT_RESOLUTION));
356 outputBackground = StringUtils.trimToNull(operation.getConfiguration(OUTPUT_BACKGROUND));
357
358 watermarkIdentifier = UUID.randomUUID().toString();
359
360 if (outputBackground == null) {
361 outputBackground = DEFAULT_BG_COLOR;
362 }
363
364 if (layoutMultipleString != null) {
365 Tuple<List<HorizontalCoverageLayoutSpec>, Option<AbsolutePositionLayoutSpec>> multipleLayouts = parseMultipleLayouts(layoutMultipleString);
366 multiSourceLayouts.addAll(multipleLayouts.getA());
367 watermarkLayout = multipleLayouts.getB();
368 }
369
370 if (layoutSingleString != null) {
371 Tuple<HorizontalCoverageLayoutSpec, Option<AbsolutePositionLayoutSpec>> singleLayouts = parseSingleLayouts(layoutSingleString);
372 singleSourceLayout = singleLayouts.getA();
373 watermarkLayout = singleLayouts.getB();
374 }
375
376
377 if (encodingProfile == null)
378 throw new WorkflowOperationException("Encoding profile must be set!");
379
380 profile = composerService.getProfile(encodingProfile);
381 if (profile == null)
382 throw new WorkflowOperationException("Encoding profile '" + encodingProfile + "' was not found");
383
384
385 if (outputResolution == null)
386 throw new WorkflowOperationException("Output resolution must be set!");
387
388 if (outputResolution.equals(OUTPUT_RESOLUTION_LOWER) || outputResolution.equals(OUTPUT_RESOLUTION_UPPER)) {
389 outputResolutionSource = outputResolution;
390 } else {
391 outputResolutionSource = OUTPUT_RESOLUTION_FIXED;
392 try {
393 String[] outputResolutionArray = StringUtils.split(outputResolution, "x");
394 if (outputResolutionArray.length != 2) {
395 throw new WorkflowOperationException("Invalid format of output resolution!");
396 }
397 outputDimension = Dimension.dimension(Integer.parseInt(outputResolutionArray[0]),
398 Integer.parseInt(outputResolutionArray[1]));
399 } catch (Exception e) {
400 throw new WorkflowOperationException("Unable to parse output resolution!", e);
401 }
402 }
403
404
405 if (sourceTagsUpper == null && sourceFlavorUpper == null) {
406 throw new IllegalArgumentException(
407 "No source tags or flavor for the upper video have been specified, not matching anything");
408 }
409
410
411 if (sourceTagsLower == null && sourceFlavorLower == null) {
412 throw new IllegalArgumentException(
413 "No source tags or flavor for the lower video have been specified, not matching anything");
414 }
415
416 try {
417 if ("*".equals(targetFlavor.getType()) || "*".equals(targetFlavor.getSubtype()))
418 throw new WorkflowOperationException("Target flavor must have a type and a subtype, '*' are not allowed!");
419 } catch (IllegalArgumentException e) {
420 throw new WorkflowOperationException("Target flavor '" + targetFlavor + "' is malformed");
421 }
422
423
424 if (sourceFlavorUpper != null) {
425 try {
426 upperTrackSelector.addFlavor(MediaPackageElementFlavor.parseFlavor(sourceFlavorUpper));
427 } catch (IllegalArgumentException e) {
428 throw new WorkflowOperationException("Source upper flavor '" + sourceFlavorUpper + "' is malformed");
429 }
430 }
431
432
433 if (sourceFlavorLower != null) {
434 try {
435 lowerTrackSelector.addFlavor(MediaPackageElementFlavor.parseFlavor(sourceFlavorLower));
436 } catch (IllegalArgumentException e) {
437 throw new WorkflowOperationException("Source lower flavor '" + sourceFlavorLower + "' is malformed");
438 }
439 }
440
441
442 if (sourceFlavorWatermark != null) {
443 try {
444 watermarkSelector.addFlavor(MediaPackageElementFlavor.parseFlavor(sourceFlavorWatermark));
445 } catch (IllegalArgumentException e) {
446 throw new WorkflowOperationException("Source watermark flavor '" + sourceFlavorWatermark + "' is malformed");
447 }
448 }
449
450
451 for (String tag : asList(sourceTagsUpper)) {
452 upperTrackSelector.addTag(tag);
453 }
454
455
456 for (String tag : asList(sourceTagsLower)) {
457 lowerTrackSelector.addTag(tag);
458 }
459
460
461 for (String tag : asList(sourceTagsWatermark)) {
462 watermarkSelector.addTag(tag);
463 }
464 }
465
466 private Tuple<List<HorizontalCoverageLayoutSpec>, Option<AbsolutePositionLayoutSpec>> parseMultipleLayouts(
467 String layoutString) throws WorkflowOperationException {
468 try {
469 String[] layouts = StringUtils.split(layoutString, ";");
470 if (layouts.length < 2)
471 throw new WorkflowOperationException(
472 "Multiple layout doesn't contain the required layouts for (lower, upper, optional watermark)");
473
474 List<HorizontalCoverageLayoutSpec> multipleLayouts = list(
475 Serializer.horizontalCoverageLayoutSpec(JsonObj.jsonObj(layouts[0])),
476 Serializer.horizontalCoverageLayoutSpec(JsonObj.jsonObj(layouts[1])));
477
478 AbsolutePositionLayoutSpec watermarkLayout = null;
479 if (layouts.length > 2)
480 watermarkLayout = Serializer.absolutePositionLayoutSpec(JsonObj.jsonObj(layouts[2]));
481
482 return Tuple.tuple(multipleLayouts, Option.option(watermarkLayout));
483 } catch (Exception e) {
484 throw new WorkflowOperationException("Unable to parse layout!", e);
485 }
486 }
487
488 private Tuple<HorizontalCoverageLayoutSpec, Option<AbsolutePositionLayoutSpec>> parseSingleLayouts(
489 String layoutString) throws WorkflowOperationException {
490 try {
491 String[] layouts = StringUtils.split(layoutString, ";");
492 if (layouts.length < 1)
493 throw new WorkflowOperationException(
494 "Single layout doesn't contain the required layouts for (video, optional watermark)");
495
496 HorizontalCoverageLayoutSpec singleLayout = Serializer
497 .horizontalCoverageLayoutSpec(JsonObj.jsonObj(layouts[0]));
498
499 AbsolutePositionLayoutSpec watermarkLayout = null;
500 if (layouts.length > 1)
501 watermarkLayout = Serializer.absolutePositionLayoutSpec(JsonObj.jsonObj(layouts[1]));
502
503 return Tuple.tuple(singleLayout, Option.option(watermarkLayout));
504 } catch (Exception e) {
505 throw new WorkflowOperationException("Unable to parse layout!", e);
506 }
507 }
508
509 public String getSourceUrlWatermark() {
510 return sourceUrlWatermark;
511 }
512
513 public MediaPackageElementFlavor getTargetFlavor() {
514 return targetFlavor;
515 }
516
517 public List<String> getTargetTags() {
518 return targetTags;
519 }
520
521 public String getSourceAudioName() {
522 return sourceAudioName;
523 }
524
525 public String getOutputBackground() {
526 return outputBackground;
527 }
528
529 public AbstractMediaPackageElementSelector<Track> getUpperTrackSelector() {
530 return upperTrackSelector;
531 }
532
533 public AbstractMediaPackageElementSelector<Track> getLowerTrackSelector() {
534 return lowerTrackSelector;
535 }
536
537 public AbstractMediaPackageElementSelector<Attachment> getWatermarkSelector() {
538 return watermarkSelector;
539 }
540
541 public String getWatermarkIdentifier() {
542 return watermarkIdentifier;
543 }
544
545 public Option<AbsolutePositionLayoutSpec> getWatermarkLayout() {
546 return watermarkLayout;
547 }
548
549 public List<HorizontalCoverageLayoutSpec> getMultiSourceLayouts() {
550 return multiSourceLayouts;
551 }
552
553 public HorizontalCoverageLayoutSpec getSingleSourceLayout() {
554 return singleSourceLayout;
555 }
556
557 public Track getUpperTrack() {
558 return upperTrack;
559 }
560
561 public void setUpperTrack(Track upperTrack) {
562 this.upperTrack = upperTrack;
563 }
564
565 public Track getLowerTrack() {
566 return lowerTrack;
567 }
568
569 public void setLowerTrack(Track lowerTrack) {
570 this.lowerTrack = lowerTrack;
571 }
572
573 public Track getSingleTrack() {
574 return singleTrack;
575 }
576
577 public void setSingleTrack(Track singleTrack) {
578 this.singleTrack = singleTrack;
579 }
580
581 public String getOutputResolutionSource() {
582 return outputResolutionSource;
583 }
584
585 public Dimension getOutputDimension() {
586 return outputDimension;
587 }
588
589 public EncodingProfile getProfile() {
590 return profile;
591 }
592 }
593
594 private WorkflowOperationResult handleSingleTrack(MediaPackage mediaPackage,
595 CompositeSettings compositeSettings, Option<Attachment> watermarkAttachment) throws EncoderException,
596 IOException, NotFoundException, MediaPackageException, WorkflowOperationException {
597
598 if (compositeSettings.getSingleSourceLayout() == null) {
599 throw new WorkflowOperationException("Single video layout must be set! Please verify that you have a "
600 + LAYOUT_SINGLE + " property in your composite operation in your workflow definition.");
601 }
602
603 try {
604 VideoStream[] videoStreams = TrackSupport.byType(compositeSettings.getSingleTrack().getStreams(),
605 VideoStream.class);
606 if (videoStreams.length == 0) {
607 logger.warn("No video stream available to compose! {}", compositeSettings.getSingleTrack());
608 return createResult(mediaPackage, Action.SKIP);
609 }
610
611
612 Dimension videoDimension = Dimension.dimension(videoStreams[0].getFrameWidth(), videoStreams[0].getFrameHeight());
613
614
615 List<Tuple<Dimension, HorizontalCoverageLayoutSpec>> shapes = new ArrayList<Tuple<Dimension, HorizontalCoverageLayoutSpec>>();
616 shapes.add(0, Tuple.tuple(videoDimension, compositeSettings.getSingleSourceLayout()));
617
618
619 Dimension outputDimension = null;
620 String outputResolutionSource = compositeSettings.getOutputResolutionSource();
621 if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_FIXED)) {
622 outputDimension = compositeSettings.getOutputDimension();
623 } else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_LOWER)) {
624 outputDimension = videoDimension;
625 } else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_UPPER)) {
626 outputDimension = videoDimension;
627 }
628
629
630 MultiShapeLayout multiShapeLayout = LayoutManager
631 .multiShapeLayout(outputDimension, shapes);
632
633
634 LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>(compositeSettings.getSingleTrack(),
635 multiShapeLayout.getShapes().get(0));
636
637
638 Option<LaidOutElement<Attachment>> watermarkOption = createWatermarkLaidOutElement(compositeSettings,
639 outputDimension, watermarkAttachment);
640
641 Job compositeJob = composerService.composite(outputDimension, Option
642 .<LaidOutElement<Track>> none(), lowerLaidOutElement, watermarkOption, compositeSettings.getProfile()
643 .getIdentifier(), compositeSettings.getOutputBackground(), compositeSettings.getSourceAudioName());
644
645
646 if (!waitForStatus(compositeJob).isSuccess())
647 throw new WorkflowOperationException("The composite job did not complete successfully");
648
649 if (compositeJob.getPayload().length() > 0) {
650
651 Track compoundTrack = (Track) MediaPackageElementParser.getFromXml(compositeJob.getPayload());
652
653 compoundTrack.setURI(workspace.moveTo(compoundTrack.getURI(), mediaPackage.getIdentifier().toString(),
654 compoundTrack.getIdentifier(),
655 "composite." + FilenameUtils.getExtension(compoundTrack.getURI().toString())));
656
657
658 for (String tag : compositeSettings.getTargetTags()) {
659 logger.trace("Tagging compound track with '{}'", tag);
660 compoundTrack.addTag(tag);
661 }
662
663
664 compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
665 logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
666
667
668 mediaPackage.add(compoundTrack);
669 WorkflowOperationResult result = createResult(mediaPackage, Action.CONTINUE, compositeJob.getQueueTime());
670 logger.debug("Composite operation completed");
671 return result;
672 } else {
673 logger.info("Composite operation unsuccessful, no payload returned: {}", compositeJob);
674 return createResult(mediaPackage, Action.SKIP);
675 }
676 } finally {
677 if (compositeSettings.getSourceUrlWatermark() != null)
678 workspace.deleteFromCollection(
679 COLLECTION,
680 compositeSettings.getWatermarkIdentifier() + "."
681 + FilenameUtils.getExtension(compositeSettings.getSourceUrlWatermark()));
682 }
683 }
684
685 private Option<LaidOutElement<Attachment>> createWatermarkLaidOutElement(CompositeSettings compositeSettings,
686 Dimension outputDimension, Option<Attachment> watermarkAttachment) throws WorkflowOperationException {
687 Option<LaidOutElement<Attachment>> watermarkOption = Option.<LaidOutElement<Attachment>> none();
688 if (watermarkAttachment.isSome() && compositeSettings.getWatermarkLayout().isSome()) {
689 BufferedImage image;
690 try {
691 File watermarkFile = workspace.get(watermarkAttachment.get().getURI());
692 image = ImageIO.read(watermarkFile);
693 } catch (Exception e) {
694 logger.warn("Unable to read the watermark image attachment {}", watermarkAttachment.get().getURI(), e);
695 throw new WorkflowOperationException("Unable to read the watermark image attachment", e);
696 }
697
698
699 if (null == image) {
700 logger.error("Unable to parse watermark file. File must be gif, png, jp(e)g, or bmp");
701 throw new WorkflowOperationException("Unable to parse watermark file. File must be gif, png, jp(e)g, or bmp");
702 }
703 Dimension imageDimension = Dimension.dimension(image.getWidth(), image.getHeight());
704 List<Tuple<Dimension, AbsolutePositionLayoutSpec>> watermarkShapes = new ArrayList<Tuple<Dimension, AbsolutePositionLayoutSpec>>();
705 watermarkShapes.add(0, Tuple.tuple(imageDimension, compositeSettings.getWatermarkLayout().get()));
706 MultiShapeLayout watermarkLayout = LayoutManager.absoluteMultiShapeLayout(outputDimension,
707 watermarkShapes);
708 watermarkOption = Option.some(new LaidOutElement<Attachment>(watermarkAttachment.get(), watermarkLayout
709 .getShapes().get(0)));
710 }
711 return watermarkOption;
712 }
713
714 private WorkflowOperationResult handleMultipleTracks(MediaPackage mediaPackage,
715 CompositeSettings compositeSettings, Option<Attachment> watermarkAttachment) throws EncoderException,
716 IOException, NotFoundException, MediaPackageException, WorkflowOperationException {
717 if (compositeSettings.getMultiSourceLayouts() == null || compositeSettings.getMultiSourceLayouts().size() == 0) {
718 throw new WorkflowOperationException(
719 "Multi video layout must be set! Please verify that you have a "
720 + LAYOUT_MULTIPLE
721 + " or "
722 + LAYOUT
723 + " property in your composite operation in your workflow definition to be able to handle multiple videos");
724 }
725
726 try {
727 Track upperTrack = compositeSettings.getUpperTrack();
728 Track lowerTrack = compositeSettings.getLowerTrack();
729 List<HorizontalCoverageLayoutSpec> layouts = compositeSettings.getMultiSourceLayouts();
730
731 VideoStream[] upperVideoStreams = TrackSupport.byType(upperTrack.getStreams(), VideoStream.class);
732 if (upperVideoStreams.length == 0) {
733 logger.warn("No video stream available in the upper track! {}", upperTrack);
734 return createResult(mediaPackage, Action.SKIP);
735 }
736
737 VideoStream[] lowerVideoStreams = TrackSupport.byType(lowerTrack.getStreams(), VideoStream.class);
738 if (lowerVideoStreams.length == 0) {
739 logger.warn("No video stream available in the lower track! {}", lowerTrack);
740 return createResult(mediaPackage, Action.SKIP);
741 }
742
743
744 Dimension upperDimensions = Dimension.dimension(upperVideoStreams[0].getFrameWidth(),
745 upperVideoStreams[0].getFrameHeight());
746 Dimension lowerDimensions = Dimension.dimension(lowerVideoStreams[0].getFrameWidth(),
747 lowerVideoStreams[0].getFrameHeight());
748
749
750 Dimension outputDimension = null;
751 String outputResolutionSource = compositeSettings.getOutputResolutionSource();
752 if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_FIXED)) {
753 outputDimension = compositeSettings.getOutputDimension();
754 } else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_LOWER)) {
755 outputDimension = lowerDimensions;
756 } else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_UPPER)) {
757 outputDimension = upperDimensions;
758 }
759
760
761 List<Tuple<Dimension, HorizontalCoverageLayoutSpec>> shapes = new ArrayList<Tuple<Dimension, HorizontalCoverageLayoutSpec>>();
762 shapes.add(0, Tuple.tuple(lowerDimensions, layouts.get(0)));
763 shapes.add(1, Tuple.tuple(upperDimensions, layouts.get(1)));
764
765
766 MultiShapeLayout multiShapeLayout = LayoutManager
767 .multiShapeLayout(outputDimension, shapes);
768
769
770 LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>(lowerTrack, multiShapeLayout.getShapes()
771 .get(0));
772 LaidOutElement<Track> upperLaidOutElement = new LaidOutElement<Track>(upperTrack, multiShapeLayout.getShapes()
773 .get(1));
774
775
776 Option<LaidOutElement<Attachment>> watermarkOption = createWatermarkLaidOutElement(compositeSettings,
777 outputDimension, watermarkAttachment);
778
779 Job compositeJob = composerService.composite(outputDimension, Option
780 .option(upperLaidOutElement), lowerLaidOutElement, watermarkOption, compositeSettings.getProfile()
781 .getIdentifier(), compositeSettings.getOutputBackground(), compositeSettings.getSourceAudioName());
782
783
784 if (!waitForStatus(compositeJob).isSuccess())
785 throw new WorkflowOperationException("The composite job did not complete successfully");
786
787 if (compositeJob.getPayload().length() > 0) {
788
789 Track compoundTrack = (Track) MediaPackageElementParser.getFromXml(compositeJob.getPayload());
790
791 compoundTrack.setURI(workspace.moveTo(compoundTrack.getURI(), mediaPackage.getIdentifier().toString(),
792 compoundTrack.getIdentifier(),
793 "composite." + FilenameUtils.getExtension(compoundTrack.getURI().toString())));
794
795
796 for (String tag : compositeSettings.getTargetTags()) {
797 logger.trace("Tagging compound track with '{}'", tag);
798 compoundTrack.addTag(tag);
799 }
800
801
802 compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
803 logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
804
805
806 mediaPackage.add(compoundTrack);
807 WorkflowOperationResult result = createResult(mediaPackage, Action.CONTINUE, compositeJob.getQueueTime());
808 logger.debug("Composite operation completed");
809 return result;
810 } else {
811 logger.info("Composite operation unsuccessful, no payload returned: {}", compositeJob);
812 return createResult(mediaPackage, Action.SKIP);
813 }
814 } finally {
815 if (compositeSettings.getSourceUrlWatermark() != null)
816 workspace.deleteFromCollection(
817 COLLECTION,
818 compositeSettings.getWatermarkIdentifier() + "."
819 + FilenameUtils.getExtension(compositeSettings.getSourceUrlWatermark()));
820 }
821 }
822
823 @Reference
824 @Override
825 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
826 super.setServiceRegistry(serviceRegistry);
827 }
828
829 }