From 1869855c90f67e8735f3a58361e36db2c421f838 Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Tue, 5 Jul 2022 18:47:29 +0000 Subject: [PATCH 01/16] HDR: Remove unused EGL_GL_COLORSPACE_KHR attribute. PiperOrigin-RevId: 459106221 --- .../androidx/media3/common/util/GlUtil.java | 45 +++++++------------ ...lMatrixTransformationProcessorWrapper.java | 4 +- .../transformer/GlEffectsFrameProcessor.java | 2 +- 3 files changed, 20 insertions(+), 31 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java index 1d623ef416..aa6abcad39 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java @@ -64,9 +64,6 @@ public final class GlUtil { // https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_surfaceless_context.txt private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; - // https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt - private static final int EGL_GL_COLORSPACE_KHR = 0x309D; - private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_NONE = new int[] {EGL14.EGL_NONE}; private static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_8888 = new int[] { @@ -208,14 +205,13 @@ public final class GlUtil { } /** - * Returns a new {@link EGLSurface} wrapping the specified {@code surface}, for HDR rendering with - * Rec. 2020 color primaries. + * Returns a new RGBA 1010102 {@link EGLSurface} wrapping the specified {@code surface}. * * @param eglDisplay The {@link EGLDisplay} to attach the surface to. * @param surface The surface to wrap; must be a surface, surface texture or surface holder. */ @RequiresApi(17) - public static EGLSurface getEglSurfaceBt2020(EGLDisplay eglDisplay, Object surface) + public static EGLSurface getEglSurfaceRgba1010102(EGLDisplay eglDisplay, Object surface) throws GlException { return Api17.getEglSurface( eglDisplay, @@ -230,18 +226,19 @@ public final class GlUtil { * @param eglDisplay The {@link EGLDisplay} to attach the surface to. * @param width The width of the pixel buffer. * @param height The height of the pixel buffer. + * @param configAttributes EGL configuration attributes. Valid arguments include {@link + * #EGL_CONFIG_ATTRIBUTES_RGBA_8888} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_1010102}. */ @RequiresApi(17) - private static EGLSurface createPbufferSurface(EGLDisplay eglDisplay, int width, int height) - throws GlException { + private static EGLSurface createPbufferSurface( + EGLDisplay eglDisplay, int width, int height, int[] configAttributes) throws GlException { int[] pbufferAttributes = new int[] { EGL14.EGL_WIDTH, width, EGL14.EGL_HEIGHT, height, EGL14.EGL_NONE }; - return Api17.createEglPbufferSurface( - eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888, pbufferAttributes); + return Api17.createEglPbufferSurface(eglDisplay, configAttributes, pbufferAttributes); } /** @@ -255,7 +252,8 @@ public final class GlUtil { public static EGLSurface createPlaceholderEglSurface(EGLDisplay eglDisplay) throws GlException { return isSurfacelessContextExtensionSupported() ? EGL14.EGL_NO_SURFACE - : createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1); + : createPbufferSurface( + eglDisplay, /* width= */ 1, /* height= */ 1, EGL_CONFIG_ATTRIBUTES_RGBA_8888); } /** @@ -267,33 +265,24 @@ public final class GlUtil { @RequiresApi(17) public static void focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay) throws GlException { - EGLSurface eglSurface = createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1); + EGLSurface eglSurface = + createPbufferSurface( + eglDisplay, /* width= */ 1, /* height= */ 1, EGL_CONFIG_ATTRIBUTES_RGBA_8888); focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1); } /** - * Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer, for HDR rendering - * with Rec. 2020 color primaries. + * Creates and focuses a new RGBA 1010102 {@link EGLSurface} wrapping a 1x1 pixel buffer. * * @param eglContext The {@link EGLContext} to make current. * @param eglDisplay The {@link EGLDisplay} to attach the surface to. */ @RequiresApi(17) - public static void focusPlaceholderEglSurfaceBt2020(EGLContext eglContext, EGLDisplay eglDisplay) - throws GlException { - int[] pbufferAttributes = - new int[] { - EGL14.EGL_WIDTH, - /* width= */ 1, - EGL14.EGL_HEIGHT, - /* height= */ 1, - // TODO(b/227624622): Figure out if we can remove the EGL_GL_COLORSPACE_KHR item. - EGL_GL_COLORSPACE_KHR, - EGL14.EGL_NONE - }; + public static void focusPlaceholderEglSurfaceRgba1010102( + EGLContext eglContext, EGLDisplay eglDisplay) throws GlException { EGLSurface eglSurface = - Api17.createEglPbufferSurface( - eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_1010102, pbufferAttributes); + createPbufferSurface( + eglDisplay, /* width= */ 1, /* height= */ 1, EGL_CONFIG_ATTRIBUTES_RGBA_1010102); focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java index 1be2c7ea98..fc91bb3dc5 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java @@ -195,7 +195,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Nullable EGLSurface outputEglSurface = this.outputEglSurface; if (outputEglSurface == null) { // This means that outputSurfaceInfo changed. if (useHdr) { - outputEglSurface = GlUtil.getEglSurfaceBt2020(eglDisplay, outputSurfaceInfo.surface); + outputEglSurface = GlUtil.getEglSurfaceRgba1010102(eglDisplay, outputSurfaceInfo.surface); } else { outputEglSurface = GlUtil.getEglSurface(eglDisplay, outputSurfaceInfo.surface); } @@ -317,7 +317,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; if (eglSurface == null) { if (useHdr) { - eglSurface = GlUtil.getEglSurfaceBt2020(eglDisplay, surface); + eglSurface = GlUtil.getEglSurfaceRgba1010102(eglDisplay, surface); } else { eglSurface = GlUtil.getEglSurface(eglDisplay, surface); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffectsFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffectsFrameProcessor.java index d01d690c6e..a951a9bc2c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffectsFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffectsFrameProcessor.java @@ -121,7 +121,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; GlUtil.focusEglSurface( eglDisplay, eglContext, EGL14.EGL_NO_SURFACE, /* width= */ 1, /* height= */ 1); } else if (useHdr) { - GlUtil.focusPlaceholderEglSurfaceBt2020(eglContext, eglDisplay); + GlUtil.focusPlaceholderEglSurfaceRgba1010102(eglContext, eglDisplay); } else { GlUtil.focusPlaceholderEglSurface(eglContext, eglDisplay); } From 0a9f9007c66ca725959b3fe70311dd72dc086346 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 5 Jul 2022 23:21:59 +0000 Subject: [PATCH 02/16] Use mediaId as contentId if available This is to be consistent with what cast `QueueMediaItem` is doing. If a contentId is not available the contentUrl is used as the ID. #minor-release PiperOrigin-RevId: 459133323 --- .../cast/DefaultMediaItemConverter.java | 7 +++- .../cast/DefaultMediaItemConverterTest.java | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/libraries/cast/src/main/java/androidx/media3/cast/DefaultMediaItemConverter.java b/libraries/cast/src/main/java/androidx/media3/cast/DefaultMediaItemConverter.java index d4bcbd4b9d..97b90b2b4b 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/DefaultMediaItemConverter.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/DefaultMediaItemConverter.java @@ -128,11 +128,14 @@ public final class DefaultMediaItemConverter implements MediaItemConverter { if (mediaItem.mediaMetadata.trackNumber != null) { metadata.putInt(MediaMetadata.KEY_TRACK_NUMBER, mediaItem.mediaMetadata.trackNumber); } - + String contentUrl = mediaItem.localConfiguration.uri.toString(); + String contentId = + mediaItem.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) ? contentUrl : mediaItem.mediaId; MediaInfo mediaInfo = - new MediaInfo.Builder(mediaItem.localConfiguration.uri.toString()) + new MediaInfo.Builder(contentId) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(mediaItem.localConfiguration.mimeType) + .setContentUrl(contentUrl) .setMetadata(metadata) .setCustomData(getCustomData(mediaItem)) .build(); diff --git a/libraries/cast/src/test/java/androidx/media3/cast/DefaultMediaItemConverterTest.java b/libraries/cast/src/test/java/androidx/media3/cast/DefaultMediaItemConverterTest.java index 0a760043d3..10ac47a62e 100644 --- a/libraries/cast/src/test/java/androidx/media3/cast/DefaultMediaItemConverterTest.java +++ b/libraries/cast/src/test/java/androidx/media3/cast/DefaultMediaItemConverterTest.java @@ -50,6 +50,7 @@ public class DefaultMediaItemConverterTest { MediaItem.Builder builder = new MediaItem.Builder(); MediaItem item = builder + .setMediaId("fooBar") .setUri(Uri.parse("http://example.com")) .setMediaMetadata(MediaMetadata.EMPTY) .setMimeType(MimeTypes.APPLICATION_MPD) @@ -66,4 +67,45 @@ public class DefaultMediaItemConverterTest { assertThat(reconstructedItem).isEqualTo(item); } + + @Test + public void toMediaQueueItem_nonDefaultMediaId_usedAsContentId() { + MediaItem.Builder builder = new MediaItem.Builder(); + MediaItem item = + builder + .setMediaId("fooBar") + .setUri("http://example.com") + .setMimeType(MimeTypes.APPLICATION_MPD) + .build(); + + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); + MediaQueueItem queueItem = converter.toMediaQueueItem(item); + + assertThat(queueItem.getMedia().getContentId()).isEqualTo("fooBar"); + } + + @Test + public void toMediaQueueItem_defaultMediaId_uriAsContentId() { + DefaultMediaItemConverter converter = new DefaultMediaItemConverter(); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri("http://example.com") + .setMimeType(MimeTypes.APPLICATION_MPD) + .build(); + + MediaQueueItem queueItem = converter.toMediaQueueItem(mediaItem); + + assertThat(queueItem.getMedia().getContentId()).isEqualTo("http://example.com"); + + MediaItem secondMediaItem = + new MediaItem.Builder() + .setMediaId(MediaItem.DEFAULT_MEDIA_ID) + .setUri("http://example.com") + .setMimeType(MimeTypes.APPLICATION_MPD) + .build(); + + MediaQueueItem secondQueueItem = converter.toMediaQueueItem(secondMediaItem); + + assertThat(secondQueueItem.getMedia().getContentId()).isEqualTo("http://example.com"); + } } From 656eaf74d13d5c35b67ef16b21c6ddbdd1266fc8 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 6 Jul 2022 09:46:27 +0000 Subject: [PATCH 03/16] Exclude HEVC 10bit profile on Pixel 1. This profile is declared as supported although it isn't. Issue: google/ExoPlayer#10345 Issue: google/ExoPlayer#3537 #minor-release PiperOrigin-RevId: 459205512 --- .../exoplayer/mediacodec/MediaCodecInfo.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 a435e6c9ef..745cdc5474 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 @@ -317,7 +317,9 @@ public final class MediaCodecInfo { } for (CodecProfileLevel profileLevel : profileLevels) { - if (profileLevel.profile == profile && profileLevel.level >= level) { + if (profileLevel.profile == profile + && profileLevel.level >= level + && !needsProfileExcludedWorkaround(mimeType, profile)) { return true; } } @@ -831,4 +833,15 @@ public final class MediaCodecInfo { } return true; } + + /** + * Whether a profile is excluded from the list of supported profiles. This may happen when a + * device declares support for a profile it doesn't actually support. + */ + private static boolean needsProfileExcludedWorkaround(String mimeType, int profile) { + // See https://github.com/google/ExoPlayer/issues/3537 + return MimeTypes.VIDEO_H265.equals(mimeType) + && CodecProfileLevel.HEVCProfileMain10 == profile + && ("sailfish".equals(Util.DEVICE) || "marlin".equals(Util.DEVICE)); + } } From ab7747d9533c0ed1c5d096743741607b0e08c609 Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Wed, 6 Jul 2022 10:25:30 +0000 Subject: [PATCH 04/16] HDR: Throw error if attempting HDR editing under API 31. HDR editing is not supported under API 31 PiperOrigin-RevId: 459211106 --- .../main/java/androidx/media3/common/ColorInfo.java | 5 +++++ .../media3/transformer/TransformationException.java | 4 ++++ .../media3/transformer/TransformerVideoRenderer.java | 11 +++++++++++ .../transformer/VideoTranscodingSamplePipeline.java | 8 ++------ 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java b/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java index 6477706932..71b15c053f 100644 --- a/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java @@ -83,6 +83,11 @@ public final class ColorInfo implements Bundleable { } } + /** Returns whether the {@code ColorInfo} uses an HDR {@link C.ColorTransfer}. */ + public static boolean isHdr(@Nullable ColorInfo colorInfo) { + return colorInfo != null && colorInfo.colorTransfer != C.COLOR_TRANSFER_SDR; + } + /** * The color space of the video. Valid values are {@link C#COLOR_SPACE_BT601}, {@link * C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown. diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java index 803ab0d53f..d5fb4d0819 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java @@ -69,6 +69,7 @@ public final class TransformationException extends Exception { ERROR_CODE_ENCODER_INIT_FAILED, ERROR_CODE_ENCODING_FAILED, ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED, + ERROR_CODE_HDR_EDITING_UNSUPPORTED, ERROR_CODE_GL_INIT_FAILED, ERROR_CODE_GL_PROCESSING_FAILED, ERROR_CODE_MUXING_FAILED, @@ -151,6 +152,8 @@ public final class TransformationException extends Exception { * Codec.DecoderFactory encoders} available. */ public static final int ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED = 4003; + /** Caused by the encoder not supporting HDR formats. */ + public static final int ERROR_CODE_HDR_EDITING_UNSUPPORTED = 4004; // Video editing errors (5xxx). @@ -181,6 +184,7 @@ public final class TransformationException extends Exception { .put("ERROR_CODE_ENCODER_INIT_FAILED", ERROR_CODE_ENCODER_INIT_FAILED) .put("ERROR_CODE_ENCODING_FAILED", ERROR_CODE_ENCODING_FAILED) .put("ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED", ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED) + .put("ERROR_CODE_HDR_EDITING_UNSUPPORTED", ERROR_CODE_HDR_EDITING_UNSUPPORTED) .put("ERROR_CODE_GL_INIT_FAILED", ERROR_CODE_GL_INIT_FAILED) .put("ERROR_CODE_GL_PROCESSING_FAILED", ERROR_CODE_GL_PROCESSING_FAILED) .put("ERROR_CODE_MUXING_FAILED", ERROR_CODE_MUXING_FAILED) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java index a9ae8307b2..b52ea8e054 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java @@ -17,10 +17,12 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Util.SDK_INT; import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT; import android.content.Context; import androidx.media3.common.C; +import androidx.media3.common.ColorInfo; import androidx.media3.common.Format; import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.exoplayer.FormatHolder; @@ -91,6 +93,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; return false; } Format inputFormat = checkNotNull(formatHolder.format); + if (SDK_INT < 31 && ColorInfo.isHdr(inputFormat.colorInfo)) { + throw TransformationException.createForCodec( + new IllegalArgumentException("HDR editing not supported under API 31."), + /* isVideo= */ true, + /* isDecoder= */ false, + inputFormat, + /* mediaCodecName= */ null, + TransformationException.ERROR_CODE_HDR_EDITING_UNSUPPORTED); + } if (shouldPassthrough(inputFormat)) { samplePipeline = new PassthroughSamplePipeline(inputFormat, transformationRequest, fallbackListener); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java index ad943b297c..8ad1a1ba8c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java @@ -108,7 +108,8 @@ import org.checkerframework.dataflow.qual.Pure; boolean enableRequestSdrToneMapping = transformationRequest.enableRequestSdrToneMapping; // TODO(b/237674316): While HLG10 is correctly reported, HDR10 currently will be incorrectly // processed as SDR, because the inputFormat.colorInfo reports the wrong value. - boolean useHdr = transformationRequest.enableHdrEditing && isHdr(inputFormat.colorInfo); + boolean useHdr = + transformationRequest.enableHdrEditing && ColorInfo.isHdr(inputFormat.colorInfo); if (useHdr && !encoderWrapper.supportsHdr()) { // TODO(b/236316454): Also check whether GlEffectsFrameProcessor supports HDR, i.e., whether // EXT_YUV_target is supported. @@ -167,11 +168,6 @@ import org.checkerframework.dataflow.qual.Pure; maxPendingFrameCount = decoder.getMaxPendingFrameCount(); } - /** Whether this is a supported HDR format. */ - private static boolean isHdr(@Nullable ColorInfo colorInfo) { - return colorInfo != null && colorInfo.colorTransfer != C.COLOR_TRANSFER_SDR; - } - @Override @Nullable public DecoderInputBuffer dequeueInputBuffer() throws TransformationException { From 87adb88f57afb050a96a9c67dd9eb55fb3a6706c Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 6 Jul 2022 11:02:12 +0000 Subject: [PATCH 05/16] Fix incorrect link tags PiperOrigin-RevId: 459215618 --- .../DefaultMediaNotificationProvider.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java index 3605fec100..0059a9d99a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java +++ b/libraries/session/src/main/java/androidx/media3/session/DefaultMediaNotificationProvider.java @@ -72,10 +72,11 @@ import java.util.concurrent.ExecutionException; *

Custom commands

* * Custom actions are sent to the session under the hood. You can receive them by overriding the - * session callback method {@link MediaSession.Callback#onCustomCommand(MediaSession, ControllerInfo - * controller, SessionCommand, Bundle)}. This is useful because starting with Android 13, the System - * UI notification sends commands directly to the session. So handling the custom commands on the - * session level allows you to handle them at the same callback for all API levels. + * session callback method {@link MediaSession.Callback#onCustomCommand(MediaSession, + * MediaSession.ControllerInfo, SessionCommand, Bundle)}. This is useful because starting with + * Android 13, the System UI notification sends commands directly to the session. So handling the + * custom commands on the session level allows you to handle them at the same callback for all API + * levels. * *

Drawables

* @@ -230,10 +231,10 @@ public class DefaultMediaNotificationProvider implements MediaNotification.Provi *

To make the custom layout and commands work, you need to {@linkplain * MediaSession#setCustomLayout(List) set the custom layout of commands} and add the custom * commands to the available commands when a controller {@linkplain - * MediaSession.Callback#onConnect(MediaSession, ControllerInfo) connects to the session}. - * Controllers that connect after you called {@link MediaSession#setCustomLayout(List)} need the - * custom command set in {@link MediaSession.Callback#onPostConnect(MediaSession, ControllerInfo)} - * also. + * MediaSession.Callback#onConnect(MediaSession, MediaSession.ControllerInfo) connects to the + * session}. Controllers that connect after you called {@link MediaSession#setCustomLayout(List)} + * need the custom command set in {@link MediaSession.Callback#onPostConnect(MediaSession, + * MediaSession.ControllerInfo)} also. * * @param playerCommands The available player commands. * @param customLayout The {@linkplain MediaSession#setCustomLayout(List) custom layout of From 7078ce312d3650ef70dcd8ed236af88dc07b5333 Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Wed, 6 Jul 2022 18:11:11 +0000 Subject: [PATCH 06/16] HDR: Remove ColorInfo.SDR constant The SDR constant also specified a color space and range, in addition to C.COLOR_TRANSFER_SDR. However, it turns out that SDR videos may use different color space and range values, so following prior ExoPlayer conventions to have `null` mean "generic SDR" is preferable here. PiperOrigin-RevId: 459296746 --- .../src/main/java/androidx/media3/common/ColorInfo.java | 8 -------- .../transformer/VideoTranscodingSamplePipeline.java | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java b/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java index 71b15c053f..7021acfff0 100644 --- a/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java @@ -31,14 +31,6 @@ import org.checkerframework.dataflow.qual.Pure; /** Stores color info. */ @UnstableApi public final class ColorInfo implements Bundleable { - /** Standard Dynamic Range (SDR). */ - public static final ColorInfo SDR = - new ColorInfo( - C.COLOR_SPACE_BT709, - C.COLOR_RANGE_LIMITED, - C.COLOR_TRANSFER_SDR, - /* hdrStaticInfo= */ null); - /** * Returns the {@link C.ColorSpace} corresponding to the given ISO color primary code, as per * table A.7.21.1 in Rec. ITU-T T.832 (03/2009), or {@link Format#NO_VALUE} if no mapping can be diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java index 8ad1a1ba8c..8b3ebe6bb2 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java @@ -387,7 +387,7 @@ import org.checkerframework.dataflow.qual.Pure; transformationRequest.videoMimeType != null ? transformationRequest.videoMimeType : inputFormat.sampleMimeType) - .setColorInfo(fallbackToSdr ? ColorInfo.SDR : inputFormat.colorInfo) + .setColorInfo(fallbackToSdr ? null : inputFormat.colorInfo) .build(); encoder = From a83ab05aeceb6c99f3b7b19d6fedd20f317e0aa6 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 7 Jul 2022 10:22:56 +0000 Subject: [PATCH 07/16] Don't block AudioTrack when waiting for previous release We wait until a previous AudioTrack has been released before creating a new one. This is currently done with a thread block operation, which may cause ANRs in the extreme case when someone attempts to release the player while this is still blocked. The problem can be avoided by just returning false from DefaultAudioSink.handleBuffer to try again until the previous AudioTrack is released. Reproduction steps to force the issue: 1. Add Thread.sleep(10000); to the AudioTrack release thread. 2. Add this to the demo app: private int positionMs = 0; Handler handler = new Handler(); handler.post(new Runnable() { @Override public void run() { player.seekTo(positionMs++); if (positionMs == 10) { player.release(); } else { handler.postDelayed(this, 1000); } } 3. Observe Player release timeout exception. These steps can't be easily captured in a unit test as we can't artifically delay the AudioTrack release from the test. Issue: google/ExoPlayer#10057 PiperOrigin-RevId: 459468912 --- .../exoplayer/audio/DefaultAudioSink.java | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index c1a34adb68..b53d79c47e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -29,7 +29,6 @@ import android.media.AudioManager; import android.media.AudioTrack; import android.media.PlaybackParams; import android.media.metrics.LogSessionId; -import android.os.ConditionVariable; import android.os.Handler; import android.os.SystemClock; import android.util.Pair; @@ -44,6 +43,8 @@ import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.Clock; +import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -615,7 +616,8 @@ public final class DefaultAudioSink implements AudioSink { enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && builder.enableAudioTrackPlaybackParams; offloadMode = Util.SDK_INT >= 29 ? builder.offloadMode : OFFLOAD_MODE_DISABLED; audioTrackBufferSizeProvider = builder.audioTrackBufferSizeProvider; - releasingConditionVariable = new ConditionVariable(true); + releasingConditionVariable = new ConditionVariable(Clock.DEFAULT); + releasingConditionVariable.open(); audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener()); channelMappingAudioProcessor = new ChannelMappingAudioProcessor(); trimmingAudioProcessor = new TrimmingAudioProcessor(); @@ -840,13 +842,15 @@ public final class DefaultAudioSink implements AudioSink { } } - private void initializeAudioTrack() throws InitializationException { - // If we're asynchronously releasing a previous audio track then we block until it has been + private boolean initializeAudioTrack() throws InitializationException { + // If we're asynchronously releasing a previous audio track then we wait until it has been // released. This guarantees that we cannot end up in a state where we have multiple audio // track instances. Without this guarantee it would be possible, in extreme cases, to exhaust // the shared memory that's available for audio track buffers. This would in turn cause the // initialization of the audio track to fail. - releasingConditionVariable.block(); + if (!releasingConditionVariable.isOpen()) { + return false; + } audioTrack = buildAudioTrackWithRetry(); if (isOffloadedPlayback(audioTrack)) { @@ -874,6 +878,7 @@ public final class DefaultAudioSink implements AudioSink { } startMediaTimeUsNeedsInit = true; + return true; } @Override @@ -930,7 +935,10 @@ public final class DefaultAudioSink implements AudioSink { if (!isAudioTrackInitialized()) { try { - initializeAudioTrack(); + if (!initializeAudioTrack()) { + // Not yet ready for initialization of a new AudioTrack. + return false; + } } catch (InitializationException e) { if (e.isRecoverable) { throw e; // Do not delay the exception if it can be recovered at higher level. From cb87b7432f4c08a597d048114b5006d09de29da9 Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 7 Jul 2022 12:17:36 +0000 Subject: [PATCH 08/16] Add missing Nullable annotation PiperOrigin-RevId: 459485334 --- .../media3/exoplayer/video/MediaCodecVideoRenderer.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 073f60fa2b..6c091844a4 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 @@ -1572,7 +1572,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } if (haveUnknownDimensions) { Log.w(TAG, "Resolutions unknown. Codec max resolution: " + maxWidth + "x" + maxHeight); - Point codecMaxSize = getCodecMaxSize(codecInfo, format); + @Nullable Point codecMaxSize = getCodecMaxSize(codecInfo, format); if (codecMaxSize != null) { maxWidth = max(maxWidth, codecMaxSize.x); maxHeight = max(maxHeight, codecMaxSize.y); @@ -1600,8 +1600,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { * * @param codecInfo Information about the {@link MediaCodec} being configured. * @param format The {@link Format} for which the codec is being configured. - * @return The maximum video size to use, or null if the size of {@code format} should be used. + * @return The maximum video size to use, or {@code null} if the size of {@code format} should be + * used. */ + @Nullable private static Point getCodecMaxSize(MediaCodecInfo codecInfo, Format format) { boolean isVerticalVideo = format.height > format.width; int formatLongEdgePx = isVerticalVideo ? format.height : format.width; From 91f1777741149ecf67759da8eb68828e1d92624b Mon Sep 17 00:00:00 2001 From: samrobinson Date: Thu, 7 Jul 2022 12:54:02 +0000 Subject: [PATCH 09/16] Move Encoder quality API to VideoEncoderSettings. Some other minor nits and adjustments to the API logic. PiperOrigin-RevId: 459490431 --- .../transformer/DefaultEncoderFactory.java | 96 +++++++------------ .../DeviceMappedEncoderBitrateProvider.java | 8 +- .../transformer/EncoderBitrateProvider.java | 4 +- .../media3/transformer/EncoderSelector.java | 6 +- .../transformer/VideoEncoderSettings.java | 62 ++++++++---- 5 files changed, 88 insertions(+), 88 deletions(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java index d6ba06c05c..b609b1c023 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -56,7 +56,6 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { @Nullable private EncoderSelector encoderSelector; @Nullable private VideoEncoderSettings requestedVideoEncoderSettings; private boolean enableFallback; - private boolean automaticQualityAdjustment; /** Creates a new {@link Builder}. */ public Builder(Context context) { @@ -80,15 +79,9 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { *

Values in {@code requestedVideoEncoderSettings} may be ignored to improve encoding quality * and/or reduce failures. * - *

    - *
  • {@link VideoEncoderSettings#bitrate} is ignored if {@link - * Builder#setAutomaticQualityAdjustment(boolean)} is enabled and {@link - * VideoEncoderSettings#bitrateMode} is VBR. - *
  • {@link VideoEncoderSettings#profile} and {@link VideoEncoderSettings#level} are ignored - * for {@link MimeTypes#VIDEO_H264} - *
- * - *

Consider implementing {@link Codec.EncoderFactory} if such adjustments are unwanted. + *

{@link VideoEncoderSettings#profile} and {@link VideoEncoderSettings#level} are ignored + * for {@link MimeTypes#VIDEO_H264}. Consider implementing {@link Codec.EncoderFactory} if such + * adjustments are unwanted. * *

{@code requestedVideoEncoderSettings} should be handled with care because there is no * fallback support for it. For example, using incompatible {@link VideoEncoderSettings#profile} @@ -119,19 +112,8 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { return this; } - /** - * Sets whether to use automatic quality adjustment. - * - *

With this enabled, encoders are configured to output high quality video. - * - *

Default value is {@code false}. - */ - public Builder setAutomaticQualityAdjustment(boolean automaticQualityAdjustment) { - this.automaticQualityAdjustment = automaticQualityAdjustment; - return this; - } - /** Creates an instance of {@link DefaultEncoderFactory}, using defaults if values are unset. */ + @SuppressWarnings("deprecation") public DefaultEncoderFactory build() { if (encoderSelector == null) { encoderSelector = EncoderSelector.DEFAULT; @@ -140,11 +122,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { requestedVideoEncoderSettings = VideoEncoderSettings.DEFAULT; } return new DefaultEncoderFactory( - context, - encoderSelector, - requestedVideoEncoderSettings, - enableFallback, - automaticQualityAdjustment); + context, encoderSelector, requestedVideoEncoderSettings, enableFallback); } } @@ -152,7 +130,6 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { private final EncoderSelector videoEncoderSelector; private final VideoEncoderSettings requestedVideoEncoderSettings; private final boolean enableFallback; - private final boolean automaticQualityAdjustment; /** * @deprecated Use {@link Builder} instead. @@ -182,25 +159,10 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { EncoderSelector videoEncoderSelector, VideoEncoderSettings requestedVideoEncoderSettings, boolean enableFallback) { - this( - context, - videoEncoderSelector, - requestedVideoEncoderSettings, - enableFallback, - /* automaticQualityAdjustment= */ false); - } - - private DefaultEncoderFactory( - Context context, - EncoderSelector videoEncoderSelector, - VideoEncoderSettings requestedVideoEncoderSettings, - boolean enableFallback, - boolean automaticQualityAdjustment) { this.context = context; this.videoEncoderSelector = videoEncoderSelector; this.requestedVideoEncoderSettings = requestedVideoEncoderSettings; this.enableFallback = enableFallback; - this.automaticQualityAdjustment = automaticQualityAdjustment; } @Override @@ -276,11 +238,11 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, round(format.frameRate)); int bitrate; - if (automaticQualityAdjustment) { + + if (supportedVideoEncoderSettings.enableHighQualityTargeting) { bitrate = new DeviceMappedEncoderBitrateProvider() - .getBitrate( - encoderInfo.getName(), format.width, format.height, round(format.frameRate)); + .getBitrate(encoderInfo.getName(), format.width, format.height, format.frameRate); } else if (supportedVideoEncoderSettings.bitrate != VideoEncoderSettings.NO_VALUE) { bitrate = supportedVideoEncoderSettings.bitrate; } else { @@ -367,46 +329,54 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { return null; } - List encodersForMimeType = encoderSelector.selectEncoderInfos(mimeType); - if (encodersForMimeType.isEmpty()) { + ImmutableList filteredEncoderInfos = + encoderSelector.selectEncoderInfos(mimeType); + if (filteredEncoderInfos.isEmpty()) { return null; } + if (!enableFallback) { return new VideoEncoderQueryResult( - encodersForMimeType.get(0), requestedFormat, videoEncoderSettings); + filteredEncoderInfos.get(0), requestedFormat, videoEncoderSettings); } - ImmutableList filteredEncoders = + filteredEncoderInfos = filterEncodersByResolution( - encodersForMimeType, mimeType, requestedFormat.width, requestedFormat.height); - if (filteredEncoders.isEmpty()) { + filteredEncoderInfos, mimeType, requestedFormat.width, requestedFormat.height); + if (filteredEncoderInfos.isEmpty()) { return null; } // The supported resolution is the same for all remaining encoders. Size finalResolution = checkNotNull( EncoderUtil.getSupportedResolution( - filteredEncoders.get(0), mimeType, requestedFormat.width, requestedFormat.height)); + filteredEncoderInfos.get(0), + mimeType, + requestedFormat.width, + requestedFormat.height)); int requestedBitrate = videoEncoderSettings.bitrate != VideoEncoderSettings.NO_VALUE ? videoEncoderSettings.bitrate : getSuggestedBitrate( finalResolution.getWidth(), finalResolution.getHeight(), requestedFormat.frameRate); - filteredEncoders = filterEncodersByBitrate(filteredEncoders, mimeType, requestedBitrate); - if (filteredEncoders.isEmpty()) { + + filteredEncoderInfos = + filterEncodersByBitrate(filteredEncoderInfos, mimeType, requestedBitrate); + if (filteredEncoderInfos.isEmpty()) { return null; } - filteredEncoders = - filterEncodersByBitrateMode(filteredEncoders, mimeType, videoEncoderSettings.bitrateMode); - if (filteredEncoders.isEmpty()) { + filteredEncoderInfos = + filterEncodersByBitrateMode( + filteredEncoderInfos, mimeType, videoEncoderSettings.bitrateMode); + if (filteredEncoderInfos.isEmpty()) { return null; } - MediaCodecInfo pickedEncoder = filteredEncoders.get(0); + MediaCodecInfo pickedEncoderInfo = filteredEncoderInfos.get(0); int closestSupportedBitrate = - EncoderUtil.getSupportedBitrateRange(pickedEncoder, mimeType).clamp(requestedBitrate); + EncoderUtil.getSupportedBitrateRange(pickedEncoderInfo, mimeType).clamp(requestedBitrate); VideoEncoderSettings.Builder supportedEncodingSettingBuilder = videoEncoderSettings.buildUpon().setBitrate(closestSupportedBitrate); @@ -414,7 +384,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { || videoEncoderSettings.level == VideoEncoderSettings.NO_VALUE || videoEncoderSettings.level > EncoderUtil.findHighestSupportedEncodingLevel( - pickedEncoder, mimeType, videoEncoderSettings.profile)) { + pickedEncoderInfo, mimeType, videoEncoderSettings.profile)) { supportedEncodingSettingBuilder.setEncodingProfileLevel( VideoEncoderSettings.NO_VALUE, VideoEncoderSettings.NO_VALUE); } @@ -428,7 +398,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { .setAverageBitrate(closestSupportedBitrate) .build(); return new VideoEncoderQueryResult( - pickedEncoder, supportedEncoderFormat, supportedEncodingSettingBuilder.build()); + pickedEncoderInfo, supportedEncoderFormat, supportedEncodingSettingBuilder.build()); } /** Returns a list of encoders that support the requested resolution most closely. */ @@ -642,7 +612,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { * */ private static int getSuggestedBitrate(int width, int height, float frameRate) { - // TODO(b/210591626) Implement bitrate estimation. + // TODO(b/210591626) Refactor into a BitrateProvider. // Assume medium motion factor. // 1080p60 -> 16.6Mbps, 720p30 -> 3.7Mbps. return (int) (width * height * frameRate * 0.07 * 2); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DeviceMappedEncoderBitrateProvider.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DeviceMappedEncoderBitrateProvider.java index d40aa86e63..ec38576bff 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DeviceMappedEncoderBitrateProvider.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DeviceMappedEncoderBitrateProvider.java @@ -25,10 +25,14 @@ import androidx.media3.common.util.Util; public class DeviceMappedEncoderBitrateProvider implements EncoderBitrateProvider { @Override - public int getBitrate(String encoderName, int width, int height, int frameRate) { + public int getBitrate(String encoderName, int width, int height, float frameRate) { double bitrateMultiplier = getBitrateMultiplierFromMapping( - encoderName, Util.SDK_INT, Build.MODEL, "" + width + "x" + height, frameRate); + encoderName, + Util.SDK_INT, + Build.MODEL, + "" + width + "x" + height, + Math.round(frameRate)); return (int) (width * height * frameRate * bitrateMultiplier); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderBitrateProvider.java b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderBitrateProvider.java index 02205aeb3b..59eed209c4 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderBitrateProvider.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderBitrateProvider.java @@ -26,11 +26,11 @@ public interface EncoderBitrateProvider { /** * Returns a recommended bitrate that the encoder should target. * - * @param encoderName The name of the encoder, see {@link MediaCodecInfo#getName()} + * @param encoderName The name of the encoder, see {@link MediaCodecInfo#getName()}. * @param width The output width of the video after encoding. * @param height The output height of the video after encoding. * @param frameRate The expected output frame rate of the video after encoding. * @return The bitrate the encoder should target. */ - int getBitrate(String encoderName, int width, int height, int frameRate); + int getBitrate(String encoderName, int width, int height, float frameRate); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java index c68719a1a4..0f1c7b445d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java @@ -20,7 +20,7 @@ import android.media.MediaCodec; import android.media.MediaCodecInfo; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.UnstableApi; -import java.util.List; +import com.google.common.collect.ImmutableList; /** Selector of {@link MediaCodec} encoder instances. */ @UnstableApi @@ -37,8 +37,8 @@ public interface EncoderSelector { * order. * * @param mimeType The {@linkplain MimeTypes MIME type} for which an encoder is required. - * @return An unmodifiable list of {@linkplain MediaCodecInfo encoders} that support the {@code + * @return An immutable list of {@linkplain MediaCodecInfo encoders} that support the {@code * mimeType}. The list may be empty. */ - List selectEncoderInfos(String mimeType); + ImmutableList selectEncoderInfos(String mimeType); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java index 90db319e17..7c043878fd 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java @@ -16,7 +16,10 @@ package androidx.media3.transformer; +import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR; +import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR; import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkState; import static java.lang.annotation.ElementType.TYPE_USE; import android.annotation.SuppressLint; @@ -48,14 +51,11 @@ public final class VideoEncoderSettings { public static final VideoEncoderSettings DEFAULT = new Builder().build(); /** - * The allowed values for {@code bitrateMode}, one of + * The allowed values for {@code bitrateMode}. * *

    - *
  • Constant quality: {@link MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_CQ}. *
  • Variable bitrate: {@link MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_VBR}. *
  • Constant bitrate: {@link MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_CBR}. - *
  • Constant bitrate with frame drops: {@link - * MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_CBR_FD}, available from API31. *
*/ @SuppressLint("InlinedApi") @@ -63,10 +63,8 @@ public final class VideoEncoderSettings { @Retention(RetentionPolicy.SOURCE) @Target(TYPE_USE) @IntDef({ - MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CQ, - MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR, - MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR, - MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR_FD + BITRATE_MODE_VBR, + BITRATE_MODE_CBR, }) public @interface BitrateMode {} @@ -80,11 +78,12 @@ public final class VideoEncoderSettings { private float iFrameIntervalSeconds; private int operatingRate; private int priority; + private boolean enableHighQualityTargeting; /** Creates a new instance. */ public Builder() { this.bitrate = NO_VALUE; - this.bitrateMode = MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR; + this.bitrateMode = BITRATE_MODE_VBR; this.profile = NO_VALUE; this.level = NO_VALUE; this.colorProfile = DEFAULT_COLOR_PROFILE; @@ -102,11 +101,14 @@ public final class VideoEncoderSettings { this.iFrameIntervalSeconds = videoEncoderSettings.iFrameIntervalSeconds; this.operatingRate = videoEncoderSettings.operatingRate; this.priority = videoEncoderSettings.priority; + this.enableHighQualityTargeting = videoEncoderSettings.enableHighQualityTargeting; } /** * Sets {@link VideoEncoderSettings#bitrate}. The default value is {@link #NO_VALUE}. * + *

Can not be set if enabling {@link #setEnableHighQualityTargeting(boolean)}. + * * @param bitrate The {@link VideoEncoderSettings#bitrate}. * @return This builder. */ @@ -119,16 +121,13 @@ public final class VideoEncoderSettings { * Sets {@link VideoEncoderSettings#bitrateMode}. The default value is {@code * MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR}. * - *

Only {@link MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_VBR} and {@link - * MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_CBR} are allowed. + *

Value must be in {@link BitrateMode}. * * @param bitrateMode The {@link VideoEncoderSettings#bitrateMode}. * @return This builder. */ public Builder setBitrateMode(@BitrateMode int bitrateMode) { - checkArgument( - bitrateMode == MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR - || bitrateMode == MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR); + checkArgument(bitrateMode == BITRATE_MODE_VBR || bitrateMode == BITRATE_MODE_CBR); this.bitrateMode = bitrateMode; return this; } @@ -194,8 +193,28 @@ public final class VideoEncoderSettings { return this; } + /** + * Sets whether to enable automatic adjustment of the bitrate to target a high quality encoding. + * + *

Default value is {@code false}. + * + *

Requires {@link android.media.MediaCodecInfo.EncoderCapabilities#BITRATE_MODE_VBR}. + * + *

Can not be enabled alongside setting a custom bitrate with {@link #setBitrate(int)}. + */ + public Builder setEnableHighQualityTargeting(boolean enableHighQualityTargeting) { + this.enableHighQualityTargeting = enableHighQualityTargeting; + return this; + } + /** Builds the instance. */ public VideoEncoderSettings build() { + checkState( + !enableHighQualityTargeting || bitrate == NO_VALUE, + "Bitrate can not be set if enabling high quality targeting."); + checkState( + !enableHighQualityTargeting || bitrateMode == BITRATE_MODE_VBR, + "Bitrate mode must be VBR if enabling high quality targeting."); return new VideoEncoderSettings( bitrate, bitrateMode, @@ -204,13 +223,14 @@ public final class VideoEncoderSettings { colorProfile, iFrameIntervalSeconds, operatingRate, - priority); + priority, + enableHighQualityTargeting); } } /** The encoding bitrate. */ public final int bitrate; - /** One of {@linkplain BitrateMode the allowed modes}. */ + /** One of {@linkplain BitrateMode}. */ public final @BitrateMode int bitrateMode; /** The encoding profile. */ public final int profile; @@ -224,6 +244,8 @@ public final class VideoEncoderSettings { public final int operatingRate; /** The encoder {@link MediaFormat#KEY_PRIORITY priority}. */ public final int priority; + /** Whether the encoder should automatically set the bitrate to target a high quality encoding. */ + public final boolean enableHighQualityTargeting; private VideoEncoderSettings( int bitrate, @@ -233,7 +255,8 @@ public final class VideoEncoderSettings { int colorProfile, float iFrameIntervalSeconds, int operatingRate, - int priority) { + int priority, + boolean enableHighQualityTargeting) { this.bitrate = bitrate; this.bitrateMode = bitrateMode; this.profile = profile; @@ -242,6 +265,7 @@ public final class VideoEncoderSettings { this.iFrameIntervalSeconds = iFrameIntervalSeconds; this.operatingRate = operatingRate; this.priority = priority; + this.enableHighQualityTargeting = enableHighQualityTargeting; } /** @@ -267,7 +291,8 @@ public final class VideoEncoderSettings { && colorProfile == that.colorProfile && iFrameIntervalSeconds == that.iFrameIntervalSeconds && operatingRate == that.operatingRate - && priority == that.priority; + && priority == that.priority + && enableHighQualityTargeting == that.enableHighQualityTargeting; } @Override @@ -281,6 +306,7 @@ public final class VideoEncoderSettings { result = 31 * result + Float.floatToIntBits(iFrameIntervalSeconds); result = 31 * result + operatingRate; result = 31 * result + priority; + result = 31 * result + (enableHighQualityTargeting ? 1 : 0); return result; } } From 05e728a31eb85b82610a5eb83622ee2670582a26 Mon Sep 17 00:00:00 2001 From: rohks Date: Thu, 7 Jul 2022 13:04:29 +0000 Subject: [PATCH 10/16] Add tests for extracting MP4 with large bitrates Also added the test to `MP4PlaybackTest`. PiperOrigin-RevId: 459492188 --- .../exoplayer/e2etest/Mp4PlaybackTest.java | 1 + .../mp4/FragmentedMp4ExtractorTest.java | 9 + ...ample_fragmented_large_bitrates.mp4.0.dump | 339 ++++++++++++++++++ ...ample_fragmented_large_bitrates.mp4.1.dump | 279 ++++++++++++++ ...ample_fragmented_large_bitrates.mp4.2.dump | 219 +++++++++++ ...ample_fragmented_large_bitrates.mp4.3.dump | 159 ++++++++ ...ted_large_bitrates.mp4.unknown_length.dump | 339 ++++++++++++++++++ .../mp4/sample_fragmented_large_bitrates.mp4 | Bin 0 -> 106099 bytes .../sample_fragmented_large_bitrates.mp4.dump | 82 +++++ 9 files changed, 1427 insertions(+) create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump create mode 100644 libraries/test_data/src/test/assets/media/mp4/sample_fragmented_large_bitrates.mp4 create mode 100644 libraries/test_data/src/test/assets/playbackdumps/mp4/sample_fragmented_large_bitrates.mp4.dump diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java index be3ccd914b..b7ad234f18 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/Mp4PlaybackTest.java @@ -57,6 +57,7 @@ public class Mp4PlaybackTest { "sample_eac3joc.mp4", "sample_fragmented.mp4", "sample_fragmented_seekable.mp4", + "sample_fragmented_large_bitrates.mp4", "sample_fragmented_sei.mp4", "sample_mdat_too_long.mp4", "sample.mp4", diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java index 07c663d426..269ac4291c 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/FragmentedMp4ExtractorTest.java @@ -122,6 +122,15 @@ public final class FragmentedMp4ExtractorTest { simulationConfig); } + /** https://github.com/google/ExoPlayer/issues/10381 */ + @Test + public void sampleWithLargeBitrates() throws Exception { + ExtractorAsserts.assertBehavior( + getExtractorFactory(ImmutableList.of()), + "media/mp4/sample_fragmented_large_bitrates.mp4", + simulationConfig); + } + private static ExtractorFactory getExtractorFactory(final List closedCaptionFormats) { return () -> new FragmentedMp4Extractor( diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump new file mode 100644 index 0000000000..5b9a721cb6 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.0.dump @@ -0,0 +1,339 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + total output bytes = 85933 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + width = 1080 + height = 720 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 66733 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 200200 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 133466 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100100 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166833 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 333666 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266933 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233566 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 300300 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 467133 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 400400 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367033 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433766 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 600600 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533866 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500500 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 567233 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 734066 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 667333 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633966 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700700 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 867533 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800800 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767433 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 834166 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1001000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 934266 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900900 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967633 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1034366 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + total output bytes = 18257 + sample count = 46 + format 0: + averageBitrate = 2147483647 + peakBitrate = 2147483647 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A + sample 0: + time = 0 + flags = 1 + data = length 18, hash 96519432 + sample 1: + time = 23219 + flags = 1 + data = length 4, hash EE9DF + sample 2: + time = 46439 + flags = 1 + data = length 4, hash EEDBF + sample 3: + time = 69659 + flags = 1 + data = length 157, hash E2F078F4 + sample 4: + time = 92879 + flags = 1 + data = length 371, hash B9471F94 + sample 5: + time = 116099 + flags = 1 + data = length 373, hash 2AB265CB + sample 6: + time = 139319 + flags = 1 + data = length 402, hash 1295477C + sample 7: + time = 162539 + flags = 1 + data = length 455, hash 2D8146C8 + sample 8: + time = 185759 + flags = 1 + data = length 434, hash F2C5D287 + sample 9: + time = 208979 + flags = 1 + data = length 450, hash 84143FCD + sample 10: + time = 232199 + flags = 1 + data = length 429, hash EF769D50 + sample 11: + time = 255419 + flags = 1 + data = length 450, hash EC3DE692 + sample 12: + time = 278639 + flags = 1 + data = length 447, hash 3E519E13 + sample 13: + time = 301859 + flags = 1 + data = length 457, hash 1E4F23A0 + sample 14: + time = 325079 + flags = 1 + data = length 447, hash A439EA97 + sample 15: + time = 348299 + flags = 1 + data = length 456, hash 1E9034C6 + sample 16: + time = 371519 + flags = 1 + data = length 398, hash 99DB7345 + sample 17: + time = 394739 + flags = 1 + data = length 474, hash 3F05F10A + sample 18: + time = 417959 + flags = 1 + data = length 416, hash C105EE09 + sample 19: + time = 441179 + flags = 1 + data = length 454, hash 5FDBE458 + sample 20: + time = 464399 + flags = 1 + data = length 438, hash 41A93AC3 + sample 21: + time = 487619 + flags = 1 + data = length 443, hash 10FDA652 + sample 22: + time = 510839 + flags = 1 + data = length 412, hash 1F791E25 + sample 23: + time = 534058 + flags = 1 + data = length 482, hash A6D983D + sample 24: + time = 557278 + flags = 1 + data = length 386, hash BED7392F + sample 25: + time = 580498 + flags = 1 + data = length 463, hash 5309F8C9 + sample 26: + time = 603718 + flags = 1 + data = length 394, hash 21C7321F + sample 27: + time = 626938 + flags = 1 + data = length 489, hash 71B4730D + sample 28: + time = 650158 + flags = 1 + data = length 403, hash D9C6DE89 + sample 29: + time = 673378 + flags = 1 + data = length 447, hash 9B14B73B + sample 30: + time = 696598 + flags = 1 + data = length 439, hash 4760D35B + sample 31: + time = 719818 + flags = 1 + data = length 463, hash 1601F88D + sample 32: + time = 743038 + flags = 1 + data = length 423, hash D4AE6773 + sample 33: + time = 766258 + flags = 1 + data = length 497, hash A3C674D3 + sample 34: + time = 789478 + flags = 1 + data = length 419, hash D3734A1F + sample 35: + time = 812698 + flags = 1 + data = length 474, hash DFB41F9 + sample 36: + time = 835918 + flags = 1 + data = length 413, hash 53E7CB9F + sample 37: + time = 859138 + flags = 1 + data = length 445, hash D15B0E39 + sample 38: + time = 882358 + flags = 1 + data = length 453, hash 77ED81E4 + sample 39: + time = 905578 + flags = 1 + data = length 545, hash 3321AEB9 + sample 40: + time = 928798 + flags = 1 + data = length 317, hash F557D0E + sample 41: + time = 952018 + flags = 1 + data = length 537, hash ED58CF7B + sample 42: + time = 975238 + flags = 1 + data = length 458, hash 51CDAA10 + sample 43: + time = 998458 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 44: + time = 1021678 + flags = 1 + data = length 446, hash D6735B8A + sample 45: + time = 1044897 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump new file mode 100644 index 0000000000..53cb776780 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.1.dump @@ -0,0 +1,279 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + total output bytes = 85933 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + width = 1080 + height = 720 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 66733 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 200200 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 133466 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100100 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166833 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 333666 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266933 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233566 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 300300 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 467133 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 400400 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367033 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433766 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 600600 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533866 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500500 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 567233 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 734066 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 667333 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633966 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700700 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 867533 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800800 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767433 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 834166 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1001000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 934266 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900900 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967633 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1034366 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + total output bytes = 13359 + sample count = 31 + format 0: + averageBitrate = 2147483647 + peakBitrate = 2147483647 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A + sample 0: + time = 348299 + flags = 1 + data = length 456, hash 1E9034C6 + sample 1: + time = 371519 + flags = 1 + data = length 398, hash 99DB7345 + sample 2: + time = 394739 + flags = 1 + data = length 474, hash 3F05F10A + sample 3: + time = 417959 + flags = 1 + data = length 416, hash C105EE09 + sample 4: + time = 441179 + flags = 1 + data = length 454, hash 5FDBE458 + sample 5: + time = 464399 + flags = 1 + data = length 438, hash 41A93AC3 + sample 6: + time = 487619 + flags = 1 + data = length 443, hash 10FDA652 + sample 7: + time = 510839 + flags = 1 + data = length 412, hash 1F791E25 + sample 8: + time = 534058 + flags = 1 + data = length 482, hash A6D983D + sample 9: + time = 557278 + flags = 1 + data = length 386, hash BED7392F + sample 10: + time = 580498 + flags = 1 + data = length 463, hash 5309F8C9 + sample 11: + time = 603718 + flags = 1 + data = length 394, hash 21C7321F + sample 12: + time = 626938 + flags = 1 + data = length 489, hash 71B4730D + sample 13: + time = 650158 + flags = 1 + data = length 403, hash D9C6DE89 + sample 14: + time = 673378 + flags = 1 + data = length 447, hash 9B14B73B + sample 15: + time = 696598 + flags = 1 + data = length 439, hash 4760D35B + sample 16: + time = 719818 + flags = 1 + data = length 463, hash 1601F88D + sample 17: + time = 743038 + flags = 1 + data = length 423, hash D4AE6773 + sample 18: + time = 766258 + flags = 1 + data = length 497, hash A3C674D3 + sample 19: + time = 789478 + flags = 1 + data = length 419, hash D3734A1F + sample 20: + time = 812698 + flags = 1 + data = length 474, hash DFB41F9 + sample 21: + time = 835918 + flags = 1 + data = length 413, hash 53E7CB9F + sample 22: + time = 859138 + flags = 1 + data = length 445, hash D15B0E39 + sample 23: + time = 882358 + flags = 1 + data = length 453, hash 77ED81E4 + sample 24: + time = 905578 + flags = 1 + data = length 545, hash 3321AEB9 + sample 25: + time = 928798 + flags = 1 + data = length 317, hash F557D0E + sample 26: + time = 952018 + flags = 1 + data = length 537, hash ED58CF7B + sample 27: + time = 975238 + flags = 1 + data = length 458, hash 51CDAA10 + sample 28: + time = 998458 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 29: + time = 1021678 + flags = 1 + data = length 446, hash D6735B8A + sample 30: + time = 1044897 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump new file mode 100644 index 0000000000..ecb83ddeea --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.2.dump @@ -0,0 +1,219 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + total output bytes = 85933 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + width = 1080 + height = 720 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 66733 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 200200 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 133466 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100100 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166833 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 333666 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266933 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233566 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 300300 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 467133 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 400400 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367033 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433766 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 600600 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533866 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500500 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 567233 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 734066 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 667333 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633966 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700700 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 867533 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800800 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767433 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 834166 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1001000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 934266 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900900 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967633 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1034366 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + total output bytes = 6804 + sample count = 16 + format 0: + averageBitrate = 2147483647 + peakBitrate = 2147483647 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A + sample 0: + time = 696598 + flags = 1 + data = length 439, hash 4760D35B + sample 1: + time = 719818 + flags = 1 + data = length 463, hash 1601F88D + sample 2: + time = 743038 + flags = 1 + data = length 423, hash D4AE6773 + sample 3: + time = 766258 + flags = 1 + data = length 497, hash A3C674D3 + sample 4: + time = 789478 + flags = 1 + data = length 419, hash D3734A1F + sample 5: + time = 812698 + flags = 1 + data = length 474, hash DFB41F9 + sample 6: + time = 835918 + flags = 1 + data = length 413, hash 53E7CB9F + sample 7: + time = 859138 + flags = 1 + data = length 445, hash D15B0E39 + sample 8: + time = 882358 + flags = 1 + data = length 453, hash 77ED81E4 + sample 9: + time = 905578 + flags = 1 + data = length 545, hash 3321AEB9 + sample 10: + time = 928798 + flags = 1 + data = length 317, hash F557D0E + sample 11: + time = 952018 + flags = 1 + data = length 537, hash ED58CF7B + sample 12: + time = 975238 + flags = 1 + data = length 458, hash 51CDAA10 + sample 13: + time = 998458 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 14: + time = 1021678 + flags = 1 + data = length 446, hash D6735B8A + sample 15: + time = 1044897 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump new file mode 100644 index 0000000000..c049809940 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.3.dump @@ -0,0 +1,159 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + total output bytes = 85933 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + width = 1080 + height = 720 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 66733 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 200200 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 133466 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100100 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166833 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 333666 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266933 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233566 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 300300 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 467133 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 400400 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367033 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433766 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 600600 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533866 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500500 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 567233 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 734066 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 667333 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633966 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700700 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 867533 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800800 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767433 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 834166 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1001000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 934266 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900900 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967633 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1034366 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + total output bytes = 10 + sample count = 1 + format 0: + averageBitrate = 2147483647 + peakBitrate = 2147483647 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A + sample 0: + time = 1044897 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump new file mode 100644 index 0000000000..5b9a721cb6 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_large_bitrates.mp4.unknown_length.dump @@ -0,0 +1,339 @@ +seekMap: + isSeekable = true + duration = 1067733 + getPosition(0) = [[timeUs=66733, position=1325]] + getPosition(1) = [[timeUs=66733, position=1325]] + getPosition(533866) = [[timeUs=66733, position=1325]] + getPosition(1067733) = [[timeUs=66733, position=1325]] +numberOfTracks = 2 +track 0: + total output bytes = 85933 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/avc + codecs = avc1.64001F + width = 1080 + height = 720 + initializationData: + data = length 29, hash 4746B5D9 + data = length 10, hash 7A0D0F2B + sample 0: + time = 66733 + flags = 1 + data = length 38070, hash B58E1AEE + sample 1: + time = 200200 + flags = 0 + data = length 8340, hash 8AC449FF + sample 2: + time = 133466 + flags = 0 + data = length 1295, hash C0DA5090 + sample 3: + time = 100100 + flags = 0 + data = length 469, hash D6E0A200 + sample 4: + time = 166833 + flags = 0 + data = length 564, hash E5F56C5B + sample 5: + time = 333666 + flags = 0 + data = length 6075, hash 8756E49E + sample 6: + time = 266933 + flags = 0 + data = length 847, hash DCC2B618 + sample 7: + time = 233566 + flags = 0 + data = length 455, hash B9CCE047 + sample 8: + time = 300300 + flags = 0 + data = length 467, hash 69806D94 + sample 9: + time = 467133 + flags = 0 + data = length 4549, hash 3944F501 + sample 10: + time = 400400 + flags = 0 + data = length 1087, hash 491BF106 + sample 11: + time = 367033 + flags = 0 + data = length 380, hash 5FED016A + sample 12: + time = 433766 + flags = 0 + data = length 455, hash 8A0610 + sample 13: + time = 600600 + flags = 0 + data = length 5190, hash B9031D8 + sample 14: + time = 533866 + flags = 0 + data = length 1071, hash 684E7DC8 + sample 15: + time = 500500 + flags = 0 + data = length 653, hash 8494F326 + sample 16: + time = 567233 + flags = 0 + data = length 485, hash 2CCC85F4 + sample 17: + time = 734066 + flags = 0 + data = length 4884, hash D16B6A96 + sample 18: + time = 667333 + flags = 0 + data = length 997, hash 164FF210 + sample 19: + time = 633966 + flags = 0 + data = length 640, hash F664125B + sample 20: + time = 700700 + flags = 0 + data = length 491, hash B5930C7C + sample 21: + time = 867533 + flags = 0 + data = length 2989, hash 92CF4FCF + sample 22: + time = 800800 + flags = 0 + data = length 838, hash 294A3451 + sample 23: + time = 767433 + flags = 0 + data = length 544, hash FCCE2DE6 + sample 24: + time = 834166 + flags = 0 + data = length 329, hash A654FFA1 + sample 25: + time = 1001000 + flags = 0 + data = length 1517, hash 5F7EBF8B + sample 26: + time = 934266 + flags = 0 + data = length 803, hash 7A5C4C1D + sample 27: + time = 900900 + flags = 0 + data = length 415, hash B31BBC3B + sample 28: + time = 967633 + flags = 0 + data = length 415, hash 850DFEA3 + sample 29: + time = 1034366 + flags = 0 + data = length 619, hash AB5E56CA +track 1: + total output bytes = 18257 + sample count = 46 + format 0: + averageBitrate = 2147483647 + peakBitrate = 2147483647 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + channelCount = 1 + sampleRate = 44100 + language = und + initializationData: + data = length 5, hash 2B7623A + sample 0: + time = 0 + flags = 1 + data = length 18, hash 96519432 + sample 1: + time = 23219 + flags = 1 + data = length 4, hash EE9DF + sample 2: + time = 46439 + flags = 1 + data = length 4, hash EEDBF + sample 3: + time = 69659 + flags = 1 + data = length 157, hash E2F078F4 + sample 4: + time = 92879 + flags = 1 + data = length 371, hash B9471F94 + sample 5: + time = 116099 + flags = 1 + data = length 373, hash 2AB265CB + sample 6: + time = 139319 + flags = 1 + data = length 402, hash 1295477C + sample 7: + time = 162539 + flags = 1 + data = length 455, hash 2D8146C8 + sample 8: + time = 185759 + flags = 1 + data = length 434, hash F2C5D287 + sample 9: + time = 208979 + flags = 1 + data = length 450, hash 84143FCD + sample 10: + time = 232199 + flags = 1 + data = length 429, hash EF769D50 + sample 11: + time = 255419 + flags = 1 + data = length 450, hash EC3DE692 + sample 12: + time = 278639 + flags = 1 + data = length 447, hash 3E519E13 + sample 13: + time = 301859 + flags = 1 + data = length 457, hash 1E4F23A0 + sample 14: + time = 325079 + flags = 1 + data = length 447, hash A439EA97 + sample 15: + time = 348299 + flags = 1 + data = length 456, hash 1E9034C6 + sample 16: + time = 371519 + flags = 1 + data = length 398, hash 99DB7345 + sample 17: + time = 394739 + flags = 1 + data = length 474, hash 3F05F10A + sample 18: + time = 417959 + flags = 1 + data = length 416, hash C105EE09 + sample 19: + time = 441179 + flags = 1 + data = length 454, hash 5FDBE458 + sample 20: + time = 464399 + flags = 1 + data = length 438, hash 41A93AC3 + sample 21: + time = 487619 + flags = 1 + data = length 443, hash 10FDA652 + sample 22: + time = 510839 + flags = 1 + data = length 412, hash 1F791E25 + sample 23: + time = 534058 + flags = 1 + data = length 482, hash A6D983D + sample 24: + time = 557278 + flags = 1 + data = length 386, hash BED7392F + sample 25: + time = 580498 + flags = 1 + data = length 463, hash 5309F8C9 + sample 26: + time = 603718 + flags = 1 + data = length 394, hash 21C7321F + sample 27: + time = 626938 + flags = 1 + data = length 489, hash 71B4730D + sample 28: + time = 650158 + flags = 1 + data = length 403, hash D9C6DE89 + sample 29: + time = 673378 + flags = 1 + data = length 447, hash 9B14B73B + sample 30: + time = 696598 + flags = 1 + data = length 439, hash 4760D35B + sample 31: + time = 719818 + flags = 1 + data = length 463, hash 1601F88D + sample 32: + time = 743038 + flags = 1 + data = length 423, hash D4AE6773 + sample 33: + time = 766258 + flags = 1 + data = length 497, hash A3C674D3 + sample 34: + time = 789478 + flags = 1 + data = length 419, hash D3734A1F + sample 35: + time = 812698 + flags = 1 + data = length 474, hash DFB41F9 + sample 36: + time = 835918 + flags = 1 + data = length 413, hash 53E7CB9F + sample 37: + time = 859138 + flags = 1 + data = length 445, hash D15B0E39 + sample 38: + time = 882358 + flags = 1 + data = length 453, hash 77ED81E4 + sample 39: + time = 905578 + flags = 1 + data = length 545, hash 3321AEB9 + sample 40: + time = 928798 + flags = 1 + data = length 317, hash F557D0E + sample 41: + time = 952018 + flags = 1 + data = length 537, hash ED58CF7B + sample 42: + time = 975238 + flags = 1 + data = length 458, hash 51CDAA10 + sample 43: + time = 998458 + flags = 1 + data = length 465, hash CBA1EFD7 + sample 44: + time = 1021678 + flags = 1 + data = length 446, hash D6735B8A + sample 45: + time = 1044897 + flags = 1 + data = length 10, hash A453EEBE +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_large_bitrates.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_fragmented_large_bitrates.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..39fd4c18cf250d002c505ec71e22e7894404f2b9 GIT binary patch literal 106099 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 c*MGgbqG)!!t$#GOCpEwT Date: Wed, 13 Jul 2022 17:43:03 +0530 Subject: [PATCH 11/16] Update H263 Reader to handle missing frames/fragments. Change-Id: I43dfbabcbe686c31cb54e6b95688af1fa35a3d24 --- .../exoplayer/rtsp/reader/RtpH263Reader.java | 65 ++++++++++++++----- 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java index 4aedc65aad..147887e665 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java @@ -15,6 +15,7 @@ */ package androidx.media3.exoplayer.rtsp.reader; +import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; import androidx.media3.common.C; @@ -61,12 +62,22 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private boolean isKeyFrame; private boolean isOutputFormatSet; private long startTimeOffsetUs; + private long sampleTimeUsOfFragmentedSample; + /** + * Whether the first packet of a H263 frame is received, it mark the start of a H263 partition. A + * H263 frame can be split into multiple RTP packets. + */ + private boolean gotFirstPacketOfH263Frame; /** Creates an instance. */ public RtpH263Reader(RtpPayloadFormat payloadFormat) { this.payloadFormat = payloadFormat; firstReceivedTimestamp = C.TIME_UNSET; previousSequenceNumber = C.INDEX_UNSET; + isKeyFrame = false; + fragmentedSampleSizeBytes = 0; + sampleTimeUsOfFragmentedSample = C.TIME_UNSET; + gotFirstPacketOfH263Frame = false; } @Override @@ -76,7 +87,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @Override - public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {} + public void onReceivingFirstPacket(long timestamp, int sequenceNumber) { + checkState(firstReceivedTimestamp == C.TIME_UNSET); + firstReceivedTimestamp = timestamp; + } @Override public void consume( @@ -103,6 +117,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } if (pBitIsSet) { + if (gotFirstPacketOfH263Frame && fragmentedSampleSizeBytes > 0) { + // Received new H263 fragment, output data of previous fragment to decoder. + outputSampleMetadataForFragmentedPackets(); + } + gotFirstPacketOfH263Frame = true; + int payloadStartCode = data.peekUnsignedByte() & 0xFC; // Packets that begin with a Picture Start Code(100000). Refer RFC4629 Section 6.1. if (payloadStartCode < PICTURE_START_CODE) { @@ -113,10 +133,10 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; data.getData()[currentPosition] = 0; data.getData()[currentPosition + 1] = 0; data.setPosition(currentPosition); - } else { + } else if (gotFirstPacketOfH263Frame) { // Check that this packet is in the sequence of the previous packet. int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); - if (sequenceNumber != expectedSequenceNumber) { + if (sequenceNumber < expectedSequenceNumber) { Log.w( TAG, Util.formatInvariant( @@ -125,6 +145,12 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; expectedSequenceNumber, sequenceNumber)); return; } + } else { + Log.w( + TAG, + "First payload octet of the H263 packet is not the beginning of a new H263 partition," + + " Dropping current packet."); + return; } if (fragmentedSampleSizeBytes == 0) { @@ -141,20 +167,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // Write the video sample. trackOutput.sampleData(data, fragmentSize); fragmentedSampleSizeBytes += fragmentSize; + sampleTimeUsOfFragmentedSample = + toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); if (rtpMarker) { - if (firstReceivedTimestamp == C.TIME_UNSET) { - firstReceivedTimestamp = timestamp; - } - long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); - trackOutput.sampleMetadata( - timeUs, - isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0, - fragmentedSampleSizeBytes, - /* offset= */ 0, - /* cryptoData= */ null); - fragmentedSampleSizeBytes = 0; - isKeyFrame = false; + outputSampleMetadataForFragmentedPackets(); } previousSequenceNumber = sequenceNumber; } @@ -211,6 +228,24 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; isKeyFrame = false; } + /** + * Outputs sample metadata. + * + *

Call this method only when receiving a end of VP8 partition + */ + private void outputSampleMetadataForFragmentedPackets() { + trackOutput.sampleMetadata( + sampleTimeUsOfFragmentedSample, + isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0, + fragmentedSampleSizeBytes, + /* offset= */ 0, + /* cryptoData= */ null); + fragmentedSampleSizeBytes = 0; + sampleTimeUsOfFragmentedSample = C.TIME_UNSET; + isKeyFrame = false; + gotFirstPacketOfH263Frame = false; + } + private static long toSampleUs( long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) { return startTimeOffsetUs From da47771d105bd6d45135e6258c5775cf3ab563b9 Mon Sep 17 00:00:00 2001 From: Manisha Jajoo Date: Tue, 5 Jul 2022 18:00:54 +0530 Subject: [PATCH 12/16] Add test for Rtp H263 Reader Change-Id: I57d57881ef5c158d41be1bf1e3714332d50cd3a9 --- .../rtsp/reader/RtpH263ReaderTest.java | 198 ++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java new file mode 100644 index 0000000000..7e5560ce9f --- /dev/null +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2022 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.rtsp.reader; + +import static androidx.media3.common.util.Util.getBytesFromHexString; +import static com.google.common.truth.Truth.assertThat; + +import androidx.media3.common.Format; +import androidx.media3.common.MimeTypes; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.test.utils.FakeExtractorOutput; +import androidx.media3.test.utils.FakeTrackOutput; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableMap; +import com.google.common.primitives.Bytes; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +/** + * Unit test for {@link RtpH263Reader}. + */ +@RunWith(AndroidJUnit4.class) +public final class RtpH263ReaderTest { + private static final byte[] FRAME_1_FRAGMENT_1_DATA = + getBytesFromHexString("80020c0419b7b7d9591f03023e0c37b"); + private final RtpPacket FRAME_1_FRAGMENT_1 = + new RtpPacket.Builder() + .setTimestamp((int) 2599168056L) + .setSequenceNumber(40289) + .setMarker(false) + .setPayloadData( + Bytes.concat( + /*payload header */ getBytesFromHexString("0400"), FRAME_1_FRAGMENT_1_DATA)) + .build(); + private static final byte[] FRAME_1_FRAGMENT_2_DATA = + getBytesFromHexString("03140e0e77d5e83021a0c37"); + private static final RtpPacket FRAME_1_FRAGMENT_2 = + new RtpPacket.Builder() + .setTimestamp((int) 2599168056L) + .setSequenceNumber(40290) + .setMarker(true) + .setPayloadData( + Bytes.concat( + /*payload header */ getBytesFromHexString("0000"), FRAME_1_FRAGMENT_2_DATA)) + .build(); + private static final byte[] FRAME_1_DATA = + Bytes.concat(getBytesFromHexString("0000"), FRAME_1_FRAGMENT_1_DATA, FRAME_1_FRAGMENT_2_DATA); + + private static final byte[] FRAME_2_FRAGMENT_1_DATA = + getBytesFromHexString("800a0e023ffffffffffffffffff"); + private final RtpPacket FRAME_2_FRAGMENT_1 = + new RtpPacket.Builder() + .setTimestamp((int) 2599168344L) + .setSequenceNumber(40291) + .setMarker(false) + .setPayloadData( + Bytes.concat( + /*payload header */ getBytesFromHexString("0400"), FRAME_2_FRAGMENT_1_DATA)) + .build(); + private static final byte[] FRAME_2_FRAGMENT_2_DATA = + getBytesFromHexString("830df80c501839dfccdbdbecac"); + private static final RtpPacket FRAME_2_FRAGMENT_2 = + new RtpPacket.Builder() + .setTimestamp((int) 2599168344L) + .setSequenceNumber(40292) + .setMarker(true) + .setPayloadData( + Bytes.concat( + /*payload header */ getBytesFromHexString("0000"), FRAME_2_FRAGMENT_2_DATA)) + .build(); + private static final byte[] FRAME_2_DATA = + Bytes.concat(getBytesFromHexString("0000"), FRAME_2_FRAGMENT_1_DATA, FRAME_2_FRAGMENT_2_DATA); + + private static final RtpPayloadFormat H263_FORMAT = + new RtpPayloadFormat( + new Format.Builder() + .setSampleMimeType(MimeTypes.VIDEO_H263) + .setWidth(352) + .setHeight(288) + .build(), + /* rtpPayloadType= */ 96, + /* clockRate= */ 90_000, + /* fmtpParameters= */ ImmutableMap.of()); + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + private FakeTrackOutput trackOutput; + + private FakeExtractorOutput extractorOutput; + + @Before + public void setUp() { + extractorOutput = new FakeExtractorOutput(); + } + + @Test + public void consume_validPackets() { + RtpH263Reader h263Reader = new RtpH263Reader(H263_FORMAT); + h263Reader.createTracks(extractorOutput, /* trackId= */ 0); + h263Reader.onReceivingFirstPacket( + FRAME_1_FRAGMENT_1.timestamp, FRAME_1_FRAGMENT_1.sequenceNumber); + consume(h263Reader, FRAME_1_FRAGMENT_1); + consume(h263Reader, FRAME_1_FRAGMENT_2); + consume(h263Reader, FRAME_2_FRAGMENT_1); + consume(h263Reader, FRAME_2_FRAGMENT_2); + + trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_1_DATA); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_DATA); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(3200); + } + + @Test + public void consume_fragmentedFrameMissingFirstFragment() { + RtpH263Reader h263Reader = new RtpH263Reader(H263_FORMAT); + h263Reader.createTracks(extractorOutput, /* trackId= */ 0); + h263Reader.onReceivingFirstPacket( + FRAME_1_FRAGMENT_1.timestamp, FRAME_1_FRAGMENT_1.sequenceNumber); + consume(h263Reader, FRAME_1_FRAGMENT_2); + consume(h263Reader, FRAME_2_FRAGMENT_1); + consume(h263Reader, FRAME_2_FRAGMENT_2); + + trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(1); + assertThat(trackOutput.getSampleData(0)).isEqualTo(FRAME_2_DATA); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(3200); + } + + @Test + public void consume_fragmentedFrameMissingBoundaryFragment() { + RtpH263Reader h263Reader = new RtpH263Reader(H263_FORMAT); + h263Reader.createTracks(extractorOutput, /* trackId= */ 0); + h263Reader.onReceivingFirstPacket( + FRAME_1_FRAGMENT_1.timestamp, FRAME_1_FRAGMENT_1.sequenceNumber); + consume(h263Reader, FRAME_1_FRAGMENT_1); + consume(h263Reader, FRAME_2_FRAGMENT_1); + consume(h263Reader, FRAME_2_FRAGMENT_2); + + trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)) + .isEqualTo(Bytes.concat(getBytesFromHexString("0000"), FRAME_1_FRAGMENT_1_DATA)); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_DATA); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(3200); + } + + @Test + public void consume_outOfOrderPackets() { + RtpH263Reader h263Reader = new RtpH263Reader(H263_FORMAT); + h263Reader.createTracks(extractorOutput, /* trackId= */ 0); + h263Reader.onReceivingFirstPacket( + FRAME_1_FRAGMENT_1.timestamp, FRAME_1_FRAGMENT_1.sequenceNumber); + consume(h263Reader, FRAME_1_FRAGMENT_1); + consume(h263Reader, FRAME_2_FRAGMENT_1); + consume(h263Reader, FRAME_1_FRAGMENT_1); + consume(h263Reader, FRAME_2_FRAGMENT_2); + + trackOutput = extractorOutput.trackOutputs.get(0); + assertThat(trackOutput.getSampleCount()).isEqualTo(2); + assertThat(trackOutput.getSampleData(0)) + .isEqualTo(Bytes.concat(getBytesFromHexString("0000"), FRAME_1_FRAGMENT_1_DATA)); + assertThat(trackOutput.getSampleTimeUs(0)).isEqualTo(0); + assertThat(trackOutput.getSampleData(1)).isEqualTo(FRAME_2_DATA); + assertThat(trackOutput.getSampleTimeUs(1)).isEqualTo(3200); + } + + private static void consume(RtpH263Reader h263Reader, RtpPacket rtpPacket) { + ParsableByteArray packetData = new ParsableByteArray(); + packetData.reset(rtpPacket.payloadData); + h263Reader.consume( + packetData, + rtpPacket.timestamp, + rtpPacket.sequenceNumber, + /* isFrameBoundary= */ rtpPacket.marker); + } +} From 3bacb1646c9ecd01e403465c7643888e83a9e79d Mon Sep 17 00:00:00 2001 From: Manisha Jajoo Date: Mon, 18 Jul 2022 10:43:41 +0530 Subject: [PATCH 13/16] Keep the input data constant in consume method Earlier, the consume method of RtpH263Reader was changing the bytes of the input bitstream during header parse. This commit copies the input into local context and changes the local variable as per the specifications thus keeping the input constant. --- .../exoplayer/rtsp/reader/RtpH263Reader.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java index 147887e665..341d64256c 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java @@ -104,7 +104,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // | RR |P|V| PLEN |PEBIT| // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ int currentPosition = data.getPosition(); - int header = data.readUnsignedShort(); + ParsableByteArray bitstreamData = new ParsableByteArray(data.getData().clone()); + int header = bitstreamData.readUnsignedShort(); boolean pBitIsSet = (header & 0x400) > 0; // Check if optional V (Video Redundancy Coding), PLEN or PEBIT is present, RFC4629 Section 5.1. @@ -123,16 +124,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } gotFirstPacketOfH263Frame = true; - int payloadStartCode = data.peekUnsignedByte() & 0xFC; + int payloadStartCode = bitstreamData.peekUnsignedByte() & 0xFC; // Packets that begin with a Picture Start Code(100000). Refer RFC4629 Section 6.1. if (payloadStartCode < PICTURE_START_CODE) { Log.w(TAG, "Picture start Code (PSC) missing, dropping packet."); return; } // Setting first two bytes of the start code. Refer RFC4629 Section 6.1.1. - data.getData()[currentPosition] = 0; - data.getData()[currentPosition + 1] = 0; - data.setPosition(currentPosition); + bitstreamData.getData()[currentPosition] = 0; + bitstreamData.getData()[currentPosition + 1] = 0; + bitstreamData.setPosition(currentPosition); } else if (gotFirstPacketOfH263Frame) { // Check that this packet is in the sequence of the previous packet. int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); @@ -154,7 +155,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } if (fragmentedSampleSizeBytes == 0) { - parseVopHeader(data, isOutputFormatSet); + parseVopHeader(bitstreamData, isOutputFormatSet); if (!isOutputFormatSet && isKeyFrame) { if (width != payloadFormat.format.width || height != payloadFormat.format.height) { trackOutput.format( @@ -163,9 +164,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; isOutputFormatSet = true; } } - int fragmentSize = data.bytesLeft(); + int fragmentSize = bitstreamData.bytesLeft(); // Write the video sample. - trackOutput.sampleData(data, fragmentSize); + trackOutput.sampleData(bitstreamData, fragmentSize); fragmentedSampleSizeBytes += fragmentSize; sampleTimeUsOfFragmentedSample = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); From 69a716f633c466652bd6fabcae0502a1a0279398 Mon Sep 17 00:00:00 2001 From: Manisha Jajoo Date: Mon, 18 Jul 2022 10:44:51 +0530 Subject: [PATCH 14/16] fix review comments in RtpH263ReaderTest --- .../media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java index 7e5560ce9f..7f010b6f6a 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java @@ -42,7 +42,7 @@ import org.mockito.junit.MockitoRule; public final class RtpH263ReaderTest { private static final byte[] FRAME_1_FRAGMENT_1_DATA = getBytesFromHexString("80020c0419b7b7d9591f03023e0c37b"); - private final RtpPacket FRAME_1_FRAGMENT_1 = + private static final RtpPacket FRAME_1_FRAGMENT_1 = new RtpPacket.Builder() .setTimestamp((int) 2599168056L) .setSequenceNumber(40289) @@ -67,7 +67,7 @@ public final class RtpH263ReaderTest { private static final byte[] FRAME_2_FRAGMENT_1_DATA = getBytesFromHexString("800a0e023ffffffffffffffffff"); - private final RtpPacket FRAME_2_FRAGMENT_1 = + private static final RtpPacket FRAME_2_FRAGMENT_1 = new RtpPacket.Builder() .setTimestamp((int) 2599168344L) .setSequenceNumber(40291) @@ -174,7 +174,7 @@ public final class RtpH263ReaderTest { FRAME_1_FRAGMENT_1.timestamp, FRAME_1_FRAGMENT_1.sequenceNumber); consume(h263Reader, FRAME_1_FRAGMENT_1); consume(h263Reader, FRAME_2_FRAGMENT_1); - consume(h263Reader, FRAME_1_FRAGMENT_1); + consume(h263Reader, FRAME_1_FRAGMENT_2); consume(h263Reader, FRAME_2_FRAGMENT_2); trackOutput = extractorOutput.trackOutputs.get(0); From c7fbf3437fba0b57c4039304c6240d4f5a05a614 Mon Sep 17 00:00:00 2001 From: Manisha Jajoo Date: Tue, 19 Jul 2022 11:40:01 +0530 Subject: [PATCH 15/16] Revert "Keep the input data constant in consume method" This reverts commit 3bacb1646c9ecd01e403465c7643888e83a9e79d. --- .../exoplayer/rtsp/reader/RtpH263Reader.java | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java index 341d64256c..147887e665 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpH263Reader.java @@ -104,8 +104,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; // | RR |P|V| PLEN |PEBIT| // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ int currentPosition = data.getPosition(); - ParsableByteArray bitstreamData = new ParsableByteArray(data.getData().clone()); - int header = bitstreamData.readUnsignedShort(); + int header = data.readUnsignedShort(); boolean pBitIsSet = (header & 0x400) > 0; // Check if optional V (Video Redundancy Coding), PLEN or PEBIT is present, RFC4629 Section 5.1. @@ -124,16 +123,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } gotFirstPacketOfH263Frame = true; - int payloadStartCode = bitstreamData.peekUnsignedByte() & 0xFC; + int payloadStartCode = data.peekUnsignedByte() & 0xFC; // Packets that begin with a Picture Start Code(100000). Refer RFC4629 Section 6.1. if (payloadStartCode < PICTURE_START_CODE) { Log.w(TAG, "Picture start Code (PSC) missing, dropping packet."); return; } // Setting first two bytes of the start code. Refer RFC4629 Section 6.1.1. - bitstreamData.getData()[currentPosition] = 0; - bitstreamData.getData()[currentPosition + 1] = 0; - bitstreamData.setPosition(currentPosition); + data.getData()[currentPosition] = 0; + data.getData()[currentPosition + 1] = 0; + data.setPosition(currentPosition); } else if (gotFirstPacketOfH263Frame) { // Check that this packet is in the sequence of the previous packet. int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); @@ -155,7 +154,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } if (fragmentedSampleSizeBytes == 0) { - parseVopHeader(bitstreamData, isOutputFormatSet); + parseVopHeader(data, isOutputFormatSet); if (!isOutputFormatSet && isKeyFrame) { if (width != payloadFormat.format.width || height != payloadFormat.format.height) { trackOutput.format( @@ -164,9 +163,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; isOutputFormatSet = true; } } - int fragmentSize = bitstreamData.bytesLeft(); + int fragmentSize = data.bytesLeft(); // Write the video sample. - trackOutput.sampleData(bitstreamData, fragmentSize); + trackOutput.sampleData(data, fragmentSize); fragmentedSampleSizeBytes += fragmentSize; sampleTimeUsOfFragmentedSample = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); From ef57a061b74d3d5ab5c2c5aa41aa725e70bbee6d Mon Sep 17 00:00:00 2001 From: Manisha Jajoo Date: Tue, 19 Jul 2022 11:45:39 +0530 Subject: [PATCH 16/16] Pass local copy of input to RtpH263ReaderTest's consume method This change is done to keep the frame data unchanged. RtpH263Reader changes the header data in input, so to send the same RTP packet across multiple tests, each test copies the frame data into a new packet and sends that to the reader. --- .../rtsp/reader/RtpH263ReaderTest.java | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java index 7f010b6f6a..2bbef4befd 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpH263ReaderTest.java @@ -28,6 +28,7 @@ import androidx.media3.test.utils.FakeTrackOutput; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableMap; import com.google.common.primitives.Bytes; +import java.util.Arrays; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -118,10 +119,10 @@ public final class RtpH263ReaderTest { h263Reader.createTracks(extractorOutput, /* trackId= */ 0); h263Reader.onReceivingFirstPacket( FRAME_1_FRAGMENT_1.timestamp, FRAME_1_FRAGMENT_1.sequenceNumber); - consume(h263Reader, FRAME_1_FRAGMENT_1); + consume(h263Reader, copyPacket(FRAME_1_FRAGMENT_1)); consume(h263Reader, FRAME_1_FRAGMENT_2); - consume(h263Reader, FRAME_2_FRAGMENT_1); - consume(h263Reader, FRAME_2_FRAGMENT_2); + consume(h263Reader, copyPacket(FRAME_2_FRAGMENT_1)); + consume(h263Reader, copyPacket(FRAME_2_FRAGMENT_2)); trackOutput = extractorOutput.trackOutputs.get(0); assertThat(trackOutput.getSampleCount()).isEqualTo(2); @@ -137,9 +138,9 @@ public final class RtpH263ReaderTest { h263Reader.createTracks(extractorOutput, /* trackId= */ 0); h263Reader.onReceivingFirstPacket( FRAME_1_FRAGMENT_1.timestamp, FRAME_1_FRAGMENT_1.sequenceNumber); - consume(h263Reader, FRAME_1_FRAGMENT_2); - consume(h263Reader, FRAME_2_FRAGMENT_1); - consume(h263Reader, FRAME_2_FRAGMENT_2); + consume(h263Reader, copyPacket(FRAME_1_FRAGMENT_2)); + consume(h263Reader, copyPacket(FRAME_2_FRAGMENT_1)); + consume(h263Reader, copyPacket(FRAME_2_FRAGMENT_2)); trackOutput = extractorOutput.trackOutputs.get(0); assertThat(trackOutput.getSampleCount()).isEqualTo(1); @@ -153,9 +154,9 @@ public final class RtpH263ReaderTest { h263Reader.createTracks(extractorOutput, /* trackId= */ 0); h263Reader.onReceivingFirstPacket( FRAME_1_FRAGMENT_1.timestamp, FRAME_1_FRAGMENT_1.sequenceNumber); - consume(h263Reader, FRAME_1_FRAGMENT_1); - consume(h263Reader, FRAME_2_FRAGMENT_1); - consume(h263Reader, FRAME_2_FRAGMENT_2); + consume(h263Reader, copyPacket(FRAME_1_FRAGMENT_1)); + consume(h263Reader, copyPacket(FRAME_2_FRAGMENT_1)); + consume(h263Reader, copyPacket(FRAME_2_FRAGMENT_2)); trackOutput = extractorOutput.trackOutputs.get(0); assertThat(trackOutput.getSampleCount()).isEqualTo(2); @@ -172,10 +173,10 @@ public final class RtpH263ReaderTest { h263Reader.createTracks(extractorOutput, /* trackId= */ 0); h263Reader.onReceivingFirstPacket( FRAME_1_FRAGMENT_1.timestamp, FRAME_1_FRAGMENT_1.sequenceNumber); - consume(h263Reader, FRAME_1_FRAGMENT_1); - consume(h263Reader, FRAME_2_FRAGMENT_1); - consume(h263Reader, FRAME_1_FRAGMENT_2); - consume(h263Reader, FRAME_2_FRAGMENT_2); + consume(h263Reader, copyPacket(FRAME_1_FRAGMENT_1)); + consume(h263Reader, copyPacket(FRAME_2_FRAGMENT_1)); + consume(h263Reader, copyPacket(FRAME_1_FRAGMENT_2)); + consume(h263Reader, copyPacket(FRAME_2_FRAGMENT_2)); trackOutput = extractorOutput.trackOutputs.get(0); assertThat(trackOutput.getSampleCount()).isEqualTo(2); @@ -195,4 +196,23 @@ public final class RtpH263ReaderTest { rtpPacket.sequenceNumber, /* isFrameBoundary= */ rtpPacket.marker); } + + private static RtpPacket copyPacket(RtpPacket packet) { + RtpPacket.Builder builder = + new RtpPacket.Builder() + .setPadding(packet.padding) + .setMarker(packet.marker) + .setPayloadType(packet.payloadType) + .setSequenceNumber(packet.sequenceNumber) + .setTimestamp(packet.timestamp) + .setSsrc(packet.ssrc); + + if (packet.csrc.length > 0) { + builder.setCsrc(Arrays.copyOf(packet.csrc, packet.csrc.length)); + } + if (packet.payloadData.length > 0) { + builder.setPayloadData(Arrays.copyOf(packet.payloadData, packet.payloadData.length)); + } + return builder.build(); + } }