diff --git a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java index 57578e44d5..04469dedc3 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/RendererCapabilities.java @@ -127,21 +127,23 @@ public interface RendererCapabilities { int HARDWARE_ACCELERATION_NOT_SUPPORTED = 0; /** - * Level of decoder support. One of {@link #DECODER_SUPPORT_PRIMARY} and {@link - * #DECODER_SUPPORT_FALLBACK}. + * Level of decoder support. One of {@link #DECODER_SUPPORT_FALLBACK_MIMETYPE}, {@link + * #DECODER_SUPPORT_FALLBACK}, and {@link #DECODER_SUPPORT_PRIMARY}. * *

For video renderers, the level of support is indicated for non-tunneled output. */ @Documented @Retention(RetentionPolicy.SOURCE) @Target(TYPE_USE) - @IntDef({ - DECODER_SUPPORT_PRIMARY, - DECODER_SUPPORT_FALLBACK, - }) + @IntDef({DECODER_SUPPORT_FALLBACK_MIMETYPE, DECODER_SUPPORT_PRIMARY, DECODER_SUPPORT_FALLBACK}) @interface DecoderSupport {} /** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */ - int MODE_SUPPORT_MASK = 0b1 << 7; + int MODE_SUPPORT_MASK = 0b11 << 7; + /** + * The renderer will use a decoder for fallback mimetype if possible as format's MIME type is + * unsupported + */ + int DECODER_SUPPORT_FALLBACK_MIMETYPE = 0b10 << 7; /** The renderer is able to use the primary decoder for the format's MIME type. */ int DECODER_SUPPORT_PRIMARY = 0b1 << 7; /** The renderer will use a fallback decoder. */ diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java index 2ae767e384..76c7ba8e55 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelector.java @@ -3100,6 +3100,8 @@ public class DefaultTrackSelector extends MappingTrackSelector { return 0; } switch (mimeType) { + case MimeTypes.VIDEO_DOLBY_VISION: + return 5; case MimeTypes.VIDEO_AV1: return 4; case MimeTypes.VIDEO_H265: 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 c04a544512..615355caf4 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 @@ -40,6 +40,7 @@ import android.util.Pair; import android.view.Display; import android.view.Surface; import androidx.annotation.CallSuper; +import androidx.annotation.DoNotInline; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.google.android.exoplayer2.C; @@ -409,6 +410,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @DecoderSupport int decoderSupport = isPreferredDecoder ? DECODER_SUPPORT_PRIMARY : DECODER_SUPPORT_FALLBACK; + if (Util.SDK_INT >= 26 + && MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType) + && !Api26.doesDisplaySupportDolbyVision(context)) { + decoderSupport = DECODER_SUPPORT_FALLBACK_MIMETYPE; + } + @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED; if (isFormatSupported) { List tunnelingDecoderInfos = @@ -485,8 +492,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { alternativeMimeType, requiresSecureDecoder, requiresTunnelingDecoder); if (Util.SDK_INT >= 26 && MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType) - && !alternativeDecoderInfos.isEmpty()) { - // If sample type is Dolby Vision, check if Display supports Dolby Vision + && !alternativeDecoderInfos.isEmpty() + && !Api26.doesDisplaySupportDolbyVision(context)) { + return ImmutableList.copyOf(alternativeDecoderInfos); + } + return ImmutableList.builder() + .addAll(decoderInfos) + .addAll(alternativeDecoderInfos) + .build(); + } + + @RequiresApi(26) + private static final class Api26 { + @DoNotInline + public static boolean doesDisplaySupportDolbyVision(Context context) { boolean supportsDolbyVision = false; DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); @@ -501,14 +520,8 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } } - if (!supportsDolbyVision) { - return ImmutableList.copyOf(alternativeDecoderInfos); - } + return supportsDolbyVision; } - return ImmutableList.builder() - .addAll(decoderInfos) - .addAll(alternativeDecoderInfos) - .build(); } @Override diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java index a50e1e1def..c57f611752 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/DefaultTrackSelectorTest.java @@ -21,6 +21,7 @@ import static com.google.android.exoplayer2.C.FORMAT_UNSUPPORTED_SUBTYPE; import static com.google.android.exoplayer2.C.FORMAT_UNSUPPORTED_TYPE; import static com.google.android.exoplayer2.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; import static com.google.android.exoplayer2.RendererCapabilities.DECODER_SUPPORT_FALLBACK; +import static com.google.android.exoplayer2.RendererCapabilities.DECODER_SUPPORT_FALLBACK_MIMETYPE; import static com.google.android.exoplayer2.RendererCapabilities.DECODER_SUPPORT_PRIMARY; import static com.google.android.exoplayer2.RendererCapabilities.HARDWARE_ACCELERATION_NOT_SUPPORTED; import static com.google.android.exoplayer2.RendererCapabilities.HARDWARE_ACCELERATION_SUPPORTED; @@ -2242,6 +2243,68 @@ public final class DefaultTrackSelectorTest { assertAdaptiveSelection(result.selections[0], adaptiveGroup, /* expectedTracks...= */ 1, 0); } + /** + * Tests that track selector will select video track with support of its primary decoder over a + * track that will use a decoder for it's format fallback sampleMimetype. + */ + @Test + public void selectTracks_withDecoderSupportFallbackMimetype_selectsTrackWithPrimaryDecoder() + throws Exception { + Format formatDV = + new Format.Builder().setId("0").setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION).build(); + Format formatHevc = + new Format.Builder().setId("1").setSampleMimeType(MimeTypes.VIDEO_H265).build(); + TrackGroupArray trackGroups = + new TrackGroupArray(new TrackGroup(formatDV), new TrackGroup(formatHevc)); + @Capabilities + int capabilitiesDecoderSupportPrimary = + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + HARDWARE_ACCELERATION_SUPPORTED, + DECODER_SUPPORT_PRIMARY); + int capabilitiesDecoderSupportFallbackType = + RendererCapabilities.create( + FORMAT_HANDLED, + ADAPTIVE_NOT_SEAMLESS, + TUNNELING_NOT_SUPPORTED, + HARDWARE_ACCELERATION_SUPPORTED, + DECODER_SUPPORT_FALLBACK_MIMETYPE); + + // Select track supported by primary decoder by default. + ImmutableMap rendererCapabilitiesMapDifferingDecoderSupport = + ImmutableMap.of( + "0", capabilitiesDecoderSupportFallbackType, "1", capabilitiesDecoderSupportPrimary); + RendererCapabilities rendererCapabilitiesDifferingDecoderSupport = + new FakeMappedRendererCapabilities( + C.TRACK_TYPE_VIDEO, rendererCapabilitiesMapDifferingDecoderSupport); + TrackSelectorResult result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilitiesDifferingDecoderSupport}, + trackGroups, + periodId, + TIMELINE); + + assertFixedSelection(result.selections[0], trackGroups, formatHevc); + + // Select Dolby Vision track over HEVC when renderer supports both equally + ImmutableMap rendererCapabilitiesMapAllPrimaryDecoderSupport = + ImmutableMap.of( + "0", capabilitiesDecoderSupportPrimary, "1", capabilitiesDecoderSupportPrimary); + RendererCapabilities rendererCapabilitiesAllPrimaryDecoderSupport = + new FakeMappedRendererCapabilities( + C.TRACK_TYPE_VIDEO, rendererCapabilitiesMapAllPrimaryDecoderSupport); + result = + trackSelector.selectTracks( + new RendererCapabilities[] {rendererCapabilitiesAllPrimaryDecoderSupport}, + trackGroups, + periodId, + TIMELINE); + + assertFixedSelection(result.selections[0], trackGroups, formatDV); + } + /** * Tests that track selector will select the video track with the highest number of matching role * flags given by {@link Parameters}. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java index 0e487748de..fc2fa8fc2e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.video; +import static android.view.Display.DEFAULT_DISPLAY; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.END_OF_STREAM_ITEM; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.format; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; @@ -26,13 +27,16 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.robolectric.Shadows.shadowOf; +import android.content.Context; import android.graphics.SurfaceTexture; +import android.hardware.display.DisplayManager; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaFormat; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.view.Display; import android.view.Surface; import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; @@ -63,6 +67,8 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import org.robolectric.Shadows; +import org.robolectric.shadows.ShadowDisplay; import org.robolectric.shadows.ShadowLooper; /** Unit test for {@link MediaCodecVideoRenderer}. */ @@ -610,6 +616,100 @@ public class MediaCodecVideoRendererTest { .isEqualTo(C.FORMAT_UNSUPPORTED_SUBTYPE); } + @Test + public void supportsFormat_withDolbyVision_setsDecoderSupportFlagsByDisplayDolbyVisionSupport() + throws Exception { + Format formatDvheDtr = + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dvhe.04.01") + .build(); + // Provide supporting Dolby Vision and fallback HEVC decoders + MediaCodecSelector mediaCodecSelector = + (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> { + switch (mimeType) { + case MimeTypes.VIDEO_DOLBY_VISION: + { + CodecCapabilities capabilitiesDolby = new CodecCapabilities(); + capabilitiesDolby.profileLevels = new CodecProfileLevel[] {new CodecProfileLevel()}; + capabilitiesDolby.profileLevels[0].profile = + CodecProfileLevel.DolbyVisionProfileDvheDtr; + capabilitiesDolby.profileLevels[0].level = CodecProfileLevel.DolbyVisionLevelFhd30; + return ImmutableList.of( + MediaCodecInfo.newInstance( + /* name= */ "dvhe-codec", + /* mimeType= */ mimeType, + /* codecMimeType= */ mimeType, + /* capabilities= */ capabilitiesDolby, + /* hardwareAccelerated= */ true, + /* softwareOnly= */ false, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); + } + case MimeTypes.VIDEO_H265: + { + CodecCapabilities capabilitiesH265 = new CodecCapabilities(); + capabilitiesH265.profileLevels = + new CodecProfileLevel[] {new CodecProfileLevel(), new CodecProfileLevel()}; + capabilitiesH265.profileLevels[0].profile = CodecProfileLevel.HEVCProfileMain; + capabilitiesH265.profileLevels[0].level = CodecProfileLevel.HEVCMainTierLevel41; + capabilitiesH265.profileLevels[1].profile = CodecProfileLevel.HEVCProfileMain10; + capabilitiesH265.profileLevels[1].level = CodecProfileLevel.HEVCHighTierLevel51; + return ImmutableList.of( + MediaCodecInfo.newInstance( + /* name= */ "h265-codec", + /* mimeType= */ mimeType, + /* codecMimeType= */ mimeType, + /* capabilities= */ capabilitiesH265, + /* hardwareAccelerated= */ true, + /* softwareOnly= */ false, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false)); + } + default: + return ImmutableList.of(); + } + }; + MediaCodecVideoRenderer renderer = + new MediaCodecVideoRenderer( + ApplicationProvider.getApplicationContext(), + mediaCodecSelector, + /* allowedJoiningTimeMs= */ 0, + /* eventHandler= */ new Handler(testMainLooper), + /* eventListener= */ eventListener, + /* maxDroppedFramesToNotify= */ 1); + renderer.init(/* index= */ 0, PlayerId.UNSET); + + @Capabilities int capabilitiesDvheDtr = renderer.supportsFormat(formatDvheDtr); + + assertThat(RendererCapabilities.getDecoderSupport(capabilitiesDvheDtr)) + .isEqualTo(RendererCapabilities.DECODER_SUPPORT_FALLBACK_MIMETYPE); + + // Set Display to have Dolby Vision support + Context context = ApplicationProvider.getApplicationContext(); + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + Display display = (displayManager != null) ? displayManager.getDisplay(DEFAULT_DISPLAY) : null; + ShadowDisplay shadowDisplay = Shadows.shadowOf(display); + int[] hdrCapabilities = + new int[] { + Display.HdrCapabilities.HDR_TYPE_HDR10, Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION + }; + shadowDisplay.setDisplayHdrCapabilities( + display.getDisplayId(), + /* maxLuminance= */ 100f, + /* maxAverageLuminance= */ 100f, + /* minLuminance= */ 100f, + hdrCapabilities); + + capabilitiesDvheDtr = renderer.supportsFormat(formatDvheDtr); + + assertThat(RendererCapabilities.getDecoderSupport(capabilitiesDvheDtr)) + .isEqualTo(RendererCapabilities.DECODER_SUPPORT_PRIMARY); + } + @Test public void getCodecMaxInputSize_videoH263() { MediaCodecInfo codecInfo = createMediaCodecInfo(MimeTypes.VIDEO_H263);