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(org.opencastproject.workflow.api.WorkflowInstance,
162    *      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     Collection<Track> upperElements = compositeSettings.getUpperTrackSelector().select(mediaPackage, false);
225     Collection<Track> lowerElements = compositeSettings.getLowerTrackSelector().select(mediaPackage, false);
226 
227     // There is only a single track to work with.
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       // Look for upper elements matching the tags and flavor
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       // Look for lower elements matching the tags and flavor
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    * This class collects and calculates all of the relevant data for doing a composite whether there is a single or two
270    * video tracks.
271    */
272   private class CompositeSettings {
273 
274     /** Use a fixed output resolution */
275     public static final String OUTPUT_RESOLUTION_FIXED = "fixed";
276 
277     /** Use resolution of lower part as output resolution */
278     public static final String OUTPUT_RESOLUTION_LOWER =  "lower";
279 
280     /** Use resolution of upper part as output resolution */
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; // default
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       // Find the encoding profile
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       // Output resolution
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       // Make sure either one of tags or flavor for the upper source are provided
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       // Make sure either one of tags or flavor for the lower source are provided
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       // Support legacy "source-flavor-upper" option
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       // Support legacy "source-flavor-lower" option
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       // Support legacy "source-flavor-watermark" option
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       // Select the source tags upper
451       for (String tag : asList(sourceTagsUpper)) {
452         upperTrackSelector.addTag(tag);
453       }
454 
455       // Select the source tags lower
456       for (String tag : asList(sourceTagsLower)) {
457         lowerTrackSelector.addTag(tag);
458       }
459 
460       // Select the watermark source tags
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       // Read the video dimensions from the mediapackage stream information
612       Dimension videoDimension = Dimension.dimension(videoStreams[0].getFrameWidth(), videoStreams[0].getFrameHeight());
613 
614       // Create the video layout definitions
615       List<Tuple<Dimension, HorizontalCoverageLayoutSpec>> shapes = new ArrayList<Tuple<Dimension, HorizontalCoverageLayoutSpec>>();
616       shapes.add(0, Tuple.tuple(videoDimension, compositeSettings.getSingleSourceLayout()));
617 
618       // Determine dimension of output
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       // Calculate the single layout
630       MultiShapeLayout multiShapeLayout = LayoutManager
631               .multiShapeLayout(outputDimension, shapes);
632 
633       // Create the laid out element for the videos
634       LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>(compositeSettings.getSingleTrack(),
635               multiShapeLayout.getShapes().get(0));
636 
637       // Create the optionally laid out element for the watermark
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       // Wait for the jobs to return
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         // Adjust the target tags
658         applyTargetTagsToElement(compositeSettings.getTargetTags(), compoundTrack);
659 
660         // Adjust the target flavor.
661         compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
662         logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
663 
664         // store new tracks to mediaPackage
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       //Such excellent documentation Orcale, much fun.  Because returning null is a totally sane thing to do here
695       //https://docs.oracle.com/javase/tutorial/2d/images/loadimage.html
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       // Read the video dimensions from the mediapackage stream information
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       // Determine dimension of output
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       // Create the video layout definitions
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       // Calculate the layout
763       MultiShapeLayout multiShapeLayout = LayoutManager
764               .multiShapeLayout(outputDimension, shapes);
765 
766       // Create the laid out element for the videos
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       // Create the optionally laid out element for the watermark
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       // Wait for the jobs to return
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         // Adjust the target tags
793         applyTargetTagsToElement(compositeSettings.getTargetTags(), compoundTrack);
794 
795         // Adjust the target flavor.
796         compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
797         logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
798 
799         // store new tracks to mediaPackage
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 }