Search in sources :

Example 21 with PmsConfiguration

use of net.pms.configuration.PmsConfiguration in project UniversalMediaServer by UniversalMediaServer.

the class DLNAResource method discoverWithRenderer.

protected final void discoverWithRenderer(RendererConfiguration renderer, int count, boolean forced, String searchStr) {
    PmsConfiguration configurationSpecificToRenderer = PMS.getConfiguration(renderer);
    // Discover children if it hasn't been done already
    if (!isDiscovered()) {
        if (configurationSpecificToRenderer.getFolderLimit() && depthLimit()) {
            if (renderer.isPS3() || renderer.isXbox360()) {
                LOGGER.info("Depth limit potentionally hit for " + getDisplayName());
            }
            if (defaultRenderer != null) {
                defaultRenderer.addFolderLimit(this);
            }
        }
        discoverChildren(searchStr);
        boolean ready;
        if (renderer.isUseMediaInfo() && renderer.isDLNATreeHack()) {
            ready = analyzeChildren(count);
        } else {
            ready = analyzeChildren(-1);
        }
        if (!renderer.isUseMediaInfo() || ready) {
            setDiscovered(true);
        }
        notifyRefresh();
    } else {
        // if forced, then call the old 'refreshChildren' method
        LOGGER.trace("discover {} refresh forced: {}", getResourceId(), forced);
        /*if (forced && shouldRefresh(searchStr)) {
				doRefreshChildren(searchStr);
				notifyRefresh();
			} */
        if (forced) {
            // (refreshChildren is not overridden in MapFile)
            if (refreshChildren(searchStr)) {
                notifyRefresh();
            }
        } else {
            // if not, then the regular isRefreshNeeded/doRefreshChildren pair.
            if (shouldRefresh(searchStr)) {
                doRefreshChildren(searchStr);
                notifyRefresh();
            }
        }
    }
}
Also used : PmsConfiguration(net.pms.configuration.PmsConfiguration)

Example 22 with PmsConfiguration

use of net.pms.configuration.PmsConfiguration in project UniversalMediaServer by UniversalMediaServer.

the class DLNAResource method resolvePlayer.

/**
 * Determine whether we are a candidate for streaming or transcoding to the
 * given renderer, and return the relevant player or null as appropriate.
 *
 * @param renderer The target renderer
 * @return A player if transcoding or null if streaming
 */
public Player resolvePlayer(RendererConfiguration renderer) {
    // Use device-specific pms conf, if any
    PmsConfiguration configurationSpecificToRenderer = PMS.getConfiguration(renderer);
    boolean parserV2 = media != null && renderer != null && renderer.isUseMediaInfo();
    Player resolvedPlayer = null;
    if (media == null) {
        media = new DLNAMediaInfo();
    }
    if (format == null) {
        // Shouldn't happen, this is just a desperate measure
        Format f = FormatFactory.getAssociatedFormat(getSystemName());
        setFormat(f != null ? f : FormatFactory.getAssociatedFormat(".mpg"));
    }
    // Check if we're a transcode folder item
    if (isNoName() && (getParent() instanceof FileTranscodeVirtualFolder)) {
        // Yes, leave everything as-is
        resolvedPlayer = getPlayer();
        LOGGER.trace("Selecting player {} based on transcode item settings", resolvedPlayer);
        return resolvedPlayer;
    }
    boolean hasSubsToTranscode = false;
    boolean hasEmbeddedSubs = false;
    for (DLNAMediaSubtitle s : media.getSubtitleTracksList()) {
        hasEmbeddedSubs = (hasEmbeddedSubs || s.isEmbedded());
    }
    /**
     * At this stage, we know the media is compatible with the renderer based on its
     * "Supported" lines, and can therefore be streamed to the renderer without a
     * player. However, other details about the media can change this, such as
     * whether it has subtitles that match this user's language settings, so here we
     * perform those checks.
     */
    if (format.isVideo() && !configurationSpecificToRenderer.isDisableSubtitles()) {
        if (hasEmbeddedSubs || hasExternalSubtitles()) {
            OutputParams params = new OutputParams(configurationSpecificToRenderer);
            // set proper subtitles in accordance with user setting
            Player.setAudioAndSubs(getSystemName(), media, params);
            if (params.sid != null) {
                if (params.sid.isExternal()) {
                    if (renderer != null && renderer.isExternalSubtitlesFormatSupported(params.sid, media)) {
                        media_subtitle = params.sid;
                        media_subtitle.setSubsStreamable(true);
                        LOGGER.trace("This video has external subtitles that could be streamed");
                    } else {
                        hasSubsToTranscode = true;
                        LOGGER.trace("This video has external subtitles that should be transcoded");
                    }
                } else if (params.sid.isEmbedded()) {
                    if (renderer != null && renderer.isEmbeddedSubtitlesFormatSupported(params.sid)) {
                        LOGGER.trace("This video has embedded subtitles that could be streamed");
                    } else {
                        hasSubsToTranscode = true;
                        LOGGER.trace("This video has embedded subtitles that should be transcoded");
                    }
                }
            }
        } else {
            LOGGER.trace("This video does not have subtitles");
        }
    }
    if (configurationSpecificToRenderer.isDisableTranscoding()) {
        LOGGER.trace("Final verdict: \"{}\" will be streamed since transcoding is disabled", getName());
        return null;
    }
    String configurationSkipExtensions = configurationSpecificToRenderer.getDisableTranscodeForExtensions();
    String rendererSkipExtensions = renderer == null ? null : renderer.getStreamedExtensions();
    // Should transcoding be skipped for this format?
    skipTranscode = format.skip(configurationSkipExtensions, rendererSkipExtensions);
    if (skipTranscode) {
        LOGGER.trace("Final verdict: \"{}\" will be streamed since it is forced by configuration", getName());
        return null;
    }
    // Try to match a player based on media information and format.
    resolvedPlayer = PlayerFactory.getPlayer(this);
    if (resolvedPlayer != null) {
        String configurationForceExtensions = configurationSpecificToRenderer.getForceTranscodeForExtensions();
        String rendererForceExtensions = null;
        if (renderer != null) {
            rendererForceExtensions = renderer.getTranscodedExtensions();
        }
        // Should transcoding be forced for this format?
        boolean forceTranscode = format.skip(configurationForceExtensions, rendererForceExtensions);
        boolean isIncompatible = false;
        String prependTraceReason = "File \"{}\" will not be streamed because ";
        if (forceTranscode) {
            LOGGER.trace(prependTraceReason + "transcoding is forced by configuration", getName());
        } else if (this instanceof DVDISOTitle) {
            forceTranscode = true;
            LOGGER.trace("DVD video track \"{}\" will be transcoded because streaming isn't supported", getName());
        } else if (!format.isCompatible(media, renderer)) {
            isIncompatible = true;
            LOGGER.trace(prependTraceReason + "it is not supported by the renderer", getName());
        } else if (configurationSpecificToRenderer.isEncodedAudioPassthrough()) {
            if (getMediaAudio() != null && (FormatConfiguration.AC3.equals(getMediaAudio().getAudioCodec()) || FormatConfiguration.DTS.equals(getMediaAudio().getAudioCodec()))) {
                isIncompatible = true;
                LOGGER.trace(prependTraceReason + "the audio will use the encoded audio passthrough feature", getName());
            } else {
                for (DLNAMediaAudio audioTrack : media.getAudioTracksList()) {
                    if (audioTrack != null && (FormatConfiguration.AC3.equals(audioTrack.getAudioCodec()) || FormatConfiguration.DTS.equals(audioTrack.getAudioCodec()))) {
                        isIncompatible = true;
                        LOGGER.trace(prependTraceReason + "the audio will use the encoded audio passthrough feature", getName());
                        break;
                    }
                }
            }
        }
        if (!isIncompatible && format.isVideo() && parserV2 && renderer != null) {
            int maxBandwidth = renderer.getMaxBandwidth();
            if (renderer.isKeepAspectRatio() && !"16:9".equals(media.getAspectRatioContainer())) {
                isIncompatible = true;
                LOGGER.trace(prependTraceReason + "the renderer needs us to add borders to change the aspect ratio from {} to 16/9.", getName(), media.getAspectRatioContainer());
            } else if (!renderer.isResolutionCompatibleWithRenderer(media.getWidth(), media.getHeight())) {
                isIncompatible = true;
                LOGGER.trace(prependTraceReason + "the resolution is incompatible with the renderer.", getName());
            } else if (media.getBitrate() > maxBandwidth) {
                isIncompatible = true;
                LOGGER.trace(prependTraceReason + "the bitrate ({} b/s) is too high ({} b/s).", getName(), media.getBitrate(), maxBandwidth);
            } else if (!renderer.isVideoBitDepthSupported(media.getVideoBitDepth())) {
                isIncompatible = true;
                LOGGER.trace(prependTraceReason + "the video bit depth ({}) is not supported.", getName(), media.getVideoBitDepth());
            } else if (renderer.isH264Level41Limited() && media.isH264()) {
                if (media.getAvcLevel() != null) {
                    double h264Level = 4.1;
                    try {
                        h264Level = Double.parseDouble(media.getAvcLevel());
                    } catch (NumberFormatException e) {
                        LOGGER.trace("Could not convert {} to double: {}", media.getAvcLevel(), e.getMessage());
                    }
                    if (h264Level > 4.1) {
                        isIncompatible = true;
                        LOGGER.trace(prependTraceReason + "the H.264 level ({}) is not supported.", getName(), h264Level);
                    }
                } else {
                    isIncompatible = true;
                    LOGGER.trace(prependTraceReason + "the H.264 level is unknown.", getName());
                }
            } else if (media.is3d() && StringUtils.isNotBlank(renderer.getOutput3DFormat()) && (!media.get3DLayout().toString().toLowerCase(Locale.ROOT).equals(renderer.getOutput3DFormat()))) {
                forceTranscode = true;
                LOGGER.trace("Video \"{}\" is 3D and is forced to transcode to the format \"{}\"", getName(), renderer.getOutput3DFormat());
            }
        }
        // Prefer transcoding over streaming if:
        // 1) the media is unsupported by the renderer, or
        // 2) there are subs to transcode
        boolean preferTranscode = isIncompatible || hasSubsToTranscode;
        // 2) transcoding is preferred and not prevented by configuration
        if (forceTranscode || (preferTranscode && !isSkipTranscode())) {
            if (parserV2) {
                LOGGER.trace("Final verdict: \"{}\" will be transcoded with player \"{}\" with mime type \"{}\"", getName(), resolvedPlayer.toString(), renderer != null ? renderer.getMimeType(mimeType(resolvedPlayer), media) : media.getMimeType());
            } else {
                LOGGER.trace("Final verdict: \"{}\" will be transcoded with player \"{}\"", getName(), resolvedPlayer.toString());
            }
        } else {
            resolvedPlayer = null;
            LOGGER.trace("Final verdict: \"{}\" will be streamed", getName());
        }
    } else {
        LOGGER.trace("Final verdict: \"{}\" will be streamed because no compatible player was found", getName());
    }
    return resolvedPlayer;
}
Also used : ImageFormat(net.pms.image.ImageFormat) SimpleDateFormat(java.text.SimpleDateFormat) Format(net.pms.formats.Format) PmsConfiguration(net.pms.configuration.PmsConfiguration) OutputParams(net.pms.io.OutputParams)

