Package net.pms.encoders

Source Code of net.pms.encoders.MEncoderVideo

/*
* 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 static net.pms.formats.v2.AudioUtils.getLPCMChannelMappingForMencoder;
import static org.apache.commons.lang3.BooleanUtils.isTrue;
import static org.apache.commons.lang3.StringUtils.defaultString;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.apache.commons.lang3.StringUtils.startsWith;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;

import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

import net.pms.Messages;
import net.pms.PMS;
import net.pms.configuration.FormatConfiguration;
import net.pms.configuration.PmsConfiguration;
import net.pms.configuration.RendererConfiguration;
import net.pms.dlna.DLNAMediaAudio;
import net.pms.dlna.DLNAMediaInfo;
import net.pms.dlna.DLNAResource;
import net.pms.dlna.InputFile;
import net.pms.formats.Format;
import net.pms.formats.v2.SubtitleType;
import net.pms.formats.v2.SubtitleUtils;
import net.pms.io.OutputParams;
import net.pms.io.PipeIPCProcess;
import net.pms.io.PipeProcess;
import net.pms.io.ProcessWrapper;
import net.pms.io.ProcessWrapperImpl;
import net.pms.io.StreamModifier;
import net.pms.network.HTTPResource;
import net.pms.util.CodecUtil;
import net.pms.util.FileUtil;
import net.pms.util.FormLayoutUtil;
import net.pms.util.PlayerUtil;
import net.pms.util.ProcessUtil;

import org.apache.commons.configuration.event.ConfigurationEvent;
import org.apache.commons.configuration.event.ConfigurationListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import bsh.EvalError;
import bsh.Interpreter;

import com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import com.sun.jna.Platform;

public class MEncoderVideo extends Player {
  private static final Logger logger = LoggerFactory.getLogger(MEncoderVideo.class);
  private static final String COL_SPEC = "left:pref, 3dlu, p:grow, 3dlu, right:p:grow, 3dlu, p:grow, 3dlu, right:p:grow,3dlu, p:grow, 3dlu, right:p:grow,3dlu, pref:grow";
  private static final String ROW_SPEC = "p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 9dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p";
  private static final String REMOVE_OPTION = "---REMOVE-ME---"; // use an out-of-band option that can't be confused with a real option

  private JTextField mencoder_noass_scale;
  private JTextField mencoder_noass_subpos;
  private JTextField mencoder_noass_blur;
  private JTextField mencoder_noass_outline;
  private JTextField mencoder_custom_options;
  private JTextField subq;
  private JCheckBox forcefps;
  private JCheckBox yadif;
  private JCheckBox scaler;
  private JTextField scaleX;
  private JTextField scaleY;
  private JCheckBox assdefaultstyle;
  private JCheckBox fc;
  private JCheckBox ass;
  private JCheckBox checkBox;
  private JCheckBox mencodermt;
  private JCheckBox noskip;
  private JCheckBox intelligentsync;
  private JTextField ocw;
  private JTextField och;
  private final PmsConfiguration configuration;

  private static final String[] INVALID_CUSTOM_OPTIONS = {
    "-of",
    "-oac",
    "-ovc",
    "-mpegopts"
  };

  private static final String INVALID_CUSTOM_OPTIONS_LIST = Arrays.toString(INVALID_CUSTOM_OPTIONS);

  public static final String ID = "mencoder";

  // TODO (breaking change): most (probably all) of these
  // protected fields should be private. And at least two
  // shouldn't be fields

  @Deprecated
  protected boolean dvd;

  @Deprecated
  protected String overriddenMainArgs[];

  protected boolean dtsRemux;
  protected boolean pcm;
  protected boolean ovccopy;
  protected boolean ac3Remux;
  protected boolean mpegts;
  protected boolean wmv;

  public static final String DEFAULT_CODEC_CONF_SCRIPT =
    Messages.getString("MEncoderVideo.68")
    + Messages.getString("MEncoderVideo.69")
    + Messages.getString("MEncoderVideo.70")
    + Messages.getString("MEncoderVideo.71")
    + Messages.getString("MEncoderVideo.72")
    + Messages.getString("MEncoderVideo.73")
    + Messages.getString("MEncoderVideo.75")
    + Messages.getString("MEncoderVideo.76")
    + Messages.getString("MEncoderVideo.77")
    + Messages.getString("MEncoderVideo.78")
    + Messages.getString("MEncoderVideo.79")
    + "#\n"
    + Messages.getString("MEncoderVideo.80")
    + "container == iso :: -nosync\n"
    + "(container == avi || container == matroska) && vcodec == mpeg4 && acodec == mp3 :: -mc 0.1\n"
    + "container == flv :: -mc 0.1\n"
    + "container == mov :: -mc 0.1\n"
    + "container == rm  :: -mc 0.1\n"
    + "container == matroska && framerate == 29.97  :: -nomux -mc 0\n"
    + "container == mp4 && vcodec == h264 :: -mc 0.1\n"
    + "\n"
    + Messages.getString("MEncoderVideo.87")
    + Messages.getString("MEncoderVideo.88")
    + Messages.getString("MEncoderVideo.89")
    + Messages.getString("MEncoderVideo.91");

  public JCheckBox getCheckBox() {
    return checkBox;
  }

  public JCheckBox getNoskip() {
    return noskip;
  }

  public MEncoderVideo(PmsConfiguration configuration) {
    this.configuration = configuration;
  }

  @Override
  public JComponent config() {
    // Apply the orientation for the locale
    Locale locale = new Locale(configuration.getLanguage());
    ComponentOrientation orientation = ComponentOrientation.getOrientation(locale);
    String colSpec = FormLayoutUtil.getColSpec(COL_SPEC, orientation);

    FormLayout layout = new FormLayout(colSpec, ROW_SPEC);
    PanelBuilder builder = new PanelBuilder(layout);

    CellConstraints cc = new CellConstraints();

    checkBox = new JCheckBox(Messages.getString("MEncoderVideo.0"));
    checkBox.setContentAreaFilled(false);

    if (configuration.getSkipLoopFilterEnabled()) {
      checkBox.setSelected(true);
    }

    checkBox.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setSkipLoopFilterEnabled((e.getStateChange() == ItemEvent.SELECTED));
      }
    });

    JComponent cmp = builder.addSeparator(Messages.getString("NetworkTab.5"), FormLayoutUtil.flip(cc.xyw(1, 1, 15), colSpec, orientation));
    cmp = (JComponent) cmp.getComponent(0);
    cmp.setFont(cmp.getFont().deriveFont(Font.BOLD));

    mencodermt = new JCheckBox(Messages.getString("MEncoderVideo.35"));
    mencodermt.setContentAreaFilled(false);

    if (configuration.getMencoderMT()) {
      mencodermt.setSelected(true);
    }

    mencodermt.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        configuration.setMencoderMT(mencodermt.isSelected());
      }
    });

    mencodermt.setEnabled(Platform.isWindows() || Platform.isMac());

    builder.add(mencodermt, FormLayoutUtil.flip(cc.xy(1, 3), colSpec, orientation));
    builder.add(checkBox, FormLayoutUtil.flip(cc.xyw(3, 3, 12), colSpec, orientation));

    noskip = new JCheckBox(Messages.getString("MEncoderVideo.2"));
    noskip.setContentAreaFilled(false);

    if (configuration.isMencoderNoOutOfSync()) {
      noskip.setSelected(true);
    }

    noskip.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setMencoderNoOutOfSync((e.getStateChange() == ItemEvent.SELECTED));
      }
    });

    builder.add(noskip, FormLayoutUtil.flip(cc.xy(1, 5), colSpec, orientation));

    JButton button = new JButton(Messages.getString("MEncoderVideo.29"));
    button.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        JPanel codecPanel = new JPanel(new BorderLayout());
        final JTextArea textArea = new JTextArea();
        textArea.setText(configuration.getMencoderCodecSpecificConfig());
        textArea.setFont(new Font("Courier", Font.PLAIN, 12));
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setPreferredSize(new java.awt.Dimension(900, 100));

        final JTextArea textAreaDefault = new JTextArea();
        textAreaDefault.setText(DEFAULT_CODEC_CONF_SCRIPT);
        textAreaDefault.setBackground(Color.WHITE);
        textAreaDefault.setFont(new Font("Courier", Font.PLAIN, 12));
        textAreaDefault.setEditable(false);
        textAreaDefault.setEnabled(configuration.isMencoderIntelligentSync());
        JScrollPane scrollPaneDefault = new JScrollPane(textAreaDefault);
        scrollPaneDefault.setPreferredSize(new java.awt.Dimension(900, 450));

        JPanel customPanel = new JPanel(new BorderLayout());
        intelligentsync = new JCheckBox(Messages.getString("MEncoderVideo.3"));
        intelligentsync.setContentAreaFilled(false);

        if (configuration.isMencoderIntelligentSync()) {
          intelligentsync.setSelected(true);
        }

        intelligentsync.addItemListener(new ItemListener() {
          @Override
          public void itemStateChanged(ItemEvent e) {
            configuration.setMencoderIntelligentSync((e.getStateChange() == ItemEvent.SELECTED));
            textAreaDefault.setEnabled(configuration.isMencoderIntelligentSync());

          }
        });

        JLabel label = new JLabel(Messages.getString("MEncoderVideo.33"));
        customPanel.add(label, BorderLayout.NORTH);
        customPanel.add(scrollPane, BorderLayout.SOUTH);

        codecPanel.add(intelligentsync, BorderLayout.NORTH);
        codecPanel.add(scrollPaneDefault, BorderLayout.CENTER);
        codecPanel.add(customPanel, BorderLayout.SOUTH);

        while (JOptionPane.showOptionDialog(SwingUtilities.getWindowAncestor((Component) PMS.get().getFrame()),
          codecPanel, Messages.getString("MEncoderVideo.34"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null) == JOptionPane.OK_OPTION) {
          String newCodecparam = textArea.getText();
          DLNAMediaInfo fakemedia = new DLNAMediaInfo();
          DLNAMediaAudio audio = new DLNAMediaAudio();
          audio.setCodecA("ac3");
          fakemedia.setCodecV("mpeg4");
          fakemedia.setContainer("matroska");
          fakemedia.setDuration(45d*60);
          audio.getAudioProperties().setNumberOfChannels(2);
          fakemedia.setWidth(1280);
          fakemedia.setHeight(720);
          audio.setSampleFrequency("48000");
          fakemedia.setFrameRate("23.976");
          fakemedia.getAudioTracksList().add(audio);
          String result[] = getSpecificCodecOptions(newCodecparam, fakemedia, new OutputParams(configuration), "dummy.mpg", "dummy.srt", false, true);

          if (result.length > 0 && result[0].startsWith("@@")) {
            String errorMessage = result[0].substring(2);
            JOptionPane.showMessageDialog(
              SwingUtilities.getWindowAncestor((Component) PMS.get().getFrame()),
              errorMessage,
              Messages.getString("Dialog.Error"),
              JOptionPane.ERROR_MESSAGE
            );
          } else {
            configuration.setMencoderCodecSpecificConfig(newCodecparam);
            break;
          }
        }
      }
    });
    builder.add(button, FormLayoutUtil.flip(cc.xy(1, 11), colSpec, orientation));

    forcefps = new JCheckBox(Messages.getString("MEncoderVideo.4"));
    forcefps.setContentAreaFilled(false);
    if (configuration.isMencoderForceFps()) {
      forcefps.setSelected(true);
    }
    forcefps.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setMencoderForceFps(e.getStateChange() == ItemEvent.SELECTED);
      }
    });

    builder.add(forcefps, FormLayoutUtil.flip(cc.xyw(1, 7, 2), colSpec, orientation));

    yadif = new JCheckBox(Messages.getString("MEncoderVideo.26"));
    yadif.setContentAreaFilled(false);
    if (configuration.isMencoderYadif()) {
      yadif.setSelected(true);
    }
    yadif.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setMencoderYadif(e.getStateChange() == ItemEvent.SELECTED);
      }
    });
    builder.add(yadif, FormLayoutUtil.flip(cc.xyw(3, 7, 7), colSpec, orientation));

    scaler = new JCheckBox(Messages.getString("MEncoderVideo.27"));
    scaler.setContentAreaFilled(false);
    scaler.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setMencoderScaler(e.getStateChange() == ItemEvent.SELECTED);
        scaleX.setEnabled(configuration.isMencoderScaler());
        scaleY.setEnabled(configuration.isMencoderScaler());
      }
    });
    builder.add(scaler, FormLayoutUtil.flip(cc.xyw(3, 5, 7), colSpec, orientation));

    builder.addLabel(Messages.getString("MEncoderVideo.28"), FormLayoutUtil.flip(cc.xy(9, 5, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation));
    scaleX = new JTextField("" + configuration.getMencoderScaleX());
    scaleX.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        try {
          configuration.setMencoderScaleX(Integer.parseInt(scaleX.getText()));
        } catch (NumberFormatException nfe) {
          logger.debug("Could not parse scaleX from \"" + scaleX.getText() + "\"");
        }
      }
    });
    builder.add(scaleX, FormLayoutUtil.flip(cc.xy(11, 5), colSpec, orientation));

    builder.addLabel(Messages.getString("MEncoderVideo.30"), FormLayoutUtil.flip(cc.xy(13, 5, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation));
    scaleY = new JTextField("" + configuration.getMencoderScaleY());
    scaleY.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        try {
          configuration.setMencoderScaleY(Integer.parseInt(scaleY.getText()));
        } catch (NumberFormatException nfe) {
          logger.debug("Could not parse scaleY from \"" + scaleY.getText() + "\"");
        }
      }
    });
    builder.add(scaleY, FormLayoutUtil.flip(cc.xy(15, 5), colSpec, orientation));

    if (configuration.isMencoderScaler()) {
      scaler.setSelected(true);
    } else {
      scaleX.setEnabled(false);
      scaleY.setEnabled(false);
    }

    builder.addLabel(Messages.getString("MEncoderVideo.6"), FormLayoutUtil.flip(cc.xy(1, 13), colSpec, orientation));
    mencoder_custom_options = new JTextField(configuration.getMencoderCustomOptions());
    mencoder_custom_options.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        configuration.setMencoderCustomOptions(mencoder_custom_options.getText());
      }
    });
    builder.add(mencoder_custom_options, FormLayoutUtil.flip(cc.xyw(3, 13, 13), colSpec, orientation));

    builder.addLabel(Messages.getString("MEncoderVideo.93"), FormLayoutUtil.flip(cc.xy(1, 15), colSpec, orientation));

    builder.addLabel(Messages.getString("MEncoderVideo.28") + " (%)", FormLayoutUtil.flip(cc.xy(1, 15, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation));
    ocw = new JTextField(configuration.getMencoderOverscanCompensationWidth());
    ocw.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        configuration.setMencoderOverscanCompensationWidth(ocw.getText());
      }
    });
    builder.add(ocw, FormLayoutUtil.flip(cc.xy(3, 15), colSpec, orientation));

    builder.addLabel(Messages.getString("MEncoderVideo.30") + " (%)", FormLayoutUtil.flip(cc.xy(5, 15), colSpec, orientation));
    och = new JTextField(configuration.getMencoderOverscanCompensationHeight());
    och.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        configuration.setMencoderOverscanCompensationHeight(och.getText());
      }
    });
    builder.add(och, FormLayoutUtil.flip(cc.xy(7, 15), colSpec, orientation));

    cmp = builder.addSeparator(Messages.getString("MEncoderVideo.8"), FormLayoutUtil.flip(cc.xyw(1, 17, 15), colSpec, orientation));
    cmp = (JComponent) cmp.getComponent(0);
    cmp.setFont(cmp.getFont().deriveFont(Font.BOLD));

    builder.addLabel(Messages.getString("MEncoderVideo.16"), FormLayoutUtil.flip(cc.xy(1, 27, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation));

    mencoder_noass_scale = new JTextField(configuration.getMencoderNoAssScale());
    mencoder_noass_scale.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        configuration.setMencoderNoAssScale(mencoder_noass_scale.getText());
      }
    });

    builder.addLabel(Messages.getString("MEncoderVideo.17"), FormLayoutUtil.flip(cc.xy(5, 27), colSpec, orientation));

    mencoder_noass_outline = new JTextField(configuration.getMencoderNoAssOutline());
    mencoder_noass_outline.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        configuration.setMencoderNoAssOutline(mencoder_noass_outline.getText());
      }
    });

    builder.addLabel(Messages.getString("MEncoderVideo.18"), FormLayoutUtil.flip(cc.xy(9, 27), colSpec, orientation));

    mencoder_noass_blur = new JTextField(configuration.getMencoderNoAssBlur());
    mencoder_noass_blur.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        configuration.setMencoderNoAssBlur(mencoder_noass_blur.getText());
      }
    });

    builder.addLabel(Messages.getString("MEncoderVideo.19"), FormLayoutUtil.flip(cc.xy(13, 27), colSpec, orientation));

    mencoder_noass_subpos = new JTextField(configuration.getMencoderNoAssSubPos());
    mencoder_noass_subpos.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        configuration.setMencoderNoAssSubPos(mencoder_noass_subpos.getText());
      }
    });

    builder.add(mencoder_noass_scale, FormLayoutUtil.flip(cc.xy(3, 27), colSpec, orientation));
    builder.add(mencoder_noass_outline, FormLayoutUtil.flip(cc.xy(7, 27), colSpec, orientation));
    builder.add(mencoder_noass_blur, FormLayoutUtil.flip(cc.xy(11, 27), colSpec, orientation));
    builder.add(mencoder_noass_subpos, FormLayoutUtil.flip(cc.xy(15, 27), colSpec, orientation));

    ass = new JCheckBox(Messages.getString("MEncoderVideo.20"));
    ass.setContentAreaFilled(false);
    ass.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        if (e != null) {
          configuration.setMencoderAss(e.getStateChange() == ItemEvent.SELECTED);
        }
      }
    });
    builder.add(ass, FormLayoutUtil.flip(cc.xy(1, 23), colSpec, orientation));
    ass.setSelected(configuration.isMencoderAss());
    ass.getItemListeners()[0].itemStateChanged(null);

    fc = new JCheckBox(Messages.getString("MEncoderVideo.21"));
    fc.setContentAreaFilled(false);
    fc.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setMencoderFontConfig(e.getStateChange() == ItemEvent.SELECTED);
      }
    });
    builder.add(fc, FormLayoutUtil.flip(cc.xyw(3, 23, 5), colSpec, orientation));
    fc.setSelected(configuration.isMencoderFontConfig());

    assdefaultstyle = new JCheckBox(Messages.getString("MEncoderVideo.36"));
    assdefaultstyle.setContentAreaFilled(false);
    assdefaultstyle.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setMencoderAssDefaultStyle(e.getStateChange() == ItemEvent.SELECTED);
      }
    });
    builder.add(assdefaultstyle, FormLayoutUtil.flip(cc.xyw(8, 23, 4), colSpec, orientation));
    assdefaultstyle.setSelected(configuration.isMencoderAssDefaultStyle());

    builder.addLabel(Messages.getString("MEncoderVideo.92"), FormLayoutUtil.flip(cc.xy(1, 29), colSpec, orientation));
    subq = new JTextField(configuration.getMencoderVobsubSubtitleQuality());
    subq.addKeyListener(new KeyAdapter() {
      @Override
      public void keyReleased(KeyEvent e) {
        configuration.setMencoderVobsubSubtitleQuality(subq.getText());
      }
    });
    builder.add(subq, FormLayoutUtil.flip(cc.xyw(3, 29, 1), colSpec, orientation));

    configuration.addConfigurationListener(new ConfigurationListener() {
      @Override
      public void configurationChanged(ConfigurationEvent event) {
        if (event.getPropertyName() == null) {
          return;
        }
        if ((!event.isBeforeUpdate()) && event.getPropertyName().equals(PmsConfiguration.KEY_DISABLE_SUBTITLES)) {
          boolean enabled = !configuration.isDisableSubtitles();
          ass.setEnabled(enabled);
          assdefaultstyle.setEnabled(enabled);
          fc.setEnabled(enabled);
          mencoder_noass_scale.setEnabled(enabled);
          mencoder_noass_outline.setEnabled(enabled);
          mencoder_noass_blur.setEnabled(enabled);
          mencoder_noass_subpos.setEnabled(enabled);
          ocw.setEnabled(enabled);
          och.setEnabled(enabled);
          subq.setEnabled(enabled);

          if (enabled) {
            ass.getItemListeners()[0].itemStateChanged(null);
          }
        }
      }
    });

    JPanel panel = builder.getPanel();

    // Apply the orientation to the panel and all components in it
    panel.applyComponentOrientation(orientation);

    return panel;
  }

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

  @Override
  public String id() {
    return ID;
  }

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

  protected String[] getDefaultArgs() {
    List<String> defaultArgsList = new ArrayList<String>();

    defaultArgsList.add("-msglevel");
    defaultArgsList.add("statusline=2");

    defaultArgsList.add("-oac");
    defaultArgsList.add((ac3Remux || dtsRemux) ? "copy" : (pcm ? "pcm" : "lavc"));

    defaultArgsList.add("-of");
    defaultArgsList.add((wmv || mpegts) ? "lavf" : ((pcm && avisynth()) ? "avi" : ((pcm || dtsRemux) ? "rawvideo" : "mpeg")));

    if (wmv) {
      defaultArgsList.add("-lavfopts");
      defaultArgsList.add("format=asf");
    } else if (mpegts) {
      defaultArgsList.add("-lavfopts");
      defaultArgsList.add("format=mpegts");
    }

    defaultArgsList.add("-mpegopts");
    defaultArgsList.add("format=mpeg2:muxrate=500000:vbuf_size=1194:abuf_size=64");


    defaultArgsList.add("-ovc");
    defaultArgsList.add(ovccopy ? "copy" : "lavc");

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

    return defaultArgsArray;
  }

  /**
   * Returns the argument string surrounded with quotes if it contains a space,
   * otherwise returns the string as is.
   *
   * @param arg The argument string
   * @return The string, optionally in quotes.
   */
  private String quoteArg(String arg) {
    if (arg == null || arg.indexOf(" ") == -1) {
      return arg;
    }

    StringBuilder result = new StringBuilder();
    result.append("\"").append(arg).append("\"");
    return result.toString();
  }

  private String[] sanitizeArgs(String[] args) {
    List<String> sanitized = new ArrayList<String>();
    int i = 0;

    while (i < args.length) {
      String name = args[i];
      String value = null;

      for (String option : INVALID_CUSTOM_OPTIONS) {
        if (option.equals(name)) {
          if ((i + 1) < args.length) {
             value = " " + args[i + 1];
             ++i;
          } else {
             value = "";
          }

          logger.warn(
            "Ignoring custom MEncoder option: {}{}; the following options cannot be changed: " + INVALID_CUSTOM_OPTIONS_LIST,
            name,
            value
          );

          break;
        }
      }

      if (value == null) {
        sanitized.add(args[i]);
      }

      ++i;
    }

    return sanitized.toArray(new String[sanitized.size()]);
  }

  @Override
  public String[] args() {
    String args[];
    String defaultArgs[] = getDefaultArgs();

    if (overriddenMainArgs != null) {
      // add the sanitized custom MEncoder options.
      // not cached because they may be changed on the fly in the GUI
      // TODO if/when we upgrade to org.apache.commons.lang3:
      // args = ArrayUtils.addAll(defaultArgs, sanitizeArgs(overriddenMainArgs))
      String[] sanitizedCustomArgs = sanitizeArgs(overriddenMainArgs);
      args = new String[defaultArgs.length + sanitizedCustomArgs.length];
      System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length);
      System.arraycopy(sanitizedCustomArgs, 0, args, defaultArgs.length, sanitizedCustomArgs.length);
    } else {
      args = defaultArgs;
    }

    return args;
  }

  @Override
  public String executable() {
    return configuration.getMencoderPath();
  }

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

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

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

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

    try {
      bitrates[0] = (int) Double.parseDouble(bitrate);
    } catch (NumberFormatException e) {
      bitrates[0] = 0;
    }

    return bitrates;
  }

  /**
   * Note: This is not exact. The bitrate can go above this but it is generally pretty good.
   * @return The maximum bitrate the video should be along with the buffer size using MEncoder vars
   */
  private String addMaximumBitrateConstraints(String encodeSettings, DLNAMediaInfo media, String quality, RendererConfiguration mediaRenderer, String audioType) {
    int defaultMaxBitrates[] = getVideoBitrateConfig(configuration.getMaximumBitrate());
    int rendererMaxBitrates[] = new int[2];

    if (mediaRenderer.getMaxVideoBitrate() != null) {
      rendererMaxBitrates = getVideoBitrateConfig(mediaRenderer.getMaxVideoBitrate());
    }

    if ((rendererMaxBitrates[0] > 0) && ((defaultMaxBitrates[0] == 0) || (rendererMaxBitrates[0] < defaultMaxBitrates[0]))) {
      defaultMaxBitrates = rendererMaxBitrates;
    }

    if (mediaRenderer.getCBRVideoBitrate() == 0 && defaultMaxBitrates[0] > 0 && !quality.contains("vrc_buf_size") && !quality.contains("vrc_maxrate") && !quality.contains("vbitrate")) {
      // Convert value from Mb to Kb
      defaultMaxBitrates[0] = 1000 * defaultMaxBitrates[0];

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

      int bufSize = 1835;
      if (media.isHDVideo()) {
        bufSize = defaultMaxBitrates[0] / 3;
      }

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

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

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

      // Make room for audio
      // If audio is PCM, subtract 4600kb/s
      if ("pcm".equals(audioType)) {
        defaultMaxBitrates[0] = defaultMaxBitrates[0] - 4600;
      }
      // If audio is DTS, subtract 1510kb/s
      else if ("dts".equals(audioType)) {
        defaultMaxBitrates[0] = defaultMaxBitrates[0] - 1510;
      }
      // If audio is AC3, subtract 640kb/s to be safe
      else if ("ac3".equals(audioType)) {
        defaultMaxBitrates[0] = defaultMaxBitrates[0] - 640;
      }

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

      encodeSettings += ":vrc_maxrate=" + defaultMaxBitrates[0] + ":vrc_buf_size=" + bufSize;
    }

    return encodeSettings;
  }

  /*
   * 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.isMencoderDisableSubs()
   *     2) params.sid == null
   *     3) avisynth()
   */
  private boolean isDisableSubtitles(OutputParams params) {
    return configuration.isDisableSubtitles() || (params.sid == null) || avisynth();
  }

  @Override
  public ProcessWrapper launchTranscode(
    DLNAResource dlna,
    DLNAMediaInfo media,
    OutputParams params
  ) throws IOException {
    params.manageFastStart();

    boolean avisynth = avisynth();

    final String filename = dlna.getSystemName();
    setAudioAndSubs(filename, media, params, configuration);
    String externalSubtitlesFileName = null;

    if (params.sid != null && params.sid.isExternal()) {
      if (params.sid.isExternalFileUtf16()) {
        // convert UTF-16 -> UTF-8
        File convertedSubtitles = new File(PMS.getConfiguration().getTempFolder(), "utf8_" + params.sid.getExternalFile().getName());
        FileUtil.convertFileFromUtf16ToUtf8(params.sid.getExternalFile(), convertedSubtitles);
        externalSubtitlesFileName = ProcessUtil.getShortFileNameIfWideChars(convertedSubtitles.getAbsolutePath());
      } else {
        externalSubtitlesFileName = ProcessUtil.getShortFileNameIfWideChars(params.sid.getExternalFile().getAbsolutePath());
      }
    }

    InputFile newInput = new InputFile();
    newInput.setFilename(filename);
    newInput.setPush(params.stdin);

    dvd = false;

    if (media != null && media.getDvdtrack() > 0) {
      dvd = true;
    }

    ovccopy = false;
    pcm = false;
    ac3Remux = false;
    dtsRemux = false;
    wmv = false;

    int intOCW = 0;
    int intOCH = 0;

    try {
      intOCW = Integer.parseInt(configuration.getMencoderOverscanCompensationWidth());
    } catch (NumberFormatException e) {
      logger.error("Cannot parse configured MEncoder overscan compensation width: \"{}\"", configuration.getMencoderOverscanCompensationWidth());
    }

    try {
      intOCH = Integer.parseInt(configuration.getMencoderOverscanCompensationHeight());
    } catch (NumberFormatException e) {
      logger.error("Cannot parse configured MEncoder overscan compensation height: \"{}\"", configuration.getMencoderOverscanCompensationHeight());
    }

    if (params.sid == null && dvd && configuration.isMencoderRemuxMPEG2() && params.mediaRenderer.isMpeg2Supported()) {
      String expertOptions[] = getSpecificCodecOptions(
        configuration.getMencoderCodecSpecificConfig(),
        media,
        params,
        filename,
        externalSubtitlesFileName,
        configuration.isMencoderIntelligentSync(),
        false
      );

      boolean nomux = false;

      for (String s : expertOptions) {
        if (s.equals("-nomux")) {
          nomux = true;
        }
      }

      if (!nomux) {
        ovccopy = true;
      }
    }

    String vcodec = "mpeg2video";

    if (params.mediaRenderer.isTranscodeToWMV()) {
      wmv = true;
      vcodec = "wmv2"; // http://wiki.megaframe.org/wiki/Ubuntu_XBOX_360#MEncoder not usable in streaming
    }

    mpegts = params.mediaRenderer.isTranscodeToMPEGTSAC3();

    /*
     Disable AC-3 remux for stereo tracks with 384 kbits bitrate and PS3 renderer (PS3 FW bug?)
     TODO check new firmwares
     Commented out until we can find a way to detect when a video has an audio track that switches from 2 to 6 channels
     because MEncoder can't handle those files, which are very common these days.
    */
    // final boolean ps3_and_stereo_and_384_kbits = params.aid != null
    //  && (params.mediaRenderer.isPS3() && params.aid.getAudioProperties().getNumberOfChannels() == 2)
    //  && (params.aid.getBitRate() > 370000 && params.aid.getBitRate() < 400000);
    final boolean ps3_and_stereo_and_384_kbits = false;

    final boolean isTSMuxerVideoEngineEnabled = PMS.getConfiguration().getEnginesAsList().contains(TsMuxeRVideo.ID);
    final boolean mencoderAC3RemuxAudioDelayBug = (params.aid != null) && (params.aid.getAudioProperties().getAudioDelay() != 0) && (params.timeseek == 0);
    if (!mencoderAC3RemuxAudioDelayBug && configuration.isAudioRemuxAC3() && params.aid != null && params.aid.isAC3() && !ps3_and_stereo_and_384_kbits && !avisynth() && params.mediaRenderer.isTranscodeToAC3()) {
      // AC3 remux takes priority
      ac3Remux = true;
    } else {
      // now check for DTS remux and LPCM streaming
      dtsRemux = isTSMuxerVideoEngineEnabled && configuration.isAudioEmbedDtsInPcm() &&
        (
          !dvd ||
          configuration.isMencoderRemuxMPEG2()
        ) && params.aid != null &&
        params.aid.isDTS() &&
        !avisynth() &&
        params.mediaRenderer.isDTSPlayable();
      pcm = isTSMuxerVideoEngineEnabled && configuration.isAudioUsePCM() &&
        (
          !dvd ||
          configuration.isMencoderRemuxMPEG2()
        )
        // disable LPCM transcoding for MP4 container with non-H264 video as workaround for mencoder's A/V sync bug
        && !(media.getContainer().equals("mp4") && !media.getCodecV().equals("h264"))
        && params.aid != null &&
        (
          (params.aid.isDTS() && params.aid.getAudioProperties().getNumberOfChannels() <= 6) || // disable 7.1 DTS-HD => LPCM because of channels mapping bug
          params.aid.isLossless() ||
          params.aid.isTrueHD() ||
          (
            !configuration.isMencoderUsePcmForHQAudioOnly() &&
            (
              params.aid.isAC3() ||
              params.aid.isMP3() ||
              params.aid.isAAC() ||
              params.aid.isVorbis() ||
              // disable WMA to LPCM transcoding because of mencoder's channel mapping bug
              // (see CodecUtil.getMixerOutput)
              // params.aid.isWMA() ||
              params.aid.isMpegAudio()
            )
          )
        ) && params.mediaRenderer.isLPCMPlayable();
    }

    if (dtsRemux || pcm) {
      params.losslessaudio = true;
      params.forceFps = media.getValidFps(false);
    }

    // mpeg2 remux still buggy with mencoder :\
    // TODO when we can still use it?
    ovccopy = false;

    if (pcm && avisynth()) {
      params.avidemux = true;
    }

    int channels;
    if (ac3Remux) {
      channels = params.aid.getAudioProperties().getNumberOfChannels(); // ac3 remux
    } else if (dtsRemux || wmv) {
      channels = 2;
    } else if (pcm) {
      channels = params.aid.getAudioProperties().getNumberOfChannels();
    } else {
      channels = configuration.getAudioChannelCount(); // 5.1 max for ac3 encoding
    }

    logger.trace("channels=" + channels);

    String add = "";
    String rendererMencoderOptions = params.mediaRenderer.getCustomMencoderOptions(); // default: empty string
    String globalMencoderOptions = configuration.getMencoderCustomOptions(); // default: empty string

    if (params.mediaRenderer.isPadVideoWithBlackBordersTo169AR()) {
      rendererMencoderOptions += " -vf softskip,expand=::::1:16/9:4";
    }

    String combinedCustomOptions = defaultString(globalMencoderOptions)
      + " "
      + defaultString(rendererMencoderOptions);

    if (!combinedCustomOptions.contains("-lavdopts")) {
      add = " -lavdopts debug=0";
    }

    if (isNotBlank(rendererMencoderOptions)) {
      // don't use the renderer-specific options if they break DVD streaming
      // XXX we should weed out the unused/unwanted settings and keep the rest
      // (see sanitizeArgs()) rather than ignoring the options entirely
      if (dvd && rendererMencoderOptions.contains("expand=")) {
        logger.warn("renderer MEncoder options are incompatible with DVD streaming; ignoring: " + rendererMencoderOptions);
        rendererMencoderOptions = null;
      }
    }

    StringTokenizer st = new StringTokenizer(
      "-channels " + channels
      + (isNotBlank(globalMencoderOptions) ? " " + globalMencoderOptions : "")
      + (isNotBlank(rendererMencoderOptions) ? " " + rendererMencoderOptions : "")
      + add,
      " "
    );

    // XXX why does this field (which is used to populate the array returned by args(),
    // called below) store the renderer-specific (i.e. not global) MEncoder options?
    overriddenMainArgs = new String[st.countTokens()];

    {
      int nThreads = (dvd || filename.toLowerCase().endsWith("dvr-ms")) ?
        1 :
        configuration.getMencoderMaxThreads();
      boolean handleToken = false;
      int i = 0;

      while (st.hasMoreTokens()) {
        String token = st.nextToken().trim();

        if (handleToken) {
          token += ":threads=" + nThreads;

          if (configuration.getSkipLoopFilterEnabled() && !avisynth()) {
            token += ":skiploopfilter=all";
          }

          handleToken = false;
        }

        if (token.toLowerCase().contains("lavdopts")) {
          handleToken = true;
        }

        overriddenMainArgs[i++] = token;
      }
    }

    if (configuration.getMPEG2MainSettings() != null) {
      String mpeg2Options = configuration.getMPEG2MainSettings();
      String mpeg2OptionsRenderer = params.mediaRenderer.getCustomMEncoderMPEG2Options();

      // Renderer settings take priority over user settings
      if (isNotBlank(mpeg2OptionsRenderer)) {
        mpeg2Options = mpeg2OptionsRenderer;
      } else {
        // Remove comment from the value
        if (mpeg2Options.contains("/*")) {
          mpeg2Options = mpeg2Options.substring(mpeg2Options.indexOf("/*"));
        }

        // Find out the maximum bandwidth we are supposed to use
        int defaultMaxBitrates[] = getVideoBitrateConfig(configuration.getMaximumBitrate());
        int rendererMaxBitrates[] = new int[2];

        if (params.mediaRenderer.getMaxVideoBitrate() != null) {
          rendererMaxBitrates = getVideoBitrateConfig(params.mediaRenderer.getMaxVideoBitrate());
        }

        if ((rendererMaxBitrates[0] > 0) && (rendererMaxBitrates[0] < defaultMaxBitrates[0])) {
          defaultMaxBitrates = rendererMaxBitrates;
        }

        int maximumBitrate = defaultMaxBitrates[0];

        // Determine a good quality setting based on video attributes
        if (mpeg2Options.contains("Automatic")) {
          mpeg2Options = "keyint=5:vqscale=1:vqmin=2:vqmax=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 = "keyint=25:vqscale=1:vqmin=2:vqmax=3";
          }

          if (mpeg2Options.contains("Wireless") || maximumBitrate < 70) {
            // Lower quality for 720p+ content
            if (media.getWidth() > 1280) {
              mpeg2Options = "keyint=25:vqmax=7:vqmin=2";
            } else if (media.getWidth() > 720) {
              mpeg2Options = "keyint=25:vqmax=5:vqmin=2";
            }
          }
        }
      }

      // Ditlew - WDTV Live (+ other byte asking clients), CBR. This probably ought to be placed in addMaximumBitrateConstraints(..)
      int cbr_bitrate = params.mediaRenderer.getCBRVideoBitrate();
      String cbr_settings = (cbr_bitrate > 0) ?
          ":vrc_buf_size=5000:vrc_minrate=" + cbr_bitrate + ":vrc_maxrate=" + cbr_bitrate + ":vbitrate=" + ((cbr_bitrate > 16000) ? cbr_bitrate * 1000 : cbr_bitrate) :
          "";

      String encodeSettings = "-lavcopts autoaspect=1:vcodec=" + vcodec +
          (wmv && !params.mediaRenderer.isXBOX() ? ":acodec=wmav2:abitrate=448" : (cbr_settings + ":acodec=" + (configuration.isMencoderAc3Fixed() ? "ac3_fixed" : "ac3") +
              ":abitrate=" + CodecUtil.getAC3Bitrate(configuration, params.aid))) +
          ":threads=" + (wmv && !params.mediaRenderer.isXBOX() ? 1 : configuration.getMencoderMaxThreads()) +
          ("".equals(mpeg2Options) ? "" : ":" + mpeg2Options);

      String audioType = "ac3";
      if (dtsRemux) {
        audioType = "dts";
      } else if (pcm) {
        audioType = "pcm";
      }

      encodeSettings = addMaximumBitrateConstraints(encodeSettings, media, mpeg2Options, params.mediaRenderer, audioType);
      st = new StringTokenizer(encodeSettings, " ");

      {
        int i = overriddenMainArgs.length; // Old length
        overriddenMainArgs = Arrays.copyOf(overriddenMainArgs, overriddenMainArgs.length + st.countTokens());

        while (st.hasMoreTokens()) {
          overriddenMainArgs[i++] = st.nextToken();
        }
      }
    }

    boolean foundNoassParam = false;

    if (media != null) {
      String expertOptions [] = getSpecificCodecOptions(
        configuration.getMencoderCodecSpecificConfig(),
        media,
        params,
        filename,
        externalSubtitlesFileName,
        configuration.isMencoderIntelligentSync(),
        false
      );

      for (String s : expertOptions) {
        if (s.equals("-noass")) {
          foundNoassParam = true;
        }
      }
    }

    ArrayList<String> subtitleArgs = new ArrayList<String>();

    // Set subtitles options
    if (!configuration.isDisableSubtitles() && !avisynth() && params.sid != null) {
      int subtitleMargin = 0;
      int userMargin     = 0;

      // Use ASS flag (and therefore ASS font styles) for all subtitled files except vobsub, PGS and dvd
      boolean apply_ass_styling = params.sid.getType() != SubtitleType.VOBSUB &&
          params.sid.getType() != SubtitleType.PGS &&
          configuration.isMencoderAss() &&   // GUI: enable subtitles formating
          !foundNoassParam &&                // GUI: codec specific options
          !dvd;

      if (apply_ass_styling) {
        subtitleArgs.add("-ass");

        // GUI: Override ASS subtitles style if requested (always for SRT and TX3G subtitles)
        boolean override_ass_style = !configuration.isMencoderAssDefaultStyle() ||
            params.sid.getType() == SubtitleType.SUBRIP ||
            params.sid.getType() == SubtitleType.TX3G;

        if (override_ass_style) {
          String assSubColor = "ffffff00";

          if (configuration.getSubsColor() != 0) {
            assSubColor = Integer.toHexString(configuration.getSubsColor());
            if (assSubColor.length() > 2) {
              assSubColor = assSubColor.substring(2) + "00";
            }
          }

          subtitleArgs.add("-ass-color");
          subtitleArgs.add(assSubColor);
          subtitleArgs.add("-ass-border-color");
          subtitleArgs.add("00000000");
          subtitleArgs.add("-ass-font-scale");
          subtitleArgs.add(configuration.getAssScale());
          StringBuilder assForceStyle = new StringBuilder();

          // set subtitles font
          if (configuration.getFont() != null && configuration.getFont().length() > 0) {
            // set font with -font option, workaround for
            // https://github.com/Happy-Neko/ps3mediaserver/commit/52e62203ea12c40628de1869882994ce1065446a#commitcomment-990156 bug
            subtitleArgs.add("-font");
            subtitleArgs.add(configuration.getFont());
            assForceStyle.append("FontName=").append(quoteArg(configuration.getFont())).append(",");
          } else {
            String font = CodecUtil.getDefaultFontPath();
            if (isNotBlank(font)) {
              // Variable "font" contains a font path instead of a font name.
              // Does "-ass-force-style" support font paths? In tests on OS X
              // the font path is ignored (Outline, Shadow and MarginV are
              // used, though) and the "-font" definition is used instead.
              // See: https://github.com/ps3mediaserver/ps3mediaserver/pull/14
              subtitleArgs.add("-font");
              subtitleArgs.add(font);
              assForceStyle.append("FontName=").append(quoteArg(font)).append(",");
            } else {
              subtitleArgs.add("-font");
              subtitleArgs.add("Arial");
              assForceStyle.append("FontName=Arial,");
            }
          }

          // Add to the subtitle margin if overscan compensation is being used
          // This keeps the subtitle text inside the frame instead of in the border
          if (intOCH > 0) {
            subtitleMargin = (media.getHeight() / 100) * intOCH;
          }

          assForceStyle.append("Outline=").append(configuration.getAssOutline());
          assForceStyle.append(",Shadow=").append(configuration.getAssShadow());

          try {
            userMargin = Integer.parseInt(configuration.getAssMargin());
          } catch (NumberFormatException n) {
            logger.debug("Could not parse SSA margin from \"" + configuration.getAssMargin() + "\"");
          }

          subtitleMargin = subtitleMargin + userMargin;
          assForceStyle.append(",MarginV=").append(subtitleMargin);
          subtitleArgs.add("-ass-force-style");
          subtitleArgs.add(assForceStyle.toString());
        } else if (intOCH > 0) {
          subtitleArgs.add("-ass-force-style");
          subtitleArgs.add("MarginV=" + subtitleMargin);
        }

        // MEncoder is not compiled with fontconfig on Mac OS X, therefore
        // use of the "-ass" option also requires the "-font" option.
        if (Platform.isMac() && !subtitleArgs.contains("-font")) {
          String font = CodecUtil.getDefaultFontPath();

          if (isNotBlank(font)) {
            subtitleArgs.add("-font");
            subtitleArgs.add(font);
          }
        }

        // Workaround for MPlayer #2041, remove when that bug is fixed
        if (!params.sid.isEmbedded()) {
          subtitleArgs.add("-noflip-hebrew");
        }
      // use PLAINTEXT formating
      } else {
        // set subtitles font
        if (configuration.getFont() != null && configuration.getFont().length() > 0) {
          subtitleArgs.add("-font");
          subtitleArgs.add(configuration.getFont());
        } else {
          String font = CodecUtil.getDefaultFontPath();

          if (isNotBlank(font)) {
            subtitleArgs.add("-font");
            subtitleArgs.add(font);
          }
        }

        subtitleArgs.add("-subfont-text-scale");
        subtitleArgs.add(configuration.getMencoderNoAssScale());
        subtitleArgs.add("-subfont-outline");
        subtitleArgs.add(configuration.getMencoderNoAssOutline());
        subtitleArgs.add("-subfont-blur");
        subtitleArgs.add(configuration.getMencoderNoAssBlur());

        // Add to the subtitle margin if overscan compensation is being used
        // This keeps the subtitle text inside the frame instead of in the border
        if (intOCH > 0) {
          subtitleMargin = intOCH;
        }

        try {
          userMargin = Integer.parseInt(configuration.getMencoderNoAssSubPos());
        } catch (NumberFormatException n) {
          logger.debug("Could not parse subpos from \"" + configuration.getMencoderNoAssSubPos() + "\"");
        }

        subtitleMargin = subtitleMargin + userMargin;

        subtitleArgs.add("-subpos");
        subtitleArgs.add(String.valueOf(100 - subtitleMargin));
      }

      // Common subtitle options

      // MEncoder on Mac OS X is compiled without fontconfig support.
      // Appending the flag will break execution, so skip it on Mac OS X.
      if (!Platform.isMac()) {
        // Use fontconfig if enabled
        if (configuration.isMencoderFontConfig()) {
          subtitleArgs.add("-fontconfig");
        } else {
          subtitleArgs.add("-nofontconfig");
        }
      }

      // Apply DVD/VOBSUB subtitle quality
      if (params.sid.getType() == SubtitleType.VOBSUB && configuration.getMencoderVobsubSubtitleQuality() != null) {
        String subtitleQuality = configuration.getMencoderVobsubSubtitleQuality();

        subtitleArgs.add("-spuaa");
        subtitleArgs.add(subtitleQuality);
      }

      // external subtitles file
      if (params.sid.isExternal()) {
        if (!params.sid.isExternalFileUtf()) {
          String subcp = null;

          // append -subcp option for non UTF external subtitles
          if (isNotBlank(configuration.getSubtitlesCodepage())) {
            // manual setting
            subcp = configuration.getSubtitlesCodepage();
          } else if (isNotBlank(SubtitleUtils.getSubCpOptionForMencoder(params.sid))) {
            // autodetect charset (blank mencoder_subcp config option)
            subcp = SubtitleUtils.getSubCpOptionForMencoder(params.sid);
          }

          if (isNotBlank(subcp)) {
            subtitleArgs.add("-subcp");
            subtitleArgs.add(subcp);

            if (configuration.isMencoderSubFribidi()) {
              subtitleArgs.add("-fribidi-charset");
              subtitleArgs.add(subcp);
            }
          }
        }
      }
    }

    int index = overriddenMainArgs.length;
    overriddenMainArgs = Arrays.copyOf(overriddenMainArgs, overriddenMainArgs.length + subtitleArgs.size());

    for (String subtitleArg : subtitleArgs) {
      overriddenMainArgs[index] = subtitleArg;
      index++;
    }

    List<String> cmdList = new ArrayList<String>();

    cmdList.add(executable());

    // timeseek
    // XXX -ss 0 is is included for parity with the old (cmdArray) code: it may be possible to omit it
    cmdList.add("-ss");
    cmdList.add((params.timeseek > 0) ? "" + params.timeseek : "0");

    if (dvd) {
      cmdList.add("-dvd-device");
    }

    // input filename
    if (avisynth && !filename.toLowerCase().endsWith(".iso")) {
      File avsFile = FFmpegAviSynthVideo.getAVSScript(filename, params.sid, params.fromFrame, params.toFrame);
      cmdList.add(ProcessUtil.getShortFileNameIfWideChars(avsFile.getAbsolutePath()));
    } else {
      if (params.stdin != null) {
        cmdList.add("-");
      } else {
        cmdList.add(filename);
      }
    }

    if (dvd) {
      cmdList.add("dvd://" + media.getDvdtrack());
    }

    for (String arg : args()) {
      if (arg.contains("format=mpeg2") && media.getAspect() != null && media.getValidAspect(true) != null) {
        cmdList.add(arg + ":vaspect=" + media.getValidAspect(true));
      } else {
        cmdList.add(arg);
      }
    }

    if (!dtsRemux && !pcm && !avisynth() && params.aid != null && media.getAudioTracksList().size() > 1) {
      cmdList.add("-aid");
      boolean lavf = false; // TODO Need to add support for LAVF demuxing
      cmdList.add("" + (lavf ? params.aid.getId() + 1 : params.aid.getId()));
    }

    /*
     * handle subtitles
     *
     * try to reconcile the fact that the handling of "Disable subtitles" is spread out
     * over net.pms.encoders.Player.setAudioAndSubs and here by setting both of MEncoder's "disable
     * subs" options if any of the internal conditions for disabling subtitles are met.
     */
    if (isDisableSubtitles(params)) {
      // Ensure that internal subtitles are not automatically loaded
      // MKV: in some circumstances, MEncoder automatically selects an internal sub unless we explicitly disable (internal) subtitles
      // http://www.ps3mediaserver.org/forum/viewtopic.php?f=14&t=15891
      cmdList.add("-nosub");
      // Ensure that external subtitles are not automatically loaded
      cmdList.add("-noautosub");
    } else {
      // note: isEmbedded() and isExternal() are mutually exclusive
      if (params.sid.isEmbedded()) { // internal (embedded) subs
        // Ensure that external subtitles are not automatically loaded
        cmdList.add("-noautosub");
        // Specify which internal subtitle we want
        cmdList.add("-sid");
        cmdList.add("" + params.sid.getId());
      } else if (externalSubtitlesFileName != null) { // external subtitles
        assert params.sid.isExternal(); // confirm the mutual exclusion

        // Ensure that internal subtitles are not automatically loaded
        cmdList.add("-nosub");

        if (params.sid.getType() == SubtitleType.VOBSUB) {
          cmdList.add("-vobsub");
          cmdList.add(externalSubtitlesFileName.substring(0, externalSubtitlesFileName.length() - 4));
          cmdList.add("-slang");
          cmdList.add("" + params.sid.getLang());
        } else {
          cmdList.add("-sub");
          cmdList.add(externalSubtitlesFileName.replace(",", "\\,")); // Commas in MEncoder separate multiple subtitle files

          if (params.sid.isExternalFileUtf()) {
            // append -utf8 option for UTF-8 external subtitles
            cmdList.add("-utf8");
          }
        }
      }
    }

    // -ofps
    String validFramerate = (media != null) ? media.getValidFps(true) : null; // optional input framerate: may be null
    String framerate = (validFramerate != null) ? validFramerate : "24000/1001"; // where a framerate is required, use the input framerate or 24000/1001
    String ofps = framerate;

    // optional -fps or -mc
    if (configuration.isMencoderForceFps()) {
      if (!configuration.isFix25FPSAvMismatch()) {
        cmdList.add("-fps");
        cmdList.add(framerate);
      } else if (validFramerate != null) { // XXX not sure why this "fix" requires the input to have a valid framerate, but that's the logic in the old (cmdArray) code
        cmdList.add("-mc");
        cmdList.add("0.005");
        ofps = "25";
      }
    }

    cmdList.add("-ofps");
    cmdList.add(ofps);

    /*
     * TODO: Move the following block up with the rest of the
     * subtitle stuff
     */
    // external subtitles file
    if (!configuration.isDisableSubtitles() && !avisynth() && params.sid != null && params.sid.isExternal()) {
      if (params.sid.getType() == SubtitleType.VOBSUB) {
        cmdList.add("-vobsub");
        cmdList.add(externalSubtitlesFileName.substring(0, externalSubtitlesFileName.length() - 4));
        cmdList.add("-slang");
        cmdList.add("" + params.sid.getLang());
      } else {
        cmdList.add("-sub");
        cmdList.add(externalSubtitlesFileName.replace(",", "\\,")); // Commas in MEncoder separate multiple subtitle files

        if (params.sid.isExternalFileUtf()) {
          // append -utf8 option for UTF-8 external subtitles
          cmdList.add("-utf8");
        }
      }
    }

    if (filename.toLowerCase().endsWith(".evo")) {
      cmdList.add("-psprobe");
      cmdList.add("10000");
    }

    boolean deinterlace = configuration.isMencoderYadif();

    // Check if the media renderer supports this resolution
    boolean isResolutionTooHighForRenderer = params.mediaRenderer.isVideoRescale()
      && media != null
      && (
        (media.getWidth() > params.mediaRenderer.getMaxVideoWidth())
        ||
        (media.getHeight() > params.mediaRenderer.getMaxVideoHeight())
      );

    // Video scaler and overscan compensation
    boolean scaleBool = isResolutionTooHighForRenderer
      || (configuration.isMencoderScaler() && (configuration.getMencoderScaleX() != 0 || configuration.getMencoderScaleY() != 0))
      || (intOCW > 0 || intOCH > 0);

    if ((deinterlace || scaleBool) && !avisynth()) {
      StringBuilder vfValueOverscanPrepend = new StringBuilder();
      StringBuilder vfValueOverscanMiddle  = new StringBuilder();
      StringBuilder vfValueVS              = new StringBuilder();
      StringBuilder vfValueComplete        = new StringBuilder();

      String deinterlaceComma = "";
      int scaleWidth = 0;
      int scaleHeight = 0;
      double rendererAspectRatio;

      // Set defaults
      if (media != null && media.getWidth() > 0 && media.getHeight() > 0) {
        scaleWidth = media.getWidth();
        scaleHeight = media.getHeight();
      }

      /*
       * Implement overscan compensation settings
       *
       * This feature takes into account aspect ratio,
       * making it less blunt than the Video Scaler option
       */
      if (intOCW > 0 || intOCH > 0) {
        int intOCWPixels = (media.getWidth()  / 100) * intOCW;
        int intOCHPixels = (media.getHeight() / 100) * intOCH;

        scaleWidth  = scaleWidth  + intOCWPixels;
        scaleHeight = scaleHeight + intOCHPixels;

        // See if the video needs to be scaled down
        if (
          params.mediaRenderer.isVideoRescale() &&
          (
            (scaleWidth > params.mediaRenderer.getMaxVideoWidth()) ||
            (scaleHeight > params.mediaRenderer.getMaxVideoHeight())
          )
        ) {
          double overscannedAspectRatio = scaleWidth / scaleHeight;
          rendererAspectRatio = params.mediaRenderer.getMaxVideoWidth() / params.mediaRenderer.getMaxVideoHeight();

          if (overscannedAspectRatio > rendererAspectRatio) {
            // Limit video by width
            scaleWidth  = params.mediaRenderer.getMaxVideoWidth();
            scaleHeight = (int) Math.round(params.mediaRenderer.getMaxVideoWidth() / overscannedAspectRatio);
          } else {
            // Limit video by height
            scaleWidth  = (int) Math.round(params.mediaRenderer.getMaxVideoHeight() * overscannedAspectRatio);
            scaleHeight = params.mediaRenderer.getMaxVideoHeight();
          }
        }

        vfValueOverscanPrepend.append("softskip,expand=-").append(intOCWPixels).append(":-").append(intOCHPixels);
        vfValueOverscanMiddle.append(",scale=").append(scaleWidth).append(":").append(scaleHeight);
      }

      /*
       * Video Scaler and renderer-specific resolution-limiter
       */
      if (configuration.isMencoderScaler()) {
        // Use the manual, user-controlled scaler
        if (configuration.getMencoderScaleX() != 0) {
          if (configuration.getMencoderScaleX() <= params.mediaRenderer.getMaxVideoWidth()) {
            scaleWidth = configuration.getMencoderScaleX();
          } else {
            scaleWidth = params.mediaRenderer.getMaxVideoWidth();
          }
        }

        if (configuration.getMencoderScaleY() != 0) {
          if (configuration.getMencoderScaleY() <= params.mediaRenderer.getMaxVideoHeight()) {
            scaleHeight = configuration.getMencoderScaleY();
          } else {
            scaleHeight = params.mediaRenderer.getMaxVideoHeight();
          }
        }

        logger.info("Setting video resolution to: " + scaleWidth + "x" + scaleHeight + ", your Video Scaler setting");

        vfValueVS.append("scale=").append(scaleWidth).append(":").append(scaleHeight);

      /*
       * The video resolution is too big for the renderer so we need to scale it down
       */
      } else if (
        media != null &&
        media.getWidth() > 0 &&
        media.getHeight() > 0 &&
        (
          media.getWidth()  > params.mediaRenderer.getMaxVideoWidth() ||
          media.getHeight() > params.mediaRenderer.getMaxVideoHeight()
        )
      ) {
        double videoAspectRatio = (double) media.getWidth() / (double) media.getHeight();
        rendererAspectRatio = (double) params.mediaRenderer.getMaxVideoWidth() / (double) params.mediaRenderer.getMaxVideoHeight();

        /*
         * First we deal with some exceptions, then if they are not matched we will
         * let the renderer limits work.
         *
         * This is so, for example, we can still define a maximum resolution of
         * 1920x1080 in the renderer config file but still support 1920x1088 when
         * it's needed, otherwise we would either resize 1088 to 1080, meaning the
         * ugly (unused) bottom 8 pixels would be displayed, or we would limit all
         * videos to 1088 causing the bottom 8 meaningful pixels to be cut off.
         */
        if (media.getWidth() == 3840 && media.getHeight() == 1080) {
          // Full-SBS
          scaleWidth  = 1920;
          scaleHeight = 1080;
        } else if (media.getWidth() == 1920 && media.getHeight() == 2160) {
          // Full-OU
          scaleWidth  = 1920;
          scaleHeight = 1080;
        } else if (media.getWidth() == 1920 && media.getHeight() == 1088) {
          // SAT capture
          scaleWidth  = 1920;
          scaleHeight = 1088;
        } else {
          // Passed the exceptions, now we allow the renderer to define the limits
          if (videoAspectRatio > rendererAspectRatio) {
            scaleWidth  = params.mediaRenderer.getMaxVideoWidth();
            scaleHeight = (int) Math.round(params.mediaRenderer.getMaxVideoWidth() / videoAspectRatio);
          } else {
            scaleWidth  = (int) Math.round(params.mediaRenderer.getMaxVideoHeight() * videoAspectRatio);
            scaleHeight = params.mediaRenderer.getMaxVideoHeight();
          }
        }

        logger.info("Setting video resolution to: " + scaleWidth + "x" + scaleHeight + ", the maximum your renderer supports");

        vfValueVS.append("scale=").append(scaleWidth).append(":").append(scaleHeight);
      }

      // Put the string together taking into account overscan compensation and video scaler
      if (intOCW > 0 || intOCH > 0) {
        vfValueComplete.append(vfValueOverscanPrepend).append(vfValueOverscanMiddle).append(",harddup");
        logger.info("Setting video resolution to: " + scaleWidth + "x" + scaleHeight + ", to fit your overscan compensation");
      } else {
        vfValueComplete.append(vfValueVS);
      }

      if (deinterlace) {
        deinterlaceComma = ",";
      }

      String vfValue = (deinterlace ? "yadif" : "") + (scaleBool ? deinterlaceComma + vfValueComplete : "");

      if (isNotBlank(vfValue)) {
        cmdList.add("-vf");
        cmdList.add(vfValue);
      }
    }

    /*
     * The PS3 and possibly other renderers display videos incorrectly
     * if the dimensions aren't divisible by 4, so if that is the
     * case we scale it down to the nearest 4.
     * This fixes the long-time bug of videos displaying in black and
     * white with diagonal strips of colour, weird one.
     *
     * TODO: Integrate this with the other stuff so that "scale" only
     * ever appears once in the MEncoder CMD.
     */
    if (media != null && (media.getWidth() % 4 != 0) || media.getHeight() % 4 != 0) {
      int newWidth;
      int newHeight;

      newWidth  = (media.getWidth() / 4) * 4;
      newHeight = (media.getHeight() / 4) * 4;

      cmdList.add("-vf");
      cmdList.add("softskip,expand=" + newWidth + ":" + newHeight);
    }

    if (configuration.getMencoderMT() && !avisynth && !dvd && !(startsWith(media.getCodecV(), "mpeg2"))) {
      cmdList.add("-lavdopts");
      cmdList.add("fast");
    }

    boolean disableMc0AndNoskip = false;

    // Process the options for this file in Transcoding Settings -> Mencoder -> Expert Settings: Codec-specific parameters
    // TODO this is better handled by a plugin with scripting support and will be removed
    if (media != null) {
      String expertOptions[] = getSpecificCodecOptions(
        configuration.getMencoderCodecSpecificConfig(),
        media,
        params,
        filename,
        externalSubtitlesFileName,
        configuration.isMencoderIntelligentSync(),
        false
      );

      // the parameters (expertOptions) are processed in 3 passes
      // 1) process expertOptions
      // 2) process cmdList
      // 3) append expertOptions to cmdList

      if (expertOptions != null && expertOptions.length > 0) {
        // remove this option (key) from the cmdList in pass 2.
        // if the boolean value is true, also remove the option's corresponding value
        Map<String, Boolean> removeCmdListOption = new HashMap<String, Boolean>();

        // if this option (key) is defined in cmdList, merge this string value into the
        // option's value in pass 2. the value is a string format template into which the
        // cmdList option value is injected
        Map<String, String> mergeCmdListOption = new HashMap<String, String>();

        // merges that are performed in pass 2 are logged in this map; the key (string) is
        // the option name and the value is a boolean indicating whether the option was merged
        // or not. the map is populated after pass 1 with the options from mergeCmdListOption
        // and all values initialised to false. if an option was merged, it is not appended
        // to cmdList
        Map<String, Boolean> mergedCmdListOption = new HashMap<String, Boolean>();

        // pass 1: process expertOptions
        for (int i = 0; i < expertOptions.length; ++i) {
          if (expertOptions[i].equals("-noass")) {
            // remove -ass from cmdList in pass 2.
            // -ass won't have been added in this method (getSpecificCodecOptions
            // has been called multiple times above to check for -noass and -nomux)
            // but it may have been added via the renderer or global MEncoder options.
            // XXX: there are currently 10 other -ass options (-ass-color, -ass-border-color &c.).
            // technically, they should all be removed...
            removeCmdListOption.put("-ass", false); // false: option does not have a corresponding value
            // remove -noass from expertOptions in pass 3
            expertOptions[i] = REMOVE_OPTION;
          } else if (expertOptions[i].equals("-nomux")) {
            expertOptions[i] = REMOVE_OPTION;
          } else if (expertOptions[i].equals("-mt")) {
            // not an MEncoder option so remove it from exportOptions.
            // multi-threaded MEncoder is used by default, so this is obsolete (TODO: Remove it from the description)
            expertOptions[i] = REMOVE_OPTION;
          } else if (expertOptions[i].equals("-ofps")) {
            // replace the cmdList version with the expertOptions version i.e. remove the former
            removeCmdListOption.put("-ofps", true);
            // skip (i.e. leave unchanged) the exportOptions value
            ++i;
          } else if (expertOptions[i].equals("-fps")) {
            removeCmdListOption.put("-fps", true);
            ++i;
          } else if (expertOptions[i].equals("-ovc")) {
            removeCmdListOption.put("-ovc", true);
            ++i;
          } else if (expertOptions[i].equals("-channels")) {
            removeCmdListOption.put("-channels", true);
            ++i;
          } else if (expertOptions[i].equals("-oac")) {
            removeCmdListOption.put("-oac", true);
            ++i;
          } else if (expertOptions[i].equals("-quality")) {
            // XXX like the old (cmdArray) code, this clobbers the old -lavcopts value
            String lavcopts = String.format(
              "autoaspect=1:vcodec=%s:acodec=%s:abitrate=%s:threads=%d:%s",
              vcodec,
              (configuration.isMencoderAc3Fixed() ? "ac3_fixed" : "ac3"),
              CodecUtil.getAC3Bitrate(configuration, params.aid),
              configuration.getMencoderMaxThreads(),
              expertOptions[i + 1]
            );

            // append bitrate-limiting options if configured
            lavcopts = addMaximumBitrateConstraints(
              lavcopts,
              media,
              lavcopts,
              params.mediaRenderer,
              ""
            );

            // a string format with no placeholders, so the cmdList option value is ignored.
            // note: we protect "%" from being interpreted as a format by converting it to "%%",
            // which is then turned back into "%" when the format is processed
            mergeCmdListOption.put("-lavcopts", lavcopts.replace("%", "%%"));
            // remove -quality <value>
            expertOptions[i] = expertOptions[i + 1] = REMOVE_OPTION;
            ++i;
          } else if (expertOptions[i].equals("-mpegopts")) {
            mergeCmdListOption.put("-mpegopts", "%s:" + expertOptions[i + 1].replace("%", "%%"));
            // merge if cmdList already contains -mpegopts, but don't append if it doesn't (parity with the old (cmdArray) version)
            expertOptions[i] = expertOptions[i + 1] = REMOVE_OPTION;
            ++i;
          } else if (expertOptions[i].equals("-vf")) {
            mergeCmdListOption.put("-vf", "%s," + expertOptions[i + 1].replace("%", "%%"));
            ++i;
          } else if (expertOptions[i].equals("-af")) {
            mergeCmdListOption.put("-af", "%s," + expertOptions[i + 1].replace("%", "%%"));
            ++i;
          } else if (expertOptions[i].equals("-nosync")) {
            disableMc0AndNoskip = true;
            expertOptions[i] = REMOVE_OPTION;
          } else if (expertOptions[i].equals("-mc")) {
            disableMc0AndNoskip = true;
          }
        }

        for (String key : mergeCmdListOption.keySet()) {
          mergedCmdListOption.put(key, false);
        }

        // pass 2: process cmdList
        List<String> transformedCmdList = new ArrayList<String>();

        for (int i = 0; i < cmdList.size(); ++i) {
          String option = cmdList.get(i);

          // we remove an option by *not* adding it to transformedCmdList
          if (removeCmdListOption.containsKey(option)) {
            if (isTrue(removeCmdListOption.get(option))) { // true: remove (i.e. don't add) the corresponding value
              ++i;
            }
          } else {
            transformedCmdList.add(option);

            if (mergeCmdListOption.containsKey(option)) {
              String format = mergeCmdListOption.get(option);
              String value = String.format(format, cmdList.get(i + 1));
              // record the fact that an expertOption value has been merged into this cmdList value
              mergedCmdListOption.put(option, true);
              transformedCmdList.add(value);
              ++i;
            }
          }
        }

        cmdList = transformedCmdList;

        // pass 3: append expertOptions to cmdList
        for (int i = 0; i < expertOptions.length; ++i) {
          String option = expertOptions[i];

          if (option != REMOVE_OPTION) {
            if (isTrue(mergedCmdListOption.get(option))) { // true: this option and its value have already been merged into existing cmdList options
              ++i; // skip the value
            } else {
              cmdList.add(option);
            }
          }
        }
      }
    }

    if ((pcm || dtsRemux || ac3Remux) || (configuration.isMencoderNoOutOfSync() && !disableMc0AndNoskip)) {
      if (configuration.isFix25FPSAvMismatch()) {
        cmdList.add("-mc");
        cmdList.add("0.005");
      } else {
        cmdList.add("-mc");
        cmdList.add("0");
        cmdList.add("-noskip");
      }
    }

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

    String rate = "48000";
    if (params.mediaRenderer.isXBOX()) {
      rate = "44100";
    }

    // force srate -> cause ac3's mencoder doesn't like anything other than 48khz
    if (media != null && !pcm && !dtsRemux && !ac3Remux) {
      cmdList.add("-af");
      cmdList.add("lavcresample=" + rate);
      cmdList.add("-srate");
      cmdList.add(rate);
    }

    // add a -cache option for piped media (e.g. rar/zip file entries):
    // https://code.google.com/p/ps3mediaserver/issues/detail?id=911
    if (params.stdin != null) {
      cmdList.add("-cache");
      cmdList.add("8192");
    }

    PipeProcess pipe = null;

    ProcessWrapperImpl pw = null;

    if (pcm || dtsRemux) {
      // transcode video, demux audio, remux with tsmuxer
      boolean channels_filter_present = false;

      for (String s : cmdList) {
        if (isNotBlank(s) && s.startsWith("channels")) {
          channels_filter_present = true;
          break;
        }
      }

      if (params.avidemux) {
        pipe = new PipeProcess("mencoder" + System.currentTimeMillis(), (pcm || dtsRemux || ac3Remux) ? null : params);
        params.input_pipes[0] = pipe;

        cmdList.add("-o");
        cmdList.add(pipe.getInputPipe());

        if (pcm && !channels_filter_present && params.aid != null) {
          String mixer = getLPCMChannelMappingForMencoder(params.aid);
          if (isNotBlank(mixer)) {
            cmdList.add("-af");
            cmdList.add(mixer);
          }
        }

        String[] cmdArray = new String[cmdList.size()];
        cmdList.toArray(cmdArray);
        pw = new ProcessWrapperImpl(cmdArray, params);

        PipeProcess videoPipe = new PipeProcess("videoPipe" + System.currentTimeMillis(), "out", "reconnect");
        PipeProcess audioPipe = new PipeProcess("audioPipe" + System.currentTimeMillis(), "out", "reconnect");

        ProcessWrapper videoPipeProcess = videoPipe.getPipeProcess();
        ProcessWrapper audioPipeProcess = audioPipe.getPipeProcess();

        params.output_pipes[0] = videoPipe;
        params.output_pipes[1] = audioPipe;

        pw.attachProcess(videoPipeProcess);
        pw.attachProcess(audioPipeProcess);
        videoPipeProcess.runInNewThread();
        audioPipeProcess.runInNewThread();
        try {
          Thread.sleep(50);
        } catch (InterruptedException e) { }
        videoPipe.deleteLater();
        audioPipe.deleteLater();
      } else {
        // remove the -oac switch, otherwise the "too many video packets" errors appear again
        for (ListIterator<String> it = cmdList.listIterator(); it.hasNext();) {
          String option = it.next();

          if (option.equals("-oac")) {
            it.set("-nosound");

            if (it.hasNext()) {
              it.next();
              it.remove();
            }

            break;
          }
        }

        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("-o");
        cmdList.add(ffVideoPipe.getInputPipe());

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

        String[] cmdArray = new String[cmdList.size()];
        cmdList.toArray(cmdArray);
        ProcessWrapperImpl ffVideo = new ProcessWrapperImpl(cmdArray, 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();

        String aid = null;
        if (media != null && media.getAudioTracksList().size() > 1 && params.aid != null) {
          if (media.getContainer() != null && (media.getContainer().equals(FormatConfiguration.AVI) || media.getContainer().equals(FormatConfiguration.FLV))) {
            // TODO confirm (MP4s, OGMs and MOVs already tested: first aid is 0; AVIs: first aid is 1)
            // for AVIs, FLVs and MOVs mencoder starts audio tracks numbering from 1
            aid = "" + (params.aid.getId() + 1);
          } else {
            // everything else from 0
            aid = "" + params.aid.getId();
          }
        }

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

        String mixer = null;
        if (pcm && !dtsRemux) {
          mixer = getLPCMChannelMappingForMencoder(params.aid); // LPCM always outputs 5.1/7.1 for multichannel tracks. Downmix with player if needed!
        }

        sm.setNbChannels(channels);

        // it seems the -really-quiet prevents mencoder to stop the pipe output after some time...
        // -mc 0.1 make the DTS-HD extraction works better with latest mencoder builds, and makes no impact on the regular DTS one
        String ffmpegLPCMextract[] = new String[]{
          executable(),
          "-ss", "0",
          filename,
          "-really-quiet",
          "-msglevel", "statusline=2",
          "-channels", "" + channels,
          "-ovc", "copy",
          "-of", "rawaudio",
          "-mc", dtsRemux ? "0.1" : "0",
          "-noskip",
          (aid == null) ? "-quiet" : "-aid", (aid == null) ? "-quiet" : aid,
          "-oac", (ac3Remux || dtsRemux) ? "copy" : "pcm",
          (isNotBlank(mixer) && !channels_filter_present) ? "-af" : "-quiet", (isNotBlank(mixer) && !channels_filter_present) ? mixer : "-quiet",
          "-srate", "48000",
          "-o", ffAudioPipe.getInputPipe()
        };

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

        if (media != null && media.getDvdtrack() > 0) {
          ffmpegLPCMextract[3] = "-dvd-device";
          ffmpegLPCMextract[4] = filename;
          ffmpegLPCMextract[5] = "dvd://" + media.getDvdtrack();
        } else if (params.stdin != null) {
          ffmpegLPCMextract[3] = "-";
        }

        if (filename.toLowerCase().endsWith(".evo")) {
          ffmpegLPCMextract[4] = "-psprobe";
          ffmpegLPCMextract[5] = "1000000";
        }

        if (params.timeseek > 0) {
          ffmpegLPCMextract[2] = "" + params.timeseek;
        }

        OutputParams ffaudioparams = new OutputParams(configuration);
        ffaudioparams.maxBufferSize = 1;
        ffaudioparams.stdin = params.stdin;
        ProcessWrapperImpl ffAudio = new ProcessWrapperImpl(ffmpegLPCMextract, 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 (params.no_videoencode && params.forceType != null) {
          videoType = params.forceType;
        }

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

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


        // mencoder bug (confirmed with mencoder r35003 + ffmpeg 0.11.1):
        // audio delay is ignored when playing from file start (-ss 0)
        // override with tsmuxer.meta setting
        String timeshift = "";
        if (mencoderAC3RemuxAudioDelayBug) {
          timeshift = "timeshift=" + params.aid.getAudioProperties().getAudioDelay() + "ms, ";
        }

        pwMux.println(videoType + ", \"" + ffVideoPipe.getOutputPipe() + "\", " + fps + "level=4.1, insertSEI, contSPS, track=1");
        pwMux.println(audioType + ", \"" + ffAudioPipe.getOutputPipe() + "\", " + timeshift + "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();
      }
    } else {
      boolean directpipe = Platform.isMac() || Platform.isFreeBSD();

      if (directpipe) {
        cmdList.add("-o");
        cmdList.add("-");
        cmdList.add("-really-quiet");
        cmdList.add("-msglevel");
        cmdList.add("statusline=2");
        params.input_pipes = new PipeProcess[2];
      } else {
        pipe = new PipeProcess("mencoder" + System.currentTimeMillis(), (pcm || dtsRemux) ? null : params);
        params.input_pipes[0] = pipe;
        cmdList.add("-o");
        cmdList.add(pipe.getInputPipe());
      }

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

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

      pw = new ProcessWrapperImpl(cmdArray, params);

      if (!directpipe) {
        ProcessWrapper mkfifo_process = pipe.getPipeProcess();
        pw.attachProcess(mkfifo_process);

        // It can take a long time for Windows to create a named pipe (and
        // mkfifo can be slow if /tmp isn't memory-mapped), so run this in
        // the current thread.
        mkfifo_process.runInSameThread();

        pipe.deleteLater();
      }
    }

    pw.runInNewThread();

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

    return pw;
  }

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

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

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

  private String[] getSpecificCodecOptions(
    String codecParam,
    DLNAMediaInfo media,
    OutputParams params,
    String filename,
    String externalSubtitlesFileName,
    boolean enable,
    boolean verifyOnly
  ) {
    StringBuilder sb = new StringBuilder();
    String codecs = enable ? DEFAULT_CODEC_CONF_SCRIPT : "";
    codecs += "\n" + codecParam;
    StringTokenizer stLines = new StringTokenizer(codecs, "\n");

    try {
      Interpreter interpreter = new Interpreter();
      interpreter.setStrictJava(true);
      ArrayList<String> types = CodecUtil.getPossibleCodecs();
      int rank = 1;

      if (types != null) {
        for (String type : types) {
          int r = rank++;
          interpreter.set("" + type, r);
          String secondaryType = "dummy";

          if ("matroska".equals(type)) {
            secondaryType = "mkv";
            interpreter.set(secondaryType, r);
          } else if ("rm".equals(type)) {
            secondaryType = "rmvb";
            interpreter.set(secondaryType, r);
          } else if ("mpeg2video".equals(type)) {
            secondaryType = "mpeg2";
            interpreter.set(secondaryType, r);
          } else if ("mpeg1video".equals(type)) {
            secondaryType = "mpeg1";
            interpreter.set(secondaryType, r);
          }

          if (media.getContainer() != null && (media.getContainer().equals(type) || media.getContainer().equals(secondaryType))) {
            interpreter.set("container", r);
          } else if (media.getCodecV() != null && (media.getCodecV().equals(type) || media.getCodecV().equals(secondaryType))) {
            interpreter.set("vcodec", r);
          } else if (params.aid != null && params.aid.getCodecA() != null && params.aid.getCodecA().equals(type)) {
            interpreter.set("acodec", r);
          }
        }
      } else {
        return null;
      }

      interpreter.set("filename", filename);
      interpreter.set("audio", params.aid != null);
      interpreter.set("subtitles", params.sid != null);
      interpreter.set("srtfile", externalSubtitlesFileName);

      if (params.aid != null) {
        interpreter.set("samplerate", params.aid.getSampleRate());
      }

      String framerate = media.getValidFps(false);

      try {
        if (framerate != null) {
          interpreter.set("framerate", Double.parseDouble(framerate));
        }
      } catch (NumberFormatException e) {
        logger.debug("Could not parse framerate from \"" + framerate + "\"");
      }

      interpreter.set("duration", media.getDurationInSeconds());

      if (params.aid != null) {
        interpreter.set("channels", params.aid.getAudioProperties().getNumberOfChannels());
      }

      interpreter.set("height", media.getHeight());
      interpreter.set("width", media.getWidth());

      while (stLines.hasMoreTokens()) {
        String line = stLines.nextToken();

        if (!line.startsWith("#") && line.trim().length() > 0) {
          int separator = line.indexOf("::");

          if (separator > -1) {
            String key = null;

            try {
              key = line.substring(0, separator).trim();
              String value = line.substring(separator + 2).trim();

              if (value.length() > 0) {
                if (key.length() == 0) {
                  key = "1 == 1";
                }

                Object result = interpreter.eval(key);

                if (result != null && result instanceof Boolean && (Boolean) result) {
                  sb.append(" ");
                  sb.append(value);
                }
              }
            } catch (Throwable e) {
              logger.debug("Error while executing: " + key + " : " + e.getMessage());

              if (verifyOnly) {
                return new String[]{"@@Error while parsing: " + e.getMessage()};
              }
            }
          } else if (verifyOnly) {
            return new String[]{"@@Malformatted line: " + line};
          }
        }
      }
    } catch (EvalError e) {
      logger.debug("BeanShell error: " + e.getMessage());
    }

    String completeLine = sb.toString();
    ArrayList<String> args = new ArrayList<String>();
    StringTokenizer st = new StringTokenizer(completeLine, " ");

    while (st.hasMoreTokens()) {
      String arg = st.nextToken().trim();

      if (arg.length() > 0) {
        args.add(arg);
      }
    }

    String definitiveArgs[] = new String[args.size()];
    args.toArray(definitiveArgs);

    return definitiveArgs;
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public boolean isCompatible(DLNAResource resource) {
    return PlayerUtil.isVideo(resource, Format.Identifier.ISO)
      || PlayerUtil.isVideo(resource, Format.Identifier.MKV)
      || PlayerUtil.isVideo(resource, Format.Identifier.MPG);
  }
}
TOP

Related Classes of net.pms.encoders.MEncoderVideo

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.