/* * PS3 Media Server, for streaming any medias to your PS3. * Copyright (C) 2008 A.Brochard * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms.encoders; import bsh.EvalError; import bsh.Interpreter; import com.jgoodies.forms.builder.PanelBuilder; import com.jgoodies.forms.factories.Borders; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import com.sun.jna.Platform; import java.awt.*; import java.awt.event.*; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.*; import java.util.List; import javax.swing.*; import net.pms.Messages; import net.pms.PMS; import net.pms.configuration.DeviceConfiguration; import net.pms.configuration.FormatConfiguration; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import net.pms.dlna.*; 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.newgui.components.CustomJButton; import net.pms.util.*; import static net.pms.util.AudioUtils.getLPCMChannelMappingForMencoder; import static net.pms.util.StringUtil.quoteArg; import org.apache.commons.configuration.event.ConfigurationEvent; import org.apache.commons.configuration.event.ConfigurationListener; import static org.apache.commons.lang.BooleanUtils.isTrue; import static org.apache.commons.lang3.StringUtils.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class MEncoderVideo extends Player { private static final Logger LOGGER = LoggerFactory.getLogger(MEncoderVideo.class); private static final String COL_SPEC = "left:pref, 3dlu, p:grow, 3dlu, right:p:grow, 3dlu, p:grow, 3dlu, right:p:grow,3dlu, p:grow, 3dlu, right:p:grow,3dlu, pref:grow"; private static final String ROW_SPEC = "p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 9dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p"; private static final String REMOVE_OPTION = "---REMOVE-ME---"; // use an out-of-band option that can't be confused with a real option private JTextField mencoder_noass_scale; private JTextField mencoder_noass_subpos; private JTextField mencoder_noass_blur; private JTextField mencoder_noass_outline; private JTextField mencoder_custom_options; private JTextField subq; private JCheckBox forcefps; private JCheckBox yadif; private JCheckBox scaler; private JTextField scaleX; private JTextField scaleY; private JCheckBox fc; private JCheckBox ass; private JCheckBox skipLoopFilter; private JCheckBox mencodermt; private JCheckBox videoremux; private JCheckBox normalizeaudio; private JCheckBox noskip; private JCheckBox intelligentsync; private JTextField ocw; private JTextField och; private static final String[] INVALID_CUSTOM_OPTIONS = { "-of", "-oac", "-ovc", "-mpegopts" }; private static final String INVALID_CUSTOM_OPTIONS_LIST = Arrays.toString(INVALID_CUSTOM_OPTIONS); public static final int MENCODER_MAX_THREADS = 8; public static final String ID = "mencoder"; // TODO (breaking change): most (probably all) of these // protected fields should be private. And at least two // shouldn't be fields @Deprecated protected boolean dvd; @Deprecated protected String overriddenMainArgs[]; protected boolean dtsRemux; protected boolean encodedAudioPassthrough; protected boolean pcm; protected boolean ovccopy; protected boolean ac3Remux; protected boolean isTranscodeToMPEGTS; protected boolean isTranscodeToH264; protected boolean isTranscodeToAAC; protected boolean wmv; public static final String DEFAULT_CODEC_CONF_SCRIPT = Messages.getString("MEncoderVideo.68") + Messages.getString("MEncoderVideo.70") + Messages.getString("MEncoderVideo.71") + Messages.getString("MEncoderVideo.72") + Messages.getString("MEncoderVideo.75") + Messages.getString("MEncoderVideo.76") + Messages.getString("MEncoderVideo.77") + Messages.getString("MEncoderVideo.78") + Messages.getString("MEncoderVideo.135") + "\n" + "container == iso :: -nosync\n" + "(container == avi || container == matroska) && vcodec == mpeg4 && acodec == mp3 :: -mc 0.1\n" + "container == flv :: -mc 0.1\n" + "container == mov :: -mc 0.1\n" + "container == rm :: -mc 0.1\n" + "container == mp4 && vcodec == h264 :: -mc 0.1\n" + "\n" + Messages.getString("MEncoderVideo.87") + Messages.getString("MEncoderVideo.89") + Messages.getString("MEncoderVideo.91"); @Deprecated public JCheckBox getCheckBox() { return skipLoopFilter; } public JCheckBox getNoskip() { return noskip; } @Deprecated public MEncoderVideo(PmsConfiguration configuration) { this(); } public MEncoderVideo() { } @Override public JComponent config() { // Apply the orientation for the locale ComponentOrientation orientation = ComponentOrientation.getOrientation(PMS.getLocale()); String colSpec = FormLayoutUtil.getColSpec(COL_SPEC, orientation); FormLayout layout = new FormLayout(colSpec, ROW_SPEC); PanelBuilder builder = new PanelBuilder(layout); builder.border(Borders.EMPTY); builder.opaque(false); CellConstraints cc = new CellConstraints(); JComponent cmp = builder.addSeparator(Messages.getString("NetworkTab.5"), FormLayoutUtil.flip(cc.xyw(1, 1, 15), colSpec, orientation)); cmp = (JComponent) cmp.getComponent(0); cmp.setFont(cmp.getFont().deriveFont(Font.BOLD)); mencodermt = new JCheckBox(Messages.getString("MEncoderVideo.35"), configuration.getMencoderMT()); mencodermt.setContentAreaFilled(false); mencodermt.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { configuration.setMencoderMT(mencodermt.isSelected()); } }); mencodermt.setEnabled(Platform.isWindows() || Platform.isMac()); builder.add(GuiUtil.getPreferredSizeComponent(mencodermt), FormLayoutUtil.flip(cc.xy(1, 3), colSpec, orientation)); skipLoopFilter = new JCheckBox(Messages.getString("MEncoderVideo.0"), configuration.getSkipLoopFilterEnabled()); skipLoopFilter.setContentAreaFilled(false); skipLoopFilter.setToolTipText(Messages.getString("MEncoderVideo.136")); skipLoopFilter.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setSkipLoopFilterEnabled((e.getStateChange() == ItemEvent.SELECTED)); } }); builder.add(GuiUtil.getPreferredSizeComponent(skipLoopFilter), FormLayoutUtil.flip(cc.xyw(3, 3, 12), colSpec, orientation)); noskip = new JCheckBox(Messages.getString("MEncoderVideo.2"), configuration.isMencoderNoOutOfSync()); noskip.setContentAreaFilled(false); noskip.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderNoOutOfSync((e.getStateChange() == ItemEvent.SELECTED)); } }); builder.add(GuiUtil.getPreferredSizeComponent(noskip), FormLayoutUtil.flip(cc.xy(1, 5), colSpec, orientation)); CustomJButton button = new CustomJButton(Messages.getString("MEncoderVideo.29")); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JPanel codecPanel = new JPanel(new BorderLayout()); final JTextArea textArea = new JTextArea(); textArea.setText(configuration.getMencoderCodecSpecificConfig()); textArea.setFont(new Font("Courier", Font.PLAIN, 12)); JScrollPane scrollPane = new JScrollPane(textArea); scrollPane.setPreferredSize(new Dimension(900, 100)); final JTextArea textAreaDefault = new JTextArea(); textAreaDefault.setText(DEFAULT_CODEC_CONF_SCRIPT); textAreaDefault.setBackground(Color.WHITE); textAreaDefault.setFont(new Font("Courier", Font.PLAIN, 12)); textAreaDefault.setEditable(false); textAreaDefault.setEnabled(configuration.isMencoderIntelligentSync()); JScrollPane scrollPaneDefault = new JScrollPane(textAreaDefault); scrollPaneDefault.setPreferredSize(new Dimension(900, 450)); JPanel customPanel = new JPanel(new BorderLayout()); intelligentsync = new JCheckBox(Messages.getString("MEncoderVideo.3"), configuration.isMencoderIntelligentSync()); intelligentsync.setContentAreaFilled(false); intelligentsync.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderIntelligentSync((e.getStateChange() == ItemEvent.SELECTED)); textAreaDefault.setEnabled(configuration.isMencoderIntelligentSync()); } }); JLabel label = new JLabel(Messages.getString("MEncoderVideo.33")); customPanel.add(label, BorderLayout.NORTH); customPanel.add(scrollPane, BorderLayout.SOUTH); codecPanel.add(intelligentsync, BorderLayout.NORTH); codecPanel.add(scrollPaneDefault, BorderLayout.CENTER); codecPanel.add(customPanel, BorderLayout.SOUTH); while (JOptionPane.showOptionDialog(SwingUtilities.getWindowAncestor((Component) PMS.get().getFrame()), codecPanel, Messages.getString("MEncoderVideo.34"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null) == JOptionPane.OK_OPTION) { String newCodecparam = textArea.getText(); DLNAMediaInfo fakemedia = new DLNAMediaInfo(); DLNAMediaAudio audio = new DLNAMediaAudio(); audio.setCodecA("ac3"); fakemedia.setCodecV("mpeg4"); fakemedia.setContainer("matroska"); fakemedia.setDuration(45d*60); audio.getAudioProperties().setNumberOfChannels(2); fakemedia.setWidth(1280); fakemedia.setHeight(720); audio.setSampleFrequency("48000"); fakemedia.setFrameRate("23.976"); fakemedia.getAudioTracksList().add(audio); String result[] = getSpecificCodecOptions(newCodecparam, fakemedia, new OutputParams(configuration), "dummy.mpg", "dummy.srt", false, true); if (result.length > 0 && result[0].startsWith("@@")) { String errorMessage = result[0].substring(2); JOptionPane.showMessageDialog( SwingUtilities.getWindowAncestor((Component) PMS.get().getFrame()), errorMessage, Messages.getString("Dialog.Error"), JOptionPane.ERROR_MESSAGE ); } else { configuration.setMencoderCodecSpecificConfig(newCodecparam); break; } } } }); builder.add(button, FormLayoutUtil.flip(cc.xy(1, 11), colSpec, orientation)); forcefps = new JCheckBox(Messages.getString("MEncoderVideo.4"), configuration.isMencoderForceFps()); forcefps.setContentAreaFilled(false); forcefps.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderForceFps(e.getStateChange() == ItemEvent.SELECTED); } }); builder.add(GuiUtil.getPreferredSizeComponent(forcefps), FormLayoutUtil.flip(cc.xyw(1, 7, 2), colSpec, orientation)); yadif = new JCheckBox(Messages.getString("MEncoderVideo.26"), configuration.isMencoderYadif()); yadif.setContentAreaFilled(false); yadif.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderYadif(e.getStateChange() == ItemEvent.SELECTED); } }); builder.add(GuiUtil.getPreferredSizeComponent(yadif), FormLayoutUtil.flip(cc.xyw(3, 7, 7), colSpec, orientation)); scaler = new JCheckBox(Messages.getString("MEncoderVideo.27")); scaler.setContentAreaFilled(false); scaler.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderScaler(e.getStateChange() == ItemEvent.SELECTED); scaleX.setEnabled(configuration.isMencoderScaler()); scaleY.setEnabled(configuration.isMencoderScaler()); } }); builder.add(GuiUtil.getPreferredSizeComponent(scaler), FormLayoutUtil.flip(cc.xyw(3, 5, 6), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.28"), FormLayoutUtil.flip(cc.xy(9, 5, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation)); scaleX = new JTextField("" + configuration.getMencoderScaleX()); scaleX.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { try { configuration.setMencoderScaleX(Integer.parseInt(scaleX.getText())); } catch (NumberFormatException nfe) { LOGGER.debug("Could not parse scaleX from \"" + scaleX.getText() + "\""); } } }); builder.add(scaleX, FormLayoutUtil.flip(cc.xy(11, 5), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.30"), FormLayoutUtil.flip(cc.xy(13, 5, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation)); scaleY = new JTextField("" + configuration.getMencoderScaleY()); scaleY.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { try { configuration.setMencoderScaleY(Integer.parseInt(scaleY.getText())); } catch (NumberFormatException nfe) { LOGGER.debug("Could not parse scaleY from \"" + scaleY.getText() + "\""); } } }); builder.add(scaleY, FormLayoutUtil.flip(cc.xy(15, 5), colSpec, orientation)); if (configuration.isMencoderScaler()) { scaler.setSelected(true); } else { scaleX.setEnabled(false); scaleY.setEnabled(false); } videoremux = new JCheckBox(Messages.getString("MEncoderVideo.38"), configuration.isMencoderMuxWhenCompatible()); videoremux.setContentAreaFilled(false); videoremux.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderMuxWhenCompatible((e.getStateChange() == ItemEvent.SELECTED)); } }); builder.add(GuiUtil.getPreferredSizeComponent(videoremux), FormLayoutUtil.flip(cc.xyw(1, 9, 13), colSpec, orientation)); normalizeaudio = new JCheckBox(Messages.getString("MEncoderVideo.134"), configuration.isMEncoderNormalizeVolume()); normalizeaudio.setContentAreaFilled(false); normalizeaudio.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMEncoderNormalizeVolume((e.getStateChange() == ItemEvent.SELECTED)); } }); // Uncomment this if volume normalizing in MEncoder is ever fixed. // builder.add(normalizeaudio, FormLayoutUtil.flip(cc.xyw(1, 13, 13), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.6"), FormLayoutUtil.flip(cc.xy(1, 15), colSpec, orientation)); mencoder_custom_options = new JTextField(configuration.getMencoderCustomOptions()); mencoder_custom_options.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderCustomOptions(mencoder_custom_options.getText()); } }); builder.add(mencoder_custom_options, FormLayoutUtil.flip(cc.xyw(3, 15, 13), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.93"), FormLayoutUtil.flip(cc.xy(1, 17), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.28") + " (%)", FormLayoutUtil.flip(cc.xy(1, 17, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation)); ocw = new JTextField(configuration.getMencoderOverscanCompensationWidth()); ocw.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderOverscanCompensationWidth(ocw.getText()); } }); builder.add(ocw, FormLayoutUtil.flip(cc.xy(3, 17), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.30") + " (%)", FormLayoutUtil.flip(cc.xy(5, 17), colSpec, orientation)); och = new JTextField(configuration.getMencoderOverscanCompensationHeight()); och.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderOverscanCompensationHeight(och.getText()); } }); builder.add(och, FormLayoutUtil.flip(cc.xy(7, 17), colSpec, orientation)); cmp = builder.addSeparator(Messages.getString("MEncoderVideo.8"), FormLayoutUtil.flip(cc.xyw(1, 19, 15), colSpec, orientation)); cmp = (JComponent) cmp.getComponent(0); cmp.setFont(cmp.getFont().deriveFont(Font.BOLD)); builder.addLabel(Messages.getString("MEncoderVideo.16"), FormLayoutUtil.flip(cc.xy(1, 27), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.133"), FormLayoutUtil.flip(cc.xy(1, 27, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation)); mencoder_noass_scale = new JTextField(configuration.getMencoderNoAssScale()); mencoder_noass_scale.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderNoAssScale(mencoder_noass_scale.getText()); } }); builder.addLabel(Messages.getString("MEncoderVideo.17"), FormLayoutUtil.flip(cc.xy(5, 27), colSpec, orientation)); mencoder_noass_outline = new JTextField(configuration.getMencoderNoAssOutline()); mencoder_noass_outline.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderNoAssOutline(mencoder_noass_outline.getText()); } }); builder.addLabel(Messages.getString("MEncoderVideo.18"), FormLayoutUtil.flip(cc.xy(9, 27), colSpec, orientation)); mencoder_noass_blur = new JTextField(configuration.getMencoderNoAssBlur()); mencoder_noass_blur.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderNoAssBlur(mencoder_noass_blur.getText()); } }); builder.addLabel(Messages.getString("MEncoderVideo.19"), FormLayoutUtil.flip(cc.xy(13, 27), colSpec, orientation)); mencoder_noass_subpos = new JTextField(configuration.getMencoderNoAssSubPos()); mencoder_noass_subpos.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderNoAssSubPos(mencoder_noass_subpos.getText()); } }); builder.add(mencoder_noass_scale, FormLayoutUtil.flip(cc.xy(3, 27), colSpec, orientation)); builder.add(mencoder_noass_outline, FormLayoutUtil.flip(cc.xy(7, 27), colSpec, orientation)); builder.add(mencoder_noass_blur, FormLayoutUtil.flip(cc.xy(11, 27), colSpec, orientation)); builder.add(mencoder_noass_subpos, FormLayoutUtil.flip(cc.xy(15, 27), colSpec, orientation)); ass = new JCheckBox(Messages.getString("MEncoderVideo.20"), configuration.isMencoderAss()); ass.setContentAreaFilled(false); ass.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { if (e != null) { configuration.setMencoderAss(e.getStateChange() == ItemEvent.SELECTED); } } }); builder.add(GuiUtil.getPreferredSizeComponent(ass), FormLayoutUtil.flip(cc.xy(1, 23), colSpec, orientation)); ass.getItemListeners()[0].itemStateChanged(null); fc = new JCheckBox(Messages.getString("MEncoderVideo.21"), configuration.isMencoderFontConfig()); fc.setContentAreaFilled(false); fc.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderFontConfig(e.getStateChange() == ItemEvent.SELECTED); } }); builder.add(GuiUtil.getPreferredSizeComponent(fc), FormLayoutUtil.flip(cc.xyw(3, 23, 5), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.92"), FormLayoutUtil.flip(cc.xy(1, 29), colSpec, orientation)); subq = new JTextField(configuration.getMencoderVobsubSubtitleQuality()); subq.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderVobsubSubtitleQuality(subq.getText()); } }); builder.add(subq, FormLayoutUtil.flip(cc.xyw(3, 29, 1), colSpec, orientation)); configuration.addConfigurationListener(new ConfigurationListener() { @Override public void configurationChanged(ConfigurationEvent event) { if (event.getPropertyName() == null) { return; } if ((!event.isBeforeUpdate()) && event.getPropertyName().equals(PmsConfiguration.KEY_DISABLE_SUBTITLES)) { boolean enabled = !configuration.isDisableSubtitles(); ass.setEnabled(enabled); fc.setEnabled(enabled); mencoder_noass_scale.setEnabled(enabled); mencoder_noass_outline.setEnabled(enabled); mencoder_noass_blur.setEnabled(enabled); mencoder_noass_subpos.setEnabled(enabled); ocw.setEnabled(enabled); och.setEnabled(enabled); subq.setEnabled(enabled); if (enabled) { ass.getItemListeners()[0].itemStateChanged(null); } } } }); JPanel panel = builder.getPanel(); // Apply the orientation to the panel and all components in it panel.applyComponentOrientation(orientation); return panel; } @Override public int purpose() { return VIDEO_SIMPLEFILE_PLAYER; } @Override public String id() { return ID; } @Override public boolean avisynth() { return false; } @Override public boolean isTimeSeekable() { return true; } protected String[] getDefaultArgs() { List<String> defaultArgsList = new ArrayList<>(); defaultArgsList.add("-msglevel"); defaultArgsList.add("statusline=2"); defaultArgsList.add("-oac"); if (ac3Remux || dtsRemux) { defaultArgsList.add("copy"); } else if (pcm) { defaultArgsList.add("pcm"); } else if (isTranscodeToAAC) { defaultArgsList.add("faac"); defaultArgsList.add("-faacopts"); defaultArgsList.add("br=320:mpeg=4:object=2"); } else { defaultArgsList.add("lavc"); } defaultArgsList.add("-of"); if (wmv || isTranscodeToMPEGTS) { defaultArgsList.add("lavf"); } else if (pcm && avisynth()) { defaultArgsList.add("avi"); } else if (pcm || dtsRemux || encodedAudioPassthrough) { defaultArgsList.add("rawvideo"); } else { defaultArgsList.add("mpeg"); } if (wmv) { defaultArgsList.add("-lavfopts"); defaultArgsList.add("format=asf"); } else if (isTranscodeToMPEGTS) { defaultArgsList.add("-lavfopts"); defaultArgsList.add("format=mpegts"); } defaultArgsList.add("-mpegopts"); defaultArgsList.add("format=mpeg2:muxrate=500000:vbuf_size=1194:abuf_size=64"); defaultArgsList.add("-ovc"); defaultArgsList.add(ovccopy ? "copy" : "lavc"); String[] defaultArgsArray = new String[defaultArgsList.size()]; defaultArgsList.toArray(defaultArgsArray); return defaultArgsArray; } private String[] sanitizeArgs(String[] args) { List<String> sanitized = new ArrayList<>(); int i = 0; while (i < args.length) { String name = args[i]; String value = null; for (String option : INVALID_CUSTOM_OPTIONS) { if (option.equals(name)) { if ((i + 1) < args.length) { value = " " + args[i + 1]; ++i; } else { value = ""; } LOGGER.warn( "Ignoring custom MEncoder option: {}{}; the following options cannot be changed: " + INVALID_CUSTOM_OPTIONS_LIST, name, value ); break; } } if (value == null) { sanitized.add(args[i]); } ++i; } return sanitized.toArray(new String[sanitized.size()]); } @Override public String[] args() { String args[]; String defaultArgs[] = getDefaultArgs(); if (overriddenMainArgs != null) { // add the sanitized custom MEncoder options. // not cached because they may be changed on the fly in the GUI // TODO if/when we upgrade to org.apache.commons.lang3: // args = ArrayUtils.addAll(defaultArgs, sanitizeArgs(overriddenMainArgs)) String[] sanitizedCustomArgs = sanitizeArgs(overriddenMainArgs); args = new String[defaultArgs.length + sanitizedCustomArgs.length]; System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length); System.arraycopy(sanitizedCustomArgs, 0, args, defaultArgs.length, sanitizedCustomArgs.length); } else { args = defaultArgs; } return args; } @Override public String executable() { return configuration.getMencoderPath(); } private int[] getVideoBitrateConfig(String bitrate) { int bitrates[] = new int[2]; if (bitrate.contains("(") && bitrate.contains(")")) { try { bitrates[1] = Integer.parseInt(bitrate.substring(bitrate.indexOf('(') + 1, bitrate.indexOf(')'))); } catch (NumberFormatException e) { bitrates[1] = 0; } } if (bitrate.contains("(")) { bitrate = bitrate.substring(0, bitrate.indexOf('(')).trim(); } if (isBlank(bitrate)) { bitrate = "0"; } try { bitrates[0] = (int) Double.parseDouble(bitrate); } catch (NumberFormatException e) { bitrates[0] = 0; } return bitrates; } /** * Note: This is not exact. The bitrate can go above this but it is generally pretty good. * * @return The maximum bitrate the video should be along with the buffer size using MEncoder vars */ private String addMaximumBitrateConstraints(String encodeSettings, DLNAMediaInfo media, String quality, RendererConfiguration mediaRenderer, String audioType) { // Use device-specific pms conf PmsConfiguration configuration = PMS.getConfiguration(mediaRenderer); int defaultMaxBitrates[] = getVideoBitrateConfig(configuration.getMaximumBitrate()); int rendererMaxBitrates[] = new int[2]; if (isNotEmpty(mediaRenderer.getMaxVideoBitrate())) { rendererMaxBitrates = getVideoBitrateConfig(mediaRenderer.getMaxVideoBitrate()); } // Give priority to the renderer's maximum bitrate setting over the user's setting 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)", mediaRenderer.getRendererName(), rendererMaxBitrates[0], defaultMaxBitrates[0] ); defaultMaxBitrates = rendererMaxBitrates; } else { LOGGER.trace( "Using video bitrate limit from the general configuration ({} Mb/s)", defaultMaxBitrates[0] ); } if (mediaRenderer.getCBRVideoBitrate() == 0 && !quality.contains("vrc_buf_size") && !quality.contains("vrc_maxrate") && !quality.contains("vbitrate")) { // Convert value from Mb to Kb defaultMaxBitrates[0] = 1000 * defaultMaxBitrates[0]; if (mediaRenderer.isHalveBitrate()) { defaultMaxBitrates[0] /= 2; LOGGER.trace("Halving the video bitrate limit to {} kb/s", defaultMaxBitrates[0]); } int bufSize = 1835; boolean bitrateLevel41Limited = false; boolean isXboxOneWebVideo = mediaRenderer.isXboxOne() && purpose() == VIDEO_WEBSTREAM_PLAYER; /** * Although the maximum bitrate for H.264 Level 4.1 is * officially 50,000 kbit/s, some 4.1-capable renderers * like the PS3 stutter when video exceeds roughly 31,250 * kbit/s. * * We also apply the correct buffer size in this section. */ if ((mediaRenderer.isTranscodeToH264() || mediaRenderer.isTranscodeToH265()) && !isXboxOneWebVideo) { if ( mediaRenderer.isH264Level41Limited() && defaultMaxBitrates[0] > 31250 ) { defaultMaxBitrates[0] = 31250; bitrateLevel41Limited = true; LOGGER.trace("Adjusting the video bitrate limit to the H.264 Level 4.1-safe value of 31250 kb/s"); } bufSize = defaultMaxBitrates[0]; } else { if (media.isHDVideo()) { bufSize = defaultMaxBitrates[0] / 3; } if (bufSize > 7000) { bufSize = 7000; } if (defaultMaxBitrates[1] > 0) { bufSize = defaultMaxBitrates[1]; } if (mediaRenderer.isDefaultVBVSize() && rendererMaxBitrates[1] == 0) { bufSize = 1835; } } if (!bitrateLevel41Limited) { // Make room for audio switch (audioType) { case "pcm": defaultMaxBitrates[0] -= 4600; break; case "dts": defaultMaxBitrates[0] -= 1510; break; case "aac": case "ac3": defaultMaxBitrates[0] -= configuration.getAudioBitrate(); break; default: break; } // Round down to the nearest Mb defaultMaxBitrates[0] = defaultMaxBitrates[0] / 1000 * 1000; LOGGER.trace( "Adjusting the video bitrate limit to {} kb/s to make room for audio", defaultMaxBitrates[0] ); } encodeSettings += ":vrc_maxrate=" + defaultMaxBitrates[0] + ":vrc_buf_size=" + bufSize; } return encodeSettings; } /* * Collapse the multiple internal ways of saying "subtitles are disabled" into a single method * which returns true if any of the following are true: * * 1) configuration.isDisableSubtitles() * 2) params.sid == null * 3) avisynth() */ private boolean isDisableSubtitles(OutputParams params) { return configuration.isDisableSubtitles() || (params.sid == null) || avisynth(); } @Override public ProcessWrapper launchTranscode( DLNAResource dlna, DLNAMediaInfo media, OutputParams params ) throws IOException { // Use device-specific pms conf PmsConfiguration prev = configuration; configuration = (DeviceConfiguration) params.mediaRenderer; params.manageFastStart(); boolean avisynth = avisynth(); final String filename = dlna.getSystemName(); 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); dvd = false; if (media != null && media.getDvdtrack() > 0) { dvd = true; } ovccopy = false; pcm = false; ac3Remux = false; dtsRemux = false; wmv = false; int intOCW = 0; int intOCH = 0; try { intOCW = Integer.parseInt(configuration.getMencoderOverscanCompensationWidth()); } catch (NumberFormatException e) { LOGGER.error("Cannot parse configured MEncoder overscan compensation width: \"{}\"", configuration.getMencoderOverscanCompensationWidth()); } try { intOCH = Integer.parseInt(configuration.getMencoderOverscanCompensationHeight()); } catch (NumberFormatException e) { LOGGER.error("Cannot parse configured MEncoder overscan compensation height: \"{}\"", configuration.getMencoderOverscanCompensationHeight()); } /* * 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.getHideTranscodeEnabled() && 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 && dvd) { 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 && dvd && configuration.isMencoderRemuxMPEG2() && params.mediaRenderer.isMpeg2Supported()) { String expertOptions[] = getSpecificCodecOptions( configuration.getMencoderCodecSpecificConfig(), media, params, filename, externalSubtitlesFileName, configuration.isMencoderIntelligentSync(), false ); boolean nomux = false; for (String s : expertOptions) { if (s.equals("-nomux")) { nomux = true; } } if (!nomux) { ovccopy = true; } } 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(); /** * Ignore the renderer's custom MEncoder options if a) we're streaming a DVD (i.e. via dvd://) * or b) the renderer's MEncoder options contain overscan settings (those are handled * separately) */ // XXX we should weed out the unused/unwanted settings and keep the rest // (see sanitizeArgs()) rather than ignoring the options entirely if (rendererMencoderOptions.contains("expand=") && dvd) { 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() && ( !dvd || 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() && ( !dvd || configuration.isMencoderRemuxMPEG2() ) && params.aid != null && params.aid.isDTS() && !avisynth() && params.mediaRenderer.isDTSPlayable() && !combinedCustomOptions.contains("acodec="); pcm = isTsMuxeRVideoEngineEnabled && configuration.isAudioUsePCM() && ( !dvd || 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 && ( (params.aid.isDTS() && params.aid.getAudioProperties().getNumberOfChannels() <= 6) || // disable 7.1 DTS-HD => LPCM because of channels mapping bug params.aid.isLossless() || params.aid.isTrueHD() || ( !configuration.isMencoderUsePcmForHQAudioOnly() && ( params.aid.isAC3() || params.aid.isMP3() || params.aid.isAAC() || params.aid.isVorbis() || // Disable WMA to LPCM transcoding because of mencoder's channel mapping bug // (see CodecUtil.getMixerOutput) // params.aid.isWMA() || params.aid.isMpegAudio() ) ) ) && params.mediaRenderer.isLPCMPlayable() && !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) { channels = params.aid.getAudioProperties().getNumberOfChannels(); // AC-3 remux } 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). */ channels = configuration.getAudioChannelCount(); // 5.1 max for AC-3 encoding } 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 = (dvd || filename.toLowerCase().endsWith("dvr-ms")) ? 1 : configuration.getMencoderMaxThreads(); // MEncoder loses audio/video sync if more than 4 decoder (lavdopts) threads are used. // 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 ( !dvd && ( ( 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, " "); { int i = overriddenMainArgs.length; // Old 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 && configuration.isMencoderAss() && // GUI: enable subtitles formating !foundNoassParam && // GUI: codec specific options !dvd; if (apply_ass_styling) { sb.append("-ass "); // GUI: Override ASS subtitles style if requested (always for SRT and TX3G subtitles) boolean override_ass_style = !configuration.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(' '); } // MEncoder is not compiled with fontconfig on Mac OS X, therefore // use of the "-ass" option also requires the "-font" option. if (Platform.isMac() && !sb.toString().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()); // Add to the subtitle margin if overscan compensation is being used // This keeps the subtitle text inside the frame instead of in the border if (intOCH > 0) { subtitleMargin = intOCH; } try { userMargin = Integer.parseInt(configuration.getMencoderNoAssSubPos()); } catch (NumberFormatException n) { LOGGER.debug("Could not parse subpos from \"" + configuration.getMencoderNoAssSubPos() + "\""); } subtitleMargin += userMargin; sb.append(" -subpos ").append(100 - subtitleMargin).append(' '); } // Common subtitle options // MEncoder on Mac OS X is compiled without fontconfig support. // Appending the flag will break execution, so skip it on Mac OS X. if (!Platform.isMac()) { // Use fontconfig if enabled sb.append('-').append(configuration.isMencoderFontConfig() ? "" : "no").append("fontconfig "); } // Apply DVD/VOBsub subtitle quality if (params.sid.getType() == SubtitleType.VOBSUB && configuration.getMencoderVobsubSubtitleQuality() != null) { String subtitleQuality = configuration.getMencoderVobsubSubtitleQuality(); sb.append("-spuaa ").append(subtitleQuality).append(' '); } // External subtitles file if (params.sid.isExternal()) { if (!params.sid.isExternalFileUtf()) { String subcp = null; // Append -subcp option for non UTF external subtitles if (isNotBlank(configuration.getSubtitlesCodepage())) { // Manual setting subcp = configuration.getSubtitlesCodepage(); } else if (isNotBlank(SubtitleUtils.getSubCpOptionForMencoder(params.sid))) { // Autodetect charset (blank mencoder_subcp config option) subcp = SubtitleUtils.getSubCpOptionForMencoder(params.sid); } if (isNotBlank(subcp)) { sb.append("-subcp ").append(subcp).append(' '); if (configuration.isMencoderSubFribidi()) { sb.append("-fribidi-charset ").append(subcp).append(' '); } } } } } st = new StringTokenizer(sb.toString(), " "); { int i = overriddenMainArgs.length; // Old length overriddenMainArgs = Arrays.copyOf(overriddenMainArgs, overriddenMainArgs.length + st.countTokens()); boolean handleToken = false; while (st.hasMoreTokens()) { String s = st.nextToken(); if (handleToken) { s = "-quiet"; handleToken = false; } if ((!configuration.isMencoderAss() || dvd) && s.contains("-ass")) { s = "-quiet"; handleToken = true; } overriddenMainArgs[i++] = s; } } List<String> cmdList = new ArrayList<>(); cmdList.add(executable()); // Choose which time to seek to cmdList.add("-ss"); cmdList.add((params.timeseek > 0) ? "" + params.timeseek : "0"); if (dvd) { 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 (dvd) { String dvdFileName = filename.replace("\\VIDEO_TS", ""); cmdList.add(dvdFileName); } else { cmdList.add(filename); } } } if (dvd) { 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"); boolean lavf = false; // TODO Need to add support for LAVF demuxing cmdList.add("" + (lavf ? params.aid.getId() + 1 : params.aid.getId())); } /* * Handle subtitles * * Try to reconcile the fact that the handling of "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) { // external subtitles assert params.sid.isExternal(); // confirm the mutual exclusion // Ensure that internal subtitles are not automatically loaded cmdList.add("-nosub"); if (params.sid.getType() == SubtitleType.VOBSUB) { cmdList.add("-vobsub"); cmdList.add(externalSubtitlesFileName.substring(0, externalSubtitlesFileName.length() - 4)); cmdList.add("-slang"); cmdList.add("" + params.sid.getLang()); } else 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 { cmdList.add(externalSubtitlesFileName.replace(",", "\\,")); // Commas in MEncoder separate multiple subtitle files } if (params.sid.isExternalFileUtf()) { // Append -utf8 option for UTF-8 external subtitles cmdList.add("-utf8"); } } } } // -ofps String framerate = (frameRateRatio != null) ? frameRateRatio : "24000/1001"; // where a framerate is required, use the input framerate or 24000/1001 String ofps = framerate; // Optional -fps or -mc if (configuration.isMencoderForceFps()) { if (!configuration.isFix25FPSAvMismatch()) { cmdList.add("-fps"); cmdList.add(framerate); } else if (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) { // The video resolution is too big for the renderer so we need to scale it down /* * 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 ( !dvd && ( ( ( (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.valueOf(substringBefore(resolution, "x")); scaleHeight = Integer.valueOf(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 && !dvd && !(media.getCodecV() != null && (media.getCodecV().startsWith("mpeg2")))) { cmdList.add("-lavdopts"); cmdList.add("fast"); } boolean disableMc0AndNoskip = false; // Process the options for this file in Transcoding Settings -> Mencoder -> Expert Settings: Codec-specific parameters // TODO this is better handled by a plugin with scripting support and will be removed // the parameters (expertOptions) are processed in 3 passes // 1) process expertOptions // 2) process cmdList // 3) append expertOptions to cmdList if (expertOptions != null && expertOptions.length > 0) { // remove this option (key) from the cmdList in pass 2. // if the boolean value is true, also remove the option's corresponding value Map<String, Boolean> removeCmdListOption = new HashMap<>(); // 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... removeCmdListOption.put("-ass", false); // false: option does not have a corresponding value // 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 ++i; // skip the value } 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); } // Add a -cache option for piped media (e.g. rar/zip file entries): // https://code.google.com/p/ps3mediaserver/issues/detail?id=911 if (params.stdin != null) { cmdList.add("-cache"); cmdList.add("8192"); } PipeProcess pipe = null; ProcessWrapperImpl pw; 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 { // remove the -oac switch, otherwise the "too many video packets" errors appear again for (ListIterator<String> it = cmdList.listIterator(); it.hasNext();) { String option = it.next(); if (option.equals("-oac")) { it.set("-nosound"); if (it.hasNext()) { it.next(); it.remove(); } break; } } pipe = new PipeProcess(System.currentTimeMillis() + "tsmuxerout.ts"); TsMuxeRVideo ts = new TsMuxeRVideo(); 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) { mixer = getLPCMChannelMappingForMencoder(params.aid); // LPCM always outputs 5.1/7.1 for multichannel tracks. Downmix with player if needed! } 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; } @Override public String mimeType() { return HTTPResource.VIDEO_TRANSCODE; } @Override public String name() { return "MEncoder"; } @Override public int type() { return Format.VIDEO; } private String[] getSpecificCodecOptions( String codecParam, DLNAMediaInfo media, OutputParams params, String filename, String externalSubtitlesFileName, boolean enable, boolean verifyOnly ) { StringBuilder sb = new StringBuilder(); String codecs = enable ? DEFAULT_CODEC_CONF_SCRIPT : ""; codecs += "\n" + codecParam; StringTokenizer stLines = new StringTokenizer(codecs, "\n"); try { Interpreter interpreter = new Interpreter(); interpreter.setStrictJava(true); ArrayList<String> types = CodecUtil.getPossibleCodecs(); int rank = 1; if (types != null) { for (String type : types) { int r = rank++; interpreter.set("" + type, r); String secondaryType = "dummy"; if ("matroska".equals(type)) { secondaryType = "mkv"; interpreter.set(secondaryType, r); } else if ("rm".equals(type)) { secondaryType = "rmvb"; interpreter.set(secondaryType, r); } else if ("mpeg2".startsWith(type)) { secondaryType = "mpeg2"; interpreter.set(secondaryType, r); } else if ("mpeg1video".equals(type)) { secondaryType = "mpeg1"; interpreter.set(secondaryType, r); } if (media.getContainer() != null && (media.getContainer().equals(type) || media.getContainer().equals(secondaryType))) { interpreter.set("container", r); } else if (media.getCodecV() != null && (media.getCodecV().equals(type) || media.getCodecV().equals(secondaryType))) { interpreter.set("vcodec", r); } else if (params.aid != null && params.aid.getCodecA() != null && params.aid.getCodecA().equals(type)) { interpreter.set("acodec", r); } } } else { return null; } interpreter.set("filename", filename); interpreter.set("audio", params.aid != null); interpreter.set("subtitles", params.sid != null); interpreter.set("srtfile", externalSubtitlesFileName); if (params.aid != null) { interpreter.set("samplerate", params.aid.getSampleRate()); } String frameRateNumber = media.getValidFps(false); try { if (frameRateNumber != null) { interpreter.set("framerate", Double.parseDouble(frameRateNumber)); } } catch (NumberFormatException e) { LOGGER.debug("Could not parse framerate from \"" + frameRateNumber + "\""); } interpreter.set("duration", media.getDurationInSeconds()); if (params.aid != null) { interpreter.set("channels", params.aid.getAudioProperties().getNumberOfChannels()); } interpreter.set("height", media.getHeight()); interpreter.set("width", media.getWidth()); while (stLines.hasMoreTokens()) { String line = stLines.nextToken(); if (!line.startsWith("#") && line.trim().length() > 0) { int separator = line.indexOf("::"); if (separator > -1) { String key = null; try { key = line.substring(0, separator).trim(); String value = line.substring(separator + 2).trim(); if (value.length() > 0) { if (key.length() == 0) { key = "1 == 1"; } Object result = interpreter.eval(key); if (result != null && result instanceof Boolean && (Boolean) result) { sb.append(' ').append(value); } } } catch (Throwable e) { LOGGER.debug("Error while executing: " + key + " : " + e.getMessage()); if (verifyOnly) { return new String[]{"@@Error while parsing: " + e.getMessage()}; } } } else if (verifyOnly) { return new String[]{"@@Malformatted line: " + line}; } } } } catch (EvalError e) { LOGGER.debug("BeanShell error: " + e.getMessage()); } String completeLine = sb.toString(); ArrayList<String> args = new ArrayList<>(); StringTokenizer st = new StringTokenizer(completeLine, " "); while (st.hasMoreTokens()) { String arg = st.nextToken().trim(); if (arg.length() > 0) { args.add(arg); } } String definitiveArgs[] = new String[args.size()]; args.toArray(definitiveArgs); return definitiveArgs; } /** * {@inheritDoc} */ @Override public boolean isCompatible(DLNAResource resource) { if ( PlayerUtil.isVideo(resource, Format.Identifier.ISO) || PlayerUtil.isVideo(resource, Format.Identifier.MKV) || PlayerUtil.isVideo(resource, Format.Identifier.MPG) ) { return true; } return false; } }