mirror of
https://github.com/samsonjs/media.git
synced 2026-03-27 09:45:47 +00:00
887 lines
32 KiB
Java
887 lines
32 KiB
Java
/*
|
|
* 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;
|
|
|
|
import com.google.android.exoplayer.MediaCodecUtil.DecoderQueryException;
|
|
import com.google.android.exoplayer.drm.DrmInitData;
|
|
import com.google.android.exoplayer.drm.DrmSessionManager;
|
|
import com.google.android.exoplayer.util.Assertions;
|
|
import com.google.android.exoplayer.util.Util;
|
|
|
|
import android.annotation.TargetApi;
|
|
import android.media.MediaCodec;
|
|
import android.media.MediaCodec.CodecException;
|
|
import android.media.MediaCodec.CryptoException;
|
|
import android.media.MediaCrypto;
|
|
import android.os.Handler;
|
|
import android.os.SystemClock;
|
|
|
|
import java.io.IOException;
|
|
import java.nio.ByteBuffer;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
/**
|
|
* An abstract {@link TrackRenderer} that uses {@link MediaCodec} to decode samples for rendering.
|
|
*/
|
|
@TargetApi(16)
|
|
public abstract class MediaCodecTrackRenderer extends TrackRenderer {
|
|
|
|
/**
|
|
* Interface definition for a callback to be notified of {@link MediaCodecTrackRenderer} events.
|
|
*/
|
|
public interface EventListener {
|
|
|
|
/**
|
|
* Invoked when a decoder fails to initialize.
|
|
*
|
|
* @param e The corresponding exception.
|
|
*/
|
|
void onDecoderInitializationError(DecoderInitializationException e);
|
|
|
|
/**
|
|
* Invoked when a decoder operation raises a {@link CryptoException}.
|
|
*
|
|
* @param e The corresponding exception.
|
|
*/
|
|
void onCryptoError(CryptoException e);
|
|
|
|
/**
|
|
* Invoked when a decoder is successfully created.
|
|
*
|
|
* @param decoderName The decoder that was configured and created.
|
|
* @param elapsedRealtimeMs {@code elapsedRealtime} timestamp of when the initialization
|
|
* finished.
|
|
* @param initializationDurationMs Amount of time taken to initialize the decoder.
|
|
*/
|
|
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
|
|
long initializationDurationMs);
|
|
|
|
}
|
|
|
|
/**
|
|
* Thrown when a failure occurs instantiating a decoder.
|
|
*/
|
|
public static class DecoderInitializationException extends Exception {
|
|
|
|
private static final int CUSTOM_ERROR_CODE_BASE = -50000;
|
|
private static final int NO_SUITABLE_DECODER_ERROR = CUSTOM_ERROR_CODE_BASE + 1;
|
|
private static final int DECODER_QUERY_ERROR = CUSTOM_ERROR_CODE_BASE + 2;
|
|
|
|
/**
|
|
* The name of the decoder that failed to initialize. Null if no suitable decoder was found.
|
|
*/
|
|
public final String decoderName;
|
|
|
|
/**
|
|
* An optional developer-readable diagnostic information string. May be null.
|
|
*/
|
|
public final String diagnosticInfo;
|
|
|
|
public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause, int errorCode) {
|
|
super("Decoder init failed: [" + errorCode + "], " + mediaFormat, cause);
|
|
this.decoderName = null;
|
|
this.diagnosticInfo = buildCustomDiagnosticInfo(errorCode);
|
|
}
|
|
|
|
public DecoderInitializationException(MediaFormat mediaFormat, Throwable cause,
|
|
String decoderName) {
|
|
super("Decoder init failed: " + decoderName + ", " + mediaFormat, cause);
|
|
this.decoderName = decoderName;
|
|
this.diagnosticInfo = Util.SDK_INT >= 21 ? getDiagnosticInfoV21(cause) : null;
|
|
}
|
|
|
|
@TargetApi(21)
|
|
private static String getDiagnosticInfoV21(Throwable cause) {
|
|
if (cause instanceof CodecException) {
|
|
return ((CodecException) cause).getDiagnosticInfo();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private static String buildCustomDiagnosticInfo(int errorCode) {
|
|
String sign = errorCode < 0 ? "neg_" : "";
|
|
return "com.google.android.exoplayer.MediaCodecTrackRenderer_" + sign + Math.abs(errorCode);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Value returned by {@link #getSourceState()} when the source is not ready.
|
|
*/
|
|
protected static final int SOURCE_STATE_NOT_READY = 0;
|
|
/**
|
|
* Value returned by {@link #getSourceState()} when the source is ready and we're able to read
|
|
* from it.
|
|
*/
|
|
protected static final int SOURCE_STATE_READY = 1;
|
|
/**
|
|
* Value returned by {@link #getSourceState()} when the source is ready but we might not be able
|
|
* to read from it. We transition to this state when an attempt to read a sample fails despite the
|
|
* source reporting that samples are available. This can occur when the next sample to be provided
|
|
* by the source is for another renderer.
|
|
*/
|
|
protected static final int SOURCE_STATE_READY_READ_MAY_FAIL = 2;
|
|
|
|
/**
|
|
* If the {@link MediaCodec} is hotswapped (i.e. replaced during playback), this is the period of
|
|
* time during which {@link #isReady()} will report true regardless of whether the new codec has
|
|
* output frames that are ready to be rendered.
|
|
* <p>
|
|
* This allows codec hotswapping to be performed seamlessly, without interrupting the playback of
|
|
* other renderers, provided the new codec is able to decode some frames within this time period.
|
|
*/
|
|
private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000;
|
|
|
|
/**
|
|
* There is no pending adaptive reconfiguration work.
|
|
*/
|
|
private static final int RECONFIGURATION_STATE_NONE = 0;
|
|
/**
|
|
* Codec configuration data needs to be written into the next buffer.
|
|
*/
|
|
private static final int RECONFIGURATION_STATE_WRITE_PENDING = 1;
|
|
/**
|
|
* Codec configuration data has been written into the next buffer, but that buffer still needs to
|
|
* be returned to the codec.
|
|
*/
|
|
private static final int RECONFIGURATION_STATE_QUEUE_PENDING = 2;
|
|
|
|
/**
|
|
* The codec does not need to be re-initialized.
|
|
*/
|
|
private static final int REINITIALIZATION_STATE_NONE = 0;
|
|
/**
|
|
* The input format has changed in a way that requires the codec to be re-initialized, but we
|
|
* haven't yet signaled an end of stream to the existing codec. We need to do so in order to
|
|
* ensure that it outputs any remaining buffers before we release it.
|
|
*/
|
|
private static final int REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM = 1;
|
|
/**
|
|
* The input format has changed in a way that requires the codec to be re-initialized, and we've
|
|
* signaled an end of stream to the existing codec. We're waiting for the codec to output an end
|
|
* of stream signal to indicate that it has output any remaining buffers before we release it.
|
|
*/
|
|
private static final int REINITIALIZATION_STATE_WAIT_END_OF_STREAM = 2;
|
|
|
|
public final CodecCounters codecCounters;
|
|
|
|
private final DrmSessionManager drmSessionManager;
|
|
private final boolean playClearSamplesWithoutKeys;
|
|
private final SampleSource source;
|
|
private final SampleHolder sampleHolder;
|
|
private final MediaFormatHolder formatHolder;
|
|
private final List<Long> decodeOnlyPresentationTimestamps;
|
|
private final MediaCodec.BufferInfo outputBufferInfo;
|
|
private final EventListener eventListener;
|
|
protected final Handler eventHandler;
|
|
|
|
private MediaFormat format;
|
|
private DrmInitData drmInitData;
|
|
private MediaCodec codec;
|
|
private boolean codecIsAdaptive;
|
|
private ByteBuffer[] inputBuffers;
|
|
private ByteBuffer[] outputBuffers;
|
|
private long codecHotswapTimeMs;
|
|
private int inputIndex;
|
|
private int outputIndex;
|
|
private boolean openedDrmSession;
|
|
private boolean codecReconfigured;
|
|
private int codecReconfigurationState;
|
|
private int codecReinitializationState;
|
|
private boolean codecHasQueuedBuffers;
|
|
|
|
private int trackIndex;
|
|
private int sourceState;
|
|
private boolean inputStreamEnded;
|
|
private boolean outputStreamEnded;
|
|
private boolean waitingForKeys;
|
|
private boolean waitingForFirstSyncFrame;
|
|
private long currentPositionUs;
|
|
|
|
/**
|
|
* @param source The upstream source from which the renderer obtains samples.
|
|
* @param drmSessionManager For use with encrypted media. May be null if support for encrypted
|
|
* media is not required.
|
|
* @param playClearSamplesWithoutKeys Encrypted media may contain clear (un-encrypted) regions.
|
|
* For example a media file may start with a short clear region so as to allow playback to
|
|
* begin in parallel with key acquisision. This parameter specifies whether the renderer is
|
|
* permitted to play clear regions of encrypted media files before {@code drmSessionManager}
|
|
* has obtained the keys necessary to decrypt encrypted regions of the media.
|
|
* @param eventHandler A handler to use when delivering events to {@code eventListener}. May be
|
|
* null if delivery of events is not required.
|
|
* @param eventListener A listener of events. May be null if delivery of events is not required.
|
|
*/
|
|
public MediaCodecTrackRenderer(SampleSource source, DrmSessionManager drmSessionManager,
|
|
boolean playClearSamplesWithoutKeys, Handler eventHandler, EventListener eventListener) {
|
|
Assertions.checkState(Util.SDK_INT >= 16);
|
|
this.source = source;
|
|
this.drmSessionManager = drmSessionManager;
|
|
this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
|
|
this.eventHandler = eventHandler;
|
|
this.eventListener = eventListener;
|
|
codecCounters = new CodecCounters();
|
|
sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_DISABLED);
|
|
formatHolder = new MediaFormatHolder();
|
|
decodeOnlyPresentationTimestamps = new ArrayList<Long>();
|
|
outputBufferInfo = new MediaCodec.BufferInfo();
|
|
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
|
|
codecReinitializationState = REINITIALIZATION_STATE_NONE;
|
|
}
|
|
|
|
@Override
|
|
protected int doPrepare() throws ExoPlaybackException {
|
|
try {
|
|
boolean sourcePrepared = source.prepare();
|
|
if (!sourcePrepared) {
|
|
return TrackRenderer.STATE_UNPREPARED;
|
|
}
|
|
} catch (IOException e) {
|
|
throw new ExoPlaybackException(e);
|
|
}
|
|
|
|
for (int i = 0; i < source.getTrackCount(); i++) {
|
|
// TODO: Right now this is getting the mime types of the container format
|
|
// (e.g. audio/mp4 and video/mp4 for fragmented mp4). It needs to be getting the mime types
|
|
// of the actual samples (e.g. audio/mp4a-latm and video/avc).
|
|
if (handlesMimeType(source.getTrackInfo(i).mimeType)) {
|
|
trackIndex = i;
|
|
return TrackRenderer.STATE_PREPARED;
|
|
}
|
|
}
|
|
|
|
return TrackRenderer.STATE_IGNORE;
|
|
}
|
|
|
|
/**
|
|
* Determines whether a mime type is handled by the renderer.
|
|
*
|
|
* @param mimeType The mime type to test.
|
|
* @return True if the renderer can handle the mime type. False otherwise.
|
|
*/
|
|
protected boolean handlesMimeType(String mimeType) {
|
|
return true;
|
|
// TODO: Uncomment once the TODO above is fixed.
|
|
// DecoderInfoUtil.getDecoder(mimeType) != null;
|
|
}
|
|
|
|
@Override
|
|
protected void onEnabled(long positionUs, boolean joining) {
|
|
source.enable(trackIndex, positionUs);
|
|
sourceState = SOURCE_STATE_NOT_READY;
|
|
inputStreamEnded = false;
|
|
outputStreamEnded = false;
|
|
waitingForKeys = false;
|
|
currentPositionUs = positionUs;
|
|
}
|
|
|
|
/**
|
|
* Configures a newly created {@link MediaCodec}. Sub-classes should
|
|
* override this method if they wish to configure the codec with a
|
|
* non-null surface.
|
|
**/
|
|
protected void configureCodec(MediaCodec codec, android.media.MediaFormat x, MediaCrypto crypto) {
|
|
codec.configure(x, null, crypto, 0);
|
|
}
|
|
|
|
@SuppressWarnings("deprecation")
|
|
protected final void maybeInitCodec() throws ExoPlaybackException {
|
|
if (!shouldInitCodec()) {
|
|
return;
|
|
}
|
|
|
|
String mimeType = format.mimeType;
|
|
MediaCrypto mediaCrypto = null;
|
|
boolean requiresSecureDecoder = false;
|
|
if (drmInitData != null) {
|
|
if (drmSessionManager == null) {
|
|
throw new ExoPlaybackException("Media requires a DrmSessionManager");
|
|
}
|
|
if (!openedDrmSession) {
|
|
drmSessionManager.open(drmInitData);
|
|
openedDrmSession = true;
|
|
}
|
|
int drmSessionState = drmSessionManager.getState();
|
|
if (drmSessionState == DrmSessionManager.STATE_ERROR) {
|
|
throw new ExoPlaybackException(drmSessionManager.getError());
|
|
} else if (drmSessionState == DrmSessionManager.STATE_OPENED
|
|
|| drmSessionState == DrmSessionManager.STATE_OPENED_WITH_KEYS) {
|
|
mediaCrypto = drmSessionManager.getMediaCrypto();
|
|
requiresSecureDecoder = drmSessionManager.requiresSecureDecoderComponent(mimeType);
|
|
} else {
|
|
// The drm session isn't open yet.
|
|
return;
|
|
}
|
|
}
|
|
|
|
DecoderInfo decoderInfo = null;
|
|
try {
|
|
decoderInfo = MediaCodecUtil.getDecoderInfo(mimeType, requiresSecureDecoder);
|
|
} catch (DecoderQueryException e) {
|
|
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e,
|
|
DecoderInitializationException.DECODER_QUERY_ERROR));
|
|
}
|
|
|
|
if (decoderInfo == null) {
|
|
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, null,
|
|
DecoderInitializationException.NO_SUITABLE_DECODER_ERROR));
|
|
}
|
|
|
|
String decoderName = decoderInfo.name;
|
|
codecIsAdaptive = decoderInfo.adaptive;
|
|
try {
|
|
long codecInitializingTimestamp = SystemClock.elapsedRealtime();
|
|
codec = MediaCodec.createByCodecName(decoderName);
|
|
configureCodec(codec, format.getFrameworkMediaFormatV16(), mediaCrypto);
|
|
codec.start();
|
|
long codecInitializedTimestamp = SystemClock.elapsedRealtime();
|
|
notifyDecoderInitialized(decoderName, codecInitializedTimestamp,
|
|
codecInitializedTimestamp - codecInitializingTimestamp);
|
|
inputBuffers = codec.getInputBuffers();
|
|
outputBuffers = codec.getOutputBuffers();
|
|
} catch (Exception e) {
|
|
notifyAndThrowDecoderInitError(new DecoderInitializationException(format, e, decoderName));
|
|
}
|
|
codecHotswapTimeMs = getState() == TrackRenderer.STATE_STARTED ?
|
|
SystemClock.elapsedRealtime() : -1;
|
|
inputIndex = -1;
|
|
outputIndex = -1;
|
|
waitingForFirstSyncFrame = true;
|
|
codecCounters.codecInitCount++;
|
|
}
|
|
|
|
private void notifyAndThrowDecoderInitError(DecoderInitializationException e)
|
|
throws ExoPlaybackException {
|
|
notifyDecoderInitializationError(e);
|
|
throw new ExoPlaybackException(e);
|
|
}
|
|
|
|
protected boolean shouldInitCodec() {
|
|
return codec == null && format != null;
|
|
}
|
|
|
|
protected final boolean codecInitialized() {
|
|
return codec != null;
|
|
}
|
|
|
|
protected final boolean haveFormat() {
|
|
return format != null;
|
|
}
|
|
|
|
@Override
|
|
protected void onDisabled() {
|
|
format = null;
|
|
drmInitData = null;
|
|
try {
|
|
releaseCodec();
|
|
} finally {
|
|
try {
|
|
if (openedDrmSession) {
|
|
drmSessionManager.close();
|
|
openedDrmSession = false;
|
|
}
|
|
} finally {
|
|
source.disable(trackIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
protected void releaseCodec() {
|
|
if (codec != null) {
|
|
codecHotswapTimeMs = -1;
|
|
inputIndex = -1;
|
|
outputIndex = -1;
|
|
waitingForKeys = false;
|
|
decodeOnlyPresentationTimestamps.clear();
|
|
inputBuffers = null;
|
|
outputBuffers = null;
|
|
codecReconfigured = false;
|
|
codecHasQueuedBuffers = false;
|
|
codecIsAdaptive = false;
|
|
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
|
|
codecReinitializationState = REINITIALIZATION_STATE_NONE;
|
|
codecCounters.codecReleaseCount++;
|
|
try {
|
|
codec.stop();
|
|
} finally {
|
|
try {
|
|
codec.release();
|
|
} finally {
|
|
codec = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void onReleased() {
|
|
source.release();
|
|
}
|
|
|
|
@Override
|
|
protected long getCurrentPositionUs() {
|
|
return currentPositionUs;
|
|
}
|
|
|
|
@Override
|
|
protected long getDurationUs() {
|
|
return source.getTrackInfo(trackIndex).durationUs;
|
|
}
|
|
|
|
@Override
|
|
protected long getBufferedPositionUs() {
|
|
long sourceBufferedPosition = source.getBufferedPositionUs();
|
|
return sourceBufferedPosition == UNKNOWN_TIME_US || sourceBufferedPosition == END_OF_TRACK_US
|
|
? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs());
|
|
}
|
|
|
|
@Override
|
|
protected void seekTo(long positionUs) throws ExoPlaybackException {
|
|
currentPositionUs = positionUs;
|
|
source.seekToUs(positionUs);
|
|
sourceState = SOURCE_STATE_NOT_READY;
|
|
inputStreamEnded = false;
|
|
outputStreamEnded = false;
|
|
}
|
|
|
|
@Override
|
|
protected void onStarted() {
|
|
// Do nothing. Overridden to remove throws clause.
|
|
}
|
|
|
|
@Override
|
|
protected void onStopped() {
|
|
// Do nothing. Overridden to remove throws clause.
|
|
}
|
|
|
|
@Override
|
|
protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
|
|
try {
|
|
sourceState = source.continueBuffering(positionUs)
|
|
? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState)
|
|
: SOURCE_STATE_NOT_READY;
|
|
checkForDiscontinuity();
|
|
if (format == null) {
|
|
readFormat();
|
|
}
|
|
if (codec == null && shouldInitCodec()) {
|
|
maybeInitCodec();
|
|
}
|
|
if (codec != null) {
|
|
while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {}
|
|
if (feedInputBuffer(true)) {
|
|
while (feedInputBuffer(false)) {}
|
|
}
|
|
}
|
|
codecCounters.ensureUpdated();
|
|
} catch (IOException e) {
|
|
throw new ExoPlaybackException(e);
|
|
}
|
|
}
|
|
|
|
private void readFormat() throws IOException, ExoPlaybackException {
|
|
int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
|
if (result == SampleSource.FORMAT_READ) {
|
|
onInputFormatChanged(formatHolder);
|
|
}
|
|
}
|
|
|
|
private void checkForDiscontinuity() throws IOException, ExoPlaybackException {
|
|
if (codec == null) {
|
|
return;
|
|
}
|
|
int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, true);
|
|
if (result == SampleSource.DISCONTINUITY_READ) {
|
|
flushCodec();
|
|
}
|
|
}
|
|
|
|
private void flushCodec() throws ExoPlaybackException {
|
|
codecHotswapTimeMs = -1;
|
|
inputIndex = -1;
|
|
outputIndex = -1;
|
|
waitingForFirstSyncFrame = true;
|
|
waitingForKeys = false;
|
|
decodeOnlyPresentationTimestamps.clear();
|
|
// Workaround for framework bugs.
|
|
// See [Internal: b/8347958], [Internal: b/8578467], [Internal: b/8543366].
|
|
if (Util.SDK_INT >= 18 && codecReinitializationState == REINITIALIZATION_STATE_NONE) {
|
|
codec.flush();
|
|
codecHasQueuedBuffers = false;
|
|
} else {
|
|
releaseCodec();
|
|
maybeInitCodec();
|
|
}
|
|
if (codecReconfigured && format != null) {
|
|
// Any reconfiguration data that we send shortly before the flush may be discarded. We
|
|
// avoid this issue by sending reconfiguration data following every flush.
|
|
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param firstFeed True if this is the first call to this method from the current invocation of
|
|
* {@link #doSomeWork(long, long)}. False otherwise.
|
|
* @return True if it may be possible to feed more input data. False otherwise.
|
|
* @throws IOException If an error occurs reading data from the upstream source.
|
|
* @throws ExoPlaybackException If an error occurs feeding the input buffer.
|
|
*/
|
|
private boolean feedInputBuffer(boolean firstFeed) throws IOException, ExoPlaybackException {
|
|
if (inputStreamEnded
|
|
|| codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
|
|
// The input stream has ended, or we need to re-initialize the codec but are still waiting
|
|
// for the existing codec to output any final output buffers.
|
|
return false;
|
|
}
|
|
|
|
if (inputIndex < 0) {
|
|
inputIndex = codec.dequeueInputBuffer(0);
|
|
if (inputIndex < 0) {
|
|
return false;
|
|
}
|
|
sampleHolder.data = inputBuffers[inputIndex];
|
|
sampleHolder.data.clear();
|
|
}
|
|
|
|
if (codecReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
|
|
// We need to re-initialize the codec. Send an end of stream signal to the existing codec so
|
|
// that it outputs any remaining buffers before we release it.
|
|
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
|
inputIndex = -1;
|
|
codecReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
|
|
return false;
|
|
}
|
|
|
|
int result;
|
|
if (waitingForKeys) {
|
|
// We've already read an encrypted sample into sampleHolder, and are waiting for keys.
|
|
result = SampleSource.SAMPLE_READ;
|
|
} else {
|
|
// For adaptive reconfiguration OMX decoders expect all reconfiguration data to be supplied
|
|
// at the start of the buffer that also contains the first frame in the new format.
|
|
if (codecReconfigurationState == RECONFIGURATION_STATE_WRITE_PENDING) {
|
|
for (int i = 0; i < format.initializationData.size(); i++) {
|
|
byte[] data = format.initializationData.get(i);
|
|
sampleHolder.data.put(data);
|
|
}
|
|
codecReconfigurationState = RECONFIGURATION_STATE_QUEUE_PENDING;
|
|
}
|
|
result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false);
|
|
if (firstFeed && sourceState == SOURCE_STATE_READY && result == SampleSource.NOTHING_READ) {
|
|
sourceState = SOURCE_STATE_READY_READ_MAY_FAIL;
|
|
}
|
|
}
|
|
|
|
if (result == SampleSource.NOTHING_READ) {
|
|
return false;
|
|
}
|
|
if (result == SampleSource.DISCONTINUITY_READ) {
|
|
flushCodec();
|
|
return true;
|
|
}
|
|
if (result == SampleSource.FORMAT_READ) {
|
|
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
|
|
// We received two formats in a row. Clear the current buffer of any reconfiguration data
|
|
// associated with the first format.
|
|
sampleHolder.data.clear();
|
|
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
|
}
|
|
onInputFormatChanged(formatHolder);
|
|
return true;
|
|
}
|
|
if (result == SampleSource.END_OF_STREAM) {
|
|
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
|
|
// We received a new format immediately before the end of the stream. We need to clear
|
|
// the corresponding reconfiguration data from the current buffer, but re-write it into
|
|
// a subsequent buffer if there are any (e.g. if the user seeks backwards).
|
|
sampleHolder.data.clear();
|
|
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
|
}
|
|
inputStreamEnded = true;
|
|
try {
|
|
codec.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
|
|
inputIndex = -1;
|
|
} catch (CryptoException e) {
|
|
notifyCryptoError(e);
|
|
throw new ExoPlaybackException(e);
|
|
}
|
|
return false;
|
|
}
|
|
if (waitingForFirstSyncFrame) {
|
|
// TODO: Find out if it's possible to supply samples prior to the first sync
|
|
// frame for HE-AAC.
|
|
if (!sampleHolder.isSyncFrame()) {
|
|
sampleHolder.data.clear();
|
|
if (codecReconfigurationState == RECONFIGURATION_STATE_QUEUE_PENDING) {
|
|
// The buffer we just cleared contained reconfiguration data. We need to re-write this
|
|
// data into a subsequent buffer (if there is one).
|
|
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
|
}
|
|
return true;
|
|
}
|
|
waitingForFirstSyncFrame = false;
|
|
}
|
|
boolean sampleEncrypted = sampleHolder.isEncrypted();
|
|
waitingForKeys = shouldWaitForKeys(sampleEncrypted);
|
|
if (waitingForKeys) {
|
|
return false;
|
|
}
|
|
try {
|
|
int bufferSize = sampleHolder.data.position();
|
|
int adaptiveReconfigurationBytes = bufferSize - sampleHolder.size;
|
|
long presentationTimeUs = sampleHolder.timeUs;
|
|
if (sampleHolder.isDecodeOnly()) {
|
|
decodeOnlyPresentationTimestamps.add(presentationTimeUs);
|
|
}
|
|
if (sampleEncrypted) {
|
|
MediaCodec.CryptoInfo cryptoInfo = getFrameworkCryptoInfo(sampleHolder,
|
|
adaptiveReconfigurationBytes);
|
|
codec.queueSecureInputBuffer(inputIndex, 0, cryptoInfo, presentationTimeUs, 0);
|
|
} else {
|
|
codec.queueInputBuffer(inputIndex, 0 , bufferSize, presentationTimeUs, 0);
|
|
}
|
|
inputIndex = -1;
|
|
codecHasQueuedBuffers = true;
|
|
codecReconfigurationState = RECONFIGURATION_STATE_NONE;
|
|
} catch (CryptoException e) {
|
|
notifyCryptoError(e);
|
|
throw new ExoPlaybackException(e);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private static MediaCodec.CryptoInfo getFrameworkCryptoInfo(SampleHolder sampleHolder,
|
|
int adaptiveReconfigurationBytes) {
|
|
MediaCodec.CryptoInfo cryptoInfo = sampleHolder.cryptoInfo.getFrameworkCryptoInfoV16();
|
|
if (adaptiveReconfigurationBytes == 0) {
|
|
return cryptoInfo;
|
|
}
|
|
// There must be at least one sub-sample, although numBytesOfClearData is permitted to be
|
|
// null if it contains no clear data. Instantiate it if needed, and add the reconfiguration
|
|
// bytes to the clear byte count of the first sub-sample.
|
|
if (cryptoInfo.numBytesOfClearData == null) {
|
|
cryptoInfo.numBytesOfClearData = new int[1];
|
|
}
|
|
cryptoInfo.numBytesOfClearData[0] += adaptiveReconfigurationBytes;
|
|
return cryptoInfo;
|
|
}
|
|
|
|
private boolean shouldWaitForKeys(boolean sampleEncrypted) throws ExoPlaybackException {
|
|
if (!openedDrmSession) {
|
|
return false;
|
|
}
|
|
int drmManagerState = drmSessionManager.getState();
|
|
if (drmManagerState == DrmSessionManager.STATE_ERROR) {
|
|
throw new ExoPlaybackException(drmSessionManager.getError());
|
|
}
|
|
if (drmManagerState != DrmSessionManager.STATE_OPENED_WITH_KEYS &&
|
|
(sampleEncrypted || !playClearSamplesWithoutKeys)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Invoked when a new format is read from the upstream {@link SampleSource}.
|
|
*
|
|
* @param formatHolder Holds the new format.
|
|
* @throws ExoPlaybackException If an error occurs reinitializing the {@link MediaCodec}.
|
|
*/
|
|
protected void onInputFormatChanged(MediaFormatHolder formatHolder) throws ExoPlaybackException {
|
|
MediaFormat oldFormat = format;
|
|
format = formatHolder.format;
|
|
drmInitData = formatHolder.drmInitData;
|
|
if (codec != null && canReconfigureCodec(codec, codecIsAdaptive, oldFormat, format)) {
|
|
codecReconfigured = true;
|
|
codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING;
|
|
} else {
|
|
if (codecHasQueuedBuffers) {
|
|
// Signal end of stream and wait for any final output buffers before re-initialization.
|
|
codecReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
|
|
} else {
|
|
// There aren't any final output buffers, so perform re-initialization immediately.
|
|
releaseCodec();
|
|
maybeInitCodec();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoked when the output format of the {@link MediaCodec} changes.
|
|
* <p>
|
|
* The default implementation is a no-op.
|
|
*
|
|
* @param format The new output format.
|
|
*/
|
|
protected void onOutputFormatChanged(android.media.MediaFormat format) {
|
|
// Do nothing.
|
|
}
|
|
|
|
/**
|
|
* Determines whether the existing {@link MediaCodec} should be reconfigured for a new format by
|
|
* sending codec specific initialization data at the start of the next input buffer. If true is
|
|
* returned then the {@link MediaCodec} instance will be reconfigured in this way. If false is
|
|
* returned then the instance will be released, and a new instance will be created for the new
|
|
* format.
|
|
* <p>
|
|
* The default implementation returns false.
|
|
*
|
|
* @param codec The existing {@link MediaCodec} instance.
|
|
* @param codecIsAdaptive Whether the codec is adaptive.
|
|
* @param oldFormat The format for which the existing instance is configured.
|
|
* @param newFormat The new format.
|
|
* @return True if the existing instance can be reconfigured. False otherwise.
|
|
*/
|
|
protected boolean canReconfigureCodec(MediaCodec codec, boolean codecIsAdaptive,
|
|
MediaFormat oldFormat, MediaFormat newFormat) {
|
|
return false;
|
|
}
|
|
|
|
@Override
|
|
protected boolean isEnded() {
|
|
return outputStreamEnded;
|
|
}
|
|
|
|
@Override
|
|
protected boolean isReady() {
|
|
return format != null && !waitingForKeys
|
|
&& (sourceState != SOURCE_STATE_NOT_READY || outputIndex >= 0 || isWithinHotswapPeriod());
|
|
}
|
|
|
|
/**
|
|
* Gets the source state.
|
|
*
|
|
* @return One of {@link #SOURCE_STATE_NOT_READY}, {@link #SOURCE_STATE_READY} and
|
|
* {@link #SOURCE_STATE_READY_READ_MAY_FAIL}.
|
|
*/
|
|
protected final int getSourceState() {
|
|
return sourceState;
|
|
}
|
|
|
|
private boolean isWithinHotswapPeriod() {
|
|
return SystemClock.elapsedRealtime() < codecHotswapTimeMs + MAX_CODEC_HOTSWAP_TIME_MS;
|
|
}
|
|
|
|
/**
|
|
* @return True if it may be possible to drain more output data. False otherwise.
|
|
* @throws ExoPlaybackException If an error occurs draining the output buffer.
|
|
*/
|
|
@SuppressWarnings("deprecation")
|
|
private boolean drainOutputBuffer(long positionUs, long elapsedRealtimeUs)
|
|
throws ExoPlaybackException {
|
|
if (outputStreamEnded) {
|
|
return false;
|
|
}
|
|
|
|
if (outputIndex < 0) {
|
|
outputIndex = codec.dequeueOutputBuffer(outputBufferInfo, 0);
|
|
}
|
|
|
|
if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
|
|
onOutputFormatChanged(codec.getOutputFormat());
|
|
codecCounters.outputFormatChangedCount++;
|
|
return true;
|
|
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
|
|
outputBuffers = codec.getOutputBuffers();
|
|
codecCounters.outputBuffersChangedCount++;
|
|
return true;
|
|
} else if (outputIndex < 0) {
|
|
return false;
|
|
}
|
|
|
|
if ((outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
if (codecReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
|
|
// We're waiting to re-initialize the codec, and have now received all final output buffers.
|
|
releaseCodec();
|
|
maybeInitCodec();
|
|
} else {
|
|
outputStreamEnded = true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
int decodeOnlyIndex = getDecodeOnlyIndex(outputBufferInfo.presentationTimeUs);
|
|
if (processOutputBuffer(positionUs, elapsedRealtimeUs, codec, outputBuffers[outputIndex],
|
|
outputBufferInfo, outputIndex, decodeOnlyIndex != -1)) {
|
|
if (decodeOnlyIndex != -1) {
|
|
decodeOnlyPresentationTimestamps.remove(decodeOnlyIndex);
|
|
} else {
|
|
currentPositionUs = outputBufferInfo.presentationTimeUs;
|
|
}
|
|
outputIndex = -1;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Processes the provided output buffer.
|
|
*
|
|
* @return True if the output buffer was processed (e.g. rendered or discarded) and hence is no
|
|
* longer required. False otherwise.
|
|
* @throws ExoPlaybackException If an error occurs processing the output buffer.
|
|
*/
|
|
protected abstract boolean processOutputBuffer(long positionUs, long elapsedRealtimeUs,
|
|
MediaCodec codec, ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo, int bufferIndex,
|
|
boolean shouldSkip) throws ExoPlaybackException;
|
|
|
|
private void notifyDecoderInitializationError(final DecoderInitializationException e) {
|
|
if (eventHandler != null && eventListener != null) {
|
|
eventHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
eventListener.onDecoderInitializationError(e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private void notifyCryptoError(final CryptoException e) {
|
|
if (eventHandler != null && eventListener != null) {
|
|
eventHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
eventListener.onCryptoError(e);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private void notifyDecoderInitialized(final String decoderName,
|
|
final long initializedTimestamp, final long initializationDuration) {
|
|
if (eventHandler != null && eventListener != null) {
|
|
eventHandler.post(new Runnable() {
|
|
@Override
|
|
public void run() {
|
|
eventListener.onDecoderInitialized(decoderName, initializedTimestamp,
|
|
initializationDuration);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private int getDecodeOnlyIndex(long presentationTimeUs) {
|
|
final int size = decodeOnlyPresentationTimestamps.size();
|
|
for (int i = 0; i < size; i++) {
|
|
if (decodeOnlyPresentationTimestamps.get(i).longValue() == presentationTimeUs) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
}
|