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 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 Optional<AbsolutePositionLayoutSpec> watermarkLayout = Optional.empty();
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 ConfiguredTagsAndFlavors.TargetTags 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>, Optional<AbsolutePositionLayoutSpec>> multipleLayouts = parseMultipleLayouts(layoutMultipleString);
366 multiSourceLayouts.addAll(multipleLayouts.getA());
367 watermarkLayout = multipleLayouts.getB();
368 }
369
370 if (layoutSingleString != null) {
371 Tuple<HorizontalCoverageLayoutSpec, Optional<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>, Optional<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, Optional.ofNullable(watermarkLayout));
483 } catch (Exception e) {
484 throw new WorkflowOperationException("Unable to parse layout!", e);
485 }
486 }
487
488 private Tuple<HorizontalCoverageLayoutSpec, Optional<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, Optional.ofNullable(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 ConfiguredTagsAndFlavors.TargetTags 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 Optional<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, Optional<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 Optional<LaidOutElement<Attachment>> watermarkOption = createWatermarkLaidOutElement(compositeSettings,
639 outputDimension, watermarkAttachment);
640
641 Job compositeJob = composerService.composite(outputDimension, Optional
642 .<LaidOutElement<Track>> empty(), 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 applyTargetTagsToElement(compositeSettings.getTargetTags(), compoundTrack);
659
660
661 compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
662 logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
663
664
665 mediaPackage.add(compoundTrack);
666 WorkflowOperationResult result = createResult(mediaPackage, Action.CONTINUE, compositeJob.getQueueTime());
667 logger.debug("Composite operation completed");
668 return result;
669 } else {
670 logger.info("Composite operation unsuccessful, no payload returned: {}", compositeJob);
671 return createResult(mediaPackage, Action.SKIP);
672 }
673 } finally {
674 if (compositeSettings.getSourceUrlWatermark() != null)
675 workspace.deleteFromCollection(
676 COLLECTION,
677 compositeSettings.getWatermarkIdentifier() + "."
678 + FilenameUtils.getExtension(compositeSettings.getSourceUrlWatermark()));
679 }
680 }
681
682 private Optional<LaidOutElement<Attachment>> createWatermarkLaidOutElement(CompositeSettings compositeSettings,
683 Dimension outputDimension, Optional<Attachment> watermarkAttachment) throws WorkflowOperationException {
684 Optional<LaidOutElement<Attachment>> watermarkOption = Optional.<LaidOutElement<Attachment>> empty();
685 if (watermarkAttachment.isPresent() && compositeSettings.getWatermarkLayout().isPresent()) {
686 BufferedImage image;
687 try {
688 File watermarkFile = workspace.get(watermarkAttachment.get().getURI());
689 image = ImageIO.read(watermarkFile);
690 } catch (Exception e) {
691 logger.warn("Unable to read the watermark image attachment {}", watermarkAttachment.get().getURI(), e);
692 throw new WorkflowOperationException("Unable to read the watermark image attachment", e);
693 }
694
695
696 if (null == image) {
697 logger.error("Unable to parse watermark file. File must be gif, png, jp(e)g, or bmp");
698 throw new WorkflowOperationException("Unable to parse watermark file. File must be gif, png, jp(e)g, or bmp");
699 }
700 Dimension imageDimension = Dimension.dimension(image.getWidth(), image.getHeight());
701 List<Tuple<Dimension, AbsolutePositionLayoutSpec>> watermarkShapes = new ArrayList<Tuple<Dimension, AbsolutePositionLayoutSpec>>();
702 watermarkShapes.add(0, Tuple.tuple(imageDimension, compositeSettings.getWatermarkLayout().get()));
703 MultiShapeLayout watermarkLayout = LayoutManager.absoluteMultiShapeLayout(outputDimension,
704 watermarkShapes);
705 watermarkOption = Optional.of(new LaidOutElement<Attachment>(watermarkAttachment.get(), watermarkLayout
706 .getShapes().get(0)));
707 }
708 return watermarkOption;
709 }
710
711 private WorkflowOperationResult handleMultipleTracks(MediaPackage mediaPackage,
712 CompositeSettings compositeSettings, Optional<Attachment> watermarkAttachment) throws EncoderException,
713 IOException, NotFoundException, MediaPackageException, WorkflowOperationException {
714 if (compositeSettings.getMultiSourceLayouts() == null || compositeSettings.getMultiSourceLayouts().size() == 0) {
715 throw new WorkflowOperationException(
716 "Multi video layout must be set! Please verify that you have a "
717 + LAYOUT_MULTIPLE
718 + " or "
719 + LAYOUT
720 + " property in your composite operation in your workflow definition to be able to handle multiple videos");
721 }
722
723 try {
724 Track upperTrack = compositeSettings.getUpperTrack();
725 Track lowerTrack = compositeSettings.getLowerTrack();
726 List<HorizontalCoverageLayoutSpec> layouts = compositeSettings.getMultiSourceLayouts();
727
728 VideoStream[] upperVideoStreams = TrackSupport.byType(upperTrack.getStreams(), VideoStream.class);
729 if (upperVideoStreams.length == 0) {
730 logger.warn("No video stream available in the upper track! {}", upperTrack);
731 return createResult(mediaPackage, Action.SKIP);
732 }
733
734 VideoStream[] lowerVideoStreams = TrackSupport.byType(lowerTrack.getStreams(), VideoStream.class);
735 if (lowerVideoStreams.length == 0) {
736 logger.warn("No video stream available in the lower track! {}", lowerTrack);
737 return createResult(mediaPackage, Action.SKIP);
738 }
739
740
741 Dimension upperDimensions = Dimension.dimension(upperVideoStreams[0].getFrameWidth(),
742 upperVideoStreams[0].getFrameHeight());
743 Dimension lowerDimensions = Dimension.dimension(lowerVideoStreams[0].getFrameWidth(),
744 lowerVideoStreams[0].getFrameHeight());
745
746
747 Dimension outputDimension = null;
748 String outputResolutionSource = compositeSettings.getOutputResolutionSource();
749 if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_FIXED)) {
750 outputDimension = compositeSettings.getOutputDimension();
751 } else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_LOWER)) {
752 outputDimension = lowerDimensions;
753 } else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_UPPER)) {
754 outputDimension = upperDimensions;
755 }
756
757
758 List<Tuple<Dimension, HorizontalCoverageLayoutSpec>> shapes = new ArrayList<Tuple<Dimension, HorizontalCoverageLayoutSpec>>();
759 shapes.add(0, Tuple.tuple(lowerDimensions, layouts.get(0)));
760 shapes.add(1, Tuple.tuple(upperDimensions, layouts.get(1)));
761
762
763 MultiShapeLayout multiShapeLayout = LayoutManager
764 .multiShapeLayout(outputDimension, shapes);
765
766
767 LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>(lowerTrack, multiShapeLayout.getShapes()
768 .get(0));
769 LaidOutElement<Track> upperLaidOutElement = new LaidOutElement<Track>(upperTrack, multiShapeLayout.getShapes()
770 .get(1));
771
772
773 Optional<LaidOutElement<Attachment>> watermarkOption = createWatermarkLaidOutElement(compositeSettings,
774 outputDimension, watermarkAttachment);
775
776 Job compositeJob = composerService.composite(outputDimension, Optional
777 .ofNullable(upperLaidOutElement), lowerLaidOutElement, watermarkOption, compositeSettings.getProfile()
778 .getIdentifier(), compositeSettings.getOutputBackground(), compositeSettings.getSourceAudioName());
779
780
781 if (!waitForStatus(compositeJob).isSuccess())
782 throw new WorkflowOperationException("The composite job did not complete successfully");
783
784 if (compositeJob.getPayload().length() > 0) {
785
786 Track compoundTrack = (Track) MediaPackageElementParser.getFromXml(compositeJob.getPayload());
787
788 compoundTrack.setURI(workspace.moveTo(compoundTrack.getURI(), mediaPackage.getIdentifier().toString(),
789 compoundTrack.getIdentifier(),
790 "composite." + FilenameUtils.getExtension(compoundTrack.getURI().toString())));
791
792
793 applyTargetTagsToElement(compositeSettings.getTargetTags(), compoundTrack);
794
795
796 compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
797 logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
798
799
800 mediaPackage.add(compoundTrack);
801 WorkflowOperationResult result = createResult(mediaPackage, Action.CONTINUE, compositeJob.getQueueTime());
802 logger.debug("Composite operation completed");
803 return result;
804 } else {
805 logger.info("Composite operation unsuccessful, no payload returned: {}", compositeJob);
806 return createResult(mediaPackage, Action.SKIP);
807 }
808 } finally {
809 if (compositeSettings.getSourceUrlWatermark() != null)
810 workspace.deleteFromCollection(
811 COLLECTION,
812 compositeSettings.getWatermarkIdentifier() + "."
813 + FilenameUtils.getExtension(compositeSettings.getSourceUrlWatermark()));
814 }
815 }
816
817 @Reference
818 @Override
819 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
820 super.setServiceRegistry(serviceRegistry);
821 }
822
823 }