package net.pms.configuration;
import net.pms.dlna.DLNAMediaAudio;
import net.pms.dlna.DLNAMediaInfo;
import net.pms.dlna.InputFile;
import net.pms.dlna.LibMediaInfoParser;
import net.pms.formats.Format;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
public class FormatConfiguration {
private static final Logger logger = LoggerFactory.getLogger(FormatConfiguration.class);
private ArrayList<SupportSpec> supportSpecs;
// Use old parser for JPEG files (MediaInfo does not support EXIF)
private static final String[] PARSER_V1_EXTENSIONS = new String[] { ".jpg", ".jpe", ".jpeg" };
public static final String AAC = "aac";
public static final String AC3 = "ac3";
public static final String AIFF = "aiff";
public static final String ALAC = "alac";
public static final String APE = "ape";
public static final String ATRAC = "atrac";
public static final String AVI = "avi";
public static final String BMP = "bmp";
public static final String DIVX = "divx";
public static final String DTS = "dts";
public static final String DTSHD = "dtshd";
public static final String DV = "dv";
public static final String EAC3 = "eac3";
public static final String FLAC = "flac";
public static final String FLV = "flv";
public static final String GIF = "gif";
public static final String H264 = "h264";
public static final String JPG = "jpg";
public static final String LPCM = "lpcm";
public static final String MATROSKA = "mkv";
public static final String MI_GMC = "gmc";
public static final String MI_QPEL = "qpel";
public static final String MJPEG = "mjpeg";
public static final String MLP = "mlp";
public static final String MOV = "mov";
public static final String MP3 = "mp3";
public static final String MP4 = "mp4";
public static final String MPA = "mpa";
public static final String MPC = "mpc";
public static final String MPEG1 = "mpeg1";
public static final String MPEG2 = "mpeg2";
public static final String MPEGPS = "mpegps";
public static final String MPEGTS = "mpegts";
public static final String OGG = "ogg";
public static final String PNG = "png";
public static final String RA = "ra";
public static final String RM = "rm";
public static final String SHORTEN = "shn";
public static final String TIFF = "tiff";
public static final String TRUEHD = "truehd";
public static final String VC1 = "vc1";
public static final String WAVPACK = "wavpack";
public static final String WAV = "wav";
public static final String WEBM = "WebM";
public static final String WMA = "wma";
public static final String WMV = "wmv";
public static final String MIMETYPE_AUTO = "MIMETYPE_AUTO";
public static final String und = "und";
private class SupportSpec {
private int iMaxBitrate = Integer.MAX_VALUE;
private int iMaxFrequency = Integer.MAX_VALUE;
private int iMaxNbChannels = Integer.MAX_VALUE;
private int iMaxVideoHeight = Integer.MAX_VALUE;
private int iMaxVideoWidth = Integer.MAX_VALUE;
private Map<String, Pattern> miExtras;
private Pattern pAudioCodec;
private Pattern pFormat;
private Pattern pVideoCodec;
private String audioCodec;
private String format;
private String line;
private String maxBitrate;
private String maxFrequency;
private String maxNbChannels;
private String maxVideoHeight;
private String maxVideoWidth;
private String mimeType;
private String videoCodec;
private String supportLine;
SupportSpec() {
this.mimeType = MIMETYPE_AUTO;
}
boolean isValid() {
if (StringUtils.isBlank(format)) { // required
logger.warn("No format supplied");
return false;
} else {
try {
pFormat = Pattern.compile(format);
} catch (PatternSyntaxException pse) {
logger.error("Error parsing format: " + format, pse);
return false;
}
}
if (videoCodec != null) {
try {
pVideoCodec = Pattern.compile(videoCodec);
} catch (PatternSyntaxException pse) {
logger.error("Error parsing video codec: " + videoCodec, pse);
return false;
}
}
if (audioCodec != null) {
try {
pAudioCodec = Pattern.compile(audioCodec);
} catch (PatternSyntaxException pse) {
logger.error("Error parsing audio codec: " + audioCodec, pse);
return false;
}
}
if (maxNbChannels != null) {
try {
iMaxNbChannels = Integer.parseInt(maxNbChannels);
} catch (NumberFormatException nfe) {
logger.error("Error parsing number of channels: " + maxNbChannels, nfe);
return false;
}
}
if (maxFrequency != null) {
try {
iMaxFrequency = Integer.parseInt(maxFrequency);
} catch (NumberFormatException nfe) {
logger.error("Error parsing maximum frequency: " + maxFrequency, nfe);
return false;
}
}
if (maxBitrate != null) {
try {
iMaxBitrate = Integer.parseInt(maxBitrate);
} catch (NumberFormatException nfe) {
logger.error("Error parsing maximum bitrate: " + maxBitrate, nfe);
return false;
}
}
if (maxVideoWidth != null) {
try {
iMaxVideoWidth = Integer.parseInt(maxVideoWidth);
} catch (Exception nfe) {
logger.error("Error parsing maximum video width: " + maxVideoWidth, nfe);
return false;
}
}
if (maxVideoHeight != null) {
try {
iMaxVideoHeight = Integer.parseInt(maxVideoHeight);
} catch (NumberFormatException nfe) {
logger.error("Error parsing maximum video height: " + maxVideoHeight, nfe);
return false;
}
}
return true;
}
public boolean match(String container, String videoCodec, String audioCodec) {
return match(supportLine, container, videoCodec, audioCodec, 0, 0, 0, 0, 0, null);
}
/**
* Determine whether or not the provided parameters match the
* "Supported" lines for this configuration. If a parameter is null
* or 0, its value is skipped for making the match. If any of the
* non-null parameters does not match, false is returned. For example,
* assume a configuration that contains only the following line:
*
* Supported = f:mp4 n:2
*
* match("mp4", null, null, 2, 0, 0, 0, 0, null) = true
* match("mp4", null, null, 6, 0, 0, 0, 0, null) = false
* match("wav", null, null, 2, 0, 0, 0, 0, null) = false
*
* @param format
* @param videoCodec
* @param audioCodec
* @param nbAudioChannels
* @param frequency
* @param bitrate
* @param videoWidth
* @param videoHeight
* @param extras
* @return False if any of the provided non-null parameters is not a
* match, true otherwise.
*/
public boolean match(String supportLine, String format, String videoCodec,
String audioCodec, int nbAudioChannels, int frequency,
int bitrate, int videoWidth, int videoHeight,
Map<String, String> extras) {
// Assume a match, until proven otherwise
if (format != null && !pFormat.matcher(format).matches()) {
logger.trace("Format \"{}\" failed to match supported line {}", format, supportLine);
return false;
}
if (videoCodec != null && pVideoCodec != null && !pVideoCodec.matcher(videoCodec).matches()) {
logger.trace("Video codec \"{}\" failed to match support line {}", videoCodec, supportLine);
return false;
}
if (audioCodec != null && pAudioCodec != null && !pAudioCodec.matcher(audioCodec).matches()) {
logger.trace("Audio codec \"{}\" failed to match support line {}", audioCodec, supportLine);
return false;
}
if (nbAudioChannels > 0 && iMaxNbChannels > 0 && nbAudioChannels > iMaxNbChannels) {
logger.trace("Number of channels \"{}\" failed to match support line {}", nbAudioChannels, supportLine);
return false;
}
if (frequency > 0 && iMaxFrequency > 0 && frequency > iMaxFrequency) {
logger.trace("Frequency \"{}\" failed to match support line {}", frequency, supportLine);
return false;
}
if (bitrate > 0 && iMaxBitrate > 0 && bitrate > iMaxBitrate) {
logger.trace("Bit rate \"{}\" failed to match support line {}", bitrate, supportLine);
return false;
}
if (videoWidth > 0 && iMaxVideoWidth > 0 && videoWidth > iMaxVideoWidth) {
logger.trace("Video width \"{}\" failed to match support line {}", videoWidth, supportLine);
return false;
}
if (videoHeight > 0 && iMaxVideoHeight > 0 && videoHeight > iMaxVideoHeight) {
logger.trace("Video height \"{}\" failed to match support line {}", videoHeight, supportLine);
return false;
}
if (extras != null && miExtras != null) {
Iterator<String> keyIt = extras.keySet().iterator();
while (keyIt.hasNext()) {
String key = keyIt.next();
String value = extras.get(key);
if (key.equals(MI_QPEL) && miExtras.get(MI_QPEL) != null && !miExtras.get(MI_QPEL).matcher(value).matches()) {
logger.trace("Qpel value \"{}\" failed to match support line {}", miExtras.get(MI_QPEL), supportLine);
return false;
}
if (key.equals(MI_GMC) && miExtras.get(MI_GMC) != null && !miExtras.get(MI_GMC).matcher(value).matches()) {
logger.trace("Gmc value \"{}\" failed to match support line {}", miExtras.get(MI_GMC), supportLine);
return false;
}
}
}
logger.trace("Matched support line {}", supportLine);
return true;
}
}
public FormatConfiguration(List<?> lines) {
supportSpecs = new ArrayList<SupportSpec>();
for (Object line : lines) {
if (line != null) {
SupportSpec supportSpec = parseSupportLine(line.toString());
if (supportSpec.isValid()) {
supportSpecs.add(supportSpec);
} else {
logger.warn("Invalid configuration line: " + line);
}
}
}
}
public void parse(DLNAMediaInfo media, InputFile file, Format ext, int type) {
boolean forceV1 = false;
if (file.getFile() != null) {
String fName = file.getFile().getName().toLowerCase();
for (String e : PARSER_V1_EXTENSIONS) {
if (fName.endsWith(e)) {
forceV1 = true;
break;
}
}
if (forceV1) {
// XXX this path generates thumbnails
media.parse(file, ext, type, false);
} else {
// XXX this path doesn't generate thumbnails
LibMediaInfoParser.parse(media, file, type);
}
} else {
media.parse(file, ext, type, false);
}
}
// XXX Unused
@Deprecated
public boolean isDVDVideoRemuxSupported() {
return match(MPEGPS, MPEG2, null) != null;
}
public boolean isFormatSupported(String container) {
return match(container, null, null) != null;
}
public boolean isDTSSupported() {
return match(MPEGPS, null, DTS) != null || match(MPEGTS, null, DTS) != null;
}
public boolean isLPCMSupported() {
return match(MPEGPS, null, LPCM) != null || match(MPEGTS, null, LPCM) != null;
}
public boolean isMpeg2Supported() {
return match(MPEGPS, MPEG2, null) != null || match(MPEGTS, MPEG2, null) != null;
}
// XXX Unused
@Deprecated
public boolean isHiFiMusicFileSupported() {
return match(WAV, null, null, 0, 96000, 0, 0, 0, null) != null || match(MP3, null, null, 0, 96000, 0, 0, 0, null) != null;
}
public String getPrimaryVideoTranscoder() {
for (SupportSpec supportSpec : supportSpecs) {
if (supportSpec.match(MPEGPS, MPEG2, AC3)) {
return MPEGPS;
}
if (supportSpec.match(MPEGTS, MPEG2, AC3)) {
return MPEGTS;
}
if (supportSpec.match(WMV, WMV, WMA)) {
return WMV;
}
}
return null;
}
// XXX Unused
@Deprecated
public String getPrimaryAudioTranscoder() {
for (SupportSpec supportSpec : supportSpecs) {
if (supportSpec.match(WAV, null, null)) {
return WAV;
}
if (supportSpec.match(MP3, null, null)) {
return MP3;
}
// FIXME LPCM?
}
return null;
}
/**
* Match media information to audio codecs supported by the renderer
* and return its MIME-type if the match is successful. Returns null if
* the media is not natively supported by the renderer, which means it
* has to be transcoded.
* @param media The MediaInfo metadata
* @return The MIME type or null if no match was found.
*/
public String match(DLNAMediaInfo media) {
if (media.getFirstAudioTrack() == null) {
// no sound
return match(
media.getContainer(),
media.getCodecV(),
null,
0,
0,
media.getBitrate(),
media.getWidth(),
media.getHeight(),
media.getExtras()
);
} else {
String finalMimeType = null;
for (DLNAMediaAudio audio : media.getAudioTracksList()) {
String mimeType = match(
media.getContainer(),
media.getCodecV(),
audio.getCodecA(),
audio.getAudioProperties().getNumberOfChannels(),
audio.getSampleRate(),
media.getBitrate(),
media.getWidth(),
media.getHeight(),
media.getExtras()
);
finalMimeType = mimeType;
if (mimeType == null) { // if at least one audio track is not compatible, the file must be transcoded.
return null;
}
}
return finalMimeType;
}
}
public String match(String container, String videoCodec, String audioCodec) {
return match(
container,
videoCodec,
audioCodec,
0,
0,
0,
0,
0,
null
);
}
public String match(
String container,
String videoCodec,
String audioCodec,
int nbAudioChannels,
int frequency,
int bitrate,
int videoWidth,
int videoHeight,
Map<String,
String> extras
) {
String matchedMimeType = null;
for (SupportSpec supportSpec : supportSpecs) {
if (supportSpec.match(
supportSpec.supportLine,
container,
videoCodec,
audioCodec,
nbAudioChannels,
frequency,
bitrate,
videoWidth,
videoHeight,
extras
)) {
matchedMimeType = supportSpec.mimeType;
break;
}
}
return matchedMimeType;
}
private SupportSpec parseSupportLine(String line) {
StringTokenizer st = new StringTokenizer(line, "\t ");
SupportSpec supportSpec = new SupportSpec();
supportSpec.supportLine = line;
while (st.hasMoreTokens()) {
String token = st.nextToken();
if (token.startsWith("f:")) {
supportSpec.format = token.substring(2).trim();
} else if (token.startsWith("v:")) {
supportSpec.videoCodec = token.substring(2).trim();
} else if (token.startsWith("a:")) {
supportSpec.audioCodec = token.substring(2).trim();
} else if (token.startsWith("n:")) {
supportSpec.maxNbChannels = token.substring(2).trim();
} else if (token.startsWith("s:")) {
supportSpec.maxFrequency = token.substring(2).trim();
} else if (token.startsWith("w:")) {
supportSpec.maxVideoWidth = token.substring(2).trim();
} else if (token.startsWith("h:")) {
supportSpec.maxVideoHeight = token.substring(2).trim();
} else if (token.startsWith("m:")) {
supportSpec.mimeType = token.substring(2).trim();
} else if (token.startsWith("b:")) {
supportSpec.maxBitrate = token.substring(2).trim();
} else if (token.contains(":")) {
// extra MediaInfo stuff
if (supportSpec.miExtras == null) {
supportSpec.miExtras = new HashMap<String, Pattern>();
}
String key = token.substring(0, token.indexOf(":"));
String value = token.substring(token.indexOf(":") + 1);
supportSpec.miExtras.put(key, Pattern.compile(value));
}
}
return supportSpec;
}
}