/*
* Universal Media Server, for streaming any media to DLNA
* compatible renderers based on the http://www.ps3mediaserver.org.
* Copyright (C) 2012 UMS developers.
*
* This program is a free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.pms.configuration;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
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 net.pms.formats.Format.Identifier;
import net.pms.util.AudioUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FormatConfiguration {
private static final Logger LOGGER = LoggerFactory.getLogger(FormatConfiguration.class);
private ArrayList<SupportSpec> supportSpecs;
public static final String THREEGPP = "3gp";
public static final String THREEGPP2 = "3g2";
public static final String THREEGA = "3ga";
public static final String AAC = "aac";
public static final String AAC_HE = "aac-he";
public static final String AC3 = "ac3";
public static final String ADPCM = "adpcm";
public static final String ADTS = "adts";
public static final String AIFF = "aiff";
public static final String ALAC = "alac";
public static final String AMR = "amr";
public static final String ATMOS = "atmos";
public static final String ATRAC = "atrac";
public static final String AU = "au";
public static final String AVI = "avi";
public static final String BMP = "bmp";
public static final String CINEPACK = "cvid";
public static final String COOK = "cook";
public static final String CUR = "cur";
public static final String DIVX = "divx";
public static final String DSDAudio = "dsd";
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 H263 = "h263";
public static final String H264 = "h264";
public static final String H265 = "h265";
public static final String ICNS = "icns";
public static final String ICO = "ico";
public static final String JPG = "jpg";
public static final String LPCM = "lpcm";
public static final String M4A = "m4a";
public static final String MATROSKA = "mkv";
public static final String MI_GMC = "gmc";
public static final String MI_GOP = "gop";
public static final String MI_QPEL = "qpel";
public static final String MJPEG = "mjpeg";
public static final String MKA = "mka";
public static final String MLP = "mlp";
public static final String MONKEYS_AUDIO = "ape";
public static final String MOV = "mov";
public static final String MP2 = "mp2";
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 OPUS = "opus";
public static final String PCX = "pcx";
public static final String PNG = "png";
public static final String PNM = "pnm";
public static final String PSD = "psd";
public static final String QDESIGN = "qdmc";
public static final String RA = "ra";
public static final String RAW = "raw";
public static final String REALAUDIO_LOSSLESS = "ralf";
public static final String RM = "rm";
public static final String SHORTEN = "shn";
public static final String SORENSON = "sor";
public static final String THEORA = "theora";
public static final String TIFF = "tiff";
public static final String TRUEHD = "truehd";
public static final String TTA = "tta";
public static final String VC1 = "vc1";
public static final String VORBIS = "vorbis";
public static final String VP6 = "vp6";
public static final String VP7 = "vp7";
public static final String VP8 = "vp8";
public static final String VP9 = "vp9";
public static final String WAV = "wav";
public static final String WAVPACK = "wavpack";
public static final String WBMP = "wbmp";
public static final String WEBM = "webm";
public static final String WEBP = "webp";
public static final String WMA = "wma";
public static final String WMALOSSLESS = "wmalossless";
public static final String WMAPRO = "wmapro";
public static final String WMAVOICE = "wmavoice";
public static final String WMV = "wmv";
public static final String MIMETYPE_AUTO = "MIMETYPE_AUTO";
public static final String und = "und";
private static 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 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 (NumberFormatException 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(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 format,
String videoCodec,
String audioCodec,
int nbAudioChannels,
int frequency,
int bitrate,
int videoWidth,
int videoHeight,
Map<String, String> extras
) {
// Satisfy a minimum threshold
if (format == null && videoCodec == null && audioCodec == null) {
// We have no matchable info. This can happen with unparsed
// mediainfo objects (e.g. from WEB.conf or plugins).
return false;
}
// 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<Entry<String, String>> keyIt = extras.entrySet().iterator();
while (keyIt.hasNext()) {
String key = keyIt.next().getKey();
String value = extras.get(key).toLowerCase();
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;
}
if (key.equals(MI_GOP) && miExtras.get(MI_GOP) != null && miExtras.get(MI_GOP).matcher("static").matches() && value.equals("variable")) {
LOGGER.trace("GOP value \"{}\" failed to match support line {}", value, supportLine);
return false;
}
}
}
LOGGER.trace("Matched support line {}", supportLine);
return true;
}
}
public FormatConfiguration(List<?> lines) {
supportSpecs = new ArrayList<>();
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);
}
}
}
}
@Deprecated
public void parse(DLNAMediaInfo media, InputFile file, Format ext, int type) {
parse(media, file, ext, type, null);
}
/**
* Chooses which parsing method to parse the file with.
*/
public void parse(DLNAMediaInfo media, InputFile file, Format ext, int type, RendererConfiguration renderer) {
if (file.getFile() != null) {
if (ext.getIdentifier() == Identifier.RA) {
// Special parsing for RealAudio 1.0 and 2.0 which isn't handled by MediaInfo or JAudioTagger
FileChannel channel;
try {
channel = FileChannel.open(file.getFile().toPath(), StandardOpenOption.READ);
if (AudioUtils.parseRealAudio(channel, media)) {
// If successful parsing is done, if not continue parsing the standard way
media.postParse(type, file);
return;
}
} catch (IOException e) {
LOGGER.warn("An error occurred when trying to open \"{}\" for reading: {}", file, e.getMessage());
LOGGER.trace("", e);
}
}
// MediaInfo can't correctly parse ADPCM, DSD or PNM
if (
renderer.isUseMediaInfo() &&
ext.getIdentifier() != Identifier.ADPCM &&
ext.getIdentifier() != Identifier.DSD &&
ext.getIdentifier() != Identifier.PNM
) {
LibMediaInfoParser.parse(media, file, type, renderer);
} else {
media.parse(file, ext, type, false, false, renderer);
}
} else {
media.parse(file, ext, type, false, false, renderer);
}
}
// 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;
}
// XXX Unused
@Deprecated
public String getPrimaryVideoTranscoder() {
for (SupportSpec supportSpec : supportSpecs) {
if (supportSpec.match(MPEGPS, MPEG2, AC3)) {
return MPEGPS;
}
if (
supportSpec.match(MPEGTS, MPEG2, AC3) ||
supportSpec.match(MPEGTS, H264, AAC) ||
supportSpec.match(MPEGTS, H264, 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(
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 key = token.substring(0, token.indexOf(':'));
String value = token.substring(token.indexOf(':') + 1);
supportSpec.miExtras.put(key, Pattern.compile(value));
}
}
return supportSpec;
}
}