1 /*
2  * Copyright (C) 2019 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;
17 
18 import androidx.annotation.Nullable;
19 import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
20 import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
21 import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.CommentHeader;
22 import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
23 import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.PictureFrame;
24 import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder;
25 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
26 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
27 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
28 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
29 import java.io.IOException;
30 import java.nio.charset.Charset;
31 import java.util.Arrays;
32 import java.util.Collections;
33 import java.util.List;
34 
35 /**
36  * Reads and peeks FLAC stream metadata elements according to the <a
37  * href="https://xiph.org/flac/format.html">FLAC format specification</a>.
38  */
39 public final class FlacMetadataReader {
40 
41   /** Holds a {@link FlacStreamMetadata}. */
42   public static final class FlacStreamMetadataHolder {
43     /** The FLAC stream metadata. */
44     @Nullable public FlacStreamMetadata flacStreamMetadata;
45 
FlacStreamMetadataHolder(@ullable FlacStreamMetadata flacStreamMetadata)46     public FlacStreamMetadataHolder(@Nullable FlacStreamMetadata flacStreamMetadata) {
47       this.flacStreamMetadata = flacStreamMetadata;
48     }
49   }
50 
51   private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC"
52   private static final int SYNC_CODE = 0x3FFE;
53   private static final int SEEK_POINT_SIZE = 18;
54 
55   /**
56    * Peeks ID3 Data.
57    *
58    * @param input Input stream to peek the ID3 data from.
59    * @param parseData Whether to parse the ID3 frames.
60    * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData}
61    *     is {@code false}.
62    * @throws IOException If peeking from the input fails. In this case, there is no guarantee on the
63    *     peek position.
64    * @throws InterruptedException If interrupted while peeking from input. In this case, there is no
65    *     guarantee on the peek position.
66    */
67   @Nullable
peekId3Metadata(ExtractorInput input, boolean parseData)68   public static Metadata peekId3Metadata(ExtractorInput input, boolean parseData)
69       throws IOException, InterruptedException {
70     @Nullable
71     Id3Decoder.FramePredicate id3FramePredicate = parseData ? null : Id3Decoder.NO_FRAMES_PREDICATE;
72     @Nullable Metadata id3Metadata = new Id3Peeker().peekId3Data(input, id3FramePredicate);
73     return id3Metadata == null || id3Metadata.length() == 0 ? null : id3Metadata;
74   }
75 
76   /**
77    * Peeks the FLAC stream marker.
78    *
79    * @param input Input stream to peek the stream marker from.
80    * @return Whether the data peeked is the FLAC stream marker.
81    * @throws IOException If peeking from the input fails. In this case, the peek position is left
82    *     unchanged.
83    * @throws InterruptedException If interrupted while peeking from input. In this case, the peek
84    *     position is left unchanged.
85    */
checkAndPeekStreamMarker(ExtractorInput input)86   public static boolean checkAndPeekStreamMarker(ExtractorInput input)
87       throws IOException, InterruptedException {
88     ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
89     input.peekFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE);
90     return scratch.readUnsignedInt() == STREAM_MARKER;
91   }
92 
93   /**
94    * Reads ID3 Data.
95    *
96    * <p>If no exception is thrown, the peek position of {@code input} is aligned with the read
97    * position.
98    *
99    * @param input Input stream to read the ID3 data from.
100    * @param parseData Whether to parse the ID3 frames.
101    * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData}
102    *     is {@code false}.
103    * @throws IOException If reading from the input fails. In this case, the read position is left
104    *     unchanged and there is no guarantee on the peek position.
105    * @throws InterruptedException If interrupted while reading from input. In this case, the read
106    *     position is left unchanged and there is no guarantee on the peek position.
107    */
108   @Nullable
readId3Metadata(ExtractorInput input, boolean parseData)109   public static Metadata readId3Metadata(ExtractorInput input, boolean parseData)
110       throws IOException, InterruptedException {
111     input.resetPeekPosition();
112     long startingPeekPosition = input.getPeekPosition();
113     @Nullable Metadata id3Metadata = peekId3Metadata(input, parseData);
114     int peekedId3Bytes = (int) (input.getPeekPosition() - startingPeekPosition);
115     input.skipFully(peekedId3Bytes);
116     return id3Metadata;
117   }
118 
119   /**
120    * Reads the FLAC stream marker.
121    *
122    * @param input Input stream to read the stream marker from.
123    * @throws ParserException If an error occurs parsing the stream marker. In this case, the
124    *     position of {@code input} is advanced by {@link FlacConstants#STREAM_MARKER_SIZE} bytes.
125    * @throws IOException If reading from the input fails. In this case, the position is left
126    *     unchanged.
127    * @throws InterruptedException If interrupted while reading from input. In this case, the
128    *     position is left unchanged.
129    */
readStreamMarker(ExtractorInput input)130   public static void readStreamMarker(ExtractorInput input)
131       throws IOException, InterruptedException {
132     ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
133     input.readFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE);
134     if (scratch.readUnsignedInt() != STREAM_MARKER) {
135       throw new ParserException("Failed to read FLAC stream marker.");
136     }
137   }
138 
139   /**
140    * Reads one FLAC metadata block.
141    *
142    * <p>If no exception is thrown, the peek position of {@code input} is aligned with the read
143    * position.
144    *
145    * @param input Input stream to read the metadata block from (header included).
146    * @param metadataHolder A holder for the metadata read. If the stream info block (which must be
147    *     the first metadata block) is read, the holder contains a new instance representing the
148    *     stream info data. If the block read is a Vorbis comment block or a picture block, the
149    *     holder contains a copy of the existing stream metadata with the corresponding metadata
150    *     added. Otherwise, the metadata in the holder is unchanged.
151    * @return Whether the block read is the last metadata block.
152    * @throws IllegalArgumentException If the block read is not a stream info block and the metadata
153    *     in {@code metadataHolder} is {@code null}. In this case, the read position will be at the
154    *     start of a metadata block and there is no guarantee on the peek position.
155    * @throws IOException If reading from the input fails. In this case, the read position will be at
156    *     the start of a metadata block and there is no guarantee on the peek position.
157    * @throws InterruptedException If interrupted while reading from input. In this case, the read
158    *     position will be at the start of a metadata block and there is no guarantee on the peek
159    *     position.
160    */
readMetadataBlock( ExtractorInput input, FlacStreamMetadataHolder metadataHolder)161   public static boolean readMetadataBlock(
162       ExtractorInput input, FlacStreamMetadataHolder metadataHolder)
163       throws IOException, InterruptedException {
164     input.resetPeekPosition();
165     ParsableBitArray scratch = new ParsableBitArray(new byte[4]);
166     input.peekFully(scratch.data, 0, FlacConstants.METADATA_BLOCK_HEADER_SIZE);
167 
168     boolean isLastMetadataBlock = scratch.readBit();
169     int type = scratch.readBits(7);
170     int length = FlacConstants.METADATA_BLOCK_HEADER_SIZE + scratch.readBits(24);
171     if (type == FlacConstants.METADATA_TYPE_STREAM_INFO) {
172       metadataHolder.flacStreamMetadata = readStreamInfoBlock(input);
173     } else {
174       FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata;
175       if (flacStreamMetadata == null) {
176         throw new IllegalArgumentException();
177       }
178       if (type == FlacConstants.METADATA_TYPE_SEEK_TABLE) {
179         FlacStreamMetadata.SeekTable seekTable = readSeekTableMetadataBlock(input, length);
180         metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithSeekTable(seekTable);
181       } else if (type == FlacConstants.METADATA_TYPE_VORBIS_COMMENT) {
182         List<String> vorbisComments = readVorbisCommentMetadataBlock(input, length);
183         metadataHolder.flacStreamMetadata =
184             flacStreamMetadata.copyWithVorbisComments(vorbisComments);
185       } else if (type == FlacConstants.METADATA_TYPE_PICTURE) {
186         PictureFrame pictureFrame = readPictureMetadataBlock(input, length);
187         metadataHolder.flacStreamMetadata =
188             flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame));
189       } else {
190         input.skipFully(length);
191       }
192     }
193 
194     return isLastMetadataBlock;
195   }
196 
197   /**
198    * Reads a FLAC seek table metadata block.
199    *
200    * <p>The position of {@code data} is moved to the byte following the seek table metadata block
201    * (placeholder points included).
202    *
203    * @param data The array to read the data from, whose position must correspond to the seek table
204    *     metadata block (header included).
205    * @return The seek table, without the placeholder points.
206    */
readSeekTableMetadataBlock(ParsableByteArray data)207   public static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(ParsableByteArray data) {
208     data.skipBytes(1);
209     int length = data.readUnsignedInt24();
210 
211     long seekTableEndPosition = data.getPosition() + length;
212     int seekPointCount = length / SEEK_POINT_SIZE;
213     long[] pointSampleNumbers = new long[seekPointCount];
214     long[] pointOffsets = new long[seekPointCount];
215     for (int i = 0; i < seekPointCount; i++) {
216       // The sample number is expected to fit in a signed long, except if it is a placeholder, in
217       // which case its value is -1.
218       long sampleNumber = data.readLong();
219       if (sampleNumber == -1) {
220         pointSampleNumbers = Arrays.copyOf(pointSampleNumbers, i);
221         pointOffsets = Arrays.copyOf(pointOffsets, i);
222         break;
223       }
224       pointSampleNumbers[i] = sampleNumber;
225       pointOffsets[i] = data.readLong();
226       data.skipBytes(2);
227     }
228 
229     data.skipBytes((int) (seekTableEndPosition - data.getPosition()));
230     return new FlacStreamMetadata.SeekTable(pointSampleNumbers, pointOffsets);
231   }
232 
233   /**
234    * Returns the frame start marker, consisting of the 2 first bytes of the first frame.
235    *
236    * <p>The read position of {@code input} is left unchanged and the peek position is aligned with
237    * the read position.
238    *
239    * @param input Input stream to get the start marker from (starting from the read position).
240    * @return The frame start marker (which must be the same for all the frames in the stream).
241    * @throws ParserException If an error occurs parsing the frame start marker.
242    * @throws IOException If peeking from the input fails.
243    * @throws InterruptedException If interrupted while peeking from input.
244    */
getFrameStartMarker(ExtractorInput input)245   public static int getFrameStartMarker(ExtractorInput input)
246       throws IOException, InterruptedException {
247     input.resetPeekPosition();
248     ParsableByteArray scratch = new ParsableByteArray(2);
249     input.peekFully(scratch.data, 0, 2);
250 
251     int frameStartMarker = scratch.readUnsignedShort();
252     int syncCode = frameStartMarker >> 2;
253     if (syncCode != SYNC_CODE) {
254       input.resetPeekPosition();
255       throw new ParserException("First frame does not start with sync code.");
256     }
257 
258     input.resetPeekPosition();
259     return frameStartMarker;
260   }
261 
readStreamInfoBlock(ExtractorInput input)262   private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input)
263       throws IOException, InterruptedException {
264     byte[] scratchData = new byte[FlacConstants.STREAM_INFO_BLOCK_SIZE];
265     input.readFully(scratchData, 0, FlacConstants.STREAM_INFO_BLOCK_SIZE);
266     return new FlacStreamMetadata(
267         scratchData, /* offset= */ FlacConstants.METADATA_BLOCK_HEADER_SIZE);
268   }
269 
readSeekTableMetadataBlock( ExtractorInput input, int length)270   private static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(
271       ExtractorInput input, int length) throws IOException, InterruptedException {
272     ParsableByteArray scratch = new ParsableByteArray(length);
273     input.readFully(scratch.data, 0, length);
274     return readSeekTableMetadataBlock(scratch);
275   }
276 
readVorbisCommentMetadataBlock(ExtractorInput input, int length)277   private static List<String> readVorbisCommentMetadataBlock(ExtractorInput input, int length)
278       throws IOException, InterruptedException {
279     ParsableByteArray scratch = new ParsableByteArray(length);
280     input.readFully(scratch.data, 0, length);
281     scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
282     CommentHeader commentHeader =
283         VorbisUtil.readVorbisCommentHeader(
284             scratch, /* hasMetadataHeader= */ false, /* hasFramingBit= */ false);
285     return Arrays.asList(commentHeader.comments);
286   }
287 
readPictureMetadataBlock(ExtractorInput input, int length)288   private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length)
289       throws IOException, InterruptedException {
290     ParsableByteArray scratch = new ParsableByteArray(length);
291     input.readFully(scratch.data, 0, length);
292     scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
293 
294     int pictureType = scratch.readInt();
295     int mimeTypeLength = scratch.readInt();
296     String mimeType = scratch.readString(mimeTypeLength, Charset.forName(C.ASCII_NAME));
297     int descriptionLength = scratch.readInt();
298     String description = scratch.readString(descriptionLength);
299     int width = scratch.readInt();
300     int height = scratch.readInt();
301     int depth = scratch.readInt();
302     int colors = scratch.readInt();
303     int pictureDataLength = scratch.readInt();
304     byte[] pictureData = new byte[pictureDataLength];
305     scratch.readBytes(pictureData, 0, pictureDataLength);
306 
307     return new PictureFrame(
308         pictureType, mimeType, description, width, height, depth, colors, pictureData);
309   }
310 
FlacMetadataReader()311   private FlacMetadataReader() {}
312 }
313