All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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