/* * 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 java.io.*; import java.nio.file.Files; import java.util.*; import java.util.concurrent.locks.ReentrantReadWriteLock; import net.pms.PMS; import net.pms.configuration.FormatConfiguration; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import net.pms.formats.AudioAsVideo; import net.pms.formats.Format; import net.pms.formats.Format.Identifier; import net.pms.formats.v2.SubtitleType; import net.pms.image.ExifInfo; import net.pms.image.ExifOrientation; import net.pms.image.ImageFormat; import net.pms.image.ImageInfo; import net.pms.image.ImagesUtil; import net.pms.image.ImagesUtil.ScaleType; import net.pms.io.OutputParams; import net.pms.io.ProcessWrapperImpl; import net.pms.network.HTTPResource; import net.pms.util.CoverSupplier; import net.pms.util.CoverUtil; import net.pms.util.FileUtil; import net.pms.util.MpegUtil; import net.pms.util.ProcessUtil; import net.pms.util.UnknownFormatException; import static net.pms.util.StringUtil.*; import static org.apache.commons.lang3.StringUtils.isBlank; import static org.apache.commons.lang3.StringUtils.isNotBlank; import org.apache.commons.lang3.StringUtils; import org.jaudiotagger.audio.AudioFile; import org.jaudiotagger.audio.AudioFileIO; import org.jaudiotagger.audio.AudioHeader; import org.jaudiotagger.audio.exceptions.CannotReadException; import org.jaudiotagger.audio.exceptions.InvalidAudioFrameException; import org.jaudiotagger.audio.exceptions.ReadOnlyFileException; import org.jaudiotagger.logging.ErrorMessage; import org.jaudiotagger.tag.FieldKey; import org.jaudiotagger.tag.KeyNotFoundException; import org.jaudiotagger.tag.Tag; import org.jaudiotagger.tag.TagException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * 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 PmsConfiguration configuration = PMS.getConfiguration(); 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; private final Object videoWithinH264LevelLimitsLock = new Object(); private Boolean videoWithinH264LevelLimits = null; // Stored in database private Double durationSec; /** * @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; /** * The frame rate mode as read from the parser */ private String frameRateModeRaw; private String frameRateOriginal; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String aspect; public String aspectRatioDvdIso; public String aspectRatioContainer; public String aspectRatioVideoTrack; private int videoBitDepth = 8; private volatile DLNAThumbnail thumb = null; private volatile ImageInfo imageInfo = null; /** * @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 final ReentrantReadWriteLock referenceFrameCountLock = new ReentrantReadWriteLock(); private byte referenceFrameCount = -1; private final ReentrantReadWriteLock avcLevelLock = new ReentrantReadWriteLock(); private String avcLevel = null; private final Object h264ProfileLock = new Object(); private String h264Profile = null; private List<DLNAMediaAudio> audioTracks = new ArrayList<>(); private List<DLNAMediaSubtitle> subtitleTracks = new ArrayList<>(); private boolean externalSubsExist = false; public void setExternalSubsExist(boolean exist) { this.externalSubsExist = exist; } public boolean isExternalSubsExist() { return externalSubsExist; } private boolean externalSubsParsed = false; public void setExternalSubsParsed(boolean parsed) { this.externalSubsParsed = parsed; } public boolean isExternalSubsParsed() { return externalSubsParsed; } /** * @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; private final Object h264_annexBLock = new Object(); private byte[] h264_annexB; /** * Not stored in database. */ private volatile boolean mediaparsed; public boolean ffmpegparsed; /** * isUseMediaInfo-related, used to manage thumbnail management separated * from the main parsing process. * * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public volatile 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; private final Object parsingLock = new Object(); private boolean parsing = false; private final Object ffmpeg_failureLock = new Object(); private boolean ffmpeg_failure = false; private final Object ffmpeg_annexb_failureLock = new Object(); private boolean ffmpeg_annexb_failure; private boolean muxable; private Map<String, String> extras; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public boolean encrypted; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String matrixCoefficients; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String stereoscopy; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String fileTitleFromMetadata; /** * @deprecated Use standard getter and setter to access this variable. */ @Deprecated public String videoTrackTitleFromMetadata; private int videoTrackCount = 0; private int imageCount = 0; public int getVideoTrackCount() { return videoTrackCount; } public void setVideoTrackCount(int value) { videoTrackCount = value; } public int getAudioTrackCount() { return audioTracks.size(); } public int getImageCount() { return imageCount; } public void setImageCount(int value) { imageCount = value; } public int getSubTrackCount() { return subtitleTracks.size(); } public boolean isVideo() { return MediaType.VIDEO == getMediaType(); } public boolean isAudio() { return MediaType.AUDIO == getMediaType(); } public boolean hasAudio() { return audioTracks.size() > 0; } public MediaType getMediaType() { if (videoTrackCount > 0) { return MediaType.VIDEO; } switch (audioTracks.size()) { case 1 : return MediaType.AUDIO; case 0 : if (imageCount > 0) { return MediaType.IMAGE; } default : return MediaType.UNKNOWN; } } /** * @return true when there are subtitle tracks embedded in the media file. */ public boolean hasSubtitles() { return subtitleTracks.size() > 0; } public boolean isImage() { return MediaType.IMAGE == getMediaType(); } /** * Used to determine whether tsMuxeR can mux the file to the renderer * instead of transcoding. * Also used by DLNAResource to help determine the DLNA.ORG_PN (file type) * value to send to the renderer. * * 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. * * @param mediaRenderer The renderer we might mux to * * @return */ public boolean isMuxable(RendererConfiguration mediaRenderer) { // Make sure the file is H.264 video if (isH264()) { muxable = true; } // Check if the renderer supports the resolution of the video if ( ( mediaRenderer.isMaximumResolutionSpecified() && ( width > mediaRenderer.getMaxVideoWidth() || height > mediaRenderer.getMaxVideoHeight() ) ) || ( !mediaRenderer.isMuxNonMod4Resolution() && !isMod4() ) ) { muxable = false; } // 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() && height < 288) { muxable = false; } return muxable; } /** * Whether a file is a WEB-DL release. * * It's important for some devices like PS3 because WEB-DL files often have * some difference (possibly not starting on a keyframe or something to do with * SEI output from MEncoder, possibly something else) that makes the PS3 not * accept them when output from tsMuxeR via MEncoder. * * The above statement may not be applicable when using tsMuxeR via FFmpeg * so we should reappraise the situation if we make that change. * * It is unlikely it will return false-positives but it will return * false-negatives. * * @param filename the filename * @param params the file properties * * @return whether a file is a WEB-DL release */ public boolean isWebDl(String filename, OutputParams params) { // Check the filename if (filename.toLowerCase().replaceAll("\\-", "").contains("webdl")) { return true; } // Check the metadata if ( ( getFileTitleFromMetadata() != null && getFileTitleFromMetadata().toLowerCase().replaceAll("\\-", "").contains("webdl") ) || ( getVideoTrackTitleFromMetadata() != null && getVideoTrackTitleFromMetadata().toLowerCase().replaceAll("\\-", "").contains("webdl") ) || ( params.aid != null && params.aid.getAudioTrackTitleFromMetadata() != null && params.aid.getAudioTrackTitleFromMetadata().toLowerCase().replaceAll("\\-", "").contains("webdl") ) || ( params.sid != null && params.sid.getSubtitlesTrackTitleFromMetadata() != null && params.sid.getSubtitlesTrackTitleFromMetadata().toLowerCase().replaceAll("\\-", "").contains("webdl") ) ) { return true; } return false; } public Map<String, String> getExtras() { return extras; } public void putExtra(String key, String value) { if (extras == null) { extras = new HashMap<>(); } 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); } } } } @Deprecated public void generateThumbnail(InputFile input, Format ext, int type, Double seekPosition, boolean resume) { generateThumbnail(input, ext, type, seekPosition, resume, null); } public void generateThumbnail(InputFile input, Format ext, int type, Double seekPosition, boolean resume, RendererConfiguration renderer) { DLNAMediaInfo forThumbnail = new DLNAMediaInfo(); forThumbnail.setMediaparsed(mediaparsed); // check if file was already parsed by MediaInfo forThumbnail.setImageInfo(imageInfo); forThumbnail.durationSec = getDurationInSeconds(); if (seekPosition <= forThumbnail.durationSec) { forThumbnail.durationSec = seekPosition; } else { forThumbnail.durationSec /= 2; } forThumbnail.parse(input, ext, type, true, resume, renderer); thumb = forThumbnail.thumb; thumbready = true; } private ProcessWrapperImpl getFFmpegThumbnail(InputFile media, boolean resume, RendererConfiguration renderer) { /** * Note: The text output from FFmpeg is used by renderers that do * not use MediaInfo, so do not make any changes that remove or * minimize the amount of text given by FFmpeg here */ String args[] = new String[14]; args[0] = getFfmpegPath(); File file = media.getFile(); boolean dvrms = file != null && file.getAbsolutePath().toLowerCase().endsWith("dvr-ms"); if (dvrms && isNotBlank(configuration.getFfmpegAlternativePath())) { args[0] = configuration.getFfmpegAlternativePath(); } args[1] = "-ss"; if (resume) { args[2] = Integer.toString((int) getDurationInSeconds()); } else { args[2] = Integer.toString((int) Math.min(configuration.getThumbnailSeekPos(), getDurationInSeconds())); } args[3] = "-i"; if (file != null) { args[4] = ProcessUtil.getShortFileNameIfWideChars(file.getAbsolutePath()); } else { args[4] = "-"; } args[5] = "-an"; args[6] = "-an"; // Thumbnail resolution int thumbnailWidth = 320; int thumbnailHeight = 180; double thumbnailRatio = 1.78; boolean isThumbnailPadding = true; if (renderer != null) { thumbnailWidth = renderer.getThumbnailWidth(); thumbnailHeight = renderer.getThumbnailHeight(); thumbnailRatio = renderer.getThumbnailRatio(); isThumbnailPadding = renderer.isThumbnailPadding(); } if (isThumbnailPadding) { args[7] = "-vf"; args[8] = "scale='if(gt(a," + thumbnailRatio + ")," + thumbnailWidth + ",-1)':'if(gt(a," + thumbnailRatio + "),-1," + thumbnailHeight + ")', pad=" + thumbnailWidth + ":" + thumbnailHeight + ":(" + thumbnailWidth + "-iw)/2:(" + thumbnailHeight + "-ih)/2"; } else { args[7] = "-vf"; args[8] = "scale='if(gt(a," + thumbnailRatio + ")," + thumbnailWidth + ",-1)':'if(gt(a," + thumbnailRatio + "),-1," + thumbnailHeight + ")'"; } 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 if ( !configuration.isThumbnailGenerationEnabled() || renderer != null && !renderer.isThumbnails() || 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, true, params, false, true); // FAILSAFE synchronized (parsingLock) { parsing = true; } Runnable r = new Runnable() { @Override public void run() { try { Thread.sleep(10000); synchronized (ffmpeg_failureLock) { ffmpeg_failure = true; } } catch (InterruptedException e) { } pw.stopProcess(); synchronized (parsingLock) { parsing = false; } } }; Thread failsafe = new Thread(r, "FFmpeg Thumbnail Failsafe"); failsafe.start(); pw.runInSameThread(); synchronized (parsingLock) { parsing = false; } return pw; } private ProcessWrapperImpl getMplayerThumbnail(InputFile media, boolean resume, RendererConfiguration renderer) throws IOException { File file = media.getFile(); String args[] = new String[14]; args[0] = configuration.getMplayerPath(); args[1] = "-ss"; if (resume) { args[2] = "" + (int) getDurationInSeconds(); } else { args[2] = "" + configuration.getThumbnailSeekPos(); } args[3] = "-quiet"; if (file != null) { args[4] = ProcessUtil.getShortFileNameIfWideChars(file.getAbsolutePath()); } else { args[4] = "-"; } args[5] = "-msglevel"; args[6] = "all=4"; int thumbnailWidth = 320; int thumbnailHeight = 180; boolean isThumbnailPadding = true; if (renderer != null) { thumbnailWidth = renderer.getThumbnailWidth(); thumbnailHeight = renderer.getThumbnailHeight(); isThumbnailPadding = renderer.isThumbnailPadding(); } if (isThumbnailPadding) { args[7] = "-vf"; args[8] = "scale=" + thumbnailWidth + ":-2,expand=:" + thumbnailHeight; } else { args[7] = "-vf"; args[8] = "scale=" + thumbnailWidth + ":-2"; } 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, true, params); // FAILSAFE synchronized (parsingLock) { parsing = true; } Runnable r = new Runnable() { @Override public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { } pw.stopProcess(); synchronized (parsingLock) { parsing = false; } } }; Thread failsafe = new Thread(r, "MPlayer Thumbnail Failsafe"); failsafe.start(); pw.runInSameThread(); synchronized (parsingLock) { parsing = 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; } } @Deprecated public void parse(InputFile inputFile, Format ext, int type, boolean thumbOnly, boolean resume) { parse(inputFile, ext, type, thumbOnly, resume, null); } /** * Parse media without using MediaInfo. */ public void parse(InputFile inputFile, Format ext, int type, boolean thumbOnly, boolean resume, RendererConfiguration renderer) { int i = 0; while (isParsing()) { if (i == 5) { mediaparsed = true; break; } try { Thread.sleep(1000); } catch (InterruptedException e) { } i++; } if (isMediaparsed() && !thumbOnly) { // file could be already parsed by MediaInfo and we need only thumbnail return; } if (inputFile != null) { File file = inputFile.getFile(); if (file != null) { size = file.length(); } else { size = inputFile.getSize(); } ProcessWrapperImpl pw = null; boolean ffmpeg_parsing = true; if (type == Format.AUDIO || ext instanceof AudioAsVideo) { ffmpeg_parsing = false; DLNAMediaAudio audio = new DLNAMediaAudio(); if (file != null) { try { AudioFile af; if ("mp2".equals(FileUtil.getExtension(file).toLowerCase(Locale.ROOT))) { af = AudioFileIO.readAs(file, "mp3"); } else { af = AudioFileIO.read(file); } AudioHeader ah = af.getAudioHeader(); if (ah != null && !thumbOnly) { int length = ah.getTrackLength(); int rate = ah.getSampleRateAsNumber(); if (ah.getEncodingType() != null && ah.getEncodingType().toLowerCase().contains("flac 24")) { audio.setBitsperSample(24); } audio.setSampleFrequency("" + rate); durationSec = (double) length; bitrate = (int) ah.getBitRateAsNumber(); audio.getAudioProperties().setNumberOfChannels(2); // set default value of channels to 2 String channels = ah.getChannels().toLowerCase(Locale.ROOT); if (StringUtils.isNotBlank(channels)) { if (channels.equals("1") || channels.contains("mono")) { // parse value "1" or "Mono" audio.getAudioProperties().setNumberOfChannels(1); } else if (!(channels.equals("2") || channels.equals("0") || channels.contains("stereo"))) { // No need to parse stereo as it's set as default try { audio.getAudioProperties().setNumberOfChannels(Integer.parseInt(channels)); } catch (IllegalArgumentException e) { LOGGER.debug("Could not parse number of audio channels from \"{}\"", channels); } } } if (StringUtils.isNotBlank(ah.getEncodingType())) { audio.setCodecA(ah.getEncodingType()); } if (audio.getCodecA() != null && 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) { thumb = DLNAThumbnail.toThumbnail( t.getArtworkList().get(0).getBinaryData(), 640, 480, ScaleType.MAX, ImageFormat.SOURCE, false ); } else if (!configuration.getAudioThumbnailMethod().equals(CoverSupplier.NONE)) { thumb = DLNAThumbnail.toThumbnail( CoverUtil.get().getThumbnail(t), 640, 480, ScaleType.MAX, ImageFormat.SOURCE, false ); } if (thumb != null) { thumbready = true; } 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 (NumberFormatException | KeyNotFoundException e) { LOGGER.debug("Error parsing unimportant metadata: " + e.getMessage()); } } } } catch (CannotReadException e) { if (e.getMessage().startsWith( ErrorMessage.NO_READER_FOR_THIS_FORMAT.getMsg().substring(0, ErrorMessage.NO_READER_FOR_THIS_FORMAT.getMsg().indexOf("{")) )) { LOGGER.debug("No audio tag support for audio file \"{}\"", file.getName()); } else { LOGGER.error("Error reading audio tag for \"{}\": {}", file.getName(), e.getMessage()); LOGGER.trace("", e); } } catch (IOException | TagException | ReadOnlyFileException | InvalidAudioFrameException | NumberFormatException | KeyNotFoundException e) { LOGGER.debug("Error parsing audio file tag for \"{}\": {}", file.getName(), e.getMessage()); LOGGER.trace("", e); ffmpeg_parsing = false; } // Set container for formats that the normal parsing fails to do from Format if (StringUtils.isBlank(container) && ext != null) { if (ext.getIdentifier() == Identifier.ADPCM) { audio.setCodecA(FormatConfiguration.ADPCM); } else if (ext.getIdentifier() == Identifier.DSD) { audio.setCodecA(FormatConfiguration.DSDAudio); } } if (StringUtils.isNotBlank(audio.getSongname())) { if (renderer != null && renderer.isPrependTrackNumbers() && audio.getTrack() > 0) { audio.setSongname(audio.getTrack() + ": " + audio.getSongname()); } } else { audio.setSongname(file.getName()); } if (!ffmpeg_parsing) { audioTracks.add(audio); } } if (StringUtils.isBlank(container)) { container = audio.getCodecA(); } } if (type == Format.IMAGE && file != null) { if (!thumbOnly) { try { ffmpeg_parsing = false; ImagesUtil.parseImage(file, this); imageCount++; } catch (IOException e) { LOGGER.debug("Error parsing image \"{}\", switching to FFmpeg: {}", file.getAbsolutePath(), e.getMessage()); LOGGER.trace("", e); ffmpeg_parsing = true; } } if (thumbOnly && configuration.isThumbnailGenerationEnabled() && configuration.getImageThumbnailsEnabled()) { LOGGER.trace("Creating thumbnail for \"{}\"", file.getName()); // Create the thumbnail image try { if (imageInfo instanceof ExifInfo && ((ExifInfo) imageInfo).hasExifThumbnail() && !imageInfo.isImageIOSupported()) { /* * XXX Extraction of thumbnails was removed in version * 2.10.0 of metadata-extractor because of a bug in * related code. This section is deactivated while * waiting for this to be made available again. * * Images supported by ImageIO or DCRaw aren't affected, * so this only applied to very few images anyway. * It could extract thumbnails for some "raw" images * if DCRaw was disabled. * // ImageIO can't read the file, try to get the embedded Exif thumbnail if it's there. Metadata metadata; try { metadata = ImagesUtil.getMetadata(new FileInputStream(file), imageInfo.getFormat()); } catch (ImageProcessingException e) { metadata = null; LOGGER.debug("Unexpected error reading metadata for \"{}\": {}", file.getName(), e.getMessage()); LOGGER.trace("", e); } thumb = DLNAThumbnail.toThumbnail( ImagesUtil.getThumbnailFromMetadata(file, metadata), 320, 320, ScaleType.MAX, ImageFormat.SOURCE, false ); if (thumb == null && LOGGER.isTraceEnabled()) { LOGGER.trace("Exif thumbnail extraction failed, no thumbnail will be generated for \"{}\"", file.getName()); }*/ } else { // This will fail with UnknownFormatException for any image formats not supported by ImageIO thumb = DLNAThumbnail.toThumbnail( Files.newInputStream(file.toPath()), 320, 320, ScaleType.MAX, ImageFormat.SOURCE, false ); } thumbready = true; } catch (EOFException e) { LOGGER.debug( "Error generating thumbnail for \"{}\": Unexpected end of file, probably corrupt file or read error.", file.getName() ); } catch (UnknownFormatException e) { LOGGER.debug("Could not generate thumbnail for \"{}\" because the format is unknown: {}", file.getName(), e.getMessage()); } catch (IOException e) { LOGGER.debug("Error generating thumbnail for \"{}\": {}", file.getName(), e.getMessage()); LOGGER.trace("", e); } } } if (ffmpeg_parsing) { if (!thumbOnly || (type == Format.VIDEO && !configuration.isUseMplayerForVideoThumbs())) { pw = getFFmpegThumbnail(inputFile, resume, renderer); } boolean dvrms = false; String input = "-"; if (file != null) { input = ProcessUtil.getShortFileNameIfWideChars(file.getAbsolutePath()); dvrms = file.getAbsolutePath().toLowerCase().endsWith("dvr-ms"); } synchronized (ffmpeg_failureLock) { if (pw != null && !ffmpeg_failure && !thumbOnly) { parseFFmpegInfo(pw.getResults(), input); } } if ( !thumbOnly && container != null && file != null && container.equals("mpegts") && isH264() && getDurationInSeconds() == 0 ) { // Parse the duration try { int length = MpegUtil.getDurationFromMpeg(file); if (length > 0) { durationSec = (double) length; } } catch (IOException e) { LOGGER.trace("Error retrieving length: " + e.getMessage()); } } if (configuration.isUseMplayerForVideoThumbs() && type == Format.VIDEO && !dvrms) { try { getMplayerThumbnail(inputFile, resume, renderer); String frameName = "" + inputFile.hashCode(); frameName = configuration.getTempFolder() + "/mplayer_thumbs/" + frameName + "00000001/00000001.jpg"; frameName = frameName.replace(',', '_'); File jpg = new File(frameName); if (jpg.exists()) { try (InputStream is = new FileInputStream(jpg)) { int sz = is.available(); if (sz > 0) { byte[] bytes = new byte[sz]; is.read(bytes); thumb = DLNAThumbnail.toThumbnail( bytes, 640, 480, ScaleType.MAX, ImageFormat.SOURCE, false ); thumbready = true; } } 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 && thumb == null) { byte[] bytes = pw.getOutputByteArray().toByteArray(); if (bytes != null && bytes.length > 0) { try { thumb = DLNAThumbnail.toThumbnail(bytes); } catch (IOException e) { LOGGER.debug("Error while decoding thumbnail: " + e.getMessage()); LOGGER.trace("", e); } thumbready = true; } } } postParse(type, inputFile); mediaparsed = true; } } /** * Parses media info from FFmpeg's stderr output * * @param lines The stderr output * @param input The FFmpeg input (-i) argument used */ public void parseFFmpegInfo(List<String> lines, String input) { if (lines != null) { if ("-".equals(input)) { input = "pipe:"; } boolean matches = false; int langId = 0; int subId = 0; ListIterator<String> FFmpegMetaData = lines.listIterator(); for (String line : lines) { FFmpegMetaData.next(); line = line.trim(); if (line.startsWith("Output")) { matches = false; } else if (line.startsWith("Input")) { if (line.contains(input)) { matches = true; container = line.substring(10, line.indexOf(',', 11)).trim(); /** * This method is very inaccurate because the Input line in the FFmpeg output * returns "mov,mp4,m4a,3gp,3g2,mj2" for all 6 of those formats, meaning that * we think they are all "mov". * * Here we workaround it by using the file extension, but the best idea is to * prevent using this method by using MediaInfo=true in renderer configs. */ if ("mov".equals(container)) { container = line.substring(line.lastIndexOf('.') + 1, line.lastIndexOf('\'')).trim(); LOGGER.trace("Setting container to " + container + " from the filename. To prevent false-positives, use MediaInfo=true in the renderer config."); } } else { matches = false; } } else if (matches) { if (line.contains("Duration")) { 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 += "00".substring(0, 3 - l); } if (durationStr.contains("N/A")) { durationSec = null; } else { durationSec = 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); bitrate = Integer.parseInt(value); if (unit.equals("kb/s")) { bitrate = 1024 * bitrate; } if (unit.equals("mb/s")) { bitrate = 1048576 * bitrate; } } } } } else if (line.contains("Audio:")) { 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(); 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 (token.equals("mono")) { audio.getAudioProperties().setNumberOfChannels(1); } else if (token.equals("stereo")) { audio.getAudioProperties().setNumberOfChannels(2); } else if (token.equals("5:1") || token.equals("5.1") || token.equals("6 channels")) { audio.getAudioProperties().setNumberOfChannels(6); } else if (token.equals("5 channels")) { audio.getAudioProperties().setNumberOfChannels(5); } else if (token.equals("4 channels")) { audio.getAudioProperties().setNumberOfChannels(4); } else if (token.equals("2 channels")) { audio.getAudioProperties().setNumberOfChannels(2); } 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.contains("Metadata:")) { FFmpegMetaDataNr += 1; line = lines.get(FFmpegMetaDataNr); while (line.indexOf(" ") == 0) { if (line.toLowerCase().contains("title :")) { int aa = line.indexOf(": "); int bb = line.length(); if (aa > -1 && bb > aa) { audio.setAudioTrackTitleFromMetadata(line.substring(aa + 2, bb)); break; } } else { FFmpegMetaDataNr += 1; line = lines.get(FFmpegMetaDataNr); } } } audioTracks.add(audio); } else if (line.contains("Video:")) { StringTokenizer st = new StringTokenizer(line, ","); while (st.hasMoreTokens()) { String token = st.nextToken().trim(); if (token.startsWith("Stream")) { codecV = token.substring(token.indexOf("Video: ") + 7); videoTrackCount++; } else if ((token.contains("tbc") || token.contains("tb(c)"))) { // 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(frameRate)) {// tbc taken into account only if different than tbr Double frameRateDouble = Double.parseDouble(frameRateDoubleString); frameRate = 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.contains("tbr") || token.contains("tb(r)")) && frameRate == null) { frameRate = token.substring(0, token.indexOf("tb")).trim(); } else if ((token.contains("fps") || token.contains("fps(r)")) && frameRate == null) { // dvr-ms ? frameRate = token.substring(0, token.indexOf("fps")).trim(); } else if (token.indexOf('x') > -1 && !token.contains("max")) { String resolution = token.trim(); if (resolution.contains(" [")) { resolution = resolution.substring(0, resolution.indexOf(" [")); } try { width = 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 { height = 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.contains("Subtitle:")) { DLNAMediaSubtitle lang = new DLNAMediaSubtitle(); // $ ffmpeg -codecs | grep "^...S" // ..S... = Subtitle codec // DES... ass ASS (Advanced SSA) subtitle // DES... dvb_subtitle DVB subtitles (decoders: dvbsub ) (encoders: dvbsub ) // ..S... dvb_teletext DVB teletext // DES... dvd_subtitle DVD subtitles (decoders: dvdsub ) (encoders: dvdsub ) // ..S... eia_608 EIA-608 closed captions // D.S... hdmv_pgs_subtitle HDMV Presentation Graphic Stream subtitles (decoders: pgssub ) // D.S... jacosub JACOsub subtitle // D.S... microdvd MicroDVD subtitle // DES... mov_text MOV text // D.S... mpl2 MPL2 subtitle // D.S... pjs PJS (Phoenix Japanimation Society) subtitle // D.S... realtext RealText subtitle // D.S... sami SAMI subtitle // DES... srt SubRip subtitle with embedded timing // DES... ssa SSA (SubStation Alpha) subtitle // DES... subrip SubRip subtitle // D.S... subviewer SubViewer subtitle // D.S... subviewer1 SubViewer v1 subtitle // D.S... text raw UTF-8 text // D.S... vplayer VPlayer subtitle // D.S... webvtt WebVTT subtitle // DES... xsub XSUB if (line.contains("srt") || line.contains("subrip")) { lang.setType(SubtitleType.SUBRIP); } else if (line.contains(" text")) { // excludes dvb_teletext, mov_text, realtext lang.setType(SubtitleType.TEXT); } else if (line.contains("microdvd")) { lang.setType(SubtitleType.MICRODVD); } else if (line.contains("sami")) { lang.setType(SubtitleType.SAMI); } else if (line.contains("ass") || line.contains("ssa")) { lang.setType(SubtitleType.ASS); } else if (line.contains("dvd_subtitle")) { lang.setType(SubtitleType.VOBSUB); } else if (line.contains("xsub")) { lang.setType(SubtitleType.DIVX); } else if (line.contains("mov_text")) { lang.setType(SubtitleType.TX3G); } else if (line.contains("webvtt")) { lang.setType(SubtitleType.WEBVTT); } else { lang.setType(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.contains("Metadata:")) { FFmpegMetaDataNr += 1; line = lines.get(FFmpegMetaDataNr); while (line.indexOf(" ") == 0) { if (line.toLowerCase().contains("title :")) { int aa = line.indexOf(": "); int bb = line.length(); if (aa > -1 && bb > aa) { lang.setSubtitlesTrackTitleFromMetadata(line.substring(aa + 2, bb)); break; } } else { FFmpegMetaDataNr += 1; line = lines.get(FFmpegMetaDataNr); } } } subtitleTracks.add(lang); } } } } ffmpegparsed = true; } /** * Whether the file contains H.264 (AVC) video. * * @return {boolean} */ public boolean isH264() { return codecV != null && codecV.startsWith("h264"); } /** * Disable LPCM transcoding for MP4 container with non-H264 video as workaround for MEncoder's A/V sync bug */ public boolean isValidForLPCMTranscoding() { if (container != null) { if (container.equals("mp4")) { return isH264(); } else { return true; } } return false; } public int getFrameNumbers() { double fr = Double.parseDouble(frameRate); return (int) (getDurationInSeconds() * fr); } public void setDuration(Double d) { this.durationSec = d; } /** * This is the object {@link Double} and might return <code>null</code>. * To get <code>0</code> instead of <code>null</code>, use * {@link #getDurationInSeconds()} */ 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 ? convertTimeToString(durationSec, DURATION_TIME_FORMAT) : null; } /** * @deprecated Use {@link #StringUtil.convertTimeToString(durationSec, StringUtil.DURATION_TIME_FORMAT)} instead. */ @Deprecated public static String getDurationString(double d) { return convertTimeToString(d, DURATION_TIME_FORMAT); } public static Double parseDurationString(String duration) { return duration != null ? convertStringToTime(duration) : null; } public void postParse(int type, InputFile f) { String codecA = null; if (getFirstAudioTrack() != null) { codecA = getFirstAudioTrack().getCodecA(); } if (container != null) { switch (container) { case "avi": mimeType = HTTPResource.AVI_TYPEMIME; break; case "asf": case "wmv": mimeType = HTTPResource.WMV_TYPEMIME; break; case "mov": mimeType = HTTPResource.MOV_TYPEMIME; break; case FormatConfiguration.ADPCM: mimeType = HTTPResource.AUDIO_ADPCM_TYPEMIME; break; case FormatConfiguration.ADTS: mimeType = HTTPResource.AUDIO_ADTS_TYPEMIME; break; case FormatConfiguration.M4A: mimeType = HTTPResource.AUDIO_M4A_TYPEMIME; break; case FormatConfiguration.AC3: mimeType = HTTPResource.AUDIO_AC3_TYPEMIME; break; case FormatConfiguration.DSDAudio: mimeType = HTTPResource.AUDIO_DSD_TYPEMIME; break; case FormatConfiguration.EAC3: mimeType = HTTPResource.AUDIO_EAC3_TYPEMIME; break; case FormatConfiguration.MPA: mimeType = HTTPResource.AUDIO_MPA_TYPEMIME; break; case FormatConfiguration.MP2: mimeType = HTTPResource.AUDIO_MP2_TYPEMIME; break; case FormatConfiguration.AIFF: mimeType = HTTPResource.AUDIO_AIFF_TYPEMIME; break; case FormatConfiguration.ATRAC: mimeType = HTTPResource.AUDIO_ATRAC_TYPEMIME; break; case FormatConfiguration.MKA: mimeType = HTTPResource.AUDIO_MKA_TYPEMIME; break; case FormatConfiguration.MLP: mimeType = HTTPResource.AUDIO_MLP_TYPEMIME; break; case FormatConfiguration.MONKEYS_AUDIO: mimeType = HTTPResource.AUDIO_APE_TYPEMIME; break; case FormatConfiguration.MPC: mimeType = HTTPResource.AUDIO_MPC_TYPEMIME; break; case FormatConfiguration.RA: mimeType = HTTPResource.AUDIO_RA_TYPEMIME; break; case FormatConfiguration.RM: if (isAudio()) { mimeType = HTTPResource.AUDIO_RA_TYPEMIME; } else { mimeType = HTTPResource.RM_TYPEMIME; } break; case FormatConfiguration.SHORTEN: mimeType = HTTPResource.AUDIO_SHN_TYPEMIME; break; case FormatConfiguration.THREEGA: mimeType = HTTPResource.AUDIO_THREEGPPA_TYPEMIME; break; case FormatConfiguration.TRUEHD: mimeType = HTTPResource.AUDIO_TRUEHD_TYPEMIME; break; case FormatConfiguration.TTA: mimeType = HTTPResource.AUDIO_TTA_TYPEMIME; break; case FormatConfiguration.WAVPACK: mimeType = HTTPResource.AUDIO_WV_TYPEMIME; break; case FormatConfiguration.WMA: mimeType = HTTPResource.AUDIO_WMA_TYPEMIME; break; case FormatConfiguration.OGG: mimeType = HTTPResource.AUDIO_OGG_TYPEMIME; break; case FormatConfiguration.AU: mimeType = HTTPResource.AUDIO_AU_TYPEMIME; break; } } if (mimeType == null) { if (codecV != null) { if ("matroska".equals(container) || "mkv".equals(container)) { mimeType = HTTPResource.MATROSKA_TYPEMIME; } else if ("ogg".equals(container)) { mimeType = HTTPResource.OGG_TYPEMIME; } else if ("3gp".equals(container)) { mimeType = HTTPResource.THREEGPP_TYPEMIME; } else if ("3g2".equals(container)) { mimeType = HTTPResource.THREEGPP2_TYPEMIME; } else if ("webm".equals(container)) { mimeType = HTTPResource.WEBM_TYPEMIME; } else if (codecV.equals("mjpeg") || "jpg".equals(container)) { mimeType = HTTPResource.JPEG_TYPEMIME; } else if ("png".equals(codecV) || "png".equals(container)) { mimeType = HTTPResource.PNG_TYPEMIME; } else if ("gif".equals(codecV) || "gif".equals(container)) { mimeType = HTTPResource.GIF_TYPEMIME; } else if ("tiff".equals(codecV) || "tiff".equals(container)) { mimeType = HTTPResource.TIFF_TYPEMIME; } else if ("bmp".equals(codecV) || "bmp".equals(container)) { mimeType = HTTPResource.BMP_TYPEMIME; } else if (codecV.startsWith("h264") || codecV.equals("h263") || codecV.equals("mpeg4") || codecV.equals("mp4")) { mimeType = HTTPResource.MP4_TYPEMIME; } else if (codecV.contains("mpeg") || codecV.contains("mpg")) { mimeType = HTTPResource.MPEG_TYPEMIME; } } else if (codecV == null && codecA != null) { if ("ogg".equals(container)) { mimeType = HTTPResource.AUDIO_OGG_TYPEMIME; } else if ("3gp".equals(container)) { mimeType = HTTPResource.AUDIO_THREEGPPA_TYPEMIME; } else if ("3g2".equals(container)) { mimeType = HTTPResource.AUDIO_THREEGPP2A_TYPEMIME; } else if ("adts".equals(container)) { mimeType = HTTPResource.AUDIO_ADTS_TYPEMIME; } else if ("matroska".equals(container) || "mkv".equals(container)) { mimeType = HTTPResource.AUDIO_MKA_TYPEMIME; } else if ("webm".equals(container)) { mimeType = HTTPResource.AUDIO_WEBM_TYPEMIME; } else if (codecA.contains("mp3")) { mimeType = HTTPResource.AUDIO_MP3_TYPEMIME; } else if (codecA.equals(FormatConfiguration.MPA)) { mimeType = HTTPResource.AUDIO_MPA_TYPEMIME; } else if (codecA.equals(FormatConfiguration.MP2)) { mimeType = HTTPResource.AUDIO_MP2_TYPEMIME; } else if (codecA.contains("flac")) { mimeType = HTTPResource.AUDIO_FLAC_TYPEMIME; } else if (codecA.contains("vorbis")) { mimeType = HTTPResource.AUDIO_VORBIS_TYPEMIME; } else if (codecA.contains("asf") || codecA.startsWith("wm")) { mimeType = HTTPResource.AUDIO_WMA_TYPEMIME; } else if (codecA.contains("pcm") || codecA.contains("wav") || codecA.contains("dts")) { mimeType = HTTPResource.AUDIO_WAV_TYPEMIME; } else if (codecA.contains("aac")) { mimeType = HTTPResource.AUDIO_M4A_TYPEMIME; } else if (codecA.equals(FormatConfiguration.TRUEHD)) { mimeType = HTTPResource.AUDIO_TRUEHD_TYPEMIME; } else if (codecA.equals(FormatConfiguration.DTS)) { mimeType = HTTPResource.AUDIO_DTS_TYPEMIME; } else if (codecA.equals(FormatConfiguration.DTSHD)) { mimeType = HTTPResource.AUDIO_DTSHD_TYPEMIME; } else if (codecA.equals(FormatConfiguration.EAC3)) { mimeType = HTTPResource.AUDIO_EAC3_TYPEMIME; } else if (codecA.equals(FormatConfiguration.ADPCM)) { mimeType = HTTPResource.AUDIO_ADPCM_TYPEMIME; } else if (codecA.equals(FormatConfiguration.DSDAudio)) { mimeType = HTTPResource.AUDIO_DSD_TYPEMIME; } } if (mimeType == null) { mimeType = HTTPResource.getDefaultMimeType(type); } } if (getFirstAudioTrack() == null || !(type == Format.AUDIO && getFirstAudioTrack().getBitsperSample() == 24 && getFirstAudioTrack().getSampleRate() > 48000)) { secondaryFormatValid = 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 boolean isVideoWithinH264LevelLimits(InputFile f, RendererConfiguration mediaRenderer) { synchronized (videoWithinH264LevelLimitsLock) { if (videoWithinH264LevelLimits == null) { if (isH264()) { videoWithinH264LevelLimits = true; if ( container != null && ( container.equals("matroska") || container.equals("mkv") || container.equals("mov") || container.equals("mp4") ) ) { // Containers without h264_annexB byte headers[][] = getAnnexBFrameHeader(f); synchronized (ffmpeg_annexb_failureLock) { if (ffmpeg_annexb_failure) { LOGGER.info("Error parsing information from the file: " + f.getFilename()); } } if (headers != null) { synchronized (h264_annexBLock) { h264_annexB = headers[1]; if (h264_annexB != null) { int skip = 5; if (h264_annexB[2] == 1) { skip = 4; } byte header[] = new byte[h264_annexB.length - skip]; System.arraycopy(h264_annexB, skip, header, 0, header.length); avcLevelLock.readLock().lock(); referenceFrameCountLock.readLock().lock(); try { if ( referenceFrameCount > -1 && ( "4.1".equals(avcLevel) || "4.2".equals(avcLevel) || "5".equals(avcLevel) || "5.0".equals(avcLevel) || "5.1".equals(avcLevel) || "5.2".equals(avcLevel) ) && width > 0 && height > 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 / (double) (width * height)); } else { /** * This is the math for level 4.1, which results in: * - 4 for 1920x1080 * - 9 for 1280x720 */ maxref = (int) Math.floor(8388608 / (double) (width * height)); } if (referenceFrameCount > maxref) { LOGGER.debug( "The 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, referenceFrameCount ); videoWithinH264LevelLimits = false; } else if (referenceFrameCount == -1) { LOGGER.debug( "The file \"{}\" may not be compatible with this renderer because " + "we can't get its number of reference frames", f.getFilename() ); videoWithinH264LevelLimits = false; } } } finally { referenceFrameCountLock.readLock().unlock(); avcLevelLock.readLock().unlock(); } } else { LOGGER.debug( "The H.264 stream inside the file \"{}\" is not compatible with this renderer", f.getFilename() ); videoWithinH264LevelLimits = false; } } } else { videoWithinH264LevelLimits = false; } } } else { videoWithinH264LevelLimits = false; } } return videoWithinH264LevelLimits; } } 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(); if (getContainer() != null) { result.append("Container: ").append(getContainer().toUpperCase(Locale.ROOT)).append(", "); } result.append("Size: ").append(getSize()); if (isVideo()) { result.append(", Video Bitrate: ").append(getBitrate()); result.append(", Video Tracks: ").append(getVideoTrackCount()); result.append(", Video Codec: ").append(getCodecV()); result.append(", Duration: ").append(getDurationString()); result.append(", Video Resolution: ").append(getWidth()).append(" x ").append(getHeight()); if (isNotBlank(getFrameRate())) { result.append(", Frame Rate: ").append(getFrameRate()); } if (isNotBlank(getFrameRateOriginal())) { result.append(", Original Frame Rate: ").append(getFrameRateOriginal()); } if (isNotBlank(getFrameRateMode())) { result.append(", Frame Rate Mode: "); result.append(getFrameRateModeRaw()); if (isNotBlank(getFrameRateModeRaw())) { result.append(" (").append(getFrameRateModeRaw()).append(")"); } } else if (isNotBlank(getFrameRateModeRaw())) { result.append(", Frame Rate Mode Raw: "); result.append(getFrameRateModeRaw()); } if (isNotBlank(getMuxingMode())) { result.append(", Muxing Mode: ").append(getMuxingMode()); } if (isNotBlank(getMatrixCoefficients())) { result.append(", Matrix Coefficients: ").append(getMatrixCoefficients()); } if (isNotBlank(avcLevel)) { result.append(", AVC Level: ").append(getAvcLevel()); } // if (isNotBlank(getHevcLevel())) { // result.append(", HEVC Level: "); // result.append(getHevcLevel()); if (getVideoBitDepth() != 8) { result.append(", Video Bit Depth: ").append(getVideoBitDepth()); } if (isNotBlank(getFileTitleFromMetadata())) { result.append(", File Title from Metadata: ").append(getFileTitleFromMetadata()); } if (isNotBlank(getVideoTrackTitleFromMetadata())) { result.append(", Video Track Title from Metadata: ").append(getVideoTrackTitleFromMetadata()); } if (getAudioTrackCount() > 0) { appendAudioTracks(result); } if (hasSubtitles()) { appendSubtitleTracks(result); } } else if (getAudioTrackCount() > 0) { result.append(", Bitrate: ").append(getBitrate()); result.append(", Duration: ").append(getDurationString()); appendAudioTracks(result); } if (getImageCount() > 0) { if (getImageCount() > 1) { result.append(", Images: ").append(getImageCount()); } if (getImageInfo() != null) { result.append(", ").append(getImageInfo()); } else { result.append(", Image Width: ").append(getWidth()); result.append(", Image Height: ").append(getHeight()); } } if (getThumb() != null) { result.append(", ").append(getThumb()); } result.append(", Mime Type: ").append(getMimeType()); return result.toString(); } public void appendAudioTracks(StringBuilder sb) { sb.append(", Audio Tracks: ").append(getAudioTrackCount()); for (DLNAMediaAudio audio : audioTracks) { if (!audio.equals(audioTracks.get(0))) { sb.append(","); } sb.append(" [").append(audio).append("]"); } } public void appendSubtitleTracks(StringBuilder sb) { sb.append(", Subtitle Tracks: ").append(getSubTrackCount()); for (DLNAMediaSubtitle subtitleTrack : subtitleTracks) { if (!subtitleTrack.equals(subtitleTracks.get(0))) { sb.append(","); } sb.append(" [").append(subtitleTrack).append("]"); } } public DLNAThumbnailInputStream getThumbnailInputStream() { return thumb != null ? new DLNAThumbnailInputStream(thumb) : null; } public String getValidFps(boolean ratios) { String validFrameRate = null; if (frameRate != null && frameRate.length() > 0) { try { double fr = Double.parseDouble(frameRate.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 (audioTracks.size() > 0) { return audioTracks.get(0); } return null; } /** * @deprecated use getAspectRatioMencoderMpegopts() for the original * functionality of this method, or use getAspectRatioContainer() for a * better default method to get aspect ratios. */ @Deprecated public String getValidAspect(boolean ratios) { return getAspectRatioMencoderMpegopts(ratios); } /** * Converts the result of getAspectRatioDvdIso() to provide * MEncoderVideo with a valid value for the "vaspect" option in the * "-mpegopts" command. * * Note: Our code never uses a false value for "ratios", so unless any * plugins rely on it we can simplify things by removing that parameter. * * @param ratios * @return */ public String getAspectRatioMencoderMpegopts(boolean ratios) { String a = null; if (aspectRatioDvdIso != null) { double ar = Double.parseDouble(aspectRatioDvdIso); 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 (width > 0 && height > 0) { return width + "x" + height; } return null; } public int getRealVideoBitrate() { if (bitrate > 0) { return (bitrate / 8); } int realBitrate = 10000000; if (getDurationInSeconds() != 0) { realBitrate = (int) (size / getDurationInSeconds()); } return realBitrate; } public boolean isHDVideo() { return (width > 864 || height > 576); } public boolean isMpegTS() { return container != null && container.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] = "-c:v"; cmdArray[6] = "copy"; cmdArray[7] = "-f"; cmdArray[8] = "h264"; cmdArray[9] = "-bsf"; 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, true, params); Runnable r = new Runnable() { @Override public void run() { try { Thread.sleep(3000); synchronized (ffmpeg_annexb_failureLock) { ffmpeg_annexb_failure = true; } } catch (InterruptedException e) { } pw.stopProcess(); } }; Thread failsafe = new Thread(r, "FFMpeg AnnexB Frame Header Failsafe"); failsafe.start(); pw.runInSameThread(); synchronized (ffmpeg_annexb_failureLock) { if (ffmpeg_annexb_failure) { return null; } } byte data[] = pw.getOutputByteArray().toByteArray(); returnData[0] = data; 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; } return returnData; } @Override protected DLNAMediaInfo clone() throws CloneNotSupportedException { DLNAMediaInfo mediaCloned = (DLNAMediaInfo) super.clone(); mediaCloned.setAudioTracksList(new ArrayList<DLNAMediaAudio>()); for (DLNAMediaAudio audio : audioTracks) { mediaCloned.getAudioTracksList().add((DLNAMediaAudio) audio.clone()); } mediaCloned.setSubtitleTracksList(new ArrayList<DLNAMediaSubtitle>()); for (DLNAMediaSubtitle sub : subtitleTracks) { mediaCloned.getSubtitleTracksList().add((DLNAMediaSubtitle) sub.clone()); } return mediaCloned; } /** * @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 != null ? codecV.toLowerCase(Locale.ROOT) : null ; } /** * @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 frameRateOriginal */ public String getFrameRateOriginal() { return frameRateOriginal; } /** * @param frameRateOriginal the frameRateOriginal to set */ public void setFrameRateOriginal(String frameRateOriginal) { this.frameRateOriginal = frameRateOriginal; } /** * @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 unaltered frame rate mode */ public String getFrameRateModeRaw() { return frameRateModeRaw; } /** * @param frameRateModeRaw the unaltered frame rate mode to set */ public void setFrameRateModeRaw(String frameRateModeRaw) { this.frameRateModeRaw = frameRateModeRaw; } /** * @return the video bit depth */ public int getVideoBitDepth() { return videoBitDepth; } /** * @param value the video bit depth to set */ public void setVideoBitDepth(int value) { this.videoBitDepth = value; } /** * @deprecated use getAspectRatioDvdIso() for the original * functionality of this method, or use getAspectRatioContainer() for a * better default method to get aspect ratios. */ @Deprecated public String getAspect() { return getAspectRatioDvdIso(); } /** * The aspect ratio for a DVD ISO video track * * @return the aspect * @since 1.50.0 */ public String getAspectRatioDvdIso() { return aspectRatioDvdIso; } /** * @deprecated use setAspectRatioDvdIso() for the original * functionality of this method, or use setAspectRatioContainer() for a * better default method to set aspect ratios. */ @Deprecated public void setAspect(String aspect) { setAspectRatioDvdIso(aspect); } /** * @param aspect the aspect to set * @since 1.50.0 */ public void setAspectRatioDvdIso(String aspect) { this.aspectRatioDvdIso = aspect; } /** * Get the aspect ratio reported by the file/container. * This is the aspect ratio that the renderer should display the video * at, and is usually the same as the video track aspect ratio. * * @return the aspect ratio reported by the file/container */ public String getAspectRatioContainer() { return aspectRatioContainer; } /** * Set the aspect ratio reported by the file/container. * * @see #getAspectRatioContainer() * @param aspect the aspect ratio to set */ public void setAspectRatioContainer(String aspect) { this.aspectRatioContainer = getFormattedAspectRatio(aspect); } /** * Get the aspect ratio of the video track. * This is the actual aspect ratio of the pixels, which is not * always the aspect ratio that the renderer should display or that we * should output; that is {@link #getAspectRatioContainer()} * * @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 = getFormattedAspectRatio(aspect); } /** * Make sure the aspect ratio is formatted, e.g. 16:9 not 1.78 * * @param aspect the possibly-unformatted aspect ratio * * @return the formatted aspect ratio or null */ public String getFormattedAspectRatio(String aspect) { if (isBlank(aspect)) { return null; } else { if (aspect.contains(":")) { return aspect; } else { double exactAspectRatio = Double.parseDouble(aspect); if (exactAspectRatio > 1.7 && exactAspectRatio <= 1.8) { return "16:9"; } else if (exactAspectRatio > 1.3 && exactAspectRatio < 1.4) { return "4:3"; } else if (exactAspectRatio > 1.2 && exactAspectRatio < 1.3) { return "5:4"; } else { return null; } } } } /** * @return the thumb * @since 1.50.0 */ public DLNAThumbnail getThumb() { return thumb; } /** * @param thumb the thumb to set * @since 1.50.0 * @deprecated Use {@link #setThumb(DLNAThumbnail)} instead. */ @Deprecated public void setThumb(byte[] thumb) { try { this.thumb = DLNAThumbnail.toThumbnail( thumb, 640, 480, ScaleType.MAX, ImageFormat.SOURCE, false ); if (this.thumb != null) { thumbready = true; } } catch (IOException e) { LOGGER.error("An error occurred while trying to store thumbnail: {}", e.getMessage()); LOGGER.trace("", e); } } /** * Sets the {@link DLNAThumbnail} instance to use for this {@link DLNAMediaInfo} instance. * * @param thumbnail the {@link DLNAThumbnail} to set. */ public void setThumb(DLNAThumbnail thumbnail) { this.thumb = thumbnail; if (thumbnail != null) { thumbready = true; } } /** * @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; } public String getMatrixCoefficients() { return matrixCoefficients; } public void setMatrixCoefficients(String matrixCoefficients) { this.matrixCoefficients = matrixCoefficients; } public String getFileTitleFromMetadata() { return fileTitleFromMetadata; } public void setFileTitleFromMetadata(String value) { this.fileTitleFromMetadata = value; } public String getVideoTrackTitleFromMetadata() { return videoTrackTitleFromMetadata; } public void setVideoTrackTitleFromMetadata(String value) { this.videoTrackTitleFromMetadata = value; } /** * @return The {@link ImageInfo} for this media or {@code null}. */ public ImageInfo getImageInfo() { return imageInfo; } /** * Sets the {@link ImageInfo} for this media. * * @param imageInfo the {@link ImageInfo}. */ public void setImageInfo(ImageInfo imageInfo) { this.imageInfo = imageInfo; if (imageInfo != null && imageInfo.getWidth() > 0 && imageInfo.getHeight() > 0) { setWidth(imageInfo.getWidth()); setHeight(imageInfo.getHeight()); } } /** * @return reference frame count for video stream or {@code -1} if not parsed. */ public byte getReferenceFrameCount() { referenceFrameCountLock.readLock().lock(); try { return referenceFrameCount; } finally { referenceFrameCountLock.readLock().unlock(); } } /** * Sets reference frame count for video stream or {@code -1} if not parsed. * * @param referenceFrameCount reference frame count. */ public void setReferenceFrameCount(byte referenceFrameCount) { if (referenceFrameCount < -1) { throw new IllegalArgumentException("referenceFrameCount must be >= -1."); } referenceFrameCountLock.writeLock().lock(); try { this.referenceFrameCount = referenceFrameCount; } finally { referenceFrameCountLock.writeLock().unlock(); } } /** * @return AVC level for video stream or {@code null} if not parsed. */ public String getAvcLevel() { avcLevelLock.readLock().lock(); try { return avcLevel; } finally { avcLevelLock.readLock().unlock(); } } /** * Sets AVC level for video stream or {@code null} if not parsed. * * @param avcLevel AVC level. */ public void setAvcLevel(String avcLevel) { avcLevelLock.writeLock().lock(); try { this.avcLevel = avcLevel; } finally { avcLevelLock.writeLock().unlock(); } } public int getAvcAsInt() { try { return Integer.parseInt(getAvcLevel().replaceAll("\\.", "")); } catch (Exception e) { return 0; } } public String getH264Profile() { synchronized (h264ProfileLock) { return h264Profile; } } public void setH264Profile(String s) { synchronized (h264ProfileLock) { h264Profile = s; } } /** * @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<>(); } } /** * @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<>(); } } /** * @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 Exif orientation or {@code 1} if unknown. * @since 1.50.0 */ public ExifOrientation getExifOrientation() { return imageInfo != null ? imageInfo.getExifOrientation() : ExifOrientation.TOP_LEFT; } /** * @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() { synchronized (h264_annexBLock) { if (h264_annexB == null) { return null; } byte[] result = new byte[h264_annexB.length]; System.arraycopy(h264_annexB, 0, result, 0, h264_annexB.length); return result; } } /** * @param h264AnnexB the h264_annexB to set * @since 1.50.0 */ public void setH264AnnexB(byte[] h264AnnexB) { synchronized (h264_annexBLock) { if (h264AnnexB == null) { this.h264_annexB = null; } else { this.h264_annexB = new byte[h264AnnexB.length]; System.arraycopy(h264AnnexB, 0, this.h264_annexB, 0, h264AnnexB.length); } } } /** * @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; } public boolean isFFmpegparsed() { return ffmpegparsed; } /** * @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() { synchronized (parsingLock) { return parsing; } } /** * @param parsing the parsing to set * @since 1.50.0 */ public void setParsing(boolean parsing) { synchronized (parsingLock) { 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; } public boolean isMod4() { if ( height % 4 != 0 || width % 4 != 0 ) { return false; } return true; } /** * Note: This is based on a flag in Matroska files, and as such it is * unreliable; it will be unlikely to find a false-positive but there * will be false-negatives, similar to language flags. * * @return whether the video track is 3D */ public boolean is3d() { return isNotBlank(stereoscopy); } /** * The significance of this is that the aspect ratio should not be kept * in this case when transcoding. * Example: 3840x1080 should be resized to 1920x1080, not 1920x540. * * @return whether the video track is full SBS or OU 3D */ public boolean is3dFullSbsOrOu() { if (!is3d()) { return false; } switch (stereoscopy.toLowerCase()) { case "overunderrt": case "oulf": case "ourf": case "sbslf": case "sbsrf": case "top-bottom (left eye first)": case "top-bottom (right eye first)": case "side by side (left eye first)": case "side by side (right eye first)": return true; } return false; } /** * Note: This is based on a flag in Matroska files, and as such it is * unreliable; it will be unlikely to find a false-positive but there * will be false-negatives, similar to language flags. * * @return the type of stereoscopy (3D) of the video track */ public String getStereoscopy() { return stereoscopy; } /** * Sets the type of stereoscopy (3D) of the video track. * * Note: This is based on a flag in Matroska files, and as such it is * unreliable; it will be unlikely to find a false-positive but there * will be false-negatives, similar to language flags. * * @param stereoscopy the type of stereoscopy (3D) of the video track */ public void setStereoscopy(String stereoscopy) { this.stereoscopy = stereoscopy; } /** * Used by FFmpeg for 3D video format naming */ public enum Mode3D { SBSL, SBSR, HSBSL, OUL, OUR, HOUL, ARCG, ARCH, ARCC, ARCD, AGMG, AGMH, AGMC, AGMD, AYBG, AYBH, AYBC, AYBD }; public Mode3D get3DLayout() { if (!is3d()) { return null; } isAnaglyph = true; switch (stereoscopy.toLowerCase()) { case "overunderrt": case "oulf": case "top-bottom (left eye first)": isAnaglyph = false; return Mode3D.OUL; case "ourf": case "top-bottom (right eye first)": isAnaglyph = false; return Mode3D.OUR; case "sbslf": case "side by side (left eye first)": isAnaglyph = false; return Mode3D.SBSL; case "sbsrf": case "side by side (right eye first)": isAnaglyph = false; return Mode3D.SBSR; case "half top-bottom (left eye first)": isAnaglyph = false; return Mode3D.HOUL; case "half side by side (left eye first)": isAnaglyph = false; return Mode3D.HSBSL; case "arcg": return Mode3D.ARCG; case "arch": return Mode3D.ARCH; case "arcc": return Mode3D.ARCC; case "arcd": return Mode3D.ARCD; case "agmg": return Mode3D.AGMG; case "agmh": return Mode3D.AGMH; case "agmc": return Mode3D.AGMC; case "agmd": return Mode3D.AGMD; case "aybg": return Mode3D.AYBG; case "aybh": return Mode3D.AYBH; case "aybc": return Mode3D.AYBC; case "aybd": return Mode3D.AYBD; } return null; } private boolean isAnaglyph; public boolean stereoscopyIsAnaglyph() { get3DLayout(); return isAnaglyph; } public boolean isDVDResolution() { return (width == 720 && height == 576) || (width == 720 && height == 480); } }