From 23a301fc5d3d14593f32c04634d7267c8a86a1cd 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 --- RELEASENOTES.md | 4 + .../exoplayer/mediacodec/MediaCodecInfo.java | 93 +-------- ...CodecPerformancePointCoverageProvider.java | 183 ++++++++++++++++++ 3 files changed, 192 insertions(+), 88 deletions(-) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecPerformancePointCoverageProvider.java diff --git a/RELEASENOTES.md b/RELEASENOTES.md index af120044dd..971aa41cf9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -22,6 +22,10 @@ * Allow renderer recovery by disabling offload if audio track fails to initialize in offload mode. * Video: + * Add workaround for a device issue on Galaxy Tab S7 FE, Chromecast with + Google TV, and Lenovo M10 FHD Plus that causes 60fps H265 streams to be + marked as unsupported + ([#966](https://github.com/androidx/media/issues/966)). * Text: * Metadata: * Image: diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java index ffaefde146..71a070ac3f 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecInfo.java @@ -28,7 +28,8 @@ import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO; import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_YES_WITHOUT_RECONFIGURATION; import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_YES_WITH_FLUSH; import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_YES_WITH_RECONFIGURATION; -import static java.lang.annotation.ElementType.TYPE_USE; +import static androidx.media3.exoplayer.mediacodec.MediaCodecPerformancePointCoverageProvider.COVERAGE_RESULT_NO; +import static androidx.media3.exoplayer.mediacodec.MediaCodecPerformancePointCoverageProvider.COVERAGE_RESULT_YES; import android.graphics.Point; import android.media.MediaCodec; @@ -36,10 +37,7 @@ import android.media.MediaCodecInfo.AudioCapabilities; 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; @@ -52,11 +50,6 @@ import androidx.media3.common.util.Util; import androidx.media3.exoplayer.DecoderReuseEvaluation; import androidx.media3.exoplayer.DecoderReuseEvaluation.DecoderDiscardReasons; import androidx.media3.exoplayer.DecoderReuseEvaluation.DecoderReuseResult; -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. */ @SuppressWarnings("InlinedApi") @@ -520,10 +513,10 @@ public final class MediaCodecInfo { } 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) { @@ -872,80 +865,4 @@ public final class MediaCodecInfo { && 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/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecPerformancePointCoverageProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecPerformancePointCoverageProvider.java new file mode 100644 index 0000000000..9529e39e2d --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecPerformancePointCoverageProvider.java @@ -0,0 +1,183 @@ +/* + * 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 androidx.media3.exoplayer.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 androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.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. */ +@UnstableApi +/* 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; + } + } +}