package net.pms.configuration;
import com.sun.jna.Platform;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.net.InetAddress;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.pms.Messages;
import net.pms.PMS;
import net.pms.dlna.*;
import net.pms.encoders.Player;
import net.pms.formats.Format;
import net.pms.formats.Format.Identifier;
import net.pms.formats.v2.AudioProperties;
import net.pms.io.OutputParams;
import net.pms.network.HTTPResource;
import net.pms.network.SpeedStats;
import net.pms.network.UPNPHelper;
import net.pms.newgui.StatusTab;
import net.pms.util.BasicPlayer;
import net.pms.util.FileWatcher;
import net.pms.util.FormattableColor;
import net.pms.util.InvalidArgumentException;
import net.pms.util.PropertiesUtil;
import net.pms.util.StringUtil;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.WordUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.text.translate.UnicodeUnescaper;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RendererConfiguration extends UPNPHelper.Renderer {
private static final Logger LOGGER = LoggerFactory.getLogger(RendererConfiguration.class);
protected static TreeSet<RendererConfiguration> enabledRendererConfs;
protected static final ArrayList<String> allRenderersNames = new ArrayList<>();
protected static PmsConfiguration _pmsConfiguration = PMS.getConfiguration();
protected static RendererConfiguration defaultConf;
protected static final Map<InetAddress, RendererConfiguration> addressAssociation = new HashMap<>();
protected RootFolder rootFolder;
protected File file;
protected Configuration configuration;
protected PmsConfiguration pmsConfiguration = _pmsConfiguration;
protected ConfigurationReader configurationReader;
protected FormatConfiguration formatConfiguration;
protected int rank;
protected Matcher sortedHeaderMatcher;
protected List<String> identifiers = null;
public StatusTab.RendererItem gui;
public boolean loaded, fileless = false;
protected BasicPlayer player;
public static final File NOFILE = new File("NOFILE");
public interface OutputOverride {
/**
* Override a player's default output formatting.
* To be invoked by the player after input and filter options are complete.
*
* @param cmdList the command so far
* @param dlna the media item
* @param player the player
* @param params the output parameters
*
* @return whether the options have been finalized
*/
public boolean getOutputOptions(List<String> cmdList, DLNAResource dlna, Player player, OutputParams params);
public boolean addSubtitles();
}
// Holds MIME type aliases
protected Map<String, String> mimes;
protected Map<String, String> charMap;
protected Map<String, String> DLNAPN;
// TextWrap parameters
protected int lineWidth, lineHeight, indent;
protected String inset, dots;
// property values
protected static final String LPCM = "LPCM";
protected static final String MP3 = "MP3";
protected static final String WAV = "WAV";
protected static final String WMV = "WMV";
// Old video transcoding options
@Deprecated
protected static final String DEPRECATED_MPEGAC3 = "MPEGAC3";
@Deprecated
protected static final String DEPRECATED_MPEGPSAC3 = "MPEGPSAC3";
@Deprecated
protected static final String DEPRECATED_MPEGTSAC3 = "MPEGTSAC3";
@Deprecated
protected static final String DEPRECATED_H264TSAC3 = "H264TSAC3";
// Current video transcoding options
protected static final String MPEGTSH264AAC = "MPEGTS-H264-AAC";
protected static final String MPEGTSH264AC3 = "MPEGTS-H264-AC3";
protected static final String MPEGTSH265AAC = "MPEGTS-H265-AAC";
protected static final String MPEGTSH265AC3 = "MPEGTS-H265-AC3";
protected static final String MPEGPSMPEG2AC3 = "MPEGPS-MPEG2-AC3";
protected static final String MPEGTSMPEG2AC3 = "MPEGTS-MPEG2-AC3";
// property names
protected static final String ACCURATE_DLNA_ORGPN = "AccurateDLNAOrgPN";
protected static final String AUDIO = "Audio";
protected static final String AUTO_PLAY_TMO = "AutoPlayTmo";
protected static final String BYTE_TO_TIMESEEK_REWIND_SECONDS = "ByteToTimeseekRewindSeconds"; // Ditlew
protected static final String CBR_VIDEO_BITRATE = "CBRVideoBitrate"; // Ditlew
protected static final String CHARMAP = "CharMap";
protected static final String CHUNKED_TRANSFER = "ChunkedTransfer";
protected static final String CUSTOM_FFMPEG_OPTIONS = "CustomFFmpegOptions";
protected static final String CUSTOM_MENCODER_OPTIONS = "CustomMencoderOptions";
protected static final String CUSTOM_MENCODER_MPEG2_OPTIONS = "CustomMencoderQualitySettings"; // TODO (breaking change): value should be CustomMEncoderMPEG2Options
protected static final String DEFAULT_VBV_BUFSIZE = "DefaultVBVBufSize";
protected static final String DEVICE_ID = "Device";
protected static final String DISABLE_MENCODER_NOSKIP = "DisableMencoderNoskip";
protected static final String DLNA_LOCALIZATION_REQUIRED = "DLNALocalizationRequired";
protected static final String DLNA_ORGPN_USE = "DLNAOrgPN";
protected static final String DLNA_PN_CHANGES = "DLNAProfileChanges";
protected static final String DLNA_TREE_HACK = "CreateDLNATreeFaster";
protected static final String EMBEDDED_SUBS_SUPPORTED = "InternalSubtitlesSupported";
protected static final String HALVE_BITRATE = "HalveBitrate";
protected static final String H264_L41_LIMITED = "H264Level41Limited";
protected static final String IGNORE_TRANSCODE_BYTE_RANGE_REQUEST = "IgnoreTranscodeByteRangeRequests";
protected static final String IMAGE = "Image";
protected static final String KEEP_ASPECT_RATIO = "KeepAspectRatio";
protected static final String KEEP_ASPECT_RATIO_TRANSCODING = "KeepAspectRatioTranscoding";
protected static final String LIMIT_FOLDERS = "LimitFolders";
protected static final String LOADING_PRIORITY = "LoadingPriority";
protected static final String MAX_VIDEO_BITRATE = "MaxVideoBitrateMbps";
protected static final String MAX_VIDEO_HEIGHT = "MaxVideoHeight";
protected static final String MAX_VIDEO_WIDTH = "MaxVideoWidth";
protected static final String MAX_VOLUME = "MaxVolume";
protected static final String MEDIAPARSERV2 = "MediaInfo";
protected static final String MEDIAPARSERV2_THUMB = "MediaParserV2_ThumbnailGeneration";
protected static final String MIME_TYPES_CHANGES = "MimeTypesChanges";
protected static final String MUX_DTS_TO_MPEG = "MuxDTSToMpeg";
protected static final String MUX_H264_WITH_MPEGTS = "MuxH264ToMpegTS";
protected static final String MUX_LPCM_TO_MPEG = "MuxLPCMToMpeg";
protected static final String MUX_NON_MOD4_RESOLUTION = "MuxNonMod4Resolution";
protected static final String NOT_AGGRESSIVE_BROWSING = "NotAggressiveBrowsing";
protected static final String OFFER_SUBTITLES_BY_PROTOCOL_INFO = "OfferSubtitlesByProtocolInfo";
protected static final String OFFER_SUBTITLES_AS_SOURCE = "OfferSubtitlesAsSource";
protected static final String OUTPUT_3D_FORMAT = "Output3DFormat";
protected static final String OVERRIDE_FFMPEG_VF = "OverrideFFmpegVideoFilter";
protected static final String PREPEND_TRACK_NUMBERS = "PrependTrackNumbers";
protected static final String PUSH_METADATA = "PushMetadata";
protected static final String REMOVE_TAGS_FROM_SRT_SUBS = "RemoveTagsFromSRTSubtitles";
protected static final String RENDERER_ICON = "RendererIcon";
protected static final String RENDERER_NAME = "RendererName";
protected static final String RESCALE_BY_RENDERER = "RescaleByRenderer";
protected static final String SEEK_BY_TIME = "SeekByTime";
protected static final String SEND_DATE_METADATA = "SendDateMetadata";
protected static final String SEND_FOLDER_THUMBNAILS = "SendFolderThumbnails";
protected static final String SHOW_AUDIO_METADATA = "ShowAudioMetadata";
protected static final String SHOW_DVD_TITLE_DURATION = "ShowDVDTitleDuration"; // Ditlew
protected static final String SHOW_SUB_METADATA = "ShowSubMetadata";
protected static final String STREAM_EXT = "StreamExtensions";
protected static final String STREAM_SUBS_FOR_TRANSCODED_VIDEO = "StreamSubsForTranscodedVideo";
protected static final String SUBTITLE_HTTP_HEADER = "SubtitleHttpHeader";
protected static final String SUPPORTED = "Supported";
protected static final String SUPPORTED_VIDEO_BIT_DEPTHS = "SupportedVideoBitDepths";
protected static final String SUPPORTED_EXTERNAL_SUBTITLES_FORMATS = "SupportedExternalSubtitlesFormats";
protected static final String SUPPORTED_INTERNAL_SUBTITLES_FORMATS = "SupportedInternalSubtitlesFormats";
protected static final String TEXTWRAP = "TextWrap";
protected static final String THUMBNAIL_HEIGHT = "ThumbnailHeight";
protected static final String THUMBNAIL_WIDTH = "ThumbnailWidth";
protected static final String THUMBNAIL_PADDING = "ThumbnailPadding";
protected static final String THUMBNAILS = "Thumbnails";
protected static final String TRANSCODE_AUDIO = "TranscodeAudio";
protected static final String TRANSCODE_AUDIO_441KHZ = "TranscodeAudioTo441kHz";
protected static final String TRANSCODE_EXT = "TranscodeExtensions";
protected static final String TRANSCODE_FAST_START = "TranscodeFastStart";
protected static final String TRANSCODE_VIDEO = "TranscodeVideo";
protected static final String TRANSCODED_SIZE = "TranscodedVideoFileSize";
protected static final String TRANSCODED_VIDEO_AUDIO_SAMPLE_RATE = "TranscodedVideoAudioSampleRate";
protected static final String UPNP_DETAILS = "UpnpDetailsSearch";
protected static final String UPNP_ALLOW = "UpnpAllow";
protected static final String USER_AGENT = "UserAgentSearch";
protected static final String USER_AGENT_ADDITIONAL_HEADER = "UserAgentAdditionalHeader";
protected static final String USER_AGENT_ADDITIONAL_SEARCH = "UserAgentAdditionalHeaderSearch";
protected static final String USE_CLOSED_CAPTION = "UseClosedCaption";
protected static final String USE_SAME_EXTENSION = "UseSameExtension";
protected static final String VIDEO = "Video";
protected static final String VIDEO_FORMATS_SUPPORTING_STREAMED_EXTERNAL_SUBTITLES = "VideoFormatsSupportingStreamedExternalSubtitles";
protected static final String WRAP_DTS_INTO_PCM = "WrapDTSIntoPCM";
protected static final String WRAP_ENCODED_AUDIO_INTO_PCM = "WrapEncodedAudioIntoPCM";
// Deprecated property names
@Deprecated
protected static final String THUMBNAIL_BG = "ThumbnailBackground";
@Deprecated
protected static final String THUMBNAIL_SIZE = "ThumbnailSize";
private static int maximumBitrateTotal = 0;
public static final String UNKNOWN_ICON = "unknown.png";
public static RendererConfiguration getDefaultConf() {
return defaultConf;
}
public ConfigurationReader getConfigurationReader() {
return configurationReader;
}
/**
* {@link #enabledRendererConfs} doesn't normally need locking since
* modification is rare and {@link #loadRendererConfigurations(PmsConfiguration)}
* is only called during {@link PMS#init()} (To avoid any chance of a
* race condition proper locking should be implemented though). During
* build on the other hand the method is called repeatedly and it is random
* if a {@link ConcurrentModificationException} is thrown as a result.
*
* To avoid build problems, this is used to make sure that calls to
* {@link #loadRendererConfigurations(PmsConfiguration)} is serialized.
*/
public static final Object loadRendererConfigurationsLock = new Object();
/**
* Load all renderer configuration files and set up the default renderer.
*
* @param pmsConf
*/
public static void loadRendererConfigurations(PmsConfiguration pmsConf) {
synchronized(loadRendererConfigurationsLock) {
_pmsConfiguration = pmsConf;
enabledRendererConfs = new TreeSet<>(rendererLoadingPriorityComparator);
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;
List<String> selectedRenderers = pmsConf.getSelectedRenderers();
for (File f : confs) {
if (f.getName().endsWith(".conf")) {
try {
RendererConfiguration r = new RendererConfiguration(f);
r.rank = rank++;
String rendererName = r.getConfName();
allRenderersNames.add(rendererName);
String renderersGroup = null;
if (rendererName.indexOf(' ') > 0) {
renderersGroup = rendererName.substring(0, rendererName.indexOf(' '));
}
if (selectedRenderers.contains(rendererName) || selectedRenderers.contains(renderersGroup) || selectedRenderers.contains(pmsConf.ALL_RENDERERS)) {
enabledRendererConfs.add(r);
} else {
LOGGER.debug("Ignored \"{}\" configuration", rendererName);
}
} catch (ConfigurationException ce) {
LOGGER.info("Error in loading configuration of: " + f.getAbsolutePath());
}
}
}
}
}
LOGGER.info("Enabled " + enabledRendererConfs.size() + " configurations, listed in order of loading priority:");
for (RendererConfiguration r : enabledRendererConfs) {
LOGGER.info(": " + r);
}
if (enabledRendererConfs.size() > 0) {
// See if a different default configuration was configured
String rendererFallback = pmsConf.getRendererDefault();
if (StringUtils.isNotBlank(rendererFallback)) {
RendererConfiguration fallbackConf = getRendererConfigurationByName(rendererFallback);
if (fallbackConf != null) {
// A valid fallback configuration was set, use it as default.
defaultConf = fallbackConf;
}
}
}
Collections.sort(allRenderersNames, String.CASE_INSENSITIVE_ORDER);
DeviceConfiguration.loadDeviceConfigurations(pmsConf);
}
public int getInt(String key, int def) {
return configurationReader.getInt(key, def);
}
public long getLong(String key, long def) {
return configurationReader.getLong(key, def);
}
public double getDouble(String key, double def) {
return configurationReader.getDouble(key, def);
}
public boolean getBoolean(String key, boolean def) {
return configurationReader.getBoolean(key, def);
}
public String getString(String key, String def) {
return configurationReader.getNonBlankConfigurationString(key, def);
}
public List<String> getStringList(String key, String def) {
List<String> result = configurationReader.getStringList(key, def);
if (result.size() == 1 && result.get(0).equalsIgnoreCase("None")) {
return new ArrayList<>();
} else {
return result;
}
}
public void setStringList(String key, List<String> value) {
StringBuilder result = new StringBuilder();
for (String element : value) {
if (!result.toString().equals("")) {
result.append(", ");
}
result.append(element);
}
if (result.toString().equals("")) {
result.append("None");
}
configuration.setProperty(key, result.toString());
}
public Color getColor(String key, String defaultValue) {
String colorString = getString(key, defaultValue);
if (!StringUtils.isBlank(colorString)) {
try {
return new FormattableColor(colorString);
} catch (InvalidArgumentException e) {
LOGGER.error(e.getMessage());
LOGGER.trace("", e);
}
}
if (StringUtils.isBlank(defaultValue)) {
return null;
}
try {
return new FormattableColor(defaultValue);
} catch (InvalidArgumentException e) {
LOGGER.error("Invalid default value: {}", e.getMessage());
LOGGER.trace("", e);
return null;
}
}
@Deprecated
public static ArrayList<RendererConfiguration> getAllRendererConfigurations() {
return getEnabledRenderersConfigurations();
}
public boolean nox264() {
return false;
}
/**
* Returns the list of enabled renderer configurations.
*
* @return The list of enabled renderers.
*/
public static ArrayList<RendererConfiguration> getEnabledRenderersConfigurations() {
return enabledRendererConfs != null ? new ArrayList(enabledRendererConfs) : null;
}
/**
* Returns the list of all connected renderer devices.
*
* @return The list of connected renderers.
*/
public static Collection<RendererConfiguration> getConnectedRenderersConfigurations() {
// We need to check both UPnP and http sides to ensure a complete list
HashSet<RendererConfiguration> renderers = new HashSet<>(UPNPHelper.getRenderers(UPNPHelper.ANY));
renderers.addAll(addressAssociation.values());
// Ensure any remaining secondary common-ip renderers (which are no longer in address association) are added
renderers.addAll(PMS.get().getFoundRenderers());
return renderers;
}
public static boolean hasConnectedAVTransportPlayers() {
return UPNPHelper.hasRenderer(UPNPHelper.AVT);
}
public static List<RendererConfiguration> getConnectedAVTransportPlayers() {
return UPNPHelper.getRenderers(UPNPHelper.AVT);
}
public static boolean hasConnectedRenderer(int type) {
for (RendererConfiguration r : getConnectedRenderersConfigurations()) {
if ((r.controls & type) != 0) {
return true;
}
}
return false;
}
public static List<RendererConfiguration> getConnectedRenderers(int type) {
ArrayList<RendererConfiguration> renderers = new ArrayList<>();
for (RendererConfiguration r : getConnectedRenderersConfigurations()) {
if (r.active && (r.controls & type) != 0) {
renderers.add(r);
}
}
return renderers;
}
public static boolean hasConnectedControlPlayers() {
return hasConnectedRenderer(UPNPHelper.ANY);
}
public static List<RendererConfiguration> getConnectedControlPlayers() {
return getConnectedRenderers(UPNPHelper.ANY);
}
/**
* Searches for an instance of this renderer connected at the given address.
*
* @param r the renderer.
* @param ia the address.
* @return the matching renderer or null.
*/
public static RendererConfiguration find(RendererConfiguration r, InetAddress ia) {
return find(r.getConfName(), ia);
}
/**
* Searches for a renderer of this name connected at the given address.
*
* @param name the renderer name.
* @param ia the address.
* @return the matching renderer or null.
*/
public static RendererConfiguration find(String name, InetAddress ia) {
for (RendererConfiguration r : getConnectedRenderersConfigurations()) {
if (ia.equals(r.getAddress()) && name.equals(r.getConfName())) {
return r;
}
}
return null;
}
public 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 r : getConnectedRenderersConfigurations()) {
r.rootFolder = null;
}
// Resetting enabledRendererConfs isn't strictly speaking necessary any more, since
// these are now for reference only and never actually populate their root folders.
for (RendererConfiguration r : enabledRendererConfs) {
r.rootFolder = null;
}
}
public RootFolder getRootFolder() {
if (rootFolder == null) {
ArrayList<String> tags = new ArrayList<>();
tags.add(getRendererName());
for (InetAddress sa : addressAssociation.keySet()) {
if (addressAssociation.get(sa) == this) {
tags.add(sa.getHostAddress());
}
}
rootFolder = new RootFolder(tags);
if (pmsConfiguration.getUseCache()) {
rootFolder.discoverChildren();
}
}
return rootFolder;
}
public void addFolderLimit(DLNAResource res) {
if (rootFolder != null) {
rootFolder.setFolderLim(res);
}
}
public void setRootFolder(RootFolder r) {
rootFolder = r;
}
/**
* 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.
* @return whether the device at this address is a renderer.
* @see #getRendererConfigurationBySocketAddress(InetAddress)
*/
public boolean associateIP(InetAddress sa) {
if (UPNPHelper.isNonRenderer(sa)) {
// TODO: remove it if already added unknowingly
return false;
}
// FIXME: handle multiple clients with same ip properly, now newer overwrites older
RendererConfiguration prev = addressAssociation.put(sa, this);
if (prev != null) {
// We've displaced a previous renderer at this address, so
// check if it's a ghost instance that should be deleted.
verify(prev);
}
resetUpnpMode();
if (
(
pmsConfiguration.isAutomaticMaximumBitrate() ||
pmsConfiguration.isSpeedDbg()
) &&
!(
sa.isLoopbackAddress() ||
sa.isAnyLocalAddress()
)
) {
SpeedStats.getInstance().getSpeedInMBits(sa, getRendererName());
}
return true;
}
public static void calculateAllSpeeds() {
for (InetAddress sa : addressAssociation.keySet()) {
if (sa.isLoopbackAddress() || sa.isAnyLocalAddress()) {
continue;
}
RendererConfiguration r = addressAssociation.get(sa);
if (!r.isOffline()) {
SpeedStats.getInstance().getSpeedInMBits(sa, r.getRendererName());
}
}
}
public static RendererConfiguration getRendererConfigurationBySocketAddress(InetAddress sa) {
RendererConfiguration r = addressAssociation.get(sa);
if (r != null) {
LOGGER.trace("Matched media renderer \"{}\" based on address {}", r.getRendererName(), sa.getHostAddress());
}
return r;
}
/**
* Tries to find a matching renderer configuration based on the given collection of
* request headers
*
* @param headers The headers.
* @param ia The request's origin address.
* @return The matching renderer configuration or <code>null</code>
*/
public static RendererConfiguration getRendererConfigurationByHeaders(Collection<Map.Entry<String, String>> headers, InetAddress ia) {
return getRendererConfigurationByHeaders(new SortedHeaderMap(headers), ia);
}
public static RendererConfiguration getRendererConfigurationByHeaders(SortedHeaderMap sortedHeaders, InetAddress ia) {
RendererConfiguration r = null;
RendererConfiguration ref = getRendererConfigurationByHeaders(sortedHeaders);
if (ref != null) {
boolean isNew = !addressAssociation.containsKey(ia);
r = resolve(ia, ref);
if (r != null) {
LOGGER.trace(
"Matched {}media renderer \"{}\" based on headers {}",
isNew ? "new " : "",
r.getRendererName(),
sortedHeaders
);
}
}
return r;
}
public static RendererConfiguration getRendererConfigurationByHeaders(SortedHeaderMap sortedHeaders) {
if (_pmsConfiguration.isRendererForceDefault()) {
// Force default renderer
LOGGER.debug("Forcing renderer match to \"" + defaultConf.getRendererName() + "\"");
return defaultConf;
}
for (RendererConfiguration r : enabledRendererConfs) {
if (r.match(sortedHeaders)) {
LOGGER.debug("Matched media renderer \"" + r.getRendererName() + "\" based on headers " + sortedHeaders);
return 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 : enabledRendererConfs) {
if (conf.getConfName().toLowerCase().contains(name.toLowerCase())) {
return conf;
}
}
return null;
}
public static RendererConfiguration getRendererConfigurationByUUID(String uuid) {
for (RendererConfiguration conf : getConnectedRenderersConfigurations()) {
if (uuid.equals(conf.getUUID())) {
return conf;
}
}
return null;
}
public static RendererConfiguration getRendererConfigurationByUPNPDetails(String details) {
for (RendererConfiguration r : enabledRendererConfs) {
if (r.matchUPNPDetails(details)) {
LOGGER.debug("Matched media renderer \"" + r.getRendererName() + "\" based on dlna details \"" + details + "\"");
return r;
}
}
return null;
}
public static RendererConfiguration resolve(InetAddress ia, RendererConfiguration ref) {
DeviceConfiguration r = null;
boolean recognized = ref != null;
if (!recognized) {
ref = getDefaultConf();
}
try {
if (addressAssociation.containsKey(ia)) {
// Already seen, finish configuration if required
r = (DeviceConfiguration) addressAssociation.get(ia);
boolean higher = ref != null && ref.getLoadingPriority() > r.getLoadingPriority() && recognized;
if (!r.loaded || higher) {
LOGGER.debug("Finishing configuration for {}", r);
if (higher) {
LOGGER.debug("Switching to higher priority renderer: {}", ref);
}
r.inherit(ref);
// update gui
PMS.get().updateRenderer(r);
}
} else if (!UPNPHelper.isNonRenderer(ia)) {
// It's brand new
r = new DeviceConfiguration(ref, ia);
if (r.associateIP(ia)) {
PMS.get().setRendererFound(r);
}
r.active = true;
if (r.isUpnpPostponed()) {
r.setUpnpMode(ALLOW);
}
}
} catch (ConfigurationException e) {
LOGGER.error("Configuration error while resolving renderer: {}", e.getMessage());
LOGGER.trace("", e);
}
if (!recognized) {
// Mark it as unloaded so actual recognition can happen later if UPnP sees it.
LOGGER.trace("Marking renderer \"{}\" at {} as unrecognized", r, ia.getHostAddress());
if (r != null) {
r.loaded = false;
}
}
return r;
}
public FormatConfiguration getFormatConfiguration() {
return formatConfiguration;
}
public Configuration getConfiguration() {
return configuration;
}
public PmsConfiguration getPmsConfiguration() {
return pmsConfiguration;
}
public File getFile() {
return file;
}
public String getId() {
return uuid != null ? uuid : getAddress().toString().substring(1);
}
public static String getSimpleName(RendererConfiguration r) {
return StringUtils.substringBefore(r.getRendererName(), "(").trim();
}
public static String getDefaultFilename(RendererConfiguration r) {
String id = r.getId();
return (getSimpleName(r) + "-" + (id.startsWith("uuid:") ? id.substring(5, 11) : id)).replace(" ", "") + ".conf";
}
public File getUsableFile() {
File f = getFile();
if (f == null || f.equals(NOFILE)) {
String name = getSimpleName(this);
f = new File(getRenderersDir(), name.equals(getSimpleName(defaultConf)) ? getDefaultFilename(this) : (name.replace(" ", "") + ".conf"));
}
return f;
}
public static void createNewFile(RendererConfiguration r, File file, boolean load, File ref) {
try {
ArrayList<String> conf = new ArrayList<>();
String name = getSimpleName(r);
Map<String, String> details = r.getUpnpDetails();
List<String> headers = r.getIdentifiers();
boolean hasRef = ref != null && ref != NOFILE;
// Add the header and identifiers
conf.add("#----------------------------------------------------------------------------");
conf.add("# Auto-generated profile for " + name);
conf.add("#" + (hasRef ? " Based on " + ref.getName() : ""));
conf.add("# See DefaultRenderer.conf for a description of all possible configuration options.");
conf.add("#");
conf.add("");
conf.add(RENDERER_NAME + " = " + name);
if (headers != null || details != null) {
conf.add("");
conf.add("# ============================================================================");
conf.add("# This renderer has sent the following string/s:");
if (headers != null && headers.size() > 0) {
conf.add("#");
for (String h : headers) {
conf.add("# " + h);
}
}
if (details != null) {
details.remove("address");
details.remove("udn");
conf.add("#");
conf.add("# " + details);
}
conf.add("# ============================================================================");
conf.add("");
}
conf.add(USER_AGENT + " = ");
if (headers != null && headers.size() > 1) {
conf.add(USER_AGENT_ADDITIONAL_HEADER + " = ");
conf.add(USER_AGENT_ADDITIONAL_SEARCH + " = ");
}
if (details != null) {
conf.add(UPNP_DETAILS + " = " + details.get("manufacturer") + " , " + details.get("modelName"));
}
conf.add("");
// TODO: Set more properties automatically from UPNP info
if (hasRef) {
// Copy the reference file, skipping its header and identifiers
Matcher skip = Pattern.compile(".*(" + RENDERER_ICON + "|" + RENDERER_NAME + "|" +
UPNP_DETAILS + "|" + USER_AGENT + "|" + USER_AGENT_ADDITIONAL_HEADER + "|" +
USER_AGENT_ADDITIONAL_SEARCH + ").*").matcher("");
boolean header = true;
for (String line : FileUtils.readLines(ref, Charsets.UTF_8)) {
if (
skip.reset(line).matches() ||
(
header &&
(
line.startsWith("#") ||
StringUtils.isBlank(line)
)
)
) {
continue;
}
header = false;
conf.add(line);
}
}
FileUtils.writeLines(file, "utf-8", conf, "\r\n");
if (load) {
try {
RendererConfiguration renderer = new RendererConfiguration(file);
enabledRendererConfs.add(renderer);
if (r instanceof DeviceConfiguration) {
((DeviceConfiguration)r).inherit(renderer);
}
} catch (ConfigurationException ce) {
LOGGER.debug("Error initializing renderer configuration: " + ce);
}
}
} catch (IOException ie) {
LOGGER.debug("Error creating renderer configuration file: " + ie);
}
}
public boolean isFileless() {
return fileless;
}
public void setFileless(boolean b) {
fileless = b;
}
public int getRank() {
return rank;
}
public int getThumbnailWidth() {
return getInt(THUMBNAIL_WIDTH, 320);
}
public int getThumbnailHeight() {
return getInt(THUMBNAIL_HEIGHT, 180);
}
/**
* @return the desired aspect ratio for thumbnails to two decimal places
*/
// TODO: Cache this
public double getThumbnailRatio() {
return Math.round(((double) getThumbnailWidth() / getThumbnailHeight()) * 100.0) / 100.0;
}
/**
* @see #isXbox360()
* @deprecated
*/
@Deprecated
public boolean isXBOX() {
return isXbox360();
}
/**
* @return whether this renderer is an Xbox 360
*/
public boolean isXbox360() {
return getConfName().toUpperCase().contains("XBOX 360");
}
/**
* @return whether this renderer is an Xbox One
*/
public boolean isXboxOne() {
return getConfName().toUpperCase().contains("XBOX ONE");
}
public boolean isXBMC() {
return getConfName().toUpperCase().contains("KODI") || getConfName().toUpperCase().contains("XBMC");
}
public boolean isPS3() {
return getConfName().toUpperCase().contains("PLAYSTATION 3") || getConfName().toUpperCase().contains("PS3");
}
public boolean isPS4() {
return getConfName().toUpperCase().contains("PLAYSTATION 4");
}
public boolean isBRAVIA() {
return getConfName().toUpperCase().contains("BRAVIA");
}
public boolean isFDSSDP() {
return getConfName().toUpperCase().contains("FDSSDP");
}
public boolean isLG() {
return getConfName().toUpperCase().contains("LG ");
}
// 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);
}
public RendererConfiguration() throws ConfigurationException {
this(null, null);
}
public RendererConfiguration(String uuid) throws ConfigurationException {
this(null, uuid);
}
public RendererConfiguration(int ignored) {
// Just instantiate minimally, full initialization will happen later
configuration = createPropertiesConfiguration();
configurationReader = new ConfigurationReader(configuration, true); // true: log
}
static UnicodeUnescaper unicodeUnescaper = new UnicodeUnescaper();
public RendererConfiguration(File f) throws ConfigurationException {
this(f, null);
}
public RendererConfiguration(File f, String uuid) throws ConfigurationException {
super(uuid);
configuration = createPropertiesConfiguration();
// false: don't log overrides (every renderer conf
// overrides multiple settings)
configurationReader = new ConfigurationReader(configuration, false);
pmsConfiguration = _pmsConfiguration;
player = null;
buffer = 0;
init(f);
}
static StringUtil.LaxUnicodeUnescaper laxUnicodeUnescaper = new StringUtil.LaxUnicodeUnescaper();
public static PropertiesConfiguration createPropertiesConfiguration() {
PropertiesConfiguration conf = new PropertiesConfiguration();
conf.setListDelimiter((char) 0);
// Treat backslashes in the conf as literal while also supporting double-backslash syntax, i.e.
// ensure that typical raw regex strings (and unescaped Windows file paths) are read correctly.
conf.setIOFactory(new PropertiesConfiguration.DefaultIOFactory() {
@Override
public PropertiesConfiguration.PropertiesReader createPropertiesReader(final Reader in, final char delimiter) {
return new PropertiesConfiguration.PropertiesReader(in, delimiter) {
@Override
protected void parseProperty(final String line) {
// Decode any backslashed unicode escapes, e.g. '\u005c', from the
// ISO 8859-1 (aka Latin 1) encoded java Properties file, then
// unescape any double-backslashes, then escape all backslashes before parsing
super.parseProperty(laxUnicodeUnescaper.translate(line).replace("\\\\", "\\").replace("\\", "\\\\"));
}
};
}
});
return conf;
}
public boolean load(File f) throws ConfigurationException {
if (f != null && !f.equals(NOFILE) && (configuration instanceof PropertiesConfiguration)) {
((PropertiesConfiguration) configuration).load(f);
// Set up the header matcher
SortedHeaderMap searchMap = new SortedHeaderMap();
searchMap.put("User-Agent", getUserAgent());
searchMap.put(getUserAgentAdditionalHttpHeader(), getUserAgentAdditionalHttpHeaderSearch());
String re = searchMap.toRegex();
sortedHeaderMatcher = StringUtils.isNotBlank(re) ? Pattern.compile(re, Pattern.CASE_INSENSITIVE).matcher("") : null;
boolean addWatch = file != f;
file = f;
if (addWatch) {
PMS.getFileWatcher().add(new FileWatcher.Watch(getFile().getPath(), reloader, this));
}
return true;
}
return false;
}
public void init(File f) throws ConfigurationException {
rootFolder = null;
if (!loaded) {
configuration.clear();
loaded = load(f);
}
if (isUpnpAllowed() && uuid == null) {
String id = getDeviceId();
if (StringUtils.isNotBlank(id) && !id.contains(",")) {
uuid = id;
}
}
mimes = new HashMap<>();
String mimeTypes = getString(MIME_TYPES_CHANGES, "");
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);
}
}
}
String s = getString(TEXTWRAP, "").toLowerCase();
lineWidth = getIntAt(s, "width:", 0);
if (lineWidth > 0) {
lineHeight = getIntAt(s, "height:", 0);
indent = getIntAt(s, "indent:", 0);
int whitespace = getIntAt(s, "whitespace:", 9);
int dotCount = getIntAt(s, "dots:", 0);
inset = StringUtil.fillString(whitespace, indent);
dots = StringUtil.fillString(".", dotCount);
}
charMap = new HashMap<>();
String ch = getString(CHARMAP, null);
if (StringUtils.isNotBlank(ch)) {
StringTokenizer st = new StringTokenizer(ch, " ");
String org = "";
while (st.hasMoreTokens()) {
String tok = st.nextToken().trim();
if (StringUtils.isBlank(tok)) {
continue;
}
tok = tok.replaceAll("###0", " ").replaceAll("###n", "\n").replaceAll("###r", "\r");
if (StringUtils.isBlank(org)) {
org = tok;
} else {
charMap.put(org, tok);
org = "";
}
}
}
DLNAPN = new HashMap<>();
String DLNAPNchanges = getString(DLNA_PN_CHANGES, "");
if (StringUtils.isNotBlank(DLNAPNchanges)) {
LOGGER.trace("Config DLNAPNchanges: " + 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 (isUseMediaInfo()) {
formatConfiguration = new FormatConfiguration(configuration.getList(SUPPORTED));
}
}
public void reset() {
File f = getFile();
try {
LOGGER.info("Reloading renderer configuration: {}", f);
loaded = false;
init(f);
// update gui
for (RendererConfiguration d : DeviceConfiguration.getInheritors(this)) {
PMS.get().updateRenderer(d);
}
} catch (Exception e) {
LOGGER.debug("Error reloading renderer configuration {}: {}", f, e);
e.printStackTrace();
}
}
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 isTranscodeToMPEGPSMPEG2AC3() {
String videoTranscode = getVideoTranscode();
return videoTranscode.equals(MPEGPSMPEG2AC3) || videoTranscode.equals(DEPRECATED_MPEGAC3) || videoTranscode.equals(DEPRECATED_MPEGPSAC3);
}
public boolean isTranscodeToMPEGTSMPEG2AC3() {
String videoTranscode = getVideoTranscode();
return videoTranscode.equals(MPEGTSMPEG2AC3) || videoTranscode.equals(DEPRECATED_MPEGTSAC3);
}
public boolean isTranscodeToMPEGTSH264AC3() {
String videoTranscode = getVideoTranscode();
return videoTranscode.equals(MPEGTSH264AC3) || videoTranscode.equals(DEPRECATED_H264TSAC3);
}
public boolean isTranscodeToMPEGTSH264AAC() {
return getVideoTranscode().equals(MPEGTSH264AAC);
}
public boolean isTranscodeToMPEGTSH265AAC() {
return getVideoTranscode().equals(MPEGTSH265AAC);
}
public boolean isTranscodeToMPEGTSH265AC3() {
return getVideoTranscode().equals(MPEGTSH265AC3);
}
/**
* @return whether to use the AC-3 audio codec for transcoded video
*/
public boolean isTranscodeToAC3() {
return isTranscodeToMPEGPSMPEG2AC3() || isTranscodeToMPEGTSMPEG2AC3() || isTranscodeToMPEGTSH264AC3() || isTranscodeToMPEGTSH265AC3();
}
/**
* @return whether to use the AAC audio codec for transcoded video
*/
public boolean isTranscodeToAAC() {
return isTranscodeToMPEGTSH264AAC() || isTranscodeToMPEGTSH265AAC();
}
/**
* @return whether to use the H.264 video codec for transcoded video
*/
public boolean isTranscodeToH264() {
return isTranscodeToMPEGTSH264AAC() || isTranscodeToMPEGTSH264AC3();
}
/**
* @return whether to use the H.265 video codec for transcoded video
*/
public boolean isTranscodeToH265() {
return isTranscodeToMPEGTSH265AAC() || isTranscodeToMPEGTSH265AC3();
}
/**
* @return whether to use the MPEG-TS container for transcoded video
*/
public boolean isTranscodeToMPEGTS() {
return isTranscodeToMPEGTSMPEG2AC3() || isTranscodeToMPEGTSH264AC3() || isTranscodeToMPEGTSH264AAC() || isTranscodeToMPEGTSH265AC3() || isTranscodeToMPEGTSH265AAC();
}
/**
* @return whether to use the MPEG-2 video codec for transcoded video
*/
public boolean isTranscodeToMPEG2() {
return isTranscodeToMPEGTSMPEG2AC3() || isTranscodeToMPEGPSMPEG2AC3();
}
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);
}
/**
* @return whether to transcode H.264 video if it exceeds level 4.1
*/
public boolean isH264Level41Limited() {
return getBoolean(H264_L41_LIMITED, true);
}
public boolean isTranscodeFastStart() {
return getBoolean(TRANSCODE_FAST_START, false);
}
public boolean isDLNALocalizationRequired() {
return getBoolean(DLNA_LOCALIZATION_REQUIRED, false);
}
public boolean isDisableMencoderNoskip() {
return getBoolean(DISABLE_MENCODER_NOSKIP, 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 mimeType
* 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, DLNAMediaInfo media) {
if (mimeType == null) {
return null;
}
String matchedMimeType = null;
if (isUseMediaInfo()) {
// Use the supported information in the configuration to determine the transcoding mime type.
if (HTTPResource.VIDEO_TRANSCODE.equals(mimeType)) {
if (isTranscodeToMPEGTSH264AC3()) {
matchedMimeType = getFormatConfiguration().match(FormatConfiguration.MPEGTS, FormatConfiguration.H264, FormatConfiguration.AC3);
} else if (isTranscodeToMPEGTSH264AAC()) {
matchedMimeType = getFormatConfiguration().match(FormatConfiguration.MPEGTS, FormatConfiguration.H264, FormatConfiguration.AAC);
} else if (isTranscodeToMPEGTSH265AC3()) {
matchedMimeType = getFormatConfiguration().match(FormatConfiguration.MPEGTS, FormatConfiguration.H265, FormatConfiguration.AC3);
} else if (isTranscodeToMPEGTSH265AAC()) {
matchedMimeType = getFormatConfiguration().match(FormatConfiguration.MPEGTS, FormatConfiguration.H265, FormatConfiguration.AAC);
} else if (isTranscodeToMPEGTSMPEG2AC3()) {
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 (pmsConfiguration.isAudioResample()) {
if (isTranscodeAudioTo441()) {
matchedMimeType += ";rate=44100;channels=2";
} else {
matchedMimeType += ";rate=48000;channels=2";
}
} else if (media != null && media.getFirstAudioTrack() != null) {
AudioProperties audio = media.getFirstAudioTrack().getAudioProperties();
if (audio.getSampleFrequency() > 0) {
matchedMimeType += ";rate=" + Integer.toString(audio.getSampleFrequency());
}
if (audio.getNumberOfChannels() > 0) {
matchedMimeType += ";channels=" + Integer.toString(audio.getNumberOfChannels());
}
}
}
}
}
}
if (matchedMimeType == null) {
// 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 (pmsConfiguration.isAudioResample()) {
if (isTranscodeAudioTo441()) {
matchedMimeType += ";rate=44100;channels=2";
} else {
matchedMimeType += ";rate=48000;channels=2";
}
} else if (media != null) {
AudioProperties audio = media.getFirstAudioTrack().getAudioProperties();
if (audio.getSampleFrequency() > 0) {
matchedMimeType += ";rate=" + Integer.toString(audio.getSampleFrequency());
}
if (audio.getNumberOfChannels() > 0) {
matchedMimeType += ";channels=" + Integer.toString(audio.getNumberOfChannels());
}
}
}
}
}
if (matchedMimeType == null) {
matchedMimeType = mimeType;
}
// Apply renderer specific mime type aliases
if (mimes.containsKey(matchedMimeType)) {
return mimes.get(matchedMimeType);
}
return matchedMimeType;
}
public boolean matchUPNPDetails(String details) {
String upnpDetails = getUpnpDetailsString();
Pattern pattern;
if (StringUtils.isNotBlank(upnpDetails)) {
String p = StringUtils.join(upnpDetails.split(" , "), ".*");
pattern = Pattern.compile(p, Pattern.CASE_INSENSITIVE);
return pattern.matcher(details.replace("\n", " ")).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, "");
}
/**
* Returns the unique UPnP details of this renderer as defined in the
* renderer configuration. Default value is "".
*
* @return The detail string.
*/
public String getUpnpDetailsString() {
return getString(UPNP_DETAILS, "");
}
/**
* Returns the UPnP details of this renderer as broadcast by itself, if known.
* Default value is null.
*
* @return The detail map.
*/
public Map<String, String> getUpnpDetails() {
return UPNPHelper.getDeviceDetails(UPNPHelper.getDevice(uuid));
}
public boolean isUpnp() {
return uuid != null && UPNPHelper.isUpnpDevice(uuid);
}
public boolean isControllable() {
return controls != 0;
}
public Map<String, String> getDetails() {
if (details == null) {
if (isUpnp()) {
details = UPNPHelper.getDeviceDetails(UPNPHelper.getDevice(uuid));
} else {
details = new LinkedHashMap<String, String>() {
private static final long serialVersionUID = -3998102753945339020L;
{
put(Messages.getString("RendererPanel.10"), getRendererName());
if (getAddress() != null) {
put(Messages.getString("RendererPanel.11"), getAddress().getHostAddress().toString());
}
}
};
}
}
return details;
}
/**
* Returns the current UPnP state variables of this renderer, if known. Default value is null.
*
* @return The data.
*/
public Map<String, String> getUPNPData() {
return UPNPHelper.getData(uuid, instanceID);
}
/**
* Returns the UPnP services of this renderer.
* Default value is null.
*
* @return The list of service names.
*/
public List<String> getUpnpServices() {
return isUpnp() ? UPNPHelper.getServiceNames(UPNPHelper.getDevice(uuid)) : null;
}
/**
* Returns the uuid of this renderer, if known. Default value is null.
*
* @return The uuid.
*/
public String getUUID() {
return uuid;
}
/**
* Sets the uuid of this renderer.
*
* @param uuid The uuid.
*/
public void setUUID(String uuid) {
this.uuid = uuid;
}
/**
* Returns the UPnP instance id of this renderer, if known. Default value is null.
*
* @return The instance id.
*/
public String getInstanceID() {
return instanceID;
}
/**
* Sets the UPnP instance id of this renderer.
*
* @param id The instance id.
*/
public void setInstanceID(String id) {
instanceID = id;
}
/**
* Returns whether this renderer is known to be offline.
*
* @return Whether offline.
*/
public boolean isOffline() {
return !active;
}
/**
* Returns whether this renderer is currently connected via UPnP.
*
* @return Whether connected.
*/
public boolean isUpnpConnected() {
return uuid != null ? UPNPHelper.isActive(uuid, instanceID) : false;
}
/**
* Returns whether this renderer has an associated address.
*
* @return Has address.
*/
public boolean hasAssociatedAddress() {
return addressAssociation.values().contains(this);
}
/**
* Returns this renderer's associated address.
*
* @return The address.
*/
public InetAddress getAddress() {
// If we have a uuid look up the UPnP device address, which is always
// correct even if another device has overwritten our association
if (uuid != null) {
InetAddress address = UPNPHelper.getAddress(uuid);
if (address != null) {
return address;
}
}
// Otherwise check the address association
for (InetAddress sa : addressAssociation.keySet()) {
if (addressAssociation.get(sa) == this) {
return sa;
}
}
return null;
}
/**
* Returns whether this renderer provides UPnP control services.
*
* @return Whether controllable.
*/
public boolean isUpnpControllable() {
return UPNPHelper.isUpnpControllable(uuid);
}
/**
* Returns a UPnP player for this renderer if UPnP control is supported.
*
* @return a player or null.
*/
public BasicPlayer getPlayer() {
if (player == null) {
player = isUpnpControllable() ? new UPNPHelper.Player((DeviceConfiguration) this) :
new PlaybackTimer((DeviceConfiguration) this);
}
return player;
}
/**
* Sets the UPnP player.
*
* @param player
*/
public void setPlayer(UPNPHelper.Player player) {
this.player = player;
}
@Override
public void setActive(boolean b) {
super.setActive(b);
if (gui != null) {
gui.icon.setGrey(!active);
}
}
public void delete(int delay) {
delete(this, delay);
}
public static void delete(final RendererConfiguration r, int delay) {
r.setActive(false);
// Using javax.swing.Timer because of gui (this works in headless mode too).
javax.swing.Timer t = new javax.swing.Timer(delay, new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
// Make sure we haven't been reactivated while asleep
if (! r.isActive()) {
LOGGER.debug("Deleting renderer " + r);
if (r.gui != null) {
r.gui.delete();
}
PMS.get().getFoundRenderers().remove(r);
UPNPHelper.getInstance().removeRenderer(r);
InetAddress ia = r.getAddress();
if (addressAssociation.get(ia) == r) {
addressAssociation.remove(ia);
}
// TODO: actually delete rootfolder, etc.
}
}
});
t.setRepeats(false);
t.start();
}
public void setGuiComponents(StatusTab.RendererItem item) {
gui = item;
}
public StatusTab.RendererItem getGuiComponents() {
return gui;
}
/**
* 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 (details != null && details.containsKey("friendlyName")) ? details.get("friendlyName") :
isUpnp() ? UPNPHelper.getFriendlyName(uuid) :
getConfName();
}
public String getConfName() {
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_ICON.
*
* @return The renderer icon.
*/
public String getRendererIcon() {
String icon = getString(RENDERER_ICON, UNKNOWN_ICON);
String deviceIcon = null;
if (icon.equals(UNKNOWN_ICON)) {
deviceIcon = UPNPHelper.getDeviceIcon(this, 140);
}
return deviceIcon == null ? icon : deviceIcon;
}
/**
* 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 "".
*
* @return The additional HTTP header name.
*/
public String getUserAgentAdditionalHttpHeader() {
return getString(USER_AGENT_ADDITIONAL_HEADER, "");
}
/**
* 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, "");
}
/**
* May append a custom file extension to the file path.
* Returns the original path if the renderer didn't define an extension.
*
* @param file the original file path
* @return
*/
public String getUseSameExtension(String file) {
String extension = getString(USE_SAME_EXTENSION, "");
if (StringUtils.isNotEmpty(extension)) {
file += "." + extension;
}
return file;
}
/**
* 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() || getString(SEEK_BY_TIME, "false").equalsIgnoreCase("true");
}
/**
* 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, "false").equalsIgnoreCase("exclusive");
}
public boolean isMuxH264MpegTS() {
boolean muxCompatible = getBoolean(MUX_H264_WITH_MPEGTS, true);
if (isUseMediaInfo()) {
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 (isUseMediaInfo()) {
return getFormatConfiguration().isDTSSupported();
}
return getBoolean(MUX_DTS_TO_MPEG, false);
}
public boolean isWrapDTSIntoPCM() {
return getBoolean(WRAP_DTS_INTO_PCM, false);
}
public boolean isWrapEncodedAudioIntoPCM() {
return getBoolean(WRAP_ENCODED_AUDIO_INTO_PCM, false);
}
public boolean isLPCMPlayable() {
return isMuxLPCMToMpeg();
}
public boolean isMuxLPCMToMpeg() {
if (isUseMediaInfo()) {
return getFormatConfiguration().isLPCMSupported();
}
return getBoolean(MUX_LPCM_TO_MPEG, true);
}
public boolean isMuxNonMod4Resolution() {
return getBoolean(MUX_NON_MOD4_RESOLUTION, false);
}
public boolean isMpeg2Supported() {
if (isUseMediaInfo()) {
return getFormatConfiguration().isMpeg2Supported();
}
return isPS3();
}
/**
* Returns whether or not to include metadata when pushing uris.
* This is meant as a stopgap workaround for any renderer that
* chokes on our metadata.
*
* @return whether to include metadata.
*/
public boolean isPushMetadata() {
return getBoolean(PUSH_METADATA, true);
}
/**
* Returns the codec to use for video transcoding for this renderer as
* defined in the renderer configuration. Default value is "MPEGPSMPEG2AC3".
*
* @return The codec name.
*/
public String getVideoTranscode() {
return getString(TRANSCODE_VIDEO, MPEGPSMPEG2AC3);
}
/**
* 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 "0" (unlimited).
*
* @return The bitrate.
*/
// TODO this should return an integer and the units should be bits-per-second
public String getMaxVideoBitrate() {
if (PMS.getConfiguration().isAutomaticMaximumBitrate()) {
try {
return calculatedSpeed();
} catch (InterruptedException e) {
return "0";
} catch (ExecutionException e) {
LOGGER.debug("Automatic maximum bitrate calculation failed with: {}", e.getCause().getMessage());
LOGGER.trace("", e.getCause());
}
}
return getString(MAX_VIDEO_BITRATE, "0");
}
/**
* This was originally added for the PS3 after it was observed to need
* a video whose maximum bitrate was under half of the network maximum.
*
* @return whether to set the maximum bitrate to half of the network max
*/
public boolean isHalveBitrate() {
return getBoolean(HALVE_BITRATE, false);
}
/**
* Returns the maximum bitrate (in bits-per-second) as defined by
* whichever is lower out of the renderer setting or user setting.
*
* @return The maximum bitrate in bits-per-second.
*/
public int getMaxBandwidth() {
if (maximumBitrateTotal > 0) {
return maximumBitrateTotal;
}
int defaultMaxBitrates[] = getVideoBitrateConfig(PMS.getConfiguration().getMaximumBitrate());
int rendererMaxBitrates[] = new int[2];
String maxVideoBitrate = getMaxVideoBitrate();
if (StringUtils.isNotEmpty(maxVideoBitrate)) {
rendererMaxBitrates = getVideoBitrateConfig(maxVideoBitrate);
}
// Give priority to the renderer's maximum bitrate setting over the user's setting
if (rendererMaxBitrates[0] > 0 && rendererMaxBitrates[0] < defaultMaxBitrates[0]) {
LOGGER.trace(
"Using video bitrate limit from {} configuration ({} Mb/s) because " +
"it is lower than the general configuration bitrate limit ({} Mb/s)",
getRendererName(),
rendererMaxBitrates[0],
defaultMaxBitrates[0]
);
defaultMaxBitrates = rendererMaxBitrates;
}
if (isHalveBitrate()) {
defaultMaxBitrates[0] /= 2;
}
maximumBitrateTotal = defaultMaxBitrates[0] * 1000000;
return maximumBitrateTotal;
}
@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();
if (StringUtils.isBlank(mpegSettings)) {
return "";
}
return convertMencoderSettingToFFmpegFormat(mpegSettings);
}
/**
* Converts the MEncoder's quality settings format to FFmpeg's.
*
* @return The FFmpeg format.
*/
public String convertMencoderSettingToFFmpegFormat(String mpegSettings) {
String mpegSettingsArray[] = mpegSettings.split(":");
String pairArray[];
StringBuilder returnString = new StringBuilder();
for (String pair : mpegSettingsArray) {
pairArray = pair.split("=");
switch (pairArray[0]) {
case "keyint":
returnString.append("-g ").append(pairArray[1]).append(' ');
break;
case "vqscale":
returnString.append("-q:v ").append(pairArray[1]).append(' ');
break;
case "vqmin":
returnString.append("-qmin ").append(pairArray[1]).append(' ');
break;
case "vqmax":
returnString.append("-qmax ").append(pairArray[1]).append(' ');
break;
default:
break;
}
}
return returnString.toString();
}
/**
* 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. 0 means unlimited.
*
* @see #isMaximumResolutionSpecified()
*
* @return The maximum video width.
*/
public int getMaxVideoWidth() {
return getInt(MAX_VIDEO_WIDTH, 1920);
}
/**
* Returns the maximum video height supported by the renderer as defined
* in the renderer configuration. 0 means unlimited.
*
* @see #isMaximumResolutionSpecified()
*
* @return The maximum video height.
*/
public int getMaxVideoHeight() {
return getInt(MAX_VIDEO_HEIGHT, 1080);
}
/**
* @Deprecated use isMaximumResolutionSpecified() instead
*/
@Deprecated
public boolean isVideoRescale() {
return getMaxVideoWidth() > 0 && getMaxVideoHeight() > 0;
}
/**
* Returns <code>true</code> if the renderer has a maximum supported width
* and height, <code>false</code> otherwise.
*
* @return whether the renderer has specified a maximum width and height
*/
public boolean isMaximumResolutionSpecified() {
return getMaxVideoWidth() > 0 && getMaxVideoHeight() > 0;
}
/**
* Whether the resolution is compatible with the renderer.
*
* @param width the media width
* @param height the media height
*
* @return whether the resolution is compatible with the renderer
*/
public boolean isResolutionCompatibleWithRenderer(int width, int height) {
// Check if the resolution is too high
if (
isMaximumResolutionSpecified() &&
(
width > getMaxVideoWidth() ||
(
height > getMaxVideoHeight() &&
!(
getMaxVideoHeight() == 1080 &&
height == 1088
)
)
)
) {
return false;
}
// Check if the resolution is too low
if (!isRescaleByRenderer() && getMaxVideoWidth() < 720) {
return false;
}
return true;
}
public boolean isDLNAOrgPNUsed() {
return getBoolean(DLNA_ORGPN_USE, true);
}
public boolean isAccurateDLNAOrgPN() {
return getBoolean(ACCURATE_DLNA_ORGPN, 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. The supported external
* subtitles must be set by {@link #SupportedExternalSubtitlesFormats()}.
*
* 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();
}
@Deprecated
public boolean isMediaParserV2() {
return isUseMediaInfo();
}
/**
* @return whether to use MediaInfo
*/
public boolean isUseMediaInfo() {
return getBoolean(MEDIAPARSERV2, false) && LibMediaInfoParser.isValid();
}
@Deprecated
public boolean isMediaParserV2ThumbnailGeneration() {
return isMediaInfoThumbnailGeneration();
}
public boolean isMediaInfoThumbnailGeneration() {
return getBoolean(MEDIAPARSERV2_THUMB, false) && LibMediaInfoParser.isValid();
}
public boolean isShowAudioMetadata() {
return getBoolean(SHOW_AUDIO_METADATA, true);
}
public boolean isShowSubMetadata() {
return getBoolean(SHOW_SUB_METADATA, true);
}
/**
* Whether to send the last modified date metadata for files and
* folders, which can take up screen space on some renderers.
*
* @return whether to send the metadata
*/
public boolean isSendDateMetadata() {
return getBoolean(SEND_DATE_METADATA, true);
}
/**
* Whether to send folder thumbnails.
*
* @return whether to send folder thumbnails
*/
public boolean isSendFolderThumbnails() {
return getBoolean(SEND_FOLDER_THUMBNAILS, 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 Disc players). By default, UMS 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.
* @param configuration The {@link PmsConfiguration} to use while evaluating compatibility
* @return True if the renderer natively supports the format, false
* otherwise.
*/
public boolean isCompatible(DLNAMediaInfo mediaInfo, Format format, PmsConfiguration configuration) {
if (configuration == null) {
configuration = PMS.getConfiguration(this);
}
if (
configuration != null &&
(configuration.isDisableTranscoding() ||
(format != null &&
format.skip(configuration.getDisableTranscodeForExtensions())))
) {
return true;
}
// Handle images differently because of automatic image transcoding
if (format != null && format.isImage()) {
if (
format.getIdentifier() == Identifier.RAW ||
mediaInfo != null && mediaInfo.getImageInfo() != null &&
mediaInfo.getImageInfo().getFormat() != null &&
mediaInfo.getImageInfo().getFormat().isRaw()
) {
LOGGER.trace(
"RAW ({}) images are not supported for streaming",
mediaInfo != null && mediaInfo.getImageInfo() != null && mediaInfo.getImageInfo().getFormat() != null ?
mediaInfo.getImageInfo().getFormat() :
format
);
return false;
}
if (mediaInfo != null && mediaInfo.getImageInfo() != null && mediaInfo.getImageInfo().isImageIOSupported()) {
LOGGER.trace(
"Format \"{}\" will be subject to on-demand automatic transcoding with ImageIO",
mediaInfo.getImageInfo().getFormat() != null ?
mediaInfo.getImageInfo().getFormat() :
format
);
return true;
}
LOGGER.trace("Format \"{}\" is not supported by ImageIO and will depend on a compatible transcoding engine", format);
return false;
}
// Use the configured "Supported" lines in the renderer.conf
// to see if any of them match the MediaInfo library
if (isUseMediaInfo() && mediaInfo != null && getFormatConfiguration().match(mediaInfo) != null) {
return true;
}
return format != null ? format.skip(getStreamedExtensions()) : 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) {
return isCompatible(mediainfo, format, null);
}
public int getAutoPlayTmo() {
return getInt(AUTO_PLAY_TMO, 5000);
}
public String getCustomFFmpegOptions() {
return getString(CUSTOM_FFMPEG_OPTIONS, "");
}
public boolean isNoDynPlsFolder() {
return false;
}
/**
* If this is true, we will always output video at 16/9 aspect ratio to
* the renderer, meaning that all videos with different aspect ratios
* will have black bars added to the edges to make them 16/9.
*
* This addresses a bug in some renderers (like Panasonic TVs) where
* they stretch videos that are not 16/9.
*
* @return
*/
public boolean isKeepAspectRatio() {
return getBoolean(KEEP_ASPECT_RATIO, false);
}
/**
* If this is true, we will always output transcoded video at 16/9
* aspect ratio to the renderer, meaning that all transcoded videos with
* different aspect ratios will have black bars added to the edges to
* make them 16/9.
*
* This addresses a bug in some renderers (like Panasonic TVs) where
* they stretch transcoded videos that are not 16/9.
*
* @return
*/
public boolean isKeepAspectRatioTranscoding() {
return getBoolean(KEEP_ASPECT_RATIO_TRANSCODING, false);
}
/**
* If this is false, FFmpeg will upscale videos with resolutions lower
* than SD (720 pixels wide) to the maximum resolution your renderer
* supports.
*
* Changing it to false is only recommended if your renderer has
* poor-quality upscaling, since we will use more CPU and network
* bandwidth when it is false.
*
* @return
*/
public boolean isRescaleByRenderer() {
return getBoolean(RESCALE_BY_RENDERER, true);
}
/**
* Whether to prepend audio track numbers to audio titles.
* e.g. "Stairway to Heaven" becomes "4: Stairway to Heaven".
*
* This is to provide a workaround for devices that order everything
* alphabetically instead of in the order we give, like Samsung devices.
*
* @return whether to prepend audio track numbers to audio titles.
*/
public boolean isPrependTrackNumbers() {
return getBoolean(PREPEND_TRACK_NUMBERS, false);
}
public String getFFmpegVideoFilterOverride() {
return getString(OVERRIDE_FFMPEG_VF, "");
}
public static ArrayList<String> getAllRenderersNames() {
return allRenderersNames;
}
public int getTranscodedVideoAudioSampleRate() {
return getInt(TRANSCODED_VIDEO_AUDIO_SAMPLE_RATE, 48000);
}
public boolean isLimitFolders() {
return getBoolean(LIMIT_FOLDERS, true);
}
/**
* Perform renderer-specific name reformatting:<p>
* Truncating and wrapping see {@code TextWrap}<br>
* Character substitution see {@code CharMap}
*
* @param name Original name
* @param suffix Additional media information
* @param dlna The actual DLNA resource
* @return Reformatted name
*/
public String getDcTitle(String name, String suffix, DLNAResource dlna) {
// Wrap + truncate
int len = 0;
if (lineWidth > 0 && (name.length() + suffix.length()) > lineWidth) {
int suffix_len = dots.length() + suffix.length();
if (lineHeight == 1) {
len = lineWidth - suffix_len;
} else {
// Wrap
int i = dlna.isFolder() ? 0 : indent;
String newline = "\n" + (dlna.isFolder() ? "" : inset);
name = name.substring(0, i + (i < name.length() && Character.isWhitespace(name.charAt(i)) ? 1 : 0))
+ WordUtils.wrap(name.substring(i) + suffix, lineWidth - i, newline, true);
len = lineWidth * lineHeight;
if (len != 0 && name.length() > len) {
len = name.substring(0, name.length() - lineWidth).lastIndexOf(newline) + newline.length();
name = name.substring(0, len) + name.substring(len, len + lineWidth).replace(newline, " ");
len += (lineWidth - suffix_len - i);
} else {
len = -1; // done
}
}
if (len > 0) {
// Truncate
name = name.substring(0, len).trim() + dots;
}
}
if (len > -1) {
name += suffix;
}
// Substitute
for (String s : charMap.keySet()) {
String repl = charMap.get(s).replaceAll("###e", "");
name = name.replaceAll(s, repl);
}
return name;
}
/**
* @see #isSendDateMetadata()
* @deprecated
*/
@Deprecated
public boolean isOmitDcDate() {
return !isSendDateMetadata();
}
public static int getIntAt(String s, String key, int fallback) {
try {
return Integer.valueOf((s + " ").split(key)[1].split("\\D")[0]);
} catch (Exception e) {
return fallback;
}
}
/**
* List of the renderer supported external subtitles formats
* for streaming together with streaming (not transcoded) video.
*
* @return A comma-separated list of supported text-based external subtitles formats.
*/
public String getSupportedExternalSubtitles() {
return getString(SUPPORTED_EXTERNAL_SUBTITLES_FORMATS, "");
}
/**
* List of video formats for which supported external subtitles formats
* are set for streaming together with streaming (not transcoded) video.
* If empty all subtitles listed in "SupportedExternalSubtitlesFormats" will be streamed.
* When specified only for listed video formats subtitles will be streamed.
*
* @return A comma-separated list of supported video formats listed in "Supported" section.
*/
public String getVideoFormatsSupportingStreamedExternalSubtitles() {
return getString(VIDEO_FORMATS_SUPPORTING_STREAMED_EXTERNAL_SUBTITLES, "");
}
/**
* List of the renderer supported embedded subtitles formats.
*
* @return A comma-separated list of supported embedded subtitles formats.
*/
public String getSupportedEmbeddedSubtitles() {
return getString(SUPPORTED_INTERNAL_SUBTITLES_FORMATS, "");
}
public boolean useClosedCaption() {
return getBoolean(USE_CLOSED_CAPTION, false);
}
public boolean offerSubtitlesAsResource() {
return getBoolean(OFFER_SUBTITLES_AS_SOURCE, true);
}
public boolean offerSubtitlesByProtocolInfo() {
return getBoolean(OFFER_SUBTITLES_BY_PROTOCOL_INFO, true);
}
public boolean isSubtitlesStreamingSupported() {
return StringUtils.isNotBlank(getSupportedExternalSubtitles());
}
/**
* Check if the given subtitle type is supported by renderer for streaming for given media.
*
* @param subtitle Subtitles for checking
* @param media Played media
*
* @return True if the renderer specifies support for the subtitles and
* renderer supports subs streaming for the given media video.
*/
public boolean isExternalSubtitlesFormatSupported(DLNAMediaSubtitle subtitle, DLNAMediaInfo media) {
if (subtitle == null || media == null) {
return false;
}
if (isSubtitlesStreamingSupported()) {
String[] supportedFormats = null;
if (StringUtils.isNotBlank(getVideoFormatsSupportingStreamedExternalSubtitles())) {
supportedFormats = getVideoFormatsSupportingStreamedExternalSubtitles().split(",");
}
String[] supportedSubs = getSupportedExternalSubtitles().split(",");
for (String supportedSub : supportedSubs) {
if (subtitle.getType().toString().equals(supportedSub.trim().toUpperCase())) {
if (supportedFormats != null) {
for (String supportedFormat : supportedFormats) {
if (media.getCodecV() != null && media.getCodecV().equals(supportedFormat.trim())) {
return true;
}
}
} else {
return true;
}
}
}
}
return false;
}
/**
* Check if the internal subtitle type is supported by renderer.
*
* @param subtitle Subtitles for checking
* @return True if the renderer specifies support for the subtitles
*/
public boolean isEmbeddedSubtitlesFormatSupported(DLNAMediaSubtitle subtitle) {
if (subtitle == null) {
return false;
}
if (isEmbeddedSubtitlesSupported()) {
String[] supportedSubs = getSupportedEmbeddedSubtitles().split(",");
for (String supportedSub : supportedSubs) {
if (subtitle.getType().toString().equals(supportedSub.trim().toUpperCase())) {
return true;
}
}
}
return false;
}
public boolean isEmbeddedSubtitlesSupported() {
return StringUtils.isNotBlank(getSupportedEmbeddedSubtitles());
}
public ArrayList<String> tags() {
if (rootFolder != null) {
return rootFolder.getTags();
}
return null;
}
public String getOutput3DFormat() {
return getString(OUTPUT_3D_FORMAT, "");
}
public boolean ignoreTranscodeByteRangeRequests() {
return getBoolean(IGNORE_TRANSCODE_BYTE_RANGE_REQUEST, false);
}
public String calculatedSpeed() throws InterruptedException, ExecutionException {
String max = getString(MAX_VIDEO_BITRATE, "");
for (InetAddress sa : addressAssociation.keySet()) {
if (addressAssociation.get(sa) == this) {
Future<Integer> speed = SpeedStats.getInstance().getSpeedInMBitsStored(sa, getRendererName());
if (max == null) {
return String.valueOf(speed.get());
}
try {
Integer i = Integer.parseInt(max);
if (speed.get() > i && i != 0) {
return max;
} else {
return String.valueOf(speed.get());
}
} catch (NumberFormatException e) {
return String.valueOf(speed.get());
}
}
}
return max;
}
/**
* A case-insensitive string comparator
*/
public static final Comparator<String> CaseInsensitiveComparator = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.compareToIgnoreCase(s2);
}
};
/**
* A case-insensitive key-sorted map of headers that can join its values
* into a combined string or regex.
*/
public static class SortedHeaderMap extends TreeMap<String, String> {
private static final long serialVersionUID = -5090333053981045429L;
String headers = null;
public SortedHeaderMap() {
super(CaseInsensitiveComparator);
}
public SortedHeaderMap(Collection<Map.Entry<String, String>> headers) {
this();
for (Map.Entry<String, String> h : headers) {
put(h.getKey(), h.getValue());
}
}
@Override
public String put(String key, String value) {
if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) {
headers = null; // i.e. mark as changed
return super.put(key.trim(), value.trim());
}
return null;
}
public String put(String raw) {
return put(StringUtils.substringBefore(raw, ":"), StringUtils.substringAfter(raw, ":"));
}
public String joined() {
if (headers == null) {
headers = StringUtils.join(values(), " ");
}
return headers;
}
public String toRegex() {
int size = size();
return (size > 1 ? "(" : "") + StringUtils.join(values(), ").*(") + (size > 1 ? ")" : "");
}
}
/**
* Pattern match our combined header matcher to the given collection of sorted request
* headers as a whole.
*
* @param headers The headers.
* @return True if the pattern matches or false if no match, no headers, or no matcher.
*/
public boolean match(SortedHeaderMap headers) {
if (headers != null && !headers.isEmpty() && sortedHeaderMatcher != null) {
return sortedHeaderMatcher.reset(headers.joined()).find();
}
return false;
}
/**
* The loading priority of this renderer. This should be set to 1 (or greater)
* if this renderer config is a more specific version of one we already have.
*
* For example, we have a Panasonic TVs config that is used for all
* Panasonic TVs, except the ones we have specific configs for, so the
* specific ones have a greater priority to ensure they are used when
* applicable instead of the less-specific renderer config.
*
* @return The loading priority of this renderer
*/
public int getLoadingPriority() {
return getInt(LOADING_PRIORITY, 0);
}
/**
* A loading priority comparator
*/
public static final Comparator<RendererConfiguration> rendererLoadingPriorityComparator = new Comparator<RendererConfiguration>() {
@Override
public int compare(RendererConfiguration r1, RendererConfiguration r2) {
if (r1 == null || r2 == null) {
return (r1 == null && r2 == null) ? 0 : r1 == null ? 1 : r2 == null ? -1 : 0;
}
int p1 = r1.getLoadingPriority();
int p2 = r2.getLoadingPriority();
return p1 > p2 ? -1 : p1 < p2 ? 1 : r1.getConfName().compareToIgnoreCase(r2.getConfName());
}
};
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 (StringUtils.isBlank(bitrate)) {
bitrate = "0";
}
bitrates[0] = (int) Double.parseDouble(bitrate);
return bitrates;
}
/**
* Automatic reloading
*/
public static final FileWatcher.Listener reloader = new FileWatcher.Listener() {
@Override
public void notify(String filename, String event, FileWatcher.Watch watch, boolean isDir) {
RendererConfiguration r = (RendererConfiguration) watch.getItem();
if (r != null && r.getFile().equals(new File(filename))) {
r.reset();
}
}
};
private DLNAResource playingRes;
public DLNAResource getPlayingRes() {
return playingRes;
}
public void setPlayingRes(DLNAResource dlna) {
playingRes = dlna;
getPlayer();
if (dlna != null) {
player.getState().name = dlna.getDisplayName();
player.start();
} else {
player.reset();
}
}
private long buffer;
public void setBuffer(long mb) {
buffer = mb < 0 ? 0 : mb;
getPlayer().setBuffer(mb);
}
public long getBuffer() {
return buffer;
}
public String getSubLanguage() {
return pmsConfiguration.getSubtitlesLanguages();
}
public static class PlaybackTimer extends BasicPlayer.Minimal {
private long duration = 0;
public PlaybackTimer(DeviceConfiguration renderer) {
super(renderer);
LOGGER.debug("Created playback timer for " + renderer.getRendererName());
}
@Override
public void start() {
final DLNAResource res = renderer.getPlayingRes();
state.name = res.getDisplayName();
duration = 0;
if (res.getMedia() != null) {
duration = (long) res.getMedia().getDurationInSeconds() * 1000;
state.duration = DurationFormatUtils.formatDuration(duration, "HH:mm:ss");
}
Runnable r = new Runnable() {
@Override
public void run() {
state.playback = PLAYING;
while (res == renderer.getPlayingRes()) {
long elapsed;
if ((long) res.getLastStartPosition() == 0) {
elapsed = System.currentTimeMillis() - (long) res.getStartTime();
} else {
elapsed = System.currentTimeMillis() - (long) res.getLastStartSystemTime();
elapsed += (long) (res.getLastStartPosition() * 1000);
}
if (duration == 0 || elapsed < duration + 500) {
// Position is valid as far as we can tell
state.position = DurationFormatUtils.formatDuration(elapsed, "HH:mm:ss");
} else {
// Position is invalid, blink instead
state.position = ("NOT_IMPLEMENTED" + (elapsed / 1000 % 2 == 0 ? " " : "--"));
}
alert();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
// Reset only if another item hasn't already begun playing
if (renderer.getPlayingRes() == null) {
reset();
}
}
};
new Thread(r).start();
}
}
public final String INFO = "info";
public final String OK = "okay";
public final String WARN = "warn";
public final String ERR = "err";
public void notify(String type, String msg) {
// Implemented by subclasses
}
public int getMaxVolume() {
return getInt(MAX_VOLUME, 100);
}
public void setIdentifiers(List<String> identifiers) {
this.identifiers = identifiers;
}
public List<String> getIdentifiers() {
return identifiers;
}
public String getDeviceId() {
String d = getString(DEVICE_ID, "");
if (StringUtils.isBlank(d)) {
// Backward compatibility
d = getString("device", "");
}
// Note: this might be a comma-separated list of ids
return d;
}
/**
* Upnp service startup management
*/
public static final int BLOCK = -2;
public static final int POSTPONE = -1;
public static final int NONE = 0;
public static final int ALLOW = 1;
protected volatile int upnpMode = NONE;
public static int getUpnpMode(String mode) {
if (mode != null) {
switch (mode.trim().toLowerCase()) {
case "false": return BLOCK;
case "postpone": return POSTPONE;
}
}
return ALLOW;
}
public static String getUpnpModeString(int mode) {
switch (mode) {
case BLOCK: return "blocked";
case POSTPONE: return "postponed";
case NONE: return "unknown";
}
return "allowed";
}
public int getUpnpMode() {
if (upnpMode == NONE) {
upnpMode = getUpnpMode(getString(UPNP_ALLOW, "true"));
}
return upnpMode;
}
public String getUpnpModeString() {
return getUpnpModeString(upnpMode);
}
public void resetUpnpMode() {
setUpnpMode(getUpnpMode(getString(UPNP_ALLOW, "true")));
}
public void setUpnpMode(int mode) {
if (upnpMode != mode) {
upnpMode = mode;
if (upnpMode == ALLOW) {
String id = uuid != null ? uuid : DeviceConfiguration.getUuidOf(getAddress());
if (id != null) {
configuration.setProperty(UPNP_ALLOW, "true");
UPNPHelper.activate(id);
}
}
}
}
public boolean isUpnpPostponed() {
return getUpnpMode() == POSTPONE;
}
public boolean isUpnpAllowed() {
return getUpnpMode() > NONE;
}
public static void verify(RendererConfiguration r) {
// FIXME: this is a very fallible, incomplete validity test for use only until
// we find something better. The assumption is that renderers unable determine
// their own address (i.e. non-UPnP/web renderers that have lost their spot in the
// address association to a newer renderer at the same ip) are "invalid".
if (r.getUpnpMode() != BLOCK && r.getAddress() == null) {
LOGGER.debug("Purging renderer {} as invalid", r);
r.delete(0);
}
}
/**
* Whether the renderer can display thumbnails.
*
* @return whether the renderer can display thumbnails
*/
public boolean isThumbnails() {
return getBoolean(THUMBNAILS, true);
}
/**
* Whether we should add black padding to thumbnails so they are always
* at the same resolution, or just scale to within the limits.
*
* @return whether to add padding to thumbnails
*/
public boolean isThumbnailPadding() {
return getBoolean(THUMBNAIL_PADDING, false);
}
/**
* Whether to stream subtitles even if the video is transcoded. It may work on some renderers.
*
* @return whether to stream subtitles for transcoded video
*/
public boolean streamSubsForTranscodedVideo() {
return getBoolean(STREAM_SUBS_FOR_TRANSCODED_VIDEO, false);
}
/**
* List of supported video bit depths.
*
* @return a comma-separated list of supported video bit depths.
*/
public String getSupportedVideoBitDepths() {
return getString(SUPPORTED_VIDEO_BIT_DEPTHS, "8");
}
/**
* Check if the given video bit depth is supported.
*
* @param videoBitDepth The video bit depth
*
* @return whether the video bit depth is supported.
*/
public boolean isVideoBitDepthSupported(int videoBitDepth) {
String[] supportedBitDepths = getSupportedVideoBitDepths().split(",");
for (String supportedBitDepth : supportedBitDepths) {
if (Integer.toString(videoBitDepth).equals(supportedBitDepth.trim())) {
return true;
}
}
return false;
}
public boolean isRemoveTagsFromSRTsubs() {
return getBoolean(REMOVE_TAGS_FROM_SRT_SUBS, true);
}
}