/* * 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 java.awt.ComponentOrientation; 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.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JSlider; import javax.swing.JTextField; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import net.pms.Messages; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.DLNAResource; import net.pms.formats.Format; import net.pms.io.OutputParams; import net.pms.io.PipeProcess; import net.pms.io.ProcessWrapper; import net.pms.io.ProcessWrapperImpl; import net.pms.network.HTTPResource; import net.pms.util.FileUtil; import net.pms.util.FormLayoutUtil; import net.pms.util.PlayerUtil; import net.pms.util.ProcessUtil; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.jgoodies.forms.builder.DefaultFormBuilder; import com.jgoodies.forms.layout.FormLayout; import com.sun.jna.Platform; // FIXME (breaking change): VLCWebVideo doesn't customize any of this, so everything should be *private* // TODO (when transcoding to MPEG-2): handle non-MPEG-2 compatible input framerates /** * Use VLC as a backend transcoder. Note that 0.x and 1.x versions are * unsupported (and probably will crash). Only the latest version will be * supported * * @author Leon Blakey <lord.quackstar@gmail.com> */ public class VLCVideo extends Player { private static final Logger logger = LoggerFactory.getLogger(VLCVideo.class); protected final PmsConfiguration configuration; public static final String ID = "vlctranscoder"; protected JTextField scale; protected JCheckBox experimentalCodecs; protected JCheckBox audioSyncEnabled; protected JTextField sampleRate; protected JCheckBox sampleRateOverride; protected JTextField extraParams; public VLCVideo(PmsConfiguration configuration) { this.configuration = configuration; } @Override public PlayerPurpose getPurpose() { return PlayerPurpose.VIDEO_FILE_PLAYER; } @Override public String id() { return ID; } @Override public boolean isTimeSeekable() { return true; } @Override public String[] args() { return new String[]{}; } @Override public String name() { return "VLC Video"; } @Override public int type() { return Format.VIDEO; } @Override public String mimeType() { // I think? return HTTPResource.VIDEO_TRANSCODE; } @Override public String executable() { return configuration.getVlcPath(); } @Override public boolean isCompatible(DLNAResource resource) { // only handle local video - web video is handled by VLCWebVideo return PlayerUtil.isVideo(resource) && !PlayerUtil.isWebVideo(resource); } /** * Pick codecs for VLC based on formats the renderer supports. * * @param renderer The {@link RendererConfiguration}. * @return The codec configuration */ protected CodecConfig genConfig(RendererConfiguration renderer) { CodecConfig codecConfig = new CodecConfig(); if (renderer.isTranscodeToWMV()) { // Assume WMV = XBox = all media renderers with this flag logger.debug("Using XBox WMV codecs"); codecConfig.videoCodec = "wmv2"; codecConfig.audioCodec = "wma"; codecConfig.container = "asf"; } else { // Default codecs for DLNA standard codecConfig.videoCodec = "mp2v"; // XXX a52 (AC-3) causes the audio to cut out after // a while (5, 10, and 45 minutes have been spotted) // with versions as recent as 2.0.5. MP2 works without // issue, so we use that as a workaround for now. // codecConfig.audioCodec = "a52"; codecConfig.audioCodec = "mp2a"; if (renderer.isTranscodeToMPEGTSAC3()) { logger.debug("Using standard DLNA codecs with an MPEG-PS container"); codecConfig.container = "ts"; } else { logger.debug("Using standard DLNA codecs with an MPEG-TS (default) container"); codecConfig.container = "ps"; } } logger.trace("Using " + codecConfig.videoCodec + ", " + codecConfig.audioCodec + ", " + codecConfig.container); // Audio sample rate handling if (sampleRateOverride.isSelected()) { codecConfig.sampleRate = Integer.valueOf(sampleRate.getText()); } // This has caused garbled audio, so only enable when told to if (audioSyncEnabled.isSelected()) { codecConfig.extraTrans.put("audio-sync", ""); } return codecConfig; } protected static class CodecConfig { String videoCodec; String audioCodec; String container; String extraParams; HashMap<String, Object> extraTrans = new HashMap<String, Object>(); int sampleRate; } protected Map<String, Object> getEncodingArgs(CodecConfig codecConfig) { // See: http://www.videolan.org/doc/streaming-howto/en/ch03.html // See: http://wiki.videolan.org/Codec Map<String, Object> args = new HashMap<String, Object>(); // Codecs to use args.put("vcodec", codecConfig.videoCodec); args.put("acodec", codecConfig.audioCodec); // Bitrate in kbit/s (TODO: Use global option?) args.put("vb", "4096"); args.put("ab", "128"); // Video scaling args.put("scale", scale.getText()); // Audio Channels args.put("channels", 2); // Static sample rate args.put("samplerate", codecConfig.sampleRate); // Recommended on VLC DVD encoding page args.put("keyint", 16); // Recommended on VLC DVD encoding page args.put("strict-rc", null); // Stream subtitles to client // args.add("scodec=dvbs"); // args.add("senc=dvbsub"); // Hardcode subtitles into video args.put("soverlay", null); // enable multi-threading args.put("threads", "" + configuration.getNumberOfCpuCores()); // Add extra args args.putAll(codecConfig.extraTrans); return args; } @Override public ProcessWrapper launchTranscode( DLNAResource dlna, DLNAMediaInfo media, OutputParams params ) throws IOException { final String filename = dlna.getSystemName(); boolean isWindows = Platform.isWindows(); setAudioAndSubs(filename, media, params, configuration); // Make sure we can play this CodecConfig codecConfig = genConfig(params.mediaRenderer); PipeProcess tsPipe = new PipeProcess("VLC" + System.currentTimeMillis() + "." + codecConfig.container); ProcessWrapper pipe_process = tsPipe.getPipeProcess(); // XXX 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 start this as early as possible pipe_process.runInNewThread(); tsPipe.deleteLater(); params.input_pipes[0] = tsPipe; params.minBufferSize = params.minFileSize; params.secondread_minsize = 100000; List<String> cmdList = new ArrayList<String>(); cmdList.add(executable()); cmdList.add("-I"); cmdList.add("dummy"); // XXX hardware acceleration causes issues with some videos // on VLC 2.0.5, so disable it by default. // Note: it's enabled by default in 2.0.5 (and possibly // earlier), so, if not enabled, it needs to be explicitly // disabled // These options do not exist in VLC 2.0.7 on Mac OS X if (!Platform.isMac()) { if (configuration.isVideoHardwareAcceleration()) { logger.warn("VLC hardware acceleration support is an experimental feature. Please disable it before reporting issues."); cmdList.add("--ffmpeg-hw"); } else { cmdList.add("--no-ffmpeg-hw"); } } // Useful for the more esoteric codecs people use if (experimentalCodecs.isSelected()) { cmdList.add("--sout-ffmpeg-strict=-2"); } // Stop the DOS box from appearing on windows if (isWindows) { cmdList.add("--dummy-quiet"); } // File needs to be given before sout, otherwise vlc complains cmdList.add(filename); // FIXME not sure what this hack is trying to do, but it results in no audio and no subtitles // Huge fake track id that shouldn't conflict with any real subtitle or audio id. Hopefully. String disableSuffix = "track=214748361"; // Handle audio language if (params.aid != null) { // User specified language at the client, acknowledge it if (params.aid.getLang() == null || params.aid.getLang().equals("und")) { // VLC doesn't understand "und", so try to get audio track by ID cmdList.add("--audio-track=" + params.aid.getId()); } else { cmdList.add("--audio-language=" + params.aid.getLang()); } } else { // Not specified, use language from GUI // FIXME: VLC does not understand "loc" or "und". cmdList.add("--audio-language=" + configuration.getAudioLanguages()); } // Handle subtitle language if (params.sid != null) { // User specified language at the client, acknowledge it if (params.sid.isExternal()) { String externalSubtitlesFileName = null; // External subtitle file if (params.sid.isExternalFileUtf16()) { try { // 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()); } catch (IOException e) { logger.debug("Error converting file from UTF-16 to UTF-8", e); externalSubtitlesFileName = ProcessUtil.getShortFileNameIfWideChars(params.sid.getExternalFile().getAbsolutePath()); } } else { externalSubtitlesFileName = ProcessUtil.getShortFileNameIfWideChars(params.sid.getExternalFile().getAbsolutePath()); } if (externalSubtitlesFileName != null) { cmdList.add("--sub-file"); cmdList.add(externalSubtitlesFileName); } } else if (params.sid.getLang() != null && !params.sid.getLang().equals("und")) { // Load by ID (better) cmdList.add("--sub-track=" + params.sid.getId()); } else { // VLC doesn't understand "und", but does understand a nonexistent track cmdList.add("--sub-" + disableSuffix); } } else if (!configuration.isDisableSubtitles()) { // Not specified, use language from GUI if enabled // FIXME: VLC does not understand "loc" or "und". cmdList.add("--sub-language=" + configuration.getSubtitlesLanguages()); } else { cmdList.add("--sub-" + disableSuffix); } // Skip forward if necessary if (params.timeseek != 0) { cmdList.add("--start-time"); cmdList.add(String.valueOf(params.timeseek)); } // Generate encoding args String separator = ""; StringBuilder encodingArgsBuilder = new StringBuilder(); for (Map.Entry<String, Object> curEntry : getEncodingArgs(codecConfig).entrySet()) { encodingArgsBuilder.append(separator); encodingArgsBuilder.append(curEntry.getKey()); if (curEntry.getValue() != null) { encodingArgsBuilder.append("="); encodingArgsBuilder.append(curEntry.getValue()); } separator = ","; } // Add our transcode options String transcodeSpec = String.format( "#transcode{%s}:standard{access=file,mux=%s,dst='%s%s'}", encodingArgsBuilder.toString(), codecConfig.container, (isWindows ? "\\\\" : ""), tsPipe.getInputPipe()); cmdList.add("--sout"); cmdList.add(transcodeSpec); // Force VLC to exit when finished cmdList.add("vlc://quit"); // Add any extra parameters if (!extraParams.getText().trim().isEmpty()) { // Add each part as a new item cmdList.addAll(Arrays.asList(StringUtils.split(extraParams.getText().trim(), " "))); } // Pass to process wrapper String[] cmdArray = new String[cmdList.size()]; cmdList.toArray(cmdArray); cmdArray = finalizeTranscoderArgs(filename, dlna, media, params, cmdArray); logger.trace("Finalized args: " + StringUtils.join(cmdArray, " ")); ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params); pw.attachProcess(pipe_process); // TODO: Why is this here? try { Thread.sleep(150); } catch (InterruptedException e) { } pw.runInNewThread(); return pw; } @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("right:pref, 3dlu, pref:grow, 7dlu, right:pref, 3dlu, pref:grow", orientation); FormLayout layout = new FormLayout(colSpec, ""); // Here goes my 3rd try to learn JGoodies Form layout.setColumnGroups(new int[][]{{1, 5}, {3, 7}}); DefaultFormBuilder mainPanel = new DefaultFormBuilder(layout); mainPanel.appendSeparator(Messages.getString("VlcTrans.1")); mainPanel.append(experimentalCodecs = new JCheckBox(Messages.getString("VlcTrans.3"), configuration.isVlcExperimentalCodecs()), 3); experimentalCodecs.setContentAreaFilled(false); experimentalCodecs.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setVlcExperimentalCodecs(e.getStateChange() == ItemEvent.SELECTED); } }); mainPanel.append(audioSyncEnabled = new JCheckBox(Messages.getString("VlcTrans.4"), configuration.isVlcAudioSyncEnabled()), 3); audioSyncEnabled.setContentAreaFilled(false); audioSyncEnabled.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setVlcAudioSyncEnabled(e.getStateChange() == ItemEvent.SELECTED); } }); mainPanel.nextLine(); // Developer stuff. Theoretically is temporary mainPanel.appendSeparator(Messages.getString("VlcTrans.10")); // Add scale as a subpanel because it has an awkward layout mainPanel.append(Messages.getString("VlcTrans.11")); FormLayout scaleLayout = new FormLayout("pref,3dlu,pref", ""); DefaultFormBuilder scalePanel = new DefaultFormBuilder(scaleLayout); double startingScale = Double.valueOf(configuration.getVlcScale()); scalePanel.append(scale = new JTextField(String.valueOf(startingScale))); final JSlider scaleSlider = new JSlider(JSlider.HORIZONTAL, 0, 10, (int) (startingScale * 10)); scalePanel.append(scaleSlider); scaleSlider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent ce) { String value = String.valueOf((double) scaleSlider.getValue() / 10); scale.setText(value); configuration.setVlcScale(value); } }); scale.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { String typed = scale.getText(); if (!typed.matches("\\d\\.\\d")) { return; } double value = Double.parseDouble(typed); scaleSlider.setValue((int) (value * 10)); configuration.setVlcScale(String.valueOf(value)); } }); mainPanel.append(scalePanel.getPanel(), 3); // Audio sample rate FormLayout sampleRateLayout = new FormLayout("right:pref, 3dlu, right:pref, 3dlu, right:pref, 3dlu, left:pref", ""); DefaultFormBuilder sampleRatePanel = new DefaultFormBuilder(sampleRateLayout); sampleRateOverride = new JCheckBox(Messages.getString("VlcTrans.17"), configuration.getVlcSampleRateOverride()); sampleRatePanel.append(Messages.getString("VlcTrans.18"), sampleRateOverride); sampleRate = new JTextField(configuration.getVlcSampleRate(), 8); sampleRate.setEnabled(configuration.getVlcSampleRateOverride()); sampleRate.addKeyListener(new KeyAdapter() { @Override public void keyReleased(KeyEvent e) { configuration.setVlcSampleRate(sampleRate.getText()); } }); sampleRatePanel.append(Messages.getString("VlcTrans.19"), sampleRate); sampleRateOverride.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { boolean checked = e.getStateChange() == ItemEvent.SELECTED; configuration.setVlcSampleRateOverride(checked); sampleRate.setEnabled(checked); } }); mainPanel.nextLine(); mainPanel.append(sampleRatePanel.getPanel(), 7); // Extra options mainPanel.nextLine(); mainPanel.append(Messages.getString("VlcTrans.20"), extraParams = new JTextField(), 5); return mainPanel.getPanel(); } }