diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 94d173da4e..6c0d929ca5 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -48,6 +48,12 @@ * DRM: * Fix playback failure when switching from PlayReady protected content to Widevine or Clearkey protected content in a playlist. +* Analytics: + * Pass a `DecoderReuseEvaluation` to `AnalyticsListener`'s + `onVideoInputFormatChanged` and `onAudioInputFormatChanged` methods. The + `DecoderReuseEvaluation` indicates whether it was possible to re-use an + existing decoder instance for the new format, and if not then the + reasons why. * IMA extension: * Upgrade IMA SDK dependency to 3.21.0, and release the `AdsLoader` ([#7344](https://github.com/google/ExoPlayer/issues/7344)). diff --git a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java index 7c558d24b2..a90ec1307c 100644 --- a/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java +++ b/extensions/av1/src/main/java/com/google/android/exoplayer2/ext/av1/Libgav1VideoRenderer.java @@ -15,12 +15,15 @@ */ package com.google.android.exoplayer2.ext.av1; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; + import android.os.Handler; import android.view.Surface; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; @@ -164,7 +167,13 @@ public class Libgav1VideoRenderer extends DecoderVideoRenderer { } @Override - protected boolean canKeepCodec(Format oldFormat, Format newFormat) { - return true; + protected DecoderReuseEvaluation canReuseDecoder( + String decoderName, Format oldFormat, Format newFormat) { + return new DecoderReuseEvaluation( + decoderName, + oldFormat, + newFormat, + REUSE_RESULT_YES_WITHOUT_RECONFIGURATION, + /* discardReasons= */ 0); } } diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java index d2f2fce639..19871898cb 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java @@ -15,6 +15,10 @@ */ package com.google.android.exoplayer2.ext.ffmpeg; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MIME_TYPE_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; + import android.os.Handler; import android.view.Surface; import androidx.annotation.Nullable; @@ -22,6 +26,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.Decoder; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.TraceUtil; import com.google.android.exoplayer2.util.Util; @@ -116,7 +121,15 @@ public final class FfmpegVideoRenderer extends DecoderVideoRenderer { } @Override - protected boolean canKeepCodec(Format oldFormat, Format newFormat) { - return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType); + protected DecoderReuseEvaluation canReuseDecoder( + String decoderName, Format oldFormat, Format newFormat) { + boolean sameMimeType = Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType); + // TODO: Ability to reuse the decoder may be MIME type dependent. + return new DecoderReuseEvaluation( + decoderName, + oldFormat, + newFormat, + sameMimeType ? REUSE_RESULT_YES_WITHOUT_RECONFIGURATION : REUSE_RESULT_NO, + sameMimeType ? 0 : DISCARD_REASON_MIME_TYPE_CHANGED); } } diff --git a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java index 61ebc8b0d9..2e17036fce 100644 --- a/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java +++ b/extensions/vp9/src/main/java/com/google/android/exoplayer2/ext/vp9/LibvpxVideoRenderer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.ext.vp9; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; import static java.lang.Runtime.getRuntime; import android.os.Handler; @@ -23,6 +24,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.TraceUtil; @@ -169,7 +171,13 @@ public class LibvpxVideoRenderer extends DecoderVideoRenderer { } @Override - protected boolean canKeepCodec(Format oldFormat, Format newFormat) { - return true; + protected DecoderReuseEvaluation canReuseDecoder( + String decoderName, Format oldFormat, Format newFormat) { + return new DecoderReuseEvaluation( + decoderName, + oldFormat, + newFormat, + REUSE_RESULT_YES_WITHOUT_RECONFIGURATION, + /* discardReasons= */ 0); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index c09a43feb6..e82e0dcba8 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -36,6 +36,7 @@ import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.device.DeviceInfo; import com.google.android.exoplayer2.device.DeviceListener; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; @@ -2242,10 +2243,11 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void onVideoInputFormatChanged(Format format) { + public void onVideoInputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { videoFormat = format; for (VideoRendererEventListener videoDebugListener : videoDebugListeners) { - videoDebugListener.onVideoInputFormatChanged(format); + videoDebugListener.onVideoInputFormatChanged(format, decoderReuseEvaluation); } } @@ -2337,10 +2339,11 @@ public class SimpleExoPlayer extends BasePlayer } @Override - public void onAudioInputFormatChanged(Format format) { + public void onAudioInputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { audioFormat = format; for (AudioRendererEventListener audioDebugListener : audioDebugListeners) { - audioDebugListener.onAudioInputFormatChanged(format); + audioDebugListener.onAudioInputFormatChanged(format, decoderReuseEvaluation); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java index b19ed00276..a11953421b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsCollector.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioListener; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; @@ -195,11 +196,12 @@ public class AnalyticsCollector @SuppressWarnings("deprecation") @Override - public final void onAudioInputFormatChanged(Format format) { + public final void onAudioInputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( listener -> { - listener.onAudioInputFormatChanged(eventTime, format); + listener.onAudioInputFormatChanged(eventTime, format, decoderReuseEvaluation); listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_AUDIO, format); }); } @@ -298,11 +300,12 @@ public class AnalyticsCollector @SuppressWarnings("deprecation") @Override - public final void onVideoInputFormatChanged(Format format) { + public final void onVideoInputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { EventTime eventTime = generateReadingMediaPeriodEventTime(); listeners.sendEvent( listener -> { - listener.onVideoInputFormatChanged(eventTime, format); + listener.onVideoInputFormatChanged(eventTime, format, decoderReuseEvaluation); listener.onDecoderInputFormatChanged(eventTime, C.TRACK_TYPE_VIDEO, format); }); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java index 26e93cc982..87c6308990 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/analytics/AnalyticsListener.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; @@ -452,8 +453,8 @@ public interface AnalyticsListener { EventTime eventTime, int trackType, String decoderName, long initializationDurationMs) {} /** - * @deprecated Use {@link #onAudioInputFormatChanged} and {@link #onVideoInputFormatChanged} - * instead. + * @deprecated Use {@link #onAudioInputFormatChanged(EventTime, Format, DecoderReuseEvaluation)} + * and {@link #onVideoInputFormatChanged(EventTime, Format, DecoderReuseEvaluation)}. instead. */ @Deprecated default void onDecoderInputFormatChanged(EventTime eventTime, int trackType, Format format) {} @@ -482,13 +483,26 @@ public interface AnalyticsListener { default void onAudioDecoderInitialized( EventTime eventTime, String decoderName, long initializationDurationMs) {} + /** + * @deprecated Use {@link #onAudioInputFormatChanged(EventTime, Format, DecoderReuseEvaluation)}. + */ + @Deprecated + default void onAudioInputFormatChanged(EventTime eventTime, Format format) {} + /** * Called when the format of the media being consumed by an audio renderer changes. * * @param eventTime The event time. * @param format The new format. + * @param decoderReuseEvaluation The result of the evaluation to determine whether an existing + * decoder instance can be reused for the new format, or {@code null} if the renderer did not + * have a decoder. */ - default void onAudioInputFormatChanged(EventTime eventTime, Format format) {} + @SuppressWarnings("deprecation") + default void onAudioInputFormatChanged( + EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { + onAudioInputFormatChanged(eventTime, format); + } /** * Called when the audio position has increased for the first time since the last pause or @@ -589,13 +603,26 @@ public interface AnalyticsListener { default void onVideoDecoderInitialized( EventTime eventTime, String decoderName, long initializationDurationMs) {} + /** + * @deprecated Use {@link #onVideoInputFormatChanged(EventTime, Format, DecoderReuseEvaluation)}. + */ + @Deprecated + default void onVideoInputFormatChanged(EventTime eventTime, Format format) {} + /** * Called when the format of the media being consumed by a video renderer changes. * * @param eventTime The event time. * @param format The new format. + * @param decoderReuseEvaluation The result of the evaluation to determine whether an existing + * decoder instance can be reused for the new format, or {@code null} if the renderer did not + * have a decoder. */ - default void onVideoInputFormatChanged(EventTime eventTime, Format format) {} + @SuppressWarnings("deprecation") + default void onVideoInputFormatChanged( + EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { + onVideoInputFormatChanged(eventTime, format); + } /** * Called after video frames have been dropped. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 31c3b1f0b3..48d0e03144 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -27,6 +27,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.util.Assertions; /** @@ -61,12 +62,23 @@ public interface AudioRendererEventListener { default void onAudioDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) {} + /** @deprecated Use {@link #onAudioInputFormatChanged(Format, DecoderReuseEvaluation)}. */ + @Deprecated + default void onAudioInputFormatChanged(Format format) {} + /** * Called when the format of the media being consumed by the renderer changes. * * @param format The new format. + * @param decoderReuseEvaluation The result of the evaluation to determine whether an existing + * decoder instance can be reused for the new format, or {@code null} if the renderer did not + * have a decoder. */ - default void onAudioInputFormatChanged(Format format) {} + @SuppressWarnings("deprecation") + default void onAudioInputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { + onAudioInputFormatChanged(format); + } /** * Called when the audio position has increased for the first time since the last pause or @@ -167,9 +179,11 @@ public interface AudioRendererEventListener { } /** Invokes {@link AudioRendererEventListener#onAudioInputFormatChanged(Format)}. */ - public void inputFormatChanged(Format format) { + public void inputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { if (handler != null) { - handler.post(() -> castNonNull(listener).onAudioInputFormatChanged(format)); + handler.post( + () -> castNonNull(listener).onAudioInputFormatChanged(format, decoderReuseEvaluation)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java index 5495c3d97b..7ecf58f0b2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DecoderAudioRenderer.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.audio; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_DRM_SESSION_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_REUSE_NOT_IMPLEMENTED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO; import static java.lang.Math.max; import android.media.audiofx.Virtualizer; @@ -38,6 +41,7 @@ import com.google.android.exoplayer2.decoder.Decoder; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.decoder.SimpleOutputBuffer; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; @@ -347,14 +351,19 @@ public abstract class DecoderAudioRenderer< protected abstract Format getOutputFormat(T decoder); /** - * Returns whether the existing decoder can be kept for a new format. + * Evaluates whether the existing decoder can be reused for a new {@link Format}. * + *

