• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.ext.flac;
17 
18 import static com.google.android.exoplayer2.util.Util.getPcmEncoding;
19 
20 import androidx.annotation.IntDef;
21 import androidx.annotation.Nullable;
22 import com.google.android.exoplayer2.C;
23 import com.google.android.exoplayer2.Format;
24 import com.google.android.exoplayer2.ext.flac.FlacBinarySearchSeeker.OutputFrameHolder;
25 import com.google.android.exoplayer2.extractor.Extractor;
26 import com.google.android.exoplayer2.extractor.ExtractorInput;
27 import com.google.android.exoplayer2.extractor.ExtractorOutput;
28 import com.google.android.exoplayer2.extractor.ExtractorsFactory;
29 import com.google.android.exoplayer2.extractor.FlacMetadataReader;
30 import com.google.android.exoplayer2.extractor.FlacStreamMetadata;
31 import com.google.android.exoplayer2.extractor.PositionHolder;
32 import com.google.android.exoplayer2.extractor.SeekMap;
33 import com.google.android.exoplayer2.extractor.SeekPoint;
34 import com.google.android.exoplayer2.extractor.TrackOutput;
35 import com.google.android.exoplayer2.metadata.Metadata;
36 import com.google.android.exoplayer2.util.Assertions;
37 import com.google.android.exoplayer2.util.MimeTypes;
38 import com.google.android.exoplayer2.util.ParsableByteArray;
39 import java.io.IOException;
40 import java.lang.annotation.Documented;
41 import java.lang.annotation.Retention;
42 import java.lang.annotation.RetentionPolicy;
43 import java.nio.ByteBuffer;
44 import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
45 import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
46 import org.checkerframework.checker.nullness.qual.RequiresNonNull;
47 
48 /**
49  * Facilitates the extraction of data from the FLAC container format.
50  */
51 public final class FlacExtractor implements Extractor {
52 
53   /** Factory that returns one extractor which is a {@link FlacExtractor}. */
54   public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new FlacExtractor()};
55 
56   /**
57    * Flags controlling the behavior of the extractor. Possible flag value is {@link
58    * #FLAG_DISABLE_ID3_METADATA}.
59    */
60   @Documented
61   @Retention(RetentionPolicy.SOURCE)
62   @IntDef(
63       flag = true,
64       value = {FLAG_DISABLE_ID3_METADATA})
65   public @interface Flags {}
66 
67   /**
68    * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not
69    * required.
70    */
71   public static final int FLAG_DISABLE_ID3_METADATA = 1;
72 
73   private final ParsableByteArray outputBuffer;
74   private final boolean id3MetadataDisabled;
75 
76   @Nullable private FlacDecoderJni decoderJni;
77   private @MonotonicNonNull ExtractorOutput extractorOutput;
78   private @MonotonicNonNull TrackOutput trackOutput;
79 
80   private boolean streamMetadataDecoded;
81   private @MonotonicNonNull FlacStreamMetadata streamMetadata;
82   private @MonotonicNonNull OutputFrameHolder outputFrameHolder;
83 
84   @Nullable private Metadata id3Metadata;
85   @Nullable private FlacBinarySearchSeeker binarySearchSeeker;
86 
87   /** Constructs an instance with {@code flags = 0}. */
FlacExtractor()88   public FlacExtractor() {
89     this(/* flags= */ 0);
90   }
91 
92   /**
93    * Constructs an instance.
94    *
95    * @param flags Flags that control the extractor's behavior. Possible flags are described by
96    *     {@link Flags}.
97    */
FlacExtractor(int flags)98   public FlacExtractor(int flags) {
99     outputBuffer = new ParsableByteArray();
100     id3MetadataDisabled = (flags & FLAG_DISABLE_ID3_METADATA) != 0;
101   }
102 
103   @Override
init(ExtractorOutput output)104   public void init(ExtractorOutput output) {
105     extractorOutput = output;
106     trackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO);
107     extractorOutput.endTracks();
108     try {
109       decoderJni = new FlacDecoderJni();
110     } catch (FlacDecoderException e) {
111       throw new RuntimeException(e);
112     }
113   }
114 
115   @Override
sniff(ExtractorInput input)116   public boolean sniff(ExtractorInput input) throws IOException {
117     id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ !id3MetadataDisabled);
118     return FlacMetadataReader.checkAndPeekStreamMarker(input);
119   }
120 
121   @Override
read(final ExtractorInput input, PositionHolder seekPosition)122   public int read(final ExtractorInput input, PositionHolder seekPosition) throws IOException {
123     if (input.getPosition() == 0 && !id3MetadataDisabled && id3Metadata == null) {
124       id3Metadata = FlacMetadataReader.peekId3Metadata(input, /* parseData= */ true);
125     }
126 
127     FlacDecoderJni decoderJni = initDecoderJni(input);
128     try {
129       decodeStreamMetadata(input);
130 
131       if (binarySearchSeeker != null && binarySearchSeeker.isSeeking()) {
132         return handlePendingSeek(input, seekPosition, outputBuffer, outputFrameHolder, trackOutput);
133       }
134 
135       ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
136       long lastDecodePosition = decoderJni.getDecodePosition();
137       try {
138         decoderJni.decodeSampleWithBacktrackPosition(outputByteBuffer, lastDecodePosition);
139       } catch (FlacDecoderJni.FlacFrameDecodeException e) {
140         throw new IOException("Cannot read frame at position " + lastDecodePosition, e);
141       }
142       int outputSize = outputByteBuffer.limit();
143       if (outputSize == 0) {
144         return RESULT_END_OF_INPUT;
145       }
146 
147       outputSample(outputBuffer, outputSize, decoderJni.getLastFrameTimestamp(), trackOutput);
148       return decoderJni.isEndOfData() ? RESULT_END_OF_INPUT : RESULT_CONTINUE;
149     } finally {
150       decoderJni.clearData();
151     }
152   }
153 
154   @Override
seek(long position, long timeUs)155   public void seek(long position, long timeUs) {
156     if (position == 0) {
157       streamMetadataDecoded = false;
158     }
159     if (decoderJni != null) {
160       decoderJni.reset(position);
161     }
162     if (binarySearchSeeker != null) {
163       binarySearchSeeker.setSeekTargetUs(timeUs);
164     }
165   }
166 
167   @Override
release()168   public void release() {
169     binarySearchSeeker = null;
170     if (decoderJni != null) {
171       decoderJni.release();
172       decoderJni = null;
173     }
174   }
175 
176   @EnsuresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Ensures initialized.
177   @SuppressWarnings({"contracts.postcondition.not.satisfied"})
initDecoderJni(ExtractorInput input)178   private FlacDecoderJni initDecoderJni(ExtractorInput input) {
179     FlacDecoderJni decoderJni = Assertions.checkNotNull(this.decoderJni);
180     decoderJni.setData(input);
181     return decoderJni;
182   }
183 
184   @RequiresNonNull({"decoderJni", "extractorOutput", "trackOutput"}) // Requires initialized.
185   @EnsuresNonNull({"streamMetadata", "outputFrameHolder"}) // Ensures stream metadata decoded.
186   @SuppressWarnings({"contracts.postcondition.not.satisfied"})
decodeStreamMetadata(ExtractorInput input)187   private void decodeStreamMetadata(ExtractorInput input) throws IOException {
188     if (streamMetadataDecoded) {
189       return;
190     }
191 
192     FlacDecoderJni flacDecoderJni = decoderJni;
193     FlacStreamMetadata streamMetadata;
194     try {
195       streamMetadata = flacDecoderJni.decodeStreamMetadata();
196     } catch (IOException e) {
197       flacDecoderJni.reset(/* newPosition= */ 0);
198       input.setRetryPosition(/* position= */ 0, e);
199       throw e;
200     }
201 
202     streamMetadataDecoded = true;
203     if (this.streamMetadata == null) {
204       this.streamMetadata = streamMetadata;
205       outputBuffer.reset(streamMetadata.getMaxDecodedFrameSize());
206       outputFrameHolder = new OutputFrameHolder(ByteBuffer.wrap(outputBuffer.data));
207       binarySearchSeeker =
208           outputSeekMap(
209               flacDecoderJni,
210               streamMetadata,
211               input.getLength(),
212               extractorOutput,
213               outputFrameHolder);
214       @Nullable
215       Metadata metadata = streamMetadata.getMetadataCopyWithAppendedEntriesFrom(id3Metadata);
216       outputFormat(streamMetadata, metadata, trackOutput);
217     }
218   }
219 
220   @RequiresNonNull("binarySearchSeeker")
handlePendingSeek( ExtractorInput input, PositionHolder seekPosition, ParsableByteArray outputBuffer, OutputFrameHolder outputFrameHolder, TrackOutput trackOutput)221   private int handlePendingSeek(
222       ExtractorInput input,
223       PositionHolder seekPosition,
224       ParsableByteArray outputBuffer,
225       OutputFrameHolder outputFrameHolder,
226       TrackOutput trackOutput)
227       throws IOException {
228     int seekResult = binarySearchSeeker.handlePendingSeek(input, seekPosition);
229     ByteBuffer outputByteBuffer = outputFrameHolder.byteBuffer;
230     if (seekResult == RESULT_CONTINUE && outputByteBuffer.limit() > 0) {
231       outputSample(outputBuffer, outputByteBuffer.limit(), outputFrameHolder.timeUs, trackOutput);
232     }
233     return seekResult;
234   }
235 
236   /**
237    * Outputs a {@link SeekMap} and returns a {@link FlacBinarySearchSeeker} if one is required to
238    * handle seeks.
239    */
240   @Nullable
outputSeekMap( FlacDecoderJni decoderJni, FlacStreamMetadata streamMetadata, long streamLength, ExtractorOutput output, OutputFrameHolder outputFrameHolder)241   private static FlacBinarySearchSeeker outputSeekMap(
242       FlacDecoderJni decoderJni,
243       FlacStreamMetadata streamMetadata,
244       long streamLength,
245       ExtractorOutput output,
246       OutputFrameHolder outputFrameHolder) {
247     boolean haveSeekTable = decoderJni.getSeekPoints(/* timeUs= */ 0) != null;
248     FlacBinarySearchSeeker binarySearchSeeker = null;
249     SeekMap seekMap;
250     if (haveSeekTable) {
251       seekMap = new FlacSeekMap(streamMetadata.getDurationUs(), decoderJni);
252     } else if (streamLength != C.LENGTH_UNSET && streamMetadata.totalSamples > 0) {
253       long firstFramePosition = decoderJni.getDecodePosition();
254       binarySearchSeeker =
255           new FlacBinarySearchSeeker(
256               streamMetadata, firstFramePosition, streamLength, decoderJni, outputFrameHolder);
257       seekMap = binarySearchSeeker.getSeekMap();
258     } else {
259       seekMap = new SeekMap.Unseekable(streamMetadata.getDurationUs());
260     }
261     output.seekMap(seekMap);
262     return binarySearchSeeker;
263   }
264 
outputFormat( FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output)265   private static void outputFormat(
266       FlacStreamMetadata streamMetadata, @Nullable Metadata metadata, TrackOutput output) {
267     Format mediaFormat =
268         new Format.Builder()
269             .setSampleMimeType(MimeTypes.AUDIO_RAW)
270             .setAverageBitrate(streamMetadata.getDecodedBitrate())
271             .setPeakBitrate(streamMetadata.getDecodedBitrate())
272             .setMaxInputSize(streamMetadata.getMaxDecodedFrameSize())
273             .setChannelCount(streamMetadata.channels)
274             .setSampleRate(streamMetadata.sampleRate)
275             .setPcmEncoding(getPcmEncoding(streamMetadata.bitsPerSample))
276             .setMetadata(metadata)
277             .build();
278     output.format(mediaFormat);
279   }
280 
outputSample( ParsableByteArray sampleData, int size, long timeUs, TrackOutput output)281   private static void outputSample(
282       ParsableByteArray sampleData, int size, long timeUs, TrackOutput output) {
283     sampleData.setPosition(0);
284     output.sampleData(sampleData, size);
285     output.sampleMetadata(
286         timeUs, C.BUFFER_FLAG_KEY_FRAME, size, /* offset= */ 0, /* encryptionData= */ null);
287   }
288 
289   /** A {@link SeekMap} implementation using a SeekTable within the Flac stream. */
290   private static final class FlacSeekMap implements SeekMap {
291 
292     private final long durationUs;
293     private final FlacDecoderJni decoderJni;
294 
FlacSeekMap(long durationUs, FlacDecoderJni decoderJni)295     public FlacSeekMap(long durationUs, FlacDecoderJni decoderJni) {
296       this.durationUs = durationUs;
297       this.decoderJni = decoderJni;
298     }
299 
300     @Override
isSeekable()301     public boolean isSeekable() {
302       return true;
303     }
304 
305     @Override
getSeekPoints(long timeUs)306     public SeekPoints getSeekPoints(long timeUs) {
307       @Nullable SeekPoints seekPoints = decoderJni.getSeekPoints(timeUs);
308       return seekPoints == null ? new SeekPoints(SeekPoint.START) : seekPoints;
309     }
310 
311     @Override
getDurationUs()312     public long getDurationUs() {
313       return durationUs;
314     }
315   }
316 }
317