From f7c5e475a7c8db3699f21d121337ccd7f14904b8 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 23 Apr 2018 11:04:39 -0700 Subject: [PATCH] Prevent native crash in raw decoder Playback will still fail if an input sample is larger than 32K, but will now fail gracefully. Issue: #4057 ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=193951955 --- .../exoplayer2/DefaultRenderersFactory.java | 1 + .../audio/MediaCodecAudioRenderer.java | 122 ++++++++++++++---- .../exoplayer2/mediacodec/MediaCodecInfo.java | 16 ++- .../mediacodec/MediaCodecRenderer.java | 11 +- .../video/MediaCodecVideoRenderer.java | 93 ++++++------- 5 files changed, 166 insertions(+), 77 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index a3ef67cc26..6cab53b78a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -268,6 +268,7 @@ public class DefaultRenderersFactory implements RenderersFactory { ArrayList out) { out.add( new MediaCodecAudioRenderer( + context, MediaCodecSelector.DEFAULT, drmSessionManager, /* playClearSamplesWithoutKeys= */ false, 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 602ba8836d..79b2311c88 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 @@ -17,6 +17,8 @@ package com.google.android.exoplayer2.audio; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.Context; +import android.content.pm.PackageManager; import android.media.MediaCodec; import android.media.MediaCrypto; import android.media.MediaFormat; @@ -61,6 +63,7 @@ import java.nio.ByteBuffer; @TargetApi(16) public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { + private final Context context; private final EventDispatcher eventDispatcher; private final AudioSink audioSink; @@ -78,16 +81,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private boolean allowPositionDiscontinuity; /** + * @param context A context. * @param mediaCodecSelector A decoder selector. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector) { + public MediaCodecAudioRenderer(Context context, MediaCodecSelector mediaCodecSelector) { this( + context, mediaCodecSelector, /* drmSessionManager= */ null, /* playClearSamplesWithoutKeys= */ false); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -97,10 +103,13 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * permitted to play clear regions of encrypted media files before {@code drmSessionManager} * has obtained the keys necessary to decrypt encrypted regions of the media. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, boolean playClearSamplesWithoutKeys) { this( + context, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, @@ -109,14 +118,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, - @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener) { + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener) { this( + context, mediaCodecSelector, /* drmSessionManager= */ null, /* playClearSamplesWithoutKeys= */ false, @@ -125,6 +139,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -137,15 +152,25 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * null if delivery of events is not required. * @param eventListener A listener of events. May be null if delivery of events is not required. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, eventHandler, - eventListener, (AudioCapabilities) null); + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + (AudioCapabilities) null); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -162,16 +187,27 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param audioProcessors Optional {@link AudioProcessor}s that will process PCM audio before * output. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, @Nullable AudioRendererEventListener eventListener, - @Nullable AudioCapabilities audioCapabilities, AudioProcessor... audioProcessors) { - this(mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys, - eventHandler, eventListener, new DefaultAudioSink(audioCapabilities, audioProcessors)); + @Nullable AudioCapabilities audioCapabilities, + AudioProcessor... audioProcessors) { + this( + context, + mediaCodecSelector, + drmSessionManager, + playClearSamplesWithoutKeys, + eventHandler, + eventListener, + new DefaultAudioSink(audioCapabilities, audioProcessors)); } /** + * @param context A context. * @param mediaCodecSelector A decoder selector. * @param drmSessionManager For use with encrypted content. May be null if support for encrypted * content is not required. @@ -185,13 +221,18 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * @param eventListener A listener of events. May be null if delivery of events is not required. * @param audioSink The sink to which audio will be output. */ - public MediaCodecAudioRenderer(MediaCodecSelector mediaCodecSelector, + public MediaCodecAudioRenderer( + Context context, + MediaCodecSelector mediaCodecSelector, @Nullable DrmSessionManager drmSessionManager, - boolean playClearSamplesWithoutKeys, @Nullable Handler eventHandler, - @Nullable AudioRendererEventListener eventListener, AudioSink audioSink) { + boolean playClearSamplesWithoutKeys, + @Nullable Handler eventHandler, + @Nullable AudioRendererEventListener eventListener, + AudioSink audioSink) { super(C.TRACK_TYPE_AUDIO, mediaCodecSelector, drmSessionManager, playClearSamplesWithoutKeys); - eventDispatcher = new EventDispatcher(eventHandler, eventListener); + this.context = context.getApplicationContext(); this.audioSink = audioSink; + eventDispatcher = new EventDispatcher(eventHandler, eventListener); audioSink.setListener(new AudioSinkListener()); } @@ -246,11 +287,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media if (allowPassthrough(format.sampleMimeType)) { MediaCodecInfo passthroughDecoderInfo = mediaCodecSelector.getPassthroughDecoderInfo(); if (passthroughDecoderInfo != null) { - passthroughEnabled = true; return passthroughDecoderInfo; } } - passthroughEnabled = false; return super.getDecoderInfo(mediaCodecSelector, format, requiresSecureDecoder); } @@ -270,8 +309,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void configureCodec(MediaCodecInfo codecInfo, MediaCodec codec, Format format, MediaCrypto crypto) { - codecMaxInputSize = getCodecMaxInputSize(format, getStreamFormats()); + codecMaxInputSize = getCodecMaxInputSize(codecInfo, format, getStreamFormats()); codecNeedsDiscardChannelsWorkaround = codecNeedsDiscardChannelsWorkaround(codecInfo.name); + passthroughEnabled = codecInfo.passthrough; String codecMimeType = codecInfo.mimeType == null ? MimeTypes.AUDIO_RAW : codecInfo.mimeType; MediaFormat mediaFormat = getMediaFormat(format, codecMimeType, codecMaxInputSize); codec.configure(mediaFormat, /* surface= */ null, crypto, /* flags= */ 0); @@ -286,8 +326,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected @KeepCodecResult int canKeepCodec( - MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) { - return newFormat.maxInputSize <= codecMaxInputSize + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + return getCodecMaxInputSize(codecInfo, newFormat) <= codecMaxInputSize && areAdaptationCompatible(oldFormat, newFormat) ? KEEP_CODEC_RESULT_YES_WITHOUT_RECONFIGURATION : KEEP_CODEC_RESULT_NO; @@ -523,12 +563,14 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media * Returns a maximum input size suitable for configuring a codec for {@code format} in a way that * will allow possible adaptation to other compatible formats in {@code streamFormats}. * + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. * @param format The format for which the codec is being configured. * @param streamFormats The possible stream formats. * @return A suitable maximum input size. */ - protected int getCodecMaxInputSize(Format format, Format[] streamFormats) { - int maxInputSize = format.maxInputSize; + protected int getCodecMaxInputSize( + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) { + int maxInputSize = getCodecMaxInputSize(codecInfo, format); if (streamFormats.length == 1) { // The single entry in streamFormats must correspond to the format for which the codec is // being configured. @@ -536,12 +578,42 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } for (Format streamFormat : streamFormats) { if (areAdaptationCompatible(format, streamFormat)) { - maxInputSize = Math.max(maxInputSize, streamFormat.maxInputSize); + maxInputSize = Math.max(maxInputSize, getCodecMaxInputSize(codecInfo, streamFormat)); } } return maxInputSize; } + /** + * Returns a maximum input buffer size for a given format. + * + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. + * @param format The format. + * @return A maximum input buffer size in bytes, or {@link Format#NO_VALUE} if a maximum could not + * be determined. + */ + private int getCodecMaxInputSize(MediaCodecInfo codecInfo, Format format) { + if (Util.SDK_INT < 24 && "OMX.google.raw.decoder".equals(codecInfo.name)) { + // OMX.google.raw.decoder didn't resize its output buffers correctly prior to N, so there's no + // point requesting a non-default input size. Doing so may cause a native crash, where-as not + // doing so will cause a more controlled failure when attempting to fill an input buffer. See: + // https://github.com/google/ExoPlayer/issues/4057. + boolean needsRawDecoderWorkaround = true; + if (Util.SDK_INT == 23) { + PackageManager packageManager = context.getPackageManager(); + if (packageManager != null + && packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)) { + // The workaround is not required for AndroidTV devices running M. + needsRawDecoderWorkaround = false; + } + } + if (needsRawDecoderWorkaround) { + return Format.NO_VALUE; + } + } + return format.maxInputSize; + } + /** * Returns the framework {@link MediaFormat} that can be used to configure a {@link MediaCodec} * for decoding the given {@link Format} for playback. 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 852c34ba5e..d822916bce 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 @@ -85,6 +85,9 @@ public final class MediaCodecInfo { */ public final boolean secure; + /** Whether this instance describes a passthrough codec. */ + public final boolean passthrough; + /** * Creates an instance representing an audio passthrough decoder. * @@ -96,6 +99,7 @@ public final class MediaCodecInfo { name, /* mimeType= */ null, /* capabilities= */ null, + /* passthrough= */ true, /* forceDisableAdaptive= */ false, /* forceSecure= */ false); } @@ -111,7 +115,12 @@ public final class MediaCodecInfo { public static MediaCodecInfo newInstance(String name, String mimeType, CodecCapabilities capabilities) { return new MediaCodecInfo( - name, mimeType, capabilities, /* forceDisableAdaptive= */ false, /* forceSecure= */ false); + name, + mimeType, + capabilities, + /* passthrough= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); } /** @@ -130,18 +139,21 @@ public final class MediaCodecInfo { CodecCapabilities capabilities, boolean forceDisableAdaptive, boolean forceSecure) { - return new MediaCodecInfo(name, mimeType, capabilities, forceDisableAdaptive, forceSecure); + return new MediaCodecInfo( + name, mimeType, capabilities, /* passthrough= */ false, forceDisableAdaptive, forceSecure); } private MediaCodecInfo( String name, @Nullable String mimeType, @Nullable CodecCapabilities capabilities, + boolean passthrough, boolean forceDisableAdaptive, boolean forceSecure) { this.name = Assertions.checkNotNull(name); this.mimeType = mimeType; this.capabilities = capabilities; + this.passthrough = passthrough; adaptive = !forceDisableAdaptive && capabilities != null && isAdaptive(capabilities); tunneling = capabilities != null && isTunneling(capabilities); secure = forceSecure || (capabilities != null && isSecure(capabilities)); 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 2f95d15f12..03a0b66661 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 @@ -129,7 +129,10 @@ public abstract class MediaCodecRenderer extends BaseRenderer { */ private static final long MAX_CODEC_HOTSWAP_TIME_MS = 1000; - /** The possible return values for {@link #canKeepCodec(MediaCodec, boolean, Format, Format)}. */ + /** + * The possible return values for {@link #canKeepCodec(MediaCodec, MediaCodecInfo, Format, + * Format)}. + */ @Retention(RetentionPolicy.SOURCE) @IntDef({ KEEP_CODEC_RESULT_NO, @@ -885,7 +888,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { boolean keepingCodec = false; if (pendingDrmSession == drmSession && codec != null) { - switch (canKeepCodec(codec, codecInfo.adaptive, oldFormat, format)) { + switch (canKeepCodec(codec, codecInfo, oldFormat, format)) { case KEEP_CODEC_RESULT_NO: // Do nothing. break; @@ -962,13 +965,13 @@ public abstract class MediaCodecRenderer extends BaseRenderer { *

