diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java index 2f0c734c093..6a97200aae4 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java @@ -67,6 +67,7 @@ import androidx.media3.common.util.TraceUtil; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.container.NalUnitUtil; import androidx.media3.container.ObuParser; import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.exoplayer.CodecParameters; @@ -211,6 +212,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer private @MonotonicNonNull CodecMaxValues codecMaxValues; private boolean codecNeedsSetOutputSurfaceWorkaround; private boolean codecHandlesHdr10PlusOutOfBandMetadata; + private boolean stripHdr10PlusSeiNalUnits; private @MonotonicNonNull VideoSink videoSink; private boolean hasSetVideoSink; private @VideoSink.FirstFrameReleaseInstruction int nextVideoSinkFirstFrameReleaseInstruction; @@ -1551,6 +1553,9 @@ protected void onCodecInitialized( codecNeedsSetOutputSurfaceWorkaround = codecNeedsSetOutputSurfaceWorkaround(name); codecHandlesHdr10PlusOutOfBandMetadata = checkNotNull(getCodecInfo()).isHdr10PlusOutOfBandMetadataSupported(); + stripHdr10PlusSeiNalUnits = + isDolbyVisionProfile8(configuration.format) + && MimeTypes.VIDEO_DOLBY_VISION.equals(configuration.codecInfo.codecMimeType); maybeSetupTunnelingForFirstFrame(); } @@ -1625,6 +1630,9 @@ protected DecoderReuseEvaluation onInputFormatChanged(FormatHolder formatHolder) @CallSuper @Override protected void onQueueInputBuffer(DecoderInputBuffer buffer) throws ExoPlaybackException { + if (stripHdr10PlusSeiNalUnits && buffer.data != null && buffer.data.remaining() > 0) { + stripHdr10PlusSeiFromBuffer(buffer.data); + } if (av1SampleDependencyParser != null && checkNotNull(getCodecInfo()).mimeType.equals(MimeTypes.VIDEO_AV1) && buffer.isKeyFrame() @@ -2635,6 +2643,154 @@ private static boolean deviceNeedsNoPostProcessWorkaround() { return "NVIDIA".equals(Build.MANUFACTURER); } + /** + * Returns whether the format contains Dolby Vision Profile 8. + * + * @param format The {@link Format} to check. + * @return Whether the format contains Dolby Vision Profile 8. + */ + private static boolean isDolbyVisionProfile8(Format format) { + if (!MimeTypes.VIDEO_DOLBY_VISION.equals(format.sampleMimeType)) { + return false; + } + @Nullable + Pair codecProfileAndLevel = + CodecSpecificDataUtil.getCodecProfileAndLevel(format); + return codecProfileAndLevel != null + && codecProfileAndLevel.first == CodecProfileLevel.DolbyVisionProfileDvheSt; + } + + /** + * Strips HEVC HDR10+ SEI NAL units from an AnnexB-formatted buffer in-place. + * + *

Scans for NAL unit boundaries (start codes {@code 0x000001} or {@code 0x00000001}). For each + * PREFIX_SEI (type 39) or SUFFIX_SEI (type 40) NAL unit, parses the SEI payloads to check for + * HDR10+ (ITU-T T.35 payload type 4 with country code {@code 0xB5}, provider code {@code 0x003C}, + * oriented code {@code 0x0001}, application identifier 4, and version 0 or 1). Removes matching + * NAL units by compacting the buffer and reducing {@link ByteBuffer#limit()}. + * + * @param data The buffer containing AnnexB-formatted HEVC data positioned at the start of the + * data to scan. On return, the position is reset to the original position and the limit may be + * reduced if NAL units were removed. + */ + /* package */ static void stripHdr10PlusSeiFromBuffer(ByteBuffer data) { + int startPos = data.position(); + int limit = data.limit(); + // writePos tracks where we're writing compacted data to. + int writePos = startPos; + int nalStartIndex = -1; + int startCodeLen = 0; + + int i = startPos; + while (i <= limit) { + // Find next start code or end of buffer. + boolean atEnd = (i == limit); + boolean foundStartCode = false; + int nextStartCodeLen = 0; + if (!atEnd && i + 2 < limit) { + if (data.get(i) == 0 && data.get(i + 1) == 0) { + if (data.get(i + 2) == 1) { + foundStartCode = true; + nextStartCodeLen = 3; + } else if (data.get(i + 2) == 0 && i + 3 < limit && data.get(i + 3) == 1) { + foundStartCode = true; + nextStartCodeLen = 4; + } + } + } + + if (foundStartCode || atEnd) { + if (nalStartIndex >= 0) { + // We have a complete NAL unit from nalStartIndex to i. + int nalDataStart = nalStartIndex + startCodeLen; + int nalEnd = i; + boolean strip = false; + + if (nalEnd - nalDataStart >= 2) { + // HEVC NAL header: first byte contains forbidden_zero_bit(1) + nal_unit_type(6) + + // nuh_layer_id high bit(1). NAL type = (first_byte >> 1) & 0x3F. + int nalUnitType = (data.get(nalDataStart) & 0x7E) >> 1; + if (nalUnitType == NalUnitUtil.H265_NAL_UNIT_TYPE_PREFIX_SEI + || nalUnitType == NalUnitUtil.H265_NAL_UNIT_TYPE_SUFFIX_SEI) { + strip = isHdr10PlusSeiNalUnit(data, nalDataStart + 2, nalEnd); + } + } + + if (!strip) { + // Keep this NAL: copy from nalStartIndex to i. + if (writePos != nalStartIndex) { + for (int j = nalStartIndex; j < nalEnd; j++) { + data.put(writePos++, data.get(j)); + } + } else { + writePos = nalEnd; + } + } + // If strip is true, we skip writing this NAL (writePos stays). + } + nalStartIndex = i; + startCodeLen = nextStartCodeLen; + i += nextStartCodeLen > 0 ? nextStartCodeLen : 1; + } else { + i++; + } + } + + data.limit(writePos); + data.position(startPos); + } + + /** + * Returns whether a SEI NAL unit's RBSP (starting after the 2-byte HEVC NAL header) contains an + * HDR10+ SEI message as the first payload. + */ + private static boolean isHdr10PlusSeiNalUnit(ByteBuffer data, int rbspStart, int nalEnd) { + int pos = rbspStart; + if (pos >= nalEnd) { + return false; + } + + // Parse SEI payload type (accumulated ff_byte values + last byte). + int payloadType = 0; + while (pos < nalEnd) { + int b = data.get(pos++) & 0xFF; + payloadType += b; + if (b != 0xFF) { + break; + } + } + + // Parse SEI payload size. + int payloadSize = 0; + while (pos < nalEnd) { + int b = data.get(pos++) & 0xFF; + payloadSize += b; + if (b != 0xFF) { + break; + } + } + + // Check for user_data_registered_itu_t_t35 (type 4) with HDR10+ identifiers. + if (payloadType != 4 || payloadSize < 7 || pos + 7 > nalEnd) { + return false; + } + + // Read ITU-T T.35 fields. Note: emulation prevention bytes (0x00 0x00 0x03) could appear in + // the payload, but the HDR10+ identifier bytes at the start (0xB5, 0x00, 0x3C, 0x00, 0x01, + // 0x04, 0x00/0x01) don't contain the 0x000003 pattern, so we can read them directly. + byte countryCode = data.get(pos); + int providerCode = ((data.get(pos + 1) & 0xFF) << 8) | (data.get(pos + 2) & 0xFF); + int orientedCode = ((data.get(pos + 3) & 0xFF) << 8) | (data.get(pos + 4) & 0xFF); + byte appIdentifier = data.get(pos + 5); + byte appVersion = data.get(pos + 6); + + return countryCode == (byte) 0xB5 + && providerCode == 0x003C + && orientedCode == 0x0001 + && appIdentifier == 4 + && (appVersion == 0 || appVersion == 1); + } + /* * TODO: * diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java index f062dd76331..9ef20c02675 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java @@ -21,6 +21,7 @@ import static android.media.MediaCodecInfo.CodecProfileLevel.AVCProfileHigh; import static android.media.MediaCodecInfo.CodecProfileLevel.DolbyVisionLevelFhd30; import static android.media.MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheDtr; +import static android.media.MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheSt; import static android.media.MediaCodecInfo.CodecProfileLevel.HEVCHighTierLevel51; import static android.media.MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel41; import static android.media.MediaCodecInfo.CodecProfileLevel.HEVCProfileMain; @@ -102,6 +103,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -4242,6 +4244,103 @@ public void supportsFormat_withDolbyVision_setsDecoderSupportFlagsByDisplayDolby .isEqualTo(RendererCapabilities.DECODER_SUPPORT_PRIMARY); } + // Tests for stripHdr10PlusSeiFromBuffer: strips in-band HDR10+ SEI NAL units from HEVC + // AnnexB bitstream when a native DV codec is active for DV Profile 8 content. + + @Test + public void stripHdr10PlusSei_removesHdr10PlusSeiNalUnit() { + // Build AnnexB buffer: [VCL NAL][HDR10+ PREFIX_SEI NAL][VCL NAL] + byte[] vclNal1 = annexBNalUnit(/* nalUnitType= */ 1, new byte[] {0x01, 0x02}); // TRAIL_R + byte[] hdr10PlusSei = createHdr10PlusSeiNalUnit(); + byte[] vclNal2 = annexBNalUnit(/* nalUnitType= */ 1, new byte[] {0x03, 0x04}); // TRAIL_R + ByteBuffer buffer = buildAnnexBBuffer(vclNal1, hdr10PlusSei, vclNal2); + int originalLimit = buffer.limit(); + + MediaCodecVideoRenderer.stripHdr10PlusSeiFromBuffer(buffer); + + // The HDR10+ SEI should be removed; the two VCL NALs should be preserved. + ByteBuffer expected = buildAnnexBBuffer(vclNal1, vclNal2); + assertThat(buffer.remaining()).isEqualTo(expected.remaining()); + byte[] actualBytes = new byte[buffer.remaining()]; + buffer.get(actualBytes); + byte[] expectedBytes = new byte[expected.remaining()]; + expected.get(expectedBytes); + assertThat(actualBytes).isEqualTo(expectedBytes); + assertThat(buffer.limit()).isLessThan(originalLimit); + } + + @Test + public void stripHdr10PlusSei_preservesNonHdr10PlusSei() { + // Build AnnexB buffer: [VCL NAL][CEA-608 SEI NAL (not HDR10+)][VCL NAL] + byte[] vclNal1 = annexBNalUnit(/* nalUnitType= */ 1, new byte[] {0x01, 0x02}); + // SEI with payload type 4 but non-HDR10+ country code (0x00 instead of 0xB5). + byte[] nonHdr10PlusSei = createNonHdr10PlusSeiNalUnit(); + byte[] vclNal2 = annexBNalUnit(/* nalUnitType= */ 1, new byte[] {0x03, 0x04}); + ByteBuffer buffer = buildAnnexBBuffer(vclNal1, nonHdr10PlusSei, vclNal2); + byte[] originalBytes = new byte[buffer.remaining()]; + buffer.mark(); + buffer.get(originalBytes); + buffer.reset(); + + MediaCodecVideoRenderer.stripHdr10PlusSeiFromBuffer(buffer); + + byte[] resultBytes = new byte[buffer.remaining()]; + buffer.get(resultBytes); + assertThat(resultBytes).isEqualTo(originalBytes); + } + + @Test + public void stripHdr10PlusSei_noOpWhenNoSeiPresent() { + // Build AnnexB buffer with only VCL NAL units. + byte[] vclNal1 = annexBNalUnit(/* nalUnitType= */ 1, new byte[] {0x01, 0x02}); + byte[] vclNal2 = annexBNalUnit(/* nalUnitType= */ 1, new byte[] {0x03, 0x04}); + ByteBuffer buffer = buildAnnexBBuffer(vclNal1, vclNal2); + byte[] originalBytes = new byte[buffer.remaining()]; + buffer.mark(); + buffer.get(originalBytes); + buffer.reset(); + + MediaCodecVideoRenderer.stripHdr10PlusSeiFromBuffer(buffer); + + byte[] resultBytes = new byte[buffer.remaining()]; + buffer.get(resultBytes); + assertThat(resultBytes).isEqualTo(originalBytes); + } + + @Test + public void dvProfile8_nativeDvCodec_stripsHdr10PlusSeiFromBuffer() throws Exception { + // Device has DV codec, TV supports DV. Native DV codec is selected, so HDR10+ SEI NAL + // units in the HEVC bitstream should be stripped before queueing to the codec. + ShadowMediaCodec.addDecoder( + "dvhe-codec", + new ShadowMediaCodec.CodecConfig( + /* inputBufferSize= */ 100_000, + /* outputBufferSize= */ 100_000, + /* codec= */ (in, out) -> {})); + setDisplayHdrCapabilities(/* dv= */ true, /* hdr10Plus= */ true); + + assertDvProfile8InBandHdr10PlusHandling( + createDvProfile8CodecSelector(/* hasDv= */ true, /* hasHevc= */ true), + /* expectHdr10PlusSeiPresent= */ false); + } + + @Test + public void dvProfile8_hevcFallback_preservesHdr10PlusSeiInBuffer() throws Exception { + // Device has DV + HEVC codecs, but TV only supports HDR10+ (no DV) → HEVC fallback is + // selected. HDR10+ SEI NAL units should be preserved in the buffer. + ShadowMediaCodec.addDecoder( + "hevc-codec", + new ShadowMediaCodec.CodecConfig( + /* inputBufferSize= */ 100_000, + /* outputBufferSize= */ 100_000, + /* codec= */ (in, out) -> {})); + setDisplayHdrCapabilities(/* dv= */ false, /* hdr10Plus= */ true); + + assertDvProfile8InBandHdr10PlusHandling( + createDvProfile8CodecSelector(/* hasDv= */ true, /* hasHevc= */ true), + /* expectHdr10PlusSeiPresent= */ true); + } + @Test public void getDecoderInfo_withNonPerformantHardwareDecoder_returnsHardwareDecoderFirst() throws Exception { @@ -4797,6 +4896,294 @@ private static Format createFormat(String mimeType, int width, int height) { .build(); } + /** + * Creates an HEVC AnnexB NAL unit with a 4-byte start code, 2-byte NAL header, and the given + * payload. The NAL header encodes the given nalUnitType. + */ + private static byte[] annexBNalUnit(int nalUnitType, byte[] payload) { + // 4-byte start code + 2-byte HEVC NAL header + payload + byte[] result = new byte[4 + 2 + payload.length]; + result[0] = 0x00; + result[1] = 0x00; + result[2] = 0x00; + result[3] = 0x01; + // HEVC NAL header: forbidden(1) + type(6) + layerId_high(1) | layerId_low(5) + tid(3) + // first byte: (nalUnitType << 1) & 0x7E + result[4] = (byte) ((nalUnitType << 1) & 0x7E); + result[5] = 0x01; // nuh_temporal_id_plus1 = 1 + System.arraycopy(payload, 0, result, 6, payload.length); + return result; + } + + /** Creates a PREFIX_SEI (type 39) NAL unit containing an HDR10+ SEI message. */ + private static byte[] createHdr10PlusSeiNalUnit() { + // SEI payload: type=4 (user_data_registered_itu_t_t35), size=7, then HDR10+ identifiers + byte[] seiPayload = + new byte[] { + 0x04, // payloadType = 4 + 0x07, // payloadSize = 7 + (byte) 0xB5, // ituTT35CountryCode (United States) + 0x00, 0x3C, // ituTT35TerminalProviderCode + 0x00, 0x01, // ituTT35TerminalProviderOrientedCode + 0x04, // applicationIdentifier + 0x00, // applicationVersion + }; + return annexBNalUnit(/* nalUnitType= */ 39, seiPayload); // PREFIX_SEI + } + + /** Creates a PREFIX_SEI (type 39) NAL unit with a non-HDR10+ SEI message. */ + private static byte[] createNonHdr10PlusSeiNalUnit() { + // SEI payload: type=4 (user_data_registered_itu_t_t35), size=7, but wrong country code + byte[] seiPayload = + new byte[] { + 0x04, // payloadType = 4 + 0x07, // payloadSize = 7 + 0x00, // ituTT35CountryCode (NOT 0xB5) + 0x00, 0x3C, // ituTT35TerminalProviderCode + 0x00, 0x01, // ituTT35TerminalProviderOrientedCode + 0x04, // applicationIdentifier + 0x00, // applicationVersion + }; + return annexBNalUnit(/* nalUnitType= */ 39, seiPayload); // PREFIX_SEI + } + + /** Concatenates AnnexB NAL unit byte arrays into a single ByteBuffer. */ + private static ByteBuffer buildAnnexBBuffer(byte[]... nalUnits) { + int totalLen = 0; + for (byte[] nalUnit : nalUnits) { + totalLen += nalUnit.length; + } + ByteBuffer buffer = ByteBuffer.allocate(totalLen); + for (byte[] nalUnit : nalUnits) { + buffer.put(nalUnit); + } + buffer.flip(); + return buffer; + } + + private static Format createDvProfile8Format() { + return new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_DOLBY_VISION) + .setCodecs("dvhe.08.01") + .setWidth(3840) + .setHeight(2160) + .build(); + } + + private static MediaCodecInfo createDvProfile8Codec() { + CodecCapabilities capabilities = + createCodecCapabilities(DolbyVisionProfileDvheSt, DolbyVisionLevelFhd30); + return MediaCodecInfo.newInstance( + /* name= */ "dvhe-codec", + /* mimeType= */ MimeTypes.VIDEO_DOLBY_VISION, + /* codecMimeType= */ MimeTypes.VIDEO_DOLBY_VISION, + /* capabilities= */ capabilities, + /* hardwareAccelerated= */ true, + /* softwareOnly= */ false, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); + } + + private static MediaCodecInfo createHevcCodec() { + CodecCapabilities capabilities = new CodecCapabilities(); + capabilities.profileLevels = + new CodecProfileLevel[] { + createCodecProfileLevel(HEVCProfileMain, HEVCMainTierLevel41), + createCodecProfileLevel(HEVCProfileMain10, HEVCHighTierLevel51) + }; + return MediaCodecInfo.newInstance( + /* name= */ "hevc-codec", + /* mimeType= */ MimeTypes.VIDEO_H265, + /* codecMimeType= */ MimeTypes.VIDEO_H265, + /* capabilities= */ capabilities, + /* hardwareAccelerated= */ true, + /* softwareOnly= */ false, + /* vendor= */ false, + /* forceDisableAdaptive= */ false, + /* forceSecure= */ false); + } + + /** + * Creates sample data containing HEVC frames with embedded HDR10+ SEI NAL units. The data is + * AnnexB-formatted: [VCL NAL (keyframe)][HDR10+ SEI NAL]. + */ + private static byte[] createHevcSampleWithHdr10PlusSei(boolean isKeyFrame) { + // HEVC VCL NAL unit type 19 (IDR_W_RADL) for keyframes, type 1 (TRAIL_R) otherwise. + int vclType = isKeyFrame ? 19 : 1; + byte[] vclNal = annexBNalUnit(vclType, new byte[] {0x00}); + byte[] hdr10PlusSei = createHdr10PlusSeiNalUnit(); + byte[] result = new byte[vclNal.length + hdr10PlusSei.length]; + System.arraycopy(vclNal, 0, result, 0, vclNal.length); + System.arraycopy(hdr10PlusSei, 0, result, vclNal.length, hdr10PlusSei.length); + return result; + } + + /** Returns true if the given buffer bytes contain an HDR10+ SEI NAL unit. */ + private static boolean containsHdr10PlusSei(byte[] data) { + // Scan for start codes and check for PREFIX_SEI with HDR10+ payload. + for (int i = 0; i < data.length - 6; i++) { + if (data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1) { + int nalType = (data[i + 4] & 0x7E) >> 1; + // PREFIX_SEI = 39 + if (nalType == 39 && i + 6 + 9 <= data.length) { + int rbspStart = i + 6; // after start code (4) + NAL header (2) + // Check SEI payload type = 4 and HDR10+ identifiers + if (data[rbspStart] == 0x04 + && (data[rbspStart + 2] & 0xFF) == 0xB5 + && data[rbspStart + 3] == 0x00 + && data[rbspStart + 4] == 0x3C + && data[rbspStart + 5] == 0x00 + && data[rbspStart + 6] == 0x01 + && data[rbspStart + 7] == 0x04 + && (data[rbspStart + 8] == 0x00 || data[rbspStart + 8] == 0x01)) { + return true; + } + } + } + } + return false; + } + + private static MediaCodecSelector createDvProfile8CodecSelector( + boolean hasDv, boolean hasHevc) { + return (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> { + switch (mimeType) { + case MimeTypes.VIDEO_DOLBY_VISION: + return hasDv ? ImmutableList.of(createDvProfile8Codec()) : ImmutableList.of(); + case MimeTypes.VIDEO_H265: + return hasHevc ? ImmutableList.of(createHevcCodec()) : ImmutableList.of(); + default: + return ImmutableList.of(); + } + }; + } + + private void setDisplayHdrCapabilities(boolean dv, boolean hdr10Plus) { + 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); + List types = new ArrayList<>(); + if (dv) { + types.add(Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION); + } + if (hdr10Plus) { + types.add(Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS); + } + int[] hdrTypes = types.stream().mapToInt(Integer::intValue).toArray(); + shadowDisplay.setDisplayHdrCapabilities( + display.getDisplayId(), + /* maxLuminance= */ 100f, + /* maxAverageLuminance= */ 100f, + /* minLuminance= */ 100f, + hdrTypes); + } + + /** + * Asserts whether HDR10+ SEI NAL units are present or stripped from the buffer data that reaches + * the codec during DV Profile 8 playback. + */ + private void assertDvProfile8InBandHdr10PlusHandling( + MediaCodecSelector codecSelector, boolean expectHdr10PlusSeiPresent) throws Exception { + BufferCapturingAdapterFactory capturingFactory = new BufferCapturingAdapterFactory(); + Format dvProfile8Format = createDvProfile8Format(); + byte[] keyFrameSample = createHevcSampleWithHdr10PlusSei(/* isKeyFrame= */ true); + byte[] nonKeyFrameSample = createHevcSampleWithHdr10PlusSei(/* isKeyFrame= */ false); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ dvProfile8Format, + ImmutableList.of( + sample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME, keyFrameSample), + sample(/* timeUs= */ 33_000, /* flags= */ 0, nonKeyFrameSample), + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + MediaCodecVideoRenderer renderer = + new MediaCodecVideoRenderer( + new MediaCodecVideoRenderer.Builder(ApplicationProvider.getApplicationContext()) + .setCodecAdapterFactory(capturingFactory) + .setMediaCodecSelector(codecSelector) + .setAllowedJoiningTimeMs(0) + .setEnableDecoderFallback(false) + .setEventHandler(new Handler(testMainLooper)) + .setEventListener(eventListener) + .setMaxDroppedFramesToNotify(1)) { + @Override + protected @Capabilities int supportsFormat( + MediaCodecSelector mediaCodecSelector, Format format) { + return RendererCapabilities.create(C.FORMAT_HANDLED); + } + }; + renderer.init(/* index= */ 0, PlayerId.UNSET, Clock.DEFAULT); + renderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); + renderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {dvProfile8Format}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0, + new MediaSource.MediaPeriodId(new Object())); + renderer.start(); + renderer.setCurrentStreamFinal(); + int posUs = 0; + while (!renderer.isEnded()) { + renderer.render(posUs, SystemClock.elapsedRealtime() * 1000); + posUs += 10_000; + } + + assertThat(capturingFactory.capturedBufferData).isNotEmpty(); + boolean anyBufferContainsHdr10Plus = false; + for (byte[] bufferData : capturingFactory.capturedBufferData) { + if (containsHdr10PlusSei(bufferData)) { + anyBufferContainsHdr10Plus = true; + break; + } + } + assertThat(anyBufferContainsHdr10Plus).isEqualTo(expectHdr10PlusSeiPresent); + } + + /** + * A {@link MediaCodecAdapter.Factory} that captures the buffer data contents when {@link + * MediaCodecAdapter#queueInputBuffer} is called. + */ + private static final class BufferCapturingAdapterFactory implements MediaCodecAdapter.Factory { + final List capturedBufferData = new ArrayList<>(); + + @Override + public MediaCodecAdapter createAdapter(MediaCodecAdapter.Configuration configuration) + throws IOException { + MediaCodecAdapter delegate = + new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration); + return new ForwardingMediaCodecAdapter(delegate) { + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + ByteBuffer inputBuffer = delegate.getInputBuffer(index); + if (inputBuffer != null && size > 0) { + byte[] copy = new byte[size]; + int savedPosition = inputBuffer.position(); + int savedLimit = inputBuffer.limit(); + inputBuffer.position(offset); + inputBuffer.limit(offset + size); + inputBuffer.get(copy); + inputBuffer.position(savedPosition); + inputBuffer.limit(savedLimit); + capturedBufferData.add(copy); + } + super.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + } + }; + } + } + private static final class ForwardingSynchronousMediaCodecAdapterWithReordering extends ForwardingMediaCodecAdapter { /** A factory for {@link ForwardingSynchronousMediaCodecAdapterWithReordering} instances. */