From 0515104b5f163ef1fb0dc44fa9942de44018cf70 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Fri, 23 Feb 2024 08:45:28 -0800 Subject: [PATCH] Fallback to legacy sizerate check if CDD H264 PerfPoint check fails Some devices supporting Performance Points for decoder coverage are missing coverage over the CDD requirements for H264. For these cases ExoPlayer should fall back to legacy resolution and frame rate support checks. If there is a stream evaluated as a PerformancePointCoverageResult of COVERAGE_RESULT_NO, then ExoPlayer checks for coverage of the 720p H264 CDD requirement. Issue: google/ExoPlayer#10898 Issue: androidx/media#693 Issue: androidx/media#966 PiperOrigin-RevId: 609740128 --- .../exoplayer2/mediacodec/MediaCodecInfo.java | 93 +-------- ...CodecPerformancePointCoverageProvider.java | 189 ++++++++++++++++++ 2 files changed, 194 insertions(+), 88 deletions(-) create mode 100644 library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecPerformancePointCoverageProvider.java 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 3f8a86f936e..b27c6ed1ba1 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 @@ -28,7 +28,8 @@ 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 java.lang.annotation.ElementType.TYPE_USE; +import static com.google.android.exoplayer2.mediacodec.MediaCodecPerformancePointCoverageProvider.COVERAGE_RESULT_NO; +import static com.google.android.exoplayer2.mediacodec.MediaCodecPerformancePointCoverageProvider.COVERAGE_RESULT_YES; import android.graphics.Point; import android.media.MediaCodec; @@ -36,10 +37,7 @@ import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.VideoCapabilities; -import android.media.MediaCodecInfo.VideoCapabilities.PerformancePoint; import android.util.Pair; -import androidx.annotation.DoNotInline; -import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; @@ -51,11 +49,6 @@ 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; -import java.lang.annotation.Target; -import java.util.List; /** * Information about a {@link MediaCodec} for a given MIME type. @@ -526,10 +519,10 @@ public boolean isVideoSizeAndRateSupportedV21(int width, int height, double fram } if (Util.SDK_INT >= 29) { - @PerformancePointCoverageResult + @MediaCodecPerformancePointCoverageProvider.PerformancePointCoverageResult int evaluation = - Api29.areResolutionAndFrameRateCovered( - videoCapabilities, mimeType, width, height, frameRate); + MediaCodecPerformancePointCoverageProvider.areResolutionAndFrameRateCovered( + videoCapabilities, width, height, frameRate); if (evaluation == COVERAGE_RESULT_YES) { return true; } else if (evaluation == COVERAGE_RESULT_NO) { @@ -878,80 +871,4 @@ private static boolean needsProfileExcludedWorkaround(String mimeType, int profi && CodecProfileLevel.HEVCProfileMain10 == profile && ("sailfish".equals(Util.DEVICE) || "marlin".equals(Util.DEVICE)); } - - /** Possible outcomes of evaluating PerformancePoint coverage */ - @Documented - @Retention(RetentionPolicy.SOURCE) - @Target(TYPE_USE) - @IntDef({ - COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED, - COVERAGE_RESULT_NO, - COVERAGE_RESULT_YES - }) - private @interface PerformancePointCoverageResult {} - - /** - * The VideoCapabilities does not contain any PerformancePoints or its PerformancePoints do not - * cover CDD requirements. - */ - private static final int COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED = 0; - - /** - * The decoder has at least one PerformancePoint, but none cover the resolution and frame rate. - */ - private static final int COVERAGE_RESULT_NO = 1; - - /** The decoder has a PerformancePoint that covers the resolution and frame rate. */ - private static final int COVERAGE_RESULT_YES = 2; - - @RequiresApi(29) - private static final class Api29 { - @DoNotInline - public static @PerformancePointCoverageResult int areResolutionAndFrameRateCovered( - VideoCapabilities videoCapabilities, - String mimeType, - int width, - int height, - double frameRate) { - List performancePointList = - videoCapabilities.getSupportedPerformancePoints(); - if (performancePointList == null || performancePointList.isEmpty()) { - return COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED; - } - - // Round frame rate down to to avoid situations where a range check in - // covers fails due to slightly exceeding the limits for a standard format - // (e.g., 1080p at 30 fps). [Internal ref: b/134706676] - PerformancePoint targetPerformancePoint = - new PerformancePoint(width, height, (int) frameRate); - - @PerformancePointCoverageResult - int performancePointCoverageResult = - evaluatePerformancePointCoverage(performancePointList, targetPerformancePoint); - - if (performancePointCoverageResult == COVERAGE_RESULT_NO - && mimeType.equals(MimeTypes.VIDEO_H264)) { - if (evaluatePerformancePointCoverage( - performancePointList, - new PerformancePoint(/* width= */ 1280, /* height= */ 720, /* frameRate= */ 60)) - != COVERAGE_RESULT_YES) { - // See https://github.com/google/ExoPlayer/issues/10898, - // https://github.com/androidx/media/issues/693 and [internal ref: b/267324685]. - return COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED; - } - } - - return performancePointCoverageResult; - } - - private static @PerformancePointCoverageResult int evaluatePerformancePointCoverage( - List performancePointList, PerformancePoint targetPerformancePoint) { - for (int i = 0; i < performancePointList.size(); i++) { - if (performancePointList.get(i).covers(targetPerformancePoint)) { - return COVERAGE_RESULT_YES; - } - } - return COVERAGE_RESULT_NO; - } - } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecPerformancePointCoverageProvider.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecPerformancePointCoverageProvider.java new file mode 100644 index 00000000000..41ca6ccf4dd --- /dev/null +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecPerformancePointCoverageProvider.java @@ -0,0 +1,189 @@ +/* + * Copyright 2024 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.mediacodec; + +import static java.lang.annotation.ElementType.TYPE_USE; + +import android.media.MediaCodecInfo.VideoCapabilities; +import android.media.MediaCodecInfo.VideoCapabilities.PerformancePoint; +import androidx.annotation.DoNotInline; +import androidx.annotation.IntDef; +import androidx.annotation.RequiresApi; +import com.google.android.exoplayer2.Format; +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; +import java.lang.annotation.Target; +import java.util.List; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Utility class checking media codec support through PerformancePoints. + * + * @deprecated com.google.android.exoplayer2 is deprecated. Please migrate to androidx.media3 (which + * contains the same ExoPlayer code). See the + * migration guide for more details, including a script to help with the migration. + */ +@Deprecated +/* package */ final class MediaCodecPerformancePointCoverageProvider { + + /** + * Whether if the device provides a PerformancePoints and coverage results should be ignored as + * the PerformancePoints do not cover CDD requirements. + */ + @SuppressWarnings("NonFinalStaticField") + private static @MonotonicNonNull Boolean shouldIgnorePerformancePoints; + + private MediaCodecPerformancePointCoverageProvider() {} + + /** Possible outcomes of evaluating {@link PerformancePoint} coverage. */ + @Documented + @Retention(RetentionPolicy.SOURCE) + @Target(TYPE_USE) + @IntDef({ + COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED, + COVERAGE_RESULT_NO, + COVERAGE_RESULT_YES + }) + @interface PerformancePointCoverageResult {} + + /** + * The {@link VideoCapabilities} do not contain any valid {@linkplain PerformancePoint + * PerformancePoints}. + */ + /* package */ static final int COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED = 0; + + /** + * The decoder has at least one PerformancePoint, but none cover the resolution and frame rate. + */ + /* package */ static final int COVERAGE_RESULT_NO = 1; + + /** The decoder has a PerformancePoint that covers the resolution and frame rate. */ + /* package */ static final int COVERAGE_RESULT_YES = 2; + + /** + * This method returns if a decoder's {@link VideoCapabilities} cover a resolution and frame rate + * with its {@link PerformancePoint} list. + * + * @param videoCapabilities A decoder's {@link VideoCapabilities} + * @param width Width in pixels. + * @param height Height in pixels. + * @param frameRate Optional frame rate in frames per second. Ignored if set to {@link + * Format#NO_VALUE} or any value less than or equal to 0. + * @return {@link #COVERAGE_RESULT_YES} if the {@link VideoCapabilities} has a {@link + * PerformancePoint} list that covers the resolution and frame rate or {@link + * #COVERAGE_RESULT_NO} if the list does not provide coverage. {@link + * #COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED} is returned if the {@link + * VideoCapabilities} does not contain a list of valid {@code PerformancePoints} + */ + public static @PerformancePointCoverageResult int areResolutionAndFrameRateCovered( + VideoCapabilities videoCapabilities, int width, int height, double frameRate) { + if (Util.SDK_INT < 29 + || (shouldIgnorePerformancePoints != null && shouldIgnorePerformancePoints)) { + return COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED; + } + + return Api29.areResolutionAndFrameRateCovered(videoCapabilities, width, height, frameRate); + } + + @RequiresApi(29) + private static final class Api29 { + @DoNotInline + public static @PerformancePointCoverageResult int areResolutionAndFrameRateCovered( + VideoCapabilities videoCapabilities, int width, int height, double frameRate) { + List performancePointList = + videoCapabilities.getSupportedPerformancePoints(); + if (performancePointList == null || performancePointList.isEmpty()) { + return COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED; + } + + // Round frame rate down to to avoid situations where a range check in + // covers fails due to slightly exceeding the limits for a standard format + // (e.g., 1080p at 30 fps). [Internal ref: b/134706676] + PerformancePoint targetPerformancePoint = + new PerformancePoint(width, height, (int) frameRate); + + @PerformancePointCoverageResult + int performancePointCoverageResult = + evaluatePerformancePointCoverage(performancePointList, targetPerformancePoint); + + if (performancePointCoverageResult == COVERAGE_RESULT_NO + && shouldIgnorePerformancePoints == null) { + // See https://github.com/google/ExoPlayer/issues/10898, + // https://github.com/androidx/media/issues/693, + // https://github.com/androidx/media/issues/966 and [internal ref: b/267324685]. + shouldIgnorePerformancePoints = shouldIgnorePerformancePoints(); + if (shouldIgnorePerformancePoints) { + return COVERAGE_RESULT_NO_PERFORMANCE_POINTS_UNSUPPORTED; + } + } + + return performancePointCoverageResult; + } + + /** + * Checks if the CDD-requirement to support H264 720p at 60 fps is covered by PerformancePoints. + */ + private static boolean shouldIgnorePerformancePoints() { + try { + Format formatH264 = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build(); + // Null check required to pass RequiresNonNull annotation on getDecoderInfosSoftMatch. + if (formatH264.sampleMimeType != null) { + List decoderInfos = + MediaCodecUtil.getDecoderInfosSoftMatch( + MediaCodecSelector.DEFAULT, + formatH264, + /* requiresSecureDecoder= */ false, + /* requiresTunnelingDecoder= */ false); + for (int i = 0; i < decoderInfos.size(); i++) { + if (decoderInfos.get(i).capabilities != null + && decoderInfos.get(i).capabilities.getVideoCapabilities() != null) { + List performancePointListH264 = + decoderInfos + .get(i) + .capabilities + .getVideoCapabilities() + .getSupportedPerformancePoints(); + if (performancePointListH264 != null && !performancePointListH264.isEmpty()) { + PerformancePoint targetPerformancePointH264 = + new PerformancePoint(/* width= */ 1280, /* height= */ 720, /* frameRate= */ 60); + return evaluatePerformancePointCoverage( + performancePointListH264, targetPerformancePointH264) + == COVERAGE_RESULT_NO; + } + } + } + } + return true; + } catch (MediaCodecUtil.DecoderQueryException ignored) { + return true; + } + } + + private static @PerformancePointCoverageResult int evaluatePerformancePointCoverage( + List performancePointList, PerformancePoint targetPerformancePoint) { + for (int i = 0; i < performancePointList.size(); i++) { + if (performancePointList.get(i).covers(targetPerformancePoint)) { + return COVERAGE_RESULT_YES; + } + } + return COVERAGE_RESULT_NO; + } + } +}