/**
* 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.ffmpeg;
import static org.apache.commons.lang3.StringUtils.startsWithAny;
import org.opencastproject.composer.api.EncoderException;
import org.opencastproject.composer.api.EncodingProfile;
import org.opencastproject.composer.impl.AbstractCmdlineEncoderEngine;
import org.opencastproject.util.data.Option;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Implementation for the encoder engine backed by ffmpeg.
*/
public class FFmpegEncoderEngine extends AbstractCmdlineEncoderEngine {
/** Default location of the ffmepg binary (resembling the installer) */
public static final String FFMPEG_BINARY_DEFAULT = "ffmpeg";
/** The ffmpeg commandline suffix */
public static final String CMD_SUFFIX = "ffmpeg.command";
private static final String CONFIG_FFMPEG_PATH = "org.opencastproject.composer.ffmpeg.path";
/** Format for trim times */
private static final String TIME_FORMAT = "%02d:%02d:";
/** The trimming start time property name */
public static final String PROP_TRIMMING_START_TIME = "trim.start";
/** The trimming duration property name */
public static final String PROP_TRIMMING_DURATION = "trim.duration";
/** the logging facility provided by log4j */
private static final Logger logger = LoggerFactory.getLogger(FFmpegEncoderEngine.class);
/**
* Creates the ffmpeg encoder engine.
*/
public FFmpegEncoderEngine() {
super(FFMPEG_BINARY_DEFAULT);
}
public void activate(ComponentContext cc) {
// Configure ffmpeg
String path = (String) cc.getBundleContext().getProperty(CONFIG_FFMPEG_PATH);
if (path == null) {
logger.debug("DEFAULT " + CONFIG_FFMPEG_PATH + ": " + FFmpegEncoderEngine.FFMPEG_BINARY_DEFAULT);
} else {
setBinary(path);
logger.debug("FFmpegEncoderEngine config binary: {}", path);
}
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.composer.impl.AbstractCmdlineEncoderEngine#trim(java.io.File,
* org.opencastproject.composer.api.EncodingProfile, long, long, java.util.Map)
*/
@Override
public Option<File> trim(File mediaSource, EncodingProfile format, long start, long duration,
Map<String, String> properties) throws EncoderException {
if (properties == null)
properties = new HashMap<String, String>();
double startD = (double) start / 1000;
double durationD = (double) duration / 1000;
DecimalFormatSymbols ffmpegFormat = new DecimalFormatSymbols();
ffmpegFormat.setDecimalSeparator('.');
DecimalFormat df = new DecimalFormat("00.000", ffmpegFormat);
properties.put(
PROP_TRIMMING_START_TIME,
String.format(TIME_FORMAT, (long) Math.floor(startD / 3600), (long) (startD % 3600) / 60)
+ df.format(startD % 60));
properties.put(
PROP_TRIMMING_DURATION,
String.format(TIME_FORMAT, (long) Math.floor(durationD / 3600), (long) (durationD % 3600) / 60)
+ df.format(durationD % 60));
return super.trim(mediaSource, format, start, duration, properties);
}
/**
* Creates the arguments for the commandline.
*
* @param format
* the format
* @return the argument list
*/
@Override
protected List<String> buildArgumentList(EncodingProfile format) throws EncoderException {
String commandline = format.getExtension(CMD_SUFFIX);
if (commandline == null)
throw new EncoderException(this, "No commandline configured for " + format);
// Process the commandline. The variables in that commandline might either
// be replaced by commandline parts from the configuration or commandline
// parameters as specified at runtime.
for (Map.Entry<String, String> entry : format.getExtensions().entrySet()) {
String key = entry.getKey();
if (key.startsWith(CMD_SUFFIX) && key.length() > CMD_SUFFIX.length()) {
String value = processParameters(entry.getValue());
String partName = "#\\{" + key.substring(CMD_SUFFIX.length() + 1) + "\\}";
if (!value.matches(".*#\\{.*\\}.*"))
commandline = commandline.replaceAll(partName, value);
}
}
// Replace the commandline parameters passed in at compile time
commandline = processParameters(commandline);
List<String> argumentList = new ArrayList<String>();
// Disable the print of encoding progress/statistics.
argumentList.add("-nostats");
String[] args = commandline.split(" ");
for (String a : args)
if (!"".equals(a.trim()))
argumentList.add(a);
return argumentList;
}
/**
* Handles the encoder output by analyzing it first and then firing it off to the registered listeners.
*
* @param sourceFiles
* the source files that are currently being encoded
* @param format
* the target media format
* @param message
* the message returned by the encoder
*/
@Override
protected void handleEncoderOutput(EncodingProfile format, String message, File... sourceFiles) {
super.handleEncoderOutput(format, message, sourceFiles);
message = message.trim();
if ("".equals(message))
return;
// Completely skip these messages
if (message.startsWith("Press ["))
return;
// Others go to trace logging
if (startsWithAny(message.toLowerCase(),
new String[] {"ffmpeg version", "configuration", "lib", "size=", "frame=", "built with"})) {
logger.trace(message);
// Some to debug
} else if (startsWithAny(message.toLowerCase(),
new String[] { "artist", "compatible_brands", "copyright", "creation_time", "description", "duration",
"encoder", "handler_name", "input #", "last message repeated", "major_brand", "metadata", "minor_version",
"output #", "program", "side data:", "stream #", "stream mapping", "title", "video:", "[libx264 @ "})) {
logger.debug(message);
// And the rest is likely to deserve at least info
} else {
logger.info(message);
}
}
/**
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return "ffmpeg";
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.composer.impl.AbstractEncoderEngine#getOutputFile(java.io.File,
* org.opencastproject.composer.api.EncodingProfile)
*/
@Override
protected File getOutputFile(File source, EncodingProfile profile) {
File outputFile = null;
try {
List<String> arguments = buildArgumentList(profile);
// TODO: Very unsafe! Improve!
outputFile = new File(arguments.get(arguments.size() - 1));
} catch (EncoderException e) {
// Unlikely. We checked that before
}
return outputFile;
}
}