/* * PS3 Media Server, for streaming any medias to your PS3. * Copyright (C) 2008 A.Brochard * * This program is 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.dlna; import com.sun.jna.Platform; import net.coobird.thumbnailator.Thumbnails; import net.coobird.thumbnailator.Thumbnails.Builder; import net.coobird.thumbnailator.tasks.UnsupportedFormatException; import net.pms.PMS; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import net.pms.formats.AudioAsVideo; import net.pms.formats.Format; import net.pms.formats.v2.SubtitleType; import net.pms.io.OutputParams; import net.pms.io.ProcessWrapperImpl; import net.pms.network.HTTPResource; import net.pms.util.CoverUtil; import net.pms.util.FileUtil; import net.pms.util.MpegUtil; import net.pms.util.ProcessUtil; import org.apache.sanselan.ImageInfo; import org.apache.sanselan.Sanselan; import org.apache.sanselan.common.IImageMetadata; import org.apache.sanselan.formats.jpeg.JpegImageMetadata; import org.apache.sanselan.formats.tiff.TiffField; import org.apache.sanselan.formats.tiff.constants.TiffConstants; import org.jaudiotagger.audio.AudioFile; import org.jaudiotagger.audio.AudioFileIO; import org.jaudiotagger.audio.AudioHeader; import org.jaudiotagger.tag.FieldKey; import org.jaudiotagger.tag.Tag; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.imageio.IIOException; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.*; import java.util.List; import static org.apache.commons.lang3.StringUtils.*; /** * This class keeps track of media file metadata scanned by the MediaInfo library. * * TODO: Change all instance variables to private. For backwards compatibility * with external plugin code the variables have all been marked as deprecated * instead of changed to private, but this will surely change in the future. * When everything has been changed to private, the deprecated note can be * removed. */ public class DLNAMediaInfo implements Cloneable { private static final Logger logger = LoggerFactory.getLogger(DLNAMediaInfo.class); private static final String THUMBNAIL_DIRECTORY_NAME = "thumbs"; private static final PmsConfiguration configuration = PMS.getConfiguration(); /** * XXX: Not sure why this magical number is relevant. It is only used in * constructs like <code>lowRange != DLNAMediaInfo.ENDFILE_POS</code>, but * dig deeper and you will find that <code>lowRange</code> is never set to * that specific value. So unless there is a (particular?) renderer that * uses this exact value when it does not want to receive any bytes, we * might be better off deleting this. */ public static final long ENDFILE_POS = 99999475712L; /** * Maximum size of a stream, taking into account that some renderers (like * the PS3) will convert this <code>long</code> to <code>int</code>. * Truncating this value will still return the maximum value that an * <code>int</code> can contain. */ public static final long TRANS_SIZE = Long.MAX_VALUE - Integer.MAX_VALUE - 1; // Stored in database private Double durationSec; private static final Map<String, Integer> audioChannelLayout = new HashMap<String, Integer>(); // map ffmpeg's audio layout field to the corresponding number of channels // see: libavutil/channel_layout.c static { audioChannelLayout.put("mono", 1); audioChannelLayout.put("downmix", 2); audioChannelLayout.put("stereo", 2); audioChannelLayout.put("2.1", 3); audioChannelLayout.put("3.0", 3); audioChannelLayout.put("3.0(back)", 3); audioChannelLayout.put("4.0", 4); audioChannelLayout.put("quad", 4); audioChannelLayout.put("quad(side)", 4); audioChannelLayout.put("3.1", 4); audioChannelLayout.put("5.0", 5); audioChannelLayout.put("5.0(side)", 5); audioChannelLayout.put("4.1", 5); audioChannelLayout.put("5:1", 6); audioChannelLayout.put("5.1", 6); audioChannelLayout.put("5.1(side)", 6); audioChannelLayout.put("6.0", 6); audioChannelLayout.put("6.0(front)", 6); audioChannelLayout.put("hexagonal", 6); audioChannelLayout.put("6.1", 7); audioChannelLayout.put("6.1(front)", 7); audioChannelLayout.put("7.0", 7); audioChannelLayout.put("7.0(front)", 7); audioChannelLayout.put("7.1", 8); audioChannelLayout.put("7.1(wide)", 8); audioChannelLayout.put("octagonal", 8); } /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public int bitrate; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public int width; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public int height; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public long size; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String codecV; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String frameRate; private String frameRateMode; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String aspect; public String aspectRatioContainer; public String aspectRatioVideoTrack; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public byte thumb[]; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String mimeType; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public int bitsPerPixel; private byte referenceFrameCount = -1; private String avcLevel = null; private List<DLNAMediaAudio> audioTracks = new ArrayList<DLNAMediaAudio>(); private List<DLNAMediaSubtitle> subtitleTracks = new ArrayList<DLNAMediaSubtitle>(); /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String model; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public int exposure; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public int orientation; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public int iso; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String muxingMode; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String muxingModeAudio; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String container; /** * @deprecated Use {@link #getH264AnnexB()} and {@link #setH264AnnexB(byte[])} to access this variable. */ @Deprecated public byte[] h264_annexB; /** * Not stored in database. * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public boolean mediaparsed; /** * isMediaParserV2 related, used to manage thumbnail management separated * from the main parsing process. * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public boolean thumbready; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public int dvdtrack; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public boolean secondaryFormatValid = true; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public boolean parsing = false; private boolean ffmpeg_failure; private boolean ffmpeg_annexb_failure; private Map<String, String> extras; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public boolean encrypted; /** * Used to determine whether tsMuxeR can mux the file instead of transcoding. * Also used by DLNAResource to help determine the DLNA.ORG_PN (file type) * value to send to the renderer, which is confusing. * * Some of this code is repeated in isVideoWithinH264LevelLimits(), and since * both functions are sometimes (but not always) used together, this is * not an efficient use of code. * TODO: Fix the above situation. * TODO: Now that FFmpeg is muxing without tsMuxeR, we should make a separate * function for that, or even better, re-think this whole approach. */ public boolean isMuxable(RendererConfiguration mediaRenderer) { boolean muxable = false; // Allow the formats in the following list if ( getContainer() != null && ( getContainer().equals("mkv") || getContainer().equals("mp4") || getContainer().equals("mov") || getContainer().equals("ts") || getContainer().equals("m2ts") ) ) { if ( getCodecV() != null && ( getCodecV().equals("h264") || getCodecV().equals("vc1") || getCodecV().equals("mpeg2") ) ) { if (getFirstAudioTrack() != null) { String codecA; codecA = getFirstAudioTrack().getCodecA(); if ( codecA != null && ( codecA.equals("aac") || codecA.equals("ac3") || codecA.equals("dca") || codecA.equals("dts") || codecA.equals("eac3") ) ) { muxable = true; } } } } // Temporary fix: MediaInfo support will take care of this in the future // For now, http://ps3mediaserver.org/forum/viewtopic.php?f=11&t=6361&start=0 // Bravia does not support AVC video at less than 288px high if (mediaRenderer.isBRAVIA() && getHeight() < 288) { muxable = false; } return muxable; } public Map<String, String> getExtras() { return extras; } public void putExtra(String key, String value) { if (extras == null) { extras = new HashMap<String, String>(); } extras.put(key, value); } public String getExtrasAsString() { if (extras == null) { return null; } StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> entry : extras.entrySet()) { sb.append(entry.getKey()); sb.append("|"); sb.append(entry.getValue()); sb.append("|"); } return sb.toString(); } public void setExtrasAsString(String value) { if (value != null) { StringTokenizer st = new StringTokenizer(value, "|"); while (st.hasMoreTokens()) { try { putExtra(st.nextToken(), st.nextToken()); } catch (NoSuchElementException nsee) { logger.debug("Caught exception", nsee); } } } } public DLNAMediaInfo() { setThumbready(true); // this class manages thumbnails by default with the parser_v1 method } public void generateThumbnail(InputFile input, Format ext, int type) { DLNAMediaInfo forThumbnail = new DLNAMediaInfo(); forThumbnail.durationSec = durationSec; forThumbnail.parse(input, ext, type, true); setThumb(forThumbnail.getThumb()); } private ProcessWrapperImpl getFFmpegThumbnail(InputFile media) { String args[] = new String[14]; args[0] = getFfmpegPath(); boolean dvrms = media.getFile() != null && media.getFile().getAbsolutePath().toLowerCase().endsWith("dvr-ms"); if (dvrms && isNotBlank(configuration.getFfmpegAlternativePath())) { args[0] = configuration.getFfmpegAlternativePath(); } args[1] = "-ss"; args[2] = "" + configuration.getThumbnailSeekPos(); args[3] = "-i"; if (media.getFile() != null) { args[4] = ProcessUtil.getShortFileNameIfWideChars(media.getFile().getAbsolutePath()); } else { args[4] = "-"; } args[5] = "-an"; args[6] = "-an"; args[7] = "-s"; args[8] = "320x180"; args[9] = "-vframes"; args[10] = "1"; args[11] = "-f"; args[12] = "image2"; args[13] = "pipe:"; // FIXME MPlayer should not be used if thumbnail generation is disabled (and it should be disabled in the GUI) if (!configuration.isThumbnailGenerationEnabled() || (configuration.isUseMplayerForVideoThumbs() && !dvrms)) { args[2] = "0"; for (int i = 5; i <= 13; i++) { args[i] = "-an"; } } OutputParams params = new OutputParams(configuration); params.maxBufferSize = 1; params.stdin = media.getPush(); params.noexitcheck = true; // not serious if anything happens during the thumbnailer // true: consume stderr on behalf of the caller i.e. parse() final ProcessWrapperImpl pw = new ProcessWrapperImpl(args, params, false, true); // FAILSAFE setParsing(true); Runnable r = new Runnable() { @Override public void run() { try { Thread.sleep(10000); ffmpeg_failure = true; } catch (InterruptedException e) { } pw.stopProcess(); setParsing(false); } }; Thread failsafe = new Thread(r, "FFmpeg Thumbnail Failsafe"); failsafe.start(); pw.runInSameThread(); setParsing(false); return pw; } private ProcessWrapperImpl getMplayerThumbnail(InputFile media) throws IOException { String args[] = new String[14]; args[0] = configuration.getMplayerPath(); args[1] = "-ss"; boolean toolong = getDurationInSeconds() < configuration.getThumbnailSeekPos(); args[2] = "" + (toolong ? (getDurationInSeconds() / 2) : configuration.getThumbnailSeekPos()); args[3] = "-quiet"; if (media.getFile() != null) { args[4] = ProcessUtil.getShortFileNameIfWideChars(media.getFile().getAbsolutePath()); } else { args[4] = "-"; } args[5] = "-msglevel"; args[6] = "all=4"; args[7] = "-vf"; args[8] = "scale=320:-2,expand=:180"; args[9] = "-frames"; args[10] = "1"; args[11] = "-vo"; String frameName = "" + media.hashCode(); frameName = "mplayer_thumbs:subdirs=\"" + frameName + "\""; frameName = frameName.replace(',', '_'); args[12] = "jpeg:outdir=" + frameName; args[13] = "-nosound"; OutputParams params = new OutputParams(configuration); params.workDir = configuration.getTempFolder(); params.maxBufferSize = 1; params.stdin = media.getPush(); params.log = true; params.noexitcheck = true; // not serious if anything happens during the thumbnailer final ProcessWrapperImpl pw = new ProcessWrapperImpl(args, params); // FAILSAFE setParsing(true); Runnable r = new Runnable() { @Override public void run() { try { Thread.sleep(3000); //mplayer_thumb_failure = true; } catch (InterruptedException e) { } pw.stopProcess(); setParsing(false); } }; Thread failsafe = new Thread(r, "MPlayer Thumbnail Failsafe"); failsafe.start(); pw.runInSameThread(); setParsing(false); return pw; } private String getFfmpegPath() { String value = configuration.getFfmpegPath(); if (value == null) { logger.info("No FFmpeg - unable to thumbnail"); throw new RuntimeException("No FFmpeg - unable to thumbnail"); } else { return value; } } public void parse(InputFile inputFile, Format ext, int type, boolean thumbOnly) { int i = 0; while (isParsing()) { if (i == 5) { setMediaparsed(true); break; } try { Thread.sleep(1000); } catch (InterruptedException e) { } i++; } if (isMediaparsed()) { return; } if (inputFile != null) { if (inputFile.getFile() != null) { setSize(inputFile.getFile().length()); } else { setSize(inputFile.getSize()); } ProcessWrapperImpl pw = null; boolean ffmpeg_parsing = true; if (type == Format.AUDIO || ext instanceof AudioAsVideo) { ffmpeg_parsing = false; DLNAMediaAudio audio = new DLNAMediaAudio(); if (inputFile.getFile() != null) { try { AudioFile af = AudioFileIO.read(inputFile.getFile()); AudioHeader ah = af.getAudioHeader(); if (ah != null && !thumbOnly) { int length = ah.getTrackLength(); int rate = ah.getSampleRateAsNumber(); if (ah.getEncodingType().toLowerCase().contains("flac 24")) { audio.setBitsperSample(24); } audio.setSampleFrequency("" + rate); setDuration((double) length); setBitrate((int) ah.getBitRateAsNumber()); audio.getAudioProperties().setNumberOfChannels(2); if (ah.getChannels() != null && ah.getChannels().toLowerCase().contains("mono")) { audio.getAudioProperties().setNumberOfChannels(1); } else if (ah.getChannels() != null && ah.getChannels().toLowerCase().contains("stereo")) { audio.getAudioProperties().setNumberOfChannels(2); } else if (ah.getChannels() != null) { audio.getAudioProperties().setNumberOfChannels(Integer.parseInt(ah.getChannels())); } audio.setCodecA(ah.getEncodingType().toLowerCase()); if (audio.getCodecA().contains("(windows media")) { audio.setCodecA(audio.getCodecA().substring(0, audio.getCodecA().indexOf("(windows media")).trim()); } } Tag t = af.getTag(); if (t != null) { if (t.getArtworkList().size() > 0) { setThumb(t.getArtworkList().get(0).getBinaryData()); } else { if (configuration.getAudioThumbnailMethod() > 0) { setThumb( CoverUtil.get().getThumbnailFromArtistAlbum( configuration.getAudioThumbnailMethod() == 1 ? CoverUtil.AUDIO_AMAZON : CoverUtil.AUDIO_DISCOGS, audio.getArtist(), audio.getAlbum() ) ); } } if (!thumbOnly) { audio.setAlbum(t.getFirst(FieldKey.ALBUM)); audio.setArtist(t.getFirst(FieldKey.ARTIST)); audio.setSongname(t.getFirst(FieldKey.TITLE)); String y = t.getFirst(FieldKey.YEAR); try { if (y.length() > 4) { y = y.substring(0, 4); } audio.setYear(Integer.parseInt(((y != null && y.length() > 0) ? y : "0"))); y = t.getFirst(FieldKey.TRACK); audio.setTrack(Integer.parseInt(((y != null && y.length() > 0) ? y : "1"))); audio.setGenre(t.getFirst(FieldKey.GENRE)); } catch (Throwable e) { logger.debug("Error parsing unimportant metadata: " + e.getMessage()); } } } } catch (Throwable e) { logger.debug("Error parsing audio file: {} - {}", e.getMessage(), e.getCause() != null ? e.getCause().getMessage() : ""); ffmpeg_parsing = false; } if (audio.getSongname() == null || audio.getSongname().length() == 0) { audio.setSongname(inputFile.getFile().getName()); } if (!ffmpeg_parsing) { getAudioTracksList().add(audio); } } } if (type == Format.IMAGE && inputFile.getFile() != null) { try { ffmpeg_parsing = false; ImageInfo info = Sanselan.getImageInfo(inputFile.getFile()); setWidth(info.getWidth()); setHeight(info.getHeight()); setBitsPerPixel(info.getBitsPerPixel()); String formatName = info.getFormatName(); if (formatName.startsWith("JPEG")) { setCodecV("jpg"); IImageMetadata meta = Sanselan.getMetadata(inputFile.getFile()); if (meta != null && meta instanceof JpegImageMetadata) { JpegImageMetadata jpegmeta = (JpegImageMetadata) meta; TiffField tf = jpegmeta.findEXIFValue(TiffConstants.EXIF_TAG_MODEL); if (tf != null) { setModel(tf.getStringValue().trim()); } tf = jpegmeta.findEXIFValue(TiffConstants.EXIF_TAG_EXPOSURE_TIME); if (tf != null) { setExposure((int) (1000 * tf.getDoubleValue())); } tf = jpegmeta.findEXIFValue(TiffConstants.EXIF_TAG_ORIENTATION); if (tf != null) { setOrientation(tf.getIntValue()); } tf = jpegmeta.findEXIFValue(TiffConstants.EXIF_TAG_ISO); if (tf != null) { // Galaxy Nexus jpg pictures may contain multiple values, take the first int[] isoValues = tf.getIntArrayValue(); setIso(isoValues[0]); } } } else if (formatName.startsWith("PNG")) { setCodecV("png"); } else if (formatName.startsWith("GIF")) { setCodecV("gif"); } else if (formatName.startsWith("TIF")) { setCodecV("tiff"); } setContainer(getCodecV()); } catch (Throwable e) { logger.info("Error parsing image ({}) with Sanselan, switching to FFmpeg.", inputFile.getFile().getAbsolutePath()); } } if (configuration.getImageThumbnailsEnabled() && type != Format.VIDEO && type != Format.AUDIO) { try { File thumbDir = new File(configuration.getTempFolder(), THUMBNAIL_DIRECTORY_NAME); logger.trace("Generating thumbnail for: {}", inputFile.getFile().getAbsolutePath()); if (!thumbDir.exists() && !thumbDir.mkdirs()) { logger.warn("Could not create thumbnail directory: {}", thumbDir.getAbsolutePath()); } else { File thumbFile = new File(thumbDir, inputFile.getFile().getName() + ".jpg"); String thumbFilename = thumbFile.getAbsolutePath(); logger.trace("Creating (temporary) thumbnail: {}", thumbFilename); // Create the thumbnail image using the Thumbnailator library final Builder<File> thumbnail = Thumbnails.of(inputFile.getFile()); thumbnail.size(320, 180); thumbnail.outputFormat("jpg"); thumbnail.outputQuality(1.0f); try { thumbnail.toFile(thumbFilename); } catch (IIOException e) { logger.debug("Error generating thumbnail for: " + inputFile.getFile().getName()); logger.debug("The full error was: " + e); } File jpg = new File(thumbFilename); if (jpg.exists()) { InputStream is = new FileInputStream(jpg); int sz = is.available(); if (sz > 0) { setThumb(new byte[sz]); is.read(getThumb()); } is.close(); if (!jpg.delete()) { jpg.deleteOnExit(); } } } } catch (UnsupportedFormatException ufe) { logger.debug("Thumbnailator does not support the format of {}: {}", inputFile.getFile().getAbsolutePath(), ufe.getMessage()); } catch (Exception e) { logger.debug("Thumbnailator could not generate a thumbnail for: {}", inputFile.getFile().getAbsolutePath(), e); } } if (ffmpeg_parsing) { if (!thumbOnly || !configuration.isUseMplayerForVideoThumbs()) { pw = getFFmpegThumbnail(inputFile); } String input = "-"; boolean dvrms = false; if (inputFile.getFile() != null) { input = ProcessUtil.getShortFileNameIfWideChars(inputFile.getFile().getAbsolutePath()); dvrms = inputFile.getFile().getAbsolutePath().toLowerCase().endsWith("dvr-ms"); } if (!ffmpeg_failure && !thumbOnly) { if (input.equals("-")) { input = "pipe:"; } boolean matchs = false; ArrayList<String> lines = (ArrayList<String>) pw.getResults(); int langId = 0; int subId = 0; ListIterator<String> FFmpegMetaData = lines.listIterator(); for (String line : lines) { FFmpegMetaData.next(); line = line.trim(); if (line.startsWith("Output")) { matchs = false; } else if (line.startsWith("Input")) { if (line.indexOf(input) > -1) { matchs = true; setContainer(line.substring(10, line.indexOf(",", 11)).trim()); } else { matchs = false; } } else if (matchs) { if (line.indexOf("Duration") > -1) { StringTokenizer st = new StringTokenizer(line, ","); while (st.hasMoreTokens()) { String token = st.nextToken().trim(); if (token.startsWith("Duration: ")) { String durationStr = token.substring(10); int l = durationStr.substring(durationStr.indexOf(".") + 1).length(); if (l < 4) { durationStr = durationStr + "00".substring(0, 3 - l); } if (durationStr.indexOf("N/A") > -1) { setDuration(null); } else { setDuration(parseDurationString(durationStr)); } } else if (token.startsWith("bitrate: ")) { String bitr = token.substring(9); int spacepos = bitr.indexOf(" "); if (spacepos > -1) { String value = bitr.substring(0, spacepos); String unit = bitr.substring(spacepos + 1); setBitrate(Integer.parseInt(value)); if (unit.equals("kb/s")) { setBitrate(1024 * getBitrate()); } if (unit.equals("mb/s")) { setBitrate(1048576 * getBitrate()); } } } } } else if (line.indexOf("Audio:") > -1) { StringTokenizer st = new StringTokenizer(line, ","); int a = line.indexOf("("); int b = line.indexOf("):", a); DLNAMediaAudio audio = new DLNAMediaAudio(); audio.setId(langId++); if (a > -1 && b > a) { audio.setLang(line.substring(a + 1, b)); } else { audio.setLang(DLNAMediaLang.UND); } // Get TS IDs a = line.indexOf("[0x"); b = line.indexOf("]", a); if (a > -1 && b > a + 3) { String idString = line.substring(a + 3, b); try { audio.setId(Integer.parseInt(idString, 16)); } catch (NumberFormatException nfe) { logger.debug("Error parsing Stream ID: " + idString); } } while (st.hasMoreTokens()) { String token = st.nextToken().trim(); Integer nChannels; if (token.startsWith("Stream")) { audio.setCodecA(token.substring(token.indexOf("Audio: ") + 7)); } else if (token.endsWith("Hz")) { audio.setSampleFrequency(token.substring(0, token.indexOf("Hz")).trim()); } else if ((nChannels = audioChannelLayout.get(token)) != null) { audio.getAudioProperties().setNumberOfChannels(nChannels); } else if (token.matches("\\d+(?:\\s+channels?)")) { // implicitly anchored at both ends e.g. ^ ... $ // setNumberOfChannels(String) parses the number out of the string audio.getAudioProperties().setNumberOfChannels(token); } else if (token.equals("s32")) { audio.setBitsperSample(32); } else if (token.equals("s24")) { audio.setBitsperSample(24); } else if (token.equals("s16")) { audio.setBitsperSample(16); } } int FFmpegMetaDataNr = FFmpegMetaData.nextIndex(); if (FFmpegMetaDataNr > -1) { line = lines.get(FFmpegMetaDataNr); } if (line.indexOf("Metadata:") > -1) { FFmpegMetaDataNr = FFmpegMetaDataNr + 1; line = lines.get(FFmpegMetaDataNr); while (line.indexOf(" ") == 0) { if (line.toLowerCase().indexOf("title :") > -1) { int aa = line.indexOf(": "); int bb = line.length(); if (aa > -1 && bb > aa) { audio.setFlavor(line.substring(aa+2, bb)); break; } } else { FFmpegMetaDataNr = FFmpegMetaDataNr + 1; line = lines.get(FFmpegMetaDataNr); } } } getAudioTracksList().add(audio); } else if (line.indexOf("Video:") > -1) { StringTokenizer st = new StringTokenizer(line, ","); while (st.hasMoreTokens()) { String token = st.nextToken().trim(); if (token.startsWith("Stream")) { setCodecV(token.substring(token.indexOf("Video: ") + 7)); } else if ((token.indexOf("tbc") > -1 || token.indexOf("tb(c)") > -1)) { // A/V sync issues with newest FFmpeg, due to the new tbr/tbn/tbc outputs // Priority to tb(c) String frameRateDoubleString = token.substring(0, token.indexOf("tb")).trim(); try { if (!frameRateDoubleString.equals(getFrameRate())) {// tbc taken into account only if different than tbr Double frameRateDouble = Double.parseDouble(frameRateDoubleString); setFrameRate(String.format(Locale.ENGLISH, "%.2f", frameRateDouble / 2)); } } catch (NumberFormatException nfe) { // Could happen if tbc is "1k" or something like that, no big deal logger.debug("Could not parse frame rate \"" + frameRateDoubleString + "\""); } } else if ((token.indexOf("tbr") > -1 || token.indexOf("tb(r)") > -1) && getFrameRate() == null) { setFrameRate(token.substring(0, token.indexOf("tb")).trim()); } else if ((token.indexOf("fps") > -1 || token.indexOf("fps(r)") > -1) && getFrameRate() == null) { // dvr-ms ? setFrameRate(token.substring(0, token.indexOf("fps")).trim()); } else if (token.indexOf("x") > -1) { String resolution = token.trim(); if (resolution.indexOf(" [") > -1) { resolution = resolution.substring(0, resolution.indexOf(" [")); } try { setWidth(Integer.parseInt(resolution.substring(0, resolution.indexOf("x")))); } catch (NumberFormatException nfe) { logger.debug("Could not parse width from \"" + resolution.substring(0, resolution.indexOf("x")) + "\""); } try { setHeight(Integer.parseInt(resolution.substring(resolution.indexOf("x") + 1))); } catch (NumberFormatException nfe) { logger.debug("Could not parse height from \"" + resolution.substring(resolution.indexOf("x") + 1) + "\""); } } } } else if (line.indexOf("Subtitle:") > -1 && !line.contains("tx3g")) { DLNAMediaSubtitle lang = new DLNAMediaSubtitle(); lang.setType((line.contains("dvdsub") && Platform.isWindows() ? SubtitleType.VOBSUB : SubtitleType.UNKNOWN)); int a = line.indexOf("("); int b = line.indexOf("):", a); if (a > -1 && b > a) { lang.setLang(line.substring(a + 1, b)); } else { lang.setLang(DLNAMediaLang.UND); } lang.setId(subId++); int FFmpegMetaDataNr = FFmpegMetaData.nextIndex(); if (FFmpegMetaDataNr > -1) { line = lines.get(FFmpegMetaDataNr); } if (line.indexOf("Metadata:") > -1) { FFmpegMetaDataNr = FFmpegMetaDataNr + 1; line = lines.get(FFmpegMetaDataNr); while (line.indexOf(" ") == 0) { if (line.toLowerCase().indexOf("title :") > -1) { int aa = line.indexOf(": "); int bb = line.length(); if (aa > -1 && bb > aa) { lang.setFlavor(line.substring(aa+2, bb)); break; } } else { FFmpegMetaDataNr = FFmpegMetaDataNr + 1; line = lines.get(FFmpegMetaDataNr); } } } getSubtitleTracksList().add(lang); } } } } if (!thumbOnly && getContainer() != null && inputFile.getFile() != null && getContainer().equals("mpegts") && isH264() && getDurationInSeconds() == 0) { // Parse the duration try { int length = MpegUtil.getDurationFromMpeg(inputFile.getFile()); if (length > 0) { setDuration((double) length); } } catch (IOException e) { logger.trace("Error retrieving length: " + e.getMessage()); } } if (configuration.isUseMplayerForVideoThumbs() && type == Format.VIDEO && !dvrms) { try { getMplayerThumbnail(inputFile); String frameName = "" + inputFile.hashCode(); frameName = configuration.getTempFolder() + "/mplayer_thumbs/" + frameName + "00000001/00000001.jpg"; frameName = frameName.replace(',', '_'); File jpg = new File(frameName); if (jpg.exists()) { InputStream is = new FileInputStream(jpg); int sz = is.available(); if (sz > 0) { setThumb(new byte[sz]); is.read(getThumb()); } is.close(); if (!jpg.delete()) { jpg.deleteOnExit(); } // Try and retry if (!jpg.getParentFile().delete() && !jpg.getParentFile().delete()) { logger.debug("Failed to delete \"" + jpg.getParentFile().getAbsolutePath() + "\""); } } } catch (IOException e) { logger.debug("Caught exception", e); } } if (type == Format.VIDEO && pw != null && getThumb() == null) { InputStream is; try { is = pw.getInputStream(0); int sz = is.available(); if (sz > 0) { setThumb(new byte[sz]); is.read(getThumb()); } is.close(); if (sz > 0 && !net.pms.PMS.isHeadless()) { BufferedImage image = ImageIO.read(new ByteArrayInputStream(getThumb())); if (image != null) { Graphics g = image.getGraphics(); g.setColor(Color.WHITE); g.setFont(new Font("Arial", Font.PLAIN, 14)); int low = 0; if (getWidth() > 0) { if (getWidth() == 1920 || getWidth() == 1440) { g.drawString("1080p", 0, low += 18); } else if (getWidth() == 1280) { g.drawString("720p", 0, low += 18); } } ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(image, "jpeg", out); setThumb(out.toByteArray()); } } } catch (IOException e) { logger.debug("Error while decoding thumbnail: " + e.getMessage()); } } } finalize(type, inputFile); setMediaparsed(true); } } public boolean isH264() { return getCodecV() != null && getCodecV().contains("264"); } public int getFrameNumbers() { double fr = Double.parseDouble(getFrameRate()); return (int) (getDurationInSeconds() * fr); } public void setDuration(Double d) { this.durationSec = d; } public Double getDuration() { return durationSec; } /** * @return 0 if nothing is specified, otherwise the duration */ public double getDurationInSeconds() { return durationSec != null ? durationSec : 0; } public String getDurationString() { return durationSec != null ? getDurationString(durationSec) : null; } public static String getDurationString(double d) { int s = ((int) d) % 60; int h = (int) (d / 3600); int m = ((int) (d / 60)) % 60; return String.format("%02d:%02d:%02d.00", h, m, s); } public static Double parseDurationString(String duration) { if (duration == null) { return null; } StringTokenizer st = new StringTokenizer(duration, ":"); try { int h = Integer.parseInt(st.nextToken()); int m = Integer.parseInt(st.nextToken()); double s = Double.parseDouble(st.nextToken()); return h * 3600 + m * 60 + s; } catch (NumberFormatException nfe) { logger.debug("Failed to parse duration \"" + duration + "\""); } return null; } public void finalize(int type, InputFile f) { String codecA = null; if (getFirstAudioTrack() != null) { codecA = getFirstAudioTrack().getCodecA(); } if (getContainer() != null && getContainer().equals("avi")) { setMimeType(HTTPResource.AVI_TYPEMIME); } else if (getContainer() != null && (getContainer().equals("asf") || getContainer().equals("wmv"))) { setMimeType(HTTPResource.WMV_TYPEMIME); } else if (getContainer() != null && (getContainer().equals("matroska") || getContainer().equals("mkv"))) { setMimeType(HTTPResource.MATROSKA_TYPEMIME); } else if (getCodecV() != null && getCodecV().equals("mjpeg")) { setMimeType(HTTPResource.JPEG_TYPEMIME); } else if ("png".equals(getCodecV()) || "png".equals(getContainer())) { setMimeType(HTTPResource.PNG_TYPEMIME); } else if ("gif".equals(getCodecV()) || "gif".equals(getContainer())) { setMimeType(HTTPResource.GIF_TYPEMIME); } else if (getCodecV() != null && (getCodecV().equals("h264") || getCodecV().equals("h263") || getCodecV().toLowerCase().equals("mpeg4") || getCodecV().toLowerCase().equals("mp4"))) { setMimeType(HTTPResource.MP4_TYPEMIME); } else if (getCodecV() != null && (getCodecV().indexOf("mpeg") > -1 || getCodecV().indexOf("mpg") > -1)) { setMimeType(HTTPResource.MPEG_TYPEMIME); } else if (getCodecV() == null && codecA != null && codecA.contains("mp3")) { setMimeType(HTTPResource.AUDIO_MP3_TYPEMIME); } else if (getCodecV() == null && contains(codecA, "aac")) { setMimeType(HTTPResource.AUDIO_MP4_TYPEMIME); } else if (getCodecV() == null && contains(codecA, "flac")) { setMimeType(HTTPResource.AUDIO_FLAC_TYPEMIME); } else if (getCodecV() == null && contains(codecA, "vorbis")) { setMimeType(HTTPResource.AUDIO_OGG_TYPEMIME); } else if (getCodecV() == null && (contains(codecA, "asf") || startsWith(codecA, "wm"))) { setMimeType(HTTPResource.AUDIO_WMA_TYPEMIME); } else if (getCodecV() == null && (contains(codecA, "wav") || startsWith(codecA, "pcm"))) { setMimeType(HTTPResource.AUDIO_WAV_TYPEMIME); } else { setMimeType(HTTPResource.getDefaultMimeType(type)); } if (getFirstAudioTrack() == null || !(type == Format.AUDIO && getFirstAudioTrack().getBitsperSample() == 24 && getFirstAudioTrack().getSampleRate() > 48000)) { setSecondaryFormatValid(false); } // Check for external subs here if (f.getFile() != null && type == Format.VIDEO && configuration.isAutoloadExternalSubtitles()) { FileUtil.isSubtitlesExists(f.getFile(), this); } } /** * Checks whether the video has too many reference frames per pixels for the renderer * TODO move to PlayerUtil */ public synchronized boolean isVideoWithinH264LevelLimits(InputFile f, RendererConfiguration mediaRenderer) { if ("h264".equals(getCodecV())) { if (getReferenceFrameCount() > -1) { logger.debug("H.264 file: {} level {} / ref frames {}", f.getFilename(), defaultString(getAvcLevel(), "N/A"), getReferenceFrameCount()); // shagrath : for tsmuxer to work natively with h264, we still need to retrieve some AVC header informations to emulate the h264_mp4toannexb filter in mencoder (via the H264InputStream class) // I would love for mencoder to support it natively but I don't think it will ever happens byte headers[][] = getAnnexBFrameHeader(f); if (ffmpeg_annexb_failure) { logger.info("Error parsing information from the file: " + f.getFilename()); } else setH264AnnexB(headers[1]); if (("4.1".equals(getAvcLevel()) || "4.2".equals(getAvcLevel()) || "5".equals(getAvcLevel()) || "5.0".equals(getAvcLevel()) || "5.1".equals(getAvcLevel()) || "5.2".equals(getAvcLevel())) && getWidth() > 0 && getHeight() > 0) { int maxref; if (mediaRenderer == null || mediaRenderer.isPS3()) { /** * 2013-01-25: Confirmed maximum reference frames on PS3: * - 4 for 1920x1080 * - 11 for 1280x720 * Meaning this math is correct */ maxref = (int) Math.floor(10252743 / (getWidth() * getHeight())); } else { /** * This is the math for level 4.1, which results in: * - 4 for 1920x1080 * - 9 for 1280x720 */ maxref = (int) Math.floor(8388608 / (getWidth() * getHeight())); } if (getReferenceFrameCount() > maxref) { logger.info("H.264 file ({}) is not compatible with this renderer because it can only take {} reference frames at this resolution while this file has {} reference frames.", f.getFilename(), maxref, getReferenceFrameCount()); return false; } } return true; } else { logger.warn("H.264 file ({}): Unparsed reference frame count. Remuxing may not work.", f.getFilename()); return true; } } else { logger.debug("Non-H.264 file ({}): Do not check ref limits.", f.getFilename()); return true; } } public boolean isMuxable(String filename, String codecA) { return codecA != null && (codecA.startsWith("dts") || codecA.equals("dca")); } public boolean isLossless(String codecA) { return codecA != null && (codecA.contains("pcm") || codecA.startsWith("dts") || codecA.equals("dca") || codecA.contains("flac")) && !codecA.contains("pcm_u8") && !codecA.contains("pcm_s8"); } @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("container: "); result.append(getContainer()); result.append(", bitrate: "); result.append(getBitrate()); result.append(", size: "); result.append(getSize()); result.append(", video codec: "); result.append(getCodecV()); result.append(", duration: "); result.append(getDurationString()); result.append(", width: "); result.append(getWidth()); result.append(", height: "); result.append(getHeight()); result.append(", frame rate: "); result.append(getFrameRate()); if (getThumb() != null) { result.append(", thumb size : "); result.append(getThumb().length); } result.append(", muxing mode: "); result.append(getMuxingMode()); result.append(", mime type: "); result.append(getMimeType()); for (DLNAMediaAudio audio : getAudioTracksList()) { result.append("\n\tAudio track "); result.append(audio.toString()); } for (DLNAMediaSubtitle sub : getSubtitleTracksList()) { result.append("\n\tSubtitle track "); result.append(sub.toString()); } return result.toString(); } public InputStream getThumbnailInputStream() { return new ByteArrayInputStream(getThumb()); } public String getValidFps(boolean ratios) { String validFrameRate = null; if (getFrameRate() != null && getFrameRate().length() > 0) { try { double fr = Double.parseDouble(getFrameRate().replace(',', '.')); if (fr >= 14.99 && fr < 15.1) { validFrameRate = "15"; } else if (fr > 23.9 && fr < 23.99) { validFrameRate = ratios ? "24000/1001" : "23.976"; } else if (fr > 23.99 && fr < 24.1) { validFrameRate = "24"; } else if (fr >= 24.99 && fr < 25.1) { validFrameRate = "25"; } else if (fr > 29.9 && fr < 29.99) { validFrameRate = ratios ? "30000/1001" : "29.97"; } else if (fr >= 29.99 && fr < 30.1) { validFrameRate = "30"; } else if (fr > 47.9 && fr < 47.99) { validFrameRate = ratios ? "48000/1001" : "47.952"; } else if (fr > 49.9 && fr < 50.1) { validFrameRate = "50"; } else if (fr > 59.8 && fr < 59.99) { validFrameRate = ratios ? "60000/1001" : "59.94"; } else if (fr >= 59.99 && fr < 60.1) { validFrameRate = "60"; } } catch (NumberFormatException nfe) { logger.error(null, nfe); } } return validFrameRate; } public DLNAMediaAudio getFirstAudioTrack() { if (getAudioTracksList().size() > 0) { return getAudioTracksList().get(0); } return null; } public String getValidAspect(boolean ratios) { String a = null; if (getAspect() != null) { double ar = Double.parseDouble(getAspect()); if (ar > 1.7 && ar < 1.8) { a = ratios ? "16/9" : "1.777777777777777"; } if (ar > 1.3 && ar < 1.4) { a = ratios ? "4/3" : "1.333333333333333"; } } return a; } public String getResolution() { if (getWidth() > 0 && getHeight() > 0) { return getWidth() + "x" + getHeight(); } return null; } public int getRealVideoBitrate() { if (getBitrate() > 0) { return (getBitrate() / 8); } int realBitrate = 10000000; if (getDurationInSeconds() != 0) { realBitrate = (int) (getSize() / getDurationInSeconds()); } return realBitrate; } public boolean isHDVideo() { return (getWidth() > 1200 || getHeight() > 700); } public boolean isMpegTS() { return getContainer() != null && getContainer().equals("mpegts"); } public byte[][] getAnnexBFrameHeader(InputFile f) { String[] cmdArray = new String[14]; cmdArray[0] = configuration.getFfmpegPath(); cmdArray[1] = "-i"; if (f.getPush() == null && f.getFilename() != null) { cmdArray[2] = f.getFilename(); } else { cmdArray[2] = "-"; } cmdArray[3] = "-vframes"; cmdArray[4] = "1"; cmdArray[5] = "-vcodec"; cmdArray[6] = "copy"; cmdArray[7] = "-f"; cmdArray[8] = "h264"; cmdArray[9] = "-vbsf"; cmdArray[10] = "h264_mp4toannexb"; cmdArray[11] = "-an"; cmdArray[12] = "-y"; cmdArray[13] = "pipe:"; byte[][] returnData = new byte[2][]; OutputParams params = new OutputParams(configuration); params.maxBufferSize = 1; params.stdin = f.getPush(); final ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params); Runnable r = new Runnable() { @Override public void run() { try { Thread.sleep(3000); ffmpeg_annexb_failure = true; } catch (InterruptedException e) { } pw.stopProcess(); } }; Thread failsafe = new Thread(r, "FFmpeg AnnexB Frame Header Failsafe"); failsafe.start(); pw.runInSameThread(); if (ffmpeg_annexb_failure) { return null; } InputStream is; ByteArrayOutputStream baot = new ByteArrayOutputStream(); try { is = pw.getInputStream(0); byte b[] = new byte[4096]; int n; while ((n = is.read(b)) > 0) { baot.write(b, 0, n); } byte data[] = baot.toByteArray(); baot.close(); returnData[0] = data; is.close(); int kf = 0; for (int i = 3; i < data.length; i++) { if (data[i - 3] == 1 && (data[i - 2] & 37) == 37 && (data[i - 1] & -120) == -120) { kf = i - 2; break; } } int st = 0; boolean found = false; if (kf > 0) { for (int i = kf; i >= 5; i--) { if (data[i - 5] == 0 && data[i - 4] == 0 && data[i - 3] == 0 && (data[i - 2] & 1) == 1 && (data[i - 1] & 39) == 39) { st = i - 5; found = true; break; } } } if (found) { byte header[] = new byte[kf - st]; System.arraycopy(data, st, header, 0, kf - st); returnData[1] = header; } } catch (IOException e) { logger.debug("Caught exception", e); } return returnData; } @Override protected Object clone() throws CloneNotSupportedException { Object cloned = super.clone(); if (cloned instanceof DLNAMediaInfo) { DLNAMediaInfo mediaCloned = ((DLNAMediaInfo) cloned); mediaCloned.setAudioTracksList(new ArrayList<DLNAMediaAudio>()); for (DLNAMediaAudio audio : getAudioTracksList()) { mediaCloned.getAudioTracksList().add((DLNAMediaAudio) audio.clone()); } mediaCloned.setSubtitleTracksList(new ArrayList<DLNAMediaSubtitle>()); for (DLNAMediaSubtitle sub : getSubtitleTracksList()) { mediaCloned.getSubtitleTracksList().add((DLNAMediaSubtitle) sub.clone()); } } return cloned; } /** * @return the bitrate * @since 1.50.0 */ public int getBitrate() { return bitrate; } /** * @param bitrate the bitrate to set * @since 1.50.0 */ public void setBitrate(int bitrate) { this.bitrate = bitrate; } /** * @return the width * @since 1.50.0 */ public int getWidth() { return width; } /** * @param width the width to set * @since 1.50.0 */ public void setWidth(int width) { this.width = width; } /** * @return the height * @since 1.50.0 */ public int getHeight() { return height; } /** * @param height the height to set * @since 1.50.0 */ public void setHeight(int height) { this.height = height; } /** * @return the size * @since 1.50.0 */ public long getSize() { return size; } /** * @param size the size to set * @since 1.50.0 */ public void setSize(long size) { this.size = size; } /** * @return the codecV * @since 1.50.0 */ public String getCodecV() { return codecV; } /** * @param codecV the codecV to set * @since 1.50.0 */ public void setCodecV(String codecV) { this.codecV = codecV; } /** * @return the frameRate * @since 1.50.0 */ public String getFrameRate() { return frameRate; } /** * @param frameRate the frameRate to set * @since 1.50.0 */ public void setFrameRate(String frameRate) { this.frameRate = frameRate; } /** * @return the frameRateMode * @since 1.55.0 */ public String getFrameRateMode() { return frameRateMode; } /** * @param frameRateMode the frameRateMode to set * @since 1.55.0 */ public void setFrameRateMode(String frameRateMode) { this.frameRateMode = frameRateMode; } /** * @return the aspect * @since 1.50.0 */ public String getAspect() { return aspect; } /** * @param aspect the aspect to set * @since 1.50.0 */ public void setAspect(String aspect) { this.aspect = aspect; } /** * @return the aspect ratio reported by the container */ public String getAspectRatioContainer() { return aspectRatioContainer; } /** * @param aspect the aspect ratio to set */ public void setAspectRatioContainer(String aspect) { this.aspectRatioContainer = aspect; } /** * @return the aspect ratio of the video track */ public String getAspectRatioVideoTrack() { return aspectRatioVideoTrack; } /** * @param aspect the aspect ratio to set */ public void setAspectRatioVideoTrack(String aspect) { this.aspectRatioVideoTrack = aspect; } /** * Check if media has different Aspect Ratios for container and video track. * * @return true if container's AR is set and differs from video AR, false otherwise. */ public boolean isAspectRatioMismatch() { return isNotBlank(getAspectRatioContainer()) && !equalsIgnoreCase(getAspectRatioContainer(), getAspectRatioVideoTrack()); } /** * @return the thumb * @since 1.50.0 */ public byte[] getThumb() { return thumb; } /** * @param thumb the thumb to set * @since 1.50.0 */ public void setThumb(byte[] thumb) { this.thumb = thumb; } /** * @return the mimeType * @since 1.50.0 */ public String getMimeType() { return mimeType; } /** * @param mimeType the mimeType to set * @since 1.50.0 */ public void setMimeType(String mimeType) { this.mimeType = mimeType; } /** * @return the bitsPerPixel * @since 1.50.0 */ public int getBitsPerPixel() { return bitsPerPixel; } /** * @param bitsPerPixel the bitsPerPixel to set * @since 1.50.0 */ public void setBitsPerPixel(int bitsPerPixel) { this.bitsPerPixel = bitsPerPixel; } /** * @return reference frame count for video stream or {@code -1} if not parsed. */ public synchronized byte getReferenceFrameCount() { return referenceFrameCount; } /** * Sets reference frame count for video stream or {@code -1} if not parsed. * * @param referenceFrameCount reference frame count. */ public synchronized void setReferenceFrameCount(byte referenceFrameCount) { if (referenceFrameCount < -1) { throw new IllegalArgumentException("referenceFrameCount should be >= -1."); } this.referenceFrameCount = referenceFrameCount; } /** * @return AVC level for video stream or {@code null} if not parsed. */ public synchronized String getAvcLevel() { return avcLevel; } /** * Sets AVC level for video stream or {@code null} if not parsed. * * @param avcLevel AVC level. */ public synchronized void setAvcLevel(String avcLevel) { this.avcLevel = avcLevel; } /** * @return the audioTracks * @since 1.60.0 */ // TODO (breaking change): rename to getAudioTracks public List<DLNAMediaAudio> getAudioTracksList() { return audioTracks; } /** * @return the audioTracks * @deprecated use getAudioTracksList() instead */ @Deprecated public ArrayList<DLNAMediaAudio> getAudioCodes() { if (audioTracks instanceof ArrayList) { return (ArrayList<DLNAMediaAudio>) audioTracks; } else { return new ArrayList<DLNAMediaAudio>(); } } /** * @param audioTracks the audioTracks to set * @since 1.60.0 */ // TODO (breaking change): rename to setAudioTracks public void setAudioTracksList(List<DLNAMediaAudio> audioTracks) { this.audioTracks = audioTracks; } /** * @param audioTracks the audioTracks to set * @deprecated use setAudioTracksList(ArrayList<DLNAMediaAudio> audioTracks) instead */ @Deprecated public void setAudioCodes(List<DLNAMediaAudio> audioTracks) { setAudioTracksList(audioTracks); } /** * @return the subtitleTracks * @since 1.60.0 */ // TODO (breaking change): rename to getSubtitleTracks public List<DLNAMediaSubtitle> getSubtitleTracksList() { return subtitleTracks; } /** * @return the subtitleTracks * @deprecated use getSubtitleTracksList() instead */ @Deprecated public ArrayList<DLNAMediaSubtitle> getSubtitlesCodes() { if (subtitleTracks instanceof ArrayList) { return (ArrayList<DLNAMediaSubtitle>) subtitleTracks; } else { return new ArrayList<DLNAMediaSubtitle>(); } } /** * @param subtitleTracks the subtitleTracks to set * @since 1.60.0 */ // TODO (breaking change): rename to setSubtitleTracks public void setSubtitleTracksList(List<DLNAMediaSubtitle> subtitleTracks) { this.subtitleTracks = subtitleTracks; } /** * @param subtitleTracks the subtitleTracks to set * @deprecated use setSubtitleTracksList(ArrayList<DLNAMediaSubtitle> subtitleTracks) instead */ @Deprecated public void setSubtitlesCodes(List<DLNAMediaSubtitle> subtitleTracks) { setSubtitleTracksList(subtitleTracks); } /** * @return the model * @since 1.50.0 */ public String getModel() { return model; } /** * @param model the model to set * @since 1.50.0 */ public void setModel(String model) { this.model = model; } /** * @return the exposure * @since 1.50.0 */ public int getExposure() { return exposure; } /** * @param exposure the exposure to set * @since 1.50.0 */ public void setExposure(int exposure) { this.exposure = exposure; } /** * @return the orientation * @since 1.50.0 */ public int getOrientation() { return orientation; } /** * @param orientation the orientation to set * @since 1.50.0 */ public void setOrientation(int orientation) { this.orientation = orientation; } /** * @return the iso * @since 1.50.0 */ public int getIso() { return iso; } /** * @param iso the iso to set * @since 1.50.0 */ public void setIso(int iso) { this.iso = iso; } /** * @return the muxingMode * @since 1.50.0 */ public String getMuxingMode() { return muxingMode; } /** * @param muxingMode the muxingMode to set * @since 1.50.0 */ public void setMuxingMode(String muxingMode) { this.muxingMode = muxingMode; } /** * @return the muxingModeAudio * @since 1.50.0 */ public String getMuxingModeAudio() { return muxingModeAudio; } /** * @param muxingModeAudio the muxingModeAudio to set * @since 1.50.0 */ public void setMuxingModeAudio(String muxingModeAudio) { this.muxingModeAudio = muxingModeAudio; } /** * @return the container * @since 1.50.0 */ public String getContainer() { return container; } /** * @param container the container to set * @since 1.50.0 */ public void setContainer(String container) { this.container = container; } /** * @return the h264_annexB * @since 1.50.0 */ public byte[] getH264AnnexB() { return h264_annexB; } /** * @param h264AnnexB the h264_annexB to set * @since 1.50.0 */ public void setH264AnnexB(byte[] h264AnnexB) { this.h264_annexB = h264AnnexB; } /** * @return the mediaparsed * @since 1.50.0 */ public boolean isMediaparsed() { return mediaparsed; } /** * @param mediaparsed the mediaparsed to set * @since 1.50.0 */ public void setMediaparsed(boolean mediaparsed) { this.mediaparsed = mediaparsed; } /** * @return the thumbready * @since 1.50.0 */ public boolean isThumbready() { return thumbready; } /** * @param thumbready the thumbready to set * @since 1.50.0 */ public void setThumbready(boolean thumbready) { this.thumbready = thumbready; } /** * @return the dvdtrack * @since 1.50.0 */ public int getDvdtrack() { return dvdtrack; } /** * @param dvdtrack the dvdtrack to set * @since 1.50.0 */ public void setDvdtrack(int dvdtrack) { this.dvdtrack = dvdtrack; } /** * @return the secondaryFormatValid * @since 1.50.0 */ public boolean isSecondaryFormatValid() { return secondaryFormatValid; } /** * @param secondaryFormatValid the secondaryFormatValid to set * @since 1.50.0 */ public void setSecondaryFormatValid(boolean secondaryFormatValid) { this.secondaryFormatValid = secondaryFormatValid; } /** * @return the parsing * @since 1.50.0 */ public boolean isParsing() { return parsing; } /** * @param parsing the parsing to set * @since 1.50.0 */ public void setParsing(boolean parsing) { this.parsing = parsing; } /** * @return the encrypted * @since 1.50.0 */ public boolean isEncrypted() { return encrypted; } /** * @param encrypted the encrypted to set * @since 1.50.0 */ public void setEncrypted(boolean encrypted) { this.encrypted = encrypted; } }