package net.pms.configuration; import com.sun.jna.Platform; import net.pms.Messages; import net.pms.PMS; import net.pms.dlna.DLNAMediaInfo; import net.pms.dlna.LibMediaInfoParser; import net.pms.dlna.RootFolder; import net.pms.formats.Format; import net.pms.network.HTTPResource; import net.pms.network.SpeedStats; import net.pms.util.PropertiesUtil; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.PropertiesConfiguration; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.StringTokenizer; import java.util.regex.Pattern; public class RendererConfiguration { private static final Logger logger = LoggerFactory.getLogger(RendererConfiguration.class); private static ArrayList<RendererConfiguration> rendererConfs; private static PmsConfiguration pmsConfiguration; private static RendererConfiguration defaultConf; private static Map<InetAddress, RendererConfiguration> addressAssociation = new HashMap<InetAddress, RendererConfiguration>(); private RootFolder rootFolder; private final PropertiesConfiguration configuration; private final ConfigurationReader configurationReader; private FormatConfiguration formatConfiguration; private int rank; /** Holds mime type aliases */ private final Map<String, String> mimes; private final Map<String, String> DLNAPN; // property values private static final String DEPRECATED_MPEGPSAC3 = "MPEGAC3"; // XXX deprecated: old name with missing container private static final String LPCM = "LPCM"; private static final String MP3 = "MP3"; // TODO (breaking change): rename MPEG2PS private static final String MPEGPSAC3 = "MPEGPSAC3"; // TODO (breaking change): rename MPEG2TS private static final String MPEGTSAC3 = "MPEGTSAC3"; private static final String H264TSAC3 = "H264TSAC3"; private static final String WAV = "WAV"; private static final String WMV = "WMV"; // property names private static final String AUDIO = "Audio"; private static final String AUTO_EXIF_ROTATE = "AutoExifRotate"; private static final String BYTE_TO_TIMESEEK_REWIND_SECONDS = "ByteToTimeseekRewindSeconds"; // Ditlew private static final String CBR_VIDEO_BITRATE = "CBRVideoBitrate"; // Ditlew private static final String CHUNKED_TRANSFER = "ChunkedTransfer"; private static final String CUSTOM_MENCODER_OPTIONS = "CustomMencoderOptions"; private static final String CUSTOM_MENCODER_MPEG2_OPTIONS = "CustomMencoderQualitySettings"; // TODO (breaking change): value should be CustomMEncoderMPEG2Options private static final String DEFAULT_VBV_BUFSIZE = "DefaultVBVBufSize"; private static final String DLNA_LOCALIZATION_REQUIRED = "DLNALocalizationRequired"; private static final String DLNA_ORGPN_USE = "DLNAOrgPN"; private static final String DLNA_PN_CHANGES = "DLNAProfileChanges"; private static final String DLNA_TREE_HACK = "CreateDLNATreeFaster"; private static final String FORCE_JPG_THUMBNAILS = "ForceJPGThumbnails"; // Sony devices require JPG thumbnails private static final String H264_L41_LIMITED = "H264Level41Limited"; private static final String IMAGE = "Image"; private static final String LONG_FILENAME_FORMAT = "LongFilenameFormat"; private static final String KEEP_PAD_VIDEO_WITH_BLACK_BORDERS = "PadVideoWithBlackBordersTo169AR"; private static final String MAX_VIDEO_BITRATE = "MaxVideoBitrateMbps"; private static final String MAX_VIDEO_HEIGHT = "MaxVideoHeight"; private static final String MAX_VIDEO_WIDTH = "MaxVideoWidth"; private static final String MEDIAPARSERV2 = "MediaInfo"; private static final String MEDIAPARSERV2_THUMB = "MediaParserV2_ThumbnailGeneration"; private static final String MIME_TYPES_CHANGES = "MimeTypesChanges"; private static final String MUX_DTS_TO_MPEG = "MuxDTSToMpeg"; private static final String MUX_H264_WITH_MPEGTS = "MuxH264ToMpegTS"; private static final String MUX_LPCM_TO_MPEG = "MuxLPCMToMpeg"; private static final String RENDERER_ICON = "RendererIcon"; private static final String RENDERER_NAME = "RendererName"; private static final String RESCALE_BY_RENDERER = "RescaleByRenderer"; private static final String SEEK_BY_TIME = "SeekByTime"; private static final String SHORT_FILENAME_FORMAT = "ShortFilenameFormat"; private static final String SHOW_AUDIO_METADATA = "ShowAudioMetadata"; private static final String SHOW_DVD_TITLE_DURATION = "ShowDVDTitleDuration"; // Ditlew private static final String SHOW_SUB_METADATA = "ShowSubMetadata"; private static final String STREAM_EXT = "StreamExtensions"; private static final String SUBTITLE_HTTP_HEADER = "SubtitleHttpHeader"; private static final String SUPPORTED = "Supported"; private static final String THUMBNAIL_AS_RESOURCE = "ThumbnailAsResource"; private static final String TRANSCODE_AUDIO_441KHZ = "TranscodeAudioTo441kHz"; private static final String TRANSCODE_AUDIO = "TranscodeAudio"; private static final String TRANSCODED_SIZE = "TranscodedVideoFileSize"; private static final String TRANSCODE_EXT = "TranscodeExtensions"; private static final String TRANSCODE_FAST_START = "TranscodeFastStart"; private static final String TRANSCODE_VIDEO = "TranscodeVideo"; private static final String USER_AGENT_ADDITIONAL_HEADER = "UserAgentAdditionalHeader"; private static final String USER_AGENT_ADDITIONAL_SEARCH = "UserAgentAdditionalHeaderSearch"; private static final String USER_AGENT = "UserAgentSearch"; private static final String USE_SAME_EXTENSION = "UseSameExtension"; private static final String VIDEO = "Video"; private static final String WRAP_DTS_INTO_PCM = "WrapDTSIntoPCM"; private static final String CUSTOM_FFMPEG_OPTIONS = "CustomFFmpegOptions"; private static final String OVERRIDE_VF = "OverrideVideoFilter"; public static RendererConfiguration getDefaultConf() { return defaultConf; } /** * Load all renderer configuration files and set up the default renderer. * * @param pmsConf */ public static void loadRendererConfigurations(PmsConfiguration pmsConf) { pmsConfiguration = pmsConf; rendererConfs = new ArrayList<RendererConfiguration>(); try { defaultConf = new RendererConfiguration(); } catch (ConfigurationException e) { logger.debug("Caught exception", e); } File renderersDir = getRenderersDir(); if (renderersDir != null) { logger.info("Loading renderer configurations from " + renderersDir.getAbsolutePath()); File[] confs = renderersDir.listFiles(); Arrays.sort(confs); int rank = 1; for (File f : confs) { if (f.getName().endsWith(".conf")) { try { logger.info("Loading configuration file: {}", f.getName()); RendererConfiguration r = new RendererConfiguration(f); r.rank = rank++; rendererConfs.add(r); } catch (ConfigurationException ce) { logger.info("Error in loading configuration of: {}", f.getAbsolutePath()); } } } } if (rendererConfs.size() > 0) { // See if a different default configuration was configured String rendererFallback = pmsConfiguration.getRendererDefault(); if (StringUtils.isNotBlank(rendererFallback)) { RendererConfiguration fallbackConf = getRendererConfigurationByName(rendererFallback); if (fallbackConf != null) { // A valid fallback configuration was set, use it as default. defaultConf = fallbackConf; } } } } private int getInt(String key, int def) { return configurationReader.getInt(key, def); } private long getLong(String key, int def) { return configurationReader.getLong(key, def); } private boolean getBoolean(String key, boolean def) { return configurationReader.getBoolean(key, def); } /** * Return the <code>String</code> value for a given configuration key if the * value is non-blank (i.e. not null, not an empty string, not all whitespace). * Otherwise return the supplied default value. * The value is returned with leading and trailing whitespace removed in both cases. * @param key The key to look up. * @param def The default value to return when no valid key value can be found. * @return The value configured for the key. */ private String getString(String key, String def) { return configurationReader.getString(key, def); } /** * Returns the list of all renderer configurations. * * @return The list of all configurations. */ public static ArrayList<RendererConfiguration> getAllRendererConfigurations() { return rendererConfs; } protected static File getRenderersDir() { final String[] pathList = PropertiesUtil.getProjectProperties().get("project.renderers.dir").split(","); for (String path : pathList) { if (path.trim().length() > 0) { File file = new File(path.trim()); if (file.isDirectory()) { if (file.canRead()) { return file; } else { logger.warn("Can't read directory: {}", file.getAbsolutePath()); } } } } return null; } public static void resetAllRenderers() { for (RendererConfiguration rc : rendererConfs) { rc.rootFolder = null; } } public RootFolder getRootFolder() { if (rootFolder == null) { rootFolder = new RootFolder(); if (pmsConfiguration.getUseCache()) { rootFolder.discoverChildren(); } } return rootFolder; } /** * Associate an IP address with this renderer. The association will * persist between requests, allowing the renderer to be recognized * by its address in later requests. * @param sa The IP address to associate. * @see #getRendererConfigurationBySocketAddress(InetAddress) */ public void associateIP(InetAddress sa) { addressAssociation.put(sa, this); SpeedStats.getInstance().getSpeedInMBits(sa, getRendererName()); } /** * Tries to find a matching renderer based on the configuration setting * for forced IP address and renderer combinations. If there is no * match, the address is looked up in the address association map which * contains a mapping of previously encountered IP addresses and their * renderers. * * @param inetAddress The IP address to look up. * @return A renderer configuration or null if none can be found. */ public static RendererConfiguration getRendererConfigurationBySocketAddress(InetAddress inetAddress) { // First see if a renderer is forced for this address. String forced = pmsConfiguration.getRendererForceIp(); if (forced != null && !"".equals(forced)) { for (String tuple : forced.split(",")) { if (tuple.indexOf("@") > -1) { String name = tuple.split("@")[0]; String ip = tuple.split("@")[1]; // Sanity checks on the strings if (!"".equals(name) && !"".equals(ip)) { IpFilter filter = new IpFilter(ip); if (filter.isMatch(inetAddress)) { RendererConfiguration renderer = getRendererConfigurationByName(name); if (renderer != null) { logger.trace("Forcing renderer match to \"" + renderer.getRendererName() + "\" based on forced IP address configuration"); addressAssociation.put(inetAddress, renderer); return renderer; } } } } } } return addressAssociation.get(inetAddress); } /** * Tries to find a matching renderer configuration based on a request * header line with a User-Agent header. These matches are made using * the "UserAgentSearch" configuration option in a renderer.conf. * Returns the matched configuration or <code>null</code> if no match * could be found. * * @param userAgentString The request header line. * @return The matching renderer configuration or <code>null</code>. */ public static RendererConfiguration getRendererConfigurationByUA(String userAgentString) { if (pmsConfiguration.isRendererForceDefault()) { // Force default renderer logger.trace("Forcing renderer match to \"" + defaultConf.getRendererName() + "\""); return manageRendererMatch(defaultConf); } else { // Try to find a match for (RendererConfiguration r : rendererConfs) { if (r.matchUserAgent(userAgentString)) { return manageRendererMatch(r); } } } return null; } private static RendererConfiguration manageRendererMatch(RendererConfiguration r) { if (addressAssociation.values().contains(r)) { // FIXME: This cannot ever ever happen because of how renderer matching // is implemented in RequestHandler and RequestHandlerV2. The first header // match will associate the IP address with the renderer and from then on // all other requests from the same IP address will be recognized based on // that association. Headers will be ignored and unfortunately they happen // to be the only way to get here. logger.info("Another renderer like " + r.getRendererName() + " was found!"); } return r; } /** * Tries to find a matching renderer configuration based on a request * header line with an additional, non-User-Agent header. These matches * are made based on the "UserAgentAdditionalHeader" and * "UserAgentAdditionalHeaderSearch" configuration options in a * renderer.conf. Returns the matched configuration or <code>null</code> * if no match could be found. * * @param header The request header line. * @return The matching renderer configuration or <code>null</code>. */ public static RendererConfiguration getRendererConfigurationByUAAHH(String header) { if (pmsConfiguration.isRendererForceDefault()) { // Force default renderer logger.trace("Forcing renderer match to \"" + defaultConf.getRendererName() + "\""); return manageRendererMatch(defaultConf); } else { // Try to find a match for (RendererConfiguration r : rendererConfs) { if (StringUtils.isNotBlank(r.getUserAgentAdditionalHttpHeader()) && header.startsWith(r.getUserAgentAdditionalHttpHeader())) { String value = header.substring(header.indexOf(":", r.getUserAgentAdditionalHttpHeader().length()) + 1); if (r.matchAdditionalUserAgent(value)) { return manageRendererMatch(r); } } } } return null; } /** * Tries to find a matching renderer configuration based on the name of * the renderer. Returns true if the provided name is equal to or a * substring of the renderer name defined in a configuration, where case * does not matter. * * @param name The renderer name to match. * @return The matching renderer configuration or <code>null</code> * * @since 1.50.1 */ public static RendererConfiguration getRendererConfigurationByName(String name) { for (RendererConfiguration conf : rendererConfs) { if (conf.getRendererName().toLowerCase().contains(name.toLowerCase())) { return conf; } } return null; } public FormatConfiguration getFormatConfiguration() { return formatConfiguration; } public int getRank() { return rank; } // FIXME These 'is' methods should disappear. Use feature detection instead. @Deprecated public boolean isXBOX() { return getRendererName().toUpperCase().contains("XBOX"); } @Deprecated public boolean isXBMC() { return getRendererName().toUpperCase().contains("XBMC"); } public boolean isPS3() { return getRendererName().toUpperCase().contains("PLAYSTATION") || getRendererName().toUpperCase().contains("PS3"); } public boolean isBRAVIA() { return getRendererName().toUpperCase().contains("BRAVIA"); } @Deprecated public boolean isFDSSDP() { return getRendererName().toUpperCase().contains("FDSSDP"); } // Ditlew public int getByteToTimeseekRewindSeconds() { return getInt(BYTE_TO_TIMESEEK_REWIND_SECONDS, 0); } // Ditlew public int getCBRVideoBitrate() { return getInt(CBR_VIDEO_BITRATE, 0); } // Ditlew public boolean isShowDVDTitleDuration() { return getBoolean(SHOW_DVD_TITLE_DURATION, false); } private RendererConfiguration() throws ConfigurationException { this(null); } public RendererConfiguration(File f) throws ConfigurationException { configuration = new PropertiesConfiguration(); // false: don't log overrides (every renderer conf // overrides multiple settings) configurationReader = new ConfigurationReader(configuration, false); configuration.setListDelimiter((char) 0); if (f != null) { configuration.load(f); } mimes = new HashMap<String, String>(); String mimeTypes = getString(MIME_TYPES_CHANGES, null); if (StringUtils.isNotBlank(mimeTypes)) { StringTokenizer st = new StringTokenizer(mimeTypes, "|"); while (st.hasMoreTokens()) { String mime_change = st.nextToken().trim(); int equals = mime_change.indexOf("="); if (equals > -1) { String old = mime_change.substring(0, equals).trim().toLowerCase(); String nw = mime_change.substring(equals + 1).trim().toLowerCase(); mimes.put(old, nw); } } } DLNAPN = new HashMap<String, String>(); String DLNAPNchanges = getString(DLNA_PN_CHANGES, null); if (DLNAPNchanges != null) { logger.trace("Config DLNAPNchanges: " + DLNAPNchanges); } if (StringUtils.isNotBlank(DLNAPNchanges)) { StringTokenizer st = new StringTokenizer(DLNAPNchanges, "|"); while (st.hasMoreTokens()) { String DLNAPN_change = st.nextToken().trim(); int equals = DLNAPN_change.indexOf("="); if (equals > -1) { String old = DLNAPN_change.substring(0, equals).trim().toUpperCase(); String nw = DLNAPN_change.substring(equals + 1).trim().toUpperCase(); DLNAPN.put(old, nw); } } } if (f == null) { // the default renderer supports everything! configuration.addProperty(MEDIAPARSERV2, true); configuration.addProperty(MEDIAPARSERV2_THUMB, true); configuration.addProperty(SUPPORTED, "f:.+"); } if (isMediaParserV2()) { formatConfiguration = new FormatConfiguration(configuration.getList(SUPPORTED)); } } public String getDLNAPN(String old) { if (DLNAPN.containsKey(old)) { return DLNAPN.get(old); } return old; } public boolean supportsFormat(Format f) { switch (f.getType()) { case Format.VIDEO: return isVideoSupported(); case Format.AUDIO: return isAudioSupported(); case Format.IMAGE: return isImageSupported(); default: break; } return false; } public boolean isVideoSupported() { return getBoolean(VIDEO, true); } public boolean isAudioSupported() { return getBoolean(AUDIO, true); } public boolean isImageSupported() { return getBoolean(IMAGE, true); } public boolean isTranscodeToWMV() { return getVideoTranscode().equals(WMV); } public boolean isTranscodeToAC3() { return isTranscodeToMPEGPSAC3() || isTranscodeToMPEGTSAC3() || isTranscodeToH264TSAC3(); } public boolean isTranscodeToMPEGPSAC3() { String videoTranscode = getVideoTranscode(); return videoTranscode.equals(MPEGPSAC3) || videoTranscode.equals(DEPRECATED_MPEGPSAC3); } public boolean isTranscodeToMPEGTSAC3() { return getVideoTranscode().equals(MPEGTSAC3); } public boolean isTranscodeToH264TSAC3() { return getVideoTranscode().equals(H264TSAC3); } public boolean isAutoRotateBasedOnExif() { return getBoolean(AUTO_EXIF_ROTATE, false); } public boolean isTranscodeToMP3() { return getAudioTranscode().equals(MP3); } public boolean isTranscodeToLPCM() { return getAudioTranscode().equals(LPCM); } public boolean isTranscodeToWAV() { return getAudioTranscode().equals(WAV); } public boolean isTranscodeAudioTo441() { return getBoolean(TRANSCODE_AUDIO_441KHZ, false); } public boolean isH264Level41Limited() { return getBoolean(H264_L41_LIMITED, false); } public boolean isTranscodeFastStart() { return getBoolean(TRANSCODE_FAST_START, false); } public boolean isDLNALocalizationRequired() { return getBoolean(DLNA_LOCALIZATION_REQUIRED, false); } /** * Determine the mime type specific for this renderer, given a generic mime * type. This translation takes into account all configured "Supported" * lines and mime type aliases for this renderer. * * @param matchedMimeType * The mime type to look up. Special values are * <code>HTTPResource.VIDEO_TRANSCODE</code> and * <code>HTTPResource.AUDIO_TRANSCODE</code>, which will be * translated to the mime type of the transcoding profile * configured for this renderer. * @return The mime type. */ public String getMimeType(String mimeType) { if (mimeType == null) { return null; } String matchedMimeType = null; if (isMediaParserV2()) { // Use the supported information in the configuration to determine the transcoding mime type. if (HTTPResource.VIDEO_TRANSCODE.equals(mimeType)) { if (isTranscodeToMPEGTSAC3()) { matchedMimeType = getFormatConfiguration().match(FormatConfiguration.MPEGTS, FormatConfiguration.MPEG2, FormatConfiguration.AC3); } else if (isTranscodeToWMV()) { matchedMimeType = getFormatConfiguration().match(FormatConfiguration.WMV, FormatConfiguration.WMV, FormatConfiguration.WMA); } else { // Default video transcoding mime type matchedMimeType = getFormatConfiguration().match(FormatConfiguration.MPEGPS, FormatConfiguration.MPEG2, FormatConfiguration.AC3); } } else if (HTTPResource.AUDIO_TRANSCODE.equals(mimeType)) { if (isTranscodeToWAV()) { matchedMimeType = getFormatConfiguration().match(FormatConfiguration.WAV, null, null); } else if (isTranscodeToMP3()) { matchedMimeType = getFormatConfiguration().match(FormatConfiguration.MP3, null, null); } else { // Default audio transcoding mime type matchedMimeType = getFormatConfiguration().match(FormatConfiguration.LPCM, null, null); if (matchedMimeType != null) { if (isTranscodeAudioTo441()) { matchedMimeType += ";rate=44100;channels=2"; } else { matchedMimeType += ";rate=48000;channels=2"; } } } } if (matchedMimeType != null) { return matchedMimeType; } else { // Return the mime type as it was determined by MediaParserV2. return mimeType; } } // No match found, try without media parser v2 if (HTTPResource.VIDEO_TRANSCODE.equals(mimeType)) { if (isTranscodeToWMV()) { matchedMimeType = HTTPResource.WMV_TYPEMIME; } else { // Default video transcoding mime type matchedMimeType = HTTPResource.MPEG_TYPEMIME; } } else if (HTTPResource.AUDIO_TRANSCODE.equals(mimeType)) { if (isTranscodeToWAV()) { matchedMimeType = HTTPResource.AUDIO_WAV_TYPEMIME; } else if (isTranscodeToMP3()) { matchedMimeType = HTTPResource.AUDIO_MP3_TYPEMIME; } else { // Default audio transcoding mime type matchedMimeType = HTTPResource.AUDIO_LPCM_TYPEMIME; if (isTranscodeAudioTo441()) { matchedMimeType += ";rate=44100;channels=2"; } else { matchedMimeType += ";rate=48000;channels=2"; } } } if (matchedMimeType == null) { matchedMimeType = mimeType; } // Apply renderer specific mime type aliases if (mimes.containsKey(matchedMimeType)) { return mimes.get(matchedMimeType); } return matchedMimeType; } /** * Pattern match a user agent header string to the "UserAgentSearch" * expression for this renderer. Will return false when the pattern is * empty or when no match can be made. * * @param header The header containing the user agent. * @return True if the pattern matches. */ public boolean matchUserAgent(String header) { String userAgent = getUserAgent(); Pattern userAgentPattern; if (StringUtils.isNotBlank(userAgent)) { userAgentPattern = Pattern.compile(userAgent, Pattern.CASE_INSENSITIVE); return userAgentPattern.matcher(header).find(); } else { return false; } } /** * Pattern match a header string to the "UserAgentAdditionalHeaderSearch" * expression for this renderer. Will return false when the pattern is * empty or when no match can be made. * * @param header The additional header string. * @return True if the pattern matches. */ public boolean matchAdditionalUserAgent(String header) { String userAgentAdditionalHeader = getUserAgentAdditionalHttpHeaderSearch(); Pattern userAgentAddtionalPattern; if (StringUtils.isNotBlank(userAgentAdditionalHeader)) { userAgentAddtionalPattern = Pattern.compile(userAgentAdditionalHeader, Pattern.CASE_INSENSITIVE); return userAgentAddtionalPattern.matcher(header).find(); } else { return false; } } /** * Returns the pattern to match the User-Agent header to as defined in the * renderer configuration. Default value is "". * * @return The User-Agent search pattern. */ public String getUserAgent() { return getString(USER_AGENT, ""); } /** * RendererName: Determines the name that is displayed in the PMS user * interface when this renderer connects. Default value is "Unknown * renderer". * * @return The renderer name. */ public String getRendererName() { return getString(RENDERER_NAME, Messages.getString("PMS.17")); } /** * Returns the icon to use for displaying this renderer in PMS as defined * in the renderer configurations. Default value is "unknown.png". * * @return The renderer icon. */ public String getRendererIcon() { return getString(RENDERER_ICON, "unknown.png"); } /** * LongFilenameFormat: Determines how media file names are formatted outside the * #--TRANSCODE--# folder. Supported formatting options are described in * the PMS.conf entry for filename_format_long. * * @return The format for file names outside the #--TRANSCODE--# folder, * or {@link PmsConfiguration#getLongFilenameFormat()} if not set. */ public String getLongFilenameFormat() { return getString(LONG_FILENAME_FORMAT, pmsConfiguration.getLongFilenameFormat()); } /** * ShortFilenameFormat: Determines how media file names are formatted inside the * #--TRANSCODE--# folder. Supported formatting options are described in * the PMS.conf entry for filename_format_short. * * @return The format for file names in the #--TRANSCODE--# folder, * or {@link PmsConfiguration#getShortFilenameFormat()} if not set. */ public String getShortFilenameFormat() { return getString(SHORT_FILENAME_FORMAT, pmsConfiguration.getShortFilenameFormat()); } /** * Returns the the name of an additional HTTP header whose value should * be matched with the additional header search pattern. The header name * must be an exact match (read: the header has to start with the exact * same case sensitive string). The default value is <code>null</code>. * * @return The additional HTTP header name. */ public String getUserAgentAdditionalHttpHeader() { return getString(USER_AGENT_ADDITIONAL_HEADER, null); } /** * Returns the pattern to match additional headers to as defined in the * renderer configuration. Default value is "". * * @return The User-Agent search pattern. */ public String getUserAgentAdditionalHttpHeaderSearch() { return getString(USER_AGENT_ADDITIONAL_SEARCH, ""); } public String getUseSameExtension(String displayName) { String s = getString(USE_SAME_EXTENSION, null); if (s != null) { s = displayName + "." + s; } else { s = displayName; } return s; } /** * Returns true if SeekByTime is set to "true" or "exclusive", false otherwise. * Default value is false. * * @return true if the renderer supports seek-by-time, false otherwise. */ public boolean isSeekByTime() { return isSeekByTimeExclusive() || getBoolean(SEEK_BY_TIME, false); } /** * Returns true if SeekByTime is set to "exclusive", false otherwise. * Default value is false. * * @return true if the renderer supports seek-by-time exclusively * (i.e. not in conjunction with seek-by-byte), false otherwise. */ public boolean isSeekByTimeExclusive() { return getString(SEEK_BY_TIME, "").equalsIgnoreCase("exclusive"); } public boolean isMuxH264MpegTS() { boolean muxCompatible = getBoolean(MUX_H264_WITH_MPEGTS, true); if (isMediaParserV2()) { muxCompatible = getFormatConfiguration().match(FormatConfiguration.MPEGTS, FormatConfiguration.H264, null) != null; } if (Platform.isMac() && System.getProperty("os.version") != null && System.getProperty("os.version").contains("10.4.")) { muxCompatible = false; // no tsMuxeR for 10.4 (yet?) } return muxCompatible; } public boolean isDTSPlayable() { return isMuxDTSToMpeg() || (isWrapDTSIntoPCM() && isMuxLPCMToMpeg()); } public boolean isMuxDTSToMpeg() { if (isMediaParserV2()) { return getFormatConfiguration().isDTSSupported(); } return getBoolean(MUX_DTS_TO_MPEG, false); } public boolean isWrapDTSIntoPCM() { return getBoolean(WRAP_DTS_INTO_PCM, true); } public boolean isLPCMPlayable() { return isMuxLPCMToMpeg(); } public boolean isMuxLPCMToMpeg() { if (isMediaParserV2()) { return getFormatConfiguration().isLPCMSupported(); } return getBoolean(MUX_LPCM_TO_MPEG, true); } public boolean isMpeg2Supported() { if (isMediaParserV2()) { return getFormatConfiguration().isMpeg2Supported(); } return isPS3(); } /** * Returns the codec to use for video transcoding for this renderer as * defined in the renderer configuration. Default value is "MPEGPSAC3". * * @return The codec name. */ public String getVideoTranscode() { return getString(TRANSCODE_VIDEO, MPEGPSAC3); } /** * Returns the codec to use for audio transcoding for this renderer as * defined in the renderer configuration. Default value is "LPCM". * * @return The codec name. */ public String getAudioTranscode() { return getString(TRANSCODE_AUDIO, LPCM); } /** * Returns whether or not to use the default DVD buffer size for this * renderer as defined in the renderer configuration. Default is false. * * @return True if the default size should be used. */ public boolean isDefaultVBVSize() { return getBoolean(DEFAULT_VBV_BUFSIZE, false); } /** * Returns the maximum bitrate (in megabits-per-second) supported by the media renderer as defined * in the renderer configuration. The default value is <code>null</code>. * * @return The bitrate. */ @Deprecated // TODO this should return an integer and the units should be bits-per-second public String getMaxVideoBitrate() { return getString(MAX_VIDEO_BITRATE, null); } @Deprecated public String getCustomMencoderQualitySettings() { return getCustomMEncoderMPEG2Options(); } /** * Returns the override settings for MEncoder quality settings as * defined in the renderer configuration. The default value is "". * * @return The MEncoder quality settings. */ public String getCustomMEncoderMPEG2Options() { return getString(CUSTOM_MENCODER_MPEG2_OPTIONS, ""); } /** * Converts the getCustomMencoderQualitySettings() from MEncoder's format to FFmpeg's. * * @return The FFmpeg quality settings. */ public String getCustomFFmpegMPEG2Options() { String mpegSettings = getCustomMEncoderMPEG2Options(); String mpegSettingsArray[] = mpegSettings.split(":"); String pairArray[]; String returnString = ""; for (String pair : mpegSettingsArray) { pairArray = pair.split("="); if ("keyint".equals(pairArray[0])) { returnString += "-g " + pairArray[1] + " "; } else if ("vqscale".equals(pairArray[0])) { returnString += "-q:v " + pairArray[1] + " "; } else if ("vqmin".equals(pairArray[0])) { returnString += "-qmin " + pairArray[1] + " "; } else if ("vqmax".equals(pairArray[0])) { returnString += "-qmax " + pairArray[1] + " "; } } return returnString; } /** * Returns the override settings for MEncoder custom options in PMS as * defined in the renderer configuration. The default value is "". * * @return The MEncoder custom options. */ public String getCustomMencoderOptions() { return getString(CUSTOM_MENCODER_OPTIONS, ""); } /** * Returns the maximum video width supported by the renderer as defined in * the renderer configuration. The default value 0 means unlimited. * * @return The maximum video width. */ public int getMaxVideoWidth() { // FIXME why is this 1920 if the default value is 0 (unlimited)? // XXX we should also require width and height to both be 0 or both be > 0 return getInt(MAX_VIDEO_WIDTH, 1920); } /** * Returns the maximum video height supported by the renderer as defined * in the renderer configuration. The default value 0 means unlimited. * * @return The maximum video height. */ public int getMaxVideoHeight() { // FIXME why is this 1080 if the default value is 0 (unlimited)? // XXX we should also require width and height to both be 0 or both be > 0 return getInt(MAX_VIDEO_HEIGHT, 1080); } /** * Returns <code>true</code> if the renderer has a maximum supported width * or height, <code>false</code> otherwise. * * @return boolean indicating whether the renderer may need videos to be resized. */ public boolean isVideoRescale() { return getMaxVideoWidth() > 0 && getMaxVideoHeight() > 0; } public boolean isDLNAOrgPNUsed() { return getBoolean(DLNA_ORGPN_USE, true); } /** * Returns whether or not to use the "res" element instead of the "albumArtURI" * element for thumbnails in DLNA reponses. E.g. Samsung 2012 models do not * recognize the "albumArtURI" element. Default value is <code>false</code>. * * @return True if the "res" element should be used, false otherwise. */ public boolean getThumbNailAsResource() { return getBoolean(THUMBNAIL_AS_RESOURCE, false); } /** * Returns the comma separated list of file extensions that are forced to * be transcoded and never streamed, as defined in the renderer * configuration. Default value is "". * * @return The file extensions. */ public String getTranscodedExtensions() { return getString(TRANSCODE_EXT, ""); } /** * Returns the comma separated list of file extensions that are forced to * be streamed and never transcoded, as defined in the renderer * configuration. Default value is "". * * @return The file extensions. */ public String getStreamedExtensions() { return getString(STREAM_EXT, ""); } /** * Returns the size to report back to the renderer when transcoding media * as defined in the renderer configuration. Default value is 0. * * @return The size to report. */ public long getTranscodedSize() { return getLong(TRANSCODED_SIZE, 0); } /** * Some devices (e.g. Samsung) recognize a custom HTTP header for retrieving * the contents of a subtitles file. This method will return the name of that * custom HTTP header, or "" if no such header exists. Default value is "". * * @return The name of the custom HTTP header. */ public String getSubtitleHttpHeader() { return getString(SUBTITLE_HTTP_HEADER, ""); } @Override public String toString() { return getRendererName(); } public boolean isMediaParserV2() { return getBoolean(MEDIAPARSERV2, false) && LibMediaInfoParser.isValid(); } public boolean isMediaParserV2ThumbnailGeneration() { return getBoolean(MEDIAPARSERV2_THUMB, false) && LibMediaInfoParser.isValid(); } public boolean isForceJPGThumbnails() { return (getBoolean(FORCE_JPG_THUMBNAILS, false) && LibMediaInfoParser.isValid()) || isBRAVIA(); } public boolean isShowAudioMetadata() { return getBoolean(SHOW_AUDIO_METADATA, true); } public boolean isShowSubMetadata() { return getBoolean(SHOW_SUB_METADATA, true); } public boolean isDLNATreeHack() { return getBoolean(DLNA_TREE_HACK, false) && LibMediaInfoParser.isValid(); } /** * Returns whether or not to omit sending a content length header when the * length is unknown, as defined in the renderer configuration. Default * value is false. * <p> * Some renderers are particular about the "Content-Length" headers in * requests (e.g. Sony blu-ray players). By default, PMS will send a * "Content-Length" that refers to the total media size, even if the exact * length is unknown. * * @return True if sending the content length header should be omitted. */ public boolean isChunkedTransfer() { return getBoolean(CHUNKED_TRANSFER, false); } /** * Returns whether or not the renderer can handle the given format * natively, based on its configuration in the renderer.conf. If it can * handle a format natively, content can be streamed to the renderer. If * not, content should be transcoded before sending it to the renderer. * * @param mediainfo The {@link DLNAMediaInfo} information parsed from the * media file. * @param format The {@link Format} to test compatibility for. * @return True if the renderer natively supports the format, false * otherwise. */ public boolean isCompatible(DLNAMediaInfo mediainfo, Format format) { // Use the configured "Supported" lines in the renderer.conf // to see if any of them match the MediaInfo library if (isMediaParserV2() && mediainfo != null && getFormatConfiguration().match(mediainfo) != null) { return true; } if (format != null) { String noTranscode = ""; if (PMS.getConfiguration() != null) { noTranscode = PMS.getConfiguration().getDisableTranscodeForExtensions(); } // Is the format among the ones to be streamed? return format.skip(noTranscode, getStreamedExtensions()); } else { // Not natively supported. return false; } } public String getCustomFFmpegOptions() { return getString(CUSTOM_FFMPEG_OPTIONS, ""); } /** * Some renderers like Panasonic TV internally rescale the video. * This option forces MEncoder and FFmpeg to pad video with black borders to 16:9 AR. * * @return True if MEncoder and FFmpeg should pad video with black borders. Default value is false. */ public boolean isPadVideoWithBlackBordersTo169AR() { return getBoolean(KEEP_PAD_VIDEO_WITH_BLACK_BORDERS, false); } /** * Normally the renderer is responsible for video rescaling to output (TV) resolution. * If this option is false then PMS will do rescaling. * It can make better video quality but the CPU and bandwidth are more utilised and can produce jerking video on WiFi. * * Works only for FFmpeg and together with PadVideoWithBlackBordersTo169AR. * * @return True if the renderer is capable of rescaling video. Default value is true. */ public boolean isRescaleByRenderer() { return getBoolean(RESCALE_BY_RENDERER, true); } public String getFFmpegVideoFilterOverride() { return getString(OVERRIDE_VF, null); } /** * Reset gathered information on IP address associations with renderers. */ protected static void resetAddressAssociation() { addressAssociation = new HashMap<InetAddress, RendererConfiguration>(); } }