/**
* 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.nil;
import static org.opencastproject.util.data.Collections.smap;
import static org.opencastproject.util.data.Monadics.mlist;
import static org.opencastproject.util.data.Tuple.tuple;
import org.opencastproject.composer.api.ComposerService;
import org.opencastproject.composer.api.EncoderException;
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.MediaPackageElement;
import org.opencastproject.mediapackage.MediaPackageElementFlavor;
import org.opencastproject.mediapackage.MediaPackageElementParser;
import org.opencastproject.mediapackage.MediaPackageException;
import org.opencastproject.mediapackage.MediaPackageSupport.Filters;
import org.opencastproject.mediapackage.Track;
import org.opencastproject.util.JobUtil;
import org.opencastproject.util.data.Function;
import org.opencastproject.util.data.Function2;
import org.opencastproject.util.data.Monadics;
import org.opencastproject.util.data.Option;
import org.opencastproject.util.data.functions.Booleans;
import org.opencastproject.util.data.functions.Misc;
import org.opencastproject.util.data.functions.Strings;
import org.opencastproject.workflow.api.AbstractWorkflowOperationHandler;
import org.opencastproject.workflow.api.WorkflowInstance;
import org.opencastproject.workflow.api.WorkflowOperationException;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.SortedMap;
/**
* The workflow definition creating a video from a still image.
*/
public class ImageToVideoWorkflowOperationHandler extends AbstractWorkflowOperationHandler {
private static final String OPT_SOURCE_TAGS = "source-tags";
private static final String OPT_SOURCE_FLAVOR = "source-flavor";
private static final String OPT_TARGET_TAGS = "target-tags";
private static final String OPT_TARGET_FLAVOR = "target-flavor";
private static final String OPT_DURATION = "duration";
private static final String OPT_PROFILE = "profile";
/** The logging facility */
private static final Logger logger = LoggerFactory.getLogger(ImageToVideoWorkflowOperationHandler.class);
/** The configuration options for this handler */
private static final SortedMap<String, String> CONFIG_OPTIONS = smap(
tuple(OPT_SOURCE_TAGS, "The image attachment must be tagged with one of the given tags"),
tuple(OPT_SOURCE_FLAVOR, "The image attachment must be of the given flavor"),
tuple(OPT_DURATION, "The duration of the resulting video in seconds"));
/** 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;
}
@Override
public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context)
throws WorkflowOperationException {
logger.debug("Running image to video workflow operation on workflow {}", workflowInstance.getId());
try {
return imageToVideo(workflowInstance.getMediaPackage(), workflowInstance);
} catch (WorkflowOperationException e) {
throw e;
} catch (Exception e) {
throw new WorkflowOperationException(e);
}
}
private WorkflowOperationResult imageToVideo(MediaPackage mp, WorkflowInstance wi) throws Exception {
// read cfg
final List<String> sourceTags = getCfg(wi, OPT_SOURCE_TAGS).map(asList).getOrElse(nil(String.class));
final Option<MediaPackageElementFlavor> sourceFlavor = getCfg(wi, OPT_SOURCE_FLAVOR).map(
MediaPackageElementFlavor.parseFlavor);
if (sourceFlavor.isNone() && sourceTags.isEmpty()) {
logger.warn("No source tags or flavor are given to determine the image to use");
return createResult(mp, Action.SKIP);
}
final List<String> targetTags = getCfg(wi, OPT_TARGET_TAGS).map(asList).getOrElse(nil(String.class));
final Option<MediaPackageElementFlavor> targetFlavor = getCfg(wi, OPT_TARGET_FLAVOR).map(
MediaPackageElementFlavor.parseFlavor);
final double duration = getCfg(wi, OPT_DURATION).bind(Strings.toDouble).getOrElse(
this.<Double> cfgKeyMissing(OPT_DURATION));
final String profile = getCfg(wi, OPT_PROFILE).getOrElse(this.<String> cfgKeyMissing(OPT_PROFILE));
// run image to video jobs
final List<Job> jobs = Monadics.<MediaPackageElement> mlist(mp.getAttachments())
.filter(sourceFlavor.map(Filters.matchesFlavor).getOrElse(Booleans.<MediaPackageElement> yes()))
.filter(Filters.hasTagAny(sourceTags)).map(Misc.<MediaPackageElement, Attachment> cast())
.map(imageToVideo(profile, duration)).value();
if (JobUtil.waitForJobs(serviceRegistry, jobs).isSuccess()) {
for (final Job job : jobs) {
if (job.getPayload().length() > 0) {
Track track = (Track) MediaPackageElementParser.getFromXml(job.getPayload());
track.setURI(workspace.moveTo(track.getURI(), mp.getIdentifier().toString(), track.getIdentifier(),
FilenameUtils.getName(track.getURI().toString())));
// Adjust the target tags
for (String tag : targetTags) {
track.addTag(tag);
}
// Adjust the target flavor.
for (MediaPackageElementFlavor flavor : targetFlavor) {
track.setFlavor(flavor);
}
// store new tracks to mediaPackage
mp.add(track);
logger.debug("Image to video operation completed");
} else {
logger.info("Image to video operation unsuccessful, no payload returned: {}", job);
return createResult(mp, Action.SKIP);
}
}
return createResult(mp, Action.CONTINUE, mlist(jobs).foldl(0L, new Function2<Long, Job, Long>() {
@Override
public Long apply(Long max, Job job) {
return Math.max(max, job.getQueueTime());
}
}));
} else {
throw new WorkflowOperationException("The image to video encoding jobs did not return successfully");
}
}
/** Returned function may throw exceptions. */
private Function<Attachment, Job> imageToVideo(final String profile, final double duration) {
return new Function.X<Attachment, Job>() {
@Override
protected Job xapply(Attachment attachment) throws MediaPackageException, EncoderException {
logger.info("Converting image {} to a video of {} sec", attachment.getURI().toString(), duration);
return composerService.imageToVideo(attachment, profile, duration);
}
};
}
}