| /* |
| * Copyright (C) 2016 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 com.google.android.exoplayer2.extractor.mp3; |
| |
| import androidx.annotation.IntDef; |
| import androidx.annotation.Nullable; |
| import com.google.android.exoplayer2.C; |
| import com.google.android.exoplayer2.Format; |
| import com.google.android.exoplayer2.ParserException; |
| import com.google.android.exoplayer2.PlaybackException; |
| import com.google.android.exoplayer2.Player; |
| import com.google.android.exoplayer2.audio.MpegAudioUtil; |
| import com.google.android.exoplayer2.extractor.DummyTrackOutput; |
| import com.google.android.exoplayer2.extractor.Extractor; |
| import com.google.android.exoplayer2.extractor.ExtractorInput; |
| import com.google.android.exoplayer2.extractor.ExtractorOutput; |
| import com.google.android.exoplayer2.extractor.ExtractorsFactory; |
| import com.google.android.exoplayer2.extractor.GaplessInfoHolder; |
| import com.google.android.exoplayer2.extractor.Id3Peeker; |
| import com.google.android.exoplayer2.extractor.PositionHolder; |
| import com.google.android.exoplayer2.extractor.TrackOutput; |
| import com.google.android.exoplayer2.extractor.mp3.Seeker.UnseekableSeeker; |
| import com.google.android.exoplayer2.metadata.Metadata; |
| import com.google.android.exoplayer2.metadata.id3.Id3Decoder; |
| import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; |
| import com.google.android.exoplayer2.metadata.id3.MlltFrame; |
| import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; |
| import com.google.android.exoplayer2.util.Assertions; |
| import com.google.android.exoplayer2.util.ParsableByteArray; |
| import com.google.android.exoplayer2.util.Util; |
| import java.io.EOFException; |
| import java.io.IOException; |
| import java.lang.annotation.Documented; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import org.checkerframework.checker.nullness.qual.EnsuresNonNull; |
| import org.checkerframework.checker.nullness.qual.MonotonicNonNull; |
| import org.checkerframework.checker.nullness.qual.RequiresNonNull; |
| |
| /** Extracts data from the MP3 container format. */ |
| public final class Mp3Extractor implements Extractor { |
| |
| /** Factory for {@link Mp3Extractor} instances. */ |
| public static final ExtractorsFactory FACTORY = () -> new Extractor[] {new Mp3Extractor()}; |
| |
| /** |
| * Flags controlling the behavior of the extractor. Possible flag values are {@link |
| * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, {@link #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS}, |
| * {@link #FLAG_ENABLE_INDEX_SEEKING} and {@link #FLAG_DISABLE_ID3_METADATA}. |
| */ |
| @Documented |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef( |
| flag = true, |
| value = { |
| FLAG_ENABLE_CONSTANT_BITRATE_SEEKING, |
| FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS, |
| FLAG_ENABLE_INDEX_SEEKING, |
| FLAG_DISABLE_ID3_METADATA |
| }) |
| public @interface Flags {} |
| /** |
| * Flag to force enable seeking using a constant bitrate assumption in cases where seeking would |
| * otherwise not be possible. |
| * |
| * <p>This flag is ignored if {@link #FLAG_ENABLE_INDEX_SEEKING} is set. |
| */ |
| public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING = 1; |
| /** |
| * Like {@link #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING}, except that seeking is also enabled in |
| * cases where the content length (and hence the duration of the media) is unknown. Application |
| * code should ensure that requested seek positions are valid when using this flag, or be ready to |
| * handle playback failures reported through {@link Player.Listener#onPlayerError} with {@link |
| * PlaybackException#errorCode} set to {@link |
| * PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}. |
| * |
| * <p>If this flag is set, then the behavior enabled by {@link |
| * #FLAG_ENABLE_CONSTANT_BITRATE_SEEKING} is implicitly enabled. |
| * |
| * <p>This flag is ignored if {@link #FLAG_ENABLE_INDEX_SEEKING} is set. |
| */ |
| public static final int FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS = 1 << 1; |
| |
| /** |
| * Flag to force index seeking, in which a time-to-byte mapping is built as the file is read. |
| * |
| * <p>This seeker may require to scan a significant portion of the file to compute a seek point. |
| * Therefore, it should only be used if one of the following is true: |
| * |
| * <ul> |
| * <li>The file is small. |
| * <li>The bitrate is variable (or it's unknown whether it's variable) and the file does not |
| * provide precise enough seeking metadata. |
| * </ul> |
| */ |
| public static final int FLAG_ENABLE_INDEX_SEEKING = 1 << 2; |
| /** |
| * Flag to disable parsing of ID3 metadata. Can be set to save memory if ID3 metadata is not |
| * required. |
| */ |
| public static final int FLAG_DISABLE_ID3_METADATA = 1 << 3; |
| |
| /** Predicate that matches ID3 frames containing only required gapless/seeking metadata. */ |
| private static final FramePredicate REQUIRED_ID3_FRAME_PREDICATE = |
| (majorVersion, id0, id1, id2, id3) -> |
| ((id0 == 'C' && id1 == 'O' && id2 == 'M' && (id3 == 'M' || majorVersion == 2)) |
| || (id0 == 'M' && id1 == 'L' && id2 == 'L' && (id3 == 'T' || majorVersion == 2))); |
| |
| /** The maximum number of bytes to search when synchronizing, before giving up. */ |
| private static final int MAX_SYNC_BYTES = 128 * 1024; |
| /** |
| * The maximum number of bytes to peek when sniffing, excluding the ID3 header, before giving up. |
| */ |
| private static final int MAX_SNIFF_BYTES = 32 * 1024; |
| /** Maximum length of data read into {@link #scratch}. */ |
| private static final int SCRATCH_LENGTH = 10; |
| |
| /** Mask that includes the audio header values that must match between frames. */ |
| private static final int MPEG_AUDIO_HEADER_MASK = 0xFFFE0C00; |
| |
| private static final int SEEK_HEADER_XING = 0x58696e67; |
| private static final int SEEK_HEADER_INFO = 0x496e666f; |
| private static final int SEEK_HEADER_VBRI = 0x56425249; |
| private static final int SEEK_HEADER_UNSET = 0; |
| |
| @Flags private final int flags; |
| private final long forcedFirstSampleTimestampUs; |
| private final ParsableByteArray scratch; |
| private final MpegAudioUtil.Header synchronizedHeader; |
| private final GaplessInfoHolder gaplessInfoHolder; |
| private final Id3Peeker id3Peeker; |
| private final TrackOutput skippingTrackOutput; |
| |
| private @MonotonicNonNull ExtractorOutput extractorOutput; |
| private @MonotonicNonNull TrackOutput realTrackOutput; |
| private TrackOutput currentTrackOutput; // skippingTrackOutput or realTrackOutput. |
| |
| private int synchronizedHeaderData; |
| |
| @Nullable private Metadata metadata; |
| private long basisTimeUs; |
| private long samplesRead; |
| private long firstSamplePosition; |
| private int sampleBytesRemaining; |
| |
| private @MonotonicNonNull Seeker seeker; |
| private boolean disableSeeking; |
| private boolean isSeekInProgress; |
| private long seekTimeUs; |
| |
| public Mp3Extractor() { |
| this(0); |
| } |
| |
| /** @param flags Flags that control the extractor's behavior. */ |
| public Mp3Extractor(@Flags int flags) { |
| this(flags, C.TIME_UNSET); |
| } |
| |
| /** |
| * @param flags Flags that control the extractor's behavior. |
| * @param forcedFirstSampleTimestampUs A timestamp to force for the first sample, or {@link |
| * C#TIME_UNSET} if forcing is not required. |
| */ |
| public Mp3Extractor(@Flags int flags, long forcedFirstSampleTimestampUs) { |
| if ((flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0) { |
| flags |= FLAG_ENABLE_CONSTANT_BITRATE_SEEKING; |
| } |
| this.flags = flags; |
| this.forcedFirstSampleTimestampUs = forcedFirstSampleTimestampUs; |
| scratch = new ParsableByteArray(SCRATCH_LENGTH); |
| synchronizedHeader = new MpegAudioUtil.Header(); |
| gaplessInfoHolder = new GaplessInfoHolder(); |
| basisTimeUs = C.TIME_UNSET; |
| id3Peeker = new Id3Peeker(); |
| skippingTrackOutput = new DummyTrackOutput(); |
| currentTrackOutput = skippingTrackOutput; |
| } |
| |
| // Extractor implementation. |
| |
| @Override |
| public boolean sniff(ExtractorInput input) throws IOException { |
| return synchronize(input, true); |
| } |
| |
| @Override |
| public void init(ExtractorOutput output) { |
| extractorOutput = output; |
| realTrackOutput = extractorOutput.track(0, C.TRACK_TYPE_AUDIO); |
| currentTrackOutput = realTrackOutput; |
| extractorOutput.endTracks(); |
| } |
| |
| @Override |
| public void seek(long position, long timeUs) { |
| synchronizedHeaderData = 0; |
| basisTimeUs = C.TIME_UNSET; |
| samplesRead = 0; |
| sampleBytesRemaining = 0; |
| seekTimeUs = timeUs; |
| if (seeker instanceof IndexSeeker && !((IndexSeeker) seeker).isTimeUsInIndex(timeUs)) { |
| isSeekInProgress = true; |
| currentTrackOutput = skippingTrackOutput; |
| } |
| } |
| |
| @Override |
| public void release() { |
| // Do nothing |
| } |
| |
| @Override |
| public int read(ExtractorInput input, PositionHolder seekPosition) throws IOException { |
| assertInitialized(); |
| int readResult = readInternal(input); |
| if (readResult == RESULT_END_OF_INPUT && seeker instanceof IndexSeeker) { |
| // Duration is exact when index seeker is used. |
| long durationUs = computeTimeUs(samplesRead); |
| if (seeker.getDurationUs() != durationUs) { |
| ((IndexSeeker) seeker).setDurationUs(durationUs); |
| extractorOutput.seekMap(seeker); |
| } |
| } |
| return readResult; |
| } |
| |
| /** |
| * Disables the extractor from being able to seek through the media. |
| * |
| * <p>Please note that this needs to be called before {@link #read}. |
| */ |
| public void disableSeeking() { |
| disableSeeking = true; |
| } |
| |
| // Internal methods. |
| |
| @RequiresNonNull({"extractorOutput", "realTrackOutput"}) |
| private int readInternal(ExtractorInput input) throws IOException { |
| if (synchronizedHeaderData == 0) { |
| try { |
| synchronize(input, false); |
| } catch (EOFException e) { |
| return RESULT_END_OF_INPUT; |
| } |
| } |
| if (seeker == null) { |
| seeker = computeSeeker(input); |
| extractorOutput.seekMap(seeker); |
| currentTrackOutput.format( |
| new Format.Builder() |
| .setSampleMimeType(synchronizedHeader.mimeType) |
| .setMaxInputSize(MpegAudioUtil.MAX_FRAME_SIZE_BYTES) |
| .setChannelCount(synchronizedHeader.channels) |
| .setSampleRate(synchronizedHeader.sampleRate) |
| .setEncoderDelay(gaplessInfoHolder.encoderDelay) |
| .setEncoderPadding(gaplessInfoHolder.encoderPadding) |
| .setMetadata((flags & FLAG_DISABLE_ID3_METADATA) != 0 ? null : metadata) |
| .build()); |
| firstSamplePosition = input.getPosition(); |
| } else if (firstSamplePosition != 0) { |
| long inputPosition = input.getPosition(); |
| if (inputPosition < firstSamplePosition) { |
| // Skip past the seek frame. |
| input.skipFully((int) (firstSamplePosition - inputPosition)); |
| } |
| } |
| return readSample(input); |
| } |
| |
| @RequiresNonNull({"realTrackOutput", "seeker"}) |
| private int readSample(ExtractorInput extractorInput) throws IOException { |
| if (sampleBytesRemaining == 0) { |
| extractorInput.resetPeekPosition(); |
| if (peekEndOfStreamOrHeader(extractorInput)) { |
| return RESULT_END_OF_INPUT; |
| } |
| scratch.setPosition(0); |
| int sampleHeaderData = scratch.readInt(); |
| if (!headersMatch(sampleHeaderData, synchronizedHeaderData) |
| || MpegAudioUtil.getFrameSize(sampleHeaderData) == C.LENGTH_UNSET) { |
| // We have lost synchronization, so attempt to resynchronize starting at the next byte. |
| extractorInput.skipFully(1); |
| synchronizedHeaderData = 0; |
| return RESULT_CONTINUE; |
| } |
| synchronizedHeader.setForHeaderData(sampleHeaderData); |
| if (basisTimeUs == C.TIME_UNSET) { |
| basisTimeUs = seeker.getTimeUs(extractorInput.getPosition()); |
| if (forcedFirstSampleTimestampUs != C.TIME_UNSET) { |
| long embeddedFirstSampleTimestampUs = seeker.getTimeUs(0); |
| basisTimeUs += forcedFirstSampleTimestampUs - embeddedFirstSampleTimestampUs; |
| } |
| } |
| sampleBytesRemaining = synchronizedHeader.frameSize; |
| if (seeker instanceof IndexSeeker) { |
| IndexSeeker indexSeeker = (IndexSeeker) seeker; |
| // Add seek point corresponding to the next frame instead of the current one to be able to |
| // start writing to the realTrackOutput on time when a seek is in progress. |
| indexSeeker.maybeAddSeekPoint( |
| computeTimeUs(samplesRead + synchronizedHeader.samplesPerFrame), |
| extractorInput.getPosition() + synchronizedHeader.frameSize); |
| if (isSeekInProgress && indexSeeker.isTimeUsInIndex(seekTimeUs)) { |
| isSeekInProgress = false; |
| currentTrackOutput = realTrackOutput; |
| } |
| } |
| } |
| int bytesAppended = currentTrackOutput.sampleData(extractorInput, sampleBytesRemaining, true); |
| if (bytesAppended == C.RESULT_END_OF_INPUT) { |
| return RESULT_END_OF_INPUT; |
| } |
| sampleBytesRemaining -= bytesAppended; |
| if (sampleBytesRemaining > 0) { |
| return RESULT_CONTINUE; |
| } |
| currentTrackOutput.sampleMetadata( |
| computeTimeUs(samplesRead), C.BUFFER_FLAG_KEY_FRAME, synchronizedHeader.frameSize, 0, null); |
| samplesRead += synchronizedHeader.samplesPerFrame; |
| sampleBytesRemaining = 0; |
| return RESULT_CONTINUE; |
| } |
| |
| private long computeTimeUs(long samplesRead) { |
| return basisTimeUs + samplesRead * C.MICROS_PER_SECOND / synchronizedHeader.sampleRate; |
| } |
| |
| private boolean synchronize(ExtractorInput input, boolean sniffing) throws IOException { |
| int validFrameCount = 0; |
| int candidateSynchronizedHeaderData = 0; |
| int peekedId3Bytes = 0; |
| int searchedBytes = 0; |
| int searchLimitBytes = sniffing ? MAX_SNIFF_BYTES : MAX_SYNC_BYTES; |
| input.resetPeekPosition(); |
| if (input.getPosition() == 0) { |
| // We need to parse enough ID3 metadata to retrieve any gapless/seeking playback information |
| // even if ID3 metadata parsing is disabled. |
| boolean parseAllId3Frames = (flags & FLAG_DISABLE_ID3_METADATA) == 0; |
| Id3Decoder.FramePredicate id3FramePredicate = |
| parseAllId3Frames ? null : REQUIRED_ID3_FRAME_PREDICATE; |
| metadata = id3Peeker.peekId3Data(input, id3FramePredicate); |
| if (metadata != null) { |
| gaplessInfoHolder.setFromMetadata(metadata); |
| } |
| peekedId3Bytes = (int) input.getPeekPosition(); |
| if (!sniffing) { |
| input.skipFully(peekedId3Bytes); |
| } |
| } |
| while (true) { |
| if (peekEndOfStreamOrHeader(input)) { |
| if (validFrameCount > 0) { |
| // We reached the end of the stream but found at least one valid frame. |
| break; |
| } |
| throw new EOFException(); |
| } |
| scratch.setPosition(0); |
| int headerData = scratch.readInt(); |
| int frameSize; |
| if ((candidateSynchronizedHeaderData != 0 |
| && !headersMatch(headerData, candidateSynchronizedHeaderData)) |
| || (frameSize = MpegAudioUtil.getFrameSize(headerData)) == C.LENGTH_UNSET) { |
| // The header doesn't match the candidate header or is invalid. Try the next byte offset. |
| if (searchedBytes++ == searchLimitBytes) { |
| if (!sniffing) { |
| throw ParserException.createForMalformedContainer( |
| "Searched too many bytes.", /* cause= */ null); |
| } |
| return false; |
| } |
| validFrameCount = 0; |
| candidateSynchronizedHeaderData = 0; |
| if (sniffing) { |
| input.resetPeekPosition(); |
| input.advancePeekPosition(peekedId3Bytes + searchedBytes); |
| } else { |
| input.skipFully(1); |
| } |
| } else { |
| // The header matches the candidate header and/or is valid. |
| validFrameCount++; |
| if (validFrameCount == 1) { |
| synchronizedHeader.setForHeaderData(headerData); |
| candidateSynchronizedHeaderData = headerData; |
| } else if (validFrameCount == 4) { |
| break; |
| } |
| input.advancePeekPosition(frameSize - 4); |
| } |
| } |
| // Prepare to read the synchronized frame. |
| if (sniffing) { |
| input.skipFully(peekedId3Bytes + searchedBytes); |
| } else { |
| input.resetPeekPosition(); |
| } |
| synchronizedHeaderData = candidateSynchronizedHeaderData; |
| return true; |
| } |
| |
| /** |
| * Returns whether the extractor input is peeking the end of the stream. If {@code false}, |
| * populates the scratch buffer with the next four bytes. |
| */ |
| private boolean peekEndOfStreamOrHeader(ExtractorInput extractorInput) throws IOException { |
| if (seeker != null) { |
| long dataEndPosition = seeker.getDataEndPosition(); |
| if (dataEndPosition != C.POSITION_UNSET |
| && extractorInput.getPeekPosition() > dataEndPosition - 4) { |
| return true; |
| } |
| } |
| try { |
| return !extractorInput.peekFully( |
| scratch.getData(), /* offset= */ 0, /* length= */ 4, /* allowEndOfInput= */ true); |
| } catch (EOFException e) { |
| return true; |
| } |
| } |
| |
| private Seeker computeSeeker(ExtractorInput input) throws IOException { |
| // Read past any seek frame and set the seeker based on metadata or a seek frame. Metadata |
| // takes priority as it can provide greater precision. |
| Seeker seekFrameSeeker = maybeReadSeekFrame(input); |
| Seeker metadataSeeker = maybeHandleSeekMetadata(metadata, input.getPosition()); |
| |
| if (disableSeeking) { |
| return new UnseekableSeeker(); |
| } |
| |
| @Nullable Seeker resultSeeker = null; |
| if ((flags & FLAG_ENABLE_INDEX_SEEKING) != 0) { |
| long durationUs; |
| long dataEndPosition = C.POSITION_UNSET; |
| if (metadataSeeker != null) { |
| durationUs = metadataSeeker.getDurationUs(); |
| dataEndPosition = metadataSeeker.getDataEndPosition(); |
| } else if (seekFrameSeeker != null) { |
| durationUs = seekFrameSeeker.getDurationUs(); |
| dataEndPosition = seekFrameSeeker.getDataEndPosition(); |
| } else { |
| durationUs = getId3TlenUs(metadata); |
| } |
| resultSeeker = |
| new IndexSeeker( |
| durationUs, /* dataStartPosition= */ input.getPosition(), dataEndPosition); |
| } else if (metadataSeeker != null) { |
| resultSeeker = metadataSeeker; |
| } else if (seekFrameSeeker != null) { |
| resultSeeker = seekFrameSeeker; |
| } |
| |
| if (resultSeeker == null |
| || (!resultSeeker.isSeekable() && (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING) != 0)) { |
| resultSeeker = |
| getConstantBitrateSeeker( |
| input, (flags & FLAG_ENABLE_CONSTANT_BITRATE_SEEKING_ALWAYS) != 0); |
| } |
| |
| return resultSeeker; |
| } |
| |
| /** |
| * Consumes the next frame from the {@code input} if it contains VBRI or Xing seeking metadata, |
| * returning a {@link Seeker} if the metadata was present and valid, or {@code null} otherwise. |
| * After this method returns, the input position is the start of the first frame of audio. |
| * |
| * @param input The {@link ExtractorInput} from which to read. |
| * @return A {@link Seeker} if seeking metadata was present and valid, or {@code null} otherwise. |
| * @throws IOException Thrown if there was an error reading from the stream. Not expected if the |
| * next two frames were already peeked during synchronization. |
| */ |
| @Nullable |
| private Seeker maybeReadSeekFrame(ExtractorInput input) throws IOException { |
| ParsableByteArray frame = new ParsableByteArray(synchronizedHeader.frameSize); |
| input.peekFully(frame.getData(), 0, synchronizedHeader.frameSize); |
| int xingBase = |
| (synchronizedHeader.version & 1) != 0 |
| ? (synchronizedHeader.channels != 1 ? 36 : 21) // MPEG 1 |
| : (synchronizedHeader.channels != 1 ? 21 : 13); // MPEG 2 or 2.5 |
| int seekHeader = getSeekFrameHeader(frame, xingBase); |
| @Nullable Seeker seeker; |
| if (seekHeader == SEEK_HEADER_XING || seekHeader == SEEK_HEADER_INFO) { |
| seeker = XingSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); |
| if (seeker != null && !gaplessInfoHolder.hasGaplessInfo()) { |
| // If there is a Xing header, read gapless playback metadata at a fixed offset. |
| input.resetPeekPosition(); |
| input.advancePeekPosition(xingBase + 141); |
| input.peekFully(scratch.getData(), 0, 3); |
| scratch.setPosition(0); |
| gaplessInfoHolder.setFromXingHeaderValue(scratch.readUnsignedInt24()); |
| } |
| input.skipFully(synchronizedHeader.frameSize); |
| if (seeker != null && !seeker.isSeekable() && seekHeader == SEEK_HEADER_INFO) { |
| // Fall back to constant bitrate seeking for Info headers missing a table of contents. |
| return getConstantBitrateSeeker(input, /* allowSeeksIfLengthUnknown= */ false); |
| } |
| } else if (seekHeader == SEEK_HEADER_VBRI) { |
| seeker = VbriSeeker.create(input.getLength(), input.getPosition(), synchronizedHeader, frame); |
| input.skipFully(synchronizedHeader.frameSize); |
| } else { // seekerHeader == SEEK_HEADER_UNSET |
| // This frame doesn't contain seeking information, so reset the peek position. |
| seeker = null; |
| input.resetPeekPosition(); |
| } |
| return seeker; |
| } |
| |
| /** Peeks the next frame and returns a {@link ConstantBitrateSeeker} based on its bitrate. */ |
| private Seeker getConstantBitrateSeeker(ExtractorInput input, boolean allowSeeksIfLengthUnknown) |
| throws IOException { |
| input.peekFully(scratch.getData(), 0, 4); |
| scratch.setPosition(0); |
| synchronizedHeader.setForHeaderData(scratch.readInt()); |
| return new ConstantBitrateSeeker( |
| input.getLength(), input.getPosition(), synchronizedHeader, allowSeeksIfLengthUnknown); |
| } |
| |
| @EnsuresNonNull({"extractorOutput", "realTrackOutput"}) |
| private void assertInitialized() { |
| Assertions.checkStateNotNull(realTrackOutput); |
| Util.castNonNull(extractorOutput); |
| } |
| |
| /** Returns whether the headers match in those bits masked by {@link #MPEG_AUDIO_HEADER_MASK}. */ |
| private static boolean headersMatch(int headerA, long headerB) { |
| return (headerA & MPEG_AUDIO_HEADER_MASK) == (headerB & MPEG_AUDIO_HEADER_MASK); |
| } |
| |
| /** |
| * Returns {@link #SEEK_HEADER_XING}, {@link #SEEK_HEADER_INFO} or {@link #SEEK_HEADER_VBRI} if |
| * the provided {@code frame} may have seeking metadata, or {@link #SEEK_HEADER_UNSET} otherwise. |
| * If seeking metadata is present, {@code frame}'s position is advanced past the header. |
| */ |
| private static int getSeekFrameHeader(ParsableByteArray frame, int xingBase) { |
| if (frame.limit() >= xingBase + 4) { |
| frame.setPosition(xingBase); |
| int headerData = frame.readInt(); |
| if (headerData == SEEK_HEADER_XING || headerData == SEEK_HEADER_INFO) { |
| return headerData; |
| } |
| } |
| if (frame.limit() >= 40) { |
| frame.setPosition(36); // MPEG audio header (4 bytes) + 32 bytes. |
| if (frame.readInt() == SEEK_HEADER_VBRI) { |
| return SEEK_HEADER_VBRI; |
| } |
| } |
| return SEEK_HEADER_UNSET; |
| } |
| |
| @Nullable |
| private static MlltSeeker maybeHandleSeekMetadata( |
| @Nullable Metadata metadata, long firstFramePosition) { |
| if (metadata != null) { |
| int length = metadata.length(); |
| for (int i = 0; i < length; i++) { |
| Metadata.Entry entry = metadata.get(i); |
| if (entry instanceof MlltFrame) { |
| return MlltSeeker.create(firstFramePosition, (MlltFrame) entry, getId3TlenUs(metadata)); |
| } |
| } |
| } |
| return null; |
| } |
| |
| private static long getId3TlenUs(@Nullable Metadata metadata) { |
| if (metadata != null) { |
| int length = metadata.length(); |
| for (int i = 0; i < length; i++) { |
| Metadata.Entry entry = metadata.get(i); |
| if (entry instanceof TextInformationFrame |
| && ((TextInformationFrame) entry).id.equals("TLEN")) { |
| return Util.msToUs(Long.parseLong(((TextInformationFrame) entry).value)); |
| } |
| } |
| } |
| return C.TIME_UNSET; |
| } |
| } |