Package net.pms.encoders

Source Code of net.pms.encoders.FFmpegVideo

/*
* PS3 Media Server, for streaming any medias to your PS3.
* Copyright (C) 2008  A.Brochard
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
package net.pms.encoders;

import com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;

import net.pms.Messages;
import net.pms.PMS;
import net.pms.configuration.PmsConfiguration;
import net.pms.configuration.RendererConfiguration;
import net.pms.dlna.DLNAMediaInfo;
import net.pms.dlna.DLNAMediaSubtitle;
import net.pms.dlna.DLNAResource;
import net.pms.dlna.InputFile;
import net.pms.formats.Format;
import net.pms.formats.v2.SubtitleUtils;
import net.pms.io.*;
import net.pms.network.HTTPResource;
import net.pms.util.PlayerUtil;
import net.pms.util.ProcessUtil;

import org.apache.commons.lang3.StringUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.swing.*;

import static org.apache.commons.io.FilenameUtils.getBaseName;
import static org.apache.commons.io.FilenameUtils.getExtension;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;

/*
* Pure FFmpeg video player.
*
* Design note:
*
* Helper methods that return lists of <code>String</code>s representing
* options are public to facilitate composition e.g. a custom engine (plugin)
* that uses tsMuxeR for videos without subtitles and FFmpeg otherwise needs to
* compose and call methods on both players.
*
* To avoid API churn, and to provide wiggle room for future functionality, all
* of these methods take the same arguments as launchTranscode (and the same
* first four arguments as finalizeTranscoderArgs) even if one or more of the
* parameters are unused e.g.:
*
*     public List<String> getAudioBitrateOptions(
*         DLNAResource dlna,
*         DLNAMediaInfo media,
*         OutputParams params
*     )
*/
public class FFmpegVideo extends FFmpegBase {
  private static final Logger logger = LoggerFactory.getLogger(FFmpegVideo.class);
  private static final String DEFAULT_QSCALE = "3";
  private static final String SUB_DIR = "subs";
  private static PmsConfiguration configuration;

  private boolean dtsRemux;
  private boolean ac3Remux;
  private boolean videoRemux;

  private JCheckBox multiThreadingCheckBox;
  private JCheckBox videoRemuxCheckBox;

  @Deprecated
  public FFmpegVideo() {
    this(PMS.getConfiguration());
  }
 
  public FFmpegVideo(PmsConfiguration configuration) {
    super(configuration);
    this.configuration = configuration;
  }

  // FIXME we have an id() accessor for this; no need for the field to be public
  @Deprecated
  public static final String ID = "ffmpegvideo";

  /**
   * Returns a list of strings representing the rescale options for this transcode i.e. the ffmpeg -vf
   * options used to show subtitles in SSA/ASS format and resize a video that's too wide and/or high for the specified renderer.
   * If the renderer has no size limits, or there's no media metadata, or the video is within the renderer's
   * size limits, an empty list is returned.
   *
   * @param dlna The DLNA resource representing the file being transcoded.
   * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
   * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request.
   * @return a {@link List} of <code>String</code>s representing the rescale options for this video,
   * or an empty list if the video doesn't need to be resized.
   */
  public List<String> getVideoFilterOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException {
    List<String> options = new ArrayList<String>();
    String subsOption = null;
    String padding = null;
    final RendererConfiguration renderer = params.mediaRenderer;

    DLNAMediaSubtitle tempSubs = null;
    if (!isDisableSubtitles(params)) {
      tempSubs = getSubtitles(params);
    }

    final boolean isResolutionTooHighForRenderer = renderer.isVideoRescale() // renderer defines a max width/height
        && (media != null && media.isMediaparsed())
        && ((media.getWidth() > renderer.getMaxVideoWidth())
          || (media.getHeight() > renderer.getMaxVideoHeight()));

    if (tempSubs != null) {
      StringBuilder s = new StringBuilder();
      CharacterIterator it = new StringCharacterIterator(ProcessUtil.getShortFileNameIfWideChars(tempSubs.getExternalFile().getAbsolutePath()));

      for (char ch = it.first(); ch != CharacterIterator.DONE; ch = it.next()) {
        switch (ch) {
          case ':':
            s.append("\\\\:");
            break;
          case '\\':
            s.append("/");
            break;
          case ']':
            s.append("\\]");
            break;
          case '[':
            s.append("\\[");
            break;
          default:
            s.append(ch);
        }
      }

      String subsFile = s.toString();
      subsFile = subsFile.replace(",", "\\,");
      subsOption = "subtitles=" + subsFile;
    }

    if (renderer.isPadVideoWithBlackBordersTo169AR() && renderer.isRescaleByRenderer()) {
      if (media != null
        && media.isMediaparsed()
        && media.getHeight() != 0
        && (media.getWidth() / (double) media.getHeight()) >= (16 / (double) 9)) {
        padding = "pad=iw:iw/(16/9):0:(oh-ih)/2";
      } else {
        padding = "pad=ih*(16/9):ih:(ow-iw)/2:0";
      }
    }

    String rescaleSpec = null;

    if (isResolutionTooHighForRenderer || (renderer.isPadVideoWithBlackBordersTo169AR() && !renderer.isRescaleByRenderer())) {
      rescaleSpec = String.format(
        // http://stackoverflow.com/a/8351875
        "scale=iw*min(%1$d/iw\\,%2$d/ih):ih*min(%1$d/iw\\,%2$d/ih),pad=%1$d:%2$d:(%1$d-iw)/2:(%2$d-ih)/2",
        renderer.getMaxVideoWidth(),
        renderer.getMaxVideoHeight()
      );
    }

    String overrideVF = renderer.getFFmpegVideoFilterOverride();

    if (rescaleSpec != null || padding != null || overrideVF != null || subsOption != null) {
      options.add("-vf");
      StringBuilder filterParams = new StringBuilder();

      if (overrideVF != null) {
        filterParams.append(overrideVF);
        if (subsOption != null) {
          filterParams.append(", ");
        }
      } else {
        if (rescaleSpec != null) {
          filterParams.append(rescaleSpec);
          if (subsOption != null || padding != null) {
            filterParams.append(", ");
          }
        }

        if (padding != null && rescaleSpec == null) {
          filterParams.append(padding);
          if (subsOption != null) {
            filterParams.append(", ");
          }
        }
      }

      if (subsOption != null) {
        filterParams.append(subsOption);
      }

      options.add(filterParams.toString());
    }

    return options;
  }