Example 23 with PmsConfiguration

use of net.pms.configuration.PmsConfiguration in project UniversalMediaServer by UniversalMediaServer.

the class DLNAResource method getInputStream.

/**
 * Returns an InputStream of this DLNAResource that starts at a given time, if possible. Very useful if video chapters are being used.
 *
 * @param range
 * @param mediarenderer
 * @return The inputstream
 * @throws IOException
 */
public synchronized InputStream getInputStream(Range range, RendererConfiguration mediarenderer) throws IOException {
    // Use device-specific pms conf, if any
    PmsConfiguration configurationSpecificToRenderer = PMS.getConfiguration(mediarenderer);
    LOGGER.trace("Asked stream chunk : " + range + " of " + getName() + " and player " + player);
    // shagrath: small fix, regression on chapters
    boolean timeseek_auto = false;
    // Ditlew - WDTV Live
    // Ditlew - We convert byteoffset to timeoffset here. This needs the stream to be CBR!
    int cbr_video_bitrate = mediarenderer.getCBRVideoBitrate();
    long low = range.isByteRange() && range.isStartOffsetAvailable() ? range.asByteRange().getStart() : 0;
    long high = range.isByteRange() && range.isEndLimitAvailable() ? range.asByteRange().getEnd() : -1;
    Range.Time timeRange = range.createTimeRange();
    if (player != null && low > 0 && cbr_video_bitrate > 0) {
        // 1.04 = container overhead
        int used_bit_rated = (int) ((cbr_video_bitrate + 256) * 1024 / (double) 8 * 1.04);
        if (low > used_bit_rated) {
            timeRange.setStart(low / (double) (used_bit_rated));
            low = 0;
            // asking for an invalid offset which kills MEncoder
            if (timeRange.getStartOrZero() > media.getDurationInSeconds()) {
                return null;
            }
            // Should we rewind a little (in case our overhead isn't accurate enough)
            int rewind_secs = mediarenderer.getByteToTimeseekRewindSeconds();
            timeRange.rewindStart(rewind_secs);
            // shagrath:
            timeseek_auto = true;
        }
    }
    if (low > 0 && media.getBitrate() > 0) {
        lastStartPosition = (low * 8) / media.getBitrate();
        LOGGER.trace("Estimating seek position from byte range:");
        LOGGER.trace("   media.getBitrate: " + media.getBitrate());
        LOGGER.trace("   low: " + low);
        LOGGER.trace("   lastStartPosition: " + lastStartPosition);
    } else {
        lastStartPosition = timeRange.getStartOrZero();
        LOGGER.trace("Setting lastStartPosition from time-seeking: " + lastStartPosition);
    }
    // Determine source of the stream
    if (player == null && !isResume()) {
        // No transcoding
        if (this instanceof IPushOutput) {
            PipedOutputStream out = new PipedOutputStream();
            InputStream fis = new PipedInputStream(out);
            ((IPushOutput) this).push(out);
            if (low > 0) {
                fis.skip(low);
            }
            // http://www.ps3mediaserver.org/forum/viewtopic.php?f=11&t=12035
            lastStartSystemTime = System.currentTimeMillis();
            return wrap(fis, high, low);
        }
        InputStream fis = getInputStream();
        if (fis != null) {
            if (low > 0) {
                fis.skip(low);
            }
            // http://www.ps3mediaserver.org/forum/viewtopic.php?f=11&t=12035
            fis = wrap(fis, high, low);
            if (timeRange.getStartOrZero() > 0 && this instanceof RealFile) {
                fis.skip(MpegUtil.getPositionForTimeInMpeg(((RealFile) this).getFile(), (int) timeRange.getStartOrZero()));
            }
        }
        lastStartSystemTime = System.currentTimeMillis();
        return fis;
    } else {
        // Pipe transcoding result
        OutputParams params = new OutputParams(configurationSpecificToRenderer);
        params.aid = getMediaAudio();
        params.sid = media_subtitle;
        params.header = getHeaders();
        params.mediaRenderer = mediarenderer;
        timeRange.limit(getSplitRange());
        params.timeseek = timeRange.getStartOrZero();
        params.timeend = timeRange.getEndOrZero();
        params.shift_scr = timeseek_auto;
        if (this instanceof IPushOutput) {
            params.stdin = (IPushOutput) this;
        }
        if (resume != null) {
            if (range.isTimeRange()) {
                resume.update((Range.Time) range, this);
            }
            params.timeseek = resume.getTimeOffset() / 1000;
            if (player == null) {
                player = new FFMpegVideo();
            }
        }
        if (System.currentTimeMillis() - lastStartSystemTime < 500) {
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                LOGGER.error(null, e);
            }
        }
        // (Re)start transcoding process if necessary
        if (externalProcess == null || externalProcess.isDestroyed()) {
            // First playback attempt => start new transcoding process
            LOGGER.debug("Starting transcode/remux of " + getName() + " with media info: " + media);
            lastStartSystemTime = System.currentTimeMillis();
            externalProcess = player.launchTranscode(this, media, params);
            if (params.waitbeforestart > 0) {
                LOGGER.trace("Sleeping for {} milliseconds", params.waitbeforestart);
                try {
                    Thread.sleep(params.waitbeforestart);
                } catch (InterruptedException e) {
                    LOGGER.error(null, e);
                }
                LOGGER.trace("Finished sleeping for " + params.waitbeforestart + " milliseconds");
            }
        } else if (params.timeseek > 0 && media != null && media.isMediaparsed() && media.getDurationInSeconds() > 0) {
            // Time seek request => stop running transcode process and start a new one
            LOGGER.debug("Requesting time seek: " + params.timeseek + " seconds");
            params.minBufferSize = 1;
            Runnable r = new Runnable() {

                @Override
                public void run() {
                    externalProcess.stopProcess();
                }
            };
            new Thread(r, "External Process Stopper").start();
            lastStartSystemTime = System.currentTimeMillis();
            ProcessWrapper newExternalProcess = player.launchTranscode(this, media, params);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                LOGGER.error(null, e);
            }
            if (newExternalProcess == null) {
                LOGGER.trace("External process instance is null... sounds not good");
            }
            externalProcess = newExternalProcess;
        }
        if (externalProcess == null) {
            return null;
        }
        InputStream is = null;
        int timer = 0;
        while (is == null && timer < 10) {
            is = externalProcess.getInputStream(low);
            timer++;
            if (is == null) {
                LOGGER.debug("External input stream instance is null... sounds not good, waiting 500ms");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                }
            }
        }
        // instead of exiting
        if (is == null && !externalProcess.isDestroyed()) {
            Runnable r = new Runnable() {

                @Override
                public void run() {
                    LOGGER.error("External input stream instance is null... stopping process");
                    externalProcess.stopProcess();
                }
            };
            new Thread(r, "Hanging External Process Stopper").start();
        }
        return is;
    }
}
Also used : SizeLimitInputStream(net.pms.io.SizeLimitInputStream) ProcessWrapper(net.pms.io.ProcessWrapper) PmsConfiguration(net.pms.configuration.PmsConfiguration) OutputParams(net.pms.io.OutputParams)

