View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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   * The workflow definition for handling "composite" operations
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   /** The logging facility */
123   private static final Logger logger = LoggerFactory.getLogger(CompositeWorkflowOperationHandler.class);
124 
125   /** The legal options for SOURCE_AUDIO_NAME */
126   private static final Pattern sourceAudioOption = Pattern.compile(
127           ComposerService.LOWER + "|" + ComposerService.UPPER + "|" + ComposerService.BOTH, Pattern.CASE_INSENSITIVE);
128 
129   /** The composer service */
130   private ComposerService composerService = null;
131 
132   /** The local workspace */
133   private Workspace workspace = null;
134 
135   /**
136    * Callback for the OSGi declarative services configuration.
137    *
138    * @param composerService
139    *          the local composer service
140    */
141   @Reference
142   public void setComposerService(ComposerService composerService) {
143     this.composerService = composerService;
144   }
145 
146   /**
147    * Callback for declarative services configuration that will introduce us to the local workspace service.
148    * Implementation assumes that the reference is configured as being static.
149    *
150    * @param workspace
151    *          an instance of the workspace
152    */
153   @Reference
154   public void setWorkspace(Workspace workspace) {
155     this.workspace = workspace;
156   }
157 
158   /**
159    * {@inheritDoc}
160    *
161    * @see org.opencastproject.workflow.api.WorkflowOperationHandler#start(
162    *      org.opencastproject.workflow.api.WorkflowInstance, JobContext)
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     // There is only a single track to work with.
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       // Look for upper elements matching the tags and flavor
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       // Look for lower elements matching the tags and flavor
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    * This class collects and calculates all of the relevant data for doing a composite whether there is a single or two
273    * video tracks.
274    */
275   private class CompositeSettings {
276 
277     /** Use a fixed output resolution */
278     public static final String OUTPUT_RESOLUTION_FIXED = "fixed";
279 
280     /** Use resolution of lower part as output resolution */
281     public static final String OUTPUT_RESOLUTION_LOWER =  "lower";
282 
283     /** Use resolution of upper part as output resolution */
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; // default
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       // Find the encoding profile
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       // Output resolution
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       // Make sure either one of tags or flavor for the upper source are provided
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       // Make sure either one of tags or flavor for the lower source are provided
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       // Support legacy "source-flavor-upper" option
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       // Support legacy "source-flavor-lower" option
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       // Support legacy "source-flavor-watermark" option
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       // Select the source tags upper
460       for (String tag : asList(sourceTagsUpper)) {
461         upperTrackSelector.addTag(tag);
462       }
463 
464       // Select the source tags lower
465       for (String tag : asList(sourceTagsLower)) {
466         lowerTrackSelector.addTag(tag);
467       }
468 
469       // Select the watermark source tags
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       // Read the video dimensions from the mediapackage stream information
625       Dimension videoDimension = Dimension.dimension(videoStreams[0].getFrameWidth(), videoStreams[0].getFrameHeight());
626 
627       // Create the video layout definitions
628       List<Tuple<Dimension, HorizontalCoverageLayoutSpec>> shapes =
629           new ArrayList<Tuple<Dimension, HorizontalCoverageLayoutSpec>>();
630       shapes.add(0, Tuple.tuple(videoDimension, compositeSettings.getSingleSourceLayout()));
631 
632       // Determine dimension of output
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       // Calculate the single layout
644       MultiShapeLayout multiShapeLayout = LayoutManager
645               .multiShapeLayout(outputDimension, shapes);
646 
647       // Create the laid out element for the videos
648       LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>(compositeSettings.getSingleTrack(),
649               multiShapeLayout.getShapes().get(0));
650 
651       // Create the optionally laid out element for the watermark
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       // Wait for the jobs to return
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         // Adjust the target tags
673         applyTargetTagsToElement(compositeSettings.getTargetTags(), compoundTrack);
674 
675         // Adjust the target flavor.
676         compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
677         logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
678 
679         // store new tracks to mediaPackage
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       //Such excellent documentation Orcale, much fun.  Because returning null is a totally sane thing to do here
710       //https://docs.oracle.com/javase/tutorial/2d/images/loadimage.html
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       // Read the video dimensions from the mediapackage stream information
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       // Determine dimension of output
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       // Create the video layout definitions
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       // Calculate the layout
781       MultiShapeLayout multiShapeLayout = LayoutManager
782               .multiShapeLayout(outputDimension, shapes);
783 
784       // Create the laid out element for the videos
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       // Create the optionally laid out element for the watermark
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       // Wait for the jobs to return
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         // Adjust the target tags
812         applyTargetTagsToElement(compositeSettings.getTargetTags(), compoundTrack);
813 
814         // Adjust the target flavor.
815         compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
816         logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
817 
818         // store new tracks to mediaPackage
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 }