  /**
   * Returns a list of <code>String</code>s representing ffmpeg output
   * options (i.e. options that define the output file's video codec,
   * audio codec and container) compatible with the renderer's
   * <code>TranscodeVideo</code> profile.
   *
   * @param dlna The DLNA resource representing the file being transcoded.
   * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
   * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request.
   * @return a {@link List} of <code>String</code>s representing the FFmpeg output parameters for the renderer according
   * to its <code>TranscodeVideo</code> profile.
   */
  public synchronized List<String> getVideoTranscodeOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) {
    List<String> options = new ArrayList<String>();
    final String filename = dlna.getSystemName();
    final RendererConfiguration renderer = params.mediaRenderer;

    if (renderer.isTranscodeToWMV() && !renderer.isXBOX()) { // WMV
      options.add("-c:v");
      options.add("wmv2");

      options.add("-c:a");
      options.add("wmav2");

      options.add("-f");
      options.add("asf");
    } else { // MPEGPSAC3, MPEGTSAC3 or H264TSAC3
      if (isAc3Remux()) {
        // AC-3 remux
        options.add("-c:a");
        options.add("copy");
      } else if (isDtsRemux()) {
        // Audio is added in a separate process later
        options.add("-an");
      } else if (type() == Format.AUDIO) {
        // Skip
      } else {
        options.add("-c:a");
        options.add("ac3");
      }

      InputFile newInput = null;
      if (filename != null) {
        newInput = new InputFile();
        newInput.setFilename(filename);
        newInput.setPush(params.stdin);
      }

      // Output video codec
      if (media.isMediaparsed()
          && params.sid == null
          && ((newInput != null && media.isVideoWithinH264LevelLimits(newInput, params.mediaRenderer))
            || !params.mediaRenderer.isH264Level41Limited())
          && media.isMuxable(params.mediaRenderer)
          && configuration.isFFmpegMuxWhenCompatible()
          && params.mediaRenderer.isMuxH264MpegTS()) {

        options.add("-c:v");
        options.add("copy");
        options.add("-bsf");
        options.add("h264_mp4toannexb");
        options.add("-fflags");
        options.add("+genpts");
        // Set correct container aspect ratio if remuxed video track has different AR
        // TODO does not work with ffmpeg 1.2
        // https://ffmpeg.org/trac/ffmpeg/ticket/2046
        // possible solution http://forum.doom9.org/showthread.php?t=152419
        //
        // if (media.isAspectRatioMismatch()) {
        //  options.add("-aspect");
        //  options.add(media.getAspectRatioContainer());
        // }

        setVideoRemux(true);
      } else if (renderer.isTranscodeToH264TSAC3()) {
        options.add("-c:v");
        options.add("libx264");
        options.add("-crf");
        options.add("20");
        options.add("-preset");
        options.add("superfast");
      } else if (!isDtsRemux()) {
        options.add("-c:v");
        options.add("mpeg2video");
      }

      // Output file format
      options.add("-f");
      if (isDtsRemux()) {
        if (isVideoRemux()) {
          options.add("rawvideo");
        } else {
          options.add("mpeg2video");
        }
      } else if (renderer.isTranscodeToMPEGTSAC3() || renderer.isTranscodeToH264TSAC3() || isVideoRemux()) { // MPEGTSAC3
        options.add("mpegts");
      } else { // default: MPEGPSAC3
        options.add("vob");
      }
    }

    return options;
  }

  /**
   * Returns the video bitrate spec for the current transcode according
   * to the limits/requirements of the renderer.
   *
   * @param dlna The DLNA resource representing the file being transcoded.
   * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
   * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request.
   * @return a {@link List} of <code>String</code>s representing the video bitrate options for this transcode
   */
  public List<String> getVideoBitrateOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) { // media is currently unused
    List<String> options = new ArrayList<String>();
    String sMaxVideoBitrate = params.mediaRenderer.getMaxVideoBitrate(); // currently Mbit/s
    int iMaxVideoBitrate = 0;

    if (sMaxVideoBitrate != null) {
      try {
        iMaxVideoBitrate = Integer.parseInt(sMaxVideoBitrate);
      } catch (NumberFormatException nfe) {
        logger.error("Can't parse max video bitrate", nfe); // XXX this should be handled in RendererConfiguration
      }
    }

    if (iMaxVideoBitrate == 0) { // unlimited: try to preserve the bitrate
      options.add("-q:v"); // video qscale
      options.add(DEFAULT_QSCALE);
    } else { // limit the bitrate FIXME untested
      // convert megabits-per-second (as per the current option name: MaxVideoBitrateMbps) to bps
      // FIXME rather than dealing with megabit vs mebibit issues here, this should be left up to the client i.e.
      // the renderer.conf unit should be bits-per-second (and the option should be renamed: MaxVideoBitrateMbps -> MaxVideoBitrate)
      options.add("-maxrate");
      options.add("" + iMaxVideoBitrate * 1000 * 1000);
    }

    return options;
  }

  /**
   * Returns the audio bitrate spec for the current transcode according
   * to the limits/requirements of the renderer.
   *
   * @param dlna The DLNA resource representing the file being transcoded.
   * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
   * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request.
   * @return a {@link List} of <code>String</code>s representing the audio bitrate options for this transcode
   */
  public List<String> getAudioBitrateOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) {
    List<String> options = new ArrayList<String>();

    options.add("-q:a");
    options.add(DEFAULT_QSCALE);

    return options;
  }

  /**
   * Returns the audio channel (-ac) options.
   *
   * @param dlna The DLNA resource representing the file being transcoded.
   * @param media the media metadata for the file being transcoded. May contain null fields (e.g. for web videos).
   * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request.
   * @return The list of audio channel options.
   * @since 1.81.0
   */

  public List<String> getAudioChannelOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) {
    List<String> options = new ArrayList<String>();
    int ac = -1; // -1: don't change the number of audio channels
    int nChannels = params.aid == null ? -1 : params.aid.getAudioProperties().getNumberOfChannels();

    if (nChannels == -1) { // unknown (e.g. web video)
      ac = 2; // works fine if the video has < 2 channels
    } else if (nChannels > 2) {
      int maxOutputChannels = configuration.getAudioChannelCount();

      if (maxOutputChannels <= 2) {
        ac = maxOutputChannels;
      } else if (params.mediaRenderer.isTranscodeToWMV()) {
        // http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=16590
        // XXX WMA Pro (wmapro) supports > 2 channels, but ffmpeg doesn't have an encoder for it
        ac = 2;
      }
    }

    if (ac != -1) {
      options.add("-ac");
      options.add("" + ac);
    }

    return options;
  }

  @Override
  public PlayerPurpose getPurpose() {
    return PlayerPurpose.VIDEO_FILE_PLAYER;
  }

  @Override
  // TODO make this static so it can replace ID, instead of having both
  public String id() {
    return ID;
  }

  @Override
  public boolean isTimeSeekable() {
    return true;
  }

  public String initialString() {
    String threads = "";
    if (configuration.isFfmpegMultithreading()) {
      threads = " -threads " + configuration.getNumberOfCpuCores();
    }
    return threads;
  }

  @Override
  public String name() {
    return "FFmpeg";
  }

  @Override
  public int type() {
    return Format.VIDEO;
  }

  // unused; return this array for backwards-compatibility
  @Deprecated
  protected String[] getDefaultArgs() {
    List<String> defaultArgsList = new ArrayList<String>();

    defaultArgsList.add("-loglevel");
    defaultArgsList.add("warning");

    String[] defaultArgsArray = new String[defaultArgsList.size()];
    defaultArgsList.toArray(defaultArgsArray);

    return defaultArgsArray;
  }

  private int[] getVideoBitrateConfig(String bitrate) {
    int bitrates[] = new int[2];

    if (bitrate.contains("(") && bitrate.contains(")")) {
      bitrates[1] = Integer.parseInt(bitrate.substring(bitrate.indexOf("(") + 1, bitrate.indexOf(")")));
  }

    if (bitrate.contains("(")) {
      bitrate = bitrate.substring(0, bitrate.indexOf("(")).trim();
    }

    if (isBlank(bitrate)) {
      bitrate = "0";
    }

    bitrates[0] = (int) Double.parseDouble(bitrate);

    return bitrates;
  }

  @Override
  @Deprecated
  public String[] args() {
    return getDefaultArgs(); // unused; return this array for for backwards compatibility
  }

  @Override
  public String mimeType() {
    return HTTPResource.VIDEO_TRANSCODE;
  }

  // FIXME this is a mess: the whole point of the getXOptions methods is to prevent
  // this turning into another MEncoderVideo, with disorganised kitchen-sink methods
  // that are over a thousand lines long.
  //
  // TODO: move each chunk of functionality into submethods called by a core group of
  // getXOptions methods
  @Override
  public synchronized ProcessWrapper launchTranscode(
    DLNAResource dlna,
    DLNAMediaInfo media,
    OutputParams params
  ) throws IOException {
    int nThreads = configuration.getNumberOfCpuCores();
    List<String> cmdList = new ArrayList<String>();
    RendererConfiguration renderer = params.mediaRenderer;
    final String filename = dlna.getSystemName();
    setAudioAndSubs(filename, media, params, configuration);
    params.waitbeforestart = 2500;

    cmdList.add(executable());
    cmdList.addAll(getGlobalOptions(logger));

    if (params.timeseek > 0) {
      cmdList.add("-ss");
      cmdList.add("" + params.timeseek);
    }

    // decoder threads
    cmdList.add("-threads");
    cmdList.add("" + nThreads);

    final boolean isTsMuxeRVideoEngineEnabled = configuration.getEnginesAsList().contains(TsMuxeRVideo.ID);

    setAc3Remux(false);
    setDtsRemux(false);
    setVideoRemux(false);

    if (configuration.isAudioRemuxAC3() && params.aid != null && params.aid.isAC3() && renderer.isTranscodeToAC3()) {
      // AC-3 remux takes priority
      setAc3Remux(true);
    } else if (isTsMuxeRVideoEngineEnabled && configuration.isAudioEmbedDtsInPcm() && params.aid != null && params.aid.isDTS() && params.mediaRenderer.isDTSPlayable()) {
      // Now check for DTS remux
      setDtsRemux(true);
    }

    String frameRateRatio = media.getValidFps(true);
    String frameRateNumber = media.getValidFps(false);

    // Input filename
    cmdList.add("-i");
    cmdList.add(filename);

    if (media.getAudioTracksList().size() > 1) {
      // Set the video stream
      cmdList.add("-map");
      cmdList.add("0:v");

      // Set the proper audio stream
      cmdList.add("-map");
      cmdList.add("0:a:" + (media.getAudioTracksList().indexOf(params.aid)));
    }

    // Encoder threads
    cmdList.add("-threads");
    cmdList.add("" + nThreads);

    if (params.timeend > 0) {
      cmdList.add("-t");
      cmdList.add("" + params.timeend);
    }

    // add video bitrate options (-b:a)
    // cmdList.addAll(getVideoBitrateOptions(filename, dlna, media, params));

    // add audio bitrate options (-b:v)
    // cmdList.addAll(getAudioBitrateOptions(filename, dlna, media, params));

    // if the source is too large for the renderer, resize it
    // and/or add subtitles to video filter
    // FFmpeg must be compiled with --enable-libass parameter
    cmdList.addAll(getVideoFilterOptions(dlna, media, params));

    int defaultMaxBitrates[] = getVideoBitrateConfig(configuration.getMaximumBitrate());
    int rendererMaxBitrates[] = new int[2];

    if (renderer.getMaxVideoBitrate() != null) {
      rendererMaxBitrates = getVideoBitrateConfig(renderer.getMaxVideoBitrate());
    }
   
    // Give priority to the renderer's maximum bitrate setting over the user's setting
    if (rendererMaxBitrates[0] > 0 && rendererMaxBitrates[0] < defaultMaxBitrates[0]) {
      defaultMaxBitrates = rendererMaxBitrates;
    }

    if (params.mediaRenderer.getCBRVideoBitrate() == 0) {
      // Convert value from Mb to Kb
      defaultMaxBitrates[0] = 1000 * defaultMaxBitrates[0];

      // Halve it since it seems to send up to 1 second of video in advance
      defaultMaxBitrates[0] = defaultMaxBitrates[0] / 2;

      int bufSize = 1835;
      // x264 uses different buffering math than MPEG-2
      if (!renderer.isTranscodeToH264TSAC3()) {
        if (media.isHDVideo()) {
          bufSize = defaultMaxBitrates[0] / 3;
        }

        if (bufSize > 7000) {
          bufSize = 7000;
        }

        if (defaultMaxBitrates[1] > 0) {
          bufSize = defaultMaxBitrates[1];
        }

        if (params.mediaRenderer.isDefaultVBVSize() && rendererMaxBitrates[1] == 0) {
          bufSize = 1835;
        }
      }

      // Make room for audio
      if (isDtsRemux()) {
        defaultMaxBitrates[0] = defaultMaxBitrates[0] - 1510;
      } else {
        defaultMaxBitrates[0] = defaultMaxBitrates[0] - configuration.getAudioBitrate();
      }

      // Round down to the nearest Mb
      defaultMaxBitrates[0] = defaultMaxBitrates[0] / 1000 * 1000;

      // FFmpeg uses bytes for inputs instead of kbytes like MEncoder
      bufSize = bufSize * 1000;
      defaultMaxBitrates[0] = defaultMaxBitrates[0] * 1000;

      /**
       * Level 4.1-limited renderers like the PS3 can stutter when H.264 video exceeds
       * this bitrate
       */
      if (renderer.isTranscodeToH264TSAC3() || isVideoRemux()) {
        if (
          params.mediaRenderer.isH264Level41Limited() &&
          defaultMaxBitrates[0] > 31250000
        ) {
          defaultMaxBitrates[0] = 31250000;
        }
        bufSize = defaultMaxBitrates[0];
      }

      cmdList.add("-bufsize");
      cmdList.add("" + bufSize);

      cmdList.add("-maxrate");
      cmdList.add("" + defaultMaxBitrates[0]);
    }

    // Set audio bitrate and channel count only when doing audio transcoding
    if (!isAc3Remux() && !isDtsRemux() && !(type() == Format.AUDIO)) {
      int channels;
      if (renderer.isTranscodeToWMV() && !renderer.isXBOX()) {
        channels = 2;
      } else {
        channels = configuration.getAudioChannelCount(); // 5.1 max for AC-3 encoding
      }
      cmdList.add("-ac");
      cmdList.add("" + channels);

      cmdList.add("-ab");
      cmdList.add(configuration.getAudioBitrate() + "k");
    }

    if (params.timeseek > 0) {
      cmdList.add("-copypriorss");
      cmdList.add("0");
      cmdList.add("-avoid_negative_ts");
      cmdList.add("1");
    }

    // Add MPEG-2 quality settings
    if (!renderer.isTranscodeToH264TSAC3() && !isVideoRemux()) {
      String mpeg2Options = configuration.getMPEG2MainSettingsFFmpeg();
      String mpeg2OptionsRenderer = params.mediaRenderer.getCustomFFmpegMPEG2Options();

      // Renderer settings take priority over user settings
      if (isNotBlank(mpeg2OptionsRenderer)) {
        mpeg2Options = mpeg2OptionsRenderer;
      } else {
        if (mpeg2Options.contains("Automatic")) {
          mpeg2Options = "-g 5 -q:v 1 -qmin 2 -qmax 3";

          // It has been reported that non-PS3 renderers prefer keyint 5 but prefer it for PS3 because it lowers the average bitrate
          if (params.mediaRenderer.isPS3()) {
            mpeg2Options = "-g 25 -q:v 1 -qmin 2 -qmax 3";
          }

          if (mpeg2Options.contains("Wireless") || defaultMaxBitrates[0] < 70) {
            // Lower quality for 720p+ content
            if (media.getWidth() > 1280) {
              mpeg2Options = "-g 25 -qmax 7 -qmin 2";
            } else if (media.getWidth() > 720) {
              mpeg2Options = "-g 25 -qmax 5 -qmin 2";
            }
          }
        }
      }

      String[] customOptions = StringUtils.split(mpeg2Options);
      cmdList.addAll(new ArrayList<String>(Arrays.asList(customOptions)));
    }

    // Add the output options (-f, -c:a, -c:v, etc.)
    cmdList.addAll(getVideoTranscodeOptions(dlna, media, params));

    // Add custom options
    if (StringUtils.isNotEmpty(renderer.getCustomFFmpegOptions())) {
      parseOptions(renderer.getCustomFFmpegOptions(), cmdList);
    }

    if (!isDtsRemux()) {
      cmdList.add("pipe:");
    }

    String[] cmdArray = new String[cmdList.size()];
    cmdList.toArray(cmdArray);

    cmdArray = finalizeTranscoderArgs(
      filename,
      dlna,
      media,
      params,
      cmdArray
    );

    ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params);

    if (isDtsRemux()) {
      PipeProcess pipe;
      pipe = new PipeProcess(System.currentTimeMillis() + "tsmuxerout.ts");

      TsMuxeRVideo ts = new TsMuxeRVideo(configuration);
      File f = new File(configuration.getTempFolder(), "pms-tsmuxer.meta");
      String cmd[] = new String[]{ ts.executable(), f.getAbsolutePath(), pipe.getInputPipe() };
      pw = new ProcessWrapperImpl(cmd, params);

      PipeIPCProcess ffVideoPipe = new PipeIPCProcess(System.currentTimeMillis() + "ffmpegvideo", System.currentTimeMillis() + "videoout", false, true);

      cmdList.add(ffVideoPipe.getInputPipe());

      OutputParams ffparams = new OutputParams(configuration);
      ffparams.maxBufferSize = 1;
      ffparams.stdin = params.stdin;

      String[] cmdArrayDts = new String[cmdList.size()];
      cmdList.toArray(cmdArrayDts);

      cmdArrayDts = finalizeTranscoderArgs(
        filename,
        dlna,
        media,
        params,
        cmdArrayDts
      );

      ProcessWrapperImpl ffVideo = new ProcessWrapperImpl(cmdArrayDts, ffparams);

      ProcessWrapper ff_video_pipe_process = ffVideoPipe.getPipeProcess();
      pw.attachProcess(ff_video_pipe_process);
      ff_video_pipe_process.runInNewThread();
      ffVideoPipe.deleteLater();

      pw.attachProcess(ffVideo);
      ffVideo.runInNewThread();

      PipeIPCProcess ffAudioPipe = new PipeIPCProcess(System.currentTimeMillis() + "ffmpegaudio01", System.currentTimeMillis() + "audioout", false, true);
      StreamModifier sm = new StreamModifier();
      sm.setPcm(false);
      sm.setDtsEmbed(isDtsRemux());
      sm.setSampleFrequency(48000);
      sm.setBitsPerSample(16);
      sm.setNbChannels(2);

      List<String> cmdListDTS = new ArrayList<String>();
      cmdListDTS.add(executable());
      cmdListDTS.add("-y");
      cmdListDTS.add("-ss");

      if (params.timeseek > 0) {
        cmdListDTS.add("" + params.timeseek);
      } else {
        cmdListDTS.add("0");
      }

      if (params.stdin == null) {
        cmdListDTS.add("-i");
      } else {
        cmdListDTS.add("-");
      }
      cmdListDTS.add(filename);

      if (params.timeseek > 0) {
        cmdListDTS.add("-copypriorss");
        cmdListDTS.add("0");
        cmdListDTS.add("-avoid_negative_ts");
        cmdListDTS.add("1");
      }

      cmdListDTS.add("-ac");
      cmdListDTS.add("2");

      cmdListDTS.add("-f");
      cmdListDTS.add("dts");

      cmdListDTS.add("-c:a");
      cmdListDTS.add("copy");

      cmdListDTS.add(ffAudioPipe.getInputPipe());

      String[] cmdArrayDTS = new String[cmdListDTS.size()];
      cmdListDTS.toArray(cmdArrayDTS);

      if (!params.mediaRenderer.isMuxDTSToMpeg()) { // No need to use the PCM trick when media renderer supports DTS
        ffAudioPipe.setModifier(sm);
      }

      OutputParams ffaudioparams = new OutputParams(configuration);
      ffaudioparams.maxBufferSize = 1;
      ffaudioparams.stdin = params.stdin;
      ProcessWrapperImpl ffAudio = new ProcessWrapperImpl(cmdArrayDTS, ffaudioparams);

      params.stdin = null;

      PrintWriter pwMux = new PrintWriter(f);
      pwMux.println("MUXOPT --no-pcr-on-video-pid --no-asyncio --new-audio-pes --vbr --vbv-len=500");
      String videoType = "V_MPEG-2";

      if (isVideoRemux()) {
        videoType = "V_MPEG4/ISO/AVC";
      }

      if (params.no_videoencode && params.forceType != null) {
        videoType = params.forceType;
      }

      String fps = "";
      if (params.forceFps != null) {
        fps = "fps=" + params.forceFps + ", ";
      }

      String audioType = "A_AC3";
      if (isDtsRemux()) {
        if (params.mediaRenderer.isMuxDTSToMpeg()) {
          // Renderer can play proper DTS track
          audioType = "A_DTS";
        } else {
          // DTS padded in LPCM trick
          audioType = "A_LPCM";
        }
      }

      pwMux.println(videoType + ", \"" + ffVideoPipe.getOutputPipe() + "\", " + fps + "level=4.1, insertSEI, contSPS, track=1");
      pwMux.println(audioType + ", \"" + ffAudioPipe.getOutputPipe() + "\", track=2");
      pwMux.close();

      ProcessWrapper pipe_process = pipe.getPipeProcess();
      pw.attachProcess(pipe_process);
      pipe_process.runInNewThread();

      try {
        Thread.sleep(50);
      } catch (InterruptedException e) { }

      pipe.deleteLater();
      params.input_pipes[0] = pipe;

      ProcessWrapper ff_pipe_process = ffAudioPipe.getPipeProcess();
      pw.attachProcess(ff_pipe_process);
      ff_pipe_process.runInNewThread();

      try {
        Thread.sleep(50);
      } catch (InterruptedException e) { }

      ffAudioPipe.deleteLater();
      pw.attachProcess(ffAudio);
      ffAudio.runInNewThread();
    }

    pw.runInNewThread();
    return pw;
  }

  @Override
  public JComponent config() {
    return config("NetworkTab.5");
  }

  protected JComponent config(String languageLabel) {
    FormLayout layout = new FormLayout(
      "left:pref, 0:grow",
      "p, 3dlu, p, 3dlu, p, 3dlu, p"
    );
    PanelBuilder builder = new PanelBuilder(layout);

    CellConstraints cc = new CellConstraints();

    JComponent cmp = builder.addSeparator(Messages.getString(languageLabel), cc.xyw(2, 1, 1));
    cmp = (JComponent) cmp.getComponent(0);
    cmp.setFont(cmp.getFont().deriveFont(Font.BOLD));

    multiThreadingCheckBox = new JCheckBox(Messages.getString("MEncoderVideo.35"));
    multiThreadingCheckBox.setContentAreaFilled(false);
    if (configuration.isFfmpegMultithreading()) {
      multiThreadingCheckBox.setSelected(true);
    }
    multiThreadingCheckBox.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setFfmpegMultithreading(e.getStateChange() == ItemEvent.SELECTED);
      }
    });
    builder.add(multiThreadingCheckBox, cc.xy(2, 3));

    videoRemuxCheckBox = new JCheckBox(Messages.getString("FFmpeg.0"));
    videoRemuxCheckBox.setContentAreaFilled(false);
    if (configuration.isFFmpegMuxWhenCompatible()) {
      videoRemuxCheckBox.setSelected(true);
    }
    videoRemuxCheckBox.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setFFmpegMuxWhenCompatible(e.getStateChange() == ItemEvent.SELECTED);
      }
    });
    builder.add(videoRemuxCheckBox, cc.xy(2, 5));

    return builder.getPanel();
  }

  @Override
  public boolean isCompatible(DLNAResource dlna) {
    if (
      PlayerUtil.isVideo(dlna, Format.Identifier.MKV) ||
      PlayerUtil.isVideo(dlna, Format.Identifier.MPG)
    ) {
      return true;
    } else {
      return false;
    }
  }

  protected static List<String> parseOptions(String str) {
    return str == null ? null : parseOptions(str, new ArrayList<String>());
  }

  protected static List<String> parseOptions(String str, List<String> cmdList) {
    while (str.length() > 0) {
      if (str.charAt(0) == '\"') {
        int pos = str.indexOf("\"", 1);
        if (pos == -1) {
          // No ", error
          break;
        }
        String tmp = str.substring(1, pos);
        cmdList.add(tmp.trim());
        str = str.substring(pos + 1);
        continue;
      } else {
        // New arg, find space
        int pos = str.indexOf(" ");
        if (pos == -1) {
          // No space, we're done
          cmdList.add(str);
          break;
        }
        String tmp = str.substring(0, pos);
        cmdList.add(tmp.trim());
        str = str.substring(pos + 1);
        continue;
      }
    }
    return cmdList;
  }

  /**
   * Shift timing of external subtitles in SSA/ASS or SRT format and converts charset to UTF8 if necessary
   *
   * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request.
   * @return Converted subtitle file
   * @throws IOException
   */
  public DLNAMediaSubtitle getSubtitles(OutputParams params) throws IOException {
    DLNAMediaSubtitle tempSubs = null;

    if (params.sid.getId() == -1) {
      return null;
    }

    final File subtitleDirectory = new File(configuration.getTempFolder(), SUB_DIR + File.separator);
    if (!subtitleDirectory.exists()) {
      subtitleDirectory.mkdirs();
    }

    if (params.sid.isExternal() && SubtitleUtils.isSupportsTimeShifting(params.sid.getType())) {
      try {
        tempSubs = SubtitleUtils.shiftSubtitlesTimingWithUtfConversion(params.sid, params.timeseek);
      } catch (IOException e) {
        logger.debug("Applying timeshift caused an error: " + e);
        tempSubs = null;
      }
    }

    return tempSubs;
  }

  /**
   * Converts external subtitles file in SRT format or extract embedded subs to default SSA/ASS format.
   *
   * @param filename Subtitle file in SRT format or video file with embedded subs
   * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
   * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request.
   * @return Converted subtitle file in SSA/ASS format
   */
  // FIXME this is unused
  private File extractEmbeddedSubtitleTrack(String filename, DLNAMediaInfo media, OutputParams params) throws IOException {
    final List<String> cmdList = new ArrayList<String>();
    File tempSubsFile;
    cmdList.add(configuration.getFfmpegPath());
    cmdList.addAll(getGlobalOptions(logger));

    /* TODO Use it when external subs should be converted by ffmpeg
    if (
      isNotBlank(configuration.getSubtitlesCodepage()) &&
      params.sid.isExternal() &&
      !params.sid.isExternalFileUtf8() &&
      !params.sid.getExternalFileCharacterSet().equals(configuration.getSubtitlesCodepage()) // ExternalFileCharacterSet can be null
    ) {
      cmdList.add("-sub_charenc");
      cmdList.add(configuration.getSubtitlesCodepage());
    }
    */
    cmdList.add("-i");
    cmdList.add(filename);

    if (params.sid.isEmbedded()) {
      cmdList.add("-map");
      /* TODO broken code. Consider following example file:
        Stream #0:0(eng): Video: h264 (High), yuv420p, 720x576, SAR 178:139 DAR 445:278, 25 fps, 25 tbr, 1k tbn, 50 tbc (default)
        Metadata:
          title           : H264
        Stream #0:1(rus): Subtitle: subrip
        Metadata:
          title           : rus
        Stream #0:2(rus): Audio: mp3, 48000 Hz, stereo, s16p, 128 kb/s
        Metadata:
          title           : rus
        Stream #0:3(eng): Audio: mp3, 48000 Hz, stereo, s16p, 119 kb/s (default)
        Metadata:
          title           : eng
        Stream #0:4(eng): Subtitle: subrip (default)
        Metadata:
          title           : eng

        FFmpeg sub track ids would be completely different. We should pass real ids.
       */
      cmdList.add("0:" + (params.sid.getId() + media.getAudioTracksList().size() + 1));
    }

    final File subtitleDirectory = new File(configuration.getTempFolder(), SUB_DIR + File.separator);
    if (!subtitleDirectory.exists()) {
      subtitleDirectory.mkdirs();
    }

    if (params.sid.isEmbedded()) {
      tempSubsFile = new File(subtitleDirectory.getAbsolutePath() + File.separator +
          getBaseName(new File(filename).getName()).replaceAll("\\W", "_") + "_" +
          new File(filename).length() + "_EMB_ID" + params.sid.getId() + ".ass");
    } else {
      tempSubsFile = new File(subtitleDirectory.getAbsolutePath() + File.separator +
          getBaseName(new File(filename).getName()).replaceAll("\\W", "_") + "_" +
          new File(filename).length() + "_EXT." + getExtension(new File(filename).getName()));
    }

    cmdList.add(tempSubsFile.getAbsolutePath());

    String[] cmdArray = new String[cmdList.size()];
    cmdList.toArray(cmdArray);

    ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params);
    pw.runInNewThread();

    try {
      pw.join(); // Wait until the conversion is finished
    } catch (InterruptedException e) {
      logger.debug("Subtitle conversion finished wih error: " + e);
      return null;
    }

    return tempSubsFile;
  }

  /**
   * Collapse the multiple internal ways of saying "subtitles are disabled" into a single method
   * which returns true if any of the following are true:
   *
   *     1) configuration.isDisableSubtitles()
   *     2) params.sid == null
   */
  public boolean isDisableSubtitles(OutputParams params) {
    return configuration.isDisableSubtitles() || (params.sid == null);
  }

  private synchronized boolean isAc3Remux() {
    return ac3Remux;
  }

  private synchronized void setAc3Remux(boolean ac3Remux) {
    this.ac3Remux = ac3Remux;
  }

  private synchronized boolean isDtsRemux() {
    return dtsRemux;
  }

  private synchronized void setDtsRemux(boolean dtsRemux) {
    this.dtsRemux = dtsRemux;
  }

  private synchronized boolean isVideoRemux() {
    return videoRemux;
  }

  private synchronized void setVideoRemux(boolean videoRemux) {
    this.videoRemux = videoRemux;
  }
}
TOP

Related Classes of net.pms.encoders.FFmpegVideo

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.