net.pms.encoders.MEncoderVideo.java Source code

Java tutorial

Introduction

Here is the source code for net.pms.encoders.MEncoderVideo.java

Source

/*
 * 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 bsh.EvalError;
import bsh.Interpreter;

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

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.*;
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 javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.util.List;

import static net.pms.formats.v2.AudioUtils.getLPCMChannelMappingForMencoder;
import static org.apache.commons.lang3.BooleanUtils.isTrue;
import static org.apache.commons.lang3.StringUtils.*;

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);
        builder.setBorder(Borders.EMPTY_BORDER);
        builder.setOpaque(false);

        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;
    }

    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;
                }
            }
        }

        StringBuilder sb = new StringBuilder();
        // 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) {
                sb.append("-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";
                        }
                    }

                    sb.append("-ass-color ").append(assSubColor)
                            .append(" -ass-border-color 00000000 -ass-font-scale ")
                            .append(configuration.getAssScale());

                    // 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
                        sb.append(" -font ").append(configuration.getFont()).append(" ");
                        sb.append(" -ass-force-style FontName=").append(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
                            sb.append(" -font ").append(font).append(" ");
                            sb.append(" -ass-force-style FontName=").append(font).append(",");
                        } else {
                            sb.append(" -font Arial ");
                            sb.append(" -ass-force-style 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;
                    }

                    sb.append("Outline=").append(configuration.getAssOutline()).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;

                    sb.append(",MarginV=").append(subtitleMargin).append(" ");
                } else if (intOCH > 0) {
                    sb.append("-ass-force-style MarginV=").append(subtitleMargin).append(" ");
                }

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

                    if (isNotBlank(font)) {
                        sb.append("-font ").append(font).append(" ");
                    }
                }

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

                sb.append(" -subfont-text-scale ").append(configuration.getMencoderNoAssScale());
                sb.append(" -subfont-outline ").append(configuration.getMencoderNoAssOutline());
                sb.append(" -subfont-blur ").append(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;

                sb.append(" -subpos ").append(100 - subtitleMargin).append(" ");
            }

            // 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
                sb.append("-").append(configuration.isMencoderFontConfig() ? "" : "no").append("fontconfig ");
            }
            // Apply DVD/VOBSUB subtitle quality
            if (params.sid.getType() == SubtitleType.VOBSUB
                    && configuration.getMencoderVobsubSubtitleQuality() != null) {
                String subtitleQuality = configuration.getMencoderVobsubSubtitleQuality();

                sb.append("-spuaa ").append(subtitleQuality).append(" ");
            }

            // 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)) {
                        sb.append("-subcp ").append(subcp).append(" ");
                        if (configuration.isMencoderSubFribidi()) {
                            sb.append("-fribidi-charset ").append(subcp).append(" ");
                        }
                    }
                }
            }
        }

        st = new StringTokenizer(sb.toString(), " ");

        {
            int i = overriddenMainArgs.length; // old length
            overriddenMainArgs = Arrays.copyOf(overriddenMainArgs, overriddenMainArgs.length + st.countTokens());
            boolean handleToken = false;

            while (st.hasMoreTokens()) {
                String s = st.nextToken();

                if (handleToken) {
                    s = "-quiet";
                    handleToken = false;
                }

                if ((!configuration.isMencoderAss() || dvd) && s.contains("-ass")) {
                    s = "-quiet";
                    handleToken = true;
                }

                overriddenMainArgs[i++] = s;
            }
        }

        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);
                mkfifo_process.runInNewThread();

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

                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);
    }
}