Adjust track selection with Dolby Vision if display does not support

If the sample type is Dolby Vision and the display does not support Dolby Vision, then the capabilities DecoderSupport flag is set to DECODER_SUPPORT_FALLBACK_MIMETYPE. This denotes that the renderer will use a decoder for a fallback mimetype if possible. This alters track selection as tracks with DecoderSupport DECODER_SUPPORT_PRIMARY are preferred.

UnitTests included
-DefaultTrackSelector test that checks track selection reordering with DECODER_SUPPORT_FALLBACK_MIMETYPE
-MediaCodecVideoRenderer test that checks setting of DecoderSupport flag based on Display's Dolby Vision support

Issue: google/ExoPlayer#8944
PiperOrigin-RevId: 480040876
This commit is contained in:
michaelkatz 2022-10-10 10:35:17 +00:00 committed by Marc Baechinger
parent 2188685c46
commit a366590a04
5 changed files with 196 additions and 16 deletions

View File

@ -127,21 +127,23 @@ public interface RendererCapabilities {
int HARDWARE_ACCELERATION_NOT_SUPPORTED = 0; int HARDWARE_ACCELERATION_NOT_SUPPORTED = 0;
/** /**
* Level of decoder support. One of {@link #DECODER_SUPPORT_PRIMARY} and {@link * Level of decoder support. One of {@link #DECODER_SUPPORT_FALLBACK_MIMETYPE}, {@link
* #DECODER_SUPPORT_FALLBACK}. * #DECODER_SUPPORT_FALLBACK}, and {@link #DECODER_SUPPORT_PRIMARY}.
* *
* <p>For video renderers, the level of support is indicated for non-tunneled output. * <p>For video renderers, the level of support is indicated for non-tunneled output.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({ @IntDef({DECODER_SUPPORT_FALLBACK_MIMETYPE, DECODER_SUPPORT_PRIMARY, DECODER_SUPPORT_FALLBACK})
DECODER_SUPPORT_PRIMARY,
DECODER_SUPPORT_FALLBACK,
})
@interface DecoderSupport {} @interface DecoderSupport {}
/** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */ /** 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. */ /** The renderer is able to use the primary decoder for the format's MIME type. */
int DECODER_SUPPORT_PRIMARY = 0b1 << 7; int DECODER_SUPPORT_PRIMARY = 0b1 << 7;
/** The renderer will use a fallback decoder. */ /** The renderer will use a fallback decoder. */

View File

@ -3100,6 +3100,8 @@ public class DefaultTrackSelector extends MappingTrackSelector {
return 0; return 0;
} }
switch (mimeType) { switch (mimeType) {
case MimeTypes.VIDEO_DOLBY_VISION:
return 5;
case MimeTypes.VIDEO_AV1: case MimeTypes.VIDEO_AV1:
return 4; return 4;
case MimeTypes.VIDEO_H265: case MimeTypes.VIDEO_H265:

View File

@ -40,6 +40,7 @@ import android.util.Pair;
import android.view.Display; import android.view.Display;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
@ -409,6 +410,12 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
@DecoderSupport @DecoderSupport
int decoderSupport = isPreferredDecoder ? DECODER_SUPPORT_PRIMARY : DECODER_SUPPORT_FALLBACK; 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; @TunnelingSupport int tunnelingSupport = TUNNELING_NOT_SUPPORTED;
if (isFormatSupported) { if (isFormatSupported) {
List<MediaCodecInfo> tunnelingDecoderInfos = List<MediaCodecInfo> tunnelingDecoderInfos =
@ -485,8 +492,20 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
alternativeMimeType, requiresSecureDecoder, requiresTunnelingDecoder); alternativeMimeType, requiresSecureDecoder, requiresTunnelingDecoder);
if (Util.SDK_INT >= 26 if (Util.SDK_INT >= 26
&& MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType) && MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)
&& !alternativeDecoderInfos.isEmpty()) { && !alternativeDecoderInfos.isEmpty()
// If sample type is Dolby Vision, check if Display supports Dolby Vision && !Api26.doesDisplaySupportDolbyVision(context)) {
return ImmutableList.copyOf(alternativeDecoderInfos);
}
return ImmutableList.<MediaCodecInfo>builder()
.addAll(decoderInfos)
.addAll(alternativeDecoderInfos)
.build();
}
@RequiresApi(26)
private static final class Api26 {
@DoNotInline
public static boolean doesDisplaySupportDolbyVision(Context context) {
boolean supportsDolbyVision = false; boolean supportsDolbyVision = false;
DisplayManager displayManager = DisplayManager displayManager =
(DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
@ -501,15 +520,9 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
} }
} }
if (!supportsDolbyVision) { return supportsDolbyVision;
return ImmutableList.copyOf(alternativeDecoderInfos);
} }
} }
return ImmutableList.<MediaCodecInfo>builder()
.addAll(decoderInfos)
.addAll(alternativeDecoderInfos)
.build();
}
@Override @Override
protected void onEnabled(boolean joining, boolean mayRenderStartOfStream) protected void onEnabled(boolean joining, boolean mayRenderStartOfStream)

View File

@ -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.C.FORMAT_UNSUPPORTED_TYPE;
import static com.google.android.exoplayer2.RendererCapabilities.ADAPTIVE_NOT_SEAMLESS; 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;
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.DECODER_SUPPORT_PRIMARY;
import static com.google.android.exoplayer2.RendererCapabilities.HARDWARE_ACCELERATION_NOT_SUPPORTED; import static com.google.android.exoplayer2.RendererCapabilities.HARDWARE_ACCELERATION_NOT_SUPPORTED;
import static com.google.android.exoplayer2.RendererCapabilities.HARDWARE_ACCELERATION_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); 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<String, Integer> 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<String, Integer> 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 * Tests that track selector will select the video track with the highest number of matching role
* flags given by {@link Parameters}. * flags given by {@link Parameters}.

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.video; 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.END_OF_STREAM_ITEM;
import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.format; import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.format;
import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; 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.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf; import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.graphics.SurfaceTexture; import android.graphics.SurfaceTexture;
import android.hardware.display.DisplayManager;
import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaCodecInfo.CodecProfileLevel;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.Display;
import android.view.Surface; import android.view.Surface;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider; import androidx.test.core.app.ApplicationProvider;
@ -63,6 +67,8 @@ import org.mockito.ArgumentCaptor;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule; import org.mockito.junit.MockitoRule;
import org.robolectric.Shadows;
import org.robolectric.shadows.ShadowDisplay;
import org.robolectric.shadows.ShadowLooper; import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link MediaCodecVideoRenderer}. */ /** Unit test for {@link MediaCodecVideoRenderer}. */
@ -610,6 +616,100 @@ public class MediaCodecVideoRendererTest {
.isEqualTo(C.FORMAT_UNSUPPORTED_SUBTYPE); .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 @Test
public void getCodecMaxInputSize_videoH263() { public void getCodecMaxInputSize_videoH263() {
MediaCodecInfo codecInfo = createMediaCodecInfo(MimeTypes.VIDEO_H263); MediaCodecInfo codecInfo = createMediaCodecInfo(MimeTypes.VIDEO_H263);