summaryrefslogtreecommitdiffstats
path: root/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java')
-rw-r--r--mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java758
1 files changed, 758 insertions, 0 deletions
diff --git a/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
new file mode 100644
index 0000000000..5e86e0ad78
--- /dev/null
+++ b/mobile/android/exoplayer2/src/main/java/org/mozilla/thirdparty/com/google/android/exoplayer2/audio/SimpleDecoderAudioRenderer.java
@@ -0,0 +1,758 @@
+/*
+ * 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.audio;
+
+import android.media.audiofx.Virtualizer;
+import android.os.Handler;
+import android.os.SystemClock;
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.BaseRenderer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.C;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlaybackException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.ExoPlayer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.Format;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.FormatHolder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlaybackParameters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.PlayerMessage.Target;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.RendererCapabilities;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispatcher;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderCounters;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.DecoderInputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.SimpleDecoder;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.decoder.SimpleOutputBuffer;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSession.DrmSessionException;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.DrmSessionManager;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.drm.ExoMediaCrypto;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MediaClock;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.MimeTypes;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.TraceUtil;
+import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Decodes and renders audio using a {@link SimpleDecoder}.
+ *
+ * <p>This renderer accepts the following messages sent via {@link ExoPlayer#createMessage(Target)}
+ * on the playback thread:
+ *
+ * <ul>
+ * <li>Message with type {@link C#MSG_SET_VOLUME} to set the volume. The message payload should be
+ * a {@link Float} with 0 being silence and 1 being unity gain.
+ * <li>Message with type {@link C#MSG_SET_AUDIO_ATTRIBUTES} to set the audio attributes. The
+ * message payload should be an {@link org.mozilla.thirdparty.com.google.android.exoplayer2audio.AudioAttributes}
+ * instance that will configure the underlying audio track.
+ * <li>Message with type {@link C#MSG_SET_AUX_EFFECT_INFO} to set the auxiliary effect. The
+ * message payload should be an {@link AuxEffectInfo} instance that will configure the
+ * underlying audio track.
+ * </ul>
+ */
+public abstract class SimpleDecoderAudioRenderer extends BaseRenderer implements MediaClock {
+
+ @Documented
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ REINITIALIZATION_STATE_NONE,
+ REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM,
+ REINITIALIZATION_STATE_WAIT_END_OF_STREAM
+ })
+ private @interface ReinitializationState {}
+ /**
+ * The decoder 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 decoder to be re-initialized, but we
+ * haven't yet signaled an end of stream to the existing decoder. 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 decoder to be re-initialized, and we've
+ * signaled an end of stream to the existing decoder. We're waiting for the decoder 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;
+
+ private final DrmSessionManager<ExoMediaCrypto> drmSessionManager;
+ private final boolean playClearSamplesWithoutKeys;
+ private final EventDispatcher eventDispatcher;
+ private final AudioSink audioSink;
+ private final DecoderInputBuffer flagsOnlyBuffer;
+
+ private boolean drmResourcesAcquired;
+ private DecoderCounters decoderCounters;
+ private Format inputFormat;
+ private int encoderDelay;
+ private int encoderPadding;
+ private SimpleDecoder<DecoderInputBuffer, ? extends SimpleOutputBuffer,
+ ? extends AudioDecoderException> decoder;
+ private DecoderInputBuffer inputBuffer;
+ private SimpleOutputBuffer outputBuffer;
+ @Nullable private DrmSession<ExoMediaCrypto> decoderDrmSession;
+ @Nullable private DrmSession<ExoMediaCrypto> sourceDrmSession;
+
+ @ReinitializationState private int decoderReinitializationState;
+ private boolean decoderReceivedBuffers;
+ private boolean audioTrackNeedsConfigure;
+
+ private long currentPositionUs;
+ private boolean allowFirstBufferPositionDiscontinuity;
+ private boolean allowPositionDiscontinuity;
+ private boolean inputStreamEnded;
+ private boolean outputStreamEnded;
+ private boolean waitingForKeys;
+
+ public SimpleDecoderAudioRenderer() {
+ this(/* eventHandler= */ null, /* eventListener= */ null);
+ }
+
+ /**
+ * @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.
+ * @param audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
+ */
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ AudioProcessor... audioProcessors) {
+ this(
+ eventHandler,
+ eventListener,
+ /* audioCapabilities= */ null,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false,
+ audioProcessors);
+ }
+
+ /**
+ * @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.
+ * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
+ * default capabilities (no encoded audio passthrough support) should be assumed.
+ */
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable AudioCapabilities audioCapabilities) {
+ this(
+ eventHandler,
+ eventListener,
+ audioCapabilities,
+ /* drmSessionManager= */ null,
+ /* playClearSamplesWithoutKeys= */ false);
+ }
+
+ /**
+ * @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.
+ * @param audioCapabilities The audio capabilities for playback on this device. May be null if the
+ * default capabilities (no encoded audio passthrough support) should be assumed.
+ * @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 acquisition. 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 audioProcessors Optional {@link AudioProcessor}s that will process audio before output.
+ */
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable AudioCapabilities audioCapabilities,
+ @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ AudioProcessor... audioProcessors) {
+ this(eventHandler, eventListener, drmSessionManager,
+ playClearSamplesWithoutKeys, new DefaultAudioSink(audioCapabilities, audioProcessors));
+ }
+
+ /**
+ * @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.
+ * @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 acquisition. 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 audioSink The sink to which audio will be output.
+ */
+ public SimpleDecoderAudioRenderer(
+ @Nullable Handler eventHandler,
+ @Nullable AudioRendererEventListener eventListener,
+ @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager,
+ boolean playClearSamplesWithoutKeys,
+ AudioSink audioSink) {
+ super(C.TRACK_TYPE_AUDIO);
+ this.drmSessionManager = drmSessionManager;
+ this.playClearSamplesWithoutKeys = playClearSamplesWithoutKeys;
+ eventDispatcher = new EventDispatcher(eventHandler, eventListener);
+ this.audioSink = audioSink;
+ audioSink.setListener(new AudioSinkListener());
+ flagsOnlyBuffer = DecoderInputBuffer.newFlagsOnlyInstance();
+ decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+ audioTrackNeedsConfigure = true;
+ }
+
+ @Override
+ @Nullable
+ public MediaClock getMediaClock() {
+ return this;
+ }
+
+ @Override
+ @Capabilities
+ public final int supportsFormat(Format format) {
+ if (!MimeTypes.isAudio(format.sampleMimeType)) {
+ return RendererCapabilities.create(FORMAT_UNSUPPORTED_TYPE);
+ }
+ @FormatSupport int formatSupport = supportsFormatInternal(drmSessionManager, format);
+ if (formatSupport <= FORMAT_UNSUPPORTED_DRM) {
+ return RendererCapabilities.create(formatSupport);
+ }
+ @TunnelingSupport
+ int tunnelingSupport = Util.SDK_INT >= 21 ? TUNNELING_SUPPORTED : TUNNELING_NOT_SUPPORTED;
+ return RendererCapabilities.create(formatSupport, ADAPTIVE_NOT_SEAMLESS, tunnelingSupport);
+ }
+
+ /**
+ * Returns the {@link FormatSupport} for the given {@link Format}.
+ *
+ * @param drmSessionManager The renderer's {@link DrmSessionManager}.
+ * @param format The format, which has an audio {@link Format#sampleMimeType}.
+ * @return The {@link FormatSupport} for this {@link Format}.
+ */
+ @FormatSupport
+ protected abstract int supportsFormatInternal(
+ @Nullable DrmSessionManager<ExoMediaCrypto> drmSessionManager, Format format);
+
+ /**
+ * Returns whether the sink supports the audio format.
+ *
+ * @see AudioSink#supportsOutput(int, int)
+ */
+ protected final boolean supportsOutput(int channelCount, @C.Encoding int encoding) {
+ return audioSink.supportsOutput(channelCount, encoding);
+ }
+
+ @Override
+ public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException {
+ if (outputStreamEnded) {
+ try {
+ audioSink.playToEndOfStream();
+ } catch (AudioSink.WriteException e) {
+ throw createRendererException(e, inputFormat);
+ }
+ return;
+ }
+
+ // Try and read a format if we don't have one already.
+ if (inputFormat == null) {
+ // We don't have a format yet, so try and read one.
+ FormatHolder formatHolder = getFormatHolder();
+ flagsOnlyBuffer.clear();
+ int result = readSource(formatHolder, flagsOnlyBuffer, true);
+ if (result == C.RESULT_FORMAT_READ) {
+ onInputFormatChanged(formatHolder);
+ } else if (result == C.RESULT_BUFFER_READ) {
+ // End of stream read having not read a format.
+ Assertions.checkState(flagsOnlyBuffer.isEndOfStream());
+ inputStreamEnded = true;
+ processEndOfStream();
+ return;
+ } else {
+ // We still don't have a format and can't make progress without one.
+ return;
+ }
+ }
+
+ // If we don't have a decoder yet, we need to instantiate one.
+ maybeInitDecoder();
+
+ if (decoder != null) {
+ try {
+ // Rendering loop.
+ TraceUtil.beginSection("drainAndFeed");
+ while (drainOutputBuffer()) {}
+ while (feedInputBuffer()) {}
+ TraceUtil.endSection();
+ } catch (AudioDecoderException | AudioSink.ConfigurationException
+ | AudioSink.InitializationException | AudioSink.WriteException e) {
+ throw createRendererException(e, inputFormat);
+ }
+ decoderCounters.ensureUpdated();
+ }
+ }
+
+ /**
+ * Called when the audio session id becomes known. The default implementation is a no-op. One
+ * reason for overriding this method would be to instantiate and enable a {@link Virtualizer} in
+ * order to spatialize the audio channels. For this use case, any {@link Virtualizer} instances
+ * should be released in {@link #onDisabled()} (if not before).
+ *
+ * @see AudioSink.Listener#onAudioSessionId(int)
+ */
+ protected void onAudioSessionId(int audioSessionId) {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioSink.Listener#onPositionDiscontinuity()
+ */
+ protected void onAudioTrackPositionDiscontinuity() {
+ // Do nothing.
+ }
+
+ /**
+ * @see AudioSink.Listener#onUnderrun(int, long, long)
+ */
+ protected void onAudioTrackUnderrun(int bufferSize, long bufferSizeMs,
+ long elapsedSinceLastFeedMs) {
+ // Do nothing.
+ }
+
+ /**
+ * Creates a decoder for the given format.
+ *
+ * @param format The format for which a decoder is required.
+ * @param mediaCrypto The {@link ExoMediaCrypto} object required for decoding encrypted content.
+ * Maybe null and can be ignored if decoder does not handle encrypted content.
+ * @return The decoder.
+ * @throws AudioDecoderException If an error occurred creating a suitable decoder.
+ */
+ protected abstract SimpleDecoder<
+ DecoderInputBuffer, ? extends SimpleOutputBuffer, ? extends AudioDecoderException>
+ createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto)
+ throws AudioDecoderException;
+
+ /**
+ * Returns the format of audio buffers output by the decoder. Will not be called until the first
+ * output buffer has been dequeued, so the decoder may use input data to determine the format.
+ */
+ protected abstract Format getOutputFormat();
+
+ /**
+ * Returns whether the existing decoder can be kept for a new format.
+ *
+ * @param oldFormat The previous format.
+ * @param newFormat The new format.
+ * @return True if the existing decoder can be kept.
+ */
+ protected boolean canKeepCodec(Format oldFormat, Format newFormat) {
+ return false;
+ }
+
+ private boolean drainOutputBuffer() throws ExoPlaybackException, AudioDecoderException,
+ AudioSink.ConfigurationException, AudioSink.InitializationException,
+ AudioSink.WriteException {
+ if (outputBuffer == null) {
+ outputBuffer = decoder.dequeueOutputBuffer();
+ if (outputBuffer == null) {
+ return false;
+ }
+ if (outputBuffer.skippedOutputBufferCount > 0) {
+ decoderCounters.skippedOutputBufferCount += outputBuffer.skippedOutputBufferCount;
+ audioSink.handleDiscontinuity();
+ }
+ }
+
+ if (outputBuffer.isEndOfStream()) {
+ if (decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM) {
+ // We're waiting to re-initialize the decoder, and have now processed all final buffers.
+ releaseDecoder();
+ maybeInitDecoder();
+ // The audio track may need to be recreated once the new output format is known.
+ audioTrackNeedsConfigure = true;
+ } else {
+ outputBuffer.release();
+ outputBuffer = null;
+ processEndOfStream();
+ }
+ return false;
+ }
+
+ if (audioTrackNeedsConfigure) {
+ Format outputFormat = getOutputFormat();
+ audioSink.configure(outputFormat.pcmEncoding, outputFormat.channelCount,
+ outputFormat.sampleRate, 0, null, encoderDelay, encoderPadding);
+ audioTrackNeedsConfigure = false;
+ }
+
+ if (audioSink.handleBuffer(outputBuffer.data, outputBuffer.timeUs)) {
+ decoderCounters.renderedOutputBufferCount++;
+ outputBuffer.release();
+ outputBuffer = null;
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean feedInputBuffer() throws AudioDecoderException, ExoPlaybackException {
+ if (decoder == null || decoderReinitializationState == REINITIALIZATION_STATE_WAIT_END_OF_STREAM
+ || inputStreamEnded) {
+ // We need to reinitialize the decoder or the input stream has ended.
+ return false;
+ }
+
+ if (inputBuffer == null) {
+ inputBuffer = decoder.dequeueInputBuffer();
+ if (inputBuffer == null) {
+ return false;
+ }
+ }
+
+ if (decoderReinitializationState == REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM) {
+ inputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
+ decoder.queueInputBuffer(inputBuffer);
+ inputBuffer = null;
+ decoderReinitializationState = REINITIALIZATION_STATE_WAIT_END_OF_STREAM;
+ return false;
+ }
+
+ int result;
+ FormatHolder formatHolder = getFormatHolder();
+ if (waitingForKeys) {
+ // We've already read an encrypted sample into buffer, and are waiting for keys.
+ result = C.RESULT_BUFFER_READ;
+ } else {
+ result = readSource(formatHolder, inputBuffer, false);
+ }
+
+ if (result == C.RESULT_NOTHING_READ) {
+ return false;
+ }
+ if (result == C.RESULT_FORMAT_READ) {
+ onInputFormatChanged(formatHolder);
+ return true;
+ }
+ if (inputBuffer.isEndOfStream()) {
+ inputStreamEnded = true;
+ decoder.queueInputBuffer(inputBuffer);
+ inputBuffer = null;
+ return false;
+ }
+ boolean bufferEncrypted = inputBuffer.isEncrypted();
+ waitingForKeys = shouldWaitForKeys(bufferEncrypted);
+ if (waitingForKeys) {
+ return false;
+ }
+ inputBuffer.flip();
+ onQueueInputBuffer(inputBuffer);
+ decoder.queueInputBuffer(inputBuffer);
+ decoderReceivedBuffers = true;
+ decoderCounters.inputBufferCount++;
+ inputBuffer = null;
+ return true;
+ }
+
+ private boolean shouldWaitForKeys(boolean bufferEncrypted) throws ExoPlaybackException {
+ if (decoderDrmSession == null
+ || (!bufferEncrypted
+ && (playClearSamplesWithoutKeys || decoderDrmSession.playClearSamplesWithoutKeys()))) {
+ return false;
+ }
+ @DrmSession.State int drmSessionState = decoderDrmSession.getState();
+ if (drmSessionState == DrmSession.STATE_ERROR) {
+ throw createRendererException(decoderDrmSession.getError(), inputFormat);
+ }
+ return drmSessionState != DrmSession.STATE_OPENED_WITH_KEYS;
+ }
+
+ private void processEndOfStream() throws ExoPlaybackException {
+ outputStreamEnded = true;
+ try {
+ audioSink.playToEndOfStream();
+ } catch (AudioSink.WriteException e) {
+ // TODO(internal: b/145658993) Use outputFormat for the call from drainOutputBuffer.
+ throw createRendererException(e, inputFormat);
+ }
+ }
+
+ private void flushDecoder() throws ExoPlaybackException {
+ waitingForKeys = false;
+ if (decoderReinitializationState != REINITIALIZATION_STATE_NONE) {
+ releaseDecoder();
+ maybeInitDecoder();
+ } else {
+ inputBuffer = null;
+ if (outputBuffer != null) {
+ outputBuffer.release();
+ outputBuffer = null;
+ }
+ decoder.flush();
+ decoderReceivedBuffers = false;
+ }
+ }
+
+ @Override
+ public boolean isEnded() {
+ return outputStreamEnded && audioSink.isEnded();
+ }
+
+ @Override
+ public boolean isReady() {
+ return audioSink.hasPendingData()
+ || (inputFormat != null && !waitingForKeys && (isSourceReady() || outputBuffer != null));
+ }
+
+ @Override
+ public long getPositionUs() {
+ if (getState() == STATE_STARTED) {
+ updateCurrentPosition();
+ }
+ return currentPositionUs;
+ }
+
+ @Override
+ public void setPlaybackParameters(PlaybackParameters playbackParameters) {
+ audioSink.setPlaybackParameters(playbackParameters);
+ }
+
+ @Override
+ public PlaybackParameters getPlaybackParameters() {
+ return audioSink.getPlaybackParameters();
+ }
+
+ @Override
+ protected void onEnabled(boolean joining) throws ExoPlaybackException {
+ if (drmSessionManager != null && !drmResourcesAcquired) {
+ drmResourcesAcquired = true;
+ drmSessionManager.prepare();
+ }
+ decoderCounters = new DecoderCounters();
+ eventDispatcher.enabled(decoderCounters);
+ int tunnelingAudioSessionId = getConfiguration().tunnelingAudioSessionId;
+ if (tunnelingAudioSessionId != C.AUDIO_SESSION_ID_UNSET) {
+ audioSink.enableTunnelingV21(tunnelingAudioSessionId);
+ } else {
+ audioSink.disableTunneling();
+ }
+ }
+
+ @Override
+ protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException {
+ audioSink.flush();
+ currentPositionUs = positionUs;
+ allowFirstBufferPositionDiscontinuity = true;
+ allowPositionDiscontinuity = true;
+ inputStreamEnded = false;
+ outputStreamEnded = false;
+ if (decoder != null) {
+ flushDecoder();
+ }
+ }
+
+ @Override
+ protected void onStarted() {
+ audioSink.play();
+ }
+
+ @Override
+ protected void onStopped() {
+ updateCurrentPosition();
+ audioSink.pause();
+ }
+
+ @Override
+ protected void onDisabled() {
+ inputFormat = null;
+ audioTrackNeedsConfigure = true;
+ waitingForKeys = false;
+ try {
+ setSourceDrmSession(null);
+ releaseDecoder();
+ audioSink.reset();
+ } finally {
+ eventDispatcher.disabled(decoderCounters);
+ }
+ }
+
+ @Override
+ protected void onReset() {
+ if (drmSessionManager != null && drmResourcesAcquired) {
+ drmResourcesAcquired = false;
+ drmSessionManager.release();
+ }
+ }
+
+ @Override
+ public void handleMessage(int messageType, @Nullable Object message) throws ExoPlaybackException {
+ switch (messageType) {
+ case C.MSG_SET_VOLUME:
+ audioSink.setVolume((Float) message);
+ break;
+ case C.MSG_SET_AUDIO_ATTRIBUTES:
+ AudioAttributes audioAttributes = (AudioAttributes) message;
+ audioSink.setAudioAttributes(audioAttributes);
+ break;
+ case C.MSG_SET_AUX_EFFECT_INFO:
+ AuxEffectInfo auxEffectInfo = (AuxEffectInfo) message;
+ audioSink.setAuxEffectInfo(auxEffectInfo);
+ break;
+ default:
+ super.handleMessage(messageType, message);
+ break;
+ }
+ }
+
+ private void maybeInitDecoder() throws ExoPlaybackException {
+ if (decoder != null) {
+ return;
+ }
+
+ setDecoderDrmSession(sourceDrmSession);
+
+ ExoMediaCrypto mediaCrypto = null;
+ if (decoderDrmSession != null) {
+ mediaCrypto = decoderDrmSession.getMediaCrypto();
+ if (mediaCrypto == null) {
+ DrmSessionException drmError = decoderDrmSession.getError();
+ if (drmError != null) {
+ // Continue for now. We may be able to avoid failure if the session recovers, or if a new
+ // input format causes the session to be replaced before it's used.
+ } else {
+ // The drm session isn't open yet.
+ return;
+ }
+ }
+ }
+
+ try {
+ long codecInitializingTimestamp = SystemClock.elapsedRealtime();
+ TraceUtil.beginSection("createAudioDecoder");
+ decoder = createDecoder(inputFormat, mediaCrypto);
+ TraceUtil.endSection();
+ long codecInitializedTimestamp = SystemClock.elapsedRealtime();
+ eventDispatcher.decoderInitialized(decoder.getName(), codecInitializedTimestamp,
+ codecInitializedTimestamp - codecInitializingTimestamp);
+ decoderCounters.decoderInitCount++;
+ } catch (AudioDecoderException e) {
+ throw createRendererException(e, inputFormat);
+ }
+ }
+
+ private void releaseDecoder() {
+ inputBuffer = null;
+ outputBuffer = null;
+ decoderReinitializationState = REINITIALIZATION_STATE_NONE;
+ decoderReceivedBuffers = false;
+ if (decoder != null) {
+ decoder.release();
+ decoder = null;
+ decoderCounters.decoderReleaseCount++;
+ }
+ setDecoderDrmSession(null);
+ }
+
+ private void setSourceDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
+ DrmSession.replaceSession(sourceDrmSession, session);
+ sourceDrmSession = session;
+ }
+
+ private void setDecoderDrmSession(@Nullable DrmSession<ExoMediaCrypto> session) {
+ DrmSession.replaceSession(decoderDrmSession, session);
+ decoderDrmSession = session;
+ }
+
+ @SuppressWarnings("unchecked")
+ private void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException {
+ Format newFormat = Assertions.checkNotNull(formatHolder.format);
+ if (formatHolder.includesDrmSession) {
+ setSourceDrmSession((DrmSession<ExoMediaCrypto>) formatHolder.drmSession);
+ } else {
+ sourceDrmSession =
+ getUpdatedSourceDrmSession(inputFormat, newFormat, drmSessionManager, sourceDrmSession);
+ }
+ Format oldFormat = inputFormat;
+ inputFormat = newFormat;
+
+ if (!canKeepCodec(oldFormat, inputFormat)) {
+ if (decoderReceivedBuffers) {
+ // Signal end of stream and wait for any final output buffers before re-initialization.
+ decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM;
+ } else {
+ // There aren't any final output buffers, so release the decoder immediately.
+ releaseDecoder();
+ maybeInitDecoder();
+ audioTrackNeedsConfigure = true;
+ }
+ }
+
+ encoderDelay = inputFormat.encoderDelay;
+ encoderPadding = inputFormat.encoderPadding;
+
+ eventDispatcher.inputFormatChanged(inputFormat);
+ }
+
+ private void onQueueInputBuffer(DecoderInputBuffer buffer) {
+ if (allowFirstBufferPositionDiscontinuity && !buffer.isDecodeOnly()) {
+ // TODO: Remove this hack once we have a proper fix for [Internal: b/71876314].
+ // Allow the position to jump if the first presentable input buffer has a timestamp that
+ // differs significantly from what was expected.
+ if (Math.abs(buffer.timeUs - currentPositionUs) > 500000) {
+ currentPositionUs = buffer.timeUs;
+ }
+ allowFirstBufferPositionDiscontinuity = false;
+ }
+ }
+
+ private void updateCurrentPosition() {
+ long newCurrentPositionUs = audioSink.getCurrentPositionUs(isEnded());
+ if (newCurrentPositionUs != AudioSink.CURRENT_POSITION_NOT_SET) {
+ currentPositionUs =
+ allowPositionDiscontinuity
+ ? newCurrentPositionUs
+ : Math.max(currentPositionUs, newCurrentPositionUs);
+ allowPositionDiscontinuity = false;
+ }
+ }
+
+ private final class AudioSinkListener implements AudioSink.Listener {
+
+ @Override
+ public void onAudioSessionId(int audioSessionId) {
+ eventDispatcher.audioSessionId(audioSessionId);
+ SimpleDecoderAudioRenderer.this.onAudioSessionId(audioSessionId);
+ }
+
+ @Override
+ public void onPositionDiscontinuity() {
+ onAudioTrackPositionDiscontinuity();
+ // We are out of sync so allow currentPositionUs to jump backwards.
+ SimpleDecoderAudioRenderer.this.allowPositionDiscontinuity = true;
+ }
+
+ @Override
+ public void onUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
+ eventDispatcher.audioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ onAudioTrackUnderrun(bufferSize, bufferSizeMs, elapsedSinceLastFeedMs);
+ }
+
+ }
+
+}