The default implementation does not allow decoder reuse. + * + * @param decoderName The name of the decoder. * @param oldFormat The previous format. * @param newFormat The new format. - * @return Whether the existing decoder can be kept. + * @return The result of the evaluation. */ - protected boolean canKeepCodec(Format oldFormat, Format newFormat) { - return false; + protected DecoderReuseEvaluation canReuseDecoder( + String decoderName, Format oldFormat, Format newFormat) { + return new DecoderReuseEvaluation( + decoderName, oldFormat, newFormat, REUSE_RESULT_NO, DISCARD_REASON_REUSE_NOT_IMPLEMENTED); } private boolean drainOutputBuffer() @@ -655,10 +664,29 @@ public abstract class DecoderAudioRenderer< setSourceDrmSession(formatHolder.drmSession); Format oldFormat = inputFormat; inputFormat = newFormat; + encoderDelay = newFormat.encoderDelay; + encoderPadding = newFormat.encoderPadding; if (decoder == null) { maybeInitDecoder(); - } else if (sourceDrmSession != decoderDrmSession || !canKeepCodec(oldFormat, inputFormat)) { + eventDispatcher.inputFormatChanged(inputFormat, /* decoderReuseEvaluation= */ null); + return; + } + + DecoderReuseEvaluation evaluation; + if (sourceDrmSession != decoderDrmSession) { + evaluation = + new DecoderReuseEvaluation( + decoder.getName(), + oldFormat, + newFormat, + REUSE_RESULT_NO, + DISCARD_REASON_DRM_SESSION_CHANGED); + } else { + evaluation = canReuseDecoder(decoder.getName(), oldFormat, newFormat); + } + + if (evaluation.result == REUSE_RESULT_NO) { if (decoderReceivedBuffers) { // Signal end of stream and wait for any final output buffers before re-initialization. decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; @@ -669,10 +697,7 @@ public abstract class DecoderAudioRenderer< audioTrackNeedsConfigure = true; } } - - encoderDelay = inputFormat.encoderDelay; - encoderPadding = inputFormat.encoderPadding; - eventDispatcher.inputFormatChanged(inputFormat); + eventDispatcher.inputFormatChanged(inputFormat, evaluation); } private void onQueueInputBuffer(DecoderInputBuffer buffer) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 675516cb03..19988cd54b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -15,7 +15,8 @@ */ package com.google.android.exoplayer2.audio; -import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_NO; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static java.lang.Math.max; @@ -41,9 +42,10 @@ import com.google.android.exoplayer2.audio.AudioRendererEventListener.EventDispa import com.google.android.exoplayer2.audio.AudioSink.InitializationException; import com.google.android.exoplayer2.audio.AudioSink.WriteException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DecoderDiscardReasons; import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; -import com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KeepCodecResult; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; @@ -328,13 +330,21 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - @KeepCodecResult - protected int canKeepCodec( - MediaCodecAdapter codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + protected DecoderReuseEvaluation canReuseCodec( + MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + DecoderReuseEvaluation evaluation = codecInfo.canReuseCodec(oldFormat, newFormat); + + @DecoderDiscardReasons int discardReasons = evaluation.discardReasons; if (getCodecMaxInputSize(codecInfo, newFormat) > codecMaxInputSize) { - return KEEP_CODEC_RESULT_NO; + discardReasons |= DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED; } - return codecInfo.canKeepCodec(oldFormat, newFormat); + + return new DecoderReuseEvaluation( + codecInfo.name, + oldFormat, + newFormat, + discardReasons != 0 ? REUSE_RESULT_NO : evaluation.result, + discardReasons); } @Override @@ -370,9 +380,12 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } @Override - protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { - super.onInputFormatChanged(formatHolder); - eventDispatcher.inputFormatChanged(formatHolder.format); + @Nullable + protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) + throws ExoPlaybackException { + @Nullable DecoderReuseEvaluation evaluation = super.onInputFormatChanged(formatHolder); + eventDispatcher.inputFormatChanged(formatHolder.format, evaluation); + return evaluation; } @Override @@ -664,7 +677,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media return maxInputSize; } for (Format streamFormat : streamFormats) { - if (codecInfo.canKeepCodec(format, streamFormat) != KEEP_CODEC_RESULT_NO) { + if (codecInfo.canReuseCodec(format, streamFormat).result != REUSE_RESULT_NO) { maxInputSize = max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat)); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderReuseEvaluation.java b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderReuseEvaluation.java new file mode 100644 index 0000000000..a23c92ec65 --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/decoder/DecoderReuseEvaluation.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2020 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.decoder; + +import static com.google.android.exoplayer2.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkNotEmpty; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + +import androidx.annotation.IntDef; +import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.video.ColorInfo; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * The result of an evaluation to determine whether a decoder can be reused for a new input format. + */ +public final class DecoderReuseEvaluation { + + /** Possible outcomes of the evaluation. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef({ + REUSE_RESULT_NO, + REUSE_RESULT_YES_WITH_FLUSH, + REUSE_RESULT_YES_WITH_RECONFIGURATION, + REUSE_RESULT_YES_WITHOUT_RECONFIGURATION + }) + public @interface DecoderReuseResult {} + /** The decoder cannot be reused. */ + public static final int REUSE_RESULT_NO = 0; + /** The decoder can be reused, but must be flushed. */ + public static final int REUSE_RESULT_YES_WITH_FLUSH = 1; + /** + * The decoder can be reused. It does not need to be flushed, but must be reconfigured by + * prefixing the next input buffer with the new format's configuration data. + */ + public static final int REUSE_RESULT_YES_WITH_RECONFIGURATION = 2; + /** The decoder can be kept. It does not need to be flushed and no reconfiguration is required. */ + public static final int REUSE_RESULT_YES_WITHOUT_RECONFIGURATION = 3; + + /** Possible reasons why reuse is not possible. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @IntDef( + flag = true, + value = { + DISCARD_REASON_REUSE_NOT_IMPLEMENTED, + DISCARD_REASON_WORKAROUND, + DISCARD_REASON_APP_OVERRIDE, + DISCARD_REASON_MIME_TYPE_CHANGED, + DISCARD_REASON_OPERATING_RATE_CHANGED, + DISCARD_REASON_INITIALIZATION_DATA_CHANGED, + DISCARD_REASON_DRM_SESSION_CHANGED, + DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED, + DISCARD_REASON_VIDEO_MAX_RESOLUTION_EXCEEDED, + DISCARD_REASON_VIDEO_RESOLUTION_CHANGED, + DISCARD_REASON_VIDEO_ROTATION_CHANGED, + DISCARD_REASON_VIDEO_COLOR_INFO_CHANGED, + DISCARD_REASON_AUDIO_CHANNEL_COUNT_CHANGED, + DISCARD_REASON_AUDIO_SAMPLE_RATE_CHANGED, + DISCARD_REASON_AUDIO_ENCODING_CHANGED + }) + public @interface DecoderDiscardReasons {} + + /** Decoder reuse is not implemented. */ + public static final int DISCARD_REASON_REUSE_NOT_IMPLEMENTED = 1 << 0; + /** Decoder reuse is disabled by a workaround. */ + public static final int DISCARD_REASON_WORKAROUND = 1 << 1; + /** Decoder reuse is disabled by overriding behavior in application code. */ + public static final int DISCARD_REASON_APP_OVERRIDE = 1 << 2; + /** The sample MIME type is changing. */ + public static final int DISCARD_REASON_MIME_TYPE_CHANGED = 1 << 3; + /** The codec's operating rate is changing. */ + public static final int DISCARD_REASON_OPERATING_RATE_CHANGED = 1 << 4; + /** The format initialization data is changing. */ + public static final int DISCARD_REASON_INITIALIZATION_DATA_CHANGED = 1 << 5; + /** The new format may exceed the decoder's configured maximum sample size, in bytes. */ + public static final int DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED = 1 << 6; + /** The DRM session is changing. */ + public static final int DISCARD_REASON_DRM_SESSION_CHANGED = 1 << 7; + /** The new format may exceed the decoder's configured maximum resolution. */ + public static final int DISCARD_REASON_VIDEO_MAX_RESOLUTION_EXCEEDED = 1 << 8; + /** The video resolution is changing. */ + public static final int DISCARD_REASON_VIDEO_RESOLUTION_CHANGED = 1 << 9; + /** The video rotation is changing. */ + public static final int DISCARD_REASON_VIDEO_ROTATION_CHANGED = 1 << 10; + /** The video {@link ColorInfo} is changing. */ + public static final int DISCARD_REASON_VIDEO_COLOR_INFO_CHANGED = 1 << 11; + /** The audio channel count is changing. */ + public static final int DISCARD_REASON_AUDIO_CHANNEL_COUNT_CHANGED = 1 << 12; + /** The audio sample rate is changing. */ + public static final int DISCARD_REASON_AUDIO_SAMPLE_RATE_CHANGED = 1 << 13; + /** The audio encoding is changing. */ + public static final int DISCARD_REASON_AUDIO_ENCODING_CHANGED = 1 << 14; + + /** The name of the decoder. */ + public final String decoderName; + + /** The {@link Format} for which the decoder was previously configured. */ + public final Format oldFormat; + + /** The new {@link Format} being evaluated. */ + public final Format newFormat; + + /** The {@link DecoderReuseResult result} of the evaluation. */ + @DecoderReuseResult public final int result; + + /** + * {@link DecoderDiscardReasons Reasons} why the decoder cannot be reused. Always {@code 0} if + * reuse is possible. May also be {code 0} if reuse is not possible for an unspecified reason. + */ + @DecoderDiscardReasons public final int discardReasons; + + /** + * @param decoderName The name of the decoder. + * @param oldFormat The {@link Format} for which the decoder was previously configured. + * @param newFormat The new {@link Format} being evaluated. + * @param result The {@link DecoderReuseResult result} of the evaluation. + * @param discardReasons One or more {@link DecoderDiscardReasons reasons} why the decoder cannot + * be reused, or {@code 0} if reuse is possible. + */ + public DecoderReuseEvaluation( + String decoderName, + Format oldFormat, + Format newFormat, + @DecoderReuseResult int result, + @DecoderDiscardReasons int discardReasons) { + checkArgument(result == REUSE_RESULT_NO || discardReasons == 0); + this.decoderName = checkNotEmpty(decoderName); + this.oldFormat = checkNotNull(oldFormat); + this.newFormat = checkNotNull(newFormat); + this.result = result; + this.discardReasons = discardReasons; + } + + @Override + public boolean equals(@Nullable Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DecoderReuseEvaluation other = (DecoderReuseEvaluation) obj; + return result == other.result + && discardReasons == other.discardReasons + && decoderName.equals(other.decoderName) + && oldFormat.equals(other.oldFormat) + && newFormat.equals(other.newFormat); + } + + @Override + public int hashCode() { + int hashCode = 17; + hashCode = 31 * hashCode + result; + hashCode = 31 * hashCode + discardReasons; + hashCode = 31 * hashCode + decoderName.hashCode(); + hashCode = 31 * hashCode + oldFormat.hashCode(); + hashCode = 31 * hashCode + newFormat.hashCode(); + return hashCode; + } +} diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java index cad6cc72f7..1b35fc9887 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfo.java @@ -15,6 +15,20 @@ */ package com.google.android.exoplayer2.mediacodec; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_AUDIO_CHANNEL_COUNT_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_AUDIO_ENCODING_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_AUDIO_SAMPLE_RATE_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_INITIALIZATION_DATA_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MIME_TYPE_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_COLOR_INFO_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_RESOLUTION_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_ROTATION_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_WORKAROUND; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITH_FLUSH; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITH_RECONFIGURATION; + import android.graphics.Point; import android.media.MediaCodec; import android.media.MediaCodecInfo.AudioCapabilities; @@ -22,18 +36,17 @@ import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; import android.util.Pair; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DecoderDiscardReasons; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DecoderReuseResult; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; /** Information about a {@link MediaCodec} for a given mime type. */ @SuppressWarnings("InlinedApi") @@ -47,28 +60,6 @@ public final class MediaCodecInfo { */ public static final int MAX_SUPPORTED_INSTANCES_UNKNOWN = -1; - /** The possible return values for {@link #canKeepCodec(Format, Format)}. */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @IntDef({ - KEEP_CODEC_RESULT_NO, - KEEP_CODEC_RESULT_YES_WITH_FLUSH, - KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION, - KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION - }) - public @interface KeepCodecResult {} - /** The codec cannot be kept. */ - public static final int KEEP_CODEC_RESULT_NO = 0; - /** The codec can be kept, but must be flushed. */ - public static final int KEEP_CODEC_RESULT_YES_WITH_FLUSH = 1; - /** - * The codec can be kept. It does not need to be flushed, but must be reconfigured by prefixing - * the next input buffer with the new format's configuration data. - */ - public static final int KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION = 2; - /** The codec can be kept. It does not need to be flushed and no reconfiguration is required. */ - public static final int KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION = 3; - /** * The name of the decoder. * @@ -361,7 +352,7 @@ public final class MediaCodecInfo { * @param isNewFormatComplete Whether {@code newFormat} is populated with format-specific * metadata. * @return Whether it is possible to adapt the decoder seamlessly. - * @deprecated Use {@link #canKeepCodec}. + * @deprecated Use {@link #canReuseCodec}. */ @Deprecated public boolean isSeamlessAdaptationSupported( @@ -369,49 +360,68 @@ public final class MediaCodecInfo { if (!isNewFormatComplete && oldFormat.colorInfo != null && newFormat.colorInfo == null) { newFormat = newFormat.buildUpon().setColorInfo(oldFormat.colorInfo).build(); } - @KeepCodecResult int keepCodecResult = canKeepCodec(oldFormat, newFormat); - return keepCodecResult == KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION - || keepCodecResult == KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION; + @DecoderReuseResult int reuseResult = canReuseCodec(oldFormat, newFormat).result; + return reuseResult == REUSE_RESULT_YES_WITH_RECONFIGURATION + || reuseResult == REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; } /** - * Returns the extent to which it's possible to adapt an instance of this decoder that's currently - * decoding {@code oldFormat} to decode {@code newFormat} instead. + * Evaluates whether it's possible to reuse an instance of this decoder that's currently decoding + * {@code oldFormat} to decode {@code newFormat} instead. * *

For adaptation to succeed, the codec must also be configured with maximum values that are * compatible with the new format. * * @param oldFormat The format being decoded. * @param newFormat The new format. - * @return The extent to which it's possible to adapt an instance of the decoder. + * @return The result of the evaluation. */ - @KeepCodecResult - public int canKeepCodec(Format oldFormat, Format newFormat) { + public DecoderReuseEvaluation canReuseCodec(Format oldFormat, Format newFormat) { + @DecoderDiscardReasons int discardReasons = 0; if (!Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType)) { - return KEEP_CODEC_RESULT_NO; + discardReasons |= DISCARD_REASON_MIME_TYPE_CHANGED; } if (isVideo) { - if (oldFormat.rotationDegrees == newFormat.rotationDegrees - && (adaptive - || (oldFormat.width == newFormat.width && oldFormat.height == newFormat.height)) - && Util.areEqual(oldFormat.colorInfo, newFormat.colorInfo)) { - if (oldFormat.initializationDataEquals(newFormat)) { - return KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION; - } else if (!needsAdaptationReconfigureWorkaround(name)) { - return KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; - } + if (oldFormat.rotationDegrees != newFormat.rotationDegrees) { + discardReasons |= DISCARD_REASON_VIDEO_ROTATION_CHANGED; + } + if (!adaptive + && (oldFormat.width != newFormat.width || oldFormat.height != newFormat.height)) { + discardReasons |= DISCARD_REASON_VIDEO_RESOLUTION_CHANGED; + } + if (!Util.areEqual(oldFormat.colorInfo, newFormat.colorInfo)) { + discardReasons |= DISCARD_REASON_VIDEO_COLOR_INFO_CHANGED; + } + if (needsAdaptationReconfigureWorkaround(name) + && !oldFormat.initializationDataEquals(newFormat)) { + discardReasons |= DISCARD_REASON_WORKAROUND; + } + + if (discardReasons == 0) { + return new DecoderReuseEvaluation( + name, + oldFormat, + newFormat, + oldFormat.initializationDataEquals(newFormat) + ? REUSE_RESULT_YES_WITHOUT_RECONFIGURATION + : REUSE_RESULT_YES_WITH_RECONFIGURATION, + /* discardReasons= */ 0); } } else { - if (oldFormat.channelCount != newFormat.channelCount - || oldFormat.sampleRate != newFormat.sampleRate - || oldFormat.pcmEncoding != newFormat.pcmEncoding) { - return KEEP_CODEC_RESULT_NO; + if (oldFormat.channelCount != newFormat.channelCount) { + discardReasons |= DISCARD_REASON_AUDIO_CHANNEL_COUNT_CHANGED; + } + if (oldFormat.sampleRate != newFormat.sampleRate) { + discardReasons |= DISCARD_REASON_AUDIO_SAMPLE_RATE_CHANGED; + } + if (oldFormat.pcmEncoding != newFormat.pcmEncoding) { + discardReasons |= DISCARD_REASON_AUDIO_ENCODING_CHANGED; } // Check whether we're adapting between two xHE-AAC formats, for which adaptation is possible // without reconfiguration or flushing. - if (MimeTypes.AUDIO_AAC.equals(mimeType)) { + if (discardReasons == 0 && MimeTypes.AUDIO_AAC.equals(mimeType)) { @Nullable Pair oldCodecProfileLevel = MediaCodecUtil.getCodecProfileAndLevel(oldFormat); @@ -423,18 +433,30 @@ public final class MediaCodecInfo { int newProfile = newCodecProfileLevel.first; if (oldProfile == CodecProfileLevel.AACObjectXHE && newProfile == CodecProfileLevel.AACObjectXHE) { - return KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION; + return new DecoderReuseEvaluation( + name, + oldFormat, + newFormat, + REUSE_RESULT_YES_WITHOUT_RECONFIGURATION, + /* discardReasons= */ 0); } } } - if (oldFormat.initializationDataEquals(newFormat) - && !needsAdaptationFlushWorkaround(mimeType)) { - return KEEP_CODEC_RESULT_YES_WITH_FLUSH; + if (!oldFormat.initializationDataEquals(newFormat)) { + discardReasons |= DISCARD_REASON_INITIALIZATION_DATA_CHANGED; + } + if (needsAdaptationFlushWorkaround(mimeType)) { + discardReasons |= DISCARD_REASON_WORKAROUND; + } + + if (discardReasons == 0) { + return new DecoderReuseEvaluation( + name, oldFormat, newFormat, REUSE_RESULT_YES_WITH_FLUSH, /* discardReasons= */ 0); } } - return KEEP_CODEC_RESULT_NO; + return new DecoderReuseEvaluation(name, oldFormat, newFormat, REUSE_RESULT_NO, discardReasons); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index 00914fe092..c45e17a672 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -15,10 +15,14 @@ */ package com.google.android.exoplayer2.mediacodec; -import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_NO; -import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION; -import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_YES_WITH_FLUSH; -import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_DRM_SESSION_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_OPERATING_RATE_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_REUSE_NOT_IMPLEMENTED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_WORKAROUND; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITH_FLUSH; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITH_RECONFIGURATION; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; import static java.lang.Math.max; @@ -44,11 +48,12 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DecoderDiscardReasons; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaCrypto; import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; -import com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KeepCodecResult; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.SampleStream; @@ -710,7 +715,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (codec != null && codecDrainAction != DRAIN_ACTION_REINITIALIZE && getState() != STATE_DISABLED) { - updateOperatingRateOrReinitializeCodec(codecInputFormat); + updateCodecOperatingRate(codecInputFormat); } } @@ -1375,9 +1380,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param formatHolder A {@link FormatHolder} that holds the new {@link Format}. * @throws ExoPlaybackException If an error occurs re-initializing the {@link MediaCodec}. + * @return The result of the evaluation to determine whether the existing decoder instance can be + * reused for the new format, or {@code null} if the renderer did not have a decoder. */ @CallSuper - protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { + @Nullable + protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) + throws ExoPlaybackException { waitingForFirstSampleInFormat = true; Format newFormat = checkNotNull(formatHolder.format); setSourceDrmSession(formatHolder.drmSession); @@ -1385,7 +1394,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { if (bypassEnabled) { bypassDrainAndReinitialize = true; - return; // Need to drain batch buffer first. + return null; // Need to drain batch buffer first. } if (codec == null) { @@ -1393,66 +1402,86 @@ public abstract class MediaCodecRenderer extends BaseRenderer { availableCodecInfos = null; } maybeInitCodecOrBypass(); - return; + return null; } // We have an existing codec that we may need to reconfigure, re-initialize, or release to // switch to bypass. If the existing codec instance is kept then its operating rate and DRM // session may need to be updated. - + MediaCodecAdapter oldCodec = codec; + Format oldFormat = codecInputFormat; if (drmNeedsCodecReinitialization(codecInfo, newFormat, codecDrmSession, sourceDrmSession)) { drainAndReinitializeCodec(); - return; + return new DecoderReuseEvaluation( + codecInfo.name, + oldFormat, + newFormat, + REUSE_RESULT_NO, + DISCARD_REASON_DRM_SESSION_CHANGED); } boolean drainAndUpdateCodecDrmSession = sourceDrmSession != codecDrmSession; Assertions.checkState(!drainAndUpdateCodecDrmSession || Util.SDK_INT >= 23); - switch (canKeepCodec(codec, codecInfo, codecInputFormat, newFormat)) { - case KEEP_CODEC_RESULT_NO: + DecoderReuseEvaluation evaluation = canReuseCodec(codecInfo, oldFormat, newFormat); + @DecoderDiscardReasons int overridingDiscardReasons = 0; + switch (evaluation.result) { + case REUSE_RESULT_NO: drainAndReinitializeCodec(); break; - case KEEP_CODEC_RESULT_YES_WITH_FLUSH: - if (updateOperatingRateOrReinitializeCodec(newFormat)) { - // Codec re-initialization triggered. + case REUSE_RESULT_YES_WITH_FLUSH: + if (!updateCodecOperatingRate(newFormat)) { + overridingDiscardReasons |= DISCARD_REASON_OPERATING_RATE_CHANGED; } else { codecInputFormat = newFormat; if (drainAndUpdateCodecDrmSession) { - drainAndUpdateCodecDrmSessionV23(); - } else { - drainAndFlushCodec(); + if (!drainAndUpdateCodecDrmSessionV23()) { + overridingDiscardReasons |= DISCARD_REASON_WORKAROUND; + } + } else if (!drainAndFlushCodec()) { + overridingDiscardReasons |= DISCARD_REASON_WORKAROUND; } } break; - case KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION: - if (updateOperatingRateOrReinitializeCodec(newFormat)) { - // Codec re-initialization triggered. + case REUSE_RESULT_YES_WITH_RECONFIGURATION: + if (!updateCodecOperatingRate(newFormat)) { + overridingDiscardReasons |= DISCARD_REASON_OPERATING_RATE_CHANGED; } else { codecReconfigured = true; codecReconfigurationState = RECONFIGURATION_STATE_WRITE_PENDING; codecNeedsAdaptationWorkaroundBuffer = codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_ALWAYS || (codecAdaptationWorkaroundMode == ADAPTATION_WORKAROUND_MODE_SAME_RESOLUTION - && newFormat.width == codecInputFormat.width - && newFormat.height == codecInputFormat.height); + && newFormat.width == oldFormat.width + && newFormat.height == oldFormat.height); codecInputFormat = newFormat; - if (drainAndUpdateCodecDrmSession) { - drainAndUpdateCodecDrmSessionV23(); + if (drainAndUpdateCodecDrmSession && !drainAndUpdateCodecDrmSessionV23()) { + overridingDiscardReasons |= DISCARD_REASON_WORKAROUND; } } break; - case KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION: - if (updateOperatingRateOrReinitializeCodec(newFormat)) { - // Codec re-initialization triggered. + case REUSE_RESULT_YES_WITHOUT_RECONFIGURATION: + if (!updateCodecOperatingRate(newFormat)) { + overridingDiscardReasons |= DISCARD_REASON_OPERATING_RATE_CHANGED; } else { codecInputFormat = newFormat; - if (drainAndUpdateCodecDrmSession) { - drainAndUpdateCodecDrmSessionV23(); + if (drainAndUpdateCodecDrmSession && !drainAndUpdateCodecDrmSessionV23()) { + overridingDiscardReasons |= DISCARD_REASON_WORKAROUND; } } break; default: throw new IllegalStateException(); // Never happens. } + + if (evaluation.result != REUSE_RESULT_NO + && (codec != oldCodec || codecDrainAction == DRAIN_ACTION_REINITIALIZE)) { + // Initial evaluation indicated reuse was possible, but codec re-initialization was triggered. + // The reasons are indicated by overridingDiscardReasons. + return new DecoderReuseEvaluation( + codecInfo.name, oldFormat, newFormat, REUSE_RESULT_NO, overridingDiscardReasons); + } + + return evaluation; } /** @@ -1544,21 +1573,24 @@ public abstract class MediaCodecRenderer extends BaseRenderer { } /** - * Determines whether the existing {@link MediaCodec} can be kept for a new {@link Format}, and if + * Evaluates whether the existing {@link MediaCodec} can be kept for a new {@link Format}, and if * it can whether it requires reconfiguration. * - *

The default implementation returns {@link MediaCodecInfo#KEEP_CODEC_RESULT_NO}. + *

The default implementation does not allow decoder reuse. * - * @param codec The existing {@link MediaCodecAdapter} instance. * @param codecInfo A {@link MediaCodecInfo} describing the decoder. * @param oldFormat The {@link Format} for which the existing instance is configured. * @param newFormat The new {@link Format}. - * @return Whether the instance can be kept, and if it can whether it requires reconfiguration. + * @return The result of the evaluation. */ - @KeepCodecResult - protected int canKeepCodec( - MediaCodecAdapter codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { - return KEEP_CODEC_RESULT_NO; + protected DecoderReuseEvaluation canReuseCodec( + MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + return new DecoderReuseEvaluation( + codecInfo.name, + oldFormat, + newFormat, + REUSE_RESULT_NO, + DISCARD_REASON_REUSE_NOT_IMPLEMENTED); } @Override @@ -1608,25 +1640,23 @@ public abstract class MediaCodecRenderer extends BaseRenderer { * * @param format The {@link Format} for which the operating rate should be configured. * @throws ExoPlaybackException If an error occurs releasing or initializing a codec. - * @return Whether codec release and re-initialization was triggered, rather than the existing - * codec being updated. + * @return False if codec release and re-initialization was triggered. True in all other cases. */ - private boolean updateOperatingRateOrReinitializeCodec(Format format) - throws ExoPlaybackException { + private boolean updateCodecOperatingRate(Format format) throws ExoPlaybackException { if (Util.SDK_INT < 23) { - return false; + return true; } float newCodecOperatingRate = getCodecOperatingRateV23(playbackSpeed, format, getStreamFormats()); if (codecOperatingRate == newCodecOperatingRate) { // No change. - return false; + return true; } else if (newCodecOperatingRate == CODEC_OPERATING_RATE_UNSET) { // The only way to clear the operating rate is to instantiate a new codec instance. See // [Internal ref: b/111543954]. drainAndReinitializeCodec(); - return true; + return false; } else if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET || newCodecOperatingRate > assumedMinimumCodecOperatingRate) { // We need to set the operating rate, either because we've set it previously or because it's @@ -1635,36 +1665,48 @@ public abstract class MediaCodecRenderer extends BaseRenderer { codecParameters.putFloat(MediaFormat.KEY_OPERATING_RATE, newCodecOperatingRate); codec.setParameters(codecParameters); codecOperatingRate = newCodecOperatingRate; - return false; + return true; } - return false; + return true; } - /** Starts draining the codec for flush. */ - private void drainAndFlushCodec() { + /** + * Starts draining the codec for a flush, or to release and re-initialize the codec if flushing + * will not be possible. If no buffers have been queued to the codec then this method is a no-op. + * + * @return False if codec release and re-initialization was triggered due to the need to apply a + * flush workaround. True in all other cases. + */ + private boolean drainAndFlushCodec() { if (codecReceivedBuffers) { codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM; if (codecNeedsFlushWorkaround || codecNeedsEosFlushWorkaround) { codecDrainAction = DRAIN_ACTION_REINITIALIZE; + return false; } else { codecDrainAction = DRAIN_ACTION_FLUSH; } } + return true; } /** - * Starts draining the codec to update its DRM session. The update may occur immediately if no - * buffers have been queued to the codec. + * Starts draining the codec to flush it and update its DRM session, or to release and + * re-initialize the codec if flushing will not be possible. If no buffers have been queued to the + * codec then this method updates the DRM session immediately without flushing the codec. * * @throws ExoPlaybackException If an error occurs updating the codec's DRM session. + * @return False if codec release and re-initialization was triggered due to the need to apply a + * flush workaround. True in all other cases. */ @TargetApi(23) // Only called when SDK_INT >= 23, but lint isn't clever enough to know. - private void drainAndUpdateCodecDrmSessionV23() throws ExoPlaybackException { + private boolean drainAndUpdateCodecDrmSessionV23() throws ExoPlaybackException { if (codecReceivedBuffers) { codecDrainState = DRAIN_STATE_SIGNAL_END_OF_STREAM; if (codecNeedsFlushWorkaround || codecNeedsEosFlushWorkaround) { codecDrainAction = DRAIN_ACTION_REINITIALIZE; + return false; } else { codecDrainAction = DRAIN_ACTION_FLUSH_AND_UPDATE_DRM_SESSION; } @@ -1672,6 +1714,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { // Nothing has been queued to the decoder, so we can do the update immediately. updateDrmSessionV23(); } + return true; } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java index 32fc0be8ed..9225b575e6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/EventLogger.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.analytics.AnalyticsListener; import com.google.android.exoplayer2.audio.AudioAttributes; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.source.LoadEventInfo; import com.google.android.exoplayer2.source.MediaLoadData; @@ -328,7 +329,8 @@ public class EventLogger implements AnalyticsListener { } @Override - public void onAudioInputFormatChanged(EventTime eventTime, Format format) { + public void onAudioInputFormatChanged( + EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { logd(eventTime, "audioInputFormat", Format.toLogString(format)); } @@ -393,7 +395,8 @@ public class EventLogger implements AnalyticsListener { } @Override - public void onVideoInputFormatChanged(EventTime eventTime, Format format) { + public void onVideoInputFormatChanged( + EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { logd(eventTime, "videoInputFormat", Format.toLogString(format)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/DecoderVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/DecoderVideoRenderer.java index c349813bdb..08961b9783 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/DecoderVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/DecoderVideoRenderer.java @@ -15,6 +15,9 @@ */ package com.google.android.exoplayer2.video; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_DRM_SESSION_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_REUSE_NOT_IMPLEMENTED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO; import static java.lang.Math.max; import android.os.Handler; @@ -34,6 +37,7 @@ import com.google.android.exoplayer2.decoder.Decoder; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderException; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSession.DrmSessionException; import com.google.android.exoplayer2.drm.ExoMediaCrypto; @@ -97,9 +101,12 @@ public abstract class DecoderVideoRenderer extends BaseRenderer { private Format inputFormat; private Format outputFormat; + + @Nullable private Decoder< VideoDecoderInputBuffer, ? extends VideoDecoderOutputBuffer, ? extends DecoderException> decoder; + private VideoDecoderInputBuffer inputBuffer; private VideoDecoderOutputBuffer outputBuffer; @Nullable private Surface surface; @@ -366,7 +373,24 @@ public abstract class DecoderVideoRenderer extends BaseRenderer { if (decoder == null) { maybeInitDecoder(); - } else if (sourceDrmSession != decoderDrmSession || !canKeepCodec(oldFormat, inputFormat)) { + eventDispatcher.inputFormatChanged(inputFormat, /* decoderReuseEvaluation= */ null); + return; + } + + DecoderReuseEvaluation evaluation; + if (sourceDrmSession != decoderDrmSession) { + evaluation = + new DecoderReuseEvaluation( + decoder.getName(), + oldFormat, + newFormat, + REUSE_RESULT_NO, + DISCARD_REASON_DRM_SESSION_CHANGED); + } else { + evaluation = canReuseDecoder(decoder.getName(), oldFormat, newFormat); + } + + if (evaluation.result == REUSE_RESULT_NO) { if (decoderReceivedBuffers) { // Signal end of stream and wait for any final output buffers before re-initialization. decoderReinitializationState = REINITIALIZATION_STATE_SIGNAL_END_OF_STREAM; @@ -376,8 +400,7 @@ public abstract class DecoderVideoRenderer extends BaseRenderer { maybeInitDecoder(); } } - - eventDispatcher.inputFormatChanged(inputFormat); + eventDispatcher.inputFormatChanged(inputFormat, evaluation); } /** @@ -626,14 +649,18 @@ public abstract class DecoderVideoRenderer extends BaseRenderer { protected abstract void setDecoderOutputMode(@C.VideoOutputMode int outputMode); /** - * Returns whether the existing decoder can be kept for a new format. + * Evaluates whether the existing decoder can be reused for a new {@link Format}. + * + *

The default implementation does not allow decoder reuse. * * @param oldFormat The previous format. * @param newFormat The new format. - * @return Whether the existing decoder can be kept. + * @return The result of the evaluation. */ - protected boolean canKeepCodec(Format oldFormat, Format newFormat) { - return false; + protected DecoderReuseEvaluation canReuseDecoder( + String decoderName, Format oldFormat, Format newFormat) { + return new DecoderReuseEvaluation( + decoderName, oldFormat, newFormat, REUSE_RESULT_NO, DISCARD_REASON_REUSE_NOT_IMPLEMENTED); } // Internal methods. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java index cebcece5ae..ab9f3af00b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/MediaCodecVideoRenderer.java @@ -15,7 +15,9 @@ */ package com.google.android.exoplayer2.video; -import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_NO; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_MAX_RESOLUTION_EXCEEDED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO; import static java.lang.Math.max; import static java.lang.Math.min; @@ -46,11 +48,12 @@ import com.google.android.exoplayer2.PlayerMessage.Target; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DecoderDiscardReasons; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecDecoderException; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; -import com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KeepCodecResult; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; import com.google.android.exoplayer2.mediacodec.MediaCodecUtil; @@ -566,15 +569,24 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - @KeepCodecResult - protected int canKeepCodec( - MediaCodecAdapter codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { - if (newFormat.width > codecMaxValues.width - || newFormat.height > codecMaxValues.height - || getMaxInputSize(codecInfo, newFormat) > codecMaxValues.inputSize) { - return KEEP_CODEC_RESULT_NO; + protected DecoderReuseEvaluation canReuseCodec( + MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + DecoderReuseEvaluation evaluation = codecInfo.canReuseCodec(oldFormat, newFormat); + + @DecoderDiscardReasons int discardReasons = evaluation.discardReasons; + if (newFormat.width > codecMaxValues.width || newFormat.height > codecMaxValues.height) { + discardReasons |= DISCARD_REASON_VIDEO_MAX_RESOLUTION_EXCEEDED; } - return codecInfo.canKeepCodec(oldFormat, newFormat); + if (getMaxInputSize(codecInfo, newFormat) > codecMaxValues.inputSize) { + discardReasons |= DISCARD_REASON_MAX_INPUT_SIZE_EXCEEDED; + } + + return new DecoderReuseEvaluation( + codecInfo.name, + oldFormat, + newFormat, + discardReasons != 0 ? REUSE_RESULT_NO : evaluation.result, + discardReasons); } @CallSuper @@ -621,9 +633,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } @Override - protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { - super.onInputFormatChanged(formatHolder); - eventDispatcher.inputFormatChanged(formatHolder.format); + @Nullable + protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) + throws ExoPlaybackException { + @Nullable DecoderReuseEvaluation evaluation = super.onInputFormatChanged(formatHolder); + eventDispatcher.inputFormatChanged(formatHolder.format, evaluation); + return evaluation; } /** @@ -1333,7 +1348,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { // from format to avoid codec re-use being ruled out for only this reason. streamFormat = streamFormat.buildUpon().setColorInfo(format.colorInfo).build(); } - if (codecInfo.canKeepCodec(format, streamFormat) != KEEP_CODEC_RESULT_NO) { + if (codecInfo.canReuseCodec(format, streamFormat).result != REUSE_RESULT_NO) { haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); maxWidth = max(maxWidth, streamFormat.width); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index c714f3ca21..aacd8ce8d4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -25,6 +25,7 @@ import androidx.annotation.Nullable; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderCounters; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.util.Assertions; /** @@ -52,12 +53,23 @@ public interface VideoRendererEventListener { default void onVideoDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) {} + /** @deprecated Use {@link #onVideoInputFormatChanged(Format, DecoderReuseEvaluation)}. */ + @Deprecated + default void onVideoInputFormatChanged(Format format) {} + /** * Called when the format of the media being consumed by the renderer changes. * * @param format The new format. + * @param decoderReuseEvaluation The result of the evaluation to determine whether an existing + * decoder instance can be reused for the new format, or {@code null} if the renderer did not + * have a decoder. */ - default void onVideoInputFormatChanged(Format format) {} + @SuppressWarnings("deprecation") + default void onVideoInputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { + onVideoInputFormatChanged(format); + } /** * Called to report the number of frames dropped by the renderer. Dropped frames are reported @@ -172,10 +184,15 @@ public interface VideoRendererEventListener { } } - /** Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format)}. */ - public void inputFormatChanged(Format format) { + /** + * Invokes {@link VideoRendererEventListener#onVideoInputFormatChanged(Format, + * DecoderReuseEvaluation)}. + */ + public void inputFormatChanged( + Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { if (handler != null) { - handler.post(() -> castNonNull(listener).onVideoInputFormatChanged(format)); + handler.post( + () -> castNonNull(listener).onVideoInputFormatChanged(format, decoderReuseEvaluation)); } } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfoTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfoTest.java index efef2b47b3..346d2773a6 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfoTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/MediaCodecInfoTest.java @@ -15,9 +15,15 @@ */ package com.google.android.exoplayer2.mediacodec; -import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_NO; -import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_YES_WITH_FLUSH; -import static com.google.android.exoplayer2.mediacodec.MediaCodecInfo.KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_AUDIO_CHANNEL_COUNT_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_INITIALIZATION_DATA_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_MIME_TYPE_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_COLOR_INFO_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_RESOLUTION_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.DISCARD_REASON_VIDEO_ROTATION_CHANGED; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_NO; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITH_FLUSH; +import static com.google.android.exoplayer2.decoder.DecoderReuseEvaluation.REUSE_RESULT_YES_WITH_RECONFIGURATION; import static com.google.android.exoplayer2.util.MimeTypes.AUDIO_AAC; import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_AV1; import static com.google.android.exoplayer2.util.MimeTypes.VIDEO_H264; @@ -26,6 +32,7 @@ import static com.google.common.truth.Truth.assertThat; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.video.ColorInfo; import com.google.common.collect.ImmutableList; import org.junit.Test; @@ -71,7 +78,14 @@ public final class MediaCodecInfoTest { MediaCodecInfo codecInfo = buildH264CodecInfo(/* adaptive= */ true); Format hdAv1Format = FORMAT_H264_HD.buildUpon().setSampleMimeType(VIDEO_AV1).build(); - assertThat(codecInfo.canKeepCodec(FORMAT_H264_HD, hdAv1Format)).isEqualTo(KEEP_CODEC_RESULT_NO); + assertThat(codecInfo.canReuseCodec(FORMAT_H264_HD, hdAv1Format)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + FORMAT_H264_HD, + hdAv1Format, + REUSE_RESULT_NO, + DISCARD_REASON_MIME_TYPE_CHANGED)); } @Test @@ -79,24 +93,42 @@ public final class MediaCodecInfoTest { MediaCodecInfo codecInfo = buildH264CodecInfo(/* adaptive= */ true); Format hdRotatedFormat = FORMAT_H264_HD.buildUpon().setRotationDegrees(90).build(); - assertThat(codecInfo.canKeepCodec(FORMAT_H264_HD, hdRotatedFormat)) - .isEqualTo(KEEP_CODEC_RESULT_NO); + assertThat(codecInfo.canReuseCodec(FORMAT_H264_HD, hdRotatedFormat)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + FORMAT_H264_HD, + hdRotatedFormat, + REUSE_RESULT_NO, + DISCARD_REASON_VIDEO_ROTATION_CHANGED)); } @Test public void canKeepCodec_withResolutionChange_adaptiveCodec_returnsYesWithReconfiguration() { MediaCodecInfo codecInfo = buildH264CodecInfo(/* adaptive= */ true); - assertThat(codecInfo.canKeepCodec(FORMAT_H264_HD, FORMAT_H264_4K)) - .isEqualTo(KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION); + assertThat(codecInfo.canReuseCodec(FORMAT_H264_HD, FORMAT_H264_4K)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + FORMAT_H264_HD, + FORMAT_H264_4K, + REUSE_RESULT_YES_WITH_RECONFIGURATION, + /* discardReasons= */ 0)); } @Test public void canKeepCodec_withResolutionChange_nonAdaptiveCodec_returnsNo() { MediaCodecInfo codecInfo = buildH264CodecInfo(/* adaptive= */ false); - assertThat(codecInfo.canKeepCodec(FORMAT_H264_HD, FORMAT_H264_4K)) - .isEqualTo(KEEP_CODEC_RESULT_NO); + assertThat(codecInfo.canReuseCodec(FORMAT_H264_HD, FORMAT_H264_4K)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + FORMAT_H264_HD, + FORMAT_H264_4K, + REUSE_RESULT_NO, + DISCARD_REASON_VIDEO_RESOLUTION_CHANGED)); } @Test @@ -105,8 +137,14 @@ public final class MediaCodecInfoTest { Format hdVariantFormat = FORMAT_H264_HD.buildUpon().setInitializationData(ImmutableList.of(new byte[] {0})).build(); - assertThat(codecInfo.canKeepCodec(FORMAT_H264_HD, hdVariantFormat)) - .isEqualTo(KEEP_CODEC_RESULT_YES_WITH_RECONFIGURATION); + assertThat(codecInfo.canReuseCodec(FORMAT_H264_HD, hdVariantFormat)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + FORMAT_H264_HD, + hdVariantFormat, + REUSE_RESULT_YES_WITH_RECONFIGURATION, + /* discardReasons= */ 0)); } @Test @@ -115,8 +153,14 @@ public final class MediaCodecInfoTest { Format hdrVariantFormat = FORMAT_H264_4K.buildUpon().setColorInfo(buildColorInfo(C.COLOR_SPACE_BT601)).build(); - assertThat(codecInfo.canKeepCodec(hdrVariantFormat, FORMAT_H264_4K)) - .isEqualTo(KEEP_CODEC_RESULT_NO); + assertThat(codecInfo.canReuseCodec(hdrVariantFormat, FORMAT_H264_4K)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + hdrVariantFormat, + FORMAT_H264_4K, + REUSE_RESULT_NO, + DISCARD_REASON_VIDEO_COLOR_INFO_CHANGED)); } @Test @@ -125,8 +169,14 @@ public final class MediaCodecInfoTest { Format hdrVariantFormat = FORMAT_H264_4K.buildUpon().setColorInfo(buildColorInfo(C.COLOR_SPACE_BT601)).build(); - assertThat(codecInfo.canKeepCodec(FORMAT_H264_4K, hdrVariantFormat)) - .isEqualTo(KEEP_CODEC_RESULT_NO); + assertThat(codecInfo.canReuseCodec(FORMAT_H264_4K, hdrVariantFormat)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + FORMAT_H264_4K, + hdrVariantFormat, + REUSE_RESULT_NO, + DISCARD_REASON_VIDEO_COLOR_INFO_CHANGED)); } @Test @@ -137,18 +187,28 @@ public final class MediaCodecInfoTest { FORMAT_H264_4K.buildUpon().setColorInfo(buildColorInfo(C.COLOR_SPACE_BT601)).build(); Format hdrVariantFormat2 = FORMAT_H264_4K.buildUpon().setColorInfo(buildColorInfo(C.COLOR_SPACE_BT709)).build(); - assertThat(codecInfo.canKeepCodec(hdrVariantFormat1, hdrVariantFormat2)) - .isEqualTo(KEEP_CODEC_RESULT_NO); - assertThat(codecInfo.canKeepCodec(hdrVariantFormat1, hdrVariantFormat2)) - .isEqualTo(KEEP_CODEC_RESULT_NO); + assertThat(codecInfo.canReuseCodec(hdrVariantFormat1, hdrVariantFormat2)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + hdrVariantFormat1, + hdrVariantFormat2, + REUSE_RESULT_NO, + DISCARD_REASON_VIDEO_COLOR_INFO_CHANGED)); } @Test public void canKeepCodec_audioWithDifferentChannelCounts_returnsNo() { MediaCodecInfo codecInfo = buildAacCodecInfo(); - assertThat(codecInfo.canKeepCodec(FORMAT_AAC_STEREO, FORMAT_AAC_SURROUND)) - .isEqualTo(KEEP_CODEC_RESULT_NO); + assertThat(codecInfo.canReuseCodec(FORMAT_AAC_STEREO, FORMAT_AAC_SURROUND)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + FORMAT_AAC_STEREO, + FORMAT_AAC_SURROUND, + REUSE_RESULT_NO, + DISCARD_REASON_AUDIO_CHANNEL_COUNT_CHANGED)); } @Test @@ -156,8 +216,14 @@ public final class MediaCodecInfoTest { MediaCodecInfo codecInfo = buildAacCodecInfo(); Format stereoVariantFormat = FORMAT_AAC_STEREO.buildUpon().setAverageBitrate(100).build(); - assertThat(codecInfo.canKeepCodec(FORMAT_AAC_STEREO, stereoVariantFormat)) - .isEqualTo(KEEP_CODEC_RESULT_YES_WITH_FLUSH); + assertThat(codecInfo.canReuseCodec(FORMAT_AAC_STEREO, stereoVariantFormat)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + FORMAT_AAC_STEREO, + stereoVariantFormat, + REUSE_RESULT_YES_WITH_FLUSH, + /* discardReasons= */ 0)); } @Test @@ -169,8 +235,14 @@ public final class MediaCodecInfoTest { .buildUpon() .setInitializationData(ImmutableList.of(new byte[] {0})) .build(); - assertThat(codecInfo.canKeepCodec(FORMAT_AAC_STEREO, stereoVariantFormat)) - .isEqualTo(KEEP_CODEC_RESULT_NO); + assertThat(codecInfo.canReuseCodec(FORMAT_AAC_STEREO, stereoVariantFormat)) + .isEqualTo( + new DecoderReuseEvaluation( + codecInfo.name, + FORMAT_AAC_STEREO, + stereoVariantFormat, + REUSE_RESULT_NO, + DISCARD_REASON_INITIALIZATION_DATA_CHANGED)); } @Test diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java index 0853981443..9452550740 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DebugRenderersFactory.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.FormatHolder; import com.google.android.exoplayer2.Renderer; import com.google.android.exoplayer2.decoder.DecoderInputBuffer; +import com.google.android.exoplayer2.decoder.DecoderReuseEvaluation; import com.google.android.exoplayer2.mediacodec.MediaCodecAdapter; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; @@ -165,12 +166,15 @@ import java.util.ArrayList; } @Override - protected void onInputFormatChanged(FormatHolder formatHolder) throws ExoPlaybackException { - super.onInputFormatChanged(formatHolder); + @Nullable + protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) + throws ExoPlaybackException { + @Nullable DecoderReuseEvaluation evaluation = super.onInputFormatChanged(formatHolder); // Ensure timestamps of buffers queued after this format change are never inserted into the // queue of expected output timestamps before those of buffers that have already been queued. minimumInsertIndex = startIndex + queueSize; inputFormatChanged = true; + return evaluation; } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java index 1e2f6159a5..f26d414db4 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeAudioRenderer.java @@ -55,7 +55,7 @@ public class FakeAudioRenderer extends FakeRenderer { @Override protected void onFormatChanged(Format format) { - eventDispatcher.inputFormatChanged(format); + eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null); eventDispatcher.decoderInitialized( /* decoderName= */ "fake.audio.decoder", /* initializedTimestampMs= */ SystemClock.elapsedRealtime(), diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeVideoRenderer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeVideoRenderer.java index c1388b209d..f6c1bacea0 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeVideoRenderer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeVideoRenderer.java @@ -85,7 +85,7 @@ public class FakeVideoRenderer extends FakeRenderer { @Override protected void onFormatChanged(Format format) { - eventDispatcher.inputFormatChanged(format); + eventDispatcher.inputFormatChanged(format, /* decoderReuseEvaluation= */ null); eventDispatcher.decoderInitialized( /* decoderName= */ "fake.video.decoder", /* initializedTimestampMs= */ SystemClock.elapsedRealtime(),