Java tutorial
/** * Copyright 2009, 2010 The Regents of the University of California * Licensed 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://www.osedu.org/licenses/ECL-2.0 * * 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.gstreamer; import org.opencastproject.composer.api.EncoderEngine; import org.opencastproject.composer.api.EncoderException; import org.opencastproject.composer.api.EncoderListener; import org.opencastproject.composer.api.EncodingProfile; import org.opencastproject.composer.api.EncodingProfile.MediaType; import org.apache.commons.io.FilenameUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Scanner; import java.util.UUID; import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.activation.MimetypesFileTypeMap; /** * Abstract base class for GStreamer encoder engines. */ public abstract class AbstractGSEncoderEngine implements EncoderEngine { /** Logging utility */ private static Logger logger = LoggerFactory.getLogger(AbstractGSEncoderEngine.class); /** List of installed listeners */ protected List<EncoderListener> listeners = new CopyOnWriteArrayList<EncoderListener>(); /** Supported profiles for this engine */ protected Map<String, EncodingProfile> supportedProfiles = new HashMap<String, EncodingProfile>(); /* * (non-Javadoc) * * @see * org.opencastproject.composer.api.EncoderEngine#addEncoderListener(org.opencastproject.composer.api.EncoderListener) */ @Override public void addEncoderListener(EncoderListener listener) { if (!listeners.contains(listener)) listeners.add(listener); } /* * (non-Javadoc) * * @see * org.opencastproject.composer.api.EncoderEngine#removeEncoderListener(org.opencastproject.composer.api.EncoderListener * ) */ @Override public void removeEncoderListener(EncoderListener listener) { listeners.remove(listener); } /* * (non-Javadoc) * * @see org.opencastproject.composer.api.EncoderEngine#encode(java.io.File, * org.opencastproject.composer.api.EncodingProfile, java.util.Map) */ @Override public File encode(File mediaSource, EncodingProfile format, Map<String, String> properties) throws EncoderException { return process(null, mediaSource, format, properties); } /* * (non-Javadoc) * * @see org.opencastproject.composer.api.EncoderEngine#mux(java.io.File, java.io.File, * org.opencastproject.composer.api.EncodingProfile, java.util.Map) */ @Override public File mux(File audioSource, File videoSource, EncodingProfile format, Map<String, String> properties) throws EncoderException { return process(audioSource, videoSource, format, properties); } /* * (non-Javadoc) * * @see org.opencastproject.composer.api.EncoderEngine#trim(java.io.File, * org.opencastproject.composer.api.EncodingProfile, long, long, java.util.Map) */ @Override public File trim(File mediaSource, EncodingProfile format, long start, long duration, Map<String, String> properties) throws EncoderException { if (properties == null) { properties = new Hashtable<String, String>(); } properties.put("trim.start", Long.toString(start * 1000000L)); properties.put("trim.duration", Long.toString(duration * 1000000L)); return process(null, mediaSource, format, properties); } /** * Substitutes template values from template with actual values from properties. * * @param template * String that represents template * @param properties * Map that contains substitution for template values in template * @param cleanup * if template values that were not matched should be removed * @return String built from template */ protected String substituteTemplateValues(String template, Map<String, String> properties, boolean cleanup) { StringBuffer buffer = new StringBuffer(); Pattern pattern = Pattern.compile("#\\{\\S+?\\}"); Matcher matcher = pattern.matcher(template); while (matcher.find()) { String match = template.substring(matcher.start() + 2, matcher.end() - 1); if (properties.containsKey(match)) { matcher.appendReplacement(buffer, properties.get(match)); } } matcher.appendTail(buffer); String processedTemplate = buffer.toString(); if (cleanup) { // remove all property matches buffer = new StringBuffer(); Pattern ppattern = Pattern.compile("\\S+?=#\\{\\S+?\\}"); matcher = ppattern.matcher(processedTemplate); while (matcher.find()) { matcher.appendReplacement(buffer, ""); } matcher.appendTail(buffer); processedTemplate = buffer.toString(); // remove all other templates buffer = new StringBuffer(); matcher = pattern.matcher(processedTemplate); while (matcher.find()) { matcher.appendReplacement(buffer, ""); } matcher.appendTail(buffer); processedTemplate = buffer.toString(); } return processedTemplate; } /** * Executes encoding job. At least one source has to be specified. * * @param audioSource * File that contains audio source (if used) * @param videoSource * File that contains video source (if used) * @param profile * EncodingProfile used for this encoding job * @param properties * Map containing any additional properties * @return File created as result of this encoding job * @throws EncoderException * if encoding fails */ protected File process(File audioSource, File videoSource, EncodingProfile profile, Map<String, String> properties) throws EncoderException { Map<String, String> params = new HashMap<String, String>(); if (properties != null) { params.putAll(properties); } try { if (audioSource == null && videoSource == null) { throw new IllegalArgumentException("At least one source must be specified."); } // Set encoding parameters if (audioSource != null) { String audioInput = FilenameUtils.normalize(audioSource.getAbsolutePath()); params.put("in.audio.path", audioInput); params.put("in.audio.name", FilenameUtils.getBaseName(audioInput)); params.put("in.audio.suffix", FilenameUtils.getExtension(audioInput)); params.put("in.audio.filename", FilenameUtils.getName(audioInput)); params.put("in.audio.mimetype", MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(audioInput)); } if (videoSource != null) { String videoInput = FilenameUtils.normalize(videoSource.getAbsolutePath()); params.put("in.video.path", videoInput); params.put("in.video.name", FilenameUtils.getBaseName(videoInput)); params.put("in.video.suffix", FilenameUtils.getExtension(videoInput)); params.put("in.video.filename", FilenameUtils.getName(videoInput)); params.put("in.video.mimetype", MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(videoInput)); } File parentFile; if (videoSource == null) { parentFile = audioSource; } else { parentFile = videoSource; } String outDir = parentFile.getAbsoluteFile().getParent(); String outFileName = FilenameUtils.getBaseName(parentFile.getName()); String outSuffix = substituteTemplateValues(profile.getSuffix(), params, false); if (new File(outDir, outFileName + outSuffix).exists()) { outFileName += "_" + UUID.randomUUID().toString(); } params.put("out.dir", outDir); params.put("out.name", outFileName); params.put("out.suffix", outSuffix); File encodedFile = new File(outDir, outFileName + outSuffix); params.put("out.file.path", encodedFile.getAbsolutePath()); // create and launch gstreamer pipeline createAndLaunchPipeline(profile, params); if (audioSource != null) { logger.info("Audio track {} and video track {} successfully encoded using profile '{}'", new String[] { (audioSource == null ? "N/A" : audioSource.getName()), (videoSource == null ? "N/A" : videoSource.getName()), profile.getIdentifier() }); } else { logger.info("Video track {} successfully encoded using profile '{}'", new String[] { videoSource.getName(), profile.getIdentifier() }); } fireEncoded(this, profile, audioSource, videoSource); return encodedFile; } catch (EncoderException e) { if (audioSource != null) { logger.warn("Error while encoding audio track {} and video track {} using '{}': {}", new String[] { (audioSource == null ? "N/A" : audioSource.getName()), (videoSource == null ? "N/A" : videoSource.getName()), profile.getIdentifier(), e.getMessage() }); } else { logger.warn("Error while encoding video track {} using '{}': {}", new String[] { (videoSource == null ? "N/A" : videoSource.getName()), profile.getIdentifier(), e.getMessage() }); } fireEncodingFailed(this, profile, e, audioSource, videoSource); throw e; } catch (Exception e) { logger.warn("Error while encoding audio {} and video {} to {}:{}, {}", new Object[] { (audioSource == null ? "N/A" : audioSource.getName()), (videoSource == null ? "N/A" : videoSource.getName()), profile.getName(), e.getMessage() }); fireEncodingFailed(this, profile, e, audioSource, videoSource); throw new EncoderException(this, e.getMessage(), e); } } /** * Creates Pipeline from profile and additional properties and launches it. * * @param profile * EncodingProfile used for creating Pipeline * @param properties * additional properties for creating Pipeline * @throws EncoderException * if Pipeline creation or execution fails */ protected abstract void createAndLaunchPipeline(EncodingProfile profile, Map<String, String> properties) throws EncoderException; /* * (non-Javadoc) * * @see org.opencastproject.composer.api.EncoderEngine#extract(java.io.File, * org.opencastproject.composer.api.EncodingProfile, java.util.Map, long[]) */ @Override public List<File> extract(File mediaSource, EncodingProfile profile, Map<String, String> properties, long... times) throws EncoderException { Map<String, String> params = new HashMap<String, String>(); if (properties != null) { params.putAll(properties); } try { if (mediaSource == null) { throw new IllegalArgumentException("Media source must be specified."); } if (times.length == 0) { throw new IllegalArgumentException("At least one time has to be specified"); } // build string definition String imageDimensions = profile.getExtension("gstreamer.image.dimensions"); if (imageDimensions == null) { throw new EncoderException("Missing dimension definition in encoding profile"); } StringBuilder definition = new StringBuilder(); definition.append(Long.toString(times[0]) + ":" + imageDimensions); for (int i = 1; i < times.length; i++) { definition.append("," + Long.toString(times[i]) + ":" + imageDimensions); } params.put("gstreamer.image.extraction", definition.toString()); // Set encoding parameters String mediaInput = FilenameUtils.normalize(mediaSource.getAbsolutePath()); params.put("in.video.path", mediaInput); params.put("in.video.mimetype", MimetypesFileTypeMap.getDefaultFileTypeMap().getContentType(mediaInput)); String outDir = mediaSource.getAbsoluteFile().getParent(); String outFileName = FilenameUtils.getBaseName(mediaSource.getName()); String outSuffix = profile.getSuffix(); // add time template if (!outSuffix.contains("#{time}")) { outFileName += "_#{time}"; } File encodedFileTemplate = new File(outDir, outFileName + outSuffix); params.put("out.file.path", encodedFileTemplate.getAbsolutePath()); // extract images List<File> outputImages = extractMultipleImages(profile, params); if (outputImages.size() == 0) { logger.warn("No images were extracted from video track {} using encoding profile '{}'", mediaSource.getName(), profile.getIdentifier()); } logger.info("Images successfully extracted from video track {} using profile '{}'", new String[] { mediaSource.getName(), profile.getIdentifier() }); fireEncoded(this, profile, mediaSource); return outputImages; } catch (EncoderException e) { logger.warn("Error while extracting images from video track {} using '{}': {}", new String[] { (mediaSource == null ? "N/A" : mediaSource.getName()), profile.getIdentifier(), e.getMessage() }); fireEncodingFailed(this, profile, e, mediaSource); throw e; } catch (Exception e) { logger.warn("Error while extracting images from video track {} using '{}': {}", new String[] { (mediaSource == null ? "N/A" : mediaSource.getName()), profile.getIdentifier(), e.getMessage() }); fireEncodingFailed(this, profile, e, mediaSource); throw new EncoderException(this, e.getMessage(), e); } } /** * Extracts multiple images from video stream. Profile is looked for the following template: <time in * seconds>:<image width>x<image height>. Multiple image definitions can be separated with comma. If * image width or image height is less or equal to zero, original image size will be retained. * * @param profile * EncodeingProfile used for image extraction * @param properties * additional properties used in extraction * @return List of extracted image's files * @throws EncoderException * if extraction fails */ protected abstract List<File> extractMultipleImages(EncodingProfile profile, Map<String, String> properties) throws EncoderException; /* * (non-Javadoc) * * @see org.opencastproject.composer.api.EncoderEngine#supportsMultithreading() */ @Override public boolean supportsMultithreading() { return true; } /* * (non-Javadoc) * * @see org.opencastproject.composer.api.EncoderEngine#supportsProfile(java.lang.String, * org.opencastproject.composer.api.EncodingProfile.MediaType) */ @Override public boolean supportsProfile(String profile, MediaType type) { if (supportedProfiles.containsKey(profile)) { EncodingProfile p = supportedProfiles.get(profile); return p.isApplicableTo(type); } return false; } /* * (non-Javadoc) * * @see org.opencastproject.composer.api.EncoderEngine#needsLocalWorkCopy() */ @Override public boolean needsLocalWorkCopy() { return false; } /** * This method is called to send the <code>formatEncoded</code> event to registered encoding listeners. * * @param engine * the encoding engine * @param profile * the media format * @param sourceFiles * the source files encoded */ protected void fireEncoded(EncoderEngine engine, EncodingProfile profile, File... sourceFiles) { for (EncoderListener l : listeners) { try { l.fileEncoded(engine, profile, sourceFiles); } catch (Throwable t) { logger.error("Encoder listener " + l + " threw exception while handling callback"); } } } /** * This method is called to send the <code>trackEncodingFailed</code> event to registered encoding listeners. * * @param engine * the encoding engine * @param sourceFiles * the files that were encoded * @param profile * the media format * @param cause * the reason of failure */ protected void fireEncodingFailed(EncoderEngine engine, EncodingProfile profile, Throwable cause, File... sourceFiles) { for (EncoderListener l : listeners) { try { l.fileEncodingFailed(engine, profile, cause, sourceFiles); } catch (Throwable t) { logger.error("Encoder listener {} threw exception while handling callback", l); } } } /** * This method is called to send the <code>trackEncodingProgressed</code> event to registered encoding listeners. * * @param engine * the encoding engine * @param sourceFile * the file that is being encoded * @param profile * the media format * @param progress * the progress value */ protected void fireEncodingProgressed(EncoderEngine engine, File sourceFile, EncodingProfile profile, int progress) { for (EncoderListener l : listeners) { try { l.fileEncodingProgressed(engine, sourceFile, profile, progress); } catch (Throwable t) { logger.error("Encoder listener " + l + " threw exception while handling callback"); } } } /** * Parses image extraction configuration in the following format: #{image_time_1}:#{image_width}x#{image_height}. * Multiple extraction configurations can be separated by comma. * * @param configuration * Configuration for image extraction * @param outputTemplate * output path template. Should be in the form /some_file_name_#{time}.jpg so that each image will have it's * unique path. * @return parsed List for image extraction */ protected List<ImageExtractionProperties> parseImageExtractionConfiguration(String configuration, String outputTemplate) { LinkedList<ImageExtractionProperties> propertiesList = new LinkedList<AbstractGSEncoderEngine.ImageExtractionProperties>(); Scanner scanner = new Scanner(configuration); scanner.useDelimiter(","); int counter = 0; while (scanner.hasNext()) { String nextToken = scanner.next().trim(); if (!nextToken.matches("[0-9]+:[0-9]+[x|X][0-9]+")) { throw new IllegalArgumentException("Invalid token found: " + nextToken); } String[] properties = nextToken.split("[:|x|X]"); String output = outputTemplate.replaceAll("#\\{time\\}", properties[0]); if (output.equals(outputTemplate)) { logger.warn("Output filename does not contain #{time} template: multiple images will overwrite"); } if (new File(output).exists()) { String outputFile = FilenameUtils.removeExtension(output); String extension = FilenameUtils.getExtension(output); output = outputFile + "_reencode." + extension; } ImageExtractionProperties imageProperties = new ImageExtractionProperties(counter++, Long.parseLong(properties[0]), Integer.parseInt(properties[1]), Integer.parseInt(properties[2]), output); propertiesList.add(imageProperties); } Collections.sort(propertiesList, new Comparator<ImageExtractionProperties>() { @Override public int compare(ImageExtractionProperties o1, ImageExtractionProperties o2) { return (int) (o2.timeInSeconds - o1.timeInSeconds); } }); return propertiesList; } /** * Reorder images to the same way as they were specified in profile and returns only list of filenames. * * @param extractionProperties * extraction properties for images * @return List of image filenames */ protected List<File> reorder(List<ImageExtractionProperties> extractionProperties) { Collections.sort(extractionProperties, new Comparator<ImageExtractionProperties>() { @Override public int compare(ImageExtractionProperties o1, ImageExtractionProperties o2) { return o2.order - o1.order; } }); List<File> outputImages = new LinkedList<File>(); for (ImageExtractionProperties properties : extractionProperties) { outputImages.add(new File(properties.imageOutput)); } return outputImages; } /** * Removes any existing file from image extraction properties. * * @param extractionProperties */ protected void cleanup(List<ImageExtractionProperties> extractionProperties) { for (ImageExtractionProperties properties : extractionProperties) { File file = new File(properties.imageOutput); if (file.exists() && !file.delete()) { logger.warn("Could not delete file: {}", properties.imageOutput); } } } /** * Class that holds information for image extraction. */ protected class ImageExtractionProperties { /** sequence in template */ private int order; /** extraction time */ private long timeInSeconds; /** image width */ private int imageWidth; /** image height */ private int imageHeight; /** output path */ private String imageOutput; public ImageExtractionProperties(int order, long timeInSeconds, int imageWidth, int imageHeight, String imageOutput) { this.order = order; this.timeInSeconds = timeInSeconds; this.imageWidth = imageWidth; this.imageHeight = imageHeight; this.imageOutput = imageOutput; } public int getOrder() { return order; } public long getTimeInSeconds() { return timeInSeconds; } public int getImageWidth() { return imageWidth; } public int getImageHeight() { return imageHeight; } public String getImageOutput() { return imageOutput; } } }