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