/* * Copyright (C) 2014 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.exoplayer.extractor; import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.upstream.Allocator; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.util.ParsableByteArray; import java.io.EOFException; import java.io.IOException; /** * A {@link TrackOutput} that buffers extracted samples in a queue, and allows for consumption from * that queue. */ public class DefaultTrackOutput implements TrackOutput { private final RollingSampleBuffer rollingBuffer; private final SampleHolder sampleInfoHolder; // Accessed only by the consuming thread. private boolean needKeyframe; private long lastReadTimeUs; private long spliceOutTimeUs; // Accessed by both the loading and consuming threads. private volatile long largestParsedTimestampUs; private volatile MediaFormat format; /** * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. */ public DefaultTrackOutput(Allocator allocator) { rollingBuffer = new RollingSampleBuffer(allocator); sampleInfoHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED); needKeyframe = true; lastReadTimeUs = Long.MIN_VALUE; spliceOutTimeUs = Long.MIN_VALUE; largestParsedTimestampUs = Long.MIN_VALUE; } // Called by the consuming thread, but only when there is no loading thread. /** * Clears the queue, returning all allocations to the allocator. */ public void clear() { rollingBuffer.clear(); needKeyframe = true; lastReadTimeUs = Long.MIN_VALUE; spliceOutTimeUs = Long.MIN_VALUE; largestParsedTimestampUs = Long.MIN_VALUE; } /** * Returns the current absolute write index. */ public int getWriteIndex() { return rollingBuffer.getWriteIndex(); } /** * Discards samples from the write side of the queue. * * @param discardFromIndex The absolute index of the first sample to be discarded. */ public void discardUpstreamSamples(int discardFromIndex) { rollingBuffer.discardUpstreamSamples(discardFromIndex); largestParsedTimestampUs = rollingBuffer.peekSample(sampleInfoHolder) ? sampleInfoHolder.timeUs : Long.MIN_VALUE; } // Called by the consuming thread. /** * Returns the current absolute read index. */ public int getReadIndex() { return rollingBuffer.getReadIndex(); } /** * True if the output has received a format. False otherwise. */ public boolean hasFormat() { return format != null; } /** * The format most recently received by the output, or null if a format has yet to be received. */ public MediaFormat getFormat() { return format; } /** * The largest timestamp of any sample received by the output, or {@link Long#MIN_VALUE} if a * sample has yet to be received. */ public long getLargestParsedTimestampUs() { return largestParsedTimestampUs; } /** * True if at least one sample can be read from the queue. False otherwise. */ public boolean isEmpty() { return !advanceToEligibleSample(); } /** * Removes the next sample from the head of the queue, writing it into the provided holder. * <p> * The first sample returned is guaranteed to be a keyframe, since any non-keyframe samples * queued prior to the first keyframe are discarded. * * @param holder A {@link SampleHolder} into which the sample should be read. * @return True if a sample was read. False otherwise. */ public boolean getSample(SampleHolder holder) { boolean foundEligibleSample = advanceToEligibleSample(); if (!foundEligibleSample) { return false; } // Write the sample into the holder. rollingBuffer.readSample(holder); needKeyframe = false; lastReadTimeUs = holder.timeUs; return true; } /** * Discards samples from the queue up to the specified time. * * @param timeUs The time up to which samples should be discarded, in microseconds. */ public void discardUntil(long timeUs) { while (rollingBuffer.peekSample(sampleInfoHolder) && sampleInfoHolder.timeUs < timeUs) { rollingBuffer.skipSample(); // We're discarding one or more samples. A subsequent read will need to start at a keyframe. needKeyframe = true; } lastReadTimeUs = Long.MIN_VALUE; } /** * Attempts to skip to the keyframe before the specified time, if it's present in the buffer. * * @param timeUs The seek time. * @return True if the skip was successful. False otherwise. */ public boolean skipToKeyframeBefore(long timeUs) { return rollingBuffer.skipToKeyframeBefore(timeUs); } /** * Attempts to configure a splice from this queue to the next. * * @param nextQueue The queue being spliced to. * @return Whether the splice was configured successfully. */ public boolean configureSpliceTo(DefaultTrackOutput nextQueue) { if (spliceOutTimeUs != Long.MIN_VALUE) { // We've already configured the splice. return true; } long firstPossibleSpliceTime; if (rollingBuffer.peekSample(sampleInfoHolder)) { firstPossibleSpliceTime = sampleInfoHolder.timeUs; } else { firstPossibleSpliceTime = lastReadTimeUs + 1; } RollingSampleBuffer nextRollingBuffer = nextQueue.rollingBuffer; while (nextRollingBuffer.peekSample(sampleInfoHolder) && (sampleInfoHolder.timeUs < firstPossibleSpliceTime || !sampleInfoHolder.isSyncFrame())) { // Discard samples from the next queue for as long as they are before the earliest possible // splice time, or not keyframes. nextRollingBuffer.skipSample(); } if (nextRollingBuffer.peekSample(sampleInfoHolder)) { // We've found a keyframe in the next queue that can serve as the splice point. Set the // splice point now. spliceOutTimeUs = sampleInfoHolder.timeUs; return true; } return false; } /** * Advances the underlying buffer to the next sample that is eligible to be returned. * * @boolean True if an eligible sample was found. False otherwise, in which case the underlying * buffer has been emptied. */ private boolean advanceToEligibleSample() { boolean haveNext = rollingBuffer.peekSample(sampleInfoHolder); if (needKeyframe) { while (haveNext && !sampleInfoHolder.isSyncFrame()) { rollingBuffer.skipSample(); haveNext = rollingBuffer.peekSample(sampleInfoHolder); } } if (!haveNext) { return false; } if (spliceOutTimeUs != Long.MIN_VALUE && sampleInfoHolder.timeUs >= spliceOutTimeUs) { return false; } return true; } // Called by the loading thread. /** * Invoked to write sample data to the output. * * @param dataSource A {@link DataSource} from which to read the sample data. * @param length The maximum length to read from the input. * @param allowEndOfInput True if encountering the end of the input having read no data is * allowed, and should result in {@link C#RESULT_END_OF_INPUT} being returned. False if it * should be considered an error, causing an {@link EOFException} to be thrown. * @return The number of bytes appended. * @throws IOException If an error occurred reading from the input. */ public int sampleData(DataSource dataSource, int length, boolean allowEndOfInput) throws IOException { return rollingBuffer.appendData(dataSource, length, allowEndOfInput); } // TrackOutput implementation. Called by the loading thread. @Override public void format(MediaFormat format) { this.format = format; } @Override public int sampleData(ExtractorInput input, int length, boolean allowEndOfInput) throws IOException, InterruptedException { return rollingBuffer.appendData(input, length, allowEndOfInput); } @Override public void sampleData(ParsableByteArray buffer, int length) { rollingBuffer.appendData(buffer, length); } @Override public void sampleMetadata(long timeUs, int flags, int size, int offset, byte[] encryptionKey) { largestParsedTimestampUs = Math.max(largestParsedTimestampUs, timeUs); rollingBuffer.commitSample(timeUs, flags, rollingBuffer.getWritePosition() - size - offset, size, encryptionKey); } }