/**
* 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 org.opencastproject.composer.api.EmbedderException;
import org.opencastproject.composer.impl.AbstractCmdlineEmbedderEngine;
import org.opencastproject.util.IoSupport;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class FFmpegEmbedderEngine extends AbstractCmdlineEmbedderEngine {
/** Default location of the ffmepg binary */
public static final String FFMPEG_BINARY_DEFAULT = "ffmpeg";
/** Parameter name for retrieving ffmpeg path */
private static final String CONFIG_FFMPEG_PATH = "org.opencastproject.composer.ffmpeg.path";
/** Command line template for executing job */
private static final String CMD_TEMPLATE = "#{-i in.media.path} #<-i #{in.captions.path}> -c copy #<-map #{param.input}:0> #<-map #{param.map}:0 -metadata:s:s:#{param.index} language=#{param.lang}> -scodec mov_text #{out.media.path}";
/** the logging facility provided by log4j */
private static final Logger logger = LoggerFactory.getLogger(FFmpegEmbedderEngine.class);
/**
* Creates the ffmpeg embedder engine.
*/
public FFmpegEmbedderEngine() {
super(FFMPEG_BINARY_DEFAULT);
setCmdTemplate(CMD_TEMPLATE);
}
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("FFmpegEmbedderEngine config binary: {}", path);
}
}
/**
*
* {@inheritDoc} Language attribute is normalized via <code>normalizeLanguage</code> method even if it is not present.
* If normalized language returned is <code>null</code>, exception will be thrown.
*/
@Override
public File embed(File mediaSource, File[] captionSources, String[] captionLanguages, Map<String, String> properties)
throws EmbedderException {
if (mediaSource == null) {
logger.error("Media source is missing");
throw new EmbedderException("Missing media source.");
}
if (captionSources == null || captionSources.length == 0) {
logger.error("Captions are missing");
throw new EmbedderException("Missing captions.");
}
if (captionLanguages == null || captionLanguages.length == 0) {
logger.error("Caption languages are missing");
throw new EmbedderException("Missing caption language codes.");
}
// add all properties
Map<String, String> embedderProperties = new HashMap<String, String>();
embedderProperties.putAll(properties);
// add file properties
embedderProperties.put("in.media.path", mediaSource.getAbsolutePath());
embedderProperties.put("out.media.path",
mediaSource.getAbsoluteFile().getParent() + File.separator + UUID.randomUUID() + "-caption."
+ FilenameUtils.getExtension(mediaSource.getAbsolutePath()));
int inputStreamCount;
try {
inputStreamCount = Integer.valueOf(properties.get("param.input.stream.count"));
} catch (NumberFormatException e) {
logger.info("No stream count found, assuming input file is single-stream");
inputStreamCount = 1;
}
for (int i = 0; i < inputStreamCount; i++) {
embedderProperties.put("param.input." + i, String.valueOf(i));
}
for (int i = 0; i < ((captionSources.length > captionLanguages.length) ? captionSources.length
: captionLanguages.length); i++) {
embedderProperties.put("in.captions.path." + i, captionSources[i].getAbsolutePath());
// check/normalize language property
String language = normalizeLanguage(captionLanguages[i]);
if (language == null) {
logger.error("Language property was set to null.");
throw new EmbedderException("Captions language has not been set.");
}
embedderProperties.put("param.lang." + i, language);
embedderProperties.put("param.index." + i, String.valueOf(i));
embedderProperties.put("param.map." + i, String.valueOf(i + inputStreamCount));
}
// execute command
List<String> commandList = buildCommandFromTemplate(embedderProperties);
logger.debug("Executing embedding command {}", StringUtils.join(commandList, " "));
Process embedderProcess = null;
BufferedReader processReader = null;
// create and start process
try {
ProcessBuilder pb = new ProcessBuilder(commandList);
pb.redirectErrorStream(true);
embedderProcess = pb.start();
// process embedder output
processReader = new BufferedReader(new InputStreamReader(embedderProcess.getInputStream()));
String line = null;
while ((line = processReader.readLine()) != null) {
handleEmbedderOutput(line);
}
embedderProcess.waitFor();
int exitCode = embedderProcess.exitValue();
if (exitCode != 0) {
throw new EmbedderException("Embedder exited abnormally with error code: " + exitCode);
}
return getOutputFile(embedderProperties);
} catch (Exception e) {
logger.error(e.getMessage());
throw new EmbedderException(e);
} finally {
IoSupport.closeQuietly(processReader);
IoSupport.closeQuietly(embedderProcess);
}
}
/**
* Builds command list out of template command by substituting input parameters in form #{<parameter>} with
* values from properties. If for some parameter there is no matching value, parameter is removed. Parameters that are
* set via switches are represented as #{<switch> <key>}. Arrays of parameters are represented #<
* parameters(s) >
*
* @param properties
* map that contains key/values pairs for building command. Unused pairs are ignored.
* @return built list that represents command
*/
protected List<String> buildCommandFromTemplate(Map<String, String> properties) {
// add binary
List<String> commandList = new LinkedList<String>();
commandList.add(super.getBinary());
// process command line
StringBuffer buffer = new StringBuffer();
// process array parameters
Pattern pattern = Pattern.compile("#<.+?>");
Matcher matcher = pattern.matcher(CMD_TEMPLATE);
while (matcher.find()) {
String processedArray = buildArrayCommandFromTemplate(
CMD_TEMPLATE.substring(matcher.start() + 2, matcher.end() - 1), properties);
matcher.appendReplacement(buffer, processedArray);
}
matcher.appendTail(buffer);
String arrayProcessedCmd = buffer.toString();
// process normal parameters
buffer = new StringBuffer();
pattern = Pattern.compile("#\\{.+?\\}");
matcher = pattern.matcher(arrayProcessedCmd);
while (matcher.find()) {
String match = arrayProcessedCmd.substring(matcher.start() + 2, matcher.end() - 1);
if (match.contains(" ")) {
String value = properties.get(match.split(" ")[1].trim());
if (value == null) {
matcher.appendReplacement(buffer, "");
} else {
matcher.appendReplacement(buffer, match.split(" ")[0] + " " + value);
}
} else {
String value = properties.get(match.trim());
if (value == null) {
matcher.appendReplacement(buffer, "");
} else {
matcher.appendReplacement(buffer, value);
}
}
}
matcher.appendTail(buffer);
// split and convert to array list
String[] cmdArray = buffer.toString().split(" ");
for (String e : cmdArray) {
if (!"".equals(e))
commandList.add(e);
}
return commandList;
}
/**
* Given array template, it will process all parameters in form #{<param_name>} and will try to substitute them
* with values from properties as long as there are proper values. Substitution is performed in the following way: if
* parameter name is "param.example" properties are searched for keys that starts with parameter name. If such key is
* found, suffix is extracted and applied to other parameters in attempt to retrieve corresponding values for other
* parameters. Substitution is successful if all parameters are substituted. If this is not the case this array
* element is ignored.
*
* @param template
* @param properties
* @return
*/
protected String buildArrayCommandFromTemplate(String template, Map<String, String> properties) {
List<String> arrayParameters = new LinkedList<String>();
StringBuffer buffer = new StringBuffer();
// get all parameters for array
Pattern pattern = Pattern.compile("#\\{.+?\\}");
Matcher matcher = pattern.matcher(template);
while (matcher.find()) {
arrayParameters.add(template.substring(matcher.start() + 2, matcher.end() - 1));
}
if (arrayParameters.isEmpty()) {
return "";
}
for (Map.Entry<String, String> e : properties.entrySet()) {
//The index is based on the *other* keys, so we skip this if we hit it. It gets handled below.
if (e.getKey().startsWith(arrayParameters.get(0)) && e.getKey().length() > arrayParameters.get(0).length()) {
// got element that can be inserted in array - find all corresponding elements
String suffix = e.getKey().substring(arrayParameters.get(0).length());
String arrayElement = template.replace("#{" + arrayParameters.get(0) + "}", e.getValue());
for (int i = 1; i < arrayParameters.size() && properties.containsKey(arrayParameters.get(i) + suffix); i++) {
arrayElement = arrayElement.replace("#{" + arrayParameters.get(i) + "}",
properties.get(arrayParameters.get(i) + suffix));
}
if (!arrayElement.matches("^#\\{.+?\\}$")) {
buffer.append(arrayElement);
buffer.append(" ");
}
}
}
return buffer.toString();
}
@Override
protected String normalizeLanguage(String language) {
if (language == null) {
logger.warn("Language code attribute is null. Language set to: {}", "und");
return "und";
}
// truncating if necessary
if (language.length() > 2) {
logger.warn("Language code {} too long, truncating...", language);
language = language.substring(0, 2);
}
// set to lower case
language = language.toLowerCase();
// constructing locale
Locale loc = new Locale(language);
try {
return loc.getISO3Language();
} catch (MissingResourceException e) {
logger.warn("Could not determine language. Language set to: {}", "und");
return "und";
}
}
@Override
protected void handleEmbedderOutput(String output, File... sourceFiles) {
if (output.startsWith("Info:")) {
logger.info(output);
} else if (output.startsWith("Warning:")) {
logger.warn(output);
} else if (output.startsWith("Error:")) {
logger.error(output);
}
}
@Override
protected File getOutputFile(Map<String, String> properties) {
// check if output file property has been set
if (properties.containsKey("out.media.path")) {
return new File(properties.get("out.media.path"));
}
return new File(properties.get("in.media.path"));
}
}