/*
* PS3 Media Server, for streaming any medias to your PS3.
* Copyright (C) 2008 A.Brochard
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.pms.encoders;
import com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.factories.Borders;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import java.awt.Font;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
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.DLNAResource;
import net.pms.dlna.FileTranscodeVirtualFolder;
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.io.OutputTextLogger;
import net.pms.network.HTTPResource;
import net.pms.util.CodecUtil;
import net.pms.util.PlayerUtil;
import net.pms.util.ProcessUtil;
import org.apache.commons.io.FileUtils;
import static net.pms.util.StringUtil.*;
import org.apache.commons.lang3.StringUtils;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/*
* Pure FFmpeg video player.
*
* Design note:
*
* Helper methods that return lists of <code>String</code>s representing
* options are public to facilitate composition e.g. a custom engine (plugin)
* that uses tsMuxeR for videos without subtitles and FFmpeg otherwise needs to
* compose and call methods on both players.
*
* To avoid API churn, and to provide wiggle room for future functionality, all
* of these methods take the same arguments as launchTranscode (and the same
* first four arguments as finalizeTranscoderArgs) even if one or more of the
* parameters are unused e.g.:
*
* public List<String> getAudioBitrateOptions(
* String filename,
* DLNAResource dlna,
* DLNAMediaInfo media,
* OutputParams params
* )
*/
public class FFMpegVideo extends Player {
private static final Logger LOGGER = LoggerFactory.getLogger(FFMpegVideo.class);
private static final String DEFAULT_QSCALE = "3";
private static final String SUB_DIR = "subs";
public FFMpegVideo() {
}
@Deprecated
public FFMpegVideo(PmsConfiguration configuration) {
this();
}
// 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 either SSA/ASS or picture-based format and resize a video that's too wide and/or high for the specified renderer.
* If the renderer has no size limits, or there's no media metadata, or the video is within the renderer's
* size limits, an empty list is returned.
*
* @param dlna
* @param media metadata for the DLNA resource which is being transcoded
* @param params
* @return a {@link List} of <code>String</code>s representing the rescale options for this video,
* or an empty list if the video doesn't need to be resized.
* @throws java.io.IOException
*/
public List<String> getVideoFilterOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException {
List<String> videoFilterOptions = new ArrayList<>();
ArrayList<String> filterChain = new ArrayList<>();
ArrayList<String> scalePadFilterChain = new ArrayList<>();
final RendererConfiguration renderer = params.mediaRenderer;
boolean isMediaValid = media != null && media.isMediaparsed() && media.getHeight() != 0;
boolean isResolutionTooHighForRenderer = renderer.isMaximumResolutionSpecified() && isMediaValid && // renderer defines a max width/height
(
media.getWidth() > renderer.getMaxVideoWidth() ||
media.getHeight() > renderer.getMaxVideoHeight()
);
int scaleWidth = 0;
int scaleHeight = 0;
if (media.getWidth() > 0 && media.getHeight() > 0) {
scaleWidth = media.getWidth();
scaleHeight = media.getHeight();
}
boolean is3D = media.is3d() && !media.stereoscopyIsAnaglyph();
// Make sure the aspect ratio is 16/9 if the renderer needs it.
boolean keepAR = renderer.isKeepAspectRatio() &&
!media.is3dFullSbsOrOu() &&
!"16:9".equals(media.getAspectRatioContainer());
// Scale and pad the video if necessary
if (isResolutionTooHighForRenderer || (!renderer.isRescaleByRenderer() && renderer.isMaximumResolutionSpecified() && media.getWidth() < 720)) { // Do not rescale for SD video and higher
if (media.is3dFullSbsOrOu()) {
scalePadFilterChain.add(String.format("scale=%1$d:%2$d", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight()));
} else {
scalePadFilterChain.add(String.format("scale=iw*min(%1$d/iw\\,%2$d/ih):ih*min(%1$d/iw\\,%2$d/ih)", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight()));
if (keepAR) {
scalePadFilterChain.add(String.format("pad=%1$d:%2$d:(%1$d-iw)/2:(%2$d-ih)/2", renderer.getMaxVideoWidth(), renderer.getMaxVideoHeight()));
}
}
} else if (keepAR && isMediaValid) {
if ((media.getWidth() / (double) media.getHeight()) >= (16 / (double) 9)) {
scalePadFilterChain.add("pad=iw:iw/(16/9):0:(oh-ih)/2");
scaleHeight = (int) Math.round(scaleWidth / (16 / (double) 9));
} else {
scalePadFilterChain.add("pad=ih*(16/9):ih:(ow-iw)/2:0");
scaleWidth = (int) Math.round(scaleHeight * (16 / (double) 9));
}
scaleWidth = convertToModX(scaleWidth, 4);
scaleHeight = convertToModX(scaleHeight, 4);
// Make sure we didn't exceed the renderer's maximum resolution.
if (
scaleHeight > renderer.getMaxVideoHeight() ||
scaleWidth > renderer.getMaxVideoWidth()
) {
scaleHeight = renderer.getMaxVideoHeight();
scaleWidth = renderer.getMaxVideoWidth();
}
scalePadFilterChain.add("scale=" + scaleWidth + ":" + scaleHeight);
}
if (!isDisableSubtitles(params) && !(dlna.getPlayer() instanceof WebPlayer)) {
StringBuilder subsFilter = new StringBuilder();
if (params.sid.getType().isText()) {
String originalSubsFilename;
String subsFilename;
if (params.sid.isEmbedded() || configuration.isFFmpegFontConfig() || is3D) {
originalSubsFilename = SubtitleUtils.getSubtitles(dlna, media, params, configuration, SubtitleType.ASS).getAbsolutePath();
} else {
originalSubsFilename = params.sid.getExternalFile().getAbsolutePath();
}
if (originalSubsFilename != null) {
StringBuilder s = new StringBuilder();
CharacterIterator it = new StringCharacterIterator(originalSubsFilename);
for (char ch = it.first(); ch != CharacterIterator.DONE; ch = it.next()) {
switch (ch) {
case ':':
s.append("\\\\:");
break;
case '\\':
s.append("/");
break;
case ']':
case '[':
s.append("\\");
default:
s.append(ch);
break;
}
}
subsFilename = s.toString();
subsFilename = subsFilename.replace(",", "\\,");
subsFilter.append("subtitles=").append(subsFilename);
// Set the resolution for subtitles to use
int subtitlesWidth = scaleWidth;
int subtitlesHeight = scaleHeight;
if (params.sid.isExternal() && params.sid.getType() != SubtitleType.ASS || configuration.isFFmpegFontConfig()) {
if (subtitlesWidth > 0 && subtitlesHeight > 0) {
// Let ASS/SSA subtitles specify their own resolution
if (params.sid.getType() == SubtitleType.ASS) {
setSubtitlesResolution(originalSubsFilename, subtitlesWidth, subtitlesHeight);
}
if (!is3D) {
subsFilter.append(":").append(subtitlesWidth).append("x").append(subtitlesHeight);
}
// Set the input subtitles character encoding if not UTF-8
if (!params.sid.isExternalFileUtf8()) {
String encoding = isNotBlank(configuration.getSubtitlesCodepage()) ?
configuration.getSubtitlesCodepage() : params.sid.getExternalFileCharacterSet() != null ?
params.sid.getExternalFileCharacterSet() : null;
if (encoding != null) {
subsFilter.append(":").append(encoding);
}
}
}
}
}
} else if (params.sid.getType().isPicture()) {
if (params.sid.getId() < 100) {
// Embedded
subsFilter.append("[0:v][0:s:").append(media.getSubtitleTracksList().indexOf(params.sid)).append("]overlay");
} else {
// External
videoFilterOptions.add("-i");
videoFilterOptions.add(params.sid.getExternalFile().getAbsolutePath());
subsFilter.append("[0:v][1:s]overlay"); // this assumes the sub file is single-language
}
}
if (isNotBlank(subsFilter)) {
filterChain.add(subsFilter.toString());
// based on https://trac.ffmpeg.org/ticket/2067
if (params.timeseek > 0) {
videoFilterOptions.add("-copyts");
videoFilterOptions.add("-copypriorss");
videoFilterOptions.add("0");
videoFilterOptions.add("-avoid_negative_ts");
videoFilterOptions.add("1");
videoFilterOptions.add("-af");
videoFilterOptions.add("asetpts=PTS-" + params.timeseek + "/TB");
filterChain.add("setpts=PTS-" + params.timeseek + "/TB");
}
}
}
String overrideVF = renderer.getFFmpegVideoFilterOverride();
if (StringUtils.isNotEmpty(overrideVF)) {
filterChain.add(overrideVF);
} else {
filterChain.addAll(scalePadFilterChain);
}
// Convert 3D video to the other output 3D format
if (
is3D &&
media.get3DLayout() != null &&
isNotBlank(params.mediaRenderer.getOutput3DFormat()) &&
!media.get3DLayout().toString().toLowerCase().equals(params.mediaRenderer.getOutput3DFormat().trim())
) {
filterChain.add("stereo3d=" + media.get3DLayout().toString().toLowerCase() + ":" + params.mediaRenderer.getOutput3DFormat().trim().toLowerCase());
}
if (filterChain.size() > 0) {
videoFilterOptions.add("-filter_complex");
videoFilterOptions.add(StringUtils.join(filterChain, ","));
}
return videoFilterOptions;
}
/**
* Returns a list of <code>String</code>s representing ffmpeg output
* options (i.e. options that define the output file's video codec,
* audio codec and container) compatible with the renderer's
* <code>TranscodeVideo</code> profile.
*
* @param dlna
* @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
* @param params output parameters
*
* @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> transcodeOptions = new ArrayList<>();
final String filename = dlna.getSystemName();
final RendererConfiguration renderer = params.mediaRenderer;
String customFFmpegOptions = renderer.getCustomFFmpegOptions();
if (
(
renderer.isTranscodeToWMV() &&
!renderer.isXbox360()
) ||
(
renderer.isXboxOne() &&
purpose() == VIDEO_WEBSTREAM_PLAYER
)
) { // WMV
transcodeOptions.add("-c:v");
transcodeOptions.add("wmv2");
if (!customFFmpegOptions.contains("-c:a ")) {
transcodeOptions.add("-c:a");
transcodeOptions.add("wmav2");
}
transcodeOptions.add("-f");
transcodeOptions.add("asf");
} else { // MPEGPSMPEG2AC3, MPEGTSMPEG2AC3, MPEGTSH264AC3 or MPEGTSH264AAC
final boolean isTsMuxeRVideoEngineEnabled = configuration.getEnginesAsList(PMS.get().getRegistry()).contains(TsMuxeRVideo.ID);
// Output audio codec
dtsRemux = isTsMuxeRVideoEngineEnabled &&
configuration.isAudioEmbedDtsInPcm() &&
params.aid != null &&
params.aid.isDTS() &&
!avisynth() &&
renderer.isDTSPlayable();
boolean isSubtitlesAndTimeseek = !isDisableSubtitles(params) && params.timeseek > 0;
if (configuration.isAudioRemuxAC3() && params.aid != null && params.aid.isAC3() && !avisynth() && renderer.isTranscodeToAC3() && !isSubtitlesAndTimeseek) {
// AC-3 remux
if (!customFFmpegOptions.contains("-c:a ")) {
transcodeOptions.add("-c:a");
transcodeOptions.add("copy");
}
} else {
if (dtsRemux) {
// Audio is added in a separate process later
transcodeOptions.add("-an");
} else if (type() == Format.AUDIO) {
// Skip
} else if (renderer.isTranscodeToAAC()) {
transcodeOptions.add("-c:a");
transcodeOptions.add("aac");
transcodeOptions.add("-strict");
transcodeOptions.add("experimental");
} else {
if (!customFFmpegOptions.contains("-c:a ")) {
transcodeOptions.add("-c:a");
transcodeOptions.add("ac3");
}
}
}
InputFile newInput = null;
if (filename != null) {
newInput = new InputFile();
newInput.setFilename(filename);
newInput.setPush(params.stdin);
}
// Output video codec
if (renderer.isTranscodeToH264()) {
transcodeOptions.add("-c:v");
transcodeOptions.add("libx264");
transcodeOptions.add("-preset");
transcodeOptions.add("ultrafast");
transcodeOptions.add("-level");
transcodeOptions.add("31");
transcodeOptions.add("-pix_fmt");
transcodeOptions.add("yuv420p");
} else if (!dtsRemux) {
transcodeOptions.add("-c:v");
transcodeOptions.add("mpeg2video");
}
// Output file format
transcodeOptions.add("-f");
if (dtsRemux) {
transcodeOptions.add("mpeg2video");
} else if (renderer.isTranscodeToMPEGTS()) {
transcodeOptions.add("mpegts");
} else {
transcodeOptions.add("vob");
}
}
return transcodeOptions;
}
/**
* Returns the video bitrate spec for the current transcode according
* to the limits/requirements of the renderer and the user's settings.
*
* @param dlna
* @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
* @param params
* @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) {
List<String> videoBitrateOptions = new ArrayList<>();
int defaultMaxBitrates[] = getVideoBitrateConfig(configuration.getMaximumBitrate());
int rendererMaxBitrates[] = new int[2];
if (StringUtils.isNotEmpty(params.mediaRenderer.getMaxVideoBitrate())) {
rendererMaxBitrates = getVideoBitrateConfig(params.mediaRenderer.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;
LOGGER.trace("Using the video bitrate limit from the renderer config (" + rendererMaxBitrates[0] + ") which is lower than the one from the program settings (" + defaultMaxBitrates[0] + ")");
} else {
LOGGER.trace("Using the video bitrate limit from the program settings (" + defaultMaxBitrates[0] + ")");
}
boolean isXboxOneWebVideo = params.mediaRenderer.isXboxOne() && purpose() == VIDEO_WEBSTREAM_PLAYER;
int maximumBitrate = defaultMaxBitrates[0];
if (params.mediaRenderer.getCBRVideoBitrate() == 0 && params.timeend == 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] /= 2;
LOGGER.trace("Halving the video bitrate limit to " + defaultMaxBitrates[0]);
int bufSize = 1835;
boolean bitrateLevel41Limited = false;
/**
* Although the maximum bitrate for H.264 Level 4.1 is
* officially 50,000 kbit/s, some 4.1-capable renderers
* like the PS3 stutter when video exceeds roughly 31,250
* kbit/s.
*
* We also apply the correct buffer size in this section.
*/
if (!isXboxOneWebVideo && params.mediaRenderer.isTranscodeToH264()) {
if (
params.mediaRenderer.isH264Level41Limited() &&
defaultMaxBitrates[0] > 31250
) {
defaultMaxBitrates[0] = 31250;
bitrateLevel41Limited = true;
LOGGER.trace("Adjusting the video bitrate limit to the H.264 Level 4.1-safe value of 31250");
}
bufSize = defaultMaxBitrates[0];
} else {
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;
}
}
if (!bitrateLevel41Limited) {
// Make room for audio
if (dtsRemux) {
defaultMaxBitrates[0] -= 1510;
} else {
defaultMaxBitrates[0] -= configuration.getAudioBitrate();
}
// Round down to the nearest Mb
defaultMaxBitrates[0] = defaultMaxBitrates[0] / 1000 * 1000;
LOGGER.trace("Adjusting the video bitrate limit to " + defaultMaxBitrates[0] + " to make room for audio");
}
// FFmpeg uses bytes for inputs instead of kbytes like MEncoder
bufSize *= 1000;
defaultMaxBitrates[0] *= 1000;
videoBitrateOptions.add("-bufsize");
videoBitrateOptions.add(String.valueOf(bufSize));
videoBitrateOptions.add("-maxrate");
videoBitrateOptions.add(String.valueOf(defaultMaxBitrates[0]));
}
if (isXboxOneWebVideo || !params.mediaRenderer.isTranscodeToH264()) {
// Add MPEG-2 quality settings
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")) {
boolean isWireless = mpeg2Options.contains("Wireless");
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 (isWireless || maximumBitrate < 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);
videoBitrateOptions.addAll(new ArrayList<>(Arrays.asList(customOptions)));
} else {
// Add x264 quality settings
String x264CRF = configuration.getx264ConstantRateFactor();
// Remove comment from the value
if (x264CRF.contains("/*")) {
x264CRF = x264CRF.substring(x264CRF.indexOf("/*"));
}
if (x264CRF.contains("Automatic")) {
if (x264CRF.contains("Wireless") || maximumBitrate < 70) {
x264CRF = "19";
// Lower quality for 720p+ content
if (media.getWidth() > 1280) {
x264CRF = "23";
} else if (media.getWidth() > 720) {
x264CRF = "22";
}
} else {
x264CRF = "16";
// Lower quality for 720p+ content
if (media.getWidth() > 720) {
x264CRF = "19";
}
}
}
videoBitrateOptions.add("-crf");
videoBitrateOptions.add(x264CRF);
}
return videoBitrateOptions;
}
/**
* Returns the audio bitrate spec for the current transcode according
* to the limits/requirements of the renderer.
*
* @param dlna
* @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
* @param params
* @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> audioBitrateOptions = new ArrayList<>();
audioBitrateOptions.add("-q:a");
audioBitrateOptions.add(DEFAULT_QSCALE);
return audioBitrateOptions;
}
protected boolean dtsRemux;
protected boolean ac3Remux;
@Override
public int purpose() {
return VIDEO_SIMPLEFILE_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;
}
@Override
public boolean avisynth() {
return false;
}
public String initialString() {
String threads = " -threads 1";
if (configuration.isFfmpegMultithreading()) {
if (Runtime.getRuntime().availableProcessors() == configuration.getNumberOfCpuCores()) {
threads = "";
} else {
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<>();
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;
}
@Override
public String executable() {
return configuration.getFfmpegPath();
}
@Override
public boolean isGPUAccelerationReady() {
return false;
}
@Override
public synchronized ProcessWrapper launchTranscode(
DLNAResource dlna,
DLNAMediaInfo media,
OutputParams params
) throws IOException {
final String filename = dlna.getSystemName();
InputFile newInput = new InputFile();
newInput.setFilename(filename);
newInput.setPush(params.stdin);
/*
* Check if the video track and the container report different aspect ratios
*/
boolean aspectRatiosMatch = true;
if (
media.getAspectRatioContainer() != null &&
media.getAspectRatioVideoTrack() != null &&
!media.getAspectRatioContainer().equals(media.getAspectRatioVideoTrack())
) {
aspectRatiosMatch = false;
}
/*
* FFmpeg uses multithreading by default, so provided that the
* user has not disabled FFmpeg multithreading and has not
* chosen to use more or less threads than are available, do not
* specify how many cores to use.
*/
int nThreads = 1;
if (configuration.isFfmpegMultithreading()) {
if (Runtime.getRuntime().availableProcessors() == configuration.getNumberOfCpuCores()) {
nThreads = 0;
} else {
nThreads = configuration.getNumberOfCpuCores();
}
}
List<String> cmdList = new ArrayList<>();
RendererConfiguration renderer = params.mediaRenderer;
boolean avisynth = avisynth();
if (params.timeseek > 0) {
params.waitbeforestart = 200;
} else {
params.waitbeforestart = 2500;
}
setAudioAndSubs(filename, media, params);
cmdList.add(executable());
// Prevent FFmpeg timeout
cmdList.add("-y");
cmdList.add("-loglevel");
if (LOGGER.isTraceEnabled()) { // Set -loglevel in accordance with LOGGER setting
cmdList.add("verbose"); // Could be changed to "verbose" or "debug" if "info" level is not enough
} else {
cmdList.add("fatal");
}
if (params.timeseek > 0) {
cmdList.add("-ss");
cmdList.add(String.valueOf((int) params.timeseek));
}
// Decoder threads
if (nThreads > 0) {
cmdList.add("-threads");
cmdList.add(String.valueOf(nThreads));
}
final boolean isTsMuxeRVideoEngineEnabled = configuration.getEnginesAsList(PMS.get().getRegistry()).contains(TsMuxeRVideo.ID);
final boolean isXboxOneWebVideo = params.mediaRenderer.isXboxOne() && purpose() == VIDEO_WEBSTREAM_PLAYER;
ac3Remux = false;
dtsRemux = false;
if (
configuration.isAudioRemuxAC3() &&
params.aid != null &&
params.aid.isAC3() &&
!avisynth() &&
renderer.isTranscodeToAC3() &&
!isXboxOneWebVideo
) {
// AC-3 remux takes priority
ac3Remux = true;
} else {
// Now check for DTS remux and LPCM streaming
dtsRemux = isTsMuxeRVideoEngineEnabled &&
configuration.isAudioEmbedDtsInPcm() &&
params.aid != null &&
params.aid.isDTS() &&
!avisynth() &&
params.mediaRenderer.isDTSPlayable();
}
String frameRateRatio = media.getValidFps(true);
String frameRateNumber = media.getValidFps(false);
// Input filename
cmdList.add("-i");
if (avisynth && !filename.toLowerCase().endsWith(".iso")) {
File avsFile = AviSynthFFmpeg.getAVSScript(filename, params.sid, params.fromFrame, params.toFrame, frameRateRatio, frameRateNumber);
cmdList.add(ProcessUtil.getShortFileNameIfWideChars(avsFile.getAbsolutePath()));
} else {
cmdList.add(filename);
}
/**
* Defer to MEncoder for subtitles if:
* - The setting is enabled or embedded fonts exist
* - There are subtitles to transcode
* - The file is not being played via the transcode folder
*/
String prependTraceReason = "Switching from FFmpeg to MEncoder to transcode subtitles because ";
if (
params.sid != null &&
!(
!configuration.getHideTranscodeEnabled() &&
dlna.isNoName() &&
(dlna.getParent() instanceof FileTranscodeVirtualFolder)
)
) {
boolean deferToMencoder = false;
if (configuration.isFFmpegDeferToMEncoderForSubtitles()) {
deferToMencoder = true;
LOGGER.trace(prependTraceReason + "the user setting is enabled.");
} else if (media.isEmbeddedFontExists()) {
deferToMencoder = true;
LOGGER.trace(prependTraceReason + "there are embedded fonts.");
}
if (deferToMencoder) {
MEncoderVideo mv = new MEncoderVideo();
return mv.launchTranscode(dlna, media, params);
}
}
// Decide whether to defer to tsMuxeR or continue to use FFmpeg
boolean deferToTsmuxer = true;
prependTraceReason = "Not muxing the video stream with tsMuxeR via FFmpeg because ";
if (!configuration.isFFmpegMuxWithTsMuxerWhenCompatible()) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "the user setting is disabled");
}
if (deferToTsmuxer == true && !configuration.getHideTranscodeEnabled() && dlna.isNoName() && (dlna.getParent() instanceof FileTranscodeVirtualFolder)) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "the file is being played via a FFmpeg entry in the transcode folder.");
}
if (deferToTsmuxer == true && !params.mediaRenderer.isMuxH264MpegTS()) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "the renderer does not support H.264 inside MPEG-TS.");
}
if (deferToTsmuxer == true && params.sid != null) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "we need to burn subtitles.");
}
if (deferToTsmuxer == true && avisynth()) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "we are using AviSynth.");
}
if (deferToTsmuxer == true && params.mediaRenderer.isH264Level41Limited() && !media.isVideoWithinH264LevelLimits(newInput, params.mediaRenderer)) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "the video stream is not within H.264 level limits for this renderer.");
}
if (deferToTsmuxer == true && !media.isMuxable(params.mediaRenderer)) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "the video stream is not muxable to this renderer");
}
if (deferToTsmuxer == true && !aspectRatiosMatch) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "we need to transcode to apply the correct aspect ratio.");
}
if (deferToTsmuxer == true && !params.mediaRenderer.isPS3() && filename.contains("WEB-DL")) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "the version of tsMuxeR supported by this renderer does not support WEB-DL files.");
}
if (deferToTsmuxer == true && "bt.601".equals(media.getMatrixCoefficients())) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "the colorspace probably isn't supported by the renderer.");
}
if (deferToTsmuxer == true && params.mediaRenderer.isKeepAspectRatio() && !"16:9".equals(media.getAspectRatioContainer())) {
deferToTsmuxer = false;
LOGGER.trace(prependTraceReason + "the renderer needs us to add borders so it displays the correct aspect ratio of " + media.getAspectRatioContainer() + ".");
}
if (deferToTsmuxer) {
TsMuxeRVideo tv = new TsMuxeRVideo();
params.forceFps = media.getValidFps(false);
if (media.getCodecV() != null) {
if (media.isH264()) {
params.forceType = "V_MPEG4/ISO/AVC";
} else if (media.getCodecV().startsWith("mpeg2")) {
params.forceType = "V_MPEG-2";
} else if (media.getCodecV().equals("vc1")) {
params.forceType = "V_MS/VFW/WVC1";
}
}
return tv.launchTranscode(dlna, media, params);
}
// Apply any video filters and associated options. These should go
// after video input is specified and before output streams are mapped.
cmdList.addAll(getVideoFilterOptions(dlna, media, params));
// Map the output streams if necessary
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)));
}
// Now configure the output streams
// Encoder threads
if (nThreads > 0) {
cmdList.add("-threads");
cmdList.add(String.valueOf(nThreads));
}
if (params.timeend > 0) {
cmdList.add("-t");
cmdList.add(String.valueOf(params.timeend));
}
cmdList.addAll(getVideoBitrateOptions(dlna, media, params));
// add audio bitrate options
// TODO: Integrate our (more comprehensive) code with this function
// from PMS to make keeping synchronised easier.
// Until then, leave the following line commented out.
// cmdList.addAll(getAudioBitrateOptions(dlna, media, params));
String customFFmpegOptions = renderer.getCustomFFmpegOptions();
// Audio bitrate
if (!ac3Remux && !dtsRemux && !(type() == Format.AUDIO)) {
int channels = 0;
if (
(
renderer.isTranscodeToWMV() &&
!renderer.isXbox360()
) ||
(
renderer.isXboxOne() &&
purpose() == VIDEO_WEBSTREAM_PLAYER
)
) {
channels = 2;
} else if (params.aid != null && params.aid.getAudioProperties().getNumberOfChannels() > configuration.getAudioChannelCount()) {
channels = configuration.getAudioChannelCount();
}
if (!customFFmpegOptions.contains("-ac ") && channels > 0) {
cmdList.add("-ac");
cmdList.add(String.valueOf(channels));
}
if (!customFFmpegOptions.contains("-ab ")) {
cmdList.add("-ab");
if (renderer.isTranscodeToAAC()) {
cmdList.add(Math.min(configuration.getAudioBitrate(), 320) + "k");
} else {
cmdList.add(String.valueOf(CodecUtil.getAC3Bitrate(configuration, params.aid)) + "k");
}
}
}
// Add the output options (-f, -c:a, -c:v, etc.)
cmdList.addAll(getVideoTranscodeOptions(dlna, media, params));
// Add custom options
if (StringUtils.isNotEmpty(customFFmpegOptions)) {
parseOptions(customFFmpegOptions, cmdList);
}
if (!dtsRemux) {
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);
setOutputParsing(dlna, pw, false);
if (dtsRemux) {
PipeProcess pipe;
pipe = new PipeProcess(System.currentTimeMillis() + "tsmuxerout.ts");
TsMuxeRVideo ts = new TsMuxeRVideo();
File f = new File(configuration.getTempFolder(), "pms-tsmuxer.meta");
String cmd[] = new String[]{ ts.executable(), f.getAbsolutePath(), pipe.getInputPipe() };
pw = new ProcessWrapperImpl(cmd, params);
PipeIPCProcess ffVideoPipe = new PipeIPCProcess(System.currentTimeMillis() + "ffmpegvideo", System.currentTimeMillis() + "videoout", false, true);
cmdList.add(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(dtsRemux);
sm.setSampleFrequency(48000);
sm.setBitsPerSample(16);
sm.setNbChannels(2);
List<String> cmdListDTS = new ArrayList<>();
cmdListDTS.add(executable());
cmdListDTS.add("-y");
cmdListDTS.add("-ss");
if (params.timeseek > 0) {
cmdListDTS.add(String.valueOf(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;
try (PrintWriter pwMux = new PrintWriter(f)) {
pwMux.println("MUXOPT --no-pcr-on-video-pid --no-asyncio --new-audio-pes --vbr --vbv-len=500");
String videoType = "V_MPEG-2";
if (renderer.isTranscodeToH264()) {
videoType = "V_MPEG4/ISO/AVC";
}
if (params.no_videoencode && params.forceType != null) {
videoType = params.forceType;
}
StringBuilder fps = new StringBuilder();
fps.append("");
if (params.forceFps != null) {
fps.append("fps=").append(params.forceFps).append(", ");
}
String audioType = "A_AC3";
if (dtsRemux) {
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");
}
ProcessWrapper pipe_process = pipe.getPipeProcess();
pw.attachProcess(pipe_process);
pipe_process.runInNewThread();
try {
wait(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 {
wait(50);
} catch (InterruptedException e) {
}
ffAudioPipe.deleteLater();
pw.attachProcess(ffAudio);
ffAudio.runInNewThread();
}
pw.runInNewThread();
return pw;
}
private JCheckBox multithreading;
private JCheckBox videoRemuxTsMuxer;
private JCheckBox fc;
private JCheckBox deferToMEncoderForSubtitles;
@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, 3dlu, p"
);
PanelBuilder builder = new PanelBuilder(layout);
builder.border(Borders.EMPTY);
builder.opaque(false);
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));
multithreading = new JCheckBox(Messages.getString("MEncoderVideo.35"), configuration.isFfmpegMultithreading());
multithreading.setContentAreaFilled(false);
multithreading.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
configuration.setFfmpegMultithreading(e.getStateChange() == ItemEvent.SELECTED);
}
});
builder.add(multithreading, cc.xy(2, 3));
videoRemuxTsMuxer = new JCheckBox(Messages.getString("MEncoderVideo.38"), configuration.isFFmpegMuxWithTsMuxerWhenCompatible());
videoRemuxTsMuxer.setContentAreaFilled(false);
videoRemuxTsMuxer.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
configuration.setFFmpegMuxWithTsMuxerWhenCompatible(e.getStateChange() == ItemEvent.SELECTED);
}
});
builder.add(videoRemuxTsMuxer, cc.xy(2, 5));
fc = new JCheckBox(Messages.getString("MEncoderVideo.21"), configuration.isFFmpegFontConfig());
fc.setContentAreaFilled(false);
fc.setToolTipText(Messages.getString("FFmpeg.0"));
fc.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
configuration.setFFmpegFontConfig(e.getStateChange() == ItemEvent.SELECTED);
}
});
builder.add(fc, cc.xy(2, 7));
deferToMEncoderForSubtitles = new JCheckBox(Messages.getString("FFmpeg.1"), configuration.isFFmpegDeferToMEncoderForSubtitles());
deferToMEncoderForSubtitles.setContentAreaFilled(false);
deferToMEncoderForSubtitles.setToolTipText(Messages.getString("FFmpeg.2"));
deferToMEncoderForSubtitles.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
configuration.setFFmpegDeferToMEncoderForSubtitles(e.getStateChange() == ItemEvent.SELECTED);
}
});
builder.add(deferToMEncoderForSubtitles, cc.xy(2, 9));
return builder.getPanel();
}
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);
} 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);
}
}
return cmdList;
}
/**
* Collapse the multiple internal ways of saying "subtitles are disabled" into a single method
* which returns true if any of the following are true:
*
* 1) configuration.isDisableSubtitles()
* 2) params.sid == null
* 3) avisynth()
* @param params
* @return
*/
public boolean isDisableSubtitles(OutputParams params) {
return configuration.isDisableSubtitles() || (params.sid == null) || avisynth();
}
/**
* {@inheritDoc}
*
* @return
*/
@Override
public boolean isCompatible(DLNAResource resource) {
if (
PlayerUtil.isVideo(resource, Format.Identifier.MKV) ||
PlayerUtil.isVideo(resource, Format.Identifier.MPG) ||
"m3u8".equals(resource.getFormat().getMatchedExtension())
) {
return true;
}
return false;
}
// matches 'Duration: 00:17:17.00' but not 'Duration: N/A'
static final Matcher reDuration = Pattern.compile("Duration:\\s+([\\d:.]+),").matcher("");
/**
* Set up a filter to parse ffmpeg's stderr output for info
* (e.g. duration) if required.
*/
public void setOutputParsing(final DLNAResource dlna, ProcessWrapperImpl pw, boolean force) {
if (configuration.isResumeEnabled() && dlna.getMedia() != null) {
long duration = force ? 0 : (long) dlna.getMedia().getDurationInSeconds();
if (duration == 0 || duration == DLNAMediaInfo.TRANS_SIZE) {
OutputTextLogger ffParser = new OutputTextLogger(null, pw) {
@Override
public boolean filter(String line) {
if (reDuration.reset(line).find()) {
String d = reDuration.group(1);
LOGGER.trace("[{}] setting duration: {}", ID, d);
dlna.getMedia().setDuration(convertStringToTime(d));
return false; // done, stop filtering
}
return true; // keep filtering
}
};
ffParser.setFiltered(true);
pw.setStderrConsumer(ffParser);
}
}
}
public static void deleteSubs() {
FileUtils.deleteQuietly(new File(configuration.getDataFile(SUB_DIR)));
}
private void setSubtitlesResolution(String subtitles, int subtitlesWidth, int subtitlesHeight) throws IOException {
BufferedReader input = new BufferedReader(new InputStreamReader(new FileInputStream(new File(subtitles))));
String line;
boolean resolved = false;
while ((line = input.readLine()) != null) {
if (line.contains("[Script Info]")) {
while ((line = input.readLine()) != null) {
if (isNotBlank(line)) {
if (line.contains("PlayResX:")) {
subtitlesWidth = Integer.parseInt(line.substring(9).trim());
} else if (line.contains("PlayResY:")) {
subtitlesHeight = Integer.parseInt(line.substring(9).trim());
}
} else {
resolved = true;
break;
}
}
}
if (resolved) {
input.close();
break;
}
}
}
}