net.pms.encoders.FFmpegVideo.java Source code

Java tutorial

Introduction

Here is the source code for net.pms.encoders.FFmpegVideo.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 com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.factories.Borders;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;

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

import org.apache.commons.lang3.StringUtils;

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

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

import javax.swing.*;

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

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

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

    private JCheckBox multiThreadingCheckBox;
    private JCheckBox videoRemuxCheckBox;

    @Deprecated
    public FFmpegVideo() {
        this(PMS.getConfiguration());
    }

    public FFmpegVideo(PmsConfiguration configuration) {
        super(configuration);
        this.configuration = configuration;
    }

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

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

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

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

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

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

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

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

        String rescaleSpec = null;

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

        String overrideVF = renderer.getFFmpegVideoFilterOverride();

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

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

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

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

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

        return options;
    }

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

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

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

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

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

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

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

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

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

        return options;
    }

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

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

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

        return options;
    }

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

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

        return options;
    }

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

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

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

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

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

        return options;
    }

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

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

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

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

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

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

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

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

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

        return defaultArgsArray;
    }

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

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

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

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

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

        return bitrates;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params);

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

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

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

            cmdList.add(ffVideoPipe.getInputPipe());

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

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

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

            ProcessWrapperImpl ffVideo = new ProcessWrapperImpl(cmdArrayDts, ffparams);

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

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

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

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

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

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

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

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

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

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

            cmdListDTS.add(ffAudioPipe.getInputPipe());

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

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

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

            params.stdin = null;

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

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

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

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

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

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

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

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

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

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

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

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

        pw.runInNewThread();
        return pw;
    }

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

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

        CellConstraints cc = new CellConstraints();

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

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

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

        return builder.getPanel();
    }

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

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

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

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

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

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

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

        return tempSubs;
    }

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

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

        if (params.sid.isEmbedded()) {
            cmdList.add("-map");
            /* TODO broken code. Consider following example file:
               Stream #0:0(eng): Video: h264 (High), yuv420p, 720x576, SAR 178:139 DAR 445:278, 25 fps, 25 tbr, 1k tbn, 50 tbc (default)
               Metadata:
                 title           : H264
               Stream #0:1(rus): Subtitle: subrip
               Metadata:
                 title           : rus
               Stream #0:2(rus): Audio: mp3, 48000 Hz, stereo, s16p, 128 kb/s
               Metadata:
                 title           : rus
               Stream #0:3(eng): Audio: mp3, 48000 Hz, stereo, s16p, 119 kb/s (default)
               Metadata:
                 title           : eng
               Stream #0:4(eng): Subtitle: subrip (default)
               Metadata:
                 title           : eng
                
               FFmpeg sub track ids would be completely different. We should pass real ids.
             */
            cmdList.add("0:" + (params.sid.getId() + media.getAudioTracksList().size() + 1));
        }

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

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

        cmdList.add(tempSubsFile.getAbsolutePath());

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

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

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

        return tempSubsFile;
    }

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

    private synchronized boolean isAc3Remux() {
        return ac3Remux;
    }

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

    private synchronized boolean isDtsRemux() {
        return dtsRemux;
    }

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

    private synchronized boolean isVideoRemux() {
        return videoRemux;
    }

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