/**
* Licensed to The Apereo Foundation under one or more contributor license
* agreements. See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
*
* The Apereo Foundation licenses this file to you under the Educational
* Community License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License
* at:
*
* http://opensource.org/licenses/ecl2.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
*/
package org.opencastproject.workflow.handler.composer;
import static org.opencastproject.util.data.Collections.list;
import org.opencastproject.composer.api.ComposerService;
import org.opencastproject.composer.api.EncoderException;
import org.opencastproject.composer.api.EncodingProfile;
import org.opencastproject.composer.api.LaidOutElement;
import org.opencastproject.composer.layout.AbsolutePositionLayoutSpec;
import org.opencastproject.composer.layout.Dimension;
import org.opencastproject.composer.layout.HorizontalCoverageLayoutSpec;
import org.opencastproject.composer.layout.LayoutManager;
import org.opencastproject.composer.layout.MultiShapeLayout;
import org.opencastproject.composer.layout.Serializer;
import org.opencastproject.job.api.Job;
import org.opencastproject.job.api.JobContext;
import org.opencastproject.mediapackage.Attachment;
import org.opencastproject.mediapackage.MediaPackage;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElementParser;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.Track;
import org.opencastproject.mediapackage.TrackSupport;
import org.opencastproject.mediapackage.VideoStream;
import org.opencastproject.mediapackage.attachment.AttachmentImpl;
import org.opencastproject.mediapackage.selector.AbstractMediaPackageElementSelector;
import org.opencastproject.mediapackage.selector.AttachmentSelector;
import org.opencastproject.mediapackage.selector.TrackSelector;
import org.opencastproject.util.JsonObj;
import org.opencastproject.util.NotFoundException;
import org.opencastproject.util.UrlSupport;
import org.opencastproject.util.data.Option;
import org.opencastproject.util.data.Tuple;
import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationException;
import org.opencastproject.workflow.api.WorkflowOperationInstance;
import org.opencastproject.workflow.api.WorkflowOperationResult;
import org.opencastproject.workflow.api.WorkflowOperationResult.Action;
import org.opencastproject.workspace.api.Workspace;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import javax.imageio.ImageIO;
/**
* The workflow definition for handling "composite" operations
*/
public class CompositeWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
private static final String COLLECTION = "composite";
private static final String SOURCE_TAGS_UPPER = "source-tags-upper";
private static final String SOURCE_FLAVOR_UPPER = "source-flavor-upper";
private static final String SOURCE_TAGS_LOWER = "source-tags-lower";
private static final String SOURCE_FLAVOR_LOWER = "source-flavor-lower";
private static final String SOURCE_TAGS_WATERMARK = "source-tags-watermark";
private static final String SOURCE_FLAVOR_WATERMARK = "source-flavor-watermark";
private static final String SOURCE_URL_WATERMARK = "source-url-watermark";
private static final String TARGET_TAGS = "target-tags";
private static final String TARGET_FLAVOR = "target-flavor";
private static final String ENCODING_PROFILE = "encoding-profile";
private static final String LAYOUT = "layout";
private static final String LAYOUT_MULTIPLE = "layout-multiple";
private static final String LAYOUT_SINGLE = "layout-single";
private static final String LAYOUT_PREFIX = "layout-";
private static final String OUTPUT_RESOLUTION = "output-resolution";
private static final String OUTPUT_BACKGROUND = "output-background";
private static final String DEFAULT_BG_COLOR = "black";
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(CompositeWorkflowOperationHandler.class);
/** The configuration options for this handler */
private static final SortedMap<String, String> CONFIG_OPTIONS;
static {
CONFIG_OPTIONS = new TreeMap<String, String>();
CONFIG_OPTIONS.put(SOURCE_TAGS_UPPER, "The \"tag\" of the upper track to use as a source input");
CONFIG_OPTIONS.put(SOURCE_FLAVOR_UPPER, "The \"flavor\" of the upper track to use as a source input");
CONFIG_OPTIONS.put(SOURCE_TAGS_LOWER, "The \"tag\" of the lower track to use as a source input");
CONFIG_OPTIONS.put(SOURCE_FLAVOR_LOWER, "The \"flavor\" of the lower track to use as a source input");
CONFIG_OPTIONS.put(SOURCE_TAGS_WATERMARK, "The \"tag\" of the attachement image to use as a source input");
CONFIG_OPTIONS.put(SOURCE_FLAVOR_WATERMARK, "The \"flavor\" of the attachement image to use as a source input");
CONFIG_OPTIONS.put(SOURCE_URL_WATERMARK, "The \"URL\" of the fallback image to use as a source input");
CONFIG_OPTIONS.put(ENCODING_PROFILE, "The encoding profile to use");
CONFIG_OPTIONS.put(TARGET_TAGS, "The tags to apply to the compound video track");
CONFIG_OPTIONS.put(TARGET_FLAVOR, "The flavor to apply to the compound video track");
CONFIG_OPTIONS
.put(LAYOUT_MULTIPLE,
"The layout name to use or a semi-colon separated JSON layout definition (lower, upper, optional watermark) if there are multiple videos");
CONFIG_OPTIONS
.put(LAYOUT_SINGLE,
"The layout name to use or a semi-colon separated JSON layout definition (video, optional watermark) if there is a single video source");
CONFIG_OPTIONS.put(LAYOUT_PREFIX,
"Define semi-colon separated JSON layouts (lower, upper, optional watermark) to provide by name");
CONFIG_OPTIONS.put(OUTPUT_RESOLUTION, "The resulting resolution of the compound video e.g. 1900x1080");
CONFIG_OPTIONS.put(OUTPUT_BACKGROUND, "The resulting background color of the compound video e.g. black");
}
/** The composer service */
private ComposerService composerService = null;
/** The local workspace */
private Workspace workspace = null;
/**
* Callback for the OSGi declarative services configuration.
*
* @param composerService
* the local composer service
*/
public void setComposerService(ComposerService composerService) {
this.composerService = composerService;
}
/**
* Callback for declarative services configuration that will introduce us to the local workspace service.
* Implementation assumes that the reference is configured as being static.
*
* @param workspace
* an instance of the workspace
*/
public void setWorkspace(Workspace workspace) {
this.workspace = workspace;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.api.WorkflowOperationHandler#getConfigurationOptions()
*/
@Override
public SortedMap<String, String> getConfigurationOptions() {
return CONFIG_OPTIONS;
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.workflow.api.WorkflowOperationHandler#start(org.opencastproject.workflow.api.WorkflowInstance,
* JobContext)
*/
@Override
public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
throws WorkflowOperationException {
logger.debug("Running composite workflow operation on workflow {}", workflowInstance.getId());
try {
return composite(workflowInstance.getMediaPackage(), workflowInstance.getCurrentOperation());
} catch (Exception e) {
throw new WorkflowOperationException(e);
}
}
private WorkflowOperationResult composite(MediaPackage src, WorkflowOperationInstance operation)
throws EncoderException, IOException, NotFoundException, MediaPackageException, WorkflowOperationException {
MediaPackage mediaPackage = (MediaPackage) src.clone();
CompositeSettings compositeSettings;
try {
compositeSettings = new CompositeSettings(mediaPackage, operation);
} catch (IllegalArgumentException e) {
logger.warn("Unable to parse composite settings because {}", ExceptionUtils.getStackTrace(e));
return createResult(mediaPackage, Action.SKIP);
}
Option<Attachment> watermarkAttachment = Option.<Attachment> none();
Collection<Attachment> watermarkElements = compositeSettings.getWatermarkSelector().select(mediaPackage, false);
if (watermarkElements.size() > 1) {
logger.warn("More than one watermark attachment has been found for compositing, skipping compositing!: {}",
watermarkElements);
return createResult(mediaPackage, Action.SKIP);
} else if (watermarkElements.size() == 0 && compositeSettings.getSourceUrlWatermark() != null) {
logger.info("No watermark found from flavor and tags, take watermark from URL {}",
compositeSettings.getSourceUrlWatermark());
Attachment urlAttachment = new AttachmentImpl();
urlAttachment.setIdentifier(compositeSettings.getWatermarkIdentifier());
if (compositeSettings.getSourceUrlWatermark().startsWith("http")) {
urlAttachment.setURI(UrlSupport.uri(compositeSettings.getSourceUrlWatermark()));
} else {
InputStream in = null;
try {
in = UrlSupport.url(compositeSettings.getSourceUrlWatermark()).openStream();
URI imageUrl = workspace.putInCollection(COLLECTION, compositeSettings.getWatermarkIdentifier() + "."
+ FilenameUtils.getExtension(compositeSettings.getSourceUrlWatermark()), in);
urlAttachment.setURI(imageUrl);
} catch (Exception e) {
logger.warn("Unable to read watermark source url {}: {}", compositeSettings.getSourceUrlWatermark(), e);
throw new WorkflowOperationException("Unable to read watermark source url "
+ compositeSettings.getSourceUrlWatermark(), e);
} finally {
IOUtils.closeQuietly(in);
}
}
watermarkAttachment = Option.option(urlAttachment);
} else if (watermarkElements.size() == 0 && compositeSettings.getSourceUrlWatermark() == null) {
logger.info("No watermark to composite");
} else {
for (Attachment a : watermarkElements)
watermarkAttachment = Option.option(a);
}
Collection<Track> upperElements = compositeSettings.getUpperTrackSelector().select(mediaPackage, false);
Collection<Track> lowerElements = compositeSettings.getLowerTrackSelector().select(mediaPackage, false);
// There is only a single track to work with.
if ((upperElements.size() == 1 && lowerElements.size() == 0)
|| (upperElements.size() == 0 && lowerElements.size() == 1)) {
for (Track t : upperElements)
compositeSettings.setSingleTrack(t);
for (Track t : lowerElements)
compositeSettings.setSingleTrack(t);
return handleSingleTrack(mediaPackage, operation, compositeSettings, watermarkAttachment);
} else {
// Look for upper elements matching the tags and flavor
if (upperElements.size() > 1) {
logger.warn("More than one upper track has been found for compositing, skipping compositing!: {}",
upperElements);
return createResult(mediaPackage, Action.SKIP);
} else if (upperElements.size() == 0) {
logger.warn("No upper track has been found for compositing, skipping compositing!");
return createResult(mediaPackage, Action.SKIP);
}
for (Track t : upperElements) {
compositeSettings.setUpperTrack(t);
}
// Look for lower elements matching the tags and flavor
if (lowerElements.size() > 1) {
logger.warn("More than one lower track has been found for compositing, skipping compositing!: {}",
lowerElements);
return createResult(mediaPackage, Action.SKIP);
} else if (lowerElements.size() == 0) {
logger.warn("No lower track has been found for compositing, skipping compositing!");
return createResult(mediaPackage, Action.SKIP);
}
for (Track t : lowerElements) {
compositeSettings.setLowerTrack(t);
}
return handleMultipleTracks(mediaPackage, operation, compositeSettings, watermarkAttachment);
}
}
/**
* This class collects and calculates all of the relevant data for doing a composite whether there is a single or two
* video tracks.
*/
private class CompositeSettings {
/** Use a fixed output resolution */
public static final String OUTPUT_RESOLUTION_FIXED = "fixed";
/** Use resolution of lower part as output resolution */
public static final String OUTPUT_RESOLUTION_LOWER = "lower";
/** Use resolution of upper part as output resolution */
public static final String OUTPUT_RESOLUTION_UPPER = "upper";
private String sourceTagsUpper;
private String sourceFlavorUpper;
private String sourceTagsLower;
private String sourceFlavorLower;
private String sourceTagsWatermark;
private String sourceFlavorWatermark;
private String sourceUrlWatermark;
private String targetTagsOption;
private String targetFlavorOption;
private String encodingProfile;
private String layoutMultipleString;
private String layoutSingleString;
private String outputResolution;
private String outputBackground;
private AbstractMediaPackageElementSelector<Track> upperTrackSelector = new TrackSelector();
private AbstractMediaPackageElementSelector<Track> lowerTrackSelector = new TrackSelector();
private AbstractMediaPackageElementSelector<Attachment> watermarkSelector = new AttachmentSelector();
private String watermarkIdentifier;
private Option<AbsolutePositionLayoutSpec> watermarkLayout = Option.none();
private List<HorizontalCoverageLayoutSpec> multiSourceLayouts = new ArrayList<HorizontalCoverageLayoutSpec>();
private HorizontalCoverageLayoutSpec singleSourceLayout;
private Track upperTrack;
private Track lowerTrack;
private Track singleTrack;
private String outputResolutionSource;
private Dimension outputDimension;
private EncodingProfile profile;
private List<String> targetTags;
private MediaPackageElementFlavor targetFlavor = null;
CompositeSettings(MediaPackage mediaPackage, WorkflowOperationInstance operation)
throws WorkflowOperationException {
// Check which tags have been configured
sourceTagsUpper = StringUtils.trimToNull(operation.getConfiguration(SOURCE_TAGS_UPPER));
sourceFlavorUpper = StringUtils.trimToNull(operation.getConfiguration(SOURCE_FLAVOR_UPPER));
sourceTagsLower = StringUtils.trimToNull(operation.getConfiguration(SOURCE_TAGS_LOWER));
sourceFlavorLower = StringUtils.trimToNull(operation.getConfiguration(SOURCE_FLAVOR_LOWER));
sourceTagsWatermark = StringUtils.trimToNull(operation.getConfiguration(SOURCE_TAGS_WATERMARK));
sourceFlavorWatermark = StringUtils.trimToNull(operation.getConfiguration(SOURCE_FLAVOR_WATERMARK));
sourceUrlWatermark = StringUtils.trimToNull(operation.getConfiguration(SOURCE_URL_WATERMARK));
targetTagsOption = StringUtils.trimToNull(operation.getConfiguration(TARGET_TAGS));
targetFlavorOption = StringUtils.trimToNull(operation.getConfiguration(TARGET_FLAVOR));
encodingProfile = StringUtils.trimToNull(operation.getConfiguration(ENCODING_PROFILE));
layoutMultipleString = StringUtils.trimToNull(operation.getConfiguration(LAYOUT_MULTIPLE));
if (layoutMultipleString == null) {
layoutMultipleString = StringUtils.trimToNull(operation.getConfiguration(LAYOUT));
}
if (layoutMultipleString != null && !layoutMultipleString.contains(";")) {
layoutMultipleString = StringUtils.trimToNull(operation.getConfiguration(LAYOUT_PREFIX + layoutMultipleString));
}
layoutSingleString = StringUtils.trimToNull(operation.getConfiguration(LAYOUT_SINGLE));
outputResolution = StringUtils.trimToNull(operation.getConfiguration(OUTPUT_RESOLUTION));
outputBackground = StringUtils.trimToNull(operation.getConfiguration(OUTPUT_BACKGROUND));
watermarkIdentifier = UUID.randomUUID().toString();
if (outputBackground == null) {
outputBackground = DEFAULT_BG_COLOR;
}
if (layoutMultipleString != null) {
Tuple<List<HorizontalCoverageLayoutSpec>, Option<AbsolutePositionLayoutSpec>> multipleLayouts = parseMultipleLayouts(layoutMultipleString);
multiSourceLayouts.addAll(multipleLayouts.getA());
watermarkLayout = multipleLayouts.getB();
}
if (layoutSingleString != null) {
Tuple<HorizontalCoverageLayoutSpec, Option<AbsolutePositionLayoutSpec>> singleLayouts = parseSingleLayouts(layoutSingleString);
singleSourceLayout = singleLayouts.getA();
watermarkLayout = singleLayouts.getB();
}
// Find the encoding profile
if (encodingProfile == null)
throw new WorkflowOperationException("Encoding profile must be set!");
profile = composerService.getProfile(encodingProfile);
if (profile == null)
throw new WorkflowOperationException("Encoding profile '" + encodingProfile + "' was not found");
// Target tags
targetTags = asList(targetTagsOption);
// Target flavor
if (targetFlavorOption == null)
throw new WorkflowOperationException("Target flavor must be set!");
// Output resolution
if (outputResolution == null)
throw new WorkflowOperationException("Output resolution must be set!");
if (outputResolution.equals(OUTPUT_RESOLUTION_LOWER) || outputResolution.equals(OUTPUT_RESOLUTION_UPPER)) {
outputResolutionSource = outputResolution;
} else {
outputResolutionSource = OUTPUT_RESOLUTION_FIXED;
try {
String[] outputResolutionArray = StringUtils.split(outputResolution, "x");
if (outputResolutionArray.length != 2) {
throw new WorkflowOperationException("Invalid format of output resolution!");
}
outputDimension = Dimension.dimension(Integer.parseInt(outputResolutionArray[0]),
Integer.parseInt(outputResolutionArray[1]));
} catch (Exception e) {
throw new WorkflowOperationException("Unable to parse output resolution!", e);
}
}
// Make sure either one of tags or flavor for the upper source are provided
if (sourceTagsUpper == null && sourceFlavorUpper == null) {
throw new IllegalArgumentException(
"No source tags or flavor for the upper video have been specified, not matching anything");
}
// Make sure either one of tags or flavor for the lower source are provided
if (sourceTagsLower == null && sourceFlavorLower == null) {
throw new IllegalArgumentException(
"No source tags or flavor for the lower video have been specified, not matching anything");
}
try {
targetFlavor = MediaPackageElementFlavor.parseFlavor(targetFlavorOption);
if ("*".equals(targetFlavor.getType()) || "*".equals(targetFlavor.getSubtype()))
throw new WorkflowOperationException("Target flavor must have a type and a subtype, '*' are not allowed!");
} catch (IllegalArgumentException e) {
throw new WorkflowOperationException("Target flavor '" + targetFlavorOption + "' is malformed");
}
// Support legacy "source-flavor-upper" option
if (sourceFlavorUpper != null) {
try {
upperTrackSelector.addFlavor(MediaPackageElementFlavor.parseFlavor(sourceFlavorUpper));
} catch (IllegalArgumentException e) {
throw new WorkflowOperationException("Source upper flavor '" + sourceFlavorUpper + "' is malformed");
}
}
// Support legacy "source-flavor-lower" option
if (sourceFlavorLower != null) {
try {
lowerTrackSelector.addFlavor(MediaPackageElementFlavor.parseFlavor(sourceFlavorLower));
} catch (IllegalArgumentException e) {
throw new WorkflowOperationException("Source lower flavor '" + sourceFlavorLower + "' is malformed");
}
}
// Support legacy "source-flavor-watermark" option
if (sourceFlavorWatermark != null) {
try {
watermarkSelector.addFlavor(MediaPackageElementFlavor.parseFlavor(sourceFlavorWatermark));
} catch (IllegalArgumentException e) {
throw new WorkflowOperationException("Source watermark flavor '" + sourceFlavorWatermark + "' is malformed");
}
}
// Select the source tags upper
for (String tag : asList(sourceTagsUpper)) {
upperTrackSelector.addTag(tag);
}
// Select the source tags lower
for (String tag : asList(sourceTagsLower)) {
lowerTrackSelector.addTag(tag);
}
// Select the watermark source tags
for (String tag : asList(sourceTagsWatermark)) {
watermarkSelector.addTag(tag);
}
}
private Tuple<List<HorizontalCoverageLayoutSpec>, Option<AbsolutePositionLayoutSpec>> parseMultipleLayouts(
String layoutString) throws WorkflowOperationException {
try {
String[] layouts = StringUtils.split(layoutString, ";");
if (layouts.length < 2)
throw new WorkflowOperationException(
"Multiple layout doesn't contain the required layouts for (lower, upper, optional watermark)");
List<HorizontalCoverageLayoutSpec> multipleLayouts = list(
Serializer.horizontalCoverageLayoutSpec(JsonObj.jsonObj(layouts[0])),
Serializer.horizontalCoverageLayoutSpec(JsonObj.jsonObj(layouts[1])));
AbsolutePositionLayoutSpec watermarkLayout = null;
if (layouts.length > 2)
watermarkLayout = Serializer.absolutePositionLayoutSpec(JsonObj.jsonObj(layouts[2]));
return Tuple.tuple(multipleLayouts, Option.option(watermarkLayout));
} catch (Exception e) {
throw new WorkflowOperationException("Unable to parse layout!", e);
}
}
private Tuple<HorizontalCoverageLayoutSpec, Option<AbsolutePositionLayoutSpec>> parseSingleLayouts(
String layoutString) throws WorkflowOperationException {
try {
String[] layouts = StringUtils.split(layoutString, ";");
if (layouts.length < 1)
throw new WorkflowOperationException(
"Single layout doesn't contain the required layouts for (video, optional watermark)");
HorizontalCoverageLayoutSpec singleLayout = Serializer
.horizontalCoverageLayoutSpec(JsonObj.jsonObj(layouts[0]));
AbsolutePositionLayoutSpec watermarkLayout = null;
if (layouts.length > 1)
watermarkLayout = Serializer.absolutePositionLayoutSpec(JsonObj.jsonObj(layouts[1]));
return Tuple.tuple(singleLayout, Option.option(watermarkLayout));
} catch (Exception e) {
throw new WorkflowOperationException("Unable to parse layout!", e);
}
}
public String getSourceUrlWatermark() {
return sourceUrlWatermark;
}
public MediaPackageElementFlavor getTargetFlavor() {
return targetFlavor;
}
public List<String> getTargetTags() {
return targetTags;
}
public String getOutputBackground() {
return outputBackground;
}
public AbstractMediaPackageElementSelector<Track> getUpperTrackSelector() {
return upperTrackSelector;
}
public AbstractMediaPackageElementSelector<Track> getLowerTrackSelector() {
return lowerTrackSelector;
}
public AbstractMediaPackageElementSelector<Attachment> getWatermarkSelector() {
return watermarkSelector;
}
public String getWatermarkIdentifier() {
return watermarkIdentifier;
}
public Option<AbsolutePositionLayoutSpec> getWatermarkLayout() {
return watermarkLayout;
}
public List<HorizontalCoverageLayoutSpec> getMultiSourceLayouts() {
return multiSourceLayouts;
}
public HorizontalCoverageLayoutSpec getSingleSourceLayout() {
return singleSourceLayout;
}
public Track getUpperTrack() {
return upperTrack;
}
public void setUpperTrack(Track upperTrack) {
this.upperTrack = upperTrack;
}
public Track getLowerTrack() {
return lowerTrack;
}
public void setLowerTrack(Track lowerTrack) {
this.lowerTrack = lowerTrack;
}
public Track getSingleTrack() {
return singleTrack;
}
public void setSingleTrack(Track singleTrack) {
this.singleTrack = singleTrack;
}
public String getOutputResolutionSource() {
return outputResolutionSource;
}
public Dimension getOutputDimension() {
return outputDimension;
}
public EncodingProfile getProfile() {
return profile;
}
}
private WorkflowOperationResult handleSingleTrack(MediaPackage mediaPackage, WorkflowOperationInstance operation,
CompositeSettings compositeSettings, Option<Attachment> watermarkAttachment) throws EncoderException,
IOException, NotFoundException, MediaPackageException, WorkflowOperationException {
if (compositeSettings.getSingleSourceLayout() == null) {
throw new WorkflowOperationException("Single video layout must be set! Please verify that you have a "
+ LAYOUT_SINGLE + " property in your composite operation in your workflow definition.");
}
try {
VideoStream[] videoStreams = TrackSupport.byType(compositeSettings.getSingleTrack().getStreams(),
VideoStream.class);
if (videoStreams.length == 0) {
logger.warn("No video stream available to compose! {}", compositeSettings.getSingleTrack());
return createResult(mediaPackage, Action.SKIP);
}
// Read the video dimensions from the mediapackage stream information
Dimension videoDimension = Dimension.dimension(videoStreams[0].getFrameWidth(), videoStreams[0].getFrameHeight());
// Create the video layout definitions
List<Tuple<Dimension, HorizontalCoverageLayoutSpec>> shapes = new ArrayList<Tuple<Dimension, HorizontalCoverageLayoutSpec>>();
shapes.add(0, Tuple.tuple(videoDimension, compositeSettings.getSingleSourceLayout()));
// Determine dimension of output
Dimension outputDimension = null;
String outputResolutionSource = compositeSettings.getOutputResolutionSource();
if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_FIXED)) {
outputDimension = compositeSettings.getOutputDimension();
} else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_LOWER)) {
outputDimension = videoDimension;
} else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_UPPER)) {
outputDimension = videoDimension;
}
// Calculate the single layout
MultiShapeLayout multiShapeLayout = LayoutManager
.multiShapeLayout(outputDimension, shapes);
// Create the laid out element for the videos
LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>(compositeSettings.getSingleTrack(),
multiShapeLayout.getShapes().get(0));
// Create the optionally laid out element for the watermark
Option<LaidOutElement<Attachment>> watermarkOption = createWatermarkLaidOutElement(compositeSettings,
outputDimension, watermarkAttachment);
Job compositeJob = composerService.composite(outputDimension, Option
.<LaidOutElement<Track>> none(), lowerLaidOutElement, watermarkOption, compositeSettings.getProfile()
.getIdentifier(), compositeSettings.getOutputBackground());
// Wait for the jobs to return
if (!waitForStatus(compositeJob).isSuccess())
throw new WorkflowOperationException("The composite job did not complete successfully");
if (compositeJob.getPayload().length() > 0) {
Track compoundTrack = (Track) MediaPackageElementParser.getFromXml(compositeJob.getPayload());
compoundTrack.setURI(workspace.moveTo(compoundTrack.getURI(), mediaPackage.getIdentifier().toString(),
compoundTrack.getIdentifier(),
"composite." + FilenameUtils.getExtension(compoundTrack.getURI().toString())));
// Adjust the target tags
for (String tag : compositeSettings.getTargetTags()) {
logger.trace("Tagging compound track with '{}'", tag);
compoundTrack.addTag(tag);
}
// Adjust the target flavor.
compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
// store new tracks to mediaPackage
mediaPackage.add(compoundTrack);
WorkflowOperationResult result = createResult(mediaPackage, Action.CONTINUE, compositeJob.getQueueTime());
logger.debug("Composite operation completed");
return result;
} else {
logger.info("Composite operation unsuccessful, no payload returned: {}", compositeJob);
return createResult(mediaPackage, Action.SKIP);
}
} finally {
if (compositeSettings.getSourceUrlWatermark() != null)
workspace.deleteFromCollection(
COLLECTION,
compositeSettings.getWatermarkIdentifier() + "."
+ FilenameUtils.getExtension(compositeSettings.getSourceUrlWatermark()));
}
}
private Option<LaidOutElement<Attachment>> createWatermarkLaidOutElement(CompositeSettings compositeSettings,
Dimension outputDimension, Option<Attachment> watermarkAttachment) throws WorkflowOperationException {
Option<LaidOutElement<Attachment>> watermarkOption = Option.<LaidOutElement<Attachment>> none();
if (watermarkAttachment.isSome() && compositeSettings.getWatermarkLayout().isSome()) {
BufferedImage image;
try {
File watermarkFile = workspace.get(watermarkAttachment.get().getURI());
image = ImageIO.read(watermarkFile);
} catch (Exception e) {
logger.warn("Unable to read the watermark image attachment {}: {}", watermarkAttachment.get().getURI(), e);
throw new WorkflowOperationException("Unable to read the watermark image attachment", e);
}
Dimension imageDimension = Dimension.dimension(image.getWidth(), image.getHeight());
List<Tuple<Dimension, AbsolutePositionLayoutSpec>> watermarkShapes = new ArrayList<Tuple<Dimension, AbsolutePositionLayoutSpec>>();
watermarkShapes.add(0, Tuple.tuple(imageDimension, compositeSettings.getWatermarkLayout().get()));
MultiShapeLayout watermarkLayout = LayoutManager.absoluteMultiShapeLayout(outputDimension,
watermarkShapes);
watermarkOption = Option.some(new LaidOutElement<Attachment>(watermarkAttachment.get(), watermarkLayout
.getShapes().get(0)));
}
return watermarkOption;
}
private WorkflowOperationResult handleMultipleTracks(MediaPackage mediaPackage, WorkflowOperationInstance operation,
CompositeSettings compositeSettings, Option<Attachment> watermarkAttachment) throws EncoderException,
IOException, NotFoundException, MediaPackageException, WorkflowOperationException {
if (compositeSettings.getMultiSourceLayouts() == null || compositeSettings.getMultiSourceLayouts().size() == 0) {
throw new WorkflowOperationException(
"Multi video layout must be set! Please verify that you have a "
+ LAYOUT_MULTIPLE
+ " or "
+ LAYOUT
+ " property in your composite operation in your workflow definition to be able to handle multiple videos");
}
try {
Track upperTrack = compositeSettings.getUpperTrack();
Track lowerTrack = compositeSettings.getLowerTrack();
List<HorizontalCoverageLayoutSpec> layouts = compositeSettings.getMultiSourceLayouts();
VideoStream[] upperVideoStreams = TrackSupport.byType(upperTrack.getStreams(), VideoStream.class);
if (upperVideoStreams.length == 0) {
logger.warn("No video stream available in the upper track! {}", upperTrack);
return createResult(mediaPackage, Action.SKIP);
}
VideoStream[] lowerVideoStreams = TrackSupport.byType(lowerTrack.getStreams(), VideoStream.class);
if (lowerVideoStreams.length == 0) {
logger.warn("No video stream available in the lower track! {}", lowerTrack);
return createResult(mediaPackage, Action.SKIP);
}
// Read the video dimensions from the mediapackage stream information
Dimension upperDimensions = Dimension.dimension(upperVideoStreams[0].getFrameWidth(),
upperVideoStreams[0].getFrameHeight());
Dimension lowerDimensions = Dimension.dimension(lowerVideoStreams[0].getFrameWidth(),
lowerVideoStreams[0].getFrameHeight());
// Determine dimension of output
Dimension outputDimension = null;
String outputResolutionSource = compositeSettings.getOutputResolutionSource();
if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_FIXED)) {
outputDimension = compositeSettings.getOutputDimension();
} else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_LOWER)) {
outputDimension = lowerDimensions;
} else if (outputResolutionSource.equals(CompositeSettings.OUTPUT_RESOLUTION_UPPER)) {
outputDimension = upperDimensions;
}
// Create the video layout definitions
List<Tuple<Dimension, HorizontalCoverageLayoutSpec>> shapes = new ArrayList<Tuple<Dimension, HorizontalCoverageLayoutSpec>>();
shapes.add(0, Tuple.tuple(lowerDimensions, layouts.get(0)));
shapes.add(1, Tuple.tuple(upperDimensions, layouts.get(1)));
// Calculate the layout
MultiShapeLayout multiShapeLayout = LayoutManager
.multiShapeLayout(outputDimension, shapes);
// Create the laid out element for the videos
LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>(lowerTrack, multiShapeLayout.getShapes()
.get(0));
LaidOutElement<Track> upperLaidOutElement = new LaidOutElement<Track>(upperTrack, multiShapeLayout.getShapes()
.get(1));
// Create the optionally laid out element for the watermark
Option<LaidOutElement<Attachment>> watermarkOption = createWatermarkLaidOutElement(compositeSettings,
outputDimension, watermarkAttachment);
Job compositeJob = composerService.composite(outputDimension, Option
.option(upperLaidOutElement), lowerLaidOutElement, watermarkOption, compositeSettings.getProfile()
.getIdentifier(), compositeSettings.getOutputBackground());
// Wait for the jobs to return
if (!waitForStatus(compositeJob).isSuccess())
throw new WorkflowOperationException("The composite job did not complete successfully");
if (compositeJob.getPayload().length() > 0) {
Track compoundTrack = (Track) MediaPackageElementParser.getFromXml(compositeJob.getPayload());
compoundTrack.setURI(workspace.moveTo(compoundTrack.getURI(), mediaPackage.getIdentifier().toString(),
compoundTrack.getIdentifier(),
"composite." + FilenameUtils.getExtension(compoundTrack.getURI().toString())));
// Adjust the target tags
for (String tag : compositeSettings.getTargetTags()) {
logger.trace("Tagging compound track with '{}'", tag);
compoundTrack.addTag(tag);
}
// Adjust the target flavor.
compoundTrack.setFlavor(compositeSettings.getTargetFlavor());
logger.debug("Compound track has flavor '{}'", compoundTrack.getFlavor());
// store new tracks to mediaPackage
mediaPackage.add(compoundTrack);
WorkflowOperationResult result = createResult(mediaPackage, Action.CONTINUE, compositeJob.getQueueTime());
logger.debug("Composite operation completed");
return result;
} else {
logger.info("Composite operation unsuccessful, no payload returned: {}", compositeJob);
return createResult(mediaPackage, Action.SKIP);
}
} finally {
if (compositeSettings.getSourceUrlWatermark() != null)
workspace.deleteFromCollection(
COLLECTION,
compositeSettings.getWatermarkIdentifier() + "."
+ FilenameUtils.getExtension(compositeSettings.getSourceUrlWatermark()));
}
}
}