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