Example 24 with PmsConfiguration

use of net.pms.configuration.PmsConfiguration in project UniversalMediaServer by UniversalMediaServer.

the class MEncoderVideo method launchTranscode.

@Override
public ProcessWrapper launchTranscode(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException {
    // Use device-specific pms conf
    PmsConfiguration prev = configuration;
    configuration = (DeviceConfiguration) params.mediaRenderer;
    params.manageFastStart();
    boolean avisynth = avisynth();
    final String filename = dlna.getFileName();
    setAudioAndSubs(filename, media, params);
    String externalSubtitlesFileName = null;
    if (params.sid != null && params.sid.isExternal()) {
        if (params.sid.isExternalFileUtf16()) {
            // convert UTF-16 -> UTF-8
            File convertedSubtitles = new File(configuration.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);
    boolean isDVD = dlna instanceof DVDISOTitle;
    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());
    }
    /*
		 * Check if the video track and the container report different aspect ratios
		 */
    boolean aspectRatiosMatch = true;
    if (media.getAspectRatioContainer() != null && media.getAspectRatioVideoTrack() != null && !media.getAspectRatioContainer().equals(media.getAspectRatioVideoTrack())) {
        aspectRatiosMatch = false;
    }
    // Decide whether to defer to tsMuxeR or continue to use MEncoder
    boolean deferToTsmuxer = true;
    String prependTraceReason = "Not muxing the video stream with tsMuxeR via MEncoder because ";
    if (!configuration.isMencoderMuxWhenCompatible()) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "the user setting is disabled");
    }
    if (deferToTsmuxer == true && configuration.isShowTranscodeFolder() && dlna.isNoName() && (dlna.getParent() instanceof FileTranscodeVirtualFolder)) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "the file is being played via a MEncoder entry in the transcode folder.");
    }
    if (deferToTsmuxer == true && !params.mediaRenderer.isMuxH264MpegTS()) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "the renderer does not support H.264 inside MPEG-TS.");
    }
    if (deferToTsmuxer == true && params.sid != null) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "we need to burn subtitles.");
    }
    if (deferToTsmuxer == true && isDVD) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "this is a DVD track.");
    }
    if (deferToTsmuxer == true && avisynth()) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "we are using AviSynth.");
    }
    if (deferToTsmuxer == true && params.mediaRenderer.isH264Level41Limited() && !media.isVideoWithinH264LevelLimits(newInput, params.mediaRenderer)) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "the video stream is not within H.264 level limits for this renderer.");
    }
    if (deferToTsmuxer == true && !media.isMuxable(params.mediaRenderer)) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "the video stream is not muxable to this renderer");
    }
    if (deferToTsmuxer == true && intOCW > 0 && intOCH > 0) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "we need to transcode to apply overscan compensation.");
    }
    if (deferToTsmuxer == true && !aspectRatiosMatch) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "we need to transcode to apply the correct aspect ratio.");
    }
    if (deferToTsmuxer == true && !params.mediaRenderer.isPS3() && media.isWebDl(filename, params)) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "the version of tsMuxeR supported by this renderer does not support WEB-DL files.");
    }
    if (deferToTsmuxer == true && "bt.601".equals(media.getMatrixCoefficients())) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "the colorspace probably isn't supported by the renderer.");
    }
    if (deferToTsmuxer == true && (params.mediaRenderer.isKeepAspectRatio() || params.mediaRenderer.isKeepAspectRatioTranscoding()) && !"16:9".equals(media.getAspectRatioContainer())) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "the renderer needs us to add borders so it displays the correct aspect ratio of " + media.getAspectRatioContainer() + ".");
    }
    if (deferToTsmuxer == true && !params.mediaRenderer.isResolutionCompatibleWithRenderer(media.getWidth(), media.getHeight())) {
        deferToTsmuxer = false;
        LOGGER.trace(prependTraceReason + "the resolution is incompatible with the renderer.");
    }
    if (deferToTsmuxer) {
        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) {
            TsMuxeRVideo tv = new TsMuxeRVideo();
            params.forceFps = media.getValidFps(false);
            if (media.getCodecV() != null) {
                if (media.isH264()) {
                    params.forceType = "V_MPEG4/ISO/AVC";
                } else if (media.getCodecV().startsWith("mpeg2")) {
                    params.forceType = "V_MPEG-2";
                } else if (media.getCodecV().equals("vc1")) {
                    params.forceType = "V_MS/VFW/WVC1";
                }
            }
            return tv.launchTranscode(dlna, media, params);
        }
    } else if (params.sid == null && isDVD && 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;
        }
    }
    isTranscodeToMPEGTS = params.mediaRenderer.isTranscodeToMPEGTS();
    isTranscodeToH264 = params.mediaRenderer.isTranscodeToH264() || params.mediaRenderer.isTranscodeToH265();
    isTranscodeToAAC = params.mediaRenderer.isTranscodeToAAC();
    final boolean isXboxOneWebVideo = params.mediaRenderer.isXboxOne() && purpose() == VIDEO_WEBSTREAM_PLAYER;
    String vcodec = "mpeg2video";
    if (isTranscodeToH264) {
        vcodec = "libx264";
    } else if ((params.mediaRenderer.isTranscodeToWMV() && !params.mediaRenderer.isXbox360()) || isXboxOneWebVideo) {
        wmv = true;
        vcodec = "wmv2";
    }
    // Default: Empty string
    String rendererMencoderOptions = params.mediaRenderer.getCustomMencoderOptions();
    // (see sanitizeArgs()) rather than ignoring the options entirely
    if (rendererMencoderOptions.contains("expand=") && isDVD) {
        rendererMencoderOptions = "";
    }
    // Default: Empty string
    String globalMencoderOptions = configuration.getMencoderCustomOptions();
    String combinedCustomOptions = defaultString(globalMencoderOptions) + " " + defaultString(rendererMencoderOptions);
    /**
     * Disable AC3 remux for stereo tracks with 384 kbits bitrate and PS3 renderer (PS3 FW bug?)
     *
     * 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.
     *		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 isTsMuxeRVideoEngineEnabled = configuration.getEnginesAsList(PMS.get().getRegistry()).contains(TsMuxeRVideo.ID);
    final boolean mencoderAC3RemuxAudioDelayBug = (params.aid != null) && (params.aid.getAudioProperties().getAudioDelay() != 0) && (params.timeseek == 0);
    encodedAudioPassthrough = isTsMuxeRVideoEngineEnabled && configuration.isEncodedAudioPassthrough() && params.mediaRenderer.isWrapEncodedAudioIntoPCM() && (!isDVD || configuration.isMencoderRemuxMPEG2()) && params.aid != null && params.aid.isNonPCMEncodedAudio() && !avisynth() && params.mediaRenderer.isMuxLPCMToMpeg();
    if (configuration.isAudioRemuxAC3() && params.aid != null && params.aid.isAC3() && !avisynth() && params.mediaRenderer.isTranscodeToAC3() && !configuration.isMEncoderNormalizeVolume() && !combinedCustomOptions.contains("acodec=") && !encodedAudioPassthrough && !isXboxOneWebVideo && params.aid.getAudioProperties().getNumberOfChannels() <= configuration.getAudioChannelCount()) {
        ac3Remux = true;
    } else {
        // Now check for DTS remux and LPCM streaming
        dtsRemux = isTsMuxeRVideoEngineEnabled && configuration.isAudioEmbedDtsInPcm() && (!isDVD || configuration.isMencoderRemuxMPEG2()) && params.aid != null && params.aid.isDTS() && !avisynth() && params.mediaRenderer.isDTSPlayable() && !combinedCustomOptions.contains("acodec=");
        pcm = isTsMuxeRVideoEngineEnabled && configuration.isAudioUsePCM() && (!isDVD || configuration.isMencoderRemuxMPEG2()) && // Disable LPCM transcoding for MP4 container with non-H.264 video as workaround for MEncoder's A/V sync bug
        !(media.getContainer().equals("mp4") && !media.isH264()) && params.aid != null && (// disable 7.1 DTS-HD => LPCM because of channels mapping bug
        (params.aid.isDTS() && params.aid.getAudioProperties().getNumberOfChannels() <= 6) || params.aid.isLossless() || params.aid.isTrueHD() || (!configuration.isMencoderUsePcmForHQAudioOnly() && (params.aid.isAC3() || params.aid.isMP3() || params.aid.isAAC() || params.aid.isVorbis() || // params.aid.isWMA() ||
        params.aid.isMpegAudio()))) && params.mediaRenderer.isLPCMPlayable() && !combinedCustomOptions.contains("acodec=");
    }
    if (dtsRemux || pcm || encodedAudioPassthrough) {
        params.losslessaudio = true;
        params.forceFps = media.getValidFps(false);
    }
    // MPEG-2 remux still buggy with MEncoder
    // TODO when we can still use it?
    ovccopy = false;
    if (pcm && avisynth()) {
        params.avidemux = true;
    }
    String add = "";
    if (!combinedCustomOptions.contains("-lavdopts")) {
        add = " -lavdopts debug=0";
    }
    int channels;
    if (ac3Remux) {
        // AC-3 remux
        channels = params.aid.getAudioProperties().getNumberOfChannels();
    } else if (dtsRemux || encodedAudioPassthrough || (!params.mediaRenderer.isXbox360() && wmv)) {
        channels = 2;
    } else if (pcm) {
        channels = params.aid.getAudioProperties().getNumberOfChannels();
    } else {
        /**
         * Note: MEncoder will output 2 audio channels if the input video had 2 channels
         * regardless of us telling it to output 6 (unlike FFmpeg which will output 6).
         */
        // 5.1 max for AC-3 encoding
        channels = configuration.getAudioChannelCount();
    }
    String channelsString = "-channels " + channels;
    if (combinedCustomOptions.contains("-channels")) {
        channelsString = "";
    }
    StringTokenizer st = new StringTokenizer(channelsString + (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 = (isDVD || filename.toLowerCase().endsWith("dvr-ms")) ? 1 : configuration.getMencoderMaxThreads();
        // Multithreading for decoding offers little performance gain anyway so it's not a big deal.
        if (nThreads > 4) {
            nThreads = 4;
        }
        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;
        }
    }
    String vcodecString = ":vcodec=" + vcodec;
    if (combinedCustomOptions.contains("vcodec=")) {
        vcodecString = "";
    }
    if ((configuration.getx264ConstantRateFactor() != null && isTranscodeToH264) || (configuration.getMPEG2MainSettings() != null && !isTranscodeToH264)) {
        // 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) : "";
        // Set audio codec and bitrate if audio is being transcoded
        String acodec = "";
        String abitrate = "";
        if (!ac3Remux && !dtsRemux && !isTranscodeToAAC) {
            // Set the audio codec used by Lavc
            if (!combinedCustomOptions.contains("acodec=")) {
                acodec = ":acodec=";
                if (wmv && !params.mediaRenderer.isXbox360()) {
                    acodec += "wmav2";
                } else {
                    acodec = cbr_settings + acodec;
                    if (params.mediaRenderer.isTranscodeToAAC()) {
                        acodec += "libfaac";
                    } else if (configuration.isMencoderAc3Fixed()) {
                        acodec += "ac3_fixed";
                    } else {
                        acodec += "ac3";
                    }
                }
            }
            // Set the audio bitrate used by Lavc
            if (!combinedCustomOptions.contains("abitrate=")) {
                abitrate = ":abitrate=";
                if (wmv && !params.mediaRenderer.isXbox360()) {
                    abitrate += "448";
                } else {
                    abitrate += CodecUtil.getAC3Bitrate(configuration, params.aid);
                }
            }
        }
        // Find out the maximum bandwidth we are supposed to use
        int[] defaultMaxBitrates = getVideoBitrateConfig(configuration.getMaximumBitrate());
        int[] rendererMaxBitrates = new int[2];
        if (isNotEmpty(params.mediaRenderer.getMaxVideoBitrate())) {
            rendererMaxBitrates = getVideoBitrateConfig(params.mediaRenderer.getMaxVideoBitrate());
        }
        if ((rendererMaxBitrates[0] > 0) && (rendererMaxBitrates[0] < defaultMaxBitrates[0])) {
            LOGGER.trace("Using video bitrate limit from {} configuration ({} Mb/s) because " + "it is lower than the general configuration bitrate limit ({} Mb/s)", params.mediaRenderer.getRendererName(), rendererMaxBitrates[0], defaultMaxBitrates[0]);
            defaultMaxBitrates = rendererMaxBitrates;
        }
        int maximumBitrate = defaultMaxBitrates[0];
        // Set which audio codec to use
        String audioType = "ac3";
        if (dtsRemux) {
            audioType = "dts";
        } else if (pcm || encodedAudioPassthrough) {
            audioType = "pcm";
        } else if (params.mediaRenderer.isTranscodeToAAC()) {
            audioType = "aac";
        }
        String encodeSettings = "";
        /**
         * Fixes aspect ratios on Sony TVs
         */
        String aspectRatioLavcopts = "autoaspect=1";
        if (!isDVD && ((params.mediaRenderer.isKeepAspectRatio() || params.mediaRenderer.isKeepAspectRatioTranscoding()) && !"16:9".equals(media.getAspectRatioContainer())) && !configuration.isMencoderScaler()) {
            aspectRatioLavcopts = "aspect=16/9";
        }
        if (isXboxOneWebVideo || (configuration.getMPEG2MainSettings() != null && !isTranscodeToH264)) {
            // Set MPEG-2 video quality
            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("/*"));
                }
                // 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";
                        }
                    }
                }
            }
            encodeSettings = "-lavcopts " + aspectRatioLavcopts + vcodecString + acodec + abitrate + ":threads=" + (wmv && !params.mediaRenderer.isXbox360() ? 1 : configuration.getMencoderMaxThreads()) + ("".equals(mpeg2Options) ? "" : ":" + mpeg2Options);
            encodeSettings = addMaximumBitrateConstraints(encodeSettings, media, mpeg2Options, params.mediaRenderer, audioType);
        } else if (configuration.getx264ConstantRateFactor() != null && isTranscodeToH264) {
            // Set H.264 video quality
            String x264CRF = configuration.getx264ConstantRateFactor();
            // Remove comment from the value
            if (x264CRF.contains("/*")) {
                x264CRF = x264CRF.substring(x264CRF.indexOf("/*"));
            }
            // Determine a good quality setting based on video attributes
            if (x264CRF.contains("Automatic")) {
                if (x264CRF.contains("Wireless") || maximumBitrate < 70) {
                    x264CRF = "19";
                    // Lower quality for 720p+ content
                    if (media.getWidth() > 1280) {
                        x264CRF = "23";
                    } else if (media.getWidth() > 720) {
                        x264CRF = "22";
                    }
                } else {
                    x264CRF = "16";
                    // Lower quality for 720p+ content
                    if (media.getWidth() > 720) {
                        x264CRF = "19";
                    }
                }
            }
            encodeSettings = "-lavcopts " + aspectRatioLavcopts + vcodecString + acodec + abitrate + ":threads=" + configuration.getMencoderMaxThreads() + ":o=preset=superfast,crf=" + x264CRF + ",g=250,i_qfactor=0.71,qcomp=0.6,level=3.1,weightp=0,8x8dct=0,aq-strength=0,me_range=16";
            encodeSettings = addMaximumBitrateConstraints(encodeSettings, media, "", params.mediaRenderer, audioType);
        }
        st = new StringTokenizer(encodeSettings, " ");
        {
            // Old length
            int i = overriddenMainArgs.length;
            overriddenMainArgs = Arrays.copyOf(overriddenMainArgs, overriddenMainArgs.length + st.countTokens());
            while (st.hasMoreTokens()) {
                overriddenMainArgs[i++] = st.nextToken();
            }
        }
    }
    boolean foundNoassParam = false;
    String[] expertOptions = getSpecificCodecOptions(configuration.getMencoderCodecSpecificConfig(), media, params, filename, externalSubtitlesFileName, configuration.isMencoderIntelligentSync(), false);
    if (expertOptions != null) {
        for (String s : expertOptions) {
            if (s.equals("-noass")) {
                foundNoassParam = true;
            }
        }
    }
    StringBuilder sb = new StringBuilder();
    // Set subtitles options
    if (!isDisableSubtitles(params)) {
        int subtitleMargin = 0;
        int userMargin = 0;
        // Use ASS flag (and therefore ASS font styles) for all subtitled files except vobsub, PGS (Blu-ray Disc) and DVD
        boolean apply_ass_styling = params.sid.getType() != SubtitleType.VOBSUB && params.sid.getType() != SubtitleType.PGS && // GUI: enable subtitles formating
        configuration.isMencoderAss() && // GUI: codec specific options
        !foundNoassParam && !isDVD;
        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.isUseEmbeddedSubtitlesStyle() || params.sid.getType() == SubtitleType.SUBRIP || params.sid.getType() == SubtitleType.TX3G;
            if (override_ass_style) {
                String assSubColor = configuration.getSubsColor().getMEncoderHexValue();
                sb.append("-ass-color ").append(assSubColor).append(" -ass-border-color 00000000 -ass-font-scale ").append(configuration.getAssScale());
                // Set subtitles font
                if (isNotBlank(configuration.getFont())) {
                    /* Set font with -font option, workaround for the bug:
						 * https://github.com/Happy-Neko/ps3mediaserver/commit/52e62203ea12c40628de1869882994ce1065446a#commitcomment-990156
						 */
                    sb.append(" -font ").append(quoteArg(configuration.getFont())).append(' ');
                    String font = CodecUtil.isFontRegisteredInOS(configuration.getFont());
                    if (font != null) {
                        sb.append(" -ass-force-style FontName=").append(quoteArg(font)).append(',');
                    }
                } else {
                    String font = CodecUtil.getDefaultFontPath();
                    if (isNotBlank(font)) {
                        sb.append(" -font ").append(quoteArg(font)).append(' ');
                        String fontName = CodecUtil.isFontRegisteredInOS(font);
                        if (fontName != null) {
                            sb.append(" -ass-force-style FontName=").append(quoteArg(fontName)).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;
                    subtitleMargin /= 2;
                }
                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 += userMargin;
                sb.append(",MarginV=").append(subtitleMargin).append(' ');
            } else if (intOCH > 0) {
                /*
					 * Add to the subtitle margin
					 * This keeps the subtitle text inside the frame instead of in the border
					 */
                subtitleMargin = (media.getHeight() / 100) * intOCH;
                subtitleMargin /= 2;
                sb.append("-ass-force-style MarginV=").append(subtitleMargin).append(' ');
            }
            // use of the "-ass" option also requires the "-font" option.
            if (Platform.isMac() && !sb.toString().contains(" -font ")) {
                String font = CodecUtil.getDefaultFontPath();
                if (isNotBlank(font)) {
                    sb.append("-font ").append(quoteArg(font)).append(' ');
                }
            }
            // Workaround for MPlayer #2041, remove when that bug is fixed
            if (!params.sid.isEmbedded()) {
                sb.append("-noflip-hebrew ");
            }
        // Use PLAINTEXT formatting
        } else {
            // Set subtitles font
            if (configuration.getFont() != null && configuration.getFont().length() > 0) {
                sb.append(" -font ").append(quoteArg(configuration.getFont())).append(' ');
            } else {
                String font = CodecUtil.getDefaultFontPath();
                if (isNotBlank(font)) {
                    sb.append(" -font ").append(quoteArg(font)).append(' ');
                }
            }
            sb.append(" -subfont-text-scale ").append(configuration.getMencoderNoAssScale());
            sb.append(" -subfont-outline ").append(configuration.getMencoderNoAssOutline());
            sb.append(" -subfont-blur ").append(configuration.getMencoderNoAssBlur());
            // 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 += userMargin;
            sb.append(" -subpos ").append(100 - subtitleMargin).append(' ');
        }
        // 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(), " ");
    {
        // Old length
        int i = overriddenMainArgs.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() || isDVD) && s.contains("-ass")) {
                s = "-quiet";
                handleToken = true;
            }
            overriddenMainArgs[i++] = s;
        }
    }
    List<String> cmdList = new ArrayList<>();
    cmdList.add(executable());
    // Choose which time to seek to
    cmdList.add("-ss");
    cmdList.add((params.timeseek > 0) ? "" + params.timeseek : "0");
    if (isDVD) {
        cmdList.add("-dvd-device");
    }
    String frameRateRatio = media.getValidFps(true);
    String frameRateNumber = media.getValidFps(false);
    // Input filename
    if (avisynth && !filename.toLowerCase().endsWith(".iso")) {
        File avsFile = AviSynthMEncoder.getAVSScript(filename, params.sid, params.fromFrame, params.toFrame, frameRateRatio, frameRateNumber, configuration);
        cmdList.add(ProcessUtil.getShortFileNameIfWideChars(avsFile.getAbsolutePath()));
    } else {
        if (params.stdin != null) {
            cmdList.add("-");
        } else {
            if (isDVD) {
                String dvdFileName = filename.replace("\\VIDEO_TS", "");
                cmdList.add(dvdFileName);
            } else {
                cmdList.add(filename);
            }
        }
    }
    if (isDVD) {
        cmdList.add("dvd://" + media.getDvdtrack());
    }
    for (String arg : args()) {
        if (arg.contains("format=mpeg2") && media.getAspectRatioDvdIso() != null && media.getAspectRatioMencoderMpegopts(true) != null) {
            cmdList.add(arg + ":vaspect=" + media.getAspectRatioMencoderMpegopts(true));
        } else {
            cmdList.add(arg);
        }
    }
    if (!dtsRemux && !encodedAudioPassthrough && !pcm && !avisynth() && params.aid != null && media.getAudioTracksList().size() > 1) {
        cmdList.add("-aid");
        // TODO Need to add support for LAVF demuxing
        boolean lavf = false;
        cmdList.add("" + (lavf ? params.aid.getId() + 1 : params.aid.getId()));
    }
    /*
		 * Handle subtitles
		 *
		 * Try to reconcile the fact that the handling of "Definitely 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
        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) {
            // confirm the mutual exclusion
            assert params.sid.isExternal();
            // 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 if (!params.sid.isStreamable() && !params.mediaRenderer.streamSubsForTranscodedVideo()) {
                // when subs are streamable do not transcode them
                cmdList.add("-sub");
                DLNAMediaSubtitle convertedSubs = dlna.getMediaSubtitle();
                if (media.is3d()) {
                    if (convertedSubs != null && convertedSubs.getConvertedFile() != null) {
                        // subs are already converted to 3D so use them
                        cmdList.add(convertedSubs.getConvertedFile().getAbsolutePath().replace(",", "\\,"));
                    } else if (params.sid.getType() != SubtitleType.ASS) {
                        // When subs are not converted and they are not in the ASS format and video is 3D then subs need conversion to 3D
                        File subsFilename = SubtitleUtils.getSubtitles(dlna, media, params, configuration, SubtitleType.ASS);
                        cmdList.add(subsFilename.getAbsolutePath().replace(",", "\\,"));
                    }
                } else {
                    // Commas in MEncoder separate multiple subtitle files
                    cmdList.add(externalSubtitlesFileName.replace(",", "\\,"));
                }
                if (params.sid.isExternalFileUtf()) {
                    // Append -utf8 option for UTF-8 external subtitles
                    cmdList.add("-utf8");
                }
            }
        }
    }
    // -ofps
    // where a framerate is required, use the input framerate or 24000/1001
    String framerate = (frameRateRatio != null) ? frameRateRatio : "24000/1001";
    String ofps = framerate;
    // Optional -fps or -mc
    if (configuration.isMencoderForceFps()) {
        if (!configuration.isFix25FPSAvMismatch()) {
            cmdList.add("-fps");
            cmdList.add(framerate);
        } else if (frameRateRatio != 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";
        }
    }
    // Make MEncoder output framerate correspond to InterFrame
    if (avisynth() && configuration.getAvisynthInterFrame() && !"60000/1001".equals(frameRateRatio) && !"50".equals(frameRateRatio) && !"60".equals(frameRateRatio)) {
        switch(frameRateRatio) {
            case "25":
                ofps = "50";
                break;
            case "30":
                ofps = "60";
                break;
            default:
                ofps = "60000/1001";
                break;
        }
    }
    cmdList.add("-ofps");
    cmdList.add(ofps);
    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.isResolutionCompatibleWithRenderer(media.getWidth(), media.getHeight());
    // Video scaler and overscan compensation
    boolean scaleBool = false;
    if (isResolutionTooHighForRenderer || (configuration.isMencoderScaler() && (configuration.getMencoderScaleX() != 0 || configuration.getMencoderScaleY() != 0)) || (intOCW > 0 || intOCH > 0)) {
        scaleBool = true;
    }
    int scaleWidth = 0;
    int scaleHeight = 0;
    String vfValue = "";
    if (media.getWidth() > 0 && media.getHeight() > 0) {
        scaleWidth = media.getWidth();
        scaleHeight = media.getHeight();
    }
    double videoAspectRatio = (double) media.getWidth() / (double) media.getHeight();
    double rendererAspectRatio = 1.777777777777778;
    if (params.mediaRenderer.isMaximumResolutionSpecified()) {
        rendererAspectRatio = (double) params.mediaRenderer.getMaxVideoWidth() / (double) params.mediaRenderer.getMaxVideoHeight();
    }
    if ((deinterlace || scaleBool) && !avisynth()) {
        StringBuilder vfValueOverscanPrepend = new StringBuilder();
        StringBuilder vfValueOverscanMiddle = new StringBuilder();
        StringBuilder vfValueVS = new StringBuilder();
        StringBuilder vfValueComplete = new StringBuilder();
        String deinterlaceComma = "";
        /*
			 * 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 += intOCWPixels;
            scaleHeight += intOCHPixels;
            // See if the video needs to be scaled down
            if (params.mediaRenderer.isMaximumResolutionSpecified() && ((scaleWidth > params.mediaRenderer.getMaxVideoWidth()) || (scaleHeight > params.mediaRenderer.getMaxVideoHeight()))) {
                double overscannedAspectRatio = scaleWidth / (double) scaleHeight;
                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();
                }
            }
            scaleWidth = convertToModX(scaleWidth, 4);
            scaleHeight = convertToModX(scaleHeight, 4);
            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();
                }
            }
            scaleWidth = convertToModX(scaleWidth, 4);
            scaleHeight = convertToModX(scaleHeight, 4);
            LOGGER.info("Setting video resolution to: " + scaleWidth + "x" + scaleHeight + ", your Video Scaler setting");
            vfValueVS.append("scale=").append(scaleWidth).append(':').append(scaleHeight);
        } else if (isResolutionTooHighForRenderer) {
            /*
				 * 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 = media.getHeight();
            } 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();
                }
            }
            scaleWidth = convertToModX(scaleWidth, 4);
            scaleHeight = convertToModX(scaleHeight, 4);
            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 = ",";
        }
        vfValue = (deinterlace ? "yadif" : "") + (scaleBool ? deinterlaceComma + vfValueComplete : "");
    }
    /*
		 * Make sure the video is mod4 unless the renderer has specified
		 * that it doesn't care, and make sure the aspect ratio is 16/9
		 * if the renderer needs it.
		 *
		 * The PS3 and possibly other renderers sometimes display mod2
		 * videos in black and white with diagonal strips of color.
		 *
		 * TODO: Integrate this with the other stuff so that "expand" only
		 * ever appears once in the MEncoder CMD.
		 */
    if (!isDVD && ((((scaleWidth % 4 != 0) || (scaleHeight % 4 != 0)) && !params.mediaRenderer.isMuxNonMod4Resolution()) || ((params.mediaRenderer.isKeepAspectRatio() || params.mediaRenderer.isKeepAspectRatioTranscoding()) && !"16:9".equals(media.getAspectRatioContainer()))) && !configuration.isMencoderScaler()) {
        String vfValuePrepend = "expand=";
        if (params.mediaRenderer.isKeepAspectRatio() || params.mediaRenderer.isKeepAspectRatioTranscoding()) {
            String resolution = dlna.getResolutionForKeepAR(scaleWidth, scaleHeight);
            scaleWidth = Integer.parseInt(substringBefore(resolution, "x"));
            scaleHeight = Integer.parseInt(substringAfter(resolution, "x"));
            /**
             * Now we know which resolution we want the video to be, let's see if MEncoder
             * can be trusted to output it using only the expand filter, or if we need to
             * be extra careful and use scale too (which is slower).
             *
             * For now I'm not sure exactly how MEncoder decides which resolution to
             * output so this is some cautious math. If someone does extensive testing
             * in the future it can be made less cautious.
             */
            if ((scaleWidth + 4) > params.mediaRenderer.getMaxVideoWidth() || (scaleHeight + 4) > params.mediaRenderer.getMaxVideoHeight()) {
                vfValuePrepend += "::::0:16/9,scale=" + scaleWidth + ":" + scaleHeight;
            } else {
                vfValuePrepend += "::::0:16/9:4";
            }
        } else {
            vfValuePrepend += "-" + (scaleWidth % 4) + ":-" + (scaleHeight % 4);
        }
        vfValuePrepend += ",softskip";
        if (isNotBlank(vfValue)) {
            vfValuePrepend += ",";
        }
        vfValue = vfValuePrepend + vfValue;
    }
    if (isNotBlank(vfValue)) {
        cmdList.add("-vf");
        cmdList.add(vfValue);
    }
    if (configuration.getMencoderMT() && !avisynth && !isDVD && !(media.getCodecV() != null && (media.getCodecV().startsWith("mpeg2")))) {
        cmdList.add("-lavdopts");
        cmdList.add("fast");
    }
    boolean disableMc0AndNoskip = false;
    // 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<>();
        // 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<>();
        // 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<>();
        // pass 1: process expertOptions
        for (int i = 0; i < expertOptions.length; ++i) {
            switch(expertOptions[i]) {
                case "-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...
                    // false: option does not have a corresponding value
                    removeCmdListOption.put("-ass", false);
                    // remove -noass from expertOptions in pass 3
                    expertOptions[i] = REMOVE_OPTION;
                    break;
                case "-nomux":
                    expertOptions[i] = REMOVE_OPTION;
                    break;
                case "-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;
                    break;
                case "-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;
                    break;
                case "-fps":
                    removeCmdListOption.put("-fps", true);
                    ++i;
                    break;
                case "-ovc":
                    removeCmdListOption.put("-ovc", true);
                    ++i;
                    break;
                case "-channels":
                    removeCmdListOption.put("-channels", true);
                    ++i;
                    break;
                case "-oac":
                    removeCmdListOption.put("-oac", true);
                    ++i;
                    break;
                case "-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;
                    break;
                case "-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;
                    break;
                case "-vf":
                    mergeCmdListOption.put("-vf", "%s," + expertOptions[i + 1].replace("%", "%%"));
                    ++i;
                    break;
                case "-af":
                    mergeCmdListOption.put("-af", "%s," + expertOptions[i + 1].replace("%", "%%"));
                    ++i;
                    break;
                case "-nosync":
                    disableMc0AndNoskip = true;
                    expertOptions[i] = REMOVE_OPTION;
                    break;
                case "-mc":
                    disableMc0AndNoskip = true;
                    break;
                default:
                    break;
            }
        }
        for (String key : mergeCmdListOption.keySet()) {
            mergedCmdListOption.put(key, false);
        }
        // pass 2: process cmdList
        List<String> transformedCmdList = new ArrayList<>();
        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.equals(REMOVE_OPTION)) {
                if (isTrue(mergedCmdListOption.get(option))) {
                    // true: this option and its value have already been merged into existing cmdList options
                    // skip the value
                    ++i;
                } else {
                    cmdList.add(option);
                }
            }
        }
    }
    if ((pcm || dtsRemux || encodedAudioPassthrough || ac3Remux) || (configuration.isMencoderNoOutOfSync() && !disableMc0AndNoskip)) {
        if (configuration.isFix25FPSAvMismatch()) {
            cmdList.add("-mc");
            cmdList.add("0.005");
        } else if (configuration.isMencoderNoOutOfSync() && !disableMc0AndNoskip) {
            cmdList.add("-mc");
            cmdList.add("0");
            if (!params.mediaRenderer.isDisableMencoderNoskip()) {
                cmdList.add("-noskip");
            }
        }
    }
    if (params.timeend > 0) {
        cmdList.add("-endpos");
        cmdList.add("" + params.timeend);
    }
    // Force srate because MEncoder doesn't like anything other than 48khz for AC-3
    String rate = "" + params.mediaRenderer.getTranscodedVideoAudioSampleRate();
    if (!pcm && !dtsRemux && !ac3Remux && !encodedAudioPassthrough) {
        cmdList.add("-af");
        String af = "lavcresample=" + rate;
        if (configuration.isMEncoderNormalizeVolume()) {
            af += ":volnorm=1";
        }
        cmdList.add(af);
        cmdList.add("-srate");
        cmdList.add(rate);
    }
    // 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;
    if (pcm || dtsRemux || encodedAudioPassthrough) {
        // 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 || encodedAudioPassthrough || 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 = AudioUtils.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 {
            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();
            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.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.setEncodedAudioPassthrough(encodedAudioPassthrough);
            sm.setSampleFrequency(48000);
            sm.setBitsPerSample(16);
            String mixer = null;
            if (pcm && !dtsRemux && !encodedAudioPassthrough) {
                // LPCM always outputs 5.1/7.1 for multichannel tracks. Downmix with player if needed!
                mixer = getLPCMChannelMappingForMencoder(params.aid);
            }
            sm.setNbChannels(channels);
            // It seems that -really-quiet prevents MEncoder from stopping the pipe output after some time
            // -mc 0.1 makes the DTS-HD extraction work better with latest MEncoder builds, and has no impact on the regular DTS one
            // TODO: See if these notes are still true, and if so leave specific revisions/release names of the latest version tested.
            String[] ffmpegLPCMextract = new String[] { executable(), "-ss", "0", filename, "-really-quiet", "-msglevel", "statusline=2", "-channels", "" + channels, "-ovc", "copy", "-of", "rawaudio", "-mc", (dtsRemux || encodedAudioPassthrough) ? "0.1" : "0", "-noskip", (aid == null) ? "-quiet" : "-aid", (aid == null) ? "-quiet" : aid, "-oac", (ac3Remux || dtsRemux || encodedAudioPassthrough) ? "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.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;
            try (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 {
                    // DTS padded in LPCM trick
                    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");
            }
            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 || encodedAudioPassthrough) ? null : params);
            params.input_pipes[0] = pipe;
            cmdList.add("-o");
            cmdList.add(pipe.getInputPipe());
        }
        String[] cmdArray = new String[cmdList.size()];
        cmdList.toArray(cmdArray);
        cmdArray = finalizeTranscoderArgs(filename, dlna, media, params, cmdArray);
        pw = new ProcessWrapperImpl(cmdArray, params);
        if (!directpipe) {
            ProcessWrapper mkfifo_process = pipe.getPipeProcess();
            pw.attachProcess(mkfifo_process);
            /*
				 * It can take a long time for Windows to create a named pipe (and
				 * mkfifo can be slow if /tmp isn't memory-mapped), so run this in
				 * the current thread.
				 */
            mkfifo_process.runInSameThread();
            pipe.deleteLater();
        }
    }
    pw.runInNewThread();
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
    }
    configuration = prev;
    return pw;
}
Also used : PmsConfiguration(net.pms.configuration.PmsConfiguration) PrintWriter(java.io.PrintWriter) File(java.io.File)

