/** * 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.composer.impl; import static org.apache.commons.lang3.exception.ExceptionUtils.getStackTrace; import static org.opencastproject.fun.juc.Immutables.list; import static org.opencastproject.serviceregistry.api.Incidents.NO_DETAILS; import static org.opencastproject.util.data.Option.none; import static org.opencastproject.util.data.Option.some; import static org.opencastproject.util.data.Tuple.tuple; import org.opencastproject.composer.api.ComposerService; import org.opencastproject.composer.api.EmbedderEngine; import org.opencastproject.composer.api.EmbedderEngineFactory; import org.opencastproject.composer.api.EmbedderException; import org.opencastproject.composer.api.EncoderEngine; import org.opencastproject.composer.api.EncoderEngineFactory; import org.opencastproject.composer.api.EncoderException; import org.opencastproject.composer.api.EncodingProfile; import org.opencastproject.composer.api.LaidOutElement; import org.opencastproject.composer.layout.Dimension; import org.opencastproject.composer.layout.Layout; import org.opencastproject.composer.layout.Serializer; import org.opencastproject.fun.juc.Mutables; import org.opencastproject.inspection.api.MediaInspectionException; import org.opencastproject.inspection.api.MediaInspectionService; import org.opencastproject.job.api.AbstractJobProducer; import org.opencastproject.job.api.Job; import org.opencastproject.job.api.JobBarrier; import org.opencastproject.mediapackage.Attachment; import org.opencastproject.mediapackage.Catalog; import org.opencastproject.mediapackage.MediaPackageElement; import org.opencastproject.mediapackage.MediaPackageElement.Type; import org.opencastproject.mediapackage.MediaPackageElementBuilder; import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory; import org.opencastproject.mediapackage.MediaPackageElementParser; import org.opencastproject.mediapackage.MediaPackageException; import org.opencastproject.mediapackage.Stream; import org.opencastproject.mediapackage.Track; import org.opencastproject.mediapackage.VideoStream; import org.opencastproject.mediapackage.identifier.IdBuilder; import org.opencastproject.mediapackage.identifier.IdBuilderFactory; import org.opencastproject.security.api.OrganizationDirectoryService; import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.UserDirectoryService; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.opencastproject.serviceregistry.api.ServiceRegistryException; import org.opencastproject.util.IoSupport; import org.opencastproject.util.JsonObj; import org.opencastproject.util.LoadUtil; import org.opencastproject.util.MimeTypes; import org.opencastproject.util.NotFoundException; import org.opencastproject.util.data.Collections; import org.opencastproject.util.data.Option; import org.opencastproject.util.data.Tuple; 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.math.NumberUtils; import org.osgi.service.cm.ConfigurationException; import org.osgi.service.cm.ManagedService; import org.osgi.service.component.ComponentContext; 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.net.URI; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Dictionary; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; /** FFMPEG based implementation of the composer service api. */ public class ComposerServiceImpl extends AbstractJobProducer implements ComposerService, ManagedService { /** * The indexes the composite job uses to create a Job */ private static final int BACKGROUND_COLOR_INDEX = 6; private static final int COMPOSITE_TRACK_SIZE_INDEX = 4; private static final int LOWER_TRACK_INDEX = 0; private static final int LOWER_TRACK_LAYOUT_INDEX = 1; private static final int PROFILE_ID_INDEX = 5; private static final int UPPER_TRACK_INDEX = 2; private static final int UPPER_TRACK_LAYOUT_INDEX = 3; private static final int WATERMARK_INDEX = 7; private static final int WATERMARK_LAYOUT_INDEX = 8; /** * Error codes */ private static final int WORKSPACE_GET_IO_EXCEPTION = 1; private static final int WORKSPACE_GET_NOT_FOUND = 2; private static final int WORKSPACE_PUT_COLLECTION_IO_EXCEPTION = 3; private static final int PROFILE_NOT_FOUND = 4; private static final int ENCODER_ENGINE_NOT_FOUND = 5; private static final int EMBEDDER_ENGINE_NOT_FOUND = 6; private static final int ENCODING_FAILED = 7; private static final int TRIMMING_FAILED = 8; private static final int COMPOSITE_FAILED = 9; private static final int CONCAT_FAILED = 10; private static final int CONCAT_LESS_TRACKS = 11; private static final int CONCAT_NO_DIMENSION = 12; private static final int IMAGE_TO_VIDEO_FAILED = 13; private static final int CONVERT_IMAGE_FAILED = 14; private static final int IMAGE_EXTRACTION_FAILED = 15; private static final int IMAGE_EXTRACTION_UNKNOWN_DURATION = 16; private static final int IMAGE_EXTRACTION_TIME_OUTSIDE_DURATION = 17; private static final int IMAGE_EXTRACTION_NO_VIDEO = 18; private static final int CAPTION_EMBEDD_FAILED = 19; private static final int CAPTION_NO_VIDEO = 20; private static final int CAPTION_NO_LANGUAGE = 21; private static final int WATERMARK_NOT_FOUND = 22; private static final int NO_STREAMS = 23; /** The logging instance */ private static final Logger logger = LoggerFactory.getLogger(ComposerServiceImpl.class); /** The collection name */ public static final String COLLECTION = "composer"; /** Used to mark a track unavailable to composite. */ private static final String NOT_AVAILABLE = "n/a"; /** The load introduced on the system by creating a caption job */ public static final float DEFAULT_CAPTION_JOB_LOAD = 1.0f; /** The key to look for in the service configuration file to override the {@link DEFAULT_CAPTION_JOB_LOAD} */ public static final String CAPTION_JOB_LOAD_KEY = "job.load.caption.embed"; /** The load introduced on the system by creating a caption job */ private float captionJobLoad = DEFAULT_CAPTION_JOB_LOAD; /** List of available operations on jobs */ private enum Operation { Caption, Encode, Image, ImageConversion, Mux, Trim, Watermark, Composite, Concat, ImageToVideo, ParallelEncode } /** Encoding profile manager */ private EncodingProfileScanner profileScanner = null; /** Reference to the media inspection service */ private MediaInspectionService inspectionService = null; /** Reference to the workspace service */ private Workspace workspace = null; /** Reference to the receipt service */ private ServiceRegistry serviceRegistry; /** Reference to the encoder engine factory */ private EncoderEngineFactory encoderEngineFactory; /** Reference to the embedder engine factory */ private EmbedderEngineFactory embedderEngineFactory; /** The organization directory service */ protected OrganizationDirectoryService organizationDirectoryService = null; /** Id builder used to create ids for encoded tracks */ private final IdBuilder idBuilder = IdBuilderFactory.newInstance().newIdBuilder(); /** The security service */ protected SecurityService securityService = null; /** The user directory service */ protected UserDirectoryService userDirectoryService = null; /** Creates a new composer service instance. */ public ComposerServiceImpl() { super(JOB_TYPE); } /** * OSGi callback on component activation. * * @param cc * the component context */ @Override public void activate(ComponentContext cc) { super.activate(cc); logger.info("Activating composer service"); } /** * {@inheritDoc} * * @see org.opencastproject.composer.api.ComposerService#encode(org.opencastproject.mediapackage.Track, * java.lang.String) */ @Override public Job encode(Track sourceTrack, String profileId) throws EncoderException, MediaPackageException { try { return serviceRegistry.createJob(JOB_TYPE, Operation.Encode.toString(), Arrays.asList(MediaPackageElementParser.getAsXml(sourceTrack), profileId)); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create a job", e); } } /** * Encodes audio and video track to a file. If both an audio and a video track are given, they are muxed together into * one movie container. * * @param videoTrack * the video track * @param audioTrack * the audio track * @param profileId * the encoding profile * @param properties * encoding properties * @return the encoded track or none if the operation does not return a track. This may happen for example when doing * two pass encodings where the first pass only creates metadata for the second one * @throws EncoderException * if encoding fails */ protected Option<Track> encode(final Job job, Track videoTrack, Track audioTrack, String profileId, Map<String, String> properties) throws EncoderException, MediaPackageException { if (job == null) throw new IllegalArgumentException("The Job parameter must not be null"); final String targetTrackId = idBuilder.createNew().toString(); try { // Get the tracks and make sure they exist final File audioFile; if (audioTrack == null) { audioFile = null; } else { try { audioFile = workspace.get(audioTrack.getURI()); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("audio", Type.Track, audioTrack.getURI()), NO_DETAILS); throw new EncoderException("Requested audio track " + audioTrack + " is not found"); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("audio", Type.Track, audioTrack.getURI()), NO_DETAILS); throw new EncoderException("Unable to access audio track " + audioTrack); } } final File videoFile; if (videoTrack == null) { videoFile = null; } else { try { videoFile = workspace.get(videoTrack.getURI()); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("video", Type.Track, videoTrack.getURI()), NO_DETAILS); throw new EncoderException("Requested video track " + videoTrack + " is not found"); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("video", Type.Track, videoTrack.getURI()), NO_DETAILS); throw new EncoderException("Unable to access video track " + videoTrack); } } // Get the encoding profile final EncodingProfile profile = getProfile(job, profileId); // Create the engine final EncoderEngine encoderEngine = getEncoderEngine(job, profile); if (audioTrack != null && videoTrack != null) logger.info("Muxing audio track {} and video track {} into {}", new Object[] { audioTrack.getIdentifier(), videoTrack.getIdentifier(), targetTrackId }); else if (audioTrack == null) logger.info("Encoding video track {} to {} using profile '{}'", new Object[] { videoTrack.getIdentifier(), targetTrackId, profileId }); else if (videoTrack == null) logger.info("Encoding audio track {} to {} using profile '{}'", new Object[] { audioTrack.getIdentifier(), targetTrackId, profileId }); // Do the work Option<File> output; try { output = encoderEngine.mux(audioFile, videoFile, profile, properties); } catch (EncoderException e) { Map<String, String> params = new HashMap<String, String>(); if (audioFile != null) { params.put("audio", audioTrack.getURI().toString()); } else { params.put("audio", "EMPTY"); } if (videoFile != null) { params.put("video", videoTrack.getURI().toString()); } else { params.put("video", "EMPTY"); } params.put("profile", profile.getIdentifier()); if (properties != null) { params.put("properties", properties.toString()); } else { params.put("properties", "EMPTY"); } incident().recordFailure(job, ENCODING_FAILED, e, params, detailsFor(e, encoderEngine)); throw e; } // mux did not return a file if (output.isNone() || !output.get().exists() || output.get().length() == 0) return none(); // Put the file in the workspace URI workspaceURI = putToCollection(job, output.get(), "encoded file"); // Have the encoded track inspected and return the result Job inspectionJob = inspect(job, workspaceURI); Track inspectedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload()); inspectedTrack.setIdentifier(targetTrackId); if (profile.getMimeType() != null) inspectedTrack.setMimeType(MimeTypes.parseMimeType(profile.getMimeType())); return some(inspectedTrack); } catch (Exception e) { logger.warn("Error encoding " + videoTrack + " and " + audioTrack, e); if (e instanceof EncoderException) { throw (EncoderException) e; } else { throw new EncoderException(e); } } } /** * Encodes audio and video track to a file. If both an audio and a video track are given, they are muxed together into * one movie container. * * @param job * @param mediaTrack * @param profileId * the encoding profile * @param properties * encoding properties * @return the encoded track or none if the operation does not return a track. This may happen for example when doing * two pass encodings where the first pass only creates metadata for the second one * @throws EncoderException * if encoding fails */ protected List <Track> parralelEncode(Job job, Track mediaTrack, String profileId, Map<String, String> properties) throws EncoderException, MediaPackageException { if (job == null) { throw new EncoderException("The Job parameter must not be null"); } try { // Get the tracks and make sure they exist final File mediaFile; if (mediaTrack == null) { mediaFile = null; } else { try { mediaFile = workspace.get(mediaTrack.getURI()); } catch (NotFoundException e) { throw new EncoderException("Requested media track " + mediaTrack + " is not found"); } catch (IOException e) { throw new EncoderException("Unable to access media track " + mediaTrack); } } // Create the engine final EncodingProfile profile = profileScanner.getProfile(profileId); if (profile == null) { throw new EncoderException(null, "Profile '" + profileId + " is unknown"); } final EncoderEngine encoderEngine = encoderEngineFactory.newEncoderEngine(profile); if (encoderEngine == null) { throw new EncoderException(null, "No encoder engine available for profile '" + profileId + "'"); } // List of encoded tracks LinkedList<Track> encodedTracks = new LinkedList<Track>(); // Do the work int i = 0; for (File encodingOutput : encoderEngine.parallelEncode(mediaFile, profile, properties)) { // Put the file in the workspace URI returnURL = null; InputStream in = null; final String targetTrackId = idBuilder.createNew().toString(); try { in = new FileInputStream(encodingOutput); returnURL = workspace.putInCollection(COLLECTION, job.getId() + "-" + i + "." + FilenameUtils.getExtension(encodingOutput.getAbsolutePath()), in); logger.info("Copied the encoded file to the workspace at {}", returnURL); if (encodingOutput.delete()) { logger.info("Deleted the local copy of the encoded file at {}", encodingOutput.getAbsolutePath()); } else { logger.warn("Unable to delete the encoding output at {}", encodingOutput); } } catch (Exception e) { throw new EncoderException("Unable to put the encoded file into the workspace", e); } finally { IOUtils.closeQuietly(in); } // Have the encoded track inspected and return the result Job inspectionJob = null; try { inspectionJob = inspectionService.inspect(returnURL); JobBarrier barrier = new JobBarrier(job, serviceRegistry, inspectionJob); if (!barrier.waitForJobs().isSuccess()) { throw new EncoderException("Media inspection of " + returnURL + " failed"); } } catch (MediaInspectionException e) { throw new EncoderException("Media inspection of " + returnURL + " failed", e); } Track inspectedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload()); inspectedTrack.setIdentifier(targetTrackId); List<String> tags = profile.getTags(); if (tags.size() > 0) { for (int j = 0; j < tags.size(); j++) { if (encodingOutput.getName().endsWith(profile.getSuffix(tags.get(j)))) inspectedTrack.addTag(tags.get(j)); } } // Will the mimetype be provided by the inspect service? // if (profile.getMimeType() != null) // inspectedTrack.setMimeType(MimeTypes.parseMimeType(profile.getMimeType())); encodedTracks.add(inspectedTrack); i++; } return encodedTracks; } catch (Exception e) { logger.warn("Error encoding " + mediaTrack, e); if (e instanceof EncoderException) { throw (EncoderException) e; } else { throw new EncoderException(e); } } } /** * {@inheritDoc} * * @see org.opencastproject.composer.api.ComposerService#encode(org.opencastproject.mediapackage.Track, * java.lang.String) */ @Override public Job parallelEncode(Track sourceTrack, String profileId) throws EncoderException, MediaPackageException { try { logger.info("Starting parallel encode with profile {} ", profileId); return serviceRegistry.createJob(JOB_TYPE, Operation.ParallelEncode.toString(), Arrays.asList(MediaPackageElementParser.getAsXml(sourceTrack), profileId)); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create a job", e); } } /** * {@inheritDoc} * * @see org.opencastproject.composer.api.ComposerService#trim(org.opencastproject.mediapackage.Track, * java.lang.String, long, long) */ @Override public Job trim(final Track sourceTrack, final String profileId, final long start, final long duration) throws EncoderException, MediaPackageException { try { return serviceRegistry.createJob( JOB_TYPE, Operation.Trim.toString(), Arrays.asList(MediaPackageElementParser.getAsXml(sourceTrack), profileId, Long.toString(start), Long.toString(duration))); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create a job", e); } } /** * Trims the given track using the encoding profile <code>profileId</code> and the given starting point and duration * in miliseconds. * * @param job * the associated job * @param sourceTrack * the source track * @param profileId * the encoding profile identifier * @param start * the trimming in-point in millis * @param duration * the trimming duration in millis * @return the trimmed track or none if the operation does not return a track. This may happen for example when doing * two pass encodings where the first pass only creates metadata for the second one * @throws EncoderException * if trimming fails */ protected Option<Track> trim(Job job, Track sourceTrack, String profileId, long start, long duration) throws EncoderException { try { String targetTrackId = idBuilder.createNew().toString(); // Get the track and make sure it exists final File trackFile; try { trackFile = workspace.get(sourceTrack.getURI()); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("source", Type.Track, sourceTrack.getURI()), NO_DETAILS); throw new EncoderException("Requested track " + sourceTrack + " is not found"); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("source", Type.Track, sourceTrack.getURI()), NO_DETAILS); throw new EncoderException("Unable to access track " + sourceTrack); } // Get the encoding profile final EncodingProfile profile = getProfile(job, profileId); // Create the engine final EncoderEngine encoderEngine = getEncoderEngine(job, profile); Option<File> output; try { output = encoderEngine.trim(trackFile, profile, start, duration, null); } catch (EncoderException e) { Map<String, String> params = new HashMap<String, String>(); params.put("track", sourceTrack.getURI().toString()); params.put("profile", profile.getIdentifier()); params.put("start", Long.toString(start)); params.put("duration", Long.toString(duration)); incident().recordFailure(job, TRIMMING_FAILED, e, params, detailsFor(e, encoderEngine)); throw e; } // trim did not return a file if (output.isNone() || !output.get().exists() || output.get().length() == 0) return none(); // Put the file in the workspace URI workspaceURI = putToCollection(job, output.get(), "trimmed file"); // Have the encoded track inspected and return the result Job inspectionJob = inspect(job, workspaceURI); Track inspectedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload()); inspectedTrack.setIdentifier(targetTrackId); return some(inspectedTrack); } catch (Exception e) { logger.warn("Error trimming " + sourceTrack, e); if (e instanceof EncoderException) { throw (EncoderException) e; } else { throw new EncoderException(e); } } } /** * {@inheritDoc} * * @see org.opencastproject.composer.api.ComposerService#mux(org.opencastproject.mediapackage.Track, * org.opencastproject.mediapackage.Track, java.lang.String) */ @Override public Job mux(Track videoTrack, Track audioTrack, String profileId) throws EncoderException, MediaPackageException { try { return serviceRegistry.createJob( JOB_TYPE, Operation.Mux.toString(), Arrays.asList(MediaPackageElementParser.getAsXml(videoTrack), MediaPackageElementParser.getAsXml(audioTrack), profileId)); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create a job", e); } } /** * Muxes the audio and video track into one movie container. * * @param job * the associated job * @param videoTrack * the video track * @param audioTrack * the audio track * @param profileId * the profile identifier * @return the muxed track * @throws EncoderException * if encoding fails * @throws MediaPackageException * if serializing the mediapackage elements fails */ protected Option<Track> mux(Job job, Track videoTrack, Track audioTrack, String profileId) throws EncoderException, MediaPackageException { return encode(job, videoTrack, audioTrack, profileId, null); } /** * {@inheritDoc} */ @Override public Job composite(Dimension compositeTrackSize, Option<LaidOutElement<Track>> upperTrack, LaidOutElement<Track> lowerTrack, Option<LaidOutElement<Attachment>> watermark, String profileId, String background) throws EncoderException, MediaPackageException { List<String> arguments = new ArrayList<String>(); arguments.add(LOWER_TRACK_INDEX, MediaPackageElementParser.getAsXml(lowerTrack.getElement())); arguments.add(LOWER_TRACK_LAYOUT_INDEX, Serializer.json(lowerTrack.getLayout()).toJson()); if (upperTrack.isNone()) { arguments.add(UPPER_TRACK_INDEX, NOT_AVAILABLE); arguments.add(UPPER_TRACK_LAYOUT_INDEX, NOT_AVAILABLE); } else { arguments.add(UPPER_TRACK_INDEX, MediaPackageElementParser.getAsXml(upperTrack.get().getElement())); arguments.add(UPPER_TRACK_LAYOUT_INDEX, Serializer.json(upperTrack.get().getLayout()).toJson()); } arguments.add(COMPOSITE_TRACK_SIZE_INDEX, Serializer.json(compositeTrackSize).toJson()); arguments.add(PROFILE_ID_INDEX, profileId); arguments.add(BACKGROUND_COLOR_INDEX, background); if (watermark.isSome()) { LaidOutElement<Attachment> watermarkLaidOutElement = watermark.get(); arguments.add(WATERMARK_INDEX, MediaPackageElementParser.getAsXml(watermarkLaidOutElement.getElement())); arguments.add(WATERMARK_LAYOUT_INDEX, Serializer.json(watermarkLaidOutElement.getLayout()).toJson()); } try { return serviceRegistry.createJob(JOB_TYPE, Operation.Composite.toString(), arguments); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create composite job", e); } } protected Option<Track> composite(Job job, Dimension compositeTrackSize, LaidOutElement<Track> lowerLaidOutElement, Option<LaidOutElement<Track>> upperLaidOutElement, Option<LaidOutElement<Attachment>> watermarkOption, String profileId, String backgroundColor) throws EncoderException, MediaPackageException { if (job == null) throw new EncoderException("The Job parameter must not be null"); if (compositeTrackSize == null) throw new EncoderException("The composite track size parameter must not be null"); if (lowerLaidOutElement == null) throw new EncoderException("The lower laid out element parameter must not be null"); if (upperLaidOutElement == null) throw new EncoderException("The upper laid out element parameter must not be null"); if (watermarkOption == null) throw new EncoderException("The optional watermark laid out element parameter must not be null"); if (profileId == null) throw new EncoderException("The profileId parameter must not be null"); if (backgroundColor == null) throw new EncoderException("The background color parameter must not be null"); // Get the encoding profile final EncodingProfile profile = getProfile(job, profileId); // Create the engine final EncoderEngine encoderEngine = getEncoderEngine(job, profile); final String targetTrackId = idBuilder.createNew().toString(); Option<File> upperVideoFile = Option.<File> none(); try { // Get the tracks and make sure they exist final File lowerVideoFile; try { lowerVideoFile = workspace.get(lowerLaidOutElement.getElement().getURI()); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("lower video", Type.Track, lowerLaidOutElement.getElement().getURI()), NO_DETAILS); throw new EncoderException("Requested lower video track " + lowerLaidOutElement.getElement() + " is not found"); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("lower video", Type.Track, lowerLaidOutElement.getElement().getURI()), NO_DETAILS); throw new EncoderException("Unable to access lower video track " + lowerLaidOutElement.getElement()); } if (upperLaidOutElement.isSome()) { try { upperVideoFile = Option.option(workspace.get(upperLaidOutElement.get().getElement().getURI())); } catch (NotFoundException e) { incident().recordFailure( job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("upper video", Type.Track, upperLaidOutElement.get().getElement() .getURI()), NO_DETAILS); throw new EncoderException("Requested upper video track " + upperLaidOutElement.get().getElement() + " is not found"); } catch (IOException e) { incident().recordFailure( job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("upper video", Type.Track, upperLaidOutElement.get().getElement() .getURI()), NO_DETAILS); throw new EncoderException("Unable to access upper video track " + upperLaidOutElement.get().getElement()); } } File watermarkFile = null; if (watermarkOption.isSome()) { try { watermarkFile = workspace.get(watermarkOption.get().getElement().getURI()); } catch (NotFoundException e) { incident().recordFailure( job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("watermark image", Type.Attachment, watermarkOption.get().getElement() .getURI()), NO_DETAILS); throw new EncoderException("Requested watermark image " + watermarkOption.get().getElement() + " is not found"); } catch (IOException e) { incident().recordFailure( job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("watermark image", Type.Attachment, watermarkOption.get().getElement() .getURI()), NO_DETAILS); throw new EncoderException("Unable to access right watermark image " + watermarkOption.get().getElement()); } if (upperLaidOutElement.isSome()) { logger.info("Composing lower video track {} {} and upper video track {} {} including watermark {} {} into {}", new Object[] { lowerLaidOutElement.getElement().getIdentifier(), lowerLaidOutElement.getElement().getURI(), upperLaidOutElement.get().getElement().getIdentifier(), upperLaidOutElement.get().getElement().getURI(), watermarkOption.get().getElement().getIdentifier(), watermarkOption.get().getElement().getURI(), targetTrackId }); } else { logger.info("Composing video track {} {} including watermark {} {} into {}", new Object[] { lowerLaidOutElement.getElement().getIdentifier(), lowerLaidOutElement.getElement().getURI(), watermarkOption.get().getElement().getIdentifier(), watermarkOption.get().getElement().getURI(), targetTrackId }); } } else { if (upperLaidOutElement.isSome()) { logger.info("Composing lower video track {} {} and upper video track {} {} into {}", new Object[] { lowerLaidOutElement.getElement().getIdentifier(), lowerLaidOutElement.getElement().getURI(), upperLaidOutElement.get().getElement().getIdentifier(), upperLaidOutElement.get().getElement().getURI(), targetTrackId }); } else { logger.info("Composing video track {} {} into {}", new Object[] { lowerLaidOutElement.getElement().getIdentifier(), lowerLaidOutElement.getElement().getURI(), targetTrackId }); } } // Creating video filter command final String compositeCommand = buildCompositeCommand(compositeTrackSize, lowerLaidOutElement, upperLaidOutElement, upperVideoFile, watermarkOption, watermarkFile, backgroundColor); Map<String, String> properties = new HashMap<String, String>(); properties.put("compositeCommand", compositeCommand); Option<File> output; try { if (upperVideoFile.isSome()) { output = encoderEngine.mux(upperVideoFile.get(), lowerVideoFile, profile, properties); } else { output = encoderEngine.mux(null, lowerVideoFile, profile, properties); } } catch (EncoderException e) { Map<String, String> params = new HashMap<String, String>(); if (upperLaidOutElement.isSome()) { params.put("upper", upperLaidOutElement.get().getElement().getURI().toString()); } params.put("lower", lowerLaidOutElement.getElement().getURI().toString()); if (watermarkFile != null) params.put("watermark", watermarkOption.get().getElement().getURI().toString()); params.put("profile", profile.getIdentifier()); params.put("properties", properties.toString()); incident().recordFailure(job, COMPOSITE_FAILED, e, params, detailsFor(e, encoderEngine)); throw e; } // composite did not return a file if (output.isNone() || !output.get().exists() || output.get().length() == 0) return none(); // Put the file in the workspace URI workspaceURI = putToCollection(job, output.get(), "compound file"); // Have the compound track inspected and return the result Job inspectionJob = inspect(job, workspaceURI); Track inspectedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload()); inspectedTrack.setIdentifier(targetTrackId); if (profile.getMimeType() != null) inspectedTrack.setMimeType(MimeTypes.parseMimeType(profile.getMimeType())); return some(inspectedTrack); } catch (Exception e) { if (upperLaidOutElement.isSome()) { logger.warn("Error composing {} and {}: {}", new Object[] { lowerLaidOutElement.getElement(), upperLaidOutElement.get().getElement(), getStackTrace(e) }); } else { logger.warn("Error composing {}: {}", lowerLaidOutElement.getElement(), getStackTrace(e)); } if (e instanceof EncoderException) { throw (EncoderException) e; } else { throw new EncoderException(e); } } } @Override public Job concat(String profileId, Dimension outputDimension, Track... tracks) throws EncoderException, MediaPackageException { return concat(profileId, outputDimension, -1.0f, tracks); } @Override public Job concat(String profileId, Dimension outputDimension, float outputFrameRate, Track... tracks) throws EncoderException, MediaPackageException { ArrayList<String> arguments = new ArrayList<String>(); arguments.add(0, profileId); if (outputDimension != null) { arguments.add(1, Serializer.json(outputDimension).toJson()); } else { arguments.add(1, ""); } arguments.add(2, String.format(Locale.US, "%f", outputFrameRate)); for (int i = 0; i < tracks.length; i++) { arguments.add(i + 3, MediaPackageElementParser.getAsXml(tracks[i])); } try { return serviceRegistry.createJob(JOB_TYPE, Operation.Concat.toString(), arguments); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create concat job", e); } } protected Option<Track> concat(Job job, List<Track> tracks, String profileId, Dimension outputDimension, float outputFrameRate) throws EncoderException, MediaPackageException { if (job == null) throw new EncoderException("The job parameter must not be null"); if (tracks == null) throw new EncoderException("The track parameter must not be null"); if (profileId == null) throw new EncoderException("The profile id parameter must not be null"); if (tracks.size() < 2) { Map<String, String> params = new HashMap<String, String>(); params.put("tracks-size", Integer.toString(tracks.size())); params.put("tracks", StringUtils.join(tracks, ",")); incident().recordFailure(job, CONCAT_LESS_TRACKS, params); throw new EncoderException("The track parameter must at least have two tracks present"); } boolean onlyAudio = true; for (Track t : tracks) { if (t.hasVideo()) { onlyAudio = false; break; } } if (!onlyAudio && outputDimension == null) { Map<String, String> params = new HashMap<String, String>(); params.put("tracks", StringUtils.join(tracks, ",")); incident().recordFailure(job, CONCAT_NO_DIMENSION, params); throw new EncoderException("The output dimension id parameter must not be null when concatenating video"); } // Get the encoding profile final EncodingProfile profile = getProfile(job, profileId); InputStream in = null; final String targetTrackId = idBuilder.createNew().toString(); try { // Get the tracks and make sure they exist List<File> trackFiles = new ArrayList<File>(); int i = 0; for (Track track : tracks) { if (!track.hasAudio() && !track.hasVideo()) { Map<String, String> params = new HashMap<String, String>(); params.put("track-id", track.getIdentifier()); params.put("track-url", track.getURI().toString()); incident().recordFailure(job, NO_STREAMS, params); throw new EncoderException("Track has no audio or video stream available: " + track); } try { trackFiles.add(i++, IoSupport.waitForFile(workspace.get(track.getURI()))); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("concat", Type.Track, track.getURI()), NO_DETAILS); throw new EncoderException("Requested track " + track + " is not found"); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("concat", Type.Track, track.getURI()), NO_DETAILS); throw new EncoderException("Unable to access track " + track); } } // Create the engine final EncoderEngine encoderEngine = getEncoderEngine(job, profile); if (onlyAudio) { logger.info("Concatenating audio tracks {} into {}", trackFiles, targetTrackId); } else { logger.info("Concatenating video tracks {} into {}", trackFiles, targetTrackId); } // Creating video filter command for concat String concatCommand = buildConcatCommand(onlyAudio, outputDimension, outputFrameRate, trackFiles, tracks); Map<String, String> properties = new HashMap<String, String>(); properties.put("concatCommand", concatCommand); Option<File> output; try { output = encoderEngine.encode(trackFiles.get(0), profile, properties); } catch (EncoderException e) { Map<String, String> params = new HashMap<String, String>(); List<String> trackList = new ArrayList<String>(); for (Track t : tracks) { trackList.add(t.getURI().toString()); } params.put("tracks", StringUtils.join(trackList, ",")); params.put("profile", profile.getIdentifier()); params.put("properties", properties.toString()); incident().recordFailure(job, CONCAT_FAILED, e, params, detailsFor(e, encoderEngine)); throw e; } // concat did not return a file if (output.isNone() || !output.get().exists() || output.get().length() == 0) return none(); // Put the file in the workspace URI workspaceURI = putToCollection(job, output.get(), "concatenated file"); // Have the concat track inspected and return the result Job inspectionJob = inspect(job, workspaceURI); Track inspectedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload()); inspectedTrack.setIdentifier(targetTrackId); if (profile.getMimeType() != null) inspectedTrack.setMimeType(MimeTypes.parseMimeType(profile.getMimeType())); return some(inspectedTrack); } catch (Exception e) { logger.warn("Error concatenating tracks {}: {}", tracks, e); if (e instanceof EncoderException) { throw (EncoderException) e; } else { throw new EncoderException(e); } } finally { IoSupport.closeQuietly(in); } } @Override public Job imageToVideo(Attachment sourceImageAttachment, String profileId, double time) throws EncoderException, MediaPackageException { try { return serviceRegistry.createJob(JOB_TYPE, Operation.ImageToVideo.toString(), Arrays.asList( MediaPackageElementParser.getAsXml(sourceImageAttachment), profileId, Double.toString(time))); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create image to video job", e); } } protected Option<Track> imageToVideo(Job job, Attachment sourceImage, String profileId, Double time) throws EncoderException, MediaPackageException { if (job == null) throw new EncoderException("The Job parameter must not be null"); if (sourceImage == null) throw new EncoderException("The sourceImage attachment parameter must not be null"); if (profileId == null) throw new EncoderException("The profileId parameter must not be null"); if (time == null) throw new EncoderException("The time parameter must not be null"); // Get the encoding profile final EncodingProfile profile = getProfile(job, profileId); final String targetTrackId = idBuilder.createNew().toString(); try { // Get the attachment and make sure it exist File imageFile = null; try { imageFile = workspace.get(sourceImage.getURI()); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("source image", Type.Attachment, sourceImage.getURI()), NO_DETAILS); throw new EncoderException("Requested source image " + sourceImage + " is not found"); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("source image", Type.Attachment, sourceImage.getURI()), NO_DETAILS); throw new EncoderException("Unable to access source image " + sourceImage); } // Create the engine final EncoderEngine encoderEngine = getEncoderEngine(job, profile); logger.info("Converting image attachment {} into video {}", sourceImage.getIdentifier(), targetTrackId); Map<String, String> properties = new HashMap<String, String>(); if (time == null || time == -1) time = 0D; DecimalFormatSymbols ffmpegFormat = new DecimalFormatSymbols(); ffmpegFormat.setDecimalSeparator('.'); DecimalFormat df = new DecimalFormat("0.000", ffmpegFormat); properties.put("time", df.format(time)); Option<File> output; try { output = encoderEngine.encode(imageFile, profile, properties); } catch (EncoderException e) { Map<String, String> params = new HashMap<String, String>(); params.put("image", sourceImage.getURI().toString()); params.put("profile", profile.getIdentifier()); params.put("properties", properties.toString()); incident().recordFailure(job, IMAGE_TO_VIDEO_FAILED, e, params, detailsFor(e, encoderEngine)); throw e; } // encoding did not return a file if (output.isNone() || !output.get().exists() || output.get().length() == 0) return none(); // Put the file in the workspace URI workspaceURI = putToCollection(job, output.get(), "converted image file"); // Have the compound track inspected and return the result Job inspectionJob = inspect(job, workspaceURI); Track inspectedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload()); inspectedTrack.setIdentifier(targetTrackId); if (profile.getMimeType() != null) inspectedTrack.setMimeType(MimeTypes.parseMimeType(profile.getMimeType())); return some(inspectedTrack); } catch (Exception e) { logger.warn("Error converting " + sourceImage, e); if (e instanceof EncoderException) { throw (EncoderException) e; } else { throw new EncoderException(e); } } } /** * {@inheritDoc} * * @see org.opencastproject.composer.api.ComposerService#image(Track, String, double...) */ @Override public Job image(Track sourceTrack, String profileId, double... times) throws EncoderException, MediaPackageException { if (sourceTrack == null) throw new IllegalArgumentException("SourceTrack cannot be null"); if (times.length == 0) throw new IllegalArgumentException("At least one time argument has to be specified"); List<String> parameters = new ArrayList<>(); parameters.add(MediaPackageElementParser.getAsXml(sourceTrack)); parameters.add(profileId); parameters.add(Boolean.TRUE.toString()); for (int i = 0; i < times.length; i++) { parameters.add(Double.toString(times[i])); } // TODO: This is unfortunate, since ffmpeg is slow on single images and it would be nice to be able to start a // separate job per image, so extraction can be spread over multiple machines in a cluster. try { return serviceRegistry.createJob(JOB_TYPE, Operation.Image.toString(), parameters); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create a job", e); } } @Override public Job image(Track sourceTrack, String profileId, Map<String, String> properties) throws EncoderException, MediaPackageException { if (sourceTrack == null) throw new IllegalArgumentException("SourceTrack cannot be null"); List<String> arguments = new ArrayList<String>(); arguments.add(MediaPackageElementParser.getAsXml(sourceTrack)); arguments.add(profileId); arguments.add(Boolean.FALSE.toString()); arguments.add(getPropertiesAsString(properties)); try { return serviceRegistry.createJob(JOB_TYPE, Operation.Image.toString(), arguments); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create a job", e); } } /** * Extracts an image from <code>sourceTrack</code> at the given point in time. * * @param job * the associated job * @param sourceTrack * the source track * @param profileId * the identifier of the encoding profile to use * @param times * (one or more) times in seconds * @return the images as an attachment element list * @throws EncoderException * if extracting the image fails */ protected List<Attachment> image(Job job, Track sourceTrack, String profileId, double... times) throws EncoderException, MediaPackageException { if (sourceTrack == null) throw new EncoderException("SourceTrack cannot be null"); validateVideoStream(job, sourceTrack); // The time should not be outside of the track's duration for (double time : times) { if (sourceTrack.getDuration() == null) { Map<String, String> params = new HashMap<String, String>(); params.put("track-id", sourceTrack.getIdentifier()); params.put("track-url", sourceTrack.getURI().toString()); incident().recordFailure(job, IMAGE_EXTRACTION_UNKNOWN_DURATION, params); throw new EncoderException("Unable to extract an image from a track with unknown duration"); } if (time < 0 || time * 1000 > sourceTrack.getDuration()) { Map<String, String> params = new HashMap<String, String>(); params.put("track-id", sourceTrack.getIdentifier()); params.put("track-url", sourceTrack.getURI().toString()); params.put("track-duration", sourceTrack.getDuration().toString()); params.put("time", Double.toString(time)); incident().recordFailure(job, IMAGE_EXTRACTION_TIME_OUTSIDE_DURATION, params); throw new EncoderException("Can not extract an image at time " + time + " from a track with duration " + sourceTrack.getDuration()); } } return extractImages(job, sourceTrack, profileId, null, times); } /** * Extracts an image from <code>sourceTrack</code> by the given properties and the corresponding encoding profile. * * @param job * the associated job * @param sourceTrack * the source track * @param profileId * the identifier of the encoding profile to use * @param properties * the properties applied to the encoding profile * @return the images as an attachment element list * @throws EncoderException * if extracting the image fails */ protected List<Attachment> image(Job job, Track sourceTrack, String profileId, Map<String, String> properties) throws EncoderException, MediaPackageException { if (sourceTrack == null) throw new EncoderException("SourceTrack cannot be null"); validateVideoStream(job, sourceTrack); return extractImages(job, sourceTrack, profileId, properties); } private List<Attachment> extractImages(Job job, Track sourceTrack, String profileId, Map<String, String> properties, double... times) throws EncoderException { try { logger.info("creating an image using video track {}", sourceTrack.getIdentifier()); // Get the encoding profile final EncodingProfile profile = getProfile(job, profileId); // Create the encoding engine final EncoderEngine encoderEngine = getEncoderEngine(job, profile); // Finally get the file that needs to be encoded File videoFile; try { videoFile = workspace.get(sourceTrack.getURI()); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("video", Type.Track, sourceTrack.getURI()), NO_DETAILS); throw new EncoderException("Requested video track " + sourceTrack + " was not found", e); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("video", Type.Track, sourceTrack.getURI()), NO_DETAILS); throw new EncoderException("Error accessing video track " + sourceTrack, e); } // Do the work List<File> encodingOutput; try { encodingOutput = encoderEngine.extract(videoFile, profile, properties, times); // check for validity of output if (encodingOutput == null || encodingOutput.isEmpty()) { logger.error("Image extraction from video {} with profile {} failed: no images were produced", sourceTrack.getURI(), profile.getIdentifier()); throw new EncoderException("Image extraction failed: no images were produced"); } } catch (EncoderException e) { Map<String, String> params = new HashMap<String, String>(); params.put("video", sourceTrack.getURI().toString()); params.put("profile", profile.getIdentifier()); params.put("positions", Arrays.toString(times)); incident().recordFailure(job, IMAGE_EXTRACTION_FAILED, e, params, detailsFor(e, encoderEngine)); throw e; } int i = 0; List<URI> workspaceURIs = new LinkedList<URI>(); for (File output : encodingOutput) { if (!output.exists() || output.length() == 0) { logger.warn("Extracted image {} is empty!", output); throw new NotFoundException("Extracted image " + output.toString() + " is empty!"); } // Put the file in the workspace InputStream in = null; try { in = new FileInputStream(output); URI returnURL = workspace.putInCollection(COLLECTION, job.getId() + "_" + i++ + "." + FilenameUtils.getExtension(output.getAbsolutePath()), in); logger.debug("Copied image file to the workspace at {}", returnURL); workspaceURIs.add(returnURL); } catch (Exception e) { cleanup(encodingOutput.toArray(new File[encodingOutput.size()])); cleanupWorkspace(workspaceURIs.toArray(new URI[workspaceURIs.size()])); incident().recordFailure(job, WORKSPACE_PUT_COLLECTION_IO_EXCEPTION, e, getWorkspaceCollectionParams("extracted image file", COLLECTION, output.toURI()), NO_DETAILS); throw new EncoderException("Unable to put image file into the workspace", e); } finally { IOUtils.closeQuietly(in); } } // cleanup cleanup(encodingOutput.toArray(new File[encodingOutput.size()])); MediaPackageElementBuilder builder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder(); List<Attachment> imageAttachments = new LinkedList<Attachment>(); for (URI url : workspaceURIs) { Attachment attachment = (Attachment) builder.elementFromURI(url, Attachment.TYPE, null); imageAttachments.add(attachment); } return imageAttachments; } catch (Exception e) { logger.warn("Error extracting image from " + sourceTrack, e); if (e instanceof EncoderException) { throw (EncoderException) e; } else { throw new EncoderException(e); } } } private void validateVideoStream(Job job, Track sourceTrack) throws EncoderException { // make sure there is a video stream in the track if (sourceTrack != null && !sourceTrack.hasVideo()) { Map<String, String> params = new HashMap<String, String>(); params.put("track-id", sourceTrack.getIdentifier()); params.put("track-url", sourceTrack.getURI().toString()); incident().recordFailure(job, IMAGE_EXTRACTION_NO_VIDEO, params); throw new EncoderException("Cannot extract an image without a video stream"); } } /** * {@inheritDoc} * * @see org.opencastproject.composer.api.ComposerService#convertImage(org.opencastproject.mediapackage.Attachment, * java.lang.String) */ @Override public Job convertImage(Attachment image, String profileId) throws EncoderException, MediaPackageException { if (image == null) throw new IllegalArgumentException("Source image cannot be null"); String[] parameters = new String[2]; parameters[0] = MediaPackageElementParser.getAsXml(image); parameters[1] = profileId; try { return serviceRegistry.createJob(JOB_TYPE, Operation.ImageConversion.toString(), Arrays.asList(parameters)); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create a job", e); } } /** * Converts an image from <code>sourceImage</code> to a new format. * * @param job * the associated job * @param sourceImage * the source image * @param profileId * the identifer of the encoding profile to use * @return the image as an attachment or none if the operation does not return an image. This may happen for example * when doing two pass encodings where the first pass only creates metadata for the second one * @throws EncoderException * if converting the image fails */ protected Option<Attachment> convertImage(Job job, Attachment sourceImage, String profileId) throws EncoderException, MediaPackageException { if (sourceImage == null) throw new EncoderException("SourceImage cannot be null"); try { logger.info("Converting {}", sourceImage); // Get the encoding profile final EncodingProfile profile = getProfile(job, profileId); // Create the encoding engine final EncoderEngine encoderEngine = getEncoderEngine(job, profile); // Finally get the file that needs to be encoded File imageFile; try { imageFile = workspace.get(sourceImage.getURI()); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("source image", Type.Attachment, sourceImage.getURI()), NO_DETAILS); throw new EncoderException("Requested video track " + sourceImage + " was not found", e); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("source image", Type.Attachment, sourceImage.getURI()), NO_DETAILS); throw new EncoderException("Error accessing video track " + sourceImage, e); } // Do the work Option<File> output; try { output = encoderEngine.encode(imageFile, profile, null); } catch (EncoderException e) { Map<String, String> params = new HashMap<String, String>(); params.put("image", sourceImage.getURI().toString()); params.put("profile", profile.getIdentifier()); incident().recordFailure(job, CONVERT_IMAGE_FAILED, e, params, detailsFor(e, encoderEngine)); throw e; } // encoding did not return a file if (output.isNone() || !output.get().exists() || output.get().length() == 0) return none(); // Put the file in the workspace URI workspaceURI = putToCollection(job, output.get(), "converted image file"); MediaPackageElementBuilder builder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder(); Attachment attachment = (Attachment) builder.elementFromURI(workspaceURI, Attachment.TYPE, null); return some(attachment); } catch (Exception e) { logger.warn("Error converting image " + sourceImage, e); if (e instanceof EncoderException) { throw (EncoderException) e; } else { throw new EncoderException(e); } } } /** * {@inheritDoc} * * Supports inserting captions in QuickTime files. * * @see org.opencastproject.composer.api.ComposerService#captions(org.opencastproject.mediapackage.Track, * org.opencastproject.mediapackage.Catalog[]) */ @Override public Job captions(final Track mediaTrack, final Catalog[] captions) throws EmbedderException, MediaPackageException { List<String> args = new ArrayList<String>(); args.set(0, MediaPackageElementParser.getAsXml(mediaTrack)); for (int i = 0; i < captions.length; i++) { args.set(i + 1, MediaPackageElementParser.getAsXml(captions[i])); } try { return serviceRegistry.createJob(JOB_TYPE, Operation.Caption.toString(), args, captionJobLoad); } catch (ServiceRegistryException e) { throw new EmbedderException("Unable to create a job", e); } } /** * Adds the closed captions contained in the <code>captions</code> catalog collection to <code>mediaTrack</code>. * * @param job * the associated job * @param mediaTrack * the source track * @param captions * the caption catalogs * @return the captioned track * @throws EmbedderException * if embedding captions into the track fails */ @SuppressWarnings("unchecked") protected Track captions(Job job, Track mediaTrack, Catalog[] captions) throws EmbedderException { try { logger.info("Attempting to create and embed subtitles to video track"); final String targetTrackId = idBuilder.createNew().toString(); // check if media file has video track if (mediaTrack == null || !mediaTrack.hasVideo()) { Map<String, String> params = new HashMap<String, String>(); params.put("track-id", mediaTrack.getIdentifier()); params.put("track-url", mediaTrack.getURI().toString()); incident().recordFailure(job, CAPTION_NO_VIDEO, params); throw new EmbedderException("Media track must contain video stream"); } // get embedder engine final EmbedderEngine engine = embedderEngineFactory.newEmbedderEngine(); if (engine == null) { final String msg = "Embedder engine not available"; logger.error(msg); incident().recordFailure(job, EMBEDDER_ENGINE_NOT_FOUND, list(tuple("embedder-engine-class", embedderEngineFactory.getClass().getName()))); throw new EmbedderException(msg); } // get video height Integer videoHeigth = null; for (Stream s : mediaTrack.getStreams()) { if (s instanceof VideoStream) { videoHeigth = ((VideoStream) s).getFrameHeight(); break; } } final int subHeight; if (videoHeigth != null) { // get 1/8 of track height // smallest size is 60 pixels subHeight = videoHeigth > 8 * 60 ? videoHeigth / 8 : 60; } else { // no information about video height retrieved, use 60 pixels subHeight = 60; } // retrieve media file final File mediaFile; try { mediaFile = workspace.get(mediaTrack.getURI()); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("source", Type.Track, mediaTrack.getURI()), NO_DETAILS); throw new EmbedderException("Could not find track: " + mediaTrack); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("source", Type.Track, mediaTrack.getURI()), NO_DETAILS); throw new EmbedderException("Error accessing track: " + mediaTrack); } final File[] captionFiles = new File[captions.length]; final String[] captionLanguages = new String[captions.length]; for (int i = 0; i < captions.length; i++) { // get file try { captionFiles[i] = workspace.get(captions[i].getURI()); } catch (NotFoundException e) { incident().recordFailure(job, WORKSPACE_GET_NOT_FOUND, e, getWorkspaceMediapackageParams("caption", Type.Catalog, captions[i].getURI()), NO_DETAILS); throw new EmbedderException("Could not found captions at: " + captions[i]); } catch (IOException e) { incident().recordFailure(job, WORKSPACE_GET_IO_EXCEPTION, e, getWorkspaceMediapackageParams("caption", Type.Catalog, captions[i].getURI()), NO_DETAILS); throw new EmbedderException("Error accessing captions at: " + captions[i]); } // get language captionLanguages[i] = getLanguageFromTags(captions[i].getTags()); if (captionLanguages[i] == null) { Map<String, String> params = new HashMap<String, String>(); params.put("caption-id", captions[i].getIdentifier()); params.put("caption-url", captions[i].getURI().toString()); params.put("caption-tags", StringUtils.join(captions[i].getTags())); incident().recordFailure(job, CAPTION_NO_LANGUAGE, params); throw new EmbedderException("Missing caption language information for captions at: " + captions[i]); } } // set properties Map<String, String> properties = new HashMap<String, String>(); properties.put("param.trackh", String.valueOf(subHeight)); properties.put("param.offset", String.valueOf(subHeight / 2)); properties.put("param.input.stream.count", String.valueOf(mediaTrack.getStreams().length)); File output; try { output = engine.embed(mediaFile, captionFiles, captionLanguages, properties); } catch (EmbedderException e) { Map<String, String> params = new HashMap<String, String>(); params.put("media", mediaTrack.getURI().toString()); params.put("captions", StringUtils.join(captionFiles)); params.put("languages", StringUtils.join(captionLanguages)); params.put("properties", properties.toString()); incident().recordFailure(job, CAPTION_EMBEDD_FAILED, e, params, list(tuple("embedder-engine-class", engine.getClass().getName()))); throw e; } if (!output.exists() || output.length() == 0) { logger.warn("Embedded captions output file {} is empty!", output); throw new NotFoundException("Embedded captions output file " + output.toString() + " is empty!"); } // Put the file in the workspace URI workspaceURI = putToCollection(job, output, "caption catalog file"); // Have the encoded track inspected and return the result Job inspectionJob = inspect(job, workspaceURI); Track inspectedTrack = (Track) MediaPackageElementParser.getFromXml(inspectionJob.getPayload()); inspectedTrack.setIdentifier(targetTrackId); return inspectedTrack; } catch (Exception e) { logger.warn("Error embedding captions into " + mediaTrack, e); if (e instanceof EncoderException) { throw (EmbedderException) e; } else { throw new EmbedderException(e); } } } @Override public Job watermark(Track mediaTrack, String watermark, String profileId) throws EncoderException, MediaPackageException { try { return serviceRegistry.createJob(JOB_TYPE, Operation.Watermark.toString(), Arrays.asList(MediaPackageElementParser.getAsXml(mediaTrack), watermark, profileId)); } catch (ServiceRegistryException e) { throw new EncoderException("Unable to create a job", e); } } /** * Encodes a video track with a watermark. * * @param mediaTrack * the video track * @param watermark * the watermark image * @param encodingProfile * the encoding profile * @return the watermarked track or none if the operation does not return a track. This may happen for example when * doing two pass encodings where the first pass only creates metadata for the second one * @throws EncoderException * if encoding fails */ protected Option<Track> watermark(Job job, Track mediaTrack, String watermark, String encodingProfile) throws EncoderException, MediaPackageException { logger.info("watermarking track {}.", mediaTrack.getIdentifier()); File watermarkFile = new File(watermark); if (!watermarkFile.exists()) { logger.error("Watermark image {} not found.", watermark); Map<String, String> params = new HashMap<String, String>(); params.put("watermark", watermarkFile.getAbsolutePath()); incident().recordFailure(job, WATERMARK_NOT_FOUND, params); throw new EncoderException("Watermark image not found"); } Map<String, String> watermarkProperties = new HashMap<String, String>(); watermarkProperties.put("watermark", watermarkFile.getAbsolutePath()); return encode(job, mediaTrack, null, encodingProfile, watermarkProperties); } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#process(org.opencastproject.job.api.Job) */ @Override protected String process(Job job) throws Exception { Operation op = null; String operation = job.getOperation(); List<String> arguments = job.getArguments(); try { op = Operation.valueOf(operation); Track firstTrack = null; Track secondTrack = null; String encodingProfile = null; final String serialized; switch (op) { case Caption: firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(0)); Catalog[] catalogs = new Catalog[arguments.size() - 1]; for (int i = 1; i < arguments.size(); i++) { catalogs[i] = (Catalog) MediaPackageElementParser.getFromXml(arguments.get(i)); } serialized = MediaPackageElementParser.getAsXml(captions(job, firstTrack, catalogs)); break; case Encode: firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(0)); encodingProfile = arguments.get(1); serialized = encode(job, firstTrack, null, encodingProfile, null).map( MediaPackageElementParser.<Track> getAsXml()).getOrElse(""); break; case ParallelEncode: firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(0)); encodingProfile = arguments.get(1); serialized = MediaPackageElementParser.getArrayAsXml(parralelEncode(job, firstTrack, encodingProfile, null)); break; case Image: firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(0)); encodingProfile = arguments.get(1); List<Attachment> resultingElements; if (Boolean.parseBoolean(arguments.get(2))) { double[] times = new double[arguments.size() - 3]; for (int i = 3; i < arguments.size(); i++) { times[i - 3] = Double.parseDouble(arguments.get(i)); } resultingElements = image(job, firstTrack, encodingProfile, times); } else { Map<String, String> properties = parseProperties(arguments.get(3)); resultingElements = image(job, firstTrack, encodingProfile, properties); } serialized = MediaPackageElementParser.getArrayAsXml(resultingElements); break; case ImageConversion: Attachment sourceImage = (Attachment) MediaPackageElementParser.getFromXml(arguments.get(0)); encodingProfile = arguments.get(1); serialized = convertImage(job, sourceImage, encodingProfile).map( MediaPackageElementParser.<Attachment> getAsXml()).getOrElse(""); break; case Mux: firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(0)); secondTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(1)); encodingProfile = arguments.get(2); serialized = mux(job, firstTrack, secondTrack, encodingProfile).map( MediaPackageElementParser.<Track> getAsXml()).getOrElse(""); break; case Trim: firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(0)); encodingProfile = arguments.get(1); long start = Long.parseLong(arguments.get(2)); long duration = Long.parseLong(arguments.get(3)); serialized = trim(job, firstTrack, encodingProfile, start, duration).map( MediaPackageElementParser.<Track> getAsXml()).getOrElse(""); break; case Watermark: firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(0)); String watermark = arguments.get(1); encodingProfile = arguments.get(2); serialized = watermark(job, firstTrack, watermark, encodingProfile).map( MediaPackageElementParser.<Track> getAsXml()).getOrElse(""); break; case Composite: Attachment watermarkAttachment = null; firstTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(LOWER_TRACK_INDEX)); Layout lowerLayout = Serializer.layout(JsonObj.jsonObj(arguments.get(LOWER_TRACK_LAYOUT_INDEX))); LaidOutElement<Track> lowerLaidOutElement = new LaidOutElement<Track>(firstTrack, lowerLayout); Option<LaidOutElement<Track>> upperLaidOutElement = Option.<LaidOutElement<Track>> none(); if (NOT_AVAILABLE.equals(arguments.get(UPPER_TRACK_INDEX)) && NOT_AVAILABLE.equals(arguments.get(UPPER_TRACK_LAYOUT_INDEX))) { logger.trace("This composite action does not use a second track."); } else { secondTrack = (Track) MediaPackageElementParser.getFromXml(arguments.get(UPPER_TRACK_INDEX)); Layout upperLayout = Serializer.layout(JsonObj.jsonObj(arguments.get(UPPER_TRACK_LAYOUT_INDEX))); upperLaidOutElement = Option.option(new LaidOutElement<Track>(secondTrack, upperLayout)); } Dimension compositeTrackSize = Serializer .dimension(JsonObj.jsonObj(arguments.get(COMPOSITE_TRACK_SIZE_INDEX))); encodingProfile = arguments.get(PROFILE_ID_INDEX); String backgroundColor = arguments.get(BACKGROUND_COLOR_INDEX); Option<LaidOutElement<Attachment>> watermarkOption = Option.none(); if (arguments.size() == 9) { watermarkAttachment = (Attachment) MediaPackageElementParser.getFromXml(arguments.get(WATERMARK_INDEX)); Layout watermarkLayout = Serializer.layout(JsonObj.jsonObj(arguments.get(WATERMARK_LAYOUT_INDEX))); watermarkOption = Option.some(new LaidOutElement<Attachment>(watermarkAttachment, watermarkLayout)); } serialized = composite(job, compositeTrackSize, lowerLaidOutElement, upperLaidOutElement, watermarkOption, encodingProfile, backgroundColor).map(MediaPackageElementParser.<Track> getAsXml()).getOrElse(""); break; case Concat: encodingProfile = arguments.get(0); String dimensionString = arguments.get(1); String frameRateString = arguments.get(2); Dimension outputDimension = null; if (StringUtils.isNotBlank(dimensionString)) outputDimension = Serializer.dimension(JsonObj.jsonObj(dimensionString)); float outputFrameRate = NumberUtils.toFloat(frameRateString, -1.0f); List<Track> tracks = new ArrayList<Track>(); for (int i = 3; i < arguments.size(); i++) { tracks.add(i - 3, (Track) MediaPackageElementParser.getFromXml(arguments.get(i))); } serialized = concat(job, tracks, encodingProfile, outputDimension, outputFrameRate).map( MediaPackageElementParser.<Track> getAsXml()).getOrElse(""); break; case ImageToVideo: Attachment image = (Attachment) MediaPackageElementParser.getFromXml(arguments.get(0)); encodingProfile = arguments.get(1); double time = Double.parseDouble(arguments.get(2)); serialized = imageToVideo(job, image, encodingProfile, time) .map(MediaPackageElementParser.<Track> getAsXml()).getOrElse(""); break; default: throw new IllegalStateException("Don't know how to handle operation '" + operation + "'"); } return serialized; } catch (IllegalArgumentException e) { throw new ServiceRegistryException("This service can't handle operations of type '" + op + "'", e); } catch (IndexOutOfBoundsException e) { throw new ServiceRegistryException("This argument list for operation '" + op + "' does not meet expectations", e); } catch (Exception e) { throw new ServiceRegistryException("Error handling operation '" + op + "'", e); } } /** * {@inheritDoc} * * @see org.opencastproject.composer.api.ComposerService#listProfiles() */ @Override public EncodingProfile[] listProfiles() { Collection<EncodingProfile> profiles = profileScanner.getProfiles().values(); return profiles.toArray(new EncodingProfile[profiles.size()]); } /** * {@inheritDoc} * * @see org.opencastproject.composer.api.ComposerService#getProfile(java.lang.String) */ @Override public EncodingProfile getProfile(String profileId) { return profileScanner.getProfiles().get(profileId); } protected Job inspect(Job job, URI workspaceURI) throws EncoderException { Job inspectionJob; try { inspectionJob = inspectionService.inspect(workspaceURI); } catch (MediaInspectionException e) { incident().recordJobCreationIncident(job, e); throw new EncoderException("Media inspection of " + workspaceURI + " failed", e); } JobBarrier barrier = new JobBarrier(job, serviceRegistry, inspectionJob); if (!barrier.waitForJobs().isSuccess()) { throw new EncoderException("Media inspection of " + workspaceURI + " failed"); } return inspectionJob; } /** * Helper function that iterates tags and returns language from tag in form lang:<lang> * * @param tags * catalog tags * @return language or null if no corresponding tag was found */ protected String getLanguageFromTags(String[] tags) { for (String tag : tags) { if (tag.startsWith("lang:") && tag.length() > 5) { return tag.substring(5); } } return null; } /** * Deletes any valid file in the list. * * @param encodingOutput * list of files to be deleted */ protected void cleanup(File... encodingOutput) { for (File file : encodingOutput) { if (file != null && file.isFile()) { String path = file.getAbsolutePath(); if (file.delete()) { logger.info("Deleted local copy of encoding file at {}", path); } else { logger.warn("Could not delete local copy of encoding file at {}", path); } } } } protected void cleanupWorkspace(URI... workspaceURIs) { for (URI url : workspaceURIs) { try { workspace.delete(url); } catch (Exception e) { logger.warn("Could not delete {} from workspace: {}", url, e.getMessage()); } } } @SuppressWarnings("unchecked") private EncoderEngine getEncoderEngine(Job job, final EncodingProfile profile) throws EncoderException { final EncoderEngine encoderEngine = encoderEngineFactory.newEncoderEngine(profile); if (encoderEngine == null) { final String msg = "No encoder engine available for profile '" + profile.getIdentifier() + "'"; logger.error(msg); incident().recordFailure(job, ENCODER_ENGINE_NOT_FOUND, Collections.map(tuple("profile", profile.getIdentifier()), tuple("profile-name", profile.getName()))); throw new EncoderException(msg); } return encoderEngine; } @SuppressWarnings("unchecked") private EncodingProfile getProfile(Job job, String profileId) throws EncoderException { final EncodingProfile profile = profileScanner.getProfile(profileId); if (profile == null) { final String msg = "Profile " + profileId + " is unknown"; logger.error(msg); incident().recordFailure(job, PROFILE_NOT_FOUND, Collections.map(tuple("profile", profileId))); throw new EncoderException(msg); } return profile; } private Map<String, String> getWorkspaceMediapackageParams(String description, MediaPackageElement.Type type, URI url) { Map<String, String> params = new HashMap<String, String>(); params.put("description", description); params.put("type", type.toString()); params.put("url", url.toString()); return params; } private Map<String, String> getWorkspaceCollectionParams(String description, String collectionId, URI url) { Map<String, String> params = new HashMap<String, String>(); params.put("description", description); params.put("collection", collectionId); params.put("url", url.toString()); return params; } /** * Example composite command below. Use with `-filter_complex` option of ffmpeg if upper video is available otherwise * use -filver:v option for a single video. * * Dual video sample: The ffmpeg command needs two source files set with the `-i` option. The first media file is the * `lower`, the second the `upper` one. Example filter: -filter_complex * [0:v]scale=909:682,pad=1280:720:367:4:0x444345FF[lower];[1:v]scale=358:151[upper];[lower][upper]overlay=4:4[out] * * Single video sample: The ffmpeg command needs one source files set with the `-i` option. Example filter: filter:v * [in]scale=909:682,pad=1280:720:367:4:0x444345FF[out] * * @return commandline part with -filter_complex and -map options */ protected static String buildCompositeCommand(Dimension compositeTrackSize, LaidOutElement<Track> lowerLaidOutElement, Option<LaidOutElement<Track>> upperLaidOutElement, Option<File> upperFile, Option<LaidOutElement<Attachment>> watermarkOption, File watermarkFile, String backgroundColor) { final StringBuilder cmd = new StringBuilder(); final String videoId = watermarkOption.isNone() ? "[out]" : "[video]"; if (upperLaidOutElement.isNone()) { // There is only one video track and possibly one watermark. final Layout videoLayout = lowerLaidOutElement.getLayout(); final String videoPosition = videoLayout.getOffset().getX() + ":" + videoLayout.getOffset().getY(); final String scaleVideo = videoLayout.getDimension().getWidth() + ":" + videoLayout.getDimension().getHeight(); final String padLower = compositeTrackSize.getWidth() + ":" + compositeTrackSize.getHeight() + ":" + videoPosition + ":" + backgroundColor; cmd.append("-filter:v [in]scale=").append(scaleVideo).append(",pad=").append(padLower).append(videoId); } else if (upperFile.isSome() && upperLaidOutElement.isSome()) { // There are two video tracks to handle. final Layout lowerLayout = lowerLaidOutElement.getLayout(); final Layout upperLayout = upperLaidOutElement.get().getLayout(); final String upperPosition = upperLayout.getOffset().getX() + ":" + upperLayout.getOffset().getY(); final String lowerPosition = lowerLayout.getOffset().getX() + ":" + lowerLayout.getOffset().getY(); final String scaleUpper = upperLayout.getDimension().getWidth() + ":" + upperLayout.getDimension().getHeight(); final String scaleLower = lowerLayout.getDimension().getWidth() + ":" + lowerLayout.getDimension().getHeight(); final String padLower = compositeTrackSize.getWidth() + ":" + compositeTrackSize.getHeight() + ":" + lowerPosition + ":" + backgroundColor; // Add input file for the upper track cmd.append("-i " + upperFile.get().getAbsolutePath() + " "); // Add filter complex mode cmd.append("-filter_complex"). // lower video append(" [0:v]scale=").append(scaleLower).append(",pad=").append(padLower).append("[lower]") // upper video .append(";[1:v]scale=").append(scaleUpper).append("[upper]") // mix .append(";[lower][upper]overlay=").append(upperPosition).append(videoId); } for (final LaidOutElement<Attachment> watermarkLayout : watermarkOption) { String watermarkPosition = watermarkLayout.getLayout().getOffset().getX() + ":" + watermarkLayout.getLayout().getOffset().getY(); cmd.append(";").append("movie=").append(watermarkFile.getAbsoluteFile()).append("[watermark];").append(videoId) .append("[watermark]overlay=").append(watermarkPosition).append("[out]"); } if (upperLaidOutElement.isSome()) { // handle audio // if both videos contain audio mix it into a single audio stream final boolean lowerAudio = lowerLaidOutElement.getElement().hasAudio(); final boolean upperAudio = upperLaidOutElement.get().getElement().hasAudio(); if (lowerAudio && upperAudio) { cmd.append(";[0:a][1:a]amix=inputs=2[aout] -map [out] -map [aout]"); } else if (lowerAudio) { cmd.append(" -map [out] -map 0:a"); } else if (upperAudio) { cmd.append(" -map [out] -map 1:a"); } else { cmd.append(" -map [out]"); } } return cmd.toString(); } private String buildConcatCommand(boolean onlyAudio, Dimension dimension, float outputFrameRate, List<File> files, List<Track> tracks) { StringBuilder sb = new StringBuilder(); // Add input file paths for (File f : files) { sb.append("-i ").append(f.getAbsolutePath()).append(" "); } sb.append("-filter_complex "); boolean hasAudio = false; if (!onlyAudio) { // fps video filter if outputFrameRate is valid String fpsFilter = StringUtils.EMPTY; if (outputFrameRate > 0) { fpsFilter = String.format(Locale.US, "fps=fps=%f,", outputFrameRate); } // Add video scaling and check for audio int characterCount = 0; for (int i = 0; i < files.size(); i++) { if ((i % 25) == 0) characterCount++; sb.append("[").append(i).append(":v]").append(fpsFilter) .append("scale=iw*min(").append(dimension.getWidth()).append("/iw\\,") .append(dimension.getHeight()).append("/ih):ih*min(").append(dimension.getWidth()).append("/iw\\,") .append(dimension.getHeight()).append("/ih),pad=").append(dimension.getWidth()).append(":") .append(dimension.getHeight()).append(":(ow-iw)/2:(oh-ih)/2").append(",setdar=") .append((float) dimension.getWidth() / (float) dimension.getHeight()).append("["); int character = ('a' + i + 1 - ((characterCount - 1) * 25)); for (int y = 0; y < characterCount; y++) { sb.append((char) character); } sb.append("];"); if (tracks.get(i).hasAudio()) hasAudio = true; } // Add silent audio streams if at least one audio stream is available if (hasAudio) { for (int i = 0; i < files.size(); i++) { if (!tracks.get(i).hasAudio()) sb.append("aevalsrc=0::d=1[silent").append(i + 1).append("];"); } } } // Add concat segments int characterCount = 0; for (int i = 0; i < files.size(); i++) { if ((i % 25) == 0) characterCount++; int character = ('a' + i + 1 - ((characterCount - 1) * 25)); if (!onlyAudio) { sb.append("["); for (int y = 0; y < characterCount; y++) { sb.append((char) character); } sb.append("]"); } if (tracks.get(i).hasAudio()) { sb.append("[").append(i).append(":a]"); } else if (hasAudio) { sb.append("[silent").append(i + 1).append("]"); } } // Add concat command and output mapping sb.append("concat=n=").append(files.size()).append(":v="); if (onlyAudio) { sb.append("0"); } else { sb.append("1"); } sb.append(":a="); if (!onlyAudio) { if (hasAudio) { sb.append("1[v][a] -map [v] -map [a] "); } else { sb.append("0[v] -map [v] "); } } else { sb.append("1[a] -map [a]"); } return sb.toString(); } private URI putToCollection(Job job, File output, String description) throws EncoderException { URI returnURL = null; InputStream in = null; try { in = new FileInputStream(output); returnURL = workspace.putInCollection(COLLECTION, job.getId() + "." + FilenameUtils.getExtension(output.getAbsolutePath()), in); logger.info("Copied the {} to the workspace at {}", description, returnURL); return returnURL; } catch (Exception e) { incident().recordFailure(job, WORKSPACE_PUT_COLLECTION_IO_EXCEPTION, e, getWorkspaceCollectionParams(description, COLLECTION, output.toURI()), NO_DETAILS); cleanupWorkspace(returnURL); throw new EncoderException("Unable to put the " + description + " into the workspace", e); } finally { cleanup(output); IOUtils.closeQuietly(in); } } private static List<Tuple<String, String>> detailsFor(EncoderException ex, EncoderEngine engine) { final List<Tuple<String, String>> d = Mutables.arrayList(); d.add(tuple("encoder-engine-class", engine.getClass().getName())); if (ex instanceof CmdlineEncoderException) { d.add(tuple("encoder-commandline", ((CmdlineEncoderException) ex).getCommandLine())); } return d; } private Map<String, String> parseProperties(String serializedProperties) throws IOException { Properties properties = new Properties(); InputStream in = null; try { in = IOUtils.toInputStream(serializedProperties, "UTF-8"); properties.load(in); Map<String, String> map = new HashMap<String, String>(); for (Entry<Object, Object> e : properties.entrySet()) { map.put((String) e.getKey(), (String) e.getValue()); } return map; } finally { IOUtils.closeQuietly(in); } } private String getPropertiesAsString(Map<String, String> props) { StringBuilder sb = new StringBuilder(); for (Entry<String, String> entry : props.entrySet()) { sb.append(entry.getKey()); sb.append("="); sb.append(entry.getValue()); sb.append("\n"); } return sb.toString(); } /** * Sets the media inspection service * * @param mediaInspectionService * an instance of the media inspection service */ protected void setMediaInspectionService(MediaInspectionService mediaInspectionService) { this.inspectionService = mediaInspectionService; } /** * Sets the encoder engine factory * * @param encoderEngineFactory * The encoder engine factory */ protected void setEncoderEngineFactory(EncoderEngineFactory encoderEngineFactory) { this.encoderEngineFactory = encoderEngineFactory; } /** * Sets the embedder engine factoy * * @param embedderEngineFactory * The embedder engine factory */ protected void setEmbedderEngineFactory(EmbedderEngineFactory embedderEngineFactory) { this.embedderEngineFactory = embedderEngineFactory; } /** * Sets the workspace * * @param workspace * an instance of the workspace */ protected void setWorkspace(Workspace workspace) { this.workspace = workspace; } /** * Sets the service registry * * @param serviceRegistry * the service registry */ protected void setServiceRegistry(ServiceRegistry serviceRegistry) { this.serviceRegistry = serviceRegistry; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getServiceRegistry() */ @Override protected ServiceRegistry getServiceRegistry() { return serviceRegistry; } /** * Sets the profile scanner. * * @param scanner * the profile scanner */ protected void setProfileScanner(EncodingProfileScanner scanner) { this.profileScanner = scanner; } /** * Callback for setting the security service. * * @param securityService * the securityService to set */ public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } /** * Callback for setting the user directory service. * * @param userDirectoryService * the userDirectoryService to set */ public void setUserDirectoryService(UserDirectoryService userDirectoryService) { this.userDirectoryService = userDirectoryService; } /** * Sets a reference to the organization directory service. * * @param organizationDirectory * the organization directory */ public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) { this.organizationDirectoryService = organizationDirectory; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getSecurityService() */ @Override protected SecurityService getSecurityService() { return securityService; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getUserDirectoryService() */ @Override protected UserDirectoryService getUserDirectoryService() { return userDirectoryService; } /** * {@inheritDoc} * * @see org.opencastproject.job.api.AbstractJobProducer#getOrganizationDirectoryService() */ @Override protected OrganizationDirectoryService getOrganizationDirectoryService() { return organizationDirectoryService; } @Override public void updated(Dictionary properties) throws ConfigurationException { captionJobLoad = LoadUtil.getConfiguredLoadValue(properties, CAPTION_JOB_LOAD_KEY, DEFAULT_CAPTION_JOB_LOAD, serviceRegistry); } }