• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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