package net.pms.dlna; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.apache.commons.lang3.StringUtils.lowerCase; import static org.apache.commons.lang3.StringUtils.substringAfterLast; import java.io.File; import java.util.StringTokenizer; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.pms.configuration.FormatConfiguration; import net.pms.formats.v2.SubtitleType; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class LibMediaInfoParser { private static final Logger logger = LoggerFactory.getLogger(LibMediaInfoParser.class); /** Regular expression to parse a 4 digit year number from a string */ private static final String YEAR_REGEX = ".*([\\d]{4}).*"; /** Pattern to parse the year from a string */ private static final Pattern yearPattern = Pattern.compile(YEAR_REGEX); private static MediaInfo MI; private static Base64 base64; static { MI = new MediaInfo(); if (MI.isValid()) { MI.Option("Complete", "1"); MI.Option("Language", "raw"); } base64 = new Base64(); } public static boolean isValid() { return MI.isValid(); } public static void close() { try { MI.finalize(); } catch (Throwable e) { logger.debug("Caught exception", e); } } public synchronized static void parse(DLNAMediaInfo media, InputFile inputFile, int type) { File file = inputFile.getFile(); if (!media.isMediaparsed() && file != null && MI.isValid() && MI.Open(file.getAbsolutePath()) > 0) { try { String info = MI.Inform(); MediaInfo.StreamType streamType = MediaInfo.StreamType.General; DLNAMediaAudio currentAudioTrack = new DLNAMediaAudio(); boolean audioPrepped = false; DLNAMediaSubtitle currentSubTrack = new DLNAMediaSubtitle(); boolean subPrepped = false; if (StringUtils.isNotBlank(info)) { media.setSize(file.length()); StringTokenizer st = new StringTokenizer(info, "\n\r"); while (st.hasMoreTokens()) { String line = st.nextToken().trim(); // Define the type of media if (line.equals("Video") || line.startsWith("Video #")) { streamType = MediaInfo.StreamType.Video; } else if (line.equals("Audio") || line.startsWith("Audio #")) { if (audioPrepped) { addAudio(currentAudioTrack, media); currentAudioTrack = new DLNAMediaAudio(); } audioPrepped = true; streamType = MediaInfo.StreamType.Audio; } else if (line.equals("Text") || line.startsWith("Text #")) { if (subPrepped) { addSub(currentSubTrack, media); currentSubTrack = new DLNAMediaSubtitle(); } subPrepped = true; streamType = MediaInfo.StreamType.Text; } else if (line.equals("Menu") || line.startsWith("Menu #")) { streamType = MediaInfo.StreamType.Menu; } else if (line.equals("Chapters")) { streamType = MediaInfo.StreamType.Chapters; } int point = line.indexOf(":"); if (point > -1) { String key = line.substring(0, point).trim(); String ovalue = line.substring(point + 1).trim(); String value = ovalue.toLowerCase(); if (key.equals("Format") || key.startsWith("Format_Version") || key.startsWith("Format_Profile")) { if (streamType == MediaInfo.StreamType.Text) { // First attempt to detect subtitle track format currentSubTrack.setType(SubtitleType.valueOfLibMediaInfoCodec(value)); } else { getFormat(streamType, media, currentAudioTrack, value, file); } } else if (key.equals("Duration/String1") && streamType == MediaInfo.StreamType.General) { media.setDuration(getDuration(value)); } else if (key.equals("Format_Settings_RefFrames/String") && streamType == MediaInfo.StreamType.Video) { media.setReferenceFrameCount(getReferenceFrameCount(value)); } else if (key.equals("Format_Settings_QPel") && streamType == MediaInfo.StreamType.Video) { media.putExtra(FormatConfiguration.MI_QPEL, value); } else if (key.equals("Format_Settings_GMC") && streamType == MediaInfo.StreamType.Video) { media.putExtra(FormatConfiguration.MI_GMC, value); } else if (key.equals("MuxingMode") && streamType == MediaInfo.StreamType.Video) { media.setMuxingMode(ovalue); } else if (key.equals("CodecID")) { if (streamType == MediaInfo.StreamType.Text) { // Second attempt to detect subtitle track format (CodecID usually is more accurate) currentSubTrack.setType(SubtitleType.valueOfLibMediaInfoCodec(value)); } else { getFormat(streamType, media, currentAudioTrack, value, file); } } else if (key.equals("Language/String")) { if (streamType == MediaInfo.StreamType.Audio) { currentAudioTrack.setLang(getLang(value)); } else if (streamType == MediaInfo.StreamType.Text) { currentSubTrack.setLang(getLang(value)); } } else if (key.equals("Title")) { if (streamType == MediaInfo.StreamType.Audio) { currentAudioTrack.setFlavor(getFlavor(value)); } else if (streamType == MediaInfo.StreamType.Text) { currentSubTrack.setFlavor(getFlavor(value)); } } else if (key.equals("Width")) { media.setWidth(getPixelValue(value)); } else if (key.equals("Encryption") && !media.isEncrypted()) { media.setEncrypted("encrypted".equals(value)); } else if (key.equals("Height")) { media.setHeight(getPixelValue(value)); } else if (key.equals("DisplayAspectRatio/String")) { media.setAspectRatioContainer(value); } else if (key.equals("DisplayAspectRatio_Original/Stri")) { media.setAspectRatioVideoTrack(value); } else if (key.equals("FrameRate")) { media.setFrameRate(getFPSValue(value)); } else if (key.equals("FrameRateMode")) { media.setFrameRateMode(getFrameRateModeValue(value)); } else if (key.equals("OverallBitRate")) { if (streamType == MediaInfo.StreamType.General) { media.setBitrate(getBitrate(value)); } } else if (key.equals("Channel(s)")) { if (streamType == MediaInfo.StreamType.Audio) { currentAudioTrack.getAudioProperties().setNumberOfChannels(value); } } else if (key.equals("BitRate")) { if (streamType == MediaInfo.StreamType.Audio) { currentAudioTrack.setBitRate(getBitrate(value)); } } else if (key.equals("SamplingRate")) { if (streamType == MediaInfo.StreamType.Audio) { currentAudioTrack.setSampleFrequency(getSampleFrequency(value)); } } else if (key.equals("ID/String")) { // Special check for OGM: MediaInfo reports specific Audio/Subs IDs (0xn) while mencoder does not if (value.contains("(0x") && !FormatConfiguration.OGG.equals(media.getContainer())) { if (streamType == MediaInfo.StreamType.Audio) { currentAudioTrack.setId(getSpecificID(value)); } else if (streamType == MediaInfo.StreamType.Text) { currentSubTrack.setId(getSpecificID(value)); } } else { if (streamType == MediaInfo.StreamType.Audio) { currentAudioTrack.setId(media.getAudioTracksList().size()); } else if (streamType == MediaInfo.StreamType.Text) { // TODO questionable approach. What if audio and sub tracks are mixed? // definitely would not work with ffmpeg currentSubTrack.setId(media.getSubtitleTracksList().size()); } } } else if (key.equals("Cover_Data") && streamType == MediaInfo.StreamType.General) { media.setThumb(getCover(ovalue)); } else if (key.equals("Track") && streamType == MediaInfo.StreamType.General) { currentAudioTrack.setSongname(ovalue); } else if (key.equals("Album") && streamType == MediaInfo.StreamType.General) { currentAudioTrack.setAlbum(ovalue); } else if (key.equals("Performer") && streamType == MediaInfo.StreamType.General) { currentAudioTrack.setArtist(ovalue); } else if (key.equals("Genre") && streamType == MediaInfo.StreamType.General) { currentAudioTrack.setGenre(ovalue); } else if (key.equals("Recorded_Date") && streamType == MediaInfo.StreamType.General) { // Try to parse the year from the stored date Matcher matcher = yearPattern.matcher(value); if (matcher.matches()) { try { currentAudioTrack.setYear(Integer.parseInt(matcher.group(1))); } catch (NumberFormatException nfe) { logger.debug("Could not parse year from recorded date \"" + value + "\""); } } } else if (key.equals("Track/Position") && streamType == MediaInfo.StreamType.General) { try { currentAudioTrack.setTrack(Integer.parseInt(value)); } catch (NumberFormatException nfe) { logger.debug("Could not parse track \"" + value + "\""); } } else if (key.equals("BitDepth") && streamType == MediaInfo.StreamType.Audio) { try { currentAudioTrack.setBitsperSample(Integer.parseInt(value)); } catch (NumberFormatException nfe) { logger.debug("Could not parse bits per sample \"" + value + "\""); } } else if (key.equals("Video_Delay") && streamType == MediaInfo.StreamType.Audio) { try { currentAudioTrack.getAudioProperties().setAudioDelay(value); } catch (NumberFormatException nfe) { logger.debug("Could not parse delay \"" + value + "\""); } } } } } if (audioPrepped) { addAudio(currentAudioTrack, media); } if (subPrepped) { addSub(currentSubTrack, media); } /* Native M4A/AAC streaming bug: http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=16691 Some M4A files have generic codec id "mp42" instead of "M4A". For example: General Format : MPEG-4 Format profile : Apple audio with iTunes info Codec ID : M4A vs General Format : MPEG-4 Format profile : Base Media / Version 2 Codec ID : mp42 As workaround set container type to AAC for MP4 files with single AAC audio track and no video. */ if (FormatConfiguration.MP4.equals(media.getContainer()) && isBlank(media.getCodecV()) && media.getAudioTracksList() != null && media.getAudioTracksList().size() == 1 && FormatConfiguration.AAC.equals(media.getAudioTracksList().get(0).getCodecA())) { media.setContainer(FormatConfiguration.AAC); } media.finalize(type, inputFile); } catch (Exception e) { logger.error("Error in MediaInfo parsing:", e); } finally { MI.Close(); if (media.getContainer() == null) { media.setContainer(DLNAMediaLang.UND); } if (media.getCodecV() == null) { media.setCodecV(DLNAMediaLang.UND); } media.setMediaparsed(true); } } } public static void addAudio(DLNAMediaAudio currentAudioTrack, DLNAMediaInfo media) { if (currentAudioTrack.getLang() == null) { currentAudioTrack.setLang(DLNAMediaLang.UND); } if (currentAudioTrack.getCodecA() == null) { currentAudioTrack.setCodecA(DLNAMediaLang.UND); } media.getAudioTracksList().add(currentAudioTrack); } public static void addSub(DLNAMediaSubtitle currentSubTrack, DLNAMediaInfo media) { if (currentSubTrack.getType() == SubtitleType.UNSUPPORTED) { return; } if (currentSubTrack.getLang() == null) { currentSubTrack.setLang(DLNAMediaLang.UND); } media.getSubtitleTracksList().add(currentSubTrack); } @Deprecated // FIXME this is obsolete (replaced by the private method below) and isn't called from anywhere outside this class public static void getFormat(MediaInfo.StreamType streamType, DLNAMediaInfo media, DLNAMediaAudio audio, String value) { getFormat(streamType, media, audio, value, null); } private static void getFormat(MediaInfo.StreamType streamType, DLNAMediaInfo media, DLNAMediaAudio audio, String value, File file) { String format = null; if (value.equals("matroska")) { format = FormatConfiguration.MATROSKA; } else if (value.equals("avi") || value.equals("opendml")) { format = FormatConfiguration.AVI; } else if (value.startsWith("flash")) { format = FormatConfiguration.FLV; } else if (value.toLowerCase().equals("webm")) { format = FormatConfiguration.WEBM; } else if (value.equals("qt") || value.equals("quicktime")) { format = FormatConfiguration.MOV; } else if (value.equals("isom") || value.startsWith("mp4") || value.equals("20") || value.equals("m4v") || value.startsWith("mpeg-4")) { format = FormatConfiguration.MP4; } else if (value.contains("mpeg-ps")) { format = FormatConfiguration.MPEGPS; } else if (value.contains("mpeg-ts") || value.equals("bdav")) { format = FormatConfiguration.MPEGTS; } else if (value.contains("aiff")) { format = FormatConfiguration.AIFF; } else if (value.contains("ogg")) { format = FormatConfiguration.OGG; } else if (value.contains("realmedia") || value.startsWith("rv") || value.startsWith("cook")) { format = FormatConfiguration.RM; } else if (value.contains("windows media") || value.equals("wmv1") || value.equals("wmv2") || value.equals("wmv7") || value.equals("wmv8")) { format = FormatConfiguration.WMV; } else if (value.contains("mjpg") || value.contains("m-jpeg")) { format = FormatConfiguration.MJPEG; } else if (value.startsWith("avc") || value.contains("h264")) { format = FormatConfiguration.H264; } else if (value.contains("xvid")) { format = FormatConfiguration.MP4; } else if (value.contains("mjpg") || value.contains("m-jpeg")) { format = FormatConfiguration.MJPEG; } else if (value.contains("div") || value.contains("dx")) { format = FormatConfiguration.DIVX; } else if (value.matches("(?i)(dv)|(cdv.?)|(dc25)|(dcap)|(dvc.?)|(dvs.?)|(dvrs)|(dv25)|(dv50)|(dvan)|(dvh.?)|(dvis)|(dvl.?)|(dvnm)|(dvp.?)|(mdvf)|(pdvc)|(r411)|(r420)|(sdcc)|(sl25)|(sl50)|(sldv)")) { format = FormatConfiguration.DV; } else if (value.contains("mpeg video")) { format = FormatConfiguration.MPEG2; } else if (value.equals("vc-1") || value.equals("vc1") || value.equals("wvc1") || value.equals("wmv3") || value.equals("wmv9") || value.equals("wmva")) { format = FormatConfiguration.VC1; } else if (value.equals("version 1")) { if (media.getCodecV() != null && media.getCodecV().equals(FormatConfiguration.MPEG2) && audio.getCodecA() == null) { format = FormatConfiguration.MPEG1; } } else if (value.equals("layer 3")) { if (audio.getCodecA() != null && audio.getCodecA().equals(FormatConfiguration.MPA)) { format = FormatConfiguration.MP3; // special case: if (media.getContainer() != null && media.getContainer().equals(FormatConfiguration.MPA)) { media.setContainer(FormatConfiguration.MP3); } } } else if (value.equals("ma")) { if (audio.getCodecA() != null && audio.getCodecA().equals(FormatConfiguration.DTS)) { format = FormatConfiguration.DTSHD; } } else if (value.equals("vorbis") || value.equals("a_vorbis")) { format = FormatConfiguration.OGG; } else if (value.equals("ac-3") || value.equals("a_ac3") || value.equals("2000")) { format = FormatConfiguration.AC3; } else if (value.equals("e-ac-3")) { format = FormatConfiguration.EAC3; } else if (value.contains("truehd")) { format = FormatConfiguration.TRUEHD; } else if (value.equals("55") || value.equals("a_mpeg/l3")) { format = FormatConfiguration.MP3; } else if (value.equals("m4a") || value.equals("40") || value.equals("a_aac") || value.equals("aac")) { format = FormatConfiguration.AAC; } else if (value.equals("pcm") || (value.equals("1") && (audio.getCodecA() == null || !audio.getCodecA().equals(FormatConfiguration.DTS)))) { format = FormatConfiguration.LPCM; } else if (value.equals("alac")) { format = FormatConfiguration.ALAC; } else if (value.equals("wave")) { format = FormatConfiguration.WAV; } else if (value.equals("shorten")) { format = FormatConfiguration.SHORTEN; } else if (value.equals("dts") || value.equals("a_dts") || value.equals("8")) { format = FormatConfiguration.DTS; } else if (value.equals("mpeg audio")) { format = FormatConfiguration.MPA; } else if (value.equals("161") || value.startsWith("wma")) { format = FormatConfiguration.WMA; if (media.getCodecV() == null) { media.setContainer(FormatConfiguration.WMA); } } else if (value.equals("flac")) { format = FormatConfiguration.FLAC; } else if (value.equals("monkey's audio")) { format = FormatConfiguration.APE; } else if (value.contains("musepack")) { format = FormatConfiguration.MPC; } else if (value.contains("wavpack")) { format = FormatConfiguration.WAVPACK; } else if (value.contains("mlp")) { format = FormatConfiguration.MLP; } else if (value.contains("atrac3")) { format = FormatConfiguration.ATRAC; if (media.getCodecV() == null) { media.setContainer(FormatConfiguration.ATRAC); } } else if (value.equals("jpeg")) { format = FormatConfiguration.JPG; } else if (value.equals("png")) { format = FormatConfiguration.PNG; } else if (value.equals("gif")) { format = FormatConfiguration.GIF; } else if (value.equals("bitmap")) { format = FormatConfiguration.BMP; } else if (value.equals("tiff")) { format = FormatConfiguration.TIFF; } else if (StringUtils.contains(value, "@l") && streamType == MediaInfo.StreamType.Video) { media.setAvcLevel(getAvcLevel(value)); } if (format != null) { if (streamType == MediaInfo.StreamType.General) { media.setContainer(format); } else if (streamType == MediaInfo.StreamType.Video) { media.setCodecV(format); } else if (streamType == MediaInfo.StreamType.Audio) { audio.setCodecA(format); } } } public static int getPixelValue(String value) { if (value.indexOf("pixel") > -1) { value = value.substring(0, value.indexOf("pixel")); } value = value.trim(); // Value can look like "512 / 512" at this point if (value.contains("/")) { value = value.substring(0, value.indexOf("/")).trim(); } int pixels = Integer.parseInt(value); return pixels; } /** * @param value {@code Format_Settings_RefFrames/String} value to parse. * @return reference frame count or {@code -1} if could not parse. */ public static byte getReferenceFrameCount(String value) { try { // Values like "16 frame3" return Byte.parseByte(StringUtils.substringBefore(value, " ")); } catch (NumberFormatException ex) { // Not parsed logger.warn("Could not parse ReferenceFrameCount value {}." , value); logger.warn("Exception: ", ex); return -1; } } /** * @param value {@code Format_Profile} value to parse. * @return AVC level or {@code null} if could not parse. */ public static String getAvcLevel(String value) { // Example values: // High@L3.0 // High@L4.0 // High@L4.1 final String avcLevel = substringAfterLast(lowerCase(value), "@l"); if (isNotBlank(avcLevel)) { return avcLevel; } else { logger.warn("Could not parse AvcLevel value {}." , value); return null; } } public static int getBitrate(String value) { if (value.contains("/")) { value = value.substring(0, value.indexOf("/")).trim(); } try { return Integer.parseInt(value); } catch (NumberFormatException e) { logger.trace("Could not parse bitrate from: " + value); logger.trace("The full error was: " + e); return 0; } } public static int getSpecificID(String value) { if (value.indexOf("(0x") > -1) { value = value.substring(0, value.indexOf("(0x")); } value = value.trim(); int id = Integer.parseInt(value); return id; } public static String getSampleFrequency(String value) { // Some tracks show several values like "48000 / 48000 / 24000" for HE-AAC // We store only the first value if (value.indexOf("/") > -1) { value = value.substring(0, value.indexOf("/")); } if (value.indexOf("khz") > -1) { value = value.substring(0, value.indexOf("khz")); } value = value.trim(); return value; } public static String getFPSValue(String value) { if (value.indexOf("fps") > -1) { value = value.substring(0, value.indexOf("fps")); } value = value.trim(); return value; } public static String getFrameRateModeValue(String value) { if (value.indexOf("/") > -1) { value = value.substring(0, value.indexOf("/")); } value = value.trim(); return value; } public static String getLang(String value) { if (value.indexOf("(") > -1) { value = value.substring(0, value.indexOf("(")); } if (value.indexOf("/") > -1) { value = value.substring(0, value.indexOf("/")); } value = value.trim(); return value; } public static String getFlavor(String value) { value = value.trim(); return value; } private static double getDuration(String value) { int h = 0, m = 0, s = 0; StringTokenizer st = new StringTokenizer(value, " "); while (st.hasMoreTokens()) { String token = st.nextToken(); int hl = token.indexOf("h"); if (hl > -1) { h = Integer.parseInt(token.substring(0, hl).trim()); } int mnl = token.indexOf("mn"); if (mnl > -1) { m = Integer.parseInt(token.substring(0, mnl).trim()); } int msl = token.indexOf("ms"); if (msl == -1) { // Only check if ms was not found int sl = token.indexOf("s"); if (sl > -1) { s = Integer.parseInt(token.substring(0, sl).trim()); } } } return (h * 3600) + (m * 60) + s; } public static byte[] getCover(String based64Value) { try { if (base64 != null) { return base64.decode(based64Value.getBytes()); } } catch (Exception e) { logger.error("Error in decoding thumbnail data", e); } return null; } }