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