/* * PS3 Media Server, for streaming any medias to your PS3. * Copyright (C) 2008 A.Brochard * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; version 2 * of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms.encoders; import com.jgoodies.forms.builder.PanelBuilder; import com.jgoodies.forms.layout.CellConstraints; import com.jgoodies.forms.layout.FormLayout; import net.pms.Messages; import net.pms.PMS; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.DLNAMediaSubtitle; import net.pms.dlna.DLNAResource; import net.pms.dlna.InputFile; import net.pms.formats.Format; import net.pms.formats.v2.SubtitleUtils; import net.pms.io.*; import net.pms.network.HTTPResource; 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 java.awt.*; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.swing.*; import static org.apache.commons.io.FilenameUtils.getBaseName; import static org.apache.commons.io.FilenameUtils.getExtension; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; /* * Pure FFmpeg video player. * * Design note: * * Helper methods that return lists of <code>String</code>s representing * options are public to facilitate composition e.g. a custom engine (plugin) * that uses tsMuxeR for videos without subtitles and FFmpeg otherwise needs to * compose and call methods on both players. * * To avoid API churn, and to provide wiggle room for future functionality, all * of these methods take the same arguments as launchTranscode (and the same * first four arguments as finalizeTranscoderArgs) even if one or more of the * parameters are unused e.g.: * * public List<String> getAudioBitrateOptions( * DLNAResource dlna, * DLNAMediaInfo media, * OutputParams params * ) */ public class FFmpegVideo extends FFmpegBase { private static final Logger logger = LoggerFactory.getLogger(FFmpegVideo.class); private static final String DEFAULT_QSCALE = "3"; private static final String SUB_DIR = "subs"; private static PmsConfiguration configuration; private boolean dtsRemux; private boolean ac3Remux; private boolean videoRemux; private JCheckBox multiThreadingCheckBox; private JCheckBox videoRemuxCheckBox; @Deprecated public FFmpegVideo() { this(PMS.getConfiguration()); } public FFmpegVideo(PmsConfiguration configuration) { super(configuration); this.configuration = configuration; } // FIXME we have an id() accessor for this; no need for the field to be public @Deprecated public static final String ID = "ffmpegvideo"; /** * Returns a list of strings representing the rescale options for this transcode i.e. the ffmpeg -vf * options used to show subtitles in SSA/ASS format and resize a video that's too wide and/or high for the specified renderer. * If the renderer has no size limits, or there's no media metadata, or the video is within the renderer's * size limits, an empty list is returned. * * @param dlna The DLNA resource representing the file being transcoded. * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos). * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request. * @return a {@link List} of <code>String</code>s representing the rescale options for this video, * or an empty list if the video doesn't need to be resized. */ public List<String> getVideoFilterOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException { List<String> options = new ArrayList<String>(); String subsOption = null; String padding = null; final RendererConfiguration renderer = params.mediaRenderer; DLNAMediaSubtitle tempSubs = null; if (!isDisableSubtitles(params)) { tempSubs = getSubtitles(params); } final boolean isResolutionTooHighForRenderer = renderer.isVideoRescale() // renderer defines a max width/height && (media != null && media.isMediaparsed()) && ((media.getWidth() > renderer.getMaxVideoWidth()) || (media.getHeight() > renderer.getMaxVideoHeight())); if (tempSubs != null) { StringBuilder s = new StringBuilder(); CharacterIterator it = new StringCharacterIterator(ProcessUtil.getShortFileNameIfWideChars(tempSubs.getExternalFile().getAbsolutePath())); for (char ch = it.first(); ch != CharacterIterator.DONE; ch = it.next()) { switch (ch) { case ':': s.append("\\\\:"); break; case '\\': s.append("/"); break; case ']': s.append("\\]"); break; case '[': s.append("\\["); break; default: s.append(ch); } } String subsFile = s.toString(); subsFile = subsFile.replace(",", "\\,"); subsOption = "subtitles=" + subsFile; } if (renderer.isPadVideoWithBlackBordersTo169AR() && renderer.isRescaleByRenderer()) { if (media != null && media.isMediaparsed() && media.getHeight() != 0 && (media.getWidth() / (double) media.getHeight()) >= (16 / (double) 9)) { padding = "pad=iw:iw/(16/9):0:(oh-ih)/2"; } else { padding = "pad=ih*(16/9):ih:(ow-iw)/2:0"; } } String rescaleSpec = null; if (isResolutionTooHighForRenderer || (renderer.isPadVideoWithBlackBordersTo169AR() && !renderer.isRescaleByRenderer())) { rescaleSpec = String.format( // http://stackoverflow.com/a/8351875 "scale=iw*min(%1$d/iw\\,%2$d/ih):ih*min(%1$d/iw\\,%2$d/ih),pad=%1$d:%2$d:(%1$d-iw)/2:(%2$d-ih)/2", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight() ); } String overrideVF = renderer.getFFmpegVideoFilterOverride(); if (rescaleSpec != null || padding != null || overrideVF != null || subsOption != null) { options.add("-vf"); StringBuilder filterParams = new StringBuilder(); if (overrideVF != null) { filterParams.append(overrideVF); if (subsOption != null) { filterParams.append(", "); } } else { if (rescaleSpec != null) { filterParams.append(rescaleSpec); if (subsOption != null || padding != null) { filterParams.append(", "); } } if (padding != null && rescaleSpec == null) { filterParams.append(padding); if (subsOption != null) { filterParams.append(", "); } } } if (subsOption != null) { filterParams.append(subsOption); } options.add(filterParams.toString()); } return options; } /** * Returns a list of <code>String</code>s representing ffmpeg output * options (i.e. options that define the output file's video codec, * audio codec and container) compatible with the renderer's * <code>TranscodeVideo</code> profile. * * @param dlna The DLNA resource representing the file being transcoded. * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos). * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request. * @return a {@link List} of <code>String</code>s representing the FFmpeg output parameters for the renderer according * to its <code>TranscodeVideo</code> profile. */ public synchronized List<String> getVideoTranscodeOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) { List<String> options = new ArrayList<String>(); final String filename = dlna.getSystemName(); final RendererConfiguration renderer = params.mediaRenderer; if (renderer.isTranscodeToWMV() && !renderer.isXBOX()) { // WMV options.add("-c:v"); options.add("wmv2"); options.add("-c:a"); options.add("wmav2"); options.add("-f"); options.add("asf"); } else { // MPEGPSAC3, MPEGTSAC3 or H264TSAC3 if (isAc3Remux()) { // AC-3 remux options.add("-c:a"); options.add("copy"); } else if (isDtsRemux()) { // Audio is added in a separate process later options.add("-an"); } else if (type() == Format.AUDIO) { // Skip } else { options.add("-c:a"); options.add("ac3"); } InputFile newInput = null; if (filename != null) { newInput = new InputFile(); newInput.setFilename(filename); newInput.setPush(params.stdin); } // Output video codec if (media.isMediaparsed() && params.sid == null && ((newInput != null && media.isVideoWithinH264LevelLimits(newInput, params.mediaRenderer)) || !params.mediaRenderer.isH264Level41Limited()) && media.isMuxable(params.mediaRenderer) && configuration.isFFmpegMuxWhenCompatible() && params.mediaRenderer.isMuxH264MpegTS()) { options.add("-c:v"); options.add("copy"); options.add("-bsf"); options.add("h264_mp4toannexb"); options.add("-fflags"); options.add("+genpts"); // Set correct container aspect ratio if remuxed video track has different AR // TODO does not work with ffmpeg 1.2 // https://ffmpeg.org/trac/ffmpeg/ticket/2046 // possible solution http://forum.doom9.org/showthread.php?t=152419 // // if (media.isAspectRatioMismatch()) { // options.add("-aspect"); // options.add(media.getAspectRatioContainer()); // } setVideoRemux(true); } else if (renderer.isTranscodeToH264TSAC3()) { options.add("-c:v"); options.add("libx264"); options.add("-crf"); options.add("20"); options.add("-preset"); options.add("superfast"); } else if (!isDtsRemux()) { options.add("-c:v"); options.add("mpeg2video"); } // Output file format options.add("-f"); if (isDtsRemux()) { if (isVideoRemux()) { options.add("rawvideo"); } else { options.add("mpeg2video"); } } else if (renderer.isTranscodeToMPEGTSAC3() || renderer.isTranscodeToH264TSAC3() || isVideoRemux()) { // MPEGTSAC3 options.add("mpegts"); } else { // default: MPEGPSAC3 options.add("vob"); } } return options; } /** * Returns the video bitrate spec for the current transcode according * to the limits/requirements of the renderer. * * @param dlna The DLNA resource representing the file being transcoded. * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos). * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request. * @return a {@link List} of <code>String</code>s representing the video bitrate options for this transcode */ public List<String> getVideoBitrateOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) { // media is currently unused List<String> options = new ArrayList<String>(); String sMaxVideoBitrate = params.mediaRenderer.getMaxVideoBitrate(); // currently Mbit/s int iMaxVideoBitrate = 0; if (sMaxVideoBitrate != null) { try { iMaxVideoBitrate = Integer.parseInt(sMaxVideoBitrate); } catch (NumberFormatException nfe) { logger.error("Can't parse max video bitrate", nfe); // XXX this should be handled in RendererConfiguration } } if (iMaxVideoBitrate == 0) { // unlimited: try to preserve the bitrate options.add("-q:v"); // video qscale options.add(DEFAULT_QSCALE); } else { // limit the bitrate FIXME untested // convert megabits-per-second (as per the current option name: MaxVideoBitrateMbps) to bps // FIXME rather than dealing with megabit vs mebibit issues here, this should be left up to the client i.e. // the renderer.conf unit should be bits-per-second (and the option should be renamed: MaxVideoBitrateMbps -> MaxVideoBitrate) options.add("-maxrate"); options.add("" + iMaxVideoBitrate * 1000 * 1000); } return options; } /** * Returns the audio bitrate spec for the current transcode according * to the limits/requirements of the renderer. * * @param dlna The DLNA resource representing the file being transcoded. * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos). * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request. * @return a {@link List} of <code>String</code>s representing the audio bitrate options for this transcode */ public List<String> getAudioBitrateOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) { List<String> options = new ArrayList<String>(); options.add("-q:a"); options.add(DEFAULT_QSCALE); return options; } /** * Returns the audio channel (-ac) options. * * @param dlna The DLNA resource representing the file being transcoded. * @param media the media metadata for the file being transcoded. May contain null fields (e.g. for web videos). * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request. * @return The list of audio channel options. * @since 1.81.0 */ public List<String> getAudioChannelOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) { List<String> options = new ArrayList<String>(); int ac = -1; // -1: don't change the number of audio channels int nChannels = params.aid == null ? -1 : params.aid.getAudioProperties().getNumberOfChannels(); if (nChannels == -1) { // unknown (e.g. web video) ac = 2; // works fine if the video has < 2 channels } else if (nChannels > 2) { int maxOutputChannels = configuration.getAudioChannelCount(); if (maxOutputChannels <= 2) { ac = maxOutputChannels; } else if (params.mediaRenderer.isTranscodeToWMV()) { // http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=16590 // XXX WMA Pro (wmapro) supports > 2 channels, but ffmpeg doesn't have an encoder for it ac = 2; } } if (ac != -1) { options.add("-ac"); options.add("" + ac); } return options; } @Override public PlayerPurpose getPurpose() { return PlayerPurpose.VIDEO_FILE_PLAYER; } @Override // TODO make this static so it can replace ID, instead of having both public String id() { return ID; } @Override public boolean isTimeSeekable() { return true; } public String initialString() { String threads = ""; if (configuration.isFfmpegMultithreading()) { threads = " -threads " + configuration.getNumberOfCpuCores(); } return threads; } @Override public String name() { return "FFmpeg"; } @Override public int type() { return Format.VIDEO; } // unused; return this array for backwards-compatibility @Deprecated protected String[] getDefaultArgs() { List<String> defaultArgsList = new ArrayList<String>(); defaultArgsList.add("-loglevel"); defaultArgsList.add("warning"); String[] defaultArgsArray = new String[defaultArgsList.size()]; defaultArgsList.toArray(defaultArgsArray); return defaultArgsArray; } private int[] getVideoBitrateConfig(String bitrate) { int bitrates[] = new int[2]; if (bitrate.contains("(") && bitrate.contains(")")) { bitrates[1] = Integer.parseInt(bitrate.substring(bitrate.indexOf("(") + 1, bitrate.indexOf(")"))); } if (bitrate.contains("(")) { bitrate = bitrate.substring(0, bitrate.indexOf("(")).trim(); } if (isBlank(bitrate)) { bitrate = "0"; } bitrates[0] = (int) Double.parseDouble(bitrate); return bitrates; } @Override @Deprecated public String[] args() { return getDefaultArgs(); // unused; return this array for for backwards compatibility } @Override public String mimeType() { return HTTPResource.VIDEO_TRANSCODE; } // FIXME this is a mess: the whole point of the getXOptions methods is to prevent // this turning into another MEncoderVideo, with disorganised kitchen-sink methods // that are over a thousand lines long. // // TODO: move each chunk of functionality into submethods called by a core group of // getXOptions methods @Override public synchronized ProcessWrapper launchTranscode( DLNAResource dlna, DLNAMediaInfo media, OutputParams params ) throws IOException { int nThreads = configuration.getNumberOfCpuCores(); List<String> cmdList = new ArrayList<String>(); RendererConfiguration renderer = params.mediaRenderer; final String filename = dlna.getSystemName(); setAudioAndSubs(filename, media, params, configuration); params.waitbeforestart = 2500; cmdList.add(executable()); cmdList.addAll(getGlobalOptions(logger)); if (params.timeseek > 0) { cmdList.add("-ss"); cmdList.add("" + params.timeseek); } // decoder threads cmdList.add("-threads"); cmdList.add("" + nThreads); final boolean isTsMuxeRVideoEngineEnabled = configuration.getEnginesAsList().contains(TsMuxeRVideo.ID); setAc3Remux(false); setDtsRemux(false); setVideoRemux(false); if (configuration.isAudioRemuxAC3() && params.aid != null && params.aid.isAC3() && renderer.isTranscodeToAC3()) { // AC-3 remux takes priority setAc3Remux(true); } else if (isTsMuxeRVideoEngineEnabled && configuration.isAudioEmbedDtsInPcm() && params.aid != null && params.aid.isDTS() && params.mediaRenderer.isDTSPlayable()) { // Now check for DTS remux setDtsRemux(true); } String frameRateRatio = media.getValidFps(true); String frameRateNumber = media.getValidFps(false); // Input filename cmdList.add("-i"); cmdList.add(filename); if (media.getAudioTracksList().size() > 1) { // Set the video stream cmdList.add("-map"); cmdList.add("0:v"); // Set the proper audio stream cmdList.add("-map"); cmdList.add("0:a:" + (media.getAudioTracksList().indexOf(params.aid))); } // Encoder threads cmdList.add("-threads"); cmdList.add("" + nThreads); if (params.timeend > 0) { cmdList.add("-t"); cmdList.add("" + params.timeend); } // add video bitrate options (-b:a) // cmdList.addAll(getVideoBitrateOptions(filename, dlna, media, params)); // add audio bitrate options (-b:v) // cmdList.addAll(getAudioBitrateOptions(filename, dlna, media, params)); // if the source is too large for the renderer, resize it // and/or add subtitles to video filter // FFmpeg must be compiled with --enable-libass parameter cmdList.addAll(getVideoFilterOptions(dlna, media, params)); int defaultMaxBitrates[] = getVideoBitrateConfig(configuration.getMaximumBitrate()); int rendererMaxBitrates[] = new int[2]; if (renderer.getMaxVideoBitrate() != null) { rendererMaxBitrates = getVideoBitrateConfig(renderer.getMaxVideoBitrate()); } // Give priority to the renderer's maximum bitrate setting over the user's setting if (rendererMaxBitrates[0] > 0 && rendererMaxBitrates[0] < defaultMaxBitrates[0]) { defaultMaxBitrates = rendererMaxBitrates; } if (params.mediaRenderer.getCBRVideoBitrate() == 0) { // Convert value from Mb to Kb defaultMaxBitrates[0] = 1000 * defaultMaxBitrates[0]; // Halve it since it seems to send up to 1 second of video in advance defaultMaxBitrates[0] = defaultMaxBitrates[0] / 2; int bufSize = 1835; // x264 uses different buffering math than MPEG-2 if (!renderer.isTranscodeToH264TSAC3()) { if (media.isHDVideo()) { bufSize = defaultMaxBitrates[0] / 3; } if (bufSize > 7000) { bufSize = 7000; } if (defaultMaxBitrates[1] > 0) { bufSize = defaultMaxBitrates[1]; } if (params.mediaRenderer.isDefaultVBVSize() && rendererMaxBitrates[1] == 0) { bufSize = 1835; } } // Make room for audio if (isDtsRemux()) { defaultMaxBitrates[0] = defaultMaxBitrates[0] - 1510; } else { defaultMaxBitrates[0] = defaultMaxBitrates[0] - configuration.getAudioBitrate(); } // Round down to the nearest Mb defaultMaxBitrates[0] = defaultMaxBitrates[0] / 1000 * 1000; // FFmpeg uses bytes for inputs instead of kbytes like MEncoder bufSize = bufSize * 1000; defaultMaxBitrates[0] = defaultMaxBitrates[0] * 1000; /** * Level 4.1-limited renderers like the PS3 can stutter when H.264 video exceeds * this bitrate */ if (renderer.isTranscodeToH264TSAC3() || isVideoRemux()) { if ( params.mediaRenderer.isH264Level41Limited() && defaultMaxBitrates[0] > 31250000 ) { defaultMaxBitrates[0] = 31250000; } bufSize = defaultMaxBitrates[0]; } cmdList.add("-bufsize"); cmdList.add("" + bufSize); cmdList.add("-maxrate"); cmdList.add("" + defaultMaxBitrates[0]); } // Set audio bitrate and channel count only when doing audio transcoding if (!isAc3Remux() && !isDtsRemux() && !(type() == Format.AUDIO)) { int channels; if (renderer.isTranscodeToWMV() && !renderer.isXBOX()) { channels = 2; } else { channels = configuration.getAudioChannelCount(); // 5.1 max for AC-3 encoding } cmdList.add("-ac"); cmdList.add("" + channels); cmdList.add("-ab"); cmdList.add(configuration.getAudioBitrate() + "k"); } if (params.timeseek > 0) { cmdList.add("-copypriorss"); cmdList.add("0"); cmdList.add("-avoid_negative_ts"); cmdList.add("1"); } // Add MPEG-2 quality settings if (!renderer.isTranscodeToH264TSAC3() && !isVideoRemux()) { String mpeg2Options = configuration.getMPEG2MainSettingsFFmpeg(); String mpeg2OptionsRenderer = params.mediaRenderer.getCustomFFmpegMPEG2Options(); // Renderer settings take priority over user settings if (isNotBlank(mpeg2OptionsRenderer)) { mpeg2Options = mpeg2OptionsRenderer; } else { if (mpeg2Options.contains("Automatic")) { mpeg2Options = "-g 5 -q:v 1 -qmin 2 -qmax 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 = "-g 25 -q:v 1 -qmin 2 -qmax 3"; } if (mpeg2Options.contains("Wireless") || defaultMaxBitrates[0] < 70) { // Lower quality for 720p+ content if (media.getWidth() > 1280) { mpeg2Options = "-g 25 -qmax 7 -qmin 2"; } else if (media.getWidth() > 720) { mpeg2Options = "-g 25 -qmax 5 -qmin 2"; } } } } String[] customOptions = StringUtils.split(mpeg2Options); cmdList.addAll(new ArrayList<String>(Arrays.asList(customOptions))); } // Add the output options (-f, -c:a, -c:v, etc.) cmdList.addAll(getVideoTranscodeOptions(dlna, media, params)); // Add custom options if (StringUtils.isNotEmpty(renderer.getCustomFFmpegOptions())) { parseOptions(renderer.getCustomFFmpegOptions(), cmdList); } if (!isDtsRemux()) { cmdList.add("pipe:"); } String[] cmdArray = new String[cmdList.size()]; cmdList.toArray(cmdArray); cmdArray = finalizeTranscoderArgs( filename, dlna, media, params, cmdArray ); ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params); if (isDtsRemux()) { PipeProcess pipe; 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(ffVideoPipe.getInputPipe()); OutputParams ffparams = new OutputParams(configuration); ffparams.maxBufferSize = 1; ffparams.stdin = params.stdin; String[] cmdArrayDts = new String[cmdList.size()]; cmdList.toArray(cmdArrayDts); cmdArrayDts = finalizeTranscoderArgs( filename, dlna, media, params, cmdArrayDts ); ProcessWrapperImpl ffVideo = new ProcessWrapperImpl(cmdArrayDts, 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(); PipeIPCProcess ffAudioPipe = new PipeIPCProcess(System.currentTimeMillis() + "ffmpegaudio01", System.currentTimeMillis() + "audioout", false, true); StreamModifier sm = new StreamModifier(); sm.setPcm(false); sm.setDtsEmbed(isDtsRemux()); sm.setSampleFrequency(48000); sm.setBitsPerSample(16); sm.setNbChannels(2); List<String> cmdListDTS = new ArrayList<String>(); cmdListDTS.add(executable()); cmdListDTS.add("-y"); cmdListDTS.add("-ss"); if (params.timeseek > 0) { cmdListDTS.add("" + params.timeseek); } else { cmdListDTS.add("0"); } if (params.stdin == null) { cmdListDTS.add("-i"); } else { cmdListDTS.add("-"); } cmdListDTS.add(filename); if (params.timeseek > 0) { cmdListDTS.add("-copypriorss"); cmdListDTS.add("0"); cmdListDTS.add("-avoid_negative_ts"); cmdListDTS.add("1"); } cmdListDTS.add("-ac"); cmdListDTS.add("2"); cmdListDTS.add("-f"); cmdListDTS.add("dts"); cmdListDTS.add("-c:a"); cmdListDTS.add("copy"); cmdListDTS.add(ffAudioPipe.getInputPipe()); String[] cmdArrayDTS = new String[cmdListDTS.size()]; cmdListDTS.toArray(cmdArrayDTS); if (!params.mediaRenderer.isMuxDTSToMpeg()) { // No need to use the PCM trick when media renderer supports DTS ffAudioPipe.setModifier(sm); } OutputParams ffaudioparams = new OutputParams(configuration); ffaudioparams.maxBufferSize = 1; ffaudioparams.stdin = params.stdin; ProcessWrapperImpl ffAudio = new ProcessWrapperImpl(cmdArrayDTS, 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 (isVideoRemux()) { videoType = "V_MPEG4/ISO/AVC"; } if (params.no_videoencode && params.forceType != null) { videoType = params.forceType; } String fps = ""; if (params.forceFps != null) { fps = "fps=" + params.forceFps + ", "; } String audioType = "A_AC3"; if (isDtsRemux()) { if (params.mediaRenderer.isMuxDTSToMpeg()) { // Renderer can play proper DTS track audioType = "A_DTS"; } else { // DTS padded in LPCM trick audioType = "A_LPCM"; } } pwMux.println(videoType + ", \"" + ffVideoPipe.getOutputPipe() + "\", " + fps + "level=4.1, insertSEI, contSPS, track=1"); pwMux.println(audioType + ", \"" + ffAudioPipe.getOutputPipe() + "\", 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(); } pw.runInNewThread(); return pw; } @Override public JComponent config() { return config("NetworkTab.5"); } protected JComponent config(String languageLabel) { FormLayout layout = new FormLayout( "left:pref, 0:grow", "p, 3dlu, p, 3dlu, p, 3dlu, p" ); PanelBuilder builder = new PanelBuilder(layout); CellConstraints cc = new CellConstraints(); JComponent cmp = builder.addSeparator(Messages.getString(languageLabel), cc.xyw(2, 1, 1)); cmp = (JComponent) cmp.getComponent(0); cmp.setFont(cmp.getFont().deriveFont(Font.BOLD)); multiThreadingCheckBox = new JCheckBox(Messages.getString("MEncoderVideo.35")); multiThreadingCheckBox.setContentAreaFilled(false); if (configuration.isFfmpegMultithreading()) { multiThreadingCheckBox.setSelected(true); } multiThreadingCheckBox.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setFfmpegMultithreading(e.getStateChange() == ItemEvent.SELECTED); } }); builder.add(multiThreadingCheckBox, cc.xy(2, 3)); videoRemuxCheckBox = new JCheckBox(Messages.getString("FFmpeg.0")); videoRemuxCheckBox.setContentAreaFilled(false); if (configuration.isFFmpegMuxWhenCompatible()) { videoRemuxCheckBox.setSelected(true); } videoRemuxCheckBox.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { configuration.setFFmpegMuxWhenCompatible(e.getStateChange() == ItemEvent.SELECTED); } }); builder.add(videoRemuxCheckBox, cc.xy(2, 5)); return builder.getPanel(); } @Override public boolean isCompatible(DLNAResource dlna) { if ( PlayerUtil.isVideo(dlna, Format.Identifier.MKV) || PlayerUtil.isVideo(dlna, Format.Identifier.MPG) ) { return true; } else { return false; } } protected static List<String> parseOptions(String str) { return str == null ? null : parseOptions(str, new ArrayList<String>()); } protected static List<String> parseOptions(String str, List<String> cmdList) { while (str.length() > 0) { if (str.charAt(0) == '\"') { int pos = str.indexOf("\"", 1); if (pos == -1) { // No ", error break; } String tmp = str.substring(1, pos); cmdList.add(tmp.trim()); str = str.substring(pos + 1); continue; } else { // New arg, find space int pos = str.indexOf(" "); if (pos == -1) { // No space, we're done cmdList.add(str); break; } String tmp = str.substring(0, pos); cmdList.add(tmp.trim()); str = str.substring(pos + 1); continue; } } return cmdList; } /** * Shift timing of external subtitles in SSA/ASS or SRT format and converts charset to UTF8 if necessary * * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request. * @return Converted subtitle file * @throws IOException */ public DLNAMediaSubtitle getSubtitles(OutputParams params) throws IOException { DLNAMediaSubtitle tempSubs = null; if (params.sid.getId() == -1) { return null; } final File subtitleDirectory = new File(configuration.getTempFolder(), SUB_DIR + File.separator); if (!subtitleDirectory.exists()) { subtitleDirectory.mkdirs(); } if (params.sid.isExternal() && SubtitleUtils.isSupportsTimeShifting(params.sid.getType())) { try { tempSubs = SubtitleUtils.shiftSubtitlesTimingWithUtfConversion(params.sid, params.timeseek); } catch (IOException e) { logger.debug("Applying timeshift caused an error: " + e); tempSubs = null; } } return tempSubs; } /** * Converts external subtitles file in SRT format or extract embedded subs to default SSA/ASS format. * * @param filename Subtitle file in SRT format or video file with embedded subs * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos). * @param params The {@link net.pms.io.OutputParams} context object used to store miscellaneous parameters for this request. * @return Converted subtitle file in SSA/ASS format */ // FIXME this is unused private File extractEmbeddedSubtitleTrack(String filename, DLNAMediaInfo media, OutputParams params) throws IOException { final List<String> cmdList = new ArrayList<String>(); File tempSubsFile; cmdList.add(configuration.getFfmpegPath()); cmdList.addAll(getGlobalOptions(logger)); /* TODO Use it when external subs should be converted by ffmpeg if ( isNotBlank(configuration.getSubtitlesCodepage()) && params.sid.isExternal() && !params.sid.isExternalFileUtf8() && !params.sid.getExternalFileCharacterSet().equals(configuration.getSubtitlesCodepage()) // ExternalFileCharacterSet can be null ) { cmdList.add("-sub_charenc"); cmdList.add(configuration.getSubtitlesCodepage()); } */ cmdList.add("-i"); cmdList.add(filename); if (params.sid.isEmbedded()) { cmdList.add("-map"); /* TODO broken code. Consider following example file: Stream #0:0(eng): Video: h264 (High), yuv420p, 720x576, SAR 178:139 DAR 445:278, 25 fps, 25 tbr, 1k tbn, 50 tbc (default) Metadata: title : H264 Stream #0:1(rus): Subtitle: subrip Metadata: title : rus Stream #0:2(rus): Audio: mp3, 48000 Hz, stereo, s16p, 128 kb/s Metadata: title : rus Stream #0:3(eng): Audio: mp3, 48000 Hz, stereo, s16p, 119 kb/s (default) Metadata: title : eng Stream #0:4(eng): Subtitle: subrip (default) Metadata: title : eng FFmpeg sub track ids would be completely different. We should pass real ids. */ cmdList.add("0:" + (params.sid.getId() + media.getAudioTracksList().size() + 1)); } final File subtitleDirectory = new File(configuration.getTempFolder(), SUB_DIR + File.separator); if (!subtitleDirectory.exists()) { subtitleDirectory.mkdirs(); } if (params.sid.isEmbedded()) { tempSubsFile = new File(subtitleDirectory.getAbsolutePath() + File.separator + getBaseName(new File(filename).getName()).replaceAll("\\W", "_") + "_" + new File(filename).length() + "_EMB_ID" + params.sid.getId() + ".ass"); } else { tempSubsFile = new File(subtitleDirectory.getAbsolutePath() + File.separator + getBaseName(new File(filename).getName()).replaceAll("\\W", "_") + "_" + new File(filename).length() + "_EXT." + getExtension(new File(filename).getName())); } cmdList.add(tempSubsFile.getAbsolutePath()); String[] cmdArray = new String[cmdList.size()]; cmdList.toArray(cmdArray); ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params); pw.runInNewThread(); try { pw.join(); // Wait until the conversion is finished } catch (InterruptedException e) { logger.debug("Subtitle conversion finished wih error: " + e); return null; } return tempSubsFile; } /** * 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 */ public boolean isDisableSubtitles(OutputParams params) { return configuration.isDisableSubtitles() || (params.sid == null); } private synchronized boolean isAc3Remux() { return ac3Remux; } private synchronized void setAc3Remux(boolean ac3Remux) { this.ac3Remux = ac3Remux; } private synchronized boolean isDtsRemux() { return dtsRemux; } private synchronized void setDtsRemux(boolean dtsRemux) { this.dtsRemux = dtsRemux; } private synchronized boolean isVideoRemux() { return videoRemux; } private synchronized void setVideoRemux(boolean videoRemux) { this.videoRemux = videoRemux; } }