Example 25 with PmsConfiguration

use of net.pms.configuration.PmsConfiguration in project UniversalMediaServer by UniversalMediaServer.

the class Player method setSubtitleOutputParameters.

/**
 * This method populates the supplied {@link OutputParams} object with the correct subtitles (sid)
 * based on the given filename, its MediaInfo metadata and PMS configuration settings.
 *
 * TODO: Rewrite this crazy method to be more concise and logical.
 *
 * @param fileName
 * The file name used to determine the availability of subtitles.
 * @param media
 * The MediaInfo metadata for the file.
 * @param params
 * The parameters to populate.
 */
public static void setSubtitleOutputParameters(String fileName, DLNAMediaInfo media, OutputParams params) {
    // Use device-specific pms conf
    PmsConfiguration configuration = PMS.getConfiguration(params);
    String currentLang = null;
    DLNAMediaSubtitle matchedSub = null;
    if (params.aid != null) {
        currentLang = params.aid.getLang();
    }
    if (params.sid != null && params.sid.getId() == -1) {
        LOGGER.trace("Don't want subtitles!");
        params.sid = null;
        return;
    }
    /**
     * Check for live subtitles
     */
    if (params.sid != null && !StringUtils.isEmpty(params.sid.getLiveSubURL())) {
        LOGGER.debug("Live subtitles " + params.sid.getLiveSubURL());
        try {
            matchedSub = params.sid;
            String file = OpenSubtitle.fetchSubs(matchedSub.getLiveSubURL(), matchedSub.getLiveSubFile());
            if (!StringUtils.isEmpty(file)) {
                matchedSub.setExternalFile(new File(file), null);
                params.sid = matchedSub;
                return;
            }
        } catch (IOException e) {
        }
    }
    StringTokenizer st = new StringTokenizer(configuration.getAudioSubLanguages(), ";");
    /**
     * Check for external and internal subtitles matching the user's language
     * preferences
     */
    boolean matchedInternalSubtitles = false;
    boolean matchedExternalSubtitles = false;
    while (st.hasMoreTokens()) {
        String pair = st.nextToken();
        if (pair.contains(",")) {
            String audio = pair.substring(0, pair.indexOf(','));
            String sub = pair.substring(pair.indexOf(',') + 1);
            audio = audio.trim();
            sub = sub.trim();
            LOGGER.trace("Searching for a match for: " + currentLang + " with " + audio + " and " + sub);
            if (Iso639.isCodesMatching(audio, currentLang) || (currentLang != null && audio.equals("*"))) {
                if (sub.equals("off")) {
                    /**
                     * Ignore the "off" language for external subtitles if the user setting is enabled
                     * TODO: Prioritize multiple external subtitles properly instead of just taking the first one we load
                     */
                    if (configuration.isForceExternalSubtitles()) {
                        for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
                            if (present_sub.getExternalFile() != null) {
                                matchedSub = present_sub;
                                matchedExternalSubtitles = true;
                                LOGGER.trace("Ignoring the \"off\" language because there are external subtitles");
                                break;
                            }
                        }
                    }
                    if (!matchedExternalSubtitles) {
                        matchedSub = new DLNAMediaSubtitle();
                        matchedSub.setLang("off");
                    }
                } else {
                    for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
                        if (present_sub.matchCode(sub) || sub.equals("*")) {
                            if (present_sub.getExternalFile() != null) {
                                if (configuration.isAutoloadExternalSubtitles()) {
                                    // Subtitle is external and we want external subtitles, look no further
                                    matchedSub = present_sub;
                                    LOGGER.trace("Matched external subtitles track: " + matchedSub);
                                    break;
                                } else {
                                    // Subtitle is external but we do not want external subtitles, keep searching
                                    LOGGER.trace("External subtitles ignored because of user setting: " + present_sub);
                                }
                            } else if (!matchedInternalSubtitles) {
                                matchedSub = present_sub;
                                LOGGER.trace("Matched internal subtitles track: " + matchedSub);
                                if (configuration.isAutoloadExternalSubtitles()) {
                                    // Subtitle is internal and we will wait to see if an external one is available instead
                                    matchedInternalSubtitles = true;
                                } else {
                                    // Subtitle is internal and we will use it
                                    break;
                                }
                            }
                        }
                    }
                }
                if (matchedSub != null && !matchedInternalSubtitles) {
                    break;
                }
            }
        }
    }
    /**
     * Check for external subtitles that were skipped in the above code block
     * because they didn't match language preferences, if there wasn't already
     * a match and the user settings specify it.
     */
    if (matchedSub == null && configuration.isForceExternalSubtitles()) {
        for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) {
            if (present_sub.getExternalFile() != null) {
                matchedSub = present_sub;
                LOGGER.trace("Matched external subtitles track that did not match language preferences: " + matchedSub);
                break;
            }
        }
    }
    /**
     * Disable chosen subtitles if the user has disabled all subtitles or
     * if the language preferences have specified the "off" language.
     *
     * TODO: Can't we save a bunch of looping by checking for isDisableSubtitles
     * just after the Live Subtitles check above?
     */
    if (matchedSub != null && params.sid == null) {
        if (configuration.isDisableSubtitles() || (matchedSub.getLang() != null && matchedSub.getLang().equals("off"))) {
            LOGGER.trace("Disabled the subtitles: " + matchedSub);
        } else {
            params.sid = matchedSub;
        }
    }
    /**
     * Check for forced subtitles.
     */
    if (!configuration.isDisableSubtitles() && params.sid == null && media != null) {
        // Check for subtitles again
        File video = new File(fileName);
        FileUtil.isSubtitlesExists(video, media, false);
        if (configuration.isAutoloadExternalSubtitles()) {
            boolean forcedSubsFound = false;
            // Priority to external subtitles
            for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
                if (matchedSub != null && matchedSub.getLang() != null && matchedSub.getLang().equals("off")) {
                    st = new StringTokenizer(configuration.getForcedSubtitleTags(), ",");
                    while (sub.getSubtitlesTrackTitleFromMetadata() != null && st.hasMoreTokens()) {
                        String forcedTags = st.nextToken();
                        forcedTags = forcedTags.trim();
                        if (sub.getSubtitlesTrackTitleFromMetadata().toLowerCase().contains(forcedTags) && Iso639.isCodesMatching(sub.getLang(), configuration.getForcedSubtitleLanguage())) {
                            LOGGER.trace("Forcing preferred subtitles: " + sub.getLang() + "/" + sub.getSubtitlesTrackTitleFromMetadata());
                            LOGGER.trace("Forced subtitles track: " + sub);
                            if (sub.getExternalFile() != null) {
                                LOGGER.trace("Found external forced file: " + sub.getExternalFile().getAbsolutePath());
                            }
                            params.sid = sub;
                            forcedSubsFound = true;
                            break;
                        }
                    }
                    if (forcedSubsFound == true) {
                        break;
                    }
                } else {
                    LOGGER.trace("Found subtitles track: " + sub);
                    if (sub.getExternalFile() != null) {
                        LOGGER.trace("Found external file: " + sub.getExternalFile().getAbsolutePath());
                        params.sid = sub;
                        break;
                    }
                }
            }
        }
        if (matchedSub != null && matchedSub.getLang() != null && matchedSub.getLang().equals("off")) {
            return;
        }
        if (params.sid == null) {
            st = new StringTokenizer(UMSUtils.getLangList(params.mediaRenderer), ",");
            while (st.hasMoreTokens()) {
                String lang = st.nextToken();
                lang = lang.trim();
                LOGGER.trace("Looking for a subtitle track with lang: " + lang);
                for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
                    if (sub.matchCode(lang) && !(!configuration.isAutoloadExternalSubtitles() && sub.getExternalFile() != null)) {
                        params.sid = sub;
                        LOGGER.trace("Matched subtitles track: " + params.sid);
                        return;
                    }
                }
            }
        }
    }
}
Also used : DLNAMediaSubtitle(net.pms.dlna.DLNAMediaSubtitle) StringTokenizer(java.util.StringTokenizer) PmsConfiguration(net.pms.configuration.PmsConfiguration) IOException(java.io.IOException) File(java.io.File)

