/* * 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 org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts; import android.util.Pair; import org.mozilla.thirdparty.com.google.android.exoplayer2.C; import org.mozilla.thirdparty.com.google.android.exoplayer2.Format; import org.mozilla.thirdparty.com.google.android.exoplayer2.ParserException; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.DummyTrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ExtractorOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.TrackOutput; import org.mozilla.thirdparty.com.google.android.exoplayer2.extractor.ts.TsPayloadReader.TrackIdGenerator; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.CodecSpecificDataUtil; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Log; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableBitArray; import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ParsableByteArray; import java.util.Arrays; import java.util.Collections; /** * Parses a continuous ADTS byte stream and extracts individual frames. */ public final class AdtsReader implements ElementaryStreamReader { private static final String TAG = "AdtsReader"; private static final int STATE_FINDING_SAMPLE = 0; private static final int STATE_CHECKING_ADTS_HEADER = 1; private static final int STATE_READING_ID3_HEADER = 2; private static final int STATE_READING_ADTS_HEADER = 3; private static final int STATE_READING_SAMPLE = 4; private static final int HEADER_SIZE = 5; private static final int CRC_SIZE = 2; // Match states used while looking for the next sample private static final int MATCH_STATE_VALUE_SHIFT = 8; private static final int MATCH_STATE_START = 1 << MATCH_STATE_VALUE_SHIFT; private static final int MATCH_STATE_FF = 2 << MATCH_STATE_VALUE_SHIFT; private static final int MATCH_STATE_I = 3 << MATCH_STATE_VALUE_SHIFT; private static final int MATCH_STATE_ID = 4 << MATCH_STATE_VALUE_SHIFT; private static final int ID3_HEADER_SIZE = 10; private static final int ID3_SIZE_OFFSET = 6; private static final byte[] ID3_IDENTIFIER = {'I', 'D', '3'}; private static final int VERSION_UNSET = -1; private final boolean exposeId3; private final ParsableBitArray adtsScratch; private final ParsableByteArray id3HeaderBuffer; private final String language; private String formatId; private TrackOutput output; private TrackOutput id3Output; private int state; private int bytesRead; private int matchState; private boolean hasCrc; private boolean foundFirstFrame; // Used to verifies sync words private int firstFrameVersion; private int firstFrameSampleRateIndex; private int currentFrameVersion; // Used when parsing the header. private boolean hasOutputFormat; private long sampleDurationUs; private int sampleSize; // Used when reading the samples. private long timeUs; private TrackOutput currentOutput; private long currentSampleDuration; /** * @param exposeId3 True if the reader should expose ID3 information. */ public AdtsReader(boolean exposeId3) { this(exposeId3, null); } /** * @param exposeId3 True if the reader should expose ID3 information. * @param language Track language. */ public AdtsReader(boolean exposeId3, String language) { adtsScratch = new ParsableBitArray(new byte[HEADER_SIZE + CRC_SIZE]); id3HeaderBuffer = new ParsableByteArray(Arrays.copyOf(ID3_IDENTIFIER, ID3_HEADER_SIZE)); setFindingSampleState(); firstFrameVersion = VERSION_UNSET; firstFrameSampleRateIndex = C.INDEX_UNSET; sampleDurationUs = C.TIME_UNSET; this.exposeId3 = exposeId3; this.language = language; } /** Returns whether an integer matches an ADTS SYNC word. */ public static boolean isAdtsSyncWord(int candidateSyncWord) { return (candidateSyncWord & 0xFFF6) == 0xFFF0; } @Override public void seek() { resetSync(); } @Override public void createTracks(ExtractorOutput extractorOutput, TrackIdGenerator idGenerator) { idGenerator.generateNewId(); formatId = idGenerator.getFormatId(); output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_AUDIO); if (exposeId3) { idGenerator.generateNewId(); id3Output = extractorOutput.track(idGenerator.getTrackId(), C.TRACK_TYPE_METADATA); id3Output.format(Format.createSampleFormat(idGenerator.getFormatId(), MimeTypes.APPLICATION_ID3, null, Format.NO_VALUE, null)); } else { id3Output = new DummyTrackOutput(); } } @Override public void packetStarted(long pesTimeUs, @TsPayloadReader.Flags int flags) { timeUs = pesTimeUs; } @Override public void consume(ParsableByteArray data) throws ParserException { while (data.bytesLeft() > 0) { switch (state) { case STATE_FINDING_SAMPLE: findNextSample(data); break; case STATE_READING_ID3_HEADER: if (continueRead(data, id3HeaderBuffer.data, ID3_HEADER_SIZE)) { parseId3Header(); } break; case STATE_CHECKING_ADTS_HEADER: checkAdtsHeader(data); break; case STATE_READING_ADTS_HEADER: int targetLength = hasCrc ? HEADER_SIZE + CRC_SIZE : HEADER_SIZE; if (continueRead(data, adtsScratch.data, targetLength)) { parseAdtsHeader(); } break; case STATE_READING_SAMPLE: readSample(data); break; default: throw new IllegalStateException(); } } } @Override public void packetFinished() { // Do nothing. } /** * Returns the duration in microseconds per sample, or {@link C#TIME_UNSET} if the sample duration * is not available. */ public long getSampleDurationUs() { return sampleDurationUs; } private void resetSync() { foundFirstFrame = false; setFindingSampleState(); } /** * Continues a read from the provided {@code source} into a given {@code target}. It's assumed * that the data should be written into {@code target} starting from an offset of zero. * * @param source The source from which to read. * @param target The target into which data is to be read. * @param targetLength The target length of the read. * @return Whether the target length was reached. */ private boolean continueRead(ParsableByteArray source, byte[] target, int targetLength) { int bytesToRead = Math.min(source.bytesLeft(), targetLength - bytesRead); source.readBytes(target, bytesRead, bytesToRead); bytesRead += bytesToRead; return bytesRead == targetLength; } /** * Sets the state to STATE_FINDING_SAMPLE. */ private void setFindingSampleState() { state = STATE_FINDING_SAMPLE; bytesRead = 0; matchState = MATCH_STATE_START; } /** * Sets the state to STATE_READING_ID3_HEADER and resets the fields required for * {@link #parseId3Header()}. */ private void setReadingId3HeaderState() { state = STATE_READING_ID3_HEADER; bytesRead = ID3_IDENTIFIER.length; sampleSize = 0; id3HeaderBuffer.setPosition(0); } /** * Sets the state to STATE_READING_SAMPLE. * * @param outputToUse TrackOutput object to write the sample to * @param currentSampleDuration Duration of the sample to be read * @param priorReadBytes Size of prior read bytes * @param sampleSize Size of the sample */ private void setReadingSampleState(TrackOutput outputToUse, long currentSampleDuration, int priorReadBytes, int sampleSize) { state = STATE_READING_SAMPLE; bytesRead = priorReadBytes; this.currentOutput = outputToUse; this.currentSampleDuration = currentSampleDuration; this.sampleSize = sampleSize; } /** * Sets the state to STATE_READING_ADTS_HEADER. */ private void setReadingAdtsHeaderState() { state = STATE_READING_ADTS_HEADER; bytesRead = 0; } /** Sets the state to STATE_CHECKING_ADTS_HEADER. */ private void setCheckingAdtsHeaderState() { state = STATE_CHECKING_ADTS_HEADER; bytesRead = 0; } /** * Locates the next sample start, advancing the position to the byte that immediately follows * identifier. If a sample was not located, the position is advanced to the limit. * * @param pesBuffer The buffer whose position should be advanced. */ private void findNextSample(ParsableByteArray pesBuffer) { byte[] adtsData = pesBuffer.data; int position = pesBuffer.getPosition(); int endOffset = pesBuffer.limit(); while (position < endOffset) { int data = adtsData[position++] & 0xFF; if (matchState == MATCH_STATE_FF && isAdtsSyncBytes((byte) 0xFF, (byte) data)) { if (foundFirstFrame || checkSyncPositionValid(pesBuffer, /* syncPositionCandidate= */ position - 2)) { currentFrameVersion = (data & 0x8) >> 3; hasCrc = (data & 0x1) == 0; if (!foundFirstFrame) { setCheckingAdtsHeaderState(); } else { setReadingAdtsHeaderState(); } pesBuffer.setPosition(position); return; } } switch (matchState | data) { case MATCH_STATE_START | 0xFF: matchState = MATCH_STATE_FF; break; case MATCH_STATE_START | 'I': matchState = MATCH_STATE_I; break; case MATCH_STATE_I | 'D': matchState = MATCH_STATE_ID; break; case MATCH_STATE_ID | '3': setReadingId3HeaderState(); pesBuffer.setPosition(position); return; default: if (matchState != MATCH_STATE_START) { // If matching fails in a later state, revert to MATCH_STATE_START and // check this byte again matchState = MATCH_STATE_START; position--; } break; } } pesBuffer.setPosition(position); } /** * Peeks the Adts header of the current frame and checks if it is valid. If the header is valid, * transition to {@link #STATE_READING_ADTS_HEADER}; else, transition to {@link * #STATE_FINDING_SAMPLE}. */ private void checkAdtsHeader(ParsableByteArray buffer) { if (buffer.bytesLeft() == 0) { // Not enough data to check yet, defer this check. return; } // Peek the next byte of buffer into scratch array. adtsScratch.data[0] = buffer.data[buffer.getPosition()]; adtsScratch.setPosition(2); int currentFrameSampleRateIndex = adtsScratch.readBits(4); if (firstFrameSampleRateIndex != C.INDEX_UNSET && currentFrameSampleRateIndex != firstFrameSampleRateIndex) { // Invalid header. resetSync(); return; } if (!foundFirstFrame) { foundFirstFrame = true; firstFrameVersion = currentFrameVersion; firstFrameSampleRateIndex = currentFrameSampleRateIndex; } setReadingAdtsHeaderState(); } /** * Checks whether a candidate SYNC word position is likely to be the position of a real SYNC word. * The caller must check that the first byte of the SYNC word is 0xFF before calling this method. * This method performs the following checks: * *