summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/extractor/FlacMetadataReader.java
blob: c5413cf4596647bb0adeba09b711de5c1f5a17b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.mozilla.thirdparty.com.google.android.exoplayer2.extractor;

import androidx.annotation.Nullable;
import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException;
import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.VorbisUtil.CommentHeader;
import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.Metadata;
import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.flac.PictureFrame;
import org.mozilla.thirdparty.com.google.android.exoplayer2.metadata.id3.Id3Decoder;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacConstants;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.FlacStreamMetadata;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray;
import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * Reads and peeks FLAC stream metadata elements according to the <a
 * href="https://xiph.org/flac/format.html">FLAC format specification</a>.
 */
public final class FlacMetadataReader {

  /** Holds a {@link FlacStreamMetadata}. */
  public static final class FlacStreamMetadataHolder {
    /** The FLAC stream metadata. */
    @Nullable public FlacStreamMetadata flacStreamMetadata;

    public FlacStreamMetadataHolder(@Nullable FlacStreamMetadata flacStreamMetadata) {
      this.flacStreamMetadata = flacStreamMetadata;
    }
  }

  private static final int STREAM_MARKER = 0x664C6143; // ASCII for "fLaC"
  private static final int SYNC_CODE = 0x3FFE;
  private static final int SEEK_POINT_SIZE = 18;

  /**
   * Peeks ID3 Data.
   *
   * @param input Input stream to peek the ID3 data from.
   * @param parseData Whether to parse the ID3 frames.
   * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData}
   *     is {@code false}.
   * @throws IOException If peeking from the input fails. In this case, there is no guarantee on the
   *     peek position.
   * @throws InterruptedException If interrupted while peeking from input. In this case, there is no
   *     guarantee on the peek position.
   */
  @Nullable
  public static Metadata peekId3Metadata(ExtractorInput input, boolean parseData)
      throws IOException, InterruptedException {
    @Nullable
    Id3Decoder.FramePredicate id3FramePredicate = parseData ? null : Id3Decoder.NO_FRAMES_PREDICATE;
    @Nullable Metadata id3Metadata = new Id3Peeker().peekId3Data(input, id3FramePredicate);
    return id3Metadata == null || id3Metadata.length() == 0 ? null : id3Metadata;
  }

