org.opencastproject.videoeditor.ffmpeg.FFmpegEdit Maven / Gradle / Ivy
The newest version!
/*
* 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.videoeditor.ffmpeg;
import org.opencastproject.util.IoSupport;
import org.opencastproject.videoeditor.impl.VideoClip;
import org.opencastproject.videoeditor.impl.VideoEditorProperties;
import org.apache.commons.lang3.StringUtils;
import org.osgi.framework.BundleContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
/**
* FFmpeg wrappers:
* processEdits: process SMIL definitions of segments into one consecutive video
* There is a fade in and a fade out at the beginning and end of each clip
*
*/
public class FFmpegEdit {
private static final Logger logger = LoggerFactory.getLogger(FFmpegEdit.class);
private static final String FFMPEG_BINARY_DEFAULT = "ffmpeg";
private static final String CONFIG_FFMPEG_PATH = "org.opencastproject.composer.ffmpeg.path";
private static final String DEFAULT_FFMPEG_PROPERTIES = "-preset faster -crf 18";
private static final String DEFAULT_AUDIO_FADE = "0.2";
private static final String DEFAULT_VIDEO_FADE = "0.2";
private static String binary = FFMPEG_BINARY_DEFAULT;
protected float vfade;
protected float afade;
protected String ffmpegProperties = DEFAULT_FFMPEG_PROPERTIES;
protected String ffmpegScaleFilter = null;
protected String videoCodec = null; // By default, use the same codec as source
protected String audioCodec = null;
public static void init(BundleContext bundleContext) {
String path = bundleContext.getProperty(CONFIG_FFMPEG_PATH);
if (StringUtils.isNotBlank(path)) {
binary = path.trim();
}
}
public FFmpegEdit() {
this.afade = Float.parseFloat(DEFAULT_AUDIO_FADE);
this.vfade = Float.parseFloat(DEFAULT_VIDEO_FADE);
this.ffmpegProperties = DEFAULT_FFMPEG_PROPERTIES;
}
/*
* Init with properties
*/
public FFmpegEdit(Properties properties) {
String fade = properties.getProperty(VideoEditorProperties.AUDIO_FADE, DEFAULT_AUDIO_FADE);
this.afade = Float.parseFloat(fade);
fade = properties.getProperty(VideoEditorProperties.VIDEO_FADE, DEFAULT_VIDEO_FADE);
this.vfade = Float.parseFloat(fade);
this.ffmpegProperties = properties.getProperty(VideoEditorProperties.FFMPEG_PROPERTIES, DEFAULT_FFMPEG_PROPERTIES);
this.ffmpegScaleFilter = properties.getProperty(VideoEditorProperties.FFMPEG_SCALE_FILTER, null);
this.videoCodec = properties.getProperty(VideoEditorProperties.VIDEO_CODEC, null);
this.audioCodec = properties.getProperty(VideoEditorProperties.AUDIO_CODEC, null);
}
public String processEdits(List inputfiles, String dest, String outputSize, List cleanclips)
throws Exception {
return processEdits(inputfiles, dest, outputSize, cleanclips, true, true);
}
public String processEdits(List inputfiles, String dest, String outputSize, List cleanclips,
boolean hasAudio, boolean hasVideo) throws Exception {
List cmd = makeEdits(inputfiles, dest, outputSize, cleanclips, hasAudio, hasVideo);
return run(cmd);
}
/* Run the ffmpeg command with the params
* Takes a list of words as params, the output is logged
*/
private String run(List params) {
BufferedReader in = null;
Process encoderProcess = null;
try {
params.add(0, "-nostats");
params.add(0, "-nostdin");
params.add(0, "-hide_banner");
params.add(0, binary);
logger.info("executing command: " + StringUtils.join(params, " "));
ProcessBuilder pbuilder = new ProcessBuilder(params);
pbuilder.redirectErrorStream(true);
encoderProcess = pbuilder.start();
in = new BufferedReader(new InputStreamReader(
encoderProcess.getInputStream()));
String line;
int n = 5;
while ((line = in.readLine()) != null) {
if (n-- > 0) {
logger.info(line);
}
}
// wait until the task is finished
encoderProcess.waitFor();
int exitCode = encoderProcess.exitValue();
if (exitCode != 0) {
throw new Exception("Ffmpeg exited abnormally with status " + exitCode);
}
} catch (Exception ex) {
logger.error("VideoEditor ffmpeg failed", ex);
return ex.toString();
} finally {
IoSupport.closeQuietly(in);
IoSupport.closeQuietly(encoderProcess);
}
return null;
}
/*
* Construct the ffmpeg command from src, in-out points and output resolution
* Inputfile is an ordered list of video src
* clips is a list of edit points indexing into the video src list
* outputResolution when specified is the size to which all the clips will scale
* hasAudio and hasVideo specify media type of the input files
* NOTE: This command will fail if the sizes are mismatched or
* if some of the clips aren't same as specified mediatype
* (hasn't audio or video stream while hasAudio, hasVideo parameter set)
*/
public List makeEdits(List inputfiles, String dest, String outputResolution,
List clips, boolean hasAudio, boolean hasVideo) throws Exception {
if (!hasAudio && !hasVideo) {
throw new IllegalArgumentException("Inputfiles should have at least audio or video stream.");
}
DecimalFormat f = new DecimalFormat("0.00", new DecimalFormatSymbols(Locale.US));
int n = clips.size();
int i;
String outmap = "";
String scale = "";
List command = new ArrayList();
List vpads = new ArrayList();
List apads = new ArrayList();
List clauses = new ArrayList(); // The clauses are ordered
if (n > 1) { // Create the input pads if we have multiple segments
for (i = 0; i < n ; i++) {
if (hasVideo) {
vpads.add("[v" + i + "]"); // post filter
}
if (hasAudio) {
apads.add("[a" + i + "]");
}
}
}
if (hasVideo) {
if (outputResolution != null && outputResolution.length() > 3) { // format is "x"
// scale each clip to the same size
scale = ",scale=" + outputResolution;
}
else if (ffmpegScaleFilter != null) {
// Use scale filter if configured
scale = ",scale=" + ffmpegScaleFilter;
}
}
for (i = 0; i < n; i++) { // Examine each clip
// get clip and add fades to each clip
VideoClip vclip = clips.get(i);
int fileindx = vclip.getSrc(); // get source file by index
double inpt = vclip.getStartInSeconds(); // get in points
double duration = vclip.getDurationInSeconds();
String clip = "";
if (hasVideo) {
String vfadeFilter = "";
/* Only include fade into the filter graph if necessary */
if (vfade > 0.00001) {
double vend = duration - vfade;
vfadeFilter = ",fade=t=in:st=0:d=" + vfade + ",fade=t=out:st=" + f.format(vend) + ":d=" + vfade;
}
/* Add filters for video */
clip = "[" + fileindx + ":v]trim=" + f.format(inpt) + ":duration=" + f.format(duration)
+ scale + ",setpts=PTS-STARTPTS" + vfadeFilter + "[v" + i + "]";
clauses.add(clip);
}
if (hasAudio) {
String afadeFilter = "";
/* Only include fade into the filter graph if necessary */
if (afade > 0.00001) {
double aend = duration - afade;
afadeFilter = ",afade=t=in:st=0:d=" + afade + ",afade=t=out:st=" + f.format(aend) + ":d=" + afade;
}
/* Add filters for audio */
clip = "[" + fileindx + ":a]atrim=" + f.format(inpt) + ":duration=" + f.format(duration)
+ ",asetpts=PTS-STARTPTS" + afadeFilter + "[a"
+ i + "]";
clauses.add(clip);
}
}
if (n > 1) { // concat the outpads when there are more then 1 per stream
// use unsafe because different files may have different SAR/framerate
if (hasVideo) {
clauses.add(StringUtils.join(vpads, "") + "concat=n=" + n + ":unsafe=1[ov0]"); // concat video clips
}
if (hasAudio) {
clauses.add(StringUtils.join(apads, "") + "concat=n=" + n
+ ":v=0:a=1[oa0]"); // concat audio clips in stream 0, video in stream 1
}
outmap = "o"; // if more than one clip
}
for (String o : inputfiles) {
command.add("-i"); // Add inputfile in the order of entry
command.add(o);
}
command.add("-filter_complex");
command.add(StringUtils.join(clauses, ";"));
String[] options = ffmpegProperties.split(" ");
command.addAll(Arrays.asList(options));
if (hasAudio) {
command.add("-map");
command.add("[" + outmap + "a0]");
}
if (hasVideo) {
command.add("-map");
command.add("[" + outmap + "v0]");
}
if (hasVideo && videoCodec != null) { // If using different codecs from source, add them here
command.add("-c:v");
command.add(videoCodec);
}
if (hasAudio && audioCodec != null) {
command.add("-c:a");
command.add(audioCodec);
}
command.add(dest);
return command;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy