/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.android.exoplayer.extractor.mp4; import com.google.android.exoplayer.C; import com.google.android.exoplayer.extractor.ExtractorInput; import com.google.android.exoplayer.util.ParsableByteArray; import com.google.android.exoplayer.util.Util; import java.io.IOException; /** * Provides methods that peek data from an {@link ExtractorInput} and return whether the input * appears to be in MP4 format. */ /* package */ final class Sniffer { private static final int[] COMPATIBLE_BRANDS = new int[] { Util.getIntegerCodeForString("isom"), Util.getIntegerCodeForString("iso2"), Util.getIntegerCodeForString("avc1"), Util.getIntegerCodeForString("hvc1"), Util.getIntegerCodeForString("hev1"), Util.getIntegerCodeForString("mp41"), Util.getIntegerCodeForString("mp42"), Util.getIntegerCodeForString("3g2a"), Util.getIntegerCodeForString("3g2b"), Util.getIntegerCodeForString("3gr6"), Util.getIntegerCodeForString("3gs6"), Util.getIntegerCodeForString("3ge6"), Util.getIntegerCodeForString("3gg6"), Util.getIntegerCodeForString("M4V "), Util.getIntegerCodeForString("M4A "), Util.getIntegerCodeForString("f4v "), Util.getIntegerCodeForString("kddi"), Util.getIntegerCodeForString("M4VP"), Util.getIntegerCodeForString("qt "), // Apple QuickTime Util.getIntegerCodeForString("MSNV"), // Sony PSP }; /** * Returns whether data peeked from the current position in {@code input} is consistent with the * input being a fragmented MP4 file. * * @param input The extractor input from which to peek data. The peek position will be modified. * @return True if the input appears to be in the fragmented MP4 format. False otherwise. * @throws IOException If an error occurs reading from the input. * @throws InterruptedException If the thread has been interrupted. */ public static boolean sniffFragmented(ExtractorInput input) throws IOException, InterruptedException { return sniffInternal(input, 4 * 1024, true); } /** * Returns whether data peeked from the current position in {@code input} is consistent with the * input being an unfragmented MP4 file. * * @param input The extractor input from which to peek data. The peek position will be modified. * @return True if the input appears to be in the unfragmented MP4 format. False otherwise. * @throws IOException If an error occurs reading from the input. * @throws InterruptedException If the thread has been interrupted. */ public static boolean sniffUnfragmented(ExtractorInput input) throws IOException, InterruptedException { return sniffInternal(input, 128, false); } private static boolean sniffInternal(ExtractorInput input, int searchLength, boolean fragmented) throws IOException, InterruptedException { long inputLength = input.getLength(); int bytesToSearch = (int) (inputLength == C.LENGTH_UNBOUNDED || inputLength > searchLength ? searchLength : inputLength); ParsableByteArray buffer = new ParsableByteArray(64); int bytesSearched = 0; boolean foundGoodFileType = false; boolean foundFragment = false; while (bytesSearched < bytesToSearch) { // Read an atom header. int headerSize = Atom.HEADER_SIZE; input.peekFully(buffer.data, 0, headerSize); buffer.setPosition(0); long atomSize = buffer.readUnsignedInt(); int atomType = buffer.readInt(); if (atomSize == Atom.LONG_SIZE_PREFIX) { input.peekFully(buffer.data, headerSize, Atom.LONG_HEADER_SIZE - headerSize); headerSize = Atom.LONG_HEADER_SIZE; atomSize = buffer.readLong(); } // Check the atom size is large enough to include its header. if (atomSize < headerSize) { return false; } int atomDataSize = (int) atomSize - headerSize; if (atomType == Atom.TYPE_ftyp) { if (atomDataSize < 8) { return false; } int compatibleBrandsCount = (atomDataSize - 8) / 4; input.peekFully(buffer.data, 0, 4 * (compatibleBrandsCount + 2)); for (int i = 0; i < compatibleBrandsCount + 2; i++) { if (i == 1) { // This index refers to the minorVersion, not a brand, so skip it. continue; } if (isCompatibleBrand(buffer.readInt())) { foundGoodFileType = true; break; } } // There is only one ftyp box, so reject the file if the file type in this box was invalid. if (!foundGoodFileType) { return false; } } else if (atomType == Atom.TYPE_moof) { foundFragment = true; break; } else if (atomDataSize != 0) { // Stop searching if reading this atom would exceed the search limit. if (bytesSearched + atomSize >= bytesToSearch) { break; } input.advancePeekPosition(atomDataSize); } bytesSearched += atomSize; } return foundGoodFileType && fragmented == foundFragment; } /** * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors. */ private static boolean isCompatibleBrand(int brand) { // Accept all brands starting '3gp'. if (brand >>> 8 == Util.getIntegerCodeForString("3gp")) { return true; } for (int compatibleBrand : COMPATIBLE_BRANDS) { if (compatibleBrand == brand) { return true; } } return false; } private Sniffer() { // Prevent instantiation. } }