/* * 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 static net.pms.formats.v2.AudioUtils.getLPCMChannelMappingForMencoder; import static org.apache.commons.lang3.BooleanUtils.isTrue; import static org.apache.commons.lang3.StringUtils.defaultString; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.startsWith; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.ComponentOrientation; import java.awt.Font; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.ListIterator; import java.util.Locale; import java.util.Map; import java.util.StringTokenizer; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import javax.swing.JTextField; import javax.swing.SwingUtilities; import net.pms.Messages; import net.pms.PMS; import net.pms.configuration.FormatConfiguration; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import net.pms.dlna.DLNAMediaAudio; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.DLNAResource; import net.pms.dlna.InputFile; import net.pms.formats.Format; import net.pms.formats.v2.SubtitleType; import net.pms.formats.v2.SubtitleUtils; import net.pms.io.OutputParams; import net.pms.io.PipeIPCProcess; import net.pms.io.PipeProcess; import net.pms.io.ProcessWrapper; import net.pms.io.ProcessWrapperImpl; import net.pms.io.StreamModifier; import net.pms.network.HTTPResource; import net.pms.util.CodecUtil; import net.pms.util.FileUtil; import net.pms.util.FormLayoutUtil; import net.pms.util.PlayerUtil; import net.pms.util.ProcessUtil; import org.apache.commons.configuration.event.ConfigurationEvent; import org.apache.commons.configuration.event.ConfigurationListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import bsh.EvalError; import bsh.Interpreter; import com.jgoodies.forms.builder.PanelBuilder; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import com.sun.jna.Platform; public class MEncoderVideo extends Player { private static final Logger logger = LoggerFactory.getLogger(MEncoderVideo.class); private static final String COL_SPEC = "left:pref, 3dlu, p:grow, 3dlu, right:p:grow, 3dlu, p:grow, 3dlu, right:p:grow,3dlu, p:grow, 3dlu, right:p:grow,3dlu, pref:grow"; private static final String ROW_SPEC = "p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 9dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p, 3dlu, p"; private static final String REMOVE_OPTION = "---REMOVE-ME---"; // use an out-of-band option that can't be confused with a real option private JTextField mencoder_noass_scale; private JTextField mencoder_noass_subpos; private JTextField mencoder_noass_blur; private JTextField mencoder_noass_outline; private JTextField mencoder_custom_options; private JTextField subq; private JCheckBox forcefps; private JCheckBox yadif; private JCheckBox scaler; private JTextField scaleX; private JTextField scaleY; private JCheckBox assdefaultstyle; private JCheckBox fc; private JCheckBox ass; private JCheckBox checkBox; private JCheckBox mencodermt; private JCheckBox noskip; private JCheckBox intelligentsync; private JTextField ocw; private JTextField och; private final PmsConfiguration configuration; private static final String[] INVALID_CUSTOM_OPTIONS = { "-of", "-oac", "-ovc", "-mpegopts" }; private static final String INVALID_CUSTOM_OPTIONS_LIST = Arrays.toString(INVALID_CUSTOM_OPTIONS); public static final String ID = "mencoder"; // TODO (breaking change): most (probably all) of these // protected fields should be private. And at least two // shouldn't be fields @Deprecated protected boolean dvd; @Deprecated protected String overriddenMainArgs[]; protected boolean dtsRemux; protected boolean pcm; protected boolean ovccopy; protected boolean ac3Remux; protected boolean mpegts; protected boolean wmv; public static final String DEFAULT_CODEC_CONF_SCRIPT = Messages.getString("MEncoderVideo.68") + Messages.getString("MEncoderVideo.69") + Messages.getString("MEncoderVideo.70") + Messages.getString("MEncoderVideo.71") + Messages.getString("MEncoderVideo.72") + Messages.getString("MEncoderVideo.73") + Messages.getString("MEncoderVideo.75") + Messages.getString("MEncoderVideo.76") + Messages.getString("MEncoderVideo.77") + Messages.getString("MEncoderVideo.78") + Messages.getString("MEncoderVideo.79") + "#\n" + Messages.getString("MEncoderVideo.80") + "container == iso :: -nosync\n" + "(container == avi || container == matroska) && vcodec == mpeg4 && acodec == mp3 :: -mc 0.1\n" + "container == flv :: -mc 0.1\n" + "container == mov :: -mc 0.1\n" + "container == rm :: -mc 0.1\n" + "container == matroska && framerate == 29.97 :: -nomux -mc 0\n" + "container == mp4 && vcodec == h264 :: -mc 0.1\n" + "\n" + Messages.getString("MEncoderVideo.87") + Messages.getString("MEncoderVideo.88") + Messages.getString("MEncoderVideo.89") + Messages.getString("MEncoderVideo.91"); public JCheckBox getCheckBox() { return checkBox; } public JCheckBox getNoskip() { return noskip; } public MEncoderVideo(PmsConfiguration configuration) { this.configuration = configuration; } @Override public JComponent config() { // Apply the orientation for the locale Locale locale = new Locale(configuration.getLanguage()); ComponentOrientation orientation = ComponentOrientation.getOrientation(locale); String colSpec = FormLayoutUtil.getColSpec(COL_SPEC, orientation); FormLayout layout = new FormLayout(colSpec, ROW_SPEC); PanelBuilder builder = new PanelBuilder(layout); CellConstraints cc = new CellConstraints(); checkBox = new JCheckBox(Messages.getString("MEncoderVideo.0")); checkBox.setContentAreaFilled(false); if (configuration.getSkipLoopFilterEnabled()) { checkBox.setSelected(true); } checkBox.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setSkipLoopFilterEnabled((e.getStateChange() == ItemEvent.SELECTED)); } }); JComponent cmp = builder.addSeparator(Messages.getString("NetworkTab.5"), FormLayoutUtil.flip(cc.xyw(1, 1, 15), colSpec, orientation)); cmp = (JComponent) cmp.getComponent(0); cmp.setFont(cmp.getFont().deriveFont(Font.BOLD)); mencodermt = new JCheckBox(Messages.getString("MEncoderVideo.35")); mencodermt.setContentAreaFilled(false); if (configuration.getMencoderMT()) { mencodermt.setSelected(true); } mencodermt.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { configuration.setMencoderMT(mencodermt.isSelected()); } }); mencodermt.setEnabled(Platform.isWindows() || Platform.isMac()); builder.add(mencodermt, FormLayoutUtil.flip(cc.xy(1, 3), colSpec, orientation)); builder.add(checkBox, FormLayoutUtil.flip(cc.xyw(3, 3, 12), colSpec, orientation)); noskip = new JCheckBox(Messages.getString("MEncoderVideo.2")); noskip.setContentAreaFilled(false); if (configuration.isMencoderNoOutOfSync()) { noskip.setSelected(true); } noskip.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderNoOutOfSync((e.getStateChange() == ItemEvent.SELECTED)); } }); builder.add(noskip, FormLayoutUtil.flip(cc.xy(1, 5), colSpec, orientation)); JButton button = new JButton(Messages.getString("MEncoderVideo.29")); button.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { JPanel codecPanel = new JPanel(new BorderLayout()); final JTextArea textArea = new JTextArea(); textArea.setText(configuration.getMencoderCodecSpecificConfig()); textArea.setFont(new Font("Courier", Font.PLAIN, 12)); JScrollPane scrollPane = new JScrollPane(textArea); scrollPane.setPreferredSize(new java.awt.Dimension(900, 100)); final JTextArea textAreaDefault = new JTextArea(); textAreaDefault.setText(DEFAULT_CODEC_CONF_SCRIPT); textAreaDefault.setBackground(Color.WHITE); textAreaDefault.setFont(new Font("Courier", Font.PLAIN, 12)); textAreaDefault.setEditable(false); textAreaDefault.setEnabled(configuration.isMencoderIntelligentSync()); JScrollPane scrollPaneDefault = new JScrollPane(textAreaDefault); scrollPaneDefault.setPreferredSize(new java.awt.Dimension(900, 450)); JPanel customPanel = new JPanel(new BorderLayout()); intelligentsync = new JCheckBox(Messages.getString("MEncoderVideo.3")); intelligentsync.setContentAreaFilled(false); if (configuration.isMencoderIntelligentSync()) { intelligentsync.setSelected(true); } intelligentsync.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderIntelligentSync((e.getStateChange() == ItemEvent.SELECTED)); textAreaDefault.setEnabled(configuration.isMencoderIntelligentSync()); } }); JLabel label = new JLabel(Messages.getString("MEncoderVideo.33")); customPanel.add(label, BorderLayout.NORTH); customPanel.add(scrollPane, BorderLayout.SOUTH); codecPanel.add(intelligentsync, BorderLayout.NORTH); codecPanel.add(scrollPaneDefault, BorderLayout.CENTER); codecPanel.add(customPanel, BorderLayout.SOUTH); while (JOptionPane.showOptionDialog(SwingUtilities.getWindowAncestor((Component) PMS.get().getFrame()), codecPanel, Messages.getString("MEncoderVideo.34"), JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE, null, null, null) == JOptionPane.OK_OPTION) { String newCodecparam = textArea.getText(); DLNAMediaInfo fakemedia = new DLNAMediaInfo(); DLNAMediaAudio audio = new DLNAMediaAudio(); audio.setCodecA("ac3"); fakemedia.setCodecV("mpeg4"); fakemedia.setContainer("matroska"); fakemedia.setDuration(45d*60); audio.getAudioProperties().setNumberOfChannels(2); fakemedia.setWidth(1280); fakemedia.setHeight(720); audio.setSampleFrequency("48000"); fakemedia.setFrameRate("23.976"); fakemedia.getAudioTracksList().add(audio); String result[] = getSpecificCodecOptions(newCodecparam, fakemedia, new OutputParams(configuration), "dummy.mpg", "dummy.srt", false, true); if (result.length > 0 && result[0].startsWith("@@")) { String errorMessage = result[0].substring(2); JOptionPane.showMessageDialog( SwingUtilities.getWindowAncestor((Component) PMS.get().getFrame()), errorMessage, Messages.getString("Dialog.Error"), JOptionPane.ERROR_MESSAGE ); } else { configuration.setMencoderCodecSpecificConfig(newCodecparam); break; } } } }); builder.add(button, FormLayoutUtil.flip(cc.xy(1, 11), colSpec, orientation)); forcefps = new JCheckBox(Messages.getString("MEncoderVideo.4")); forcefps.setContentAreaFilled(false); if (configuration.isMencoderForceFps()) { forcefps.setSelected(true); } forcefps.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderForceFps(e.getStateChange() == ItemEvent.SELECTED); } }); builder.add(forcefps, FormLayoutUtil.flip(cc.xyw(1, 7, 2), colSpec, orientation)); yadif = new JCheckBox(Messages.getString("MEncoderVideo.26")); yadif.setContentAreaFilled(false); if (configuration.isMencoderYadif()) { yadif.setSelected(true); } yadif.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderYadif(e.getStateChange() == ItemEvent.SELECTED); } }); builder.add(yadif, FormLayoutUtil.flip(cc.xyw(3, 7, 7), colSpec, orientation)); scaler = new JCheckBox(Messages.getString("MEncoderVideo.27")); scaler.setContentAreaFilled(false); scaler.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderScaler(e.getStateChange() == ItemEvent.SELECTED); scaleX.setEnabled(configuration.isMencoderScaler()); scaleY.setEnabled(configuration.isMencoderScaler()); } }); builder.add(scaler, FormLayoutUtil.flip(cc.xyw(3, 5, 7), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.28"), FormLayoutUtil.flip(cc.xy(9, 5, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation)); scaleX = new JTextField("" + configuration.getMencoderScaleX()); scaleX.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { try { configuration.setMencoderScaleX(Integer.parseInt(scaleX.getText())); } catch (NumberFormatException nfe) { logger.debug("Could not parse scaleX from \"" + scaleX.getText() + "\""); } } }); builder.add(scaleX, FormLayoutUtil.flip(cc.xy(11, 5), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.30"), FormLayoutUtil.flip(cc.xy(13, 5, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation)); scaleY = new JTextField("" + configuration.getMencoderScaleY()); scaleY.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { try { configuration.setMencoderScaleY(Integer.parseInt(scaleY.getText())); } catch (NumberFormatException nfe) { logger.debug("Could not parse scaleY from \"" + scaleY.getText() + "\""); } } }); builder.add(scaleY, FormLayoutUtil.flip(cc.xy(15, 5), colSpec, orientation)); if (configuration.isMencoderScaler()) { scaler.setSelected(true); } else { scaleX.setEnabled(false); scaleY.setEnabled(false); } builder.addLabel(Messages.getString("MEncoderVideo.6"), FormLayoutUtil.flip(cc.xy(1, 13), colSpec, orientation)); mencoder_custom_options = new JTextField(configuration.getMencoderCustomOptions()); mencoder_custom_options.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderCustomOptions(mencoder_custom_options.getText()); } }); builder.add(mencoder_custom_options, FormLayoutUtil.flip(cc.xyw(3, 13, 13), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.93"), FormLayoutUtil.flip(cc.xy(1, 15), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.28") + " (%)", FormLayoutUtil.flip(cc.xy(1, 15, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation)); ocw = new JTextField(configuration.getMencoderOverscanCompensationWidth()); ocw.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderOverscanCompensationWidth(ocw.getText()); } }); builder.add(ocw, FormLayoutUtil.flip(cc.xy(3, 15), colSpec, orientation)); builder.addLabel(Messages.getString("MEncoderVideo.30") + " (%)", FormLayoutUtil.flip(cc.xy(5, 15), colSpec, orientation)); och = new JTextField(configuration.getMencoderOverscanCompensationHeight()); och.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderOverscanCompensationHeight(och.getText()); } }); builder.add(och, FormLayoutUtil.flip(cc.xy(7, 15), colSpec, orientation)); cmp = builder.addSeparator(Messages.getString("MEncoderVideo.8"), FormLayoutUtil.flip(cc.xyw(1, 17, 15), colSpec, orientation)); cmp = (JComponent) cmp.getComponent(0); cmp.setFont(cmp.getFont().deriveFont(Font.BOLD)); builder.addLabel(Messages.getString("MEncoderVideo.16"), FormLayoutUtil.flip(cc.xy(1, 27, CellConstraints.RIGHT, CellConstraints.CENTER), colSpec, orientation)); mencoder_noass_scale = new JTextField(configuration.getMencoderNoAssScale()); mencoder_noass_scale.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderNoAssScale(mencoder_noass_scale.getText()); } }); builder.addLabel(Messages.getString("MEncoderVideo.17"), FormLayoutUtil.flip(cc.xy(5, 27), colSpec, orientation)); mencoder_noass_outline = new JTextField(configuration.getMencoderNoAssOutline()); mencoder_noass_outline.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderNoAssOutline(mencoder_noass_outline.getText()); } }); builder.addLabel(Messages.getString("MEncoderVideo.18"), FormLayoutUtil.flip(cc.xy(9, 27), colSpec, orientation)); mencoder_noass_blur = new JTextField(configuration.getMencoderNoAssBlur()); mencoder_noass_blur.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderNoAssBlur(mencoder_noass_blur.getText()); } }); builder.addLabel(Messages.getString("MEncoderVideo.19"), FormLayoutUtil.flip(cc.xy(13, 27), colSpec, orientation)); mencoder_noass_subpos = new JTextField(configuration.getMencoderNoAssSubPos()); mencoder_noass_subpos.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderNoAssSubPos(mencoder_noass_subpos.getText()); } }); builder.add(mencoder_noass_scale, FormLayoutUtil.flip(cc.xy(3, 27), colSpec, orientation)); builder.add(mencoder_noass_outline, FormLayoutUtil.flip(cc.xy(7, 27), colSpec, orientation)); builder.add(mencoder_noass_blur, FormLayoutUtil.flip(cc.xy(11, 27), colSpec, orientation)); builder.add(mencoder_noass_subpos, FormLayoutUtil.flip(cc.xy(15, 27), colSpec, orientation)); ass = new JCheckBox(Messages.getString("MEncoderVideo.20")); ass.setContentAreaFilled(false); ass.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { if (e != null) { configuration.setMencoderAss(e.getStateChange() == ItemEvent.SELECTED); } } }); builder.add(ass, FormLayoutUtil.flip(cc.xy(1, 23), colSpec, orientation)); ass.setSelected(configuration.isMencoderAss()); ass.getItemListeners()[0].itemStateChanged(null); fc = new JCheckBox(Messages.getString("MEncoderVideo.21")); fc.setContentAreaFilled(false); fc.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderFontConfig(e.getStateChange() == ItemEvent.SELECTED); } }); builder.add(fc, FormLayoutUtil.flip(cc.xyw(3, 23, 5), colSpec, orientation)); fc.setSelected(configuration.isMencoderFontConfig()); assdefaultstyle = new JCheckBox(Messages.getString("MEncoderVideo.36")); assdefaultstyle.setContentAreaFilled(false); assdefaultstyle.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setMencoderAssDefaultStyle(e.getStateChange() == ItemEvent.SELECTED); } }); builder.add(assdefaultstyle, FormLayoutUtil.flip(cc.xyw(8, 23, 4), colSpec, orientation)); assdefaultstyle.setSelected(configuration.isMencoderAssDefaultStyle()); builder.addLabel(Messages.getString("MEncoderVideo.92"), FormLayoutUtil.flip(cc.xy(1, 29), colSpec, orientation)); subq = new JTextField(configuration.getMencoderVobsubSubtitleQuality()); subq.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setMencoderVobsubSubtitleQuality(subq.getText()); } }); builder.add(subq, FormLayoutUtil.flip(cc.xyw(3, 29, 1), colSpec, orientation)); configuration.addConfigurationListener(new ConfigurationListener() { @Override public void configurationChanged(ConfigurationEvent event) { if (event.getPropertyName() == null) { return; } if ((!event.isBeforeUpdate()) && event.getPropertyName().equals(PmsConfiguration.KEY_DISABLE_SUBTITLES)) { boolean enabled = !configuration.isDisableSubtitles(); ass.setEnabled(enabled); assdefaultstyle.setEnabled(enabled); fc.setEnabled(enabled); mencoder_noass_scale.setEnabled(enabled); mencoder_noass_outline.setEnabled(enabled); mencoder_noass_blur.setEnabled(enabled); mencoder_noass_subpos.setEnabled(enabled); ocw.setEnabled(enabled); och.setEnabled(enabled); subq.setEnabled(enabled); if (enabled) { ass.getItemListeners()[0].itemStateChanged(null); } } } }); JPanel panel = builder.getPanel(); // Apply the orientation to the panel and all components in it panel.applyComponentOrientation(orientation); return panel; } @Override public PlayerPurpose getPurpose() { return PlayerPurpose.VIDEO_FILE_PLAYER; } @Override public String id() { return ID; } @Override public boolean isTimeSeekable() { return true; } protected String[] getDefaultArgs() { List<String> defaultArgsList = new ArrayList<String>(); defaultArgsList.add("-msglevel"); defaultArgsList.add("statusline=2"); defaultArgsList.add("-oac"); defaultArgsList.add((ac3Remux || dtsRemux) ? "copy" : (pcm ? "pcm" : "lavc")); defaultArgsList.add("-of"); defaultArgsList.add((wmv || mpegts) ? "lavf" : ((pcm && avisynth()) ? "avi" : ((pcm || dtsRemux) ? "rawvideo" : "mpeg"))); if (wmv) { defaultArgsList.add("-lavfopts"); defaultArgsList.add("format=asf"); } else if (mpegts) { defaultArgsList.add("-lavfopts"); defaultArgsList.add("format=mpegts"); } defaultArgsList.add("-mpegopts"); defaultArgsList.add("format=mpeg2:muxrate=500000:vbuf_size=1194:abuf_size=64"); defaultArgsList.add("-ovc"); defaultArgsList.add(ovccopy ? "copy" : "lavc"); String[] defaultArgsArray = new String[defaultArgsList.size()]; defaultArgsList.toArray(defaultArgsArray); return defaultArgsArray; } /** * Returns the argument string surrounded with quotes if it contains a space, * otherwise returns the string as is. * * @param arg The argument string * @return The string, optionally in quotes. */ private String quoteArg(String arg) { if (arg == null || arg.indexOf(" ") == -1) { return arg; } StringBuilder result = new StringBuilder(); result.append("\"").append(arg).append("\""); return result.toString(); } private String[] sanitizeArgs(String[] args) { List<String> sanitized = new ArrayList<String>(); int i = 0; while (i < args.length) { String name = args[i]; String value = null; for (String option : INVALID_CUSTOM_OPTIONS) { if (option.equals(name)) { if ((i + 1) < args.length) { value = " " + args[i + 1]; ++i; } else { value = ""; } logger.warn( "Ignoring custom MEncoder option: {}{}; the following options cannot be changed: " + INVALID_CUSTOM_OPTIONS_LIST, name, value ); break; } } if (value == null) { sanitized.add(args[i]); } ++i; } return sanitized.toArray(new String[sanitized.size()]); } @Override public String[] args() { String args[]; String defaultArgs[] = getDefaultArgs(); if (overriddenMainArgs != null) { // add the sanitized custom MEncoder options. // not cached because they may be changed on the fly in the GUI // TODO if/when we upgrade to org.apache.commons.lang3: // args = ArrayUtils.addAll(defaultArgs, sanitizeArgs(overriddenMainArgs)) String[] sanitizedCustomArgs = sanitizeArgs(overriddenMainArgs); args = new String[defaultArgs.length + sanitizedCustomArgs.length]; System.arraycopy(defaultArgs, 0, args, 0, defaultArgs.length); System.arraycopy(sanitizedCustomArgs, 0, args, defaultArgs.length, sanitizedCustomArgs.length); } else { args = defaultArgs; } return args; } @Override public String executable() { return configuration.getMencoderPath(); } private int[] getVideoBitrateConfig(String bitrate) { int bitrates[] = new int[2]; if (bitrate.contains("(") && bitrate.contains(")")) { try { bitrates[1] = Integer.parseInt(bitrate.substring(bitrate.indexOf("(") + 1, bitrate.indexOf(")"))); } catch (NumberFormatException e) { bitrates[1] = 0; } } if (bitrate.contains("(")) { bitrate = bitrate.substring(0, bitrate.indexOf("(")).trim(); } if (isBlank(bitrate)) { bitrate = "0"; } try { bitrates[0] = (int) Double.parseDouble(bitrate); } catch (NumberFormatException e) { bitrates[0] = 0; } return bitrates; } /** * Note: This is not exact. The bitrate can go above this but it is generally pretty good. * @return The maximum bitrate the video should be along with the buffer size using MEncoder vars */ private String addMaximumBitrateConstraints(String encodeSettings, DLNAMediaInfo media, String quality, RendererConfiguration mediaRenderer, String audioType) { int defaultMaxBitrates[] = getVideoBitrateConfig(configuration.getMaximumBitrate()); int rendererMaxBitrates[] = new int[2]; if (mediaRenderer.getMaxVideoBitrate() != null) { rendererMaxBitrates = getVideoBitrateConfig(mediaRenderer.getMaxVideoBitrate()); } if ((rendererMaxBitrates[0] > 0) && ((defaultMaxBitrates[0] == 0) || (rendererMaxBitrates[0] < defaultMaxBitrates[0]))) { defaultMaxBitrates = rendererMaxBitrates; } if (mediaRenderer.getCBRVideoBitrate() == 0 && defaultMaxBitrates[0] > 0 && !quality.contains("vrc_buf_size") && !quality.contains("vrc_maxrate") && !quality.contains("vbitrate")) { // Convert value from Mb to Kb defaultMaxBitrates[0] = 1000 * defaultMaxBitrates[0]; // Half it since it seems to send up to 1 second of video in advance defaultMaxBitrates[0] = defaultMaxBitrates[0] / 2; int bufSize = 1835; if (media.isHDVideo()) { bufSize = defaultMaxBitrates[0] / 3; } if (bufSize > 7000) { bufSize = 7000; } if (defaultMaxBitrates[1] > 0) { bufSize = defaultMaxBitrates[1]; } if (mediaRenderer.isDefaultVBVSize() && rendererMaxBitrates[1] == 0) { bufSize = 1835; } // Make room for audio // If audio is PCM, subtract 4600kb/s if ("pcm".equals(audioType)) { defaultMaxBitrates[0] = defaultMaxBitrates[0] - 4600; } // If audio is DTS, subtract 1510kb/s else if ("dts".equals(audioType)) { defaultMaxBitrates[0] = defaultMaxBitrates[0] - 1510; } // If audio is AC3, subtract 640kb/s to be safe else if ("ac3".equals(audioType)) { defaultMaxBitrates[0] = defaultMaxBitrates[0] - 640; } // Round down to the nearest Mb defaultMaxBitrates[0] = defaultMaxBitrates[0] / 1000 * 1000; encodeSettings += ":vrc_maxrate=" + defaultMaxBitrates[0] + ":vrc_buf_size=" + bufSize; } return encodeSettings; } /* * collapse the multiple internal ways of saying "subtitles are disabled" into a single method * which returns true if any of the following are true: * * 1) configuration.isMencoderDisableSubs() * 2) params.sid == null * 3) avisynth() */ private boolean isDisableSubtitles(OutputParams params) { return configuration.isDisableSubtitles() || (params.sid == null) || avisynth(); } @Override public ProcessWrapper launchTranscode( DLNAResource dlna, DLNAMediaInfo media, OutputParams params ) throws IOException { params.manageFastStart(); boolean avisynth = avisynth(); final String filename = dlna.getSystemName(); setAudioAndSubs(filename, media, params, configuration); String externalSubtitlesFileName = null; if (params.sid != null && params.sid.isExternal()) { if (params.sid.isExternalFileUtf16()) { // convert UTF-16 -> UTF-8 File convertedSubtitles = new File(PMS.getConfiguration().getTempFolder(), "utf8_" + params.sid.getExternalFile().getName()); FileUtil.convertFileFromUtf16ToUtf8(params.sid.getExternalFile(), convertedSubtitles); externalSubtitlesFileName = ProcessUtil.getShortFileNameIfWideChars(convertedSubtitles.getAbsolutePath()); } else { externalSubtitlesFileName = ProcessUtil.getShortFileNameIfWideChars(params.sid.getExternalFile().getAbsolutePath()); } } InputFile newInput = new InputFile(); newInput.setFilename(filename); newInput.setPush(params.stdin); dvd = false; if (media != null && media.getDvdtrack() > 0) { dvd = true; } ovccopy = false; pcm = false; ac3Remux = false; dtsRemux = false; wmv = false; int intOCW = 0; int intOCH = 0; try { intOCW = Integer.parseInt(configuration.getMencoderOverscanCompensationWidth()); } catch (NumberFormatException e) { logger.error("Cannot parse configured MEncoder overscan compensation width: \"{}\"", configuration.getMencoderOverscanCompensationWidth()); } try { intOCH = Integer.parseInt(configuration.getMencoderOverscanCompensationHeight()); } catch (NumberFormatException e) { logger.error("Cannot parse configured MEncoder overscan compensation height: \"{}\"", configuration.getMencoderOverscanCompensationHeight()); } if (params.sid == null && dvd && configuration.isMencoderRemuxMPEG2() && params.mediaRenderer.isMpeg2Supported()) { String expertOptions[] = getSpecificCodecOptions( configuration.getMencoderCodecSpecificConfig(), media, params, filename, externalSubtitlesFileName, configuration.isMencoderIntelligentSync(), false ); boolean nomux = false; for (String s : expertOptions) { if (s.equals("-nomux")) { nomux = true; } } if (!nomux) { ovccopy = true; } } String vcodec = "mpeg2video"; if (params.mediaRenderer.isTranscodeToWMV()) { wmv = true; vcodec = "wmv2"; // http://wiki.megaframe.org/wiki/Ubuntu_XBOX_360#MEncoder not usable in streaming } mpegts = params.mediaRenderer.isTranscodeToMPEGTSAC3(); /* Disable AC-3 remux for stereo tracks with 384 kbits bitrate and PS3 renderer (PS3 FW bug?) TODO check new firmwares Commented out until we can find a way to detect when a video has an audio track that switches from 2 to 6 channels because MEncoder can't handle those files, which are very common these days. */ // final boolean ps3_and_stereo_and_384_kbits = params.aid != null // && (params.mediaRenderer.isPS3() && params.aid.getAudioProperties().getNumberOfChannels() == 2) // && (params.aid.getBitRate() > 370000 && params.aid.getBitRate() < 400000); final boolean ps3_and_stereo_and_384_kbits = false; final boolean isTSMuxerVideoEngineEnabled = PMS.getConfiguration().getEnginesAsList().contains(TsMuxeRVideo.ID); final boolean mencoderAC3RemuxAudioDelayBug = (params.aid != null) && (params.aid.getAudioProperties().getAudioDelay() != 0) && (params.timeseek == 0); if (!mencoderAC3RemuxAudioDelayBug && configuration.isAudioRemuxAC3() && params.aid != null && params.aid.isAC3() && !ps3_and_stereo_and_384_kbits && !avisynth() && params.mediaRenderer.isTranscodeToAC3()) { // AC3 remux takes priority ac3Remux = true; } else { // now check for DTS remux and LPCM streaming dtsRemux = isTSMuxerVideoEngineEnabled && configuration.isAudioEmbedDtsInPcm() && ( !dvd || configuration.isMencoderRemuxMPEG2() ) && params.aid != null && params.aid.isDTS() && !avisynth() && params.mediaRenderer.isDTSPlayable(); pcm = isTSMuxerVideoEngineEnabled && configuration.isAudioUsePCM() && ( !dvd || configuration.isMencoderRemuxMPEG2() ) // disable LPCM transcoding for MP4 container with non-H264 video as workaround for mencoder's A/V sync bug && !(media.getContainer().equals("mp4") && !media.getCodecV().equals("h264")) && params.aid != null && ( (params.aid.isDTS() && params.aid.getAudioProperties().getNumberOfChannels() <= 6) || // disable 7.1 DTS-HD => LPCM because of channels mapping bug params.aid.isLossless() || params.aid.isTrueHD() || ( !configuration.isMencoderUsePcmForHQAudioOnly() && ( params.aid.isAC3() || params.aid.isMP3() || params.aid.isAAC() || params.aid.isVorbis() || // disable WMA to LPCM transcoding because of mencoder's channel mapping bug // (see CodecUtil.getMixerOutput) // params.aid.isWMA() || params.aid.isMpegAudio() ) ) ) && params.mediaRenderer.isLPCMPlayable(); } if (dtsRemux || pcm) { params.losslessaudio = true; params.forceFps = media.getValidFps(false); } // mpeg2 remux still buggy with mencoder :\ // TODO when we can still use it? ovccopy = false; if (pcm && avisynth()) { params.avidemux = true; } int channels; if (ac3Remux) { channels = params.aid.getAudioProperties().getNumberOfChannels(); // ac3 remux } else if (dtsRemux || wmv) { channels = 2; } else if (pcm) { channels = params.aid.getAudioProperties().getNumberOfChannels(); } else { channels = configuration.getAudioChannelCount(); // 5.1 max for ac3 encoding } logger.trace("channels=" + channels); String add = ""; String rendererMencoderOptions = params.mediaRenderer.getCustomMencoderOptions(); // default: empty string String globalMencoderOptions = configuration.getMencoderCustomOptions(); // default: empty string if (params.mediaRenderer.isPadVideoWithBlackBordersTo169AR()) { rendererMencoderOptions += " -vf softskip,expand=::::1:16/9:4"; } String combinedCustomOptions = defaultString(globalMencoderOptions) + " " + defaultString(rendererMencoderOptions); if (!combinedCustomOptions.contains("-lavdopts")) { add = " -lavdopts debug=0"; } if (isNotBlank(rendererMencoderOptions)) { // don't use the renderer-specific options if they break DVD streaming // XXX we should weed out the unused/unwanted settings and keep the rest // (see sanitizeArgs()) rather than ignoring the options entirely if (dvd && rendererMencoderOptions.contains("expand=")) { logger.warn("renderer MEncoder options are incompatible with DVD streaming; ignoring: " + rendererMencoderOptions); rendererMencoderOptions = null; } } StringTokenizer st = new StringTokenizer( "-channels " + channels + (isNotBlank(globalMencoderOptions) ? " " + globalMencoderOptions : "") + (isNotBlank(rendererMencoderOptions) ? " " + rendererMencoderOptions : "") + add, " " ); // XXX why does this field (which is used to populate the array returned by args(), // called below) store the renderer-specific (i.e. not global) MEncoder options? overriddenMainArgs = new String[st.countTokens()]; { int nThreads = (dvd || filename.toLowerCase().endsWith("dvr-ms")) ? 1 : configuration.getMencoderMaxThreads(); boolean handleToken = false; int i = 0; while (st.hasMoreTokens()) { String token = st.nextToken().trim(); if (handleToken) { token += ":threads=" + nThreads; if (configuration.getSkipLoopFilterEnabled() && !avisynth()) { token += ":skiploopfilter=all"; } handleToken = false; } if (token.toLowerCase().contains("lavdopts")) { handleToken = true; } overriddenMainArgs[i++] = token; } } if (configuration.getMPEG2MainSettings() != null) { String mpeg2Options = configuration.getMPEG2MainSettings(); String mpeg2OptionsRenderer = params.mediaRenderer.getCustomMEncoderMPEG2Options(); // Renderer settings take priority over user settings if (isNotBlank(mpeg2OptionsRenderer)) { mpeg2Options = mpeg2OptionsRenderer; } else { // Remove comment from the value if (mpeg2Options.contains("/*")) { mpeg2Options = mpeg2Options.substring(mpeg2Options.indexOf("/*")); } // Find out the maximum bandwidth we are supposed to use int defaultMaxBitrates[] = getVideoBitrateConfig(configuration.getMaximumBitrate()); int rendererMaxBitrates[] = new int[2]; if (params.mediaRenderer.getMaxVideoBitrate() != null) { rendererMaxBitrates = getVideoBitrateConfig(params.mediaRenderer.getMaxVideoBitrate()); } if ((rendererMaxBitrates[0] > 0) && (rendererMaxBitrates[0] < defaultMaxBitrates[0])) { defaultMaxBitrates = rendererMaxBitrates; } int maximumBitrate = defaultMaxBitrates[0]; // Determine a good quality setting based on video attributes if (mpeg2Options.contains("Automatic")) { mpeg2Options = "keyint=5:vqscale=1:vqmin=2:vqmax=3"; // It has been reported that non-PS3 renderers prefer keyint 5 but prefer it for PS3 because it lowers the average bitrate if (params.mediaRenderer.isPS3()) { mpeg2Options = "keyint=25:vqscale=1:vqmin=2:vqmax=3"; } if (mpeg2Options.contains("Wireless") || maximumBitrate < 70) { // Lower quality for 720p+ content if (media.getWidth() > 1280) { mpeg2Options = "keyint=25:vqmax=7:vqmin=2"; } else if (media.getWidth() > 720) { mpeg2Options = "keyint=25:vqmax=5:vqmin=2"; } } } } // Ditlew - WDTV Live (+ other byte asking clients), CBR. This probably ought to be placed in addMaximumBitrateConstraints(..) int cbr_bitrate = params.mediaRenderer.getCBRVideoBitrate(); String cbr_settings = (cbr_bitrate > 0) ? ":vrc_buf_size=5000:vrc_minrate=" + cbr_bitrate + ":vrc_maxrate=" + cbr_bitrate + ":vbitrate=" + ((cbr_bitrate > 16000) ? cbr_bitrate * 1000 : cbr_bitrate) : ""; String encodeSettings = "-lavcopts autoaspect=1:vcodec=" + vcodec + (wmv && !params.mediaRenderer.isXBOX() ? ":acodec=wmav2:abitrate=448" : (cbr_settings + ":acodec=" + (configuration.isMencoderAc3Fixed() ? "ac3_fixed" : "ac3") + ":abitrate=" + CodecUtil.getAC3Bitrate(configuration, params.aid))) + ":threads=" + (wmv && !params.mediaRenderer.isXBOX() ? 1 : configuration.getMencoderMaxThreads()) + ("".equals(mpeg2Options) ? "" : ":" + mpeg2Options); String audioType = "ac3"; if (dtsRemux) { audioType = "dts"; } else if (pcm) { audioType = "pcm"; } encodeSettings = addMaximumBitrateConstraints(encodeSettings, media, mpeg2Options, params.mediaRenderer, audioType); st = new StringTokenizer(encodeSettings, " "); { int i = overriddenMainArgs.length; // Old length overriddenMainArgs = Arrays.copyOf(overriddenMainArgs, overriddenMainArgs.length + st.countTokens()); while (st.hasMoreTokens()) { overriddenMainArgs[i++] = st.nextToken(); } } } boolean foundNoassParam = false; if (media != null) { String expertOptions [] = getSpecificCodecOptions( configuration.getMencoderCodecSpecificConfig(), media, params, filename, externalSubtitlesFileName, configuration.isMencoderIntelligentSync(), false ); for (String s : expertOptions) { if (s.equals("-noass")) { foundNoassParam = true; } } } ArrayList<String> subtitleArgs = new ArrayList<String>(); // Set subtitles options if (!configuration.isDisableSubtitles() && !avisynth() && params.sid != null) { int subtitleMargin = 0; int userMargin = 0; // Use ASS flag (and therefore ASS font styles) for all subtitled files except vobsub, PGS and dvd boolean apply_ass_styling = params.sid.getType() != SubtitleType.VOBSUB && params.sid.getType() != SubtitleType.PGS && configuration.isMencoderAss() && // GUI: enable subtitles formating !foundNoassParam && // GUI: codec specific options !dvd; if (apply_ass_styling) { subtitleArgs.add("-ass"); // GUI: Override ASS subtitles style if requested (always for SRT and TX3G subtitles) boolean override_ass_style = !configuration.isMencoderAssDefaultStyle() || params.sid.getType() == SubtitleType.SUBRIP || params.sid.getType() == SubtitleType.TX3G; if (override_ass_style) { String assSubColor = "ffffff00"; if (configuration.getSubsColor() != 0) { assSubColor = Integer.toHexString(configuration.getSubsColor()); if (assSubColor.length() > 2) { assSubColor = assSubColor.substring(2) + "00"; } } subtitleArgs.add("-ass-color"); subtitleArgs.add(assSubColor); subtitleArgs.add("-ass-border-color"); subtitleArgs.add("00000000"); subtitleArgs.add("-ass-font-scale"); subtitleArgs.add(configuration.getAssScale()); StringBuilder assForceStyle = new StringBuilder(); // set subtitles font if (configuration.getFont() != null && configuration.getFont().length() > 0) { // set font with -font option, workaround for // https://github.com/Happy-Neko/ps3mediaserver/commit/52e62203ea12c40628de1869882994ce1065446a#commitcomment-990156 bug subtitleArgs.add("-font"); subtitleArgs.add(configuration.getFont()); assForceStyle.append("FontName=").append(quoteArg(configuration.getFont())).append(","); } else { String font = CodecUtil.getDefaultFontPath(); if (isNotBlank(font)) { // Variable "font" contains a font path instead of a font name. // Does "-ass-force-style" support font paths? In tests on OS X // the font path is ignored (Outline, Shadow and MarginV are // used, though) and the "-font" definition is used instead. // See: https://github.com/ps3mediaserver/ps3mediaserver/pull/14 subtitleArgs.add("-font"); subtitleArgs.add(font); assForceStyle.append("FontName=").append(quoteArg(font)).append(","); } else { subtitleArgs.add("-font"); subtitleArgs.add("Arial"); assForceStyle.append("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; } assForceStyle.append("Outline=").append(configuration.getAssOutline()); assForceStyle.append(",Shadow=").append(configuration.getAssShadow()); try { userMargin = Integer.parseInt(configuration.getAssMargin()); } catch (NumberFormatException n) { logger.debug("Could not parse SSA margin from \"" + configuration.getAssMargin() + "\""); } subtitleMargin = subtitleMargin + userMargin; assForceStyle.append(",MarginV=").append(subtitleMargin); subtitleArgs.add("-ass-force-style"); subtitleArgs.add(assForceStyle.toString()); } else if (intOCH > 0) { subtitleArgs.add("-ass-force-style"); subtitleArgs.add("MarginV=" + subtitleMargin); } // MEncoder is not compiled with fontconfig on Mac OS X, therefore // use of the "-ass" option also requires the "-font" option. if (Platform.isMac() && !subtitleArgs.contains("-font")) { String font = CodecUtil.getDefaultFontPath(); if (isNotBlank(font)) { subtitleArgs.add("-font"); subtitleArgs.add(font); } } // Workaround for MPlayer #2041, remove when that bug is fixed if (!params.sid.isEmbedded()) { subtitleArgs.add("-noflip-hebrew"); } // use PLAINTEXT formating } else { // set subtitles font if (configuration.getFont() != null && configuration.getFont().length() > 0) { subtitleArgs.add("-font"); subtitleArgs.add(configuration.getFont()); } else { String font = CodecUtil.getDefaultFontPath(); if (isNotBlank(font)) { subtitleArgs.add("-font"); subtitleArgs.add(font); } } subtitleArgs.add("-subfont-text-scale"); subtitleArgs.add(configuration.getMencoderNoAssScale()); subtitleArgs.add("-subfont-outline"); subtitleArgs.add(configuration.getMencoderNoAssOutline()); subtitleArgs.add("-subfont-blur"); subtitleArgs.add(configuration.getMencoderNoAssBlur()); // Add to the subtitle margin if overscan compensation is being used // This keeps the subtitle text inside the frame instead of in the border if (intOCH > 0) { subtitleMargin = intOCH; } try { userMargin = Integer.parseInt(configuration.getMencoderNoAssSubPos()); } catch (NumberFormatException n) { logger.debug("Could not parse subpos from \"" + configuration.getMencoderNoAssSubPos() + "\""); } subtitleMargin = subtitleMargin + userMargin; subtitleArgs.add("-subpos"); subtitleArgs.add(String.valueOf(100 - subtitleMargin)); } // 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 if (configuration.isMencoderFontConfig()) { subtitleArgs.add("-fontconfig"); } else { subtitleArgs.add("-nofontconfig"); } } // Apply DVD/VOBSUB subtitle quality if (params.sid.getType() == SubtitleType.VOBSUB && configuration.getMencoderVobsubSubtitleQuality() != null) { String subtitleQuality = configuration.getMencoderVobsubSubtitleQuality(); subtitleArgs.add("-spuaa"); subtitleArgs.add(subtitleQuality); } // 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)) { subtitleArgs.add("-subcp"); subtitleArgs.add(subcp); if (configuration.isMencoderSubFribidi()) { subtitleArgs.add("-fribidi-charset"); subtitleArgs.add(subcp); } } } } } int index = overriddenMainArgs.length; overriddenMainArgs = Arrays.copyOf(overriddenMainArgs, overriddenMainArgs.length + subtitleArgs.size()); for (String subtitleArg : subtitleArgs) { overriddenMainArgs[index] = subtitleArg; index++; } List<String> cmdList = new ArrayList<String>(); cmdList.add(executable()); // timeseek // XXX -ss 0 is is included for parity with the old (cmdArray) code: it may be possible to omit it cmdList.add("-ss"); cmdList.add((params.timeseek > 0) ? "" + params.timeseek : "0"); if (dvd) { cmdList.add("-dvd-device"); } // input filename if (avisynth && !filename.toLowerCase().endsWith(".iso")) { File avsFile = FFmpegAviSynthVideo.getAVSScript(filename, params.sid, params.fromFrame, params.toFrame); cmdList.add(ProcessUtil.getShortFileNameIfWideChars(avsFile.getAbsolutePath())); } else { if (params.stdin != null) { cmdList.add("-"); } else { cmdList.add(filename); } } if (dvd) { cmdList.add("dvd://" + media.getDvdtrack()); } for (String arg : args()) { if (arg.contains("format=mpeg2") && media.getAspect() != null && media.getValidAspect(true) != null) { cmdList.add(arg + ":vaspect=" + media.getValidAspect(true)); } else { cmdList.add(arg); } } if (!dtsRemux && !pcm && !avisynth() && params.aid != null && media.getAudioTracksList().size() > 1) { cmdList.add("-aid"); boolean lavf = false; // TODO Need to add support for LAVF demuxing cmdList.add("" + (lavf ? params.aid.getId() + 1 : params.aid.getId())); } /* * handle subtitles * * try to reconcile the fact that the handling of "Disable subtitles" is spread out * over net.pms.encoders.Player.setAudioAndSubs and here by setting both of MEncoder's "disable * subs" options if any of the internal conditions for disabling subtitles are met. */ if (isDisableSubtitles(params)) { // Ensure that internal subtitles are not automatically loaded // MKV: in some circumstances, MEncoder automatically selects an internal sub unless we explicitly disable (internal) subtitles // http://www.ps3mediaserver.org/forum/viewtopic.php?f=14&t=15891 cmdList.add("-nosub"); // Ensure that external subtitles are not automatically loaded cmdList.add("-noautosub"); } else { // note: isEmbedded() and isExternal() are mutually exclusive if (params.sid.isEmbedded()) { // internal (embedded) subs // Ensure that external subtitles are not automatically loaded cmdList.add("-noautosub"); // Specify which internal subtitle we want cmdList.add("-sid"); cmdList.add("" + params.sid.getId()); } else if (externalSubtitlesFileName != null) { // external subtitles assert params.sid.isExternal(); // confirm the mutual exclusion // Ensure that internal subtitles are not automatically loaded cmdList.add("-nosub"); if (params.sid.getType() == SubtitleType.VOBSUB) { cmdList.add("-vobsub"); cmdList.add(externalSubtitlesFileName.substring(0, externalSubtitlesFileName.length() - 4)); cmdList.add("-slang"); cmdList.add("" + params.sid.getLang()); } else { cmdList.add("-sub"); cmdList.add(externalSubtitlesFileName.replace(",", "\\,")); // Commas in MEncoder separate multiple subtitle files if (params.sid.isExternalFileUtf()) { // append -utf8 option for UTF-8 external subtitles cmdList.add("-utf8"); } } } } // -ofps String validFramerate = (media != null) ? media.getValidFps(true) : null; // optional input framerate: may be null String framerate = (validFramerate != null) ? validFramerate : "24000/1001"; // where a framerate is required, use the input framerate or 24000/1001 String ofps = framerate; // optional -fps or -mc if (configuration.isMencoderForceFps()) { if (!configuration.isFix25FPSAvMismatch()) { cmdList.add("-fps"); cmdList.add(framerate); } else if (validFramerate != null) { // XXX not sure why this "fix" requires the input to have a valid framerate, but that's the logic in the old (cmdArray) code cmdList.add("-mc"); cmdList.add("0.005"); ofps = "25"; } } cmdList.add("-ofps"); cmdList.add(ofps); /* * TODO: Move the following block up with the rest of the * subtitle stuff */ // external subtitles file if (!configuration.isDisableSubtitles() && !avisynth() && params.sid != null && params.sid.isExternal()) { if (params.sid.getType() == SubtitleType.VOBSUB) { cmdList.add("-vobsub"); cmdList.add(externalSubtitlesFileName.substring(0, externalSubtitlesFileName.length() - 4)); cmdList.add("-slang"); cmdList.add("" + params.sid.getLang()); } else { cmdList.add("-sub"); cmdList.add(externalSubtitlesFileName.replace(",", "\\,")); // Commas in MEncoder separate multiple subtitle files if (params.sid.isExternalFileUtf()) { // append -utf8 option for UTF-8 external subtitles cmdList.add("-utf8"); } } } if (filename.toLowerCase().endsWith(".evo")) { cmdList.add("-psprobe"); cmdList.add("10000"); } boolean deinterlace = configuration.isMencoderYadif(); // Check if the media renderer supports this resolution boolean isResolutionTooHighForRenderer = params.mediaRenderer.isVideoRescale() && media != null && ( (media.getWidth() > params.mediaRenderer.getMaxVideoWidth()) || (media.getHeight() > params.mediaRenderer.getMaxVideoHeight()) ); // Video scaler and overscan compensation boolean scaleBool = isResolutionTooHighForRenderer || (configuration.isMencoderScaler() && (configuration.getMencoderScaleX() != 0 || configuration.getMencoderScaleY() != 0)) || (intOCW > 0 || intOCH > 0); if ((deinterlace || scaleBool) && !avisynth()) { StringBuilder vfValueOverscanPrepend = new StringBuilder(); StringBuilder vfValueOverscanMiddle = new StringBuilder(); StringBuilder vfValueVS = new StringBuilder(); StringBuilder vfValueComplete = new StringBuilder(); String deinterlaceComma = ""; int scaleWidth = 0; int scaleHeight = 0; double rendererAspectRatio; // Set defaults if (media != null && media.getWidth() > 0 && media.getHeight() > 0) { scaleWidth = media.getWidth(); scaleHeight = media.getHeight(); } /* * Implement overscan compensation settings * * This feature takes into account aspect ratio, * making it less blunt than the Video Scaler option */ if (intOCW > 0 || intOCH > 0) { int intOCWPixels = (media.getWidth() / 100) * intOCW; int intOCHPixels = (media.getHeight() / 100) * intOCH; scaleWidth = scaleWidth + intOCWPixels; scaleHeight = scaleHeight + intOCHPixels; // See if the video needs to be scaled down if ( params.mediaRenderer.isVideoRescale() && ( (scaleWidth > params.mediaRenderer.getMaxVideoWidth()) || (scaleHeight > params.mediaRenderer.getMaxVideoHeight()) ) ) { double overscannedAspectRatio = scaleWidth / scaleHeight; rendererAspectRatio = params.mediaRenderer.getMaxVideoWidth() / params.mediaRenderer.getMaxVideoHeight(); if (overscannedAspectRatio > rendererAspectRatio) { // Limit video by width scaleWidth = params.mediaRenderer.getMaxVideoWidth(); scaleHeight = (int) Math.round(params.mediaRenderer.getMaxVideoWidth() / overscannedAspectRatio); } else { // Limit video by height scaleWidth = (int) Math.round(params.mediaRenderer.getMaxVideoHeight() * overscannedAspectRatio); scaleHeight = params.mediaRenderer.getMaxVideoHeight(); } } vfValueOverscanPrepend.append("softskip,expand=-").append(intOCWPixels).append(":-").append(intOCHPixels); vfValueOverscanMiddle.append(",scale=").append(scaleWidth).append(":").append(scaleHeight); } /* * Video Scaler and renderer-specific resolution-limiter */ if (configuration.isMencoderScaler()) { // Use the manual, user-controlled scaler if (configuration.getMencoderScaleX() != 0) { if (configuration.getMencoderScaleX() <= params.mediaRenderer.getMaxVideoWidth()) { scaleWidth = configuration.getMencoderScaleX(); } else { scaleWidth = params.mediaRenderer.getMaxVideoWidth(); } } if (configuration.getMencoderScaleY() != 0) { if (configuration.getMencoderScaleY() <= params.mediaRenderer.getMaxVideoHeight()) { scaleHeight = configuration.getMencoderScaleY(); } else { scaleHeight = params.mediaRenderer.getMaxVideoHeight(); } } logger.info("Setting video resolution to: " + scaleWidth + "x" + scaleHeight + ", your Video Scaler setting"); vfValueVS.append("scale=").append(scaleWidth).append(":").append(scaleHeight); /* * The video resolution is too big for the renderer so we need to scale it down */ } else if ( media != null && media.getWidth() > 0 && media.getHeight() > 0 && ( media.getWidth() > params.mediaRenderer.getMaxVideoWidth() || media.getHeight() > params.mediaRenderer.getMaxVideoHeight() ) ) { double videoAspectRatio = (double) media.getWidth() / (double) media.getHeight(); rendererAspectRatio = (double) params.mediaRenderer.getMaxVideoWidth() / (double) params.mediaRenderer.getMaxVideoHeight(); /* * First we deal with some exceptions, then if they are not matched we will * let the renderer limits work. * * This is so, for example, we can still define a maximum resolution of * 1920x1080 in the renderer config file but still support 1920x1088 when * it's needed, otherwise we would either resize 1088 to 1080, meaning the * ugly (unused) bottom 8 pixels would be displayed, or we would limit all * videos to 1088 causing the bottom 8 meaningful pixels to be cut off. */ if (media.getWidth() == 3840 && media.getHeight() == 1080) { // Full-SBS scaleWidth = 1920; scaleHeight = 1080; } else if (media.getWidth() == 1920 && media.getHeight() == 2160) { // Full-OU scaleWidth = 1920; scaleHeight = 1080; } else if (media.getWidth() == 1920 && media.getHeight() == 1088) { // SAT capture scaleWidth = 1920; scaleHeight = 1088; } else { // Passed the exceptions, now we allow the renderer to define the limits if (videoAspectRatio > rendererAspectRatio) { scaleWidth = params.mediaRenderer.getMaxVideoWidth(); scaleHeight = (int) Math.round(params.mediaRenderer.getMaxVideoWidth() / videoAspectRatio); } else { scaleWidth = (int) Math.round(params.mediaRenderer.getMaxVideoHeight() * videoAspectRatio); scaleHeight = params.mediaRenderer.getMaxVideoHeight(); } } logger.info("Setting video resolution to: " + scaleWidth + "x" + scaleHeight + ", the maximum your renderer supports"); vfValueVS.append("scale=").append(scaleWidth).append(":").append(scaleHeight); } // Put the string together taking into account overscan compensation and video scaler if (intOCW > 0 || intOCH > 0) { vfValueComplete.append(vfValueOverscanPrepend).append(vfValueOverscanMiddle).append(",harddup"); logger.info("Setting video resolution to: " + scaleWidth + "x" + scaleHeight + ", to fit your overscan compensation"); } else { vfValueComplete.append(vfValueVS); } if (deinterlace) { deinterlaceComma = ","; } String vfValue = (deinterlace ? "yadif" : "") + (scaleBool ? deinterlaceComma + vfValueComplete : ""); if (isNotBlank(vfValue)) { cmdList.add("-vf"); cmdList.add(vfValue); } } /* * The PS3 and possibly other renderers display videos incorrectly * if the dimensions aren't divisible by 4, so if that is the * case we scale it down to the nearest 4. * This fixes the long-time bug of videos displaying in black and * white with diagonal strips of colour, weird one. * * TODO: Integrate this with the other stuff so that "scale" only * ever appears once in the MEncoder CMD. */ if (media != null && (media.getWidth() % 4 != 0) || media.getHeight() % 4 != 0) { int newWidth; int newHeight; newWidth = (media.getWidth() / 4) * 4; newHeight = (media.getHeight() / 4) * 4; cmdList.add("-vf"); cmdList.add("softskip,expand=" + newWidth + ":" + newHeight); } if (configuration.getMencoderMT() && !avisynth && !dvd && !(startsWith(media.getCodecV(), "mpeg2"))) { cmdList.add("-lavdopts"); cmdList.add("fast"); } boolean disableMc0AndNoskip = false; // Process the options for this file in Transcoding Settings -> Mencoder -> Expert Settings: Codec-specific parameters // TODO this is better handled by a plugin with scripting support and will be removed if (media != null) { String expertOptions[] = getSpecificCodecOptions( configuration.getMencoderCodecSpecificConfig(), media, params, filename, externalSubtitlesFileName, configuration.isMencoderIntelligentSync(), false ); // the parameters (expertOptions) are processed in 3 passes // 1) process expertOptions // 2) process cmdList // 3) append expertOptions to cmdList if (expertOptions != null && expertOptions.length > 0) { // remove this option (key) from the cmdList in pass 2. // if the boolean value is true, also remove the option's corresponding value Map<String, Boolean> removeCmdListOption = new HashMap<String, Boolean>(); // if this option (key) is defined in cmdList, merge this string value into the // option's value in pass 2. the value is a string format template into which the // cmdList option value is injected Map<String, String> mergeCmdListOption = new HashMap<String, String>(); // merges that are performed in pass 2 are logged in this map; the key (string) is // the option name and the value is a boolean indicating whether the option was merged // or not. the map is populated after pass 1 with the options from mergeCmdListOption // and all values initialised to false. if an option was merged, it is not appended // to cmdList Map<String, Boolean> mergedCmdListOption = new HashMap<String, Boolean>(); // pass 1: process expertOptions for (int i = 0; i < expertOptions.length; ++i) { if (expertOptions[i].equals("-noass")) { // remove -ass from cmdList in pass 2. // -ass won't have been added in this method (getSpecificCodecOptions // has been called multiple times above to check for -noass and -nomux) // but it may have been added via the renderer or global MEncoder options. // XXX: there are currently 10 other -ass options (-ass-color, -ass-border-color &c.). // technically, they should all be removed... removeCmdListOption.put("-ass", false); // false: option does not have a corresponding value // remove -noass from expertOptions in pass 3 expertOptions[i] = REMOVE_OPTION; } else if (expertOptions[i].equals("-nomux")) { expertOptions[i] = REMOVE_OPTION; } else if (expertOptions[i].equals("-mt")) { // not an MEncoder option so remove it from exportOptions. // multi-threaded MEncoder is used by default, so this is obsolete (TODO: Remove it from the description) expertOptions[i] = REMOVE_OPTION; } else if (expertOptions[i].equals("-ofps")) { // replace the cmdList version with the expertOptions version i.e. remove the former removeCmdListOption.put("-ofps", true); // skip (i.e. leave unchanged) the exportOptions value ++i; } else if (expertOptions[i].equals("-fps")) { removeCmdListOption.put("-fps", true); ++i; } else if (expertOptions[i].equals("-ovc")) { removeCmdListOption.put("-ovc", true); ++i; } else if (expertOptions[i].equals("-channels")) { removeCmdListOption.put("-channels", true); ++i; } else if (expertOptions[i].equals("-oac")) { removeCmdListOption.put("-oac", true); ++i; } else if (expertOptions[i].equals("-quality")) { // XXX like the old (cmdArray) code, this clobbers the old -lavcopts value String lavcopts = String.format( "autoaspect=1:vcodec=%s:acodec=%s:abitrate=%s:threads=%d:%s", vcodec, (configuration.isMencoderAc3Fixed() ? "ac3_fixed" : "ac3"), CodecUtil.getAC3Bitrate(configuration, params.aid), configuration.getMencoderMaxThreads(), expertOptions[i + 1] ); // append bitrate-limiting options if configured lavcopts = addMaximumBitrateConstraints( lavcopts, media, lavcopts, params.mediaRenderer, "" ); // a string format with no placeholders, so the cmdList option value is ignored. // note: we protect "%" from being interpreted as a format by converting it to "%%", // which is then turned back into "%" when the format is processed mergeCmdListOption.put("-lavcopts", lavcopts.replace("%", "%%")); // remove -quality <value> expertOptions[i] = expertOptions[i + 1] = REMOVE_OPTION; ++i; } else if (expertOptions[i].equals("-mpegopts")) { mergeCmdListOption.put("-mpegopts", "%s:" + expertOptions[i + 1].replace("%", "%%")); // merge if cmdList already contains -mpegopts, but don't append if it doesn't (parity with the old (cmdArray) version) expertOptions[i] = expertOptions[i + 1] = REMOVE_OPTION; ++i; } else if (expertOptions[i].equals("-vf")) { mergeCmdListOption.put("-vf", "%s," + expertOptions[i + 1].replace("%", "%%")); ++i; } else if (expertOptions[i].equals("-af")) { mergeCmdListOption.put("-af", "%s," + expertOptions[i + 1].replace("%", "%%")); ++i; } else if (expertOptions[i].equals("-nosync")) { disableMc0AndNoskip = true; expertOptions[i] = REMOVE_OPTION; } else if (expertOptions[i].equals("-mc")) { disableMc0AndNoskip = true; } } for (String key : mergeCmdListOption.keySet()) { mergedCmdListOption.put(key, false); } // pass 2: process cmdList List<String> transformedCmdList = new ArrayList<String>(); for (int i = 0; i < cmdList.size(); ++i) { String option = cmdList.get(i); // we remove an option by *not* adding it to transformedCmdList if (removeCmdListOption.containsKey(option)) { if (isTrue(removeCmdListOption.get(option))) { // true: remove (i.e. don't add) the corresponding value ++i; } } else { transformedCmdList.add(option); if (mergeCmdListOption.containsKey(option)) { String format = mergeCmdListOption.get(option); String value = String.format(format, cmdList.get(i + 1)); // record the fact that an expertOption value has been merged into this cmdList value mergedCmdListOption.put(option, true); transformedCmdList.add(value); ++i; } } } cmdList = transformedCmdList; // pass 3: append expertOptions to cmdList for (int i = 0; i < expertOptions.length; ++i) { String option = expertOptions[i]; if (option != REMOVE_OPTION) { if (isTrue(mergedCmdListOption.get(option))) { // true: this option and its value have already been merged into existing cmdList options ++i; // skip the value } else { cmdList.add(option); } } } } } if ((pcm || dtsRemux || ac3Remux) || (configuration.isMencoderNoOutOfSync() && !disableMc0AndNoskip)) { if (configuration.isFix25FPSAvMismatch()) { cmdList.add("-mc"); cmdList.add("0.005"); } else { cmdList.add("-mc"); cmdList.add("0"); cmdList.add("-noskip"); } } if (params.timeend > 0) { cmdList.add("-endpos"); cmdList.add("" + params.timeend); } String rate = "48000"; if (params.mediaRenderer.isXBOX()) { rate = "44100"; } // force srate -> cause ac3's mencoder doesn't like anything other than 48khz if (media != null && !pcm && !dtsRemux && !ac3Remux) { cmdList.add("-af"); cmdList.add("lavcresample=" + rate); cmdList.add("-srate"); cmdList.add(rate); } // add a -cache option for piped media (e.g. rar/zip file entries): // https://code.google.com/p/ps3mediaserver/issues/detail?id=911 if (params.stdin != null) { cmdList.add("-cache"); cmdList.add("8192"); } PipeProcess pipe = null; ProcessWrapperImpl pw = null; if (pcm || dtsRemux) { // transcode video, demux audio, remux with tsmuxer boolean channels_filter_present = false; for (String s : cmdList) { if (isNotBlank(s) && s.startsWith("channels")) { channels_filter_present = true; break; } } if (params.avidemux) { pipe = new PipeProcess("mencoder" + System.currentTimeMillis(), (pcm || dtsRemux || ac3Remux) ? null : params); params.input_pipes[0] = pipe; cmdList.add("-o"); cmdList.add(pipe.getInputPipe()); if (pcm && !channels_filter_present && params.aid != null) { String mixer = getLPCMChannelMappingForMencoder(params.aid); if (isNotBlank(mixer)) { cmdList.add("-af"); cmdList.add(mixer); } } String[] cmdArray = new String[cmdList.size()]; cmdList.toArray(cmdArray); pw = new ProcessWrapperImpl(cmdArray, params); PipeProcess videoPipe = new PipeProcess("videoPipe" + System.currentTimeMillis(), "out", "reconnect"); PipeProcess audioPipe = new PipeProcess("audioPipe" + System.currentTimeMillis(), "out", "reconnect"); ProcessWrapper videoPipeProcess = videoPipe.getPipeProcess(); ProcessWrapper audioPipeProcess = audioPipe.getPipeProcess(); params.output_pipes[0] = videoPipe; params.output_pipes[1] = audioPipe; pw.attachProcess(videoPipeProcess); pw.attachProcess(audioPipeProcess); videoPipeProcess.runInNewThread(); audioPipeProcess.runInNewThread(); try { Thread.sleep(50); } catch (InterruptedException e) { } videoPipe.deleteLater(); audioPipe.deleteLater(); } else { // remove the -oac switch, otherwise the "too many video packets" errors appear again for (ListIterator<String> it = cmdList.listIterator(); it.hasNext();) { String option = it.next(); if (option.equals("-oac")) { it.set("-nosound"); if (it.hasNext()) { it.next(); it.remove(); } break; } } pipe = new PipeProcess(System.currentTimeMillis() + "tsmuxerout.ts"); TsMuxeRVideo ts = new TsMuxeRVideo(configuration); File f = new File(configuration.getTempFolder(), "pms-tsmuxer.meta"); String cmd[] = new String[]{ ts.executable(), f.getAbsolutePath(), pipe.getInputPipe() }; pw = new ProcessWrapperImpl(cmd, params); PipeIPCProcess ffVideoPipe = new PipeIPCProcess(System.currentTimeMillis() + "ffmpegvideo", System.currentTimeMillis() + "videoout", false, true); cmdList.add("-o"); cmdList.add(ffVideoPipe.getInputPipe()); OutputParams ffparams = new OutputParams(configuration); ffparams.maxBufferSize = 1; ffparams.stdin = params.stdin; String[] cmdArray = new String[cmdList.size()]; cmdList.toArray(cmdArray); ProcessWrapperImpl ffVideo = new ProcessWrapperImpl(cmdArray, ffparams); ProcessWrapper ff_video_pipe_process = ffVideoPipe.getPipeProcess(); pw.attachProcess(ff_video_pipe_process); ff_video_pipe_process.runInNewThread(); ffVideoPipe.deleteLater(); pw.attachProcess(ffVideo); ffVideo.runInNewThread(); String aid = null; if (media != null && media.getAudioTracksList().size() > 1 && params.aid != null) { if (media.getContainer() != null && (media.getContainer().equals(FormatConfiguration.AVI) || media.getContainer().equals(FormatConfiguration.FLV))) { // TODO confirm (MP4s, OGMs and MOVs already tested: first aid is 0; AVIs: first aid is 1) // for AVIs, FLVs and MOVs mencoder starts audio tracks numbering from 1 aid = "" + (params.aid.getId() + 1); } else { // everything else from 0 aid = "" + params.aid.getId(); } } PipeIPCProcess ffAudioPipe = new PipeIPCProcess(System.currentTimeMillis() + "ffmpegaudio01", System.currentTimeMillis() + "audioout", false, true); StreamModifier sm = new StreamModifier(); sm.setPcm(pcm); sm.setDtsEmbed(dtsRemux); sm.setSampleFrequency(48000); sm.setBitsPerSample(16); String mixer = null; if (pcm && !dtsRemux) { mixer = getLPCMChannelMappingForMencoder(params.aid); // LPCM always outputs 5.1/7.1 for multichannel tracks. Downmix with player if needed! } sm.setNbChannels(channels); // it seems the -really-quiet prevents mencoder to stop the pipe output after some time... // -mc 0.1 make the DTS-HD extraction works better with latest mencoder builds, and makes no impact on the regular DTS one String ffmpegLPCMextract[] = new String[]{ executable(), "-ss", "0", filename, "-really-quiet", "-msglevel", "statusline=2", "-channels", "" + channels, "-ovc", "copy", "-of", "rawaudio", "-mc", dtsRemux ? "0.1" : "0", "-noskip", (aid == null) ? "-quiet" : "-aid", (aid == null) ? "-quiet" : aid, "-oac", (ac3Remux || dtsRemux) ? "copy" : "pcm", (isNotBlank(mixer) && !channels_filter_present) ? "-af" : "-quiet", (isNotBlank(mixer) && !channels_filter_present) ? mixer : "-quiet", "-srate", "48000", "-o", ffAudioPipe.getInputPipe() }; if (!params.mediaRenderer.isMuxDTSToMpeg()) { // no need to use the PCM trick when media renderer supports DTS ffAudioPipe.setModifier(sm); } if (media != null && media.getDvdtrack() > 0) { ffmpegLPCMextract[3] = "-dvd-device"; ffmpegLPCMextract[4] = filename; ffmpegLPCMextract[5] = "dvd://" + media.getDvdtrack(); } else if (params.stdin != null) { ffmpegLPCMextract[3] = "-"; } if (filename.toLowerCase().endsWith(".evo")) { ffmpegLPCMextract[4] = "-psprobe"; ffmpegLPCMextract[5] = "1000000"; } if (params.timeseek > 0) { ffmpegLPCMextract[2] = "" + params.timeseek; } OutputParams ffaudioparams = new OutputParams(configuration); ffaudioparams.maxBufferSize = 1; ffaudioparams.stdin = params.stdin; ProcessWrapperImpl ffAudio = new ProcessWrapperImpl(ffmpegLPCMextract, ffaudioparams); params.stdin = null; PrintWriter pwMux = new PrintWriter(f); pwMux.println("MUXOPT --no-pcr-on-video-pid --no-asyncio --new-audio-pes --vbr --vbv-len=500"); String videoType = "V_MPEG-2"; if (params.no_videoencode && params.forceType != null) { videoType = params.forceType; } String fps = ""; if (params.forceFps != null) { fps = "fps=" + params.forceFps + ", "; } String audioType; if (ac3Remux) { audioType = "A_AC3"; } else if (dtsRemux) { if (params.mediaRenderer.isMuxDTSToMpeg()) { //renderer can play proper DTS track audioType = "A_DTS"; } else { // DTS padded in LPCM trick audioType = "A_LPCM"; } } else { // PCM audioType = "A_LPCM"; } // mencoder bug (confirmed with mencoder r35003 + ffmpeg 0.11.1): // audio delay is ignored when playing from file start (-ss 0) // override with tsmuxer.meta setting String timeshift = ""; if (mencoderAC3RemuxAudioDelayBug) { timeshift = "timeshift=" + params.aid.getAudioProperties().getAudioDelay() + "ms, "; } pwMux.println(videoType + ", \"" + ffVideoPipe.getOutputPipe() + "\", " + fps + "level=4.1, insertSEI, contSPS, track=1"); pwMux.println(audioType + ", \"" + ffAudioPipe.getOutputPipe() + "\", " + timeshift + "track=2"); pwMux.close(); ProcessWrapper pipe_process = pipe.getPipeProcess(); pw.attachProcess(pipe_process); pipe_process.runInNewThread(); try { Thread.sleep(50); } catch (InterruptedException e) { } pipe.deleteLater(); params.input_pipes[0] = pipe; ProcessWrapper ff_pipe_process = ffAudioPipe.getPipeProcess(); pw.attachProcess(ff_pipe_process); ff_pipe_process.runInNewThread(); try { Thread.sleep(50); } catch (InterruptedException e) { } ffAudioPipe.deleteLater(); pw.attachProcess(ffAudio); ffAudio.runInNewThread(); } } else { boolean directpipe = Platform.isMac() || Platform.isFreeBSD(); if (directpipe) { cmdList.add("-o"); cmdList.add("-"); cmdList.add("-really-quiet"); cmdList.add("-msglevel"); cmdList.add("statusline=2"); params.input_pipes = new PipeProcess[2]; } else { pipe = new PipeProcess("mencoder" + System.currentTimeMillis(), (pcm || dtsRemux) ? null : params); params.input_pipes[0] = pipe; cmdList.add("-o"); cmdList.add(pipe.getInputPipe()); } String[] cmdArray = new String[ cmdList.size() ]; cmdList.toArray(cmdArray); cmdArray = finalizeTranscoderArgs( filename, dlna, media, params, cmdArray ); pw = new ProcessWrapperImpl(cmdArray, params); if (!directpipe) { ProcessWrapper mkfifo_process = pipe.getPipeProcess(); pw.attachProcess(mkfifo_process); // 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) { } return pw; } @Override public String mimeType() { return HTTPResource.VIDEO_TRANSCODE; } @Override public String name() { return "MEncoder Video"; } @Override public int type() { return Format.VIDEO; } private String[] getSpecificCodecOptions( String codecParam, DLNAMediaInfo media, OutputParams params, String filename, String externalSubtitlesFileName, boolean enable, boolean verifyOnly ) { StringBuilder sb = new StringBuilder(); String codecs = enable ? DEFAULT_CODEC_CONF_SCRIPT : ""; codecs += "\n" + codecParam; StringTokenizer stLines = new StringTokenizer(codecs, "\n"); try { Interpreter interpreter = new Interpreter(); interpreter.setStrictJava(true); ArrayList<String> types = CodecUtil.getPossibleCodecs(); int rank = 1; if (types != null) { for (String type : types) { int r = rank++; interpreter.set("" + type, r); String secondaryType = "dummy"; if ("matroska".equals(type)) { secondaryType = "mkv"; interpreter.set(secondaryType, r); } else if ("rm".equals(type)) { secondaryType = "rmvb"; interpreter.set(secondaryType, r); } else if ("mpeg2video".equals(type)) { secondaryType = "mpeg2"; interpreter.set(secondaryType, r); } else if ("mpeg1video".equals(type)) { secondaryType = "mpeg1"; interpreter.set(secondaryType, r); } if (media.getContainer() != null && (media.getContainer().equals(type) || media.getContainer().equals(secondaryType))) { interpreter.set("container", r); } else if (media.getCodecV() != null && (media.getCodecV().equals(type) || media.getCodecV().equals(secondaryType))) { interpreter.set("vcodec", r); } else if (params.aid != null && params.aid.getCodecA() != null && params.aid.getCodecA().equals(type)) { interpreter.set("acodec", r); } } } else { return null; } interpreter.set("filename", filename); interpreter.set("audio", params.aid != null); interpreter.set("subtitles", params.sid != null); interpreter.set("srtfile", externalSubtitlesFileName); if (params.aid != null) { interpreter.set("samplerate", params.aid.getSampleRate()); } String framerate = media.getValidFps(false); try { if (framerate != null) { interpreter.set("framerate", Double.parseDouble(framerate)); } } catch (NumberFormatException e) { logger.debug("Could not parse framerate from \"" + framerate + "\""); } interpreter.set("duration", media.getDurationInSeconds()); if (params.aid != null) { interpreter.set("channels", params.aid.getAudioProperties().getNumberOfChannels()); } interpreter.set("height", media.getHeight()); interpreter.set("width", media.getWidth()); while (stLines.hasMoreTokens()) { String line = stLines.nextToken(); if (!line.startsWith("#") && line.trim().length() > 0) { int separator = line.indexOf("::"); if (separator > -1) { String key = null; try { key = line.substring(0, separator).trim(); String value = line.substring(separator + 2).trim(); if (value.length() > 0) { if (key.length() == 0) { key = "1 == 1"; } Object result = interpreter.eval(key); if (result != null && result instanceof Boolean && (Boolean) result) { sb.append(" "); sb.append(value); } } } catch (Throwable e) { logger.debug("Error while executing: " + key + " : " + e.getMessage()); if (verifyOnly) { return new String[]{"@@Error while parsing: " + e.getMessage()}; } } } else if (verifyOnly) { return new String[]{"@@Malformatted line: " + line}; } } } } catch (EvalError e) { logger.debug("BeanShell error: " + e.getMessage()); } String completeLine = sb.toString(); ArrayList<String> args = new ArrayList<String>(); StringTokenizer st = new StringTokenizer(completeLine, " "); while (st.hasMoreTokens()) { String arg = st.nextToken().trim(); if (arg.length() > 0) { args.add(arg); } } String definitiveArgs[] = new String[args.size()]; args.toArray(definitiveArgs); return definitiveArgs; } /** * {@inheritDoc} */ @Override public boolean isCompatible(DLNAResource resource) { return PlayerUtil.isVideo(resource, Format.Identifier.ISO) || PlayerUtil.isVideo(resource, Format.Identifier.MKV) || PlayerUtil.isVideo(resource, Format.Identifier.MPG); } }