Aggregations

PmsConfiguration (net.pms.configuration.PmsConfiguration)32 File (java.io.File)7 ProcessWrapperImpl (net.pms.io.ProcessWrapperImpl)7 ArrayList (java.util.ArrayList)6 OutputParams (net.pms.io.OutputParams)5 IOException (java.io.IOException)4 ProcessWrapper (net.pms.io.ProcessWrapper)4 LoggerContext (ch.qos.logback.classic.LoggerContext)3 SyslogAppender (ch.qos.logback.classic.net.SyslogAppender)3 ILoggingEvent (ch.qos.logback.classic.spi.ILoggingEvent)3 Appender (ch.qos.logback.core.Appender)3 ConsoleAppender (ch.qos.logback.core.ConsoleAppender)3 FileAppender (ch.qos.logback.core.FileAppender)3 OutputStreamAppender (ch.qos.logback.core.OutputStreamAppender)3 RendererConfiguration (net.pms.configuration.RendererConfiguration)3 PipeProcess (net.pms.io.PipeProcess)3 ThresholdFilter (ch.qos.logback.classic.filter.ThresholdFilter)2 PrintWriter (java.io.PrintWriter)2 StringTokenizer (java.util.StringTokenizer)2 DLNAMediaAudio (net.pms.dlna.DLNAMediaAudio)2