/** * 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 org.opencastproject.composer.api.ComposerService; import org.opencastproject.composer.api.EncoderException; import org.opencastproject.composer.api.EncodingProfile; import org.opencastproject.job.api.Job; import org.opencastproject.job.api.JobContext; import org.opencastproject.mediapackage.Attachment; import org.opencastproject.mediapackage.Catalog; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageElement; import org.opencastproject.mediapackage.MediaPackageElementFlavor; import org.opencastproject.mediapackage.MediaPackageElementParser; import org.opencastproject.mediapackage.MediaPackageElements; import org.opencastproject.mediapackage.MediaPackageException; import org.opencastproject.mediapackage.MediaPackageReference; import org.opencastproject.mediapackage.MediaPackageReferenceImpl; import org.opencastproject.mediapackage.Track; import org.opencastproject.metadata.mpeg7.MediaTimePoint; import org.opencastproject.metadata.mpeg7.Mpeg7Catalog; import org.opencastproject.metadata.mpeg7.Mpeg7CatalogService; import org.opencastproject.metadata.mpeg7.Segment; import org.opencastproject.metadata.mpeg7.TemporalDecomposition; import org.opencastproject.metadata.mpeg7.Video; import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.opencastproject.util.MimeTypes; import org.opencastproject.util.NotFoundException; 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.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.concurrent.ExecutionException; /** * The workflow definition for creating segment preview images from an segment mpeg-7 catalog. */ public class SegmentPreviewsWorkflowOperationHandler extends AbstractWorkflowOperationHandler { /** The logging facility */ private static final Logger logger = LoggerFactory.getLogger(SegmentPreviewsWorkflowOperationHandler.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-flavor", "The \"flavor\" of the track to use as a video source input"); CONFIG_OPTIONS.put("source-tags", "The required tags that must exist on the track for the track to be used as a video source"); CONFIG_OPTIONS.put("encoding-profile", "The encoding profile to use for generating the image"); CONFIG_OPTIONS.put("reference-flavor", "The \"flavor\" of the track to used as the reference"); CONFIG_OPTIONS.put("reference-tags", "The \"tags\" of the track to used as the reference"); CONFIG_OPTIONS.put("target-flavor", "The flavor to apply to the extracted images"); CONFIG_OPTIONS.put("target-tags", "The tags to apply to the extracted images"); } /** The composer service */ private ComposerService composerService = null; /** The mpeg7 catalog service */ private Mpeg7CatalogService mpeg7CatalogService = null; /** The local workspace */ private Workspace workspace = null; /** * {@inheritDoc} * * @see org.opencastproject.workflow.api.WorkflowOperationHandler#getConfigurationOptions() */ @Override public SortedMap<String, String> getConfigurationOptions() { return CONFIG_OPTIONS; } /** * Callback for the OSGi declarative services configuration. * * @param composerService * the composer service */ protected void setComposerService(ComposerService composerService) { this.composerService = composerService; } /** * Callback for the OSGi declarative services configuration. * * @param catalogService * the catalog service */ protected void setMpeg7CatalogService(Mpeg7CatalogService catalogService) { this.mpeg7CatalogService = catalogService; } /** * 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#start(org.opencastproject.workflow.api.WorkflowInstance, * JobContext) */ public WorkflowOperationResult start(final WorkflowInstance workflowInstance, JobContext context) throws WorkflowOperationException { logger.debug("Running segments preview workflow operation on {}", workflowInstance); // Check if there is an mpeg-7 catalog containing video segments MediaPackage src = (MediaPackage) workflowInstance.getMediaPackage().clone(); Catalog[] segmentCatalogs = src.getCatalogs(MediaPackageElements.SEGMENTS); if (segmentCatalogs.length == 0) { logger.info("Media package {} does not contain segment information", src); return createResult(Action.CONTINUE); } // Create the images try { return createPreviews(src, workflowInstance.getCurrentOperation()); } catch (Exception e) { throw new WorkflowOperationException(e); } } /** * Encode tracks from MediaPackage using profiles stored in properties and updates current MediaPackage. * * @param mediaPackage * @param properties * @return the operation result containing the updated mediapackage * @throws EncoderException * @throws ExecutionException * @throws InterruptedException * @throws IOException * @throws NotFoundException * @throws WorkflowOperationException */ private WorkflowOperationResult createPreviews(final MediaPackage mediaPackage, WorkflowOperationInstance operation) throws EncoderException, InterruptedException, ExecutionException, NotFoundException, MediaPackageException, IOException, WorkflowOperationException { long totalTimeInQueue = 0; // Read the configuration properties String sourceVideoFlavor = StringUtils.trimToNull(operation.getConfiguration("source-flavor")); String sourceTags = StringUtils.trimToNull(operation.getConfiguration("source-tags")); String targetImageTags = StringUtils.trimToNull(operation.getConfiguration("target-tags")); String targetImageFlavor = StringUtils.trimToNull(operation.getConfiguration("target-flavor")); String encodingProfileName = StringUtils.trimToNull(operation.getConfiguration("encoding-profile")); String referenceFlavor = StringUtils.trimToNull(operation.getConfiguration("reference-flavor")); String referenceTags = StringUtils.trimToNull(operation.getConfiguration("reference-tags")); // Find the encoding profile EncodingProfile profile = composerService.getProfile(encodingProfileName); if (profile == null) throw new IllegalStateException("Encoding profile '" + encodingProfileName + "' was not found"); List<String> sourceTagSet = asList(sourceTags); // Select the tracks based on the tags and flavors Set<Track> videoTrackSet = new HashSet<Track>(); for (Track track : mediaPackage.getTracksByTags(sourceTagSet)) { if (sourceVideoFlavor == null || (track.getFlavor() != null && sourceVideoFlavor.equals(track.getFlavor().toString()))) { if (!track.hasVideo()) continue; videoTrackSet.add(track); } } if (videoTrackSet.size() == 0) { logger.debug("Mediapackage {} has no suitable tracks to extract images based on tags {} and flavor {}", new Object[] { mediaPackage, sourceTags, sourceVideoFlavor }); return createResult(mediaPackage, Action.CONTINUE); } else { // Determine the tagset for the reference List<String> referenceTagSet = asList(referenceTags); // Determine the reference master for (Track t : videoTrackSet) { // Try to load the segments catalog MediaPackageReference trackReference = new MediaPackageReferenceImpl(t); Catalog[] segmentCatalogs = mediaPackage.getCatalogs(MediaPackageElements.SEGMENTS, trackReference); Mpeg7Catalog mpeg7 = null; if (segmentCatalogs.length > 0) { mpeg7 = loadMpeg7Catalog(segmentCatalogs[0]); if (segmentCatalogs.length > 1) logger.warn("More than one segments catalog found for track {}. Resuming with the first one ({})", t, mpeg7); } else { logger.debug("No segments catalog found for track {}", t); continue; } // Check the catalog's consistency if (mpeg7.videoContent() == null || mpeg7.videoContent().next() == null) { logger.info("Segments catalog {} contains no video content", mpeg7); continue; } Video videoContent = mpeg7.videoContent().next(); TemporalDecomposition<? extends Segment> decomposition = videoContent.getTemporalDecomposition(); // Are there any segments? if (decomposition == null || !decomposition.hasSegments()) { logger.info("Segments catalog {} contains no video content", mpeg7); continue; } // Is a derived track with the configured reference flavor available? MediaPackageElement referenceMaster = getReferenceMaster(mediaPackage, t, referenceFlavor, referenceTagSet); // Create the preview images according to the mpeg7 segments if (t.hasVideo() && mpeg7 != null) { Iterator<? extends Segment> segmentIterator = decomposition.segments(); List<MediaTimePoint> timePointList = new LinkedList<MediaTimePoint>(); while (segmentIterator.hasNext()) { Segment segment = segmentIterator.next(); MediaTimePoint tp = segment.getMediaTime().getMediaTimePoint(); timePointList.add(tp); } // convert to time array double[] timeArray = new double[timePointList.size()]; for (int i = 0; i < timePointList.size(); i++) timeArray[i] = (double) timePointList.get(i).getTimeInMilliseconds() / 1000; Job job = composerService.image(t, profile.getIdentifier(), timeArray); if (!waitForStatus(job).isSuccess()) { throw new WorkflowOperationException("Extracting preview image from " + t + " failed"); } // Get the latest copy try { job = serviceRegistry.getJob(job.getId()); } catch (ServiceRegistryException e) { throw new WorkflowOperationException(e); } // add this receipt's queue time to the total totalTimeInQueue += job.getQueueTime(); List<? extends MediaPackageElement> composedImages = MediaPackageElementParser.getArrayFromXml(job .getPayload()); Iterator<MediaTimePoint> it = timePointList.iterator(); for (MediaPackageElement element : composedImages) { Attachment composedImage = (Attachment) element; if (composedImage == null) throw new IllegalStateException("Unable to compose image"); // Add the flavor, either from the operation configuration or from the composer if (targetImageFlavor != null) { composedImage.setFlavor(MediaPackageElementFlavor.parseFlavor(targetImageFlavor)); logger.debug("Preview image has flavor '{}'", composedImage.getFlavor()); } // Set the mimetype if (profile.getMimeType() != null) composedImage.setMimeType(MimeTypes.parseMimeType(profile.getMimeType())); // Add tags for (String tag : asList(targetImageTags)) { logger.trace("Tagging image with '{}'", tag); composedImage.addTag(tag); } // Refer to the original track including a timestamp MediaPackageReferenceImpl ref = new MediaPackageReferenceImpl(referenceMaster); ref.setProperty("time", it.next().toString()); composedImage.setReference(ref); // store new image in the mediaPackage mediaPackage.add(composedImage); String fileName = getFileNameFromElements(t, composedImage); composedImage.setURI(workspace.moveTo(composedImage.getURI(), mediaPackage.getIdentifier().toString(), composedImage.getIdentifier(), fileName)); } } } } return createResult(mediaPackage, Action.CONTINUE, totalTimeInQueue); } /** * Returns the track that is used as the reference for the segment previews. It is either identified by flavor and tag * set and being derived from <code>t</code> or <code>t</code> itself. * * @param mediaPackage * the media package * @param t * the source track for the images * @param referenceFlavor * the required flavor * @param referenceTagSet * the required tagset * @return the reference master */ private MediaPackageElement getReferenceMaster(MediaPackage mediaPackage, Track t, String referenceFlavor, Collection<String> referenceTagSet) { MediaPackageElement referenceMaster = t; if (referenceFlavor != null) { MediaPackageElementFlavor flavor = MediaPackageElementFlavor.parseFlavor(referenceFlavor); // Find a track with the given flavor that is (indirectly) derived from t? locateReferenceMaster: for (Track e : mediaPackage.getTracks(flavor)) { MediaPackageReference ref = e.getReference(); while (ref != null) { MediaPackageElement tr = mediaPackage.getElementByReference(ref); if (tr == null) break locateReferenceMaster; if (tr.equals(t)) { boolean matches = true; for (String tag : referenceTagSet) { if (!e.containsTag(tag)) matches = false; } if (matches) { referenceMaster = e; break locateReferenceMaster; } } ref = tr.getReference(); } } } return referenceMaster; } /** * Loads an mpeg7 catalog from a mediapackage's catalog reference * * @param catalog * the mediapackage's reference to this catalog * @return the mpeg7 * @throws IOException * if there is a problem loading or parsing the mpeg7 object */ protected Mpeg7Catalog loadMpeg7Catalog(Catalog catalog) throws IOException { InputStream in = null; try { File f = workspace.get(catalog.getURI()); in = new FileInputStream(f); return mpeg7CatalogService.load(in); } catch (NotFoundException e) { throw new IOException("Unable to open catalog " + catalog + ": " + e.getMessage()); } finally { IOUtils.closeQuietly(in); } } }