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