1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.android.exoplayer2.extractor.mp4; 17 18 import com.google.android.exoplayer2.C; 19 import com.google.android.exoplayer2.extractor.ExtractorInput; 20 import com.google.android.exoplayer2.util.ParsableByteArray; 21 import java.io.IOException; 22 23 /** 24 * Provides methods that peek data from an {@link ExtractorInput} and return whether the input 25 * appears to be in MP4 format. 26 */ 27 /* package */ final class Sniffer { 28 29 /** The maximum number of bytes to peek when sniffing. */ 30 private static final int SEARCH_LENGTH = 4 * 1024; 31 32 private static final int[] COMPATIBLE_BRANDS = 33 new int[] { 34 0x69736f6d, // isom 35 0x69736f32, // iso2 36 0x69736f33, // iso3 37 0x69736f34, // iso4 38 0x69736f35, // iso5 39 0x69736f36, // iso6 40 0x61766331, // avc1 41 0x68766331, // hvc1 42 0x68657631, // hev1 43 0x61763031, // av01 44 0x6d703431, // mp41 45 0x6d703432, // mp42 46 0x33673261, // 3g2a 47 0x33673262, // 3g2b 48 0x33677236, // 3gr6 49 0x33677336, // 3gs6 50 0x33676536, // 3ge6 51 0x33676736, // 3gg6 52 0x4d345620, // M4V[space] 53 0x4d344120, // M4A[space] 54 0x66347620, // f4v[space] 55 0x6b646469, // kddi 56 0x4d345650, // M4VP 57 0x71742020, // qt[space][space], Apple QuickTime 58 0x4d534e56, // MSNV, Sony PSP 59 0x64627931, // dby1, Dolby Vision 60 }; 61 62 /** 63 * Returns whether data peeked from the current position in {@code input} is consistent with the 64 * input being a fragmented MP4 file. 65 * 66 * @param input The extractor input from which to peek data. The peek position will be modified. 67 * @return Whether the input appears to be in the fragmented MP4 format. 68 * @throws IOException If an error occurs reading from the input. 69 */ sniffFragmented(ExtractorInput input)70 public static boolean sniffFragmented(ExtractorInput input) throws IOException { 71 return sniffInternal(input, true); 72 } 73 74 /** 75 * Returns whether data peeked from the current position in {@code input} is consistent with the 76 * input being an unfragmented MP4 file. 77 * 78 * @param input The extractor input from which to peek data. The peek position will be modified. 79 * @return Whether the input appears to be in the unfragmented MP4 format. 80 * @throws IOException If an error occurs reading from the input. 81 */ sniffUnfragmented(ExtractorInput input)82 public static boolean sniffUnfragmented(ExtractorInput input) throws IOException { 83 return sniffInternal(input, false); 84 } 85 sniffInternal(ExtractorInput input, boolean fragmented)86 private static boolean sniffInternal(ExtractorInput input, boolean fragmented) 87 throws IOException { 88 long inputLength = input.getLength(); 89 int bytesToSearch = (int) (inputLength == C.LENGTH_UNSET || inputLength > SEARCH_LENGTH 90 ? SEARCH_LENGTH : inputLength); 91 92 ParsableByteArray buffer = new ParsableByteArray(64); 93 int bytesSearched = 0; 94 boolean foundGoodFileType = false; 95 boolean isFragmented = false; 96 while (bytesSearched < bytesToSearch) { 97 // Read an atom header. 98 int headerSize = Atom.HEADER_SIZE; 99 buffer.reset(headerSize); 100 input.peekFully(buffer.data, 0, headerSize); 101 long atomSize = buffer.readUnsignedInt(); 102 int atomType = buffer.readInt(); 103 if (atomSize == Atom.DEFINES_LARGE_SIZE) { 104 // Read the large atom size. 105 headerSize = Atom.LONG_HEADER_SIZE; 106 input.peekFully(buffer.data, Atom.HEADER_SIZE, Atom.LONG_HEADER_SIZE - Atom.HEADER_SIZE); 107 buffer.setLimit(Atom.LONG_HEADER_SIZE); 108 atomSize = buffer.readLong(); 109 } else if (atomSize == Atom.EXTENDS_TO_END_SIZE) { 110 // The atom extends to the end of the file. 111 long fileEndPosition = input.getLength(); 112 if (fileEndPosition != C.LENGTH_UNSET) { 113 atomSize = fileEndPosition - input.getPeekPosition() + headerSize; 114 } 115 } 116 117 if (atomSize < headerSize) { 118 // The file is invalid because the atom size is too small for its header. 119 return false; 120 } 121 bytesSearched += headerSize; 122 123 if (atomType == Atom.TYPE_moov) { 124 // We have seen the moov atom. We increase the search size to make sure we don't miss an 125 // mvex atom because the moov's size exceeds the search length. 126 bytesToSearch += (int) atomSize; 127 if (inputLength != C.LENGTH_UNSET && bytesToSearch > inputLength) { 128 // Make sure we don't exceed the file size. 129 bytesToSearch = (int) inputLength; 130 } 131 // Check for an mvex atom inside the moov atom to identify whether the file is fragmented. 132 continue; 133 } 134 135 if (atomType == Atom.TYPE_moof || atomType == Atom.TYPE_mvex) { 136 // The movie is fragmented. Stop searching as we must have read any ftyp atom already. 137 isFragmented = true; 138 break; 139 } 140 141 if (bytesSearched + atomSize - headerSize >= bytesToSearch) { 142 // Stop searching as peeking this atom would exceed the search limit. 143 break; 144 } 145 146 int atomDataSize = (int) (atomSize - headerSize); 147 bytesSearched += atomDataSize; 148 if (atomType == Atom.TYPE_ftyp) { 149 // Parse the atom and check the file type/brand is compatible with the extractors. 150 if (atomDataSize < 8) { 151 return false; 152 } 153 buffer.reset(atomDataSize); 154 input.peekFully(buffer.data, 0, atomDataSize); 155 int brandsCount = atomDataSize / 4; 156 for (int i = 0; i < brandsCount; i++) { 157 if (i == 1) { 158 // This index refers to the minorVersion, not a brand, so skip it. 159 buffer.skipBytes(4); 160 } else if (isCompatibleBrand(buffer.readInt())) { 161 foundGoodFileType = true; 162 break; 163 } 164 } 165 if (!foundGoodFileType) { 166 // The types were not compatible and there is only one ftyp atom, so reject the file. 167 return false; 168 } 169 } else if (atomDataSize != 0) { 170 // Skip the atom. 171 input.advancePeekPosition(atomDataSize); 172 } 173 } 174 return foundGoodFileType && fragmented == isFragmented; 175 } 176 177 /** 178 * Returns whether {@code brand} is an ftyp atom brand that is compatible with the MP4 extractors. 179 */ isCompatibleBrand(int brand)180 private static boolean isCompatibleBrand(int brand) { 181 // Accept all brands starting '3gp'. 182 if (brand >>> 8 == 0x00336770) { 183 return true; 184 } 185 for (int compatibleBrand : COMPATIBLE_BRANDS) { 186 if (compatibleBrand == brand) { 187 return true; 188 } 189 } 190 return false; 191 } 192 Sniffer()193 private Sniffer() { 194 // Prevent instantiation. 195 } 196 197 } 198