The default implementation returns {@link #KEEP_CODEC_RESULT_NO}. * * @param codec The existing {@link MediaCodec} instance. - * @param codecIsAdaptive Whether the codec is adaptive. + * @param codecInfo A {@link MediaCodecInfo} describing the decoder. * @param oldFormat The format for which the existing instance is configured. * @param newFormat The new format. * @return Whether the instance can be kept, and if it can whether it requires reconfiguration. */ protected @KeepCodecResult int canKeepCodec( - MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) { + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { return KEEP_CODEC_RESULT_NO; } 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 8a8aef9058..34a3eb7284 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 @@ -455,8 +455,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Override protected @KeepCodecResult int canKeepCodec( - MediaCodec codec, boolean codecIsAdaptive, Format oldFormat, Format newFormat) { - if (areAdaptationCompatible(codecIsAdaptive, oldFormat, newFormat) + MediaCodec codec, MediaCodecInfo codecInfo, Format oldFormat, Format newFormat) { + if (areAdaptationCompatible(codecInfo.adaptive, oldFormat, newFormat) && newFormat.width <= codecMaxValues.width && newFormat.height <= codecMaxValues.height && getMaxInputSize(newFormat) <= codecMaxValues.inputSize) { @@ -921,50 +921,6 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { mediaFormat.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, tunnelingAudioSessionId); } - /** - * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way - * that will allow possible adaptation to other compatible formats in {@code streamFormats}. - * - * @param codecInfo Information about the {@link MediaCodec} being configured. - * @param format The format for which the codec is being configured. - * @param streamFormats The possible stream formats. - * @return Suitable {@link CodecMaxValues}. - * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. - */ - protected CodecMaxValues getCodecMaxValues(MediaCodecInfo codecInfo, Format format, - Format[] streamFormats) throws DecoderQueryException { - int maxWidth = format.width; - int maxHeight = format.height; - int maxInputSize = getMaxInputSize(format); - if (streamFormats.length == 1) { - // The single entry in streamFormats must correspond to the format for which the codec is - // being configured. - return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); - } - boolean haveUnknownDimensions = false; - for (Format streamFormat : streamFormats) { - if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) { - haveUnknownDimensions |= (streamFormat.width == Format.NO_VALUE - || streamFormat.height == Format.NO_VALUE); - maxWidth = Math.max(maxWidth, streamFormat.width); - maxHeight = Math.max(maxHeight, streamFormat.height); - maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); - } - } - if (haveUnknownDimensions) { - Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight); - Point codecMaxSize = getCodecMaxSize(codecInfo, format); - if (codecMaxSize != null) { - maxWidth = Math.max(maxWidth, codecMaxSize.x); - maxHeight = Math.max(maxHeight, codecMaxSize.y); - maxInputSize = Math.max(maxInputSize, - getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); - Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); - } - } - return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); - } - /** * Returns the framework {@link MediaFormat} that should be used to configure the decoder. * @@ -1010,6 +966,51 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return mediaFormat; } + /** + * Returns {@link CodecMaxValues} suitable for configuring a codec for {@code format} in a way + * that will allow possible adaptation to other compatible formats in {@code streamFormats}. + * + * @param codecInfo Information about the {@link MediaCodec} being configured. + * @param format The format for which the codec is being configured. + * @param streamFormats The possible stream formats. + * @return Suitable {@link CodecMaxValues}. + * @throws DecoderQueryException If an error occurs querying {@code codecInfo}. + */ + protected CodecMaxValues getCodecMaxValues( + MediaCodecInfo codecInfo, Format format, Format[] streamFormats) + throws DecoderQueryException { + int maxWidth = format.width; + int maxHeight = format.height; + int maxInputSize = getMaxInputSize(format); + if (streamFormats.length == 1) { + // The single entry in streamFormats must correspond to the format for which the codec is + // being configured. + return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); + } + boolean haveUnknownDimensions = false; + for (Format streamFormat : streamFormats) { + if (areAdaptationCompatible(codecInfo.adaptive, format, streamFormat)) { + haveUnknownDimensions |= + (streamFormat.width == Format.NO_VALUE || streamFormat.height == Format.NO_VALUE); + maxWidth = Math.max(maxWidth, streamFormat.width); + maxHeight = Math.max(maxHeight, streamFormat.height); + maxInputSize = Math.max(maxInputSize, getMaxInputSize(streamFormat)); + } + } + if (haveUnknownDimensions) { + Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight); + Point codecMaxSize = getCodecMaxSize(codecInfo, format); + if (codecMaxSize != null) { + maxWidth = Math.max(maxWidth, codecMaxSize.x); + maxHeight = Math.max(maxHeight, codecMaxSize.y); + maxInputSize = + Math.max(maxInputSize, getMaxInputSize(format.sampleMimeType, maxWidth, maxHeight)); + Log.w(TAG, "Codec max resolution adjusted to: " + maxWidth + "x" + maxHeight); + } + } + return new CodecMaxValues(maxWidth, maxHeight, maxInputSize); + } + /** * Returns a maximum video size to use when configuring a codec for {@code format} in a way * that will allow possible adaptation to other compatible formats that are expected to have the