gt: 		List<String> transcodeOptions = new ArrayList<>();
context: /*
   * PS3 Media Server, for streaming any medias to your PS3.
   * Copyright (C) 2008  A.Brochard
   *
   * This program is free software; you can redistribute it and/or
   * modify it under the terms of the GNU General Public License
   * as published by the Free Software Foundation; version 2
   * of the License only.
   *
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU General Public License for more details.
   *
   * You should have received a copy of the GNU General Public License
   * along with this program; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
   */
  package net.pms.encoders;
  
  import com.jgoodies.forms.builder.PanelBuilder;
  import com.jgoodies.forms.factories.Borders;
  import com.jgoodies.forms.layout.CellConstraints;
  import com.jgoodies.forms.layout.FormLayout;
  import com.sun.jna.Platform;
  import java.awt.Font;
  import java.awt.event.ItemEvent;
  import java.awt.event.ItemListener;
  import java.io.*;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.List;
  import java.util.Locale;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import javax.annotation.Nonnull;
  import javax.annotation.Nullable;
  import javax.swing.JCheckBox;
  import javax.swing.JComboBox;
  import javax.swing.JComponent;
  import javax.swing.JLabel;
  import net.pms.Messages;
  import net.pms.configuration.DeviceConfiguration;
  import net.pms.configuration.ExecutableInfo;
  import net.pms.configuration.ExecutableInfo.ExecutableInfoBuilder;
  import net.pms.configuration.ExternalProgramInfo;
  import net.pms.configuration.FFmpegExecutableInfo.FFmpegExecutableInfoBuilder;
  import net.pms.configuration.PmsConfiguration;
  import net.pms.configuration.RendererConfiguration;
  import net.pms.dlna.DLNAMediaInfo;
  import net.pms.dlna.DLNAMediaSubtitle;
  import net.pms.dlna.DLNAResource;
  import net.pms.dlna.InputFile;
  import net.pms.formats.Format;
  import net.pms.formats.v2.SubtitleType;
  import net.pms.io.*;
  import net.pms.network.HTTPResource;
  import net.pms.newgui.GuiUtil;
  import net.pms.platform.windows.NTStatus;
  import net.pms.util.CodecUtil;
  import net.pms.util.PlayerUtil;
  import net.pms.util.ProcessUtil;
  import net.pms.util.StringUtil;
  import net.pms.util.SubtitleUtils;
  import net.pms.util.Version;
  import org.apache.commons.lang3.StringUtils;
  import static org.apache.commons.lang3.StringUtils.isBlank;
  import static org.apache.commons.lang3.StringUtils.isNotBlank;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /*
   * Pure FFmpeg video player.
   *
   * Design note:
   *
   * Helper methods that return lists of <code>String</code>s representing
   * options are public to facilitate composition e.g. a custom engine (plugin)
   * that uses tsMuxeR for videos without subtitles and FFmpeg otherwise needs to
   * compose and call methods on both players.
   *
   * To avoid API churn, and to provide wiggle room for future functionality, all
   * of these methods take the same arguments as launchTranscode (and the same
   * first four arguments as finalizeTranscoderArgs) even if one or more of the
   * parameters are unused e.g.:
   *
   *     public List<String> getAudioBitrateOptions(
   *         String filename,
   *         DLNAResource dlna,
   *         DLNAMediaInfo media,
   *         OutputParams params
   *     )
   */
  public class FFMpegVideo extends Player {
  	private static final Logger LOGGER = LoggerFactory.getLogger(FFMpegVideo.class);
  	public static final PlayerId ID = StandardPlayerId.FFMPEG_VIDEO;
  
  	/** The {@link Configuration} key for the custom FFmpeg path. */
  	public static final String KEY_FFMPEG_PATH = "ffmpeg_path";
  
  	/** The {@link Configuration} key for the FFmpeg executable type. */
  	public static final String KEY_FFMPEG_EXECUTABLE_TYPE = "ffmpeg_executable_type";
  	public static final String NAME = "FFmpeg Video";
  	private static final String DEFAULT_QSCALE = "3";
  
  	// Not to be instantiated by anything but PlayerFactory
  	FFMpegVideo() {
  	}
  
  	/**
  	 * Returns a list of strings representing the rescale options for this transcode i.e. the ffmpeg -vf
  	 * options used to show subtitles in either SSA/ASS or picture-based format and resize a video that's too wide and/or high for the specified renderer.
  	 * If the renderer has no size limits, or there's no media metadata, or the video is within the renderer's
  	 * size limits, an empty list is returned.
  	 *
  	 * @param dlna
  	 * @param media metadata for the DLNA resource which is being transcoded
  	 * @param params
  	 * @return a {@link List} of <code>String</code>s representing the rescale options for this video,
  	 * or an empty list if the video doesn't need to be resized.
  	 * @throws java.io.IOException
  	 */
  	public List<String> getVideoFilterOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException {
  		List<String> videoFilterOptions = new ArrayList<>();
  		ArrayList<String> filterChain = new ArrayList<>();
  		ArrayList<String> scalePadFilterChain = new ArrayList<>();
  		final RendererConfiguration renderer = params.getMediaRenderer();
  
  		boolean isMediaValid = media != null && media.isMediaparsed() && media.getHeight() != 0;
  		boolean isResolutionTooHighForRenderer = isMediaValid && !params.getMediaRenderer().isResolutionCompatibleWithRenderer(media.getWidth(), media.getHeight());
  
  		int scaleWidth = 0;
  		int scaleHeight = 0;
  		if (media.getWidth() > 0 && media.getHeight() > 0) {
  			scaleWidth = media.getWidth();
  			scaleHeight = media.getHeight();
  		}
  
  		boolean is3D = media.is3d() && !media.stereoscopyIsAnaglyph();
  
  		// Make sure the aspect ratio is 16/9 if the renderer needs it.
  		boolean keepAR = (renderer.isKeepAspectRatio() || renderer.isKeepAspectRatioTranscoding()) &&
  				!media.is3dFullSbsOrOu() &&
  				!"16:9".equals(media.getAspectRatioContainer());
  
  		// Scale and pad the video if necessary
  		if (isResolutionTooHighForRenderer || (!renderer.isRescaleByRenderer() && renderer.isMaximumResolutionSpecified() && media.getWidth() < 720)) { // Do not rescale for SD video and higher
  			if (media.is3dFullSbsOrOu()) {
  				scalePadFilterChain.add(String.format("scale=%1$d:%2$d", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight()));
  			} else {
  				scalePadFilterChain.add(String.format("scale=iw*min(%1$d/iw\\,%2$d/ih):ih*min(%1$d/iw\\,%2$d/ih)", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight()));
  
  				if (keepAR) {
  					scalePadFilterChain.add(String.format("pad=%1$d:%2$d:(%1$d-iw)/2:(%2$d-ih)/2", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight()));
  				}
  			}
  		} else if (keepAR && isMediaValid) {
  			if ((media.getWidth() / (double) media.getHeight()) >= (16 / (double) 9)) {
  				scalePadFilterChain.add("pad=iw:iw/(16/9):0:(oh-ih)/2");
  				scaleHeight = (int) Math.round(scaleWidth / (16 / (double) 9));
  			} else {
  				scalePadFilterChain.add("pad=ih*(16/9):ih:(ow-iw)/2:0");
  				scaleWidth = (int) Math.round(scaleHeight * (16 / (double) 9));
  			}
  
  			scaleWidth  = convertToModX(scaleWidth, 4);
  			scaleHeight = convertToModX(scaleHeight, 4);
  
  			// Make sure we didn't exceed the renderer's maximum resolution.
  			if (
  				scaleHeight > renderer.getMaxVideoHeight() ||
  				scaleWidth  > renderer.getMaxVideoWidth()
  			) {
  				scaleHeight = renderer.getMaxVideoHeight();
  				scaleWidth  = renderer.getMaxVideoWidth();
  			}
  
  			scalePadFilterChain.add("scale=" + scaleWidth + ":" + scaleHeight);
  		}
  
  		filterChain.addAll(scalePadFilterChain);
  
  		boolean override = true;
  		if (renderer instanceof RendererConfiguration.OutputOverride) {
  			RendererConfiguration.OutputOverride or = (RendererConfiguration.OutputOverride) renderer;
  			override = or.addSubtitles();
  		}
  
  		if (!isDisableSubtitles(params) && override) {
  			boolean isSubsManualTiming = true;
  			DLNAMediaSubtitle convertedSubs = dlna.getMediaSubtitle();
  			StringBuilder subsFilter = new StringBuilder();
  			if (params.getSid() != null && params.getSid().getType().isText()) {
  				boolean isSubsASS = params.getSid().getType() == SubtitleType.ASS;
  				String originalSubsFilename = null;
  				if (is3D) {
  					if (convertedSubs != null && convertedSubs.getConvertedFile() != null) { // subs are already converted to 3D so use them
  						originalSubsFilename = convertedSubs.getConvertedFile().getAbsolutePath();
  					} else if (!isSubsASS) { // When subs are not converted and they are not in the ASS format and video is 3D then subs need conversion to 3D
  						File subtitlesFile = SubtitleUtils.getSubtitles(dlna, media, params, configuration, SubtitleType.ASS);
  						if (subtitlesFile != null) {
  							originalSubsFilename = subtitlesFile.getAbsolutePath();
  						} else {
  							LOGGER.error("External subtitles file \"{}\" is unavailable", params.getSid().getName());
  						}
  					} else {
  						if (params.getSid().getExternalFile() != null) {
  							originalSubsFilename = params.getSid().getExternalFile().getPath();
  						} else {
  							LOGGER.error("External subtitles file \"{}\" is unavailable", params.getSid().getName());
  						}
  					}
  				} else if (params.getSid().isExternal()) {
  					if (params.getSid().getExternalFile() != null) {
  						if (
  							!renderer.streamSubsForTranscodedVideo() ||
  							!renderer.isExternalSubtitlesFormatSupported(params.getSid(), dlna)
  						) {
  							// Only transcode subtitles if they aren't streamable
  							originalSubsFilename = params.getSid().getExternalFile().getPath();
  						}
  					} else {
  						LOGGER.error("External subtitles file \"{}\" is unavailable", params.getSid().getName());
  					}
  				} else {
  					originalSubsFilename = dlna.getFileName();
  				}
  
  				if (originalSubsFilename != null) {
  					subsFilter.append("subtitles=").append(StringUtil.ffmpegEscape(originalSubsFilename));
  					if (params.getSid().isEmbedded()) {
  						subsFilter.append(":si=").append(params.getSid().getId());
  					}
  
  					// Set the input subtitles character encoding if not UTF-8
  					if (!params.getSid().isSubsUtf8()) {
  						if (isNotBlank(configuration.getSubtitlesCodepage())) {
  							subsFilter.append(":charenc=").append(configuration.getSubtitlesCodepage());
  						} else if (params.getSid().getSubCharacterSet() != null) {
  							subsFilter.append(":charenc=").append(params.getSid().getSubCharacterSet());
  						}
  					}
  
  					// If the FFmpeg font config is enabled than we need to add settings to the filter. TODO there could be also changed the font type. See http://ffmpeg.org/ffmpeg-filters.html#subtitles-1
  					if (configuration.isFFmpegFontConfig() && !is3D && !isSubsASS) { // Do not force style for 3D videos and ASS subtitles
  						subsFilter.append(":force_style=");
  						subsFilter.append("'");
  						String fontName = configuration.getFont();
  						if (isNotBlank(fontName)) {
  							String font = CodecUtil.isFontRegisteredInOS(fontName);
  							if (font != null) {
  								subsFilter.append("Fontname=").append(font);
  							}
  						}
  
  						// XXX (valib) If the font size is not acceptable it could be calculated better taking in to account the original video size. Unfortunately I don't know how to do that.
  						subsFilter.append(",Fontsize=").append(15 * Double.parseDouble(configuration.getAssScale()));
  						subsFilter.append(",PrimaryColour=").append(configuration.getSubsColor().getASSv4StylesHexValue());
  						subsFilter.append(",Outline=").append(configuration.getAssOutline());
  						subsFilter.append(",Shadow=").append(configuration.getAssShadow());
  						subsFilter.append(",MarginV=").append(configuration.getAssMargin());
  						subsFilter.append("'");
  					}
  				}
  			} else if (params.getSid().getType().isPicture()) {
  				StringBuilder subsPictureFilter = new StringBuilder();
  				if (params.getSid().isEmbedded()) {
  					// Embedded
  					subsPictureFilter.append("[0:v][0:s:").append(media.getSubtitlesTracks().indexOf(params.getSid())).append("]overlay");
  					isSubsManualTiming = false;
  				} else if (params.getSid().getExternalFile() != null) {
  					// External
  					videoFilterOptions.add("-i");
  					videoFilterOptions.add(params.getSid().getExternalFile().getPath());
  					subsPictureFilter.append("[0:v][1:s]overlay"); // this assumes the sub file is single-language
  				}
  				filterChain.add(0, subsPictureFilter.toString());
  			}
  
  			if (isNotBlank(subsFilter)) {
  				if (params.getTimeSeek() > 0 && isSubsManualTiming) {
  					filterChain.add("setpts=PTS+" + params.getTimeSeek() + "/TB"); // based on https://trac.ffmpeg.org/ticket/2067
  				}
  
  				filterChain.add(subsFilter.toString());
  				if (params.getTimeSeek() > 0 && isSubsManualTiming) {
  					filterChain.add("setpts=PTS-STARTPTS"); // based on https://trac.ffmpeg.org/ticket/2067
  				}
  			}
  		}
  
  		String overrideVF = renderer.getFFmpegVideoFilterOverride();
  		if (StringUtils.isNotEmpty(overrideVF)) {
  			filterChain.add(overrideVF);
  		}
  
  		// Convert 3D video to the other output 3D format or to 2D using "Output3DFormat = ml" or "Output3DFormat = mr" in the renderer conf
  		String stereoLayout = null;
  		String renderer3DOutputFormat = null;
  		if (media.get3DLayout() != null) {
  			stereoLayout = media.get3DLayout().toString().toLowerCase(Locale.ROOT);
  			renderer3DOutputFormat = params.getMediaRenderer().getOutput3DFormat();
  		}
  
  		if (
  			is3D &&
  			stereoLayout != null &&
  			isNotBlank(renderer3DOutputFormat) &&
  			!stereoLayout.equals(renderer3DOutputFormat)
  		) {
  			filterChain.add("stereo3d=" + stereoLayout + ":" + renderer3DOutputFormat);
  		}
  
  		if (filterChain.size() > 0) {
  			videoFilterOptions.add("-filter_complex");
  			videoFilterOptions.add(StringUtils.join(filterChain, ","));
  		}
  
  		return videoFilterOptions;
  	}
  
  	/**
  	 * Returns a list of <code>String</code>s representing ffmpeg output
  	 * options (i.e. options that define the output file's video codec,
  	 * audio codec and container) compatible with the renderer's
  	 * <code>TranscodeVideo</code> profile.
  	 *
  	 * @param dlna
  	 * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
  	 * @param params output parameters
  	 * @param canMuxVideoWithFFmpeg
  	 *
  	 * @return a {@link List} of <code>String</code>s representing the FFmpeg output parameters for the renderer according
  	 * to its <code>TranscodeVideo</code> profile.
  	 */
  	@SuppressWarnings("checkstyle:EmptyBlock")
  	public synchronized List<String> getVideoTranscodeOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params, boolean canMuxVideoWithFFmpeg) {
  