  /**
   * Peeks the FLAC stream marker.
   *
   * @param input Input stream to peek the stream marker from.
   * @return Whether the data peeked is the FLAC stream marker.
   * @throws IOException If peeking from the input fails. In this case, the peek position is left
   *     unchanged.
   * @throws InterruptedException If interrupted while peeking from input. In this case, the peek
   *     position is left unchanged.
   */
  public static boolean checkAndPeekStreamMarker(ExtractorInput input)
      throws IOException, InterruptedException {
    ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
    input.peekFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE);
    return scratch.readUnsignedInt() == STREAM_MARKER;
  }

  /**
   * Reads ID3 Data.
   *
   * <p>If no exception is thrown, the peek position of {@code input} is aligned with the read
   * position.
   *
   * @param input Input stream to read the ID3 data from.
   * @param parseData Whether to parse the ID3 frames.
   * @return The parsed ID3 data, or {@code null} if there is no such data or if {@code parseData}
   *     is {@code false}.
   * @throws IOException If reading from the input fails. In this case, the read position is left
   *     unchanged and there is no guarantee on the peek position.
   * @throws InterruptedException If interrupted while reading from input. In this case, the read
   *     position is left unchanged and there is no guarantee on the peek position.
   */
  @Nullable
  public static Metadata readId3Metadata(ExtractorInput input, boolean parseData)
      throws IOException, InterruptedException {
    input.resetPeekPosition();
    long startingPeekPosition = input.getPeekPosition();
    @Nullable Metadata id3Metadata = peekId3Metadata(input, parseData);
    int peekedId3Bytes = (int) (input.getPeekPosition() - startingPeekPosition);
    input.skipFully(peekedId3Bytes);
    return id3Metadata;
  }

  /**
   * Reads the FLAC stream marker.
   *
   * @param input Input stream to read the stream marker from.
   * @throws ParserException If an error occurs parsing the stream marker. In this case, the
   *     position of {@code input} is advanced by {@link FlacConstants#STREAM_MARKER_SIZE} bytes.
   * @throws IOException If reading from the input fails. In this case, the position is left
   *     unchanged.
   * @throws InterruptedException If interrupted while reading from input. In this case, the
   *     position is left unchanged.
   */
  public static void readStreamMarker(ExtractorInput input)
      throws IOException, InterruptedException {
    ParsableByteArray scratch = new ParsableByteArray(FlacConstants.STREAM_MARKER_SIZE);
    input.readFully(scratch.data, 0, FlacConstants.STREAM_MARKER_SIZE);
    if (scratch.readUnsignedInt() != STREAM_MARKER) {
      throw new ParserException("Failed to read FLAC stream marker.");
    }
  }

  /**
   * Reads one FLAC metadata block.
   *
   * <p>If no exception is thrown, the peek position of {@code input} is aligned with the read
   * position.
   *
   * @param input Input stream to read the metadata block from (header included).
   * @param metadataHolder A holder for the metadata read. If the stream info block (which must be
   *     the first metadata block) is read, the holder contains a new instance representing the
   *     stream info data. If the block read is a Vorbis comment block or a picture block, the
   *     holder contains a copy of the existing stream metadata with the corresponding metadata
   *     added. Otherwise, the metadata in the holder is unchanged.
   * @return Whether the block read is the last metadata block.
   * @throws IllegalArgumentException If the block read is not a stream info block and the metadata
   *     in {@code metadataHolder} is {@code null}. In this case, the read position will be at the
   *     start of a metadata block and there is no guarantee on the peek position.
   * @throws IOException If reading from the input fails. In this case, the read position will be at
   *     the start of a metadata block and there is no guarantee on the peek position.
   * @throws InterruptedException If interrupted while reading from input. In this case, the read
   *     position will be at the start of a metadata block and there is no guarantee on the peek
   *     position.
   */
  public static boolean readMetadataBlock(
      ExtractorInput input, FlacStreamMetadataHolder metadataHolder)
      throws IOException, InterruptedException {
    input.resetPeekPosition();
    ParsableBitArray scratch = new ParsableBitArray(new byte[4]);
    input.peekFully(scratch.data, 0, FlacConstants.METADATA_BLOCK_HEADER_SIZE);

    boolean isLastMetadataBlock = scratch.readBit();
    int type = scratch.readBits(7);
    int length = FlacConstants.METADATA_BLOCK_HEADER_SIZE + scratch.readBits(24);
    if (type == FlacConstants.METADATA_TYPE_STREAM_INFO) {
      metadataHolder.flacStreamMetadata = readStreamInfoBlock(input);
    } else {
      FlacStreamMetadata flacStreamMetadata = metadataHolder.flacStreamMetadata;
      if (flacStreamMetadata == null) {
        throw new IllegalArgumentException();
      }
      if (type == FlacConstants.METADATA_TYPE_SEEK_TABLE) {
        FlacStreamMetadata.SeekTable seekTable = readSeekTableMetadataBlock(input, length);
        metadataHolder.flacStreamMetadata = flacStreamMetadata.copyWithSeekTable(seekTable);
      } else if (type == FlacConstants.METADATA_TYPE_VORBIS_COMMENT) {
        List<String> vorbisComments = readVorbisCommentMetadataBlock(input, length);
        metadataHolder.flacStreamMetadata =
            flacStreamMetadata.copyWithVorbisComments(vorbisComments);
      } else if (type == FlacConstants.METADATA_TYPE_PICTURE) {
        PictureFrame pictureFrame = readPictureMetadataBlock(input, length);
        metadataHolder.flacStreamMetadata =
            flacStreamMetadata.copyWithPictureFrames(Collections.singletonList(pictureFrame));
      } else {
        input.skipFully(length);
      }
    }

    return isLastMetadataBlock;
  }

  /**
   * Reads a FLAC seek table metadata block.
   *
   * <p>The position of {@code data} is moved to the byte following the seek table metadata block
   * (placeholder points included).
   *
   * @param data The array to read the data from, whose position must correspond to the seek table
   *     metadata block (header included).
   * @return The seek table, without the placeholder points.
   */
  public static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(ParsableByteArray data) {
    data.skipBytes(1);
    int length = data.readUnsignedInt24();

    long seekTableEndPosition = data.getPosition() + length;
    int seekPointCount = length / SEEK_POINT_SIZE;
    long[] pointSampleNumbers = new long[seekPointCount];
    long[] pointOffsets = new long[seekPointCount];
    for (int i = 0; i < seekPointCount; i++) {
      // The sample number is expected to fit in a signed long, except if it is a placeholder, in
      // which case its value is -1.
      long sampleNumber = data.readLong();
      if (sampleNumber == -1) {
        pointSampleNumbers = Arrays.copyOf(pointSampleNumbers, i);
        pointOffsets = Arrays.copyOf(pointOffsets, i);
        break;
      }
      pointSampleNumbers[i] = sampleNumber;
      pointOffsets[i] = data.readLong();
      data.skipBytes(2);
    }

    data.skipBytes((int) (seekTableEndPosition - data.getPosition()));
    return new FlacStreamMetadata.SeekTable(pointSampleNumbers, pointOffsets);
  }

  /**
   * Returns the frame start marker, consisting of the 2 first bytes of the first frame.
   *
   * <p>The read position of {@code input} is left unchanged and the peek position is aligned with
   * the read position.
   *
   * @param input Input stream to get the start marker from (starting from the read position).
   * @return The frame start marker (which must be the same for all the frames in the stream).
   * @throws ParserException If an error occurs parsing the frame start marker.
   * @throws IOException If peeking from the input fails.
   * @throws InterruptedException If interrupted while peeking from input.
   */
  public static int getFrameStartMarker(ExtractorInput input)
      throws IOException, InterruptedException {
    input.resetPeekPosition();
    ParsableByteArray scratch = new ParsableByteArray(2);
    input.peekFully(scratch.data, 0, 2);

    int frameStartMarker = scratch.readUnsignedShort();
    int syncCode = frameStartMarker >> 2;
    if (syncCode != SYNC_CODE) {
      input.resetPeekPosition();
      throw new ParserException("First frame does not start with sync code.");
    }

    input.resetPeekPosition();
    return frameStartMarker;
  }

  private static FlacStreamMetadata readStreamInfoBlock(ExtractorInput input)
      throws IOException, InterruptedException {
    byte[] scratchData = new byte[FlacConstants.STREAM_INFO_BLOCK_SIZE];
    input.readFully(scratchData, 0, FlacConstants.STREAM_INFO_BLOCK_SIZE);
    return new FlacStreamMetadata(
        scratchData, /* offset= */ FlacConstants.METADATA_BLOCK_HEADER_SIZE);
  }

  private static FlacStreamMetadata.SeekTable readSeekTableMetadataBlock(
      ExtractorInput input, int length) throws IOException, InterruptedException {
    ParsableByteArray scratch = new ParsableByteArray(length);
    input.readFully(scratch.data, 0, length);
    return readSeekTableMetadataBlock(scratch);
  }

  private static List<String> readVorbisCommentMetadataBlock(ExtractorInput input, int length)
      throws IOException, InterruptedException {
    ParsableByteArray scratch = new ParsableByteArray(length);
    input.readFully(scratch.data, 0, length);
    scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);
    CommentHeader commentHeader =
        VorbisUtil.readVorbisCommentHeader(
            scratch, /* hasMetadataHeader= */ false, /* hasFramingBit= */ false);
    return Arrays.asList(commentHeader.comments);
  }

  private static PictureFrame readPictureMetadataBlock(ExtractorInput input, int length)
      throws IOException, InterruptedException {
    ParsableByteArray scratch = new ParsableByteArray(length);
    input.readFully(scratch.data, 0, length);
    scratch.skipBytes(FlacConstants.METADATA_BLOCK_HEADER_SIZE);

    int pictureType = scratch.readInt();
    int mimeTypeLength = scratch.readInt();
    String mimeType = scratch.readString(mimeTypeLength, Charset.forName(C.ASCII_NAME));
    int descriptionLength = scratch.readInt();
    String description = scratch.readString(descriptionLength);
    int width = scratch.readInt();
    int height = scratch.readInt();
    int depth = scratch.readInt();
    int colors = scratch.readInt();
    int pictureDataLength = scratch.readInt();
    byte[] pictureData = new byte[pictureDataLength];
    scratch.readBytes(pictureData, 0, pictureDataLength);

    return new PictureFrame(
        pictureType, mimeType, description, width, height, depth, colors, pictureData);
  }

  private FlacMetadataReader() {}
}