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