From 4eb34e4c58e9cdfc804a5e3347ef30e9991c0186 Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 11 Jul 2022 15:49:33 +0000 Subject: [PATCH 01/25] Listen to playWhenReady changes in LeanbackPlayerAdapter #minor-release Issue: google/ExoPlayer#10420 PiperOrigin-RevId: 460223064 --- RELEASENOTES.md | 3 +++ .../media3/ui/leanback/LeanbackPlayerAdapter.java | 13 ++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2d210c85f4..ed05a841da 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -32,6 +32,9 @@ * RTSP: * Add RTP reader for H263 ([#63](https://github.com/androidx/media/pull/63)). +* Leanback extension: + * Listen to `playWhenReady` changes in `LeanbackAdapter` + ([10420](https://github.com/google/ExoPlayer/issues/10420)). ### 1.0.0-beta01 (2022-06-16) diff --git a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java index 77d25ce9dc..84a8c9eb75 100644 --- a/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java +++ b/libraries/ui_leanback/src/main/java/androidx/media3/ui/leanback/LeanbackPlayerAdapter.java @@ -236,11 +236,6 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab // Player.Listener implementation. - @Override - public void onPlaybackStateChanged(@Player.State int playbackState) { - notifyStateChanged(); - } - @Override public void onPlayerError(PlaybackException error) { Callback callback = getCallback(); @@ -285,5 +280,13 @@ public final class LeanbackPlayerAdapter extends PlayerAdapter implements Runnab int scaledWidth = Math.round(videoSize.width * videoSize.pixelWidthHeightRatio); getCallback().onVideoSizeChanged(LeanbackPlayerAdapter.this, scaledWidth, videoSize.height); } + + @Override + public void onEvents(Player player, Player.Events events) { + if (events.containsAny( + Player.EVENT_PLAY_WHEN_READY_CHANGED, Player.EVENT_PLAYBACK_STATE_CHANGED)) { + notifyStateChanged(); + } + } } } From 18f4068c06a27ceedce8ee951b3832235a96f75e Mon Sep 17 00:00:00 2001 From: claincly Date: Mon, 11 Jul 2022 17:01:33 +0000 Subject: [PATCH 02/25] Apply priority/operating rate settings for video encoding. - Added setter to disable this feature. - Added accompanying tests. - Plan to run tests on the same set of settings on H265. PiperOrigin-RevId: 460238673 --- .../media3/transformer/DefaultCodec.java | 6 +++ .../transformer/DefaultEncoderFactory.java | 45 +++++++++++++++---- .../transformer/VideoEncoderSettings.java | 3 +- .../DefaultEncoderFactoryTest.java | 43 ++++++++++++++++-- 4 files changed, 85 insertions(+), 12 deletions(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java index 07475aa410..300939527e 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultCodec.java @@ -27,6 +27,7 @@ import android.media.MediaCodec.BufferInfo; import android.media.MediaFormat; import android.view.Surface; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; @@ -277,6 +278,11 @@ public final class DefaultCodec implements Codec { return mediaCodec.getName(); } + @VisibleForTesting + /* package */ MediaFormat getConfigurationMediaFormat() { + return configurationMediaFormat; + } + /** * Attempts to dequeue an output buffer if there is no output buffer pending. Does nothing * otherwise. 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 b609b1c023..60918c952c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -20,7 +20,6 @@ import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; -import static androidx.media3.common.util.Util.SDK_INT; import static java.lang.Math.abs; import static java.lang.Math.floor; import static java.lang.Math.round; @@ -47,6 +46,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @UnstableApi public final class DefaultEncoderFactory implements Codec.EncoderFactory { private static final int DEFAULT_FRAME_RATE = 30; + /** Best effort, or as-fast-as-possible priority setting for {@link MediaFormat#KEY_PRIORITY}. */ + private static final int PRIORITY_BEST_EFFORT = 1; + private static final String TAG = "DefaultEncoderFactory"; /** A builder for {@link DefaultEncoderFactory} instances. */ @@ -254,7 +256,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { if (supportedVideoEncoderSettings.profile != VideoEncoderSettings.NO_VALUE && supportedVideoEncoderSettings.level != VideoEncoderSettings.NO_VALUE - && SDK_INT >= 23) { + && Util.SDK_INT >= 23) { // Set profile and level at the same time to maximize compatibility, or the encoder will pick // the values. mediaFormat.setInteger(MediaFormat.KEY_PROFILE, supportedVideoEncoderSettings.profile); @@ -285,12 +287,17 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { if (Util.SDK_INT >= 23) { // Setting operating rate and priority is supported from API 23. - if (supportedVideoEncoderSettings.operatingRate != VideoEncoderSettings.NO_VALUE) { - mediaFormat.setInteger( - MediaFormat.KEY_OPERATING_RATE, supportedVideoEncoderSettings.operatingRate); - } - if (supportedVideoEncoderSettings.priority != VideoEncoderSettings.NO_VALUE) { - mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, supportedVideoEncoderSettings.priority); + if (supportedVideoEncoderSettings.operatingRate == VideoEncoderSettings.NO_VALUE + && supportedVideoEncoderSettings.priority == VideoEncoderSettings.NO_VALUE) { + adjustMediaFormatForEncoderPerformanceSettings(mediaFormat); + } else { + if (supportedVideoEncoderSettings.operatingRate != VideoEncoderSettings.NO_VALUE) { + mediaFormat.setInteger( + MediaFormat.KEY_OPERATING_RATE, supportedVideoEncoderSettings.operatingRate); + } + if (supportedVideoEncoderSettings.priority != VideoEncoderSettings.NO_VALUE) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, supportedVideoEncoderSettings.priority); + } } } @@ -462,6 +469,28 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { } } + /** + * Applies empirical {@link MediaFormat#KEY_PRIORITY} and {@link MediaFormat#KEY_OPERATING_RATE} + * settings for better encoder performance. + * + *

The adjustment is applied in-place to {@code mediaFormat}. + */ + private static void adjustMediaFormatForEncoderPerformanceSettings(MediaFormat mediaFormat) { + // TODO(b/213477153) Verify priority/operating rate settings work for non-AVC codecs. + if (Util.SDK_INT < 25) { + // Not setting priority and operating rate achieves better encoding performance. + return; + } + + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, PRIORITY_BEST_EFFORT); + + if (Util.SDK_INT == 26) { + mediaFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, DEFAULT_FRAME_RATE); + } else { + mediaFormat.setInteger(MediaFormat.KEY_OPERATING_RATE, Integer.MAX_VALUE); + } + } + /** * Applying suggested profile/level settings from * https://developer.android.com/guide/topics/media/sharing-video#b-frames_and_encoding_profiles 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 7c043878fd..536e9fdb1c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java @@ -180,7 +180,8 @@ public final class VideoEncoderSettings { } /** - * Sets encoding operating rate and priority. The default values are {@link #NO_VALUE}. + * Sets encoding operating rate and priority. The default values are {@link #NO_VALUE}, which is + * treated as configuring the encoder for maximum throughput. * * @param operatingRate The {@link MediaFormat#KEY_OPERATING_RATE operating rate}. * @param priority The {@link MediaFormat#KEY_PRIORITY priority}. diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java index 0896c7f427..48aa570c0f 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java @@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; import org.robolectric.shadows.MediaCodecInfoBuilder; import org.robolectric.shadows.ShadowMediaCodecList; @@ -40,6 +41,10 @@ public class DefaultEncoderFactoryTest { @Before public void setUp() { + createShadowH264Encoder(); + } + + private static void createShadowH264Encoder() { MediaFormat avcFormat = new MediaFormat(); avcFormat.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC); MediaCodecInfo.CodecProfileLevel profileLevel = new MediaCodecInfo.CodecProfileLevel(); @@ -48,17 +53,26 @@ public class DefaultEncoderFactoryTest { // blocks will be left for encoding height 1088. profileLevel.level = MediaCodecInfo.CodecProfileLevel.AVCLevel4; + createShadowVideoEncoder(avcFormat, profileLevel, "test.transformer.avc.encoder"); + } + + private static void createShadowVideoEncoder( + MediaFormat supportedFormat, + MediaCodecInfo.CodecProfileLevel supportedProfileLevel, + String name) { + // ShadowMediaCodecList is static. The added encoders will be visible for every test. ShadowMediaCodecList.addCodec( MediaCodecInfoBuilder.newBuilder() - .setName("test.transformer.avc.encoder") + .setName(name) .setIsEncoder(true) .setCapabilities( MediaCodecInfoBuilder.CodecCapabilitiesBuilder.newBuilder() - .setMediaFormat(avcFormat) + .setMediaFormat(supportedFormat) .setIsEncoder(true) .setColorFormats( new int[] {MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible}) - .setProfileLevels(new MediaCodecInfo.CodecProfileLevel[] {profileLevel}) + .setProfileLevels( + new MediaCodecInfo.CodecProfileLevel[] {supportedProfileLevel}) .build()) .build()); } @@ -117,6 +131,29 @@ public class DefaultEncoderFactoryTest { assertThat(actualVideoFormat.height).isEqualTo(1080); } + @Config(sdk = 29) + @Test + public void + createForVideoEncoding_withH264Encoding_configuresEncoderWithCorrectPerformanceSettings() + throws Exception { + Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); + Codec videoEncoder = + new DefaultEncoderFactory.Builder(context) + .build() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H264)); + + assertThat(videoEncoder).isInstanceOf(DefaultCodec.class); + MediaFormat configurationMediaFormat = + ((DefaultCodec) videoEncoder).getConfigurationMediaFormat(); + assertThat(configurationMediaFormat.containsKey(MediaFormat.KEY_PRIORITY)).isTrue(); + assertThat(configurationMediaFormat.getInteger(MediaFormat.KEY_PRIORITY)).isEqualTo(1); + assertThat(configurationMediaFormat.containsKey(MediaFormat.KEY_OPERATING_RATE)).isTrue(); + assertThat(configurationMediaFormat.getInteger(MediaFormat.KEY_OPERATING_RATE)) + .isEqualTo(Integer.MAX_VALUE); + } + @Test public void createForVideoEncoding_withNoSupportedEncoder_throws() { Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); From 7dc54efdb9c751aff14660fca38889ba2007e4d5 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Mon, 11 Jul 2022 17:04:11 +0000 Subject: [PATCH 03/25] Merge MatrixTransformationProcessor and ExternalTextureProcessor. This saves an intermediate texture copy step for use-cases where matrix transformations are the first or only effects in the chain. PiperOrigin-RevId: 460239403 --- .../vertex_shader_tex_transform_es2.glsl | 27 --- .../vertex_shader_transformation_es2.glsl | 8 +- ... => vertex_shader_transformation_es3.glsl} | 14 +- .../transformer/ExternalTextureProcessor.java | 104 +-------- ...lMatrixTransformationProcessorWrapper.java | 29 ++- .../transformer/GlEffectsFrameProcessor.java | 202 +++++++++--------- .../MatrixTransformationProcessor.java | 73 ++++++- .../SingleFrameGlTextureProcessor.java | 10 - 8 files changed, 212 insertions(+), 255 deletions(-) delete mode 100644 libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es2.glsl rename libraries/transformer/src/main/assets/shaders/{vertex_shader_tex_transform_es3.glsl => vertex_shader_transformation_es3.glsl} (66%) diff --git a/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es2.glsl b/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es2.glsl deleted file mode 100644 index 20f3058ce2..0000000000 --- a/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es2.glsl +++ /dev/null @@ -1,27 +0,0 @@ -#version 100 -// 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. - -// ES 2 vertex shader that applies an external surface texture's 4 * 4 texture -// transformation matrix to convert the texture coordinates to the sampling -// locations. - -attribute vec4 aFramePosition; -uniform mat4 uTexTransform; -varying vec2 vTexSamplingCoord; -void main() { - gl_Position = aFramePosition; - vec4 texturePosition = vec4(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5, 0.0, 1.0); - vTexSamplingCoord = (uTexTransform * texturePosition).xy; -} diff --git a/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es2.glsl b/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es2.glsl index 2491e3d2a2..06164bad5e 100644 --- a/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es2.glsl +++ b/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es2.glsl @@ -13,13 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -// ES 2 vertex shader that applies the 4 * 4 transformation matrix -// uTransformationMatrix. +// ES 2 vertex shader that applies the 4 * 4 transformation matrices +// uTransformationMatrix and the uTexTransformationMatrix. attribute vec4 aFramePosition; uniform mat4 uTransformationMatrix; +uniform mat4 uTexTransformationMatrix; varying vec2 vTexSamplingCoord; void main() { gl_Position = uTransformationMatrix * aFramePosition; - vTexSamplingCoord = vec2(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5); + vec4 texturePosition = vec4(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5, 0.0, 1.0); + vTexSamplingCoord = (uTexTransformationMatrix * texturePosition).xy; } diff --git a/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es3.glsl b/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es3.glsl similarity index 66% rename from libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es3.glsl rename to libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es3.glsl index f732294c90..c99b31112e 100644 --- a/libraries/transformer/src/main/assets/shaders/vertex_shader_tex_transform_es3.glsl +++ b/libraries/transformer/src/main/assets/shaders/vertex_shader_transformation_es3.glsl @@ -1,5 +1,5 @@ #version 300 es -// Copyright 2022 The Android Open Source Project +// Copyright 2021 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. @@ -13,15 +13,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -// ES 3 vertex shader that applies an external surface texture's 4 * 4 texture -// transformation matrix to convert the texture coordinates to the sampling -// locations. +// ES 3 vertex shader that applies the 4 * 4 transformation matrices +// uTransformationMatrix and the uTexTransformationMatrix. in vec4 aFramePosition; -uniform mat4 uTexTransform; +uniform mat4 uTransformationMatrix; +uniform mat4 uTexTransformationMatrix; out vec2 vTexSamplingCoord; void main() { - gl_Position = aFramePosition; + gl_Position = uTransformationMatrix * aFramePosition; vec4 texturePosition = vec4(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5, 0.0, 1.0); - vTexSamplingCoord = (uTexTransform * texturePosition).xy; + vTexSamplingCoord = (uTexTransformationMatrix * texturePosition).xy; } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalTextureProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalTextureProcessor.java index a2f96ef63f..f31c7682ad 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalTextureProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalTextureProcessor.java @@ -15,71 +15,13 @@ */ package androidx.media3.transformer; -import static androidx.media3.common.util.Assertions.checkArgument; -import static androidx.media3.common.util.Assertions.checkStateNotNull; - -import android.content.Context; -import android.opengl.GLES20; -import android.util.Size; -import androidx.media3.common.util.GlProgram; -import androidx.media3.common.util.GlUtil; -import java.io.IOException; - -/** Copies frames from an external texture and applies color transformations for HDR if needed. */ -/* package */ class ExternalTextureProcessor extends SingleFrameGlTextureProcessor { - - private static final String VERTEX_SHADER_TEX_TRANSFORM_PATH = - "shaders/vertex_shader_tex_transform_es2.glsl"; - private static final String VERTEX_SHADER_TEX_TRANSFORM_ES3_PATH = - "shaders/vertex_shader_tex_transform_es3.glsl"; - private static final String FRAGMENT_SHADER_COPY_EXTERNAL_PATH = - "shaders/fragment_shader_copy_external_es2.glsl"; - private static final String FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH = - "shaders/fragment_shader_copy_external_yuv_es3.glsl"; - // Color transform coefficients from - // https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/libstagefright/colorconversion/ColorConverter.cpp;l=668-670;drc=487adf977a50cac3929eba15fad0d0f461c7ff0f. - private static final float[] MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM = { - 1.168f, 1.168f, 1.168f, - 0.0f, -0.188f, 2.148f, - 1.683f, -0.652f, 0.0f, - }; - - private final GlProgram glProgram; - - /** - * Creates a new instance. - * - * @param useHdr Whether to process the input as an HDR signal. - * @throws FrameProcessingException If a problem occurs while reading shader files. - */ - public ExternalTextureProcessor(Context context, boolean useHdr) throws FrameProcessingException { - String vertexShaderFilePath = - useHdr ? VERTEX_SHADER_TEX_TRANSFORM_ES3_PATH : VERTEX_SHADER_TEX_TRANSFORM_PATH; - String fragmentShaderFilePath = - useHdr ? FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH : FRAGMENT_SHADER_COPY_EXTERNAL_PATH; - try { - glProgram = new GlProgram(context, vertexShaderFilePath, fragmentShaderFilePath); - } catch (IOException | GlUtil.GlException e) { - throw new FrameProcessingException(e); - } - // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. - glProgram.setBufferAttribute( - "aFramePosition", - GlUtil.getNormalizedCoordinateBounds(), - GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); - if (useHdr) { - // In HDR editing mode the decoder output is sampled in YUV. - glProgram.setFloatsUniform("uColorTransform", MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM); - } - } - - @Override - public Size configure(int inputWidth, int inputHeight) { - checkArgument(inputWidth > 0, "inputWidth must be positive"); - checkArgument(inputHeight > 0, "inputHeight must be positive"); - - return new Size(inputWidth, inputHeight); - } +/** + * Interface for a {@link GlTextureProcessor} that samples from an external texture. + * + *

Use {@link #setTextureTransformMatrix(float[])} to provide the texture's transformation + * matrix. + */ +/* package */ interface ExternalTextureProcessor extends GlTextureProcessor { /** * Sets the texture transform matrix for converting an external surface texture's coordinates to @@ -88,35 +30,5 @@ import java.io.IOException; * @param textureTransformMatrix The external surface texture's {@linkplain * android.graphics.SurfaceTexture#getTransformMatrix(float[]) transform matrix}. */ - public void setTextureTransformMatrix(float[] textureTransformMatrix) { - checkStateNotNull(glProgram); - glProgram.setFloatsUniform("uTexTransform", textureTransformMatrix); - } - - @Override - public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException { - checkStateNotNull(glProgram); - try { - glProgram.use(); - glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0); - glProgram.bindAttributesAndUniforms(); - // The four-vertex triangle strip forms a quad. - GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); - GlUtil.checkGlError(); - } catch (GlUtil.GlException e) { - throw new FrameProcessingException(e, presentationTimeUs); - } - } - - @Override - public void release() throws FrameProcessingException { - super.release(); - if (glProgram != null) { - try { - glProgram.delete(); - } catch (GlUtil.GlException e) { - throw new FrameProcessingException(e); - } - } - } + void setTextureTransformMatrix(float[] textureTransformMatrix); } 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 fc91bb3dc5..29bcb3c34d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FinalMatrixTransformationProcessorWrapper.java @@ -24,6 +24,7 @@ import android.opengl.EGLDisplay; import android.opengl.EGLExt; import android.opengl.EGLSurface; import android.opengl.GLES20; +import android.opengl.Matrix; import android.util.Size; import android.view.Surface; import android.view.SurfaceHolder; @@ -50,7 +51,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; *

This wrapper is used for the final {@link GlTextureProcessor} instance in the chain of {@link * GlTextureProcessor} instances used by {@link FrameProcessor}. */ -/* package */ final class FinalMatrixTransformationProcessorWrapper implements GlTextureProcessor { +/* package */ final class FinalMatrixTransformationProcessorWrapper + implements GlTextureProcessor, ExternalTextureProcessor { private static final String TAG = "FinalProcessorWrapper"; @@ -61,7 +63,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final long streamOffsetUs; private final DebugViewProvider debugViewProvider; private final FrameProcessor.Listener frameProcessorListener; + private final boolean sampleFromExternalTexture; private final boolean useHdr; + private final float[] textureTransformMatrix; private int inputWidth; private int inputHeight; @@ -86,6 +90,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; long streamOffsetUs, FrameProcessor.Listener frameProcessorListener, DebugViewProvider debugViewProvider, + boolean sampleFromExternalTexture, boolean useHdr) { this.context = context; this.matrixTransformations = matrixTransformations; @@ -94,7 +99,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; this.streamOffsetUs = streamOffsetUs; this.debugViewProvider = debugViewProvider; this.frameProcessorListener = frameProcessorListener; + this.sampleFromExternalTexture = sampleFromExternalTexture; this.useHdr = useHdr; + + textureTransformMatrix = new float[16]; + Matrix.setIdentityM(textureTransformMatrix, /* smOffset= */ 0); } /** @@ -239,7 +248,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; outputSurfaceInfo.width, outputSurfaceInfo.height, Presentation.LAYOUT_SCALE_TO_FIT)); MatrixTransformationProcessor matrixTransformationProcessor = - new MatrixTransformationProcessor(context, matrixTransformationListBuilder.build()); + new MatrixTransformationProcessor( + context, matrixTransformationListBuilder.build(), sampleFromExternalTexture, useHdr); + matrixTransformationProcessor.setTextureTransformMatrix(textureTransformMatrix); Size outputSize = matrixTransformationProcessor.configure(inputWidth, inputHeight); checkState(outputSize.getWidth() == outputSurfaceInfo.width); checkState(outputSize.getHeight() == outputSurfaceInfo.height); @@ -265,6 +276,20 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } + @Override + public void setTextureTransformMatrix(float[] textureTransformMatrix) { + System.arraycopy( + /* src= */ textureTransformMatrix, + /* srcPos= */ 0, + /* dest= */ this.textureTransformMatrix, + /* destPost= */ 0, + /* length= */ textureTransformMatrix.length); + + if (matrixTransformationProcessor != null) { + matrixTransformationProcessor.setTextureTransformMatrix(textureTransformMatrix); + } + } + public synchronized void setOutputSurfaceInfo(@Nullable SurfaceInfo outputSurfaceInfo) { if (!Util.areEqual(this.outputSurfaceInfo, outputSurfaceInfo)) { this.outputSurfaceInfo = outputSurfaceInfo; 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 a951a9bc2c..3856a6bdee 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffectsFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffectsFrameProcessor.java @@ -17,13 +17,13 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static com.google.common.collect.Iterables.getLast; import android.content.Context; import android.graphics.SurfaceTexture; import android.opengl.EGL14; import android.opengl.EGLContext; import android.opengl.EGLDisplay; -import android.util.Pair; import android.view.Surface; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; @@ -126,31 +126,19 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; GlUtil.focusPlaceholderEglSurface(eglContext, eglDisplay); } - Pair, FinalMatrixTransformationProcessorWrapper> - textureProcessors = - getGlTextureProcessorsForGlEffects( - context, - effects, - eglDisplay, - eglContext, - streamOffsetUs, - listener, - debugViewProvider, - useHdr); - ImmutableList intermediateTextureProcessors = textureProcessors.first; - FinalMatrixTransformationProcessorWrapper finalTextureProcessorWrapper = - textureProcessors.second; - - ExternalTextureProcessor externalTextureProcessor = - new ExternalTextureProcessor(context, useHdr); + ImmutableList textureProcessors = + getGlTextureProcessorsForGlEffects( + context, + effects, + eglDisplay, + eglContext, + streamOffsetUs, + listener, + debugViewProvider, + useHdr); FrameProcessingTaskExecutor frameProcessingTaskExecutor = new FrameProcessingTaskExecutor(singleThreadExecutorService, listener); - chainTextureProcessorsWithListeners( - externalTextureProcessor, - intermediateTextureProcessors, - finalTextureProcessorWrapper, - frameProcessingTaskExecutor, - listener); + chainTextureProcessorsWithListeners(textureProcessors, frameProcessingTaskExecutor, listener); return new GlEffectsFrameProcessor( eglDisplay, @@ -158,9 +146,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; frameProcessingTaskExecutor, streamOffsetUs, /* inputExternalTextureId= */ GlUtil.createExternalTexture(), - externalTextureProcessor, - intermediateTextureProcessors, - finalTextureProcessorWrapper); + textureProcessors); } /** @@ -168,25 +154,25 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * MatrixTransformationProcessor} and converts all other {@link GlEffect} instances to separate * {@link GlTextureProcessor} instances. * - * @return A {@link Pair} containing a list of {@link GlTextureProcessor} instances to apply in - * the given order and a {@link FinalMatrixTransformationProcessorWrapper} to apply after - * them. + * @return A non-empty list of {@link GlTextureProcessor} instances to apply in the given order. + * The first is an {@link ExternalTextureProcessor} and the last is a {@link + * FinalMatrixTransformationProcessorWrapper}. */ - private static Pair, FinalMatrixTransformationProcessorWrapper> - getGlTextureProcessorsForGlEffects( - Context context, - List effects, - EGLDisplay eglDisplay, - EGLContext eglContext, - long streamOffsetUs, - FrameProcessor.Listener listener, - DebugViewProvider debugViewProvider, - boolean useHdr) - throws FrameProcessingException { + private static ImmutableList getGlTextureProcessorsForGlEffects( + Context context, + List effects, + EGLDisplay eglDisplay, + EGLContext eglContext, + long streamOffsetUs, + FrameProcessor.Listener listener, + DebugViewProvider debugViewProvider, + boolean useHdr) + throws FrameProcessingException { ImmutableList.Builder textureProcessorListBuilder = new ImmutableList.Builder<>(); ImmutableList.Builder matrixTransformationListBuilder = new ImmutableList.Builder<>(); + boolean sampleFromExternalTexture = true; for (int i = 0; i < effects.size(); i++) { GlEffect effect = effects.get(i); if (effect instanceof GlMatrixTransformation) { @@ -195,15 +181,16 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } ImmutableList matrixTransformations = matrixTransformationListBuilder.build(); - if (!matrixTransformations.isEmpty()) { + if (!matrixTransformations.isEmpty() || sampleFromExternalTexture) { textureProcessorListBuilder.add( - new MatrixTransformationProcessor(context, matrixTransformations)); + new MatrixTransformationProcessor( + context, matrixTransformations, sampleFromExternalTexture, useHdr)); matrixTransformationListBuilder = new ImmutableList.Builder<>(); + sampleFromExternalTexture = false; } textureProcessorListBuilder.add(effect.toGlTextureProcessor(context)); } - return Pair.create( - textureProcessorListBuilder.build(), + textureProcessorListBuilder.add( new FinalMatrixTransformationProcessorWrapper( context, eglDisplay, @@ -212,51 +199,35 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; streamOffsetUs, listener, debugViewProvider, + sampleFromExternalTexture, useHdr)); + return textureProcessorListBuilder.build(); } /** * Chains the given {@link GlTextureProcessor} instances using {@link * ChainingGlTextureProcessorListener} instances. - * - *

The {@link ExternalTextureProcessor} is the first processor in the chain. */ private static void chainTextureProcessorsWithListeners( - ExternalTextureProcessor externalTextureProcessor, - ImmutableList intermediateTextureProcessors, - FinalMatrixTransformationProcessorWrapper finalTextureProcessorWrapper, + ImmutableList textureProcessors, FrameProcessingTaskExecutor frameProcessingTaskExecutor, FrameProcessor.Listener listener) { - externalTextureProcessor.setListener( - new ChainingGlTextureProcessorListener( - /* previousGlTextureProcessor= */ null, - /* nextGlTextureProcessor= */ intermediateTextureProcessors.size() > 0 - ? intermediateTextureProcessors.get(0) - : finalTextureProcessorWrapper, - frameProcessingTaskExecutor, - listener)); - GlTextureProcessor previousGlTextureProcessor = externalTextureProcessor; - for (int i = 0; i < intermediateTextureProcessors.size(); i++) { - GlTextureProcessor glTextureProcessor = intermediateTextureProcessors.get(i); + for (int i = 0; i < textureProcessors.size(); i++) { + @Nullable + GlTextureProcessor previousGlTextureProcessor = + i - 1 >= 0 ? textureProcessors.get(i - 1) : null; @Nullable GlTextureProcessor nextGlTextureProcessor = - i + 1 < intermediateTextureProcessors.size() - ? intermediateTextureProcessors.get(i + 1) - : finalTextureProcessorWrapper; - glTextureProcessor.setListener( - new ChainingGlTextureProcessorListener( - previousGlTextureProcessor, - nextGlTextureProcessor, - frameProcessingTaskExecutor, - listener)); - previousGlTextureProcessor = glTextureProcessor; + i + 1 < textureProcessors.size() ? textureProcessors.get(i + 1) : null; + textureProcessors + .get(i) + .setListener( + new ChainingGlTextureProcessorListener( + previousGlTextureProcessor, + nextGlTextureProcessor, + frameProcessingTaskExecutor, + listener)); } - finalTextureProcessorWrapper.setListener( - new ChainingGlTextureProcessorListener( - previousGlTextureProcessor, - /* nextGlTextureProcessor= */ null, - frameProcessingTaskExecutor, - listener)); } private static final String TAG = "GlEffectsFrameProcessor"; @@ -280,11 +251,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final float[] inputSurfaceTextureTransformMatrix; private final int inputExternalTextureId; private final ExternalTextureProcessor inputExternalTextureProcessor; - private final ImmutableList intermediateTextureProcessors; private final FinalMatrixTransformationProcessorWrapper finalTextureProcessorWrapper; + private final ImmutableList allTextureProcessors; private final ConcurrentLinkedQueue pendingInputFrames; + // Fields accessed on the thread used by the GlEffectsFrameProcessor's caller. private @MonotonicNonNull FrameInfo nextInputFrameInfo; + + // Fields accessed on the frameProcessingTaskExecutor's thread. + private boolean inputTextureInUse; private boolean inputStreamEnded; private GlEffectsFrameProcessor( @@ -293,18 +268,21 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; FrameProcessingTaskExecutor frameProcessingTaskExecutor, long streamOffsetUs, int inputExternalTextureId, - ExternalTextureProcessor inputExternalTextureProcessor, - ImmutableList intermediateTextureProcessors, - FinalMatrixTransformationProcessorWrapper finalTextureProcessorWrapper) { + ImmutableList textureProcessors) { this.eglDisplay = eglDisplay; this.eglContext = eglContext; this.frameProcessingTaskExecutor = frameProcessingTaskExecutor; this.streamOffsetUs = streamOffsetUs; this.inputExternalTextureId = inputExternalTextureId; - this.inputExternalTextureProcessor = inputExternalTextureProcessor; - this.intermediateTextureProcessors = intermediateTextureProcessors; - this.finalTextureProcessorWrapper = finalTextureProcessorWrapper; + + checkState(!textureProcessors.isEmpty()); + checkState(textureProcessors.get(0) instanceof ExternalTextureProcessor); + checkState(getLast(textureProcessors) instanceof FinalMatrixTransformationProcessorWrapper); + inputExternalTextureProcessor = (ExternalTextureProcessor) textureProcessors.get(0); + finalTextureProcessorWrapper = + (FinalMatrixTransformationProcessorWrapper) getLast(textureProcessors); + allTextureProcessors = textureProcessors; inputSurfaceTexture = new SurfaceTexture(inputExternalTextureId); inputSurface = new Surface(inputSurfaceTexture); @@ -321,7 +299,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @Override public void setInputFrameInfo(FrameInfo inputFrameInfo) { - nextInputFrameInfo = inputFrameInfo; + nextInputFrameInfo = adjustForPixelWidthHeightRatio(inputFrameInfo); } @Override @@ -365,36 +343,54 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } /** - * Processes an input frame from the {@linkplain #getInputSurface() external input surface - * texture}. + * Processes an input frame from the {@link #inputSurfaceTexture}. * *

This method must be called on the {@linkplain #THREAD_NAME background thread}. */ @WorkerThread private void processInputFrame() { checkState(Thread.currentThread().getName().equals(THREAD_NAME)); - if (!inputExternalTextureProcessor.acceptsInputFrame()) { + if (inputTextureInUse) { frameProcessingTaskExecutor.submit(this::processInputFrame); // Try again later. return; } + inputTextureInUse = true; inputSurfaceTexture.updateTexImage(); + inputSurfaceTexture.getTransformMatrix(inputSurfaceTextureTransformMatrix); + queueInputFrameToTextureProcessors(); + } + + /** + * Queues the input frame to the first texture processor until it is accepted. + * + *

This method must be called on the {@linkplain #THREAD_NAME background thread}. + */ + @WorkerThread + private void queueInputFrameToTextureProcessors() { + checkState(Thread.currentThread().getName().equals(THREAD_NAME)); + checkState(inputTextureInUse); + long inputFrameTimeNs = inputSurfaceTexture.getTimestamp(); // Correct for the stream offset so processors see original media presentation timestamps. long presentationTimeUs = inputFrameTimeNs / 1000 - streamOffsetUs; - inputSurfaceTexture.getTransformMatrix(inputSurfaceTextureTransformMatrix); inputExternalTextureProcessor.setTextureTransformMatrix(inputSurfaceTextureTransformMatrix); - FrameInfo inputFrameInfo = adjustForPixelWidthHeightRatio(pendingInputFrames.remove()); - checkState( - inputExternalTextureProcessor.maybeQueueInputFrame( - new TextureInfo( - inputExternalTextureId, - /* fboId= */ C.INDEX_UNSET, - inputFrameInfo.width, - inputFrameInfo.height), - presentationTimeUs)); - // After the inputExternalTextureProcessor has produced an output frame, it is processed - // asynchronously by the texture processors chained after it. + FrameInfo inputFrameInfo = checkStateNotNull(pendingInputFrames.peek()); + if (inputExternalTextureProcessor.maybeQueueInputFrame( + new TextureInfo( + inputExternalTextureId, + /* fboId= */ C.INDEX_UNSET, + inputFrameInfo.width, + inputFrameInfo.height), + presentationTimeUs)) { + inputTextureInUse = false; + pendingInputFrames.remove(); + // After the externalTextureProcessor has produced an output frame, it is processed + // asynchronously by the texture processors chained after it. + } else { + // Try again later. + frameProcessingTaskExecutor.submit(this::queueInputFrameToTextureProcessors); + } } /** @@ -442,11 +438,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @WorkerThread private void releaseTextureProcessorsAndDestroyGlContext() throws GlUtil.GlException, FrameProcessingException { - inputExternalTextureProcessor.release(); - for (int i = 0; i < intermediateTextureProcessors.size(); i++) { - intermediateTextureProcessors.get(i).release(); + for (int i = 0; i < allTextureProcessors.size(); i++) { + allTextureProcessors.get(i).release(); } - finalTextureProcessorWrapper.release(); GlUtil.destroyEglContext(eglDisplay, eglContext); } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MatrixTransformationProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MatrixTransformationProcessor.java index 9f551f0a3b..c85bf100f1 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MatrixTransformationProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MatrixTransformationProcessor.java @@ -37,20 +37,36 @@ import java.util.Arrays; * matrices are clipped to the NDC range. * *

The background color of the output frame will be (r=0, g=0, b=0, a=0). + * + *

Can copy frames from an external texture and apply color transformations for HDR if needed. */ @UnstableApi @SuppressWarnings("FunctionalInterfaceClash") // b/228192298 -/* package */ final class MatrixTransformationProcessor extends SingleFrameGlTextureProcessor { +/* package */ final class MatrixTransformationProcessor extends SingleFrameGlTextureProcessor + implements ExternalTextureProcessor { private static final String VERTEX_SHADER_TRANSFORMATION_PATH = "shaders/vertex_shader_transformation_es2.glsl"; - private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_copy_es2.glsl"; + private static final String VERTEX_SHADER_TRANSFORMATION_ES3_PATH = + "shaders/vertex_shader_transformation_es3.glsl"; + private static final String FRAGMENT_SHADER_COPY_PATH = "shaders/fragment_shader_copy_es2.glsl"; + private static final String FRAGMENT_SHADER_COPY_EXTERNAL_PATH = + "shaders/fragment_shader_copy_external_es2.glsl"; + private static final String FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH = + "shaders/fragment_shader_copy_external_yuv_es3.glsl"; private static final ImmutableList NDC_SQUARE = ImmutableList.of( new float[] {-1, -1, 0, 1}, new float[] {-1, 1, 0, 1}, new float[] {1, 1, 0, 1}, new float[] {1, -1, 0, 1}); + // Color transform coefficients from + // https://cs.android.com/android/platform/superproject/+/master:frameworks/av/media/libstagefright/colorconversion/ColorConverter.cpp;l=668-670;drc=487adf977a50cac3929eba15fad0d0f461c7ff0f. + private static final float[] MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM = { + 1.168f, 1.168f, 1.168f, + 0.0f, -0.188f, 2.148f, + 1.683f, -0.652f, 0.0f, + }; /** The {@link MatrixTransformation MatrixTransformations} to apply. */ private final ImmutableList matrixTransformations; @@ -89,7 +105,11 @@ import java.util.Arrays; */ public MatrixTransformationProcessor(Context context, MatrixTransformation matrixTransformation) throws FrameProcessingException { - this(context, ImmutableList.of(matrixTransformation)); + this( + context, + ImmutableList.of(matrixTransformation), + /* sampleFromExternalTexture= */ false, + /* enableExperimentalHdrEditing= */ false); } /** @@ -102,7 +122,11 @@ import java.util.Arrays; */ public MatrixTransformationProcessor(Context context, GlMatrixTransformation matrixTransformation) throws FrameProcessingException { - this(context, ImmutableList.of(matrixTransformation)); + this( + context, + ImmutableList.of(matrixTransformation), + /* sampleFromExternalTexture= */ false, + /* enableExperimentalHdrEditing= */ false); } /** @@ -111,10 +135,17 @@ import java.util.Arrays; * @param context The {@link Context}. * @param matrixTransformations The {@link GlMatrixTransformation GlMatrixTransformations} to * apply to each frame in order. + * @param sampleFromExternalTexture Whether the input will be provided using an external texture. + * If {@code true}, the caller should use {@link #setTextureTransformMatrix(float[])} to + * provide the transformation matrix associated with the external texture. + * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. * @throws FrameProcessingException If a problem occurs while reading shader files. */ public MatrixTransformationProcessor( - Context context, ImmutableList matrixTransformations) + Context context, + ImmutableList matrixTransformations, + boolean sampleFromExternalTexture, + boolean enableExperimentalHdrEditing) throws FrameProcessingException { this.matrixTransformations = matrixTransformations; @@ -123,11 +154,41 @@ import java.util.Arrays; tempResultMatrix = new float[16]; Matrix.setIdentityM(compositeTransformationMatrix, /* smOffset= */ 0); visiblePolygon = NDC_SQUARE; + + String vertexShaderFilePath; + String fragmentShaderFilePath; + if (sampleFromExternalTexture) { + vertexShaderFilePath = + enableExperimentalHdrEditing + ? VERTEX_SHADER_TRANSFORMATION_ES3_PATH + : VERTEX_SHADER_TRANSFORMATION_PATH; + fragmentShaderFilePath = + enableExperimentalHdrEditing + ? FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH + : FRAGMENT_SHADER_COPY_EXTERNAL_PATH; + } else { + vertexShaderFilePath = VERTEX_SHADER_TRANSFORMATION_PATH; + fragmentShaderFilePath = FRAGMENT_SHADER_COPY_PATH; + } + try { - glProgram = new GlProgram(context, VERTEX_SHADER_TRANSFORMATION_PATH, FRAGMENT_SHADER_PATH); + glProgram = new GlProgram(context, vertexShaderFilePath, fragmentShaderFilePath); } catch (IOException | GlUtil.GlException e) { throw new FrameProcessingException(e); } + + if (enableExperimentalHdrEditing && sampleFromExternalTexture) { + // In HDR editing mode the decoder output is sampled in YUV. + glProgram.setFloatsUniform("uColorTransform", MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM); + } + float[] identityMatrix = new float[16]; + Matrix.setIdentityM(identityMatrix, /* smOffset= */ 0); + glProgram.setFloatsUniform("uTexTransformationMatrix", identityMatrix); + } + + @Override + public void setTextureTransformMatrix(float[] textureTransformMatrix) { + glProgram.setFloatsUniform("uTexTransformationMatrix", textureTransformMatrix); } @Override diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SingleFrameGlTextureProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SingleFrameGlTextureProcessor.java index b767f973a5..84f05c815b 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SingleFrameGlTextureProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SingleFrameGlTextureProcessor.java @@ -75,16 +75,6 @@ public abstract class SingleFrameGlTextureProcessor implements GlTextureProcesso this.listener = listener; } - /** - * Returns whether the {@code SingleFrameGlTextureProcessor} can accept an input frame. - * - *

If this method returns {@code true}, the next call to {@link #maybeQueueInputFrame( - * TextureInfo, long)} will also return {@code true}. - */ - public boolean acceptsInputFrame() { - return !outputTextureInUse; - } - @Override public final boolean maybeQueueInputFrame(TextureInfo inputTexture, long presentationTimeUs) { if (outputTextureInUse) { From 30fbc3a27d1c2b673c3f0a6f1c8956e183b11952 Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 11 Jul 2022 23:22:27 +0000 Subject: [PATCH 04/25] Use the public MediaItem in the timeline of CastPlayer The media item needs to be assigned to `Window.mediaItem` in `CastTimeline.setWindow`. For this the `MediaItem` needs to be available in the timeline. When a `MediaItem` is passed to the `set/addMediaItems` method, we can't yet know the Cast `MediaQueueItem.itemId` that is generated on the device and arrives with an async update of the `RemoteMediaClient` state. Hence in the `CastTimelineTracker`, we need to store the `MediaItem` by Casts's `MediaItem.contentId`. When we then receive the updated queue, we look the media item up by the content ID to augment the `ItemData` that is available in the `CastTimeline`. Issue: androidx/media#25 Issue: google/ExoPlayer#8212 #minor-release PiperOrigin-RevId: 460325235 --- RELEASENOTES.md | 5 + .../java/androidx/media3/cast/CastPlayer.java | 39 ++-- .../androidx/media3/cast/CastTimeline.java | 47 ++++- .../media3/cast/CastTimelineTracker.java | 86 +++++++- .../androidx/media3/cast/CastPlayerTest.java | 195 +++++++++-------- .../media3/cast/CastTimelineTrackerTest.java | 197 +++++++++++++++++- 6 files changed, 436 insertions(+), 133 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ed05a841da..cc4841b3fb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -35,6 +35,11 @@ * Leanback extension: * Listen to `playWhenReady` changes in `LeanbackAdapter` ([10420](https://github.com/google/ExoPlayer/issues/10420)). +* Cast: + * Use the `MediaItem` that has been passed to the playlist methods as + `Window.mediaItem` in `CastTimeline` + ([#25](https://github.com/androidx/media/issues/25), + [#8212](https://github.com/google/ExoPlayer/issues/8212)). ### 1.0.0-beta01 (2022-06-16) diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index 4ec40f6846..d55d73e7bd 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -200,7 +200,7 @@ public final class CastPlayer extends BasePlayer { this.mediaItemConverter = mediaItemConverter; this.seekBackIncrementMs = seekBackIncrementMs; this.seekForwardIncrementMs = seekForwardIncrementMs; - timelineTracker = new CastTimelineTracker(); + timelineTracker = new CastTimelineTracker(mediaItemConverter); period = new Timeline.Period(); statusListener = new StatusListener(); seekResultCallback = new SeekResultCallback(); @@ -285,8 +285,7 @@ public final class CastPlayer extends BasePlayer { @Override public void setMediaItems(List mediaItems, int startIndex, long startPositionMs) { - setMediaItemsInternal( - toMediaQueueItems(mediaItems), startIndex, startPositionMs, repeatMode.value); + setMediaItemsInternal(mediaItems, startIndex, startPositionMs, repeatMode.value); } @Override @@ -296,7 +295,7 @@ public final class CastPlayer extends BasePlayer { if (index < currentTimeline.getWindowCount()) { uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid; } - addMediaItemsInternal(toMediaQueueItems(mediaItems), uid); + addMediaItemsInternal(mediaItems, uid); } @Override @@ -1022,14 +1021,13 @@ public final class CastPlayer extends BasePlayer { } } - @Nullable - private PendingResult setMediaItemsInternal( - MediaQueueItem[] mediaQueueItems, + private void setMediaItemsInternal( + List mediaItems, int startIndex, long startPositionMs, @RepeatMode int repeatMode) { - if (remoteMediaClient == null || mediaQueueItems.length == 0) { - return null; + if (remoteMediaClient == null || mediaItems.isEmpty()) { + return; } startPositionMs = startPositionMs == C.TIME_UNSET ? 0 : startPositionMs; if (startIndex == C.INDEX_UNSET) { @@ -1040,34 +1038,35 @@ public final class CastPlayer extends BasePlayer { if (!currentTimeline.isEmpty()) { pendingMediaItemRemovalPosition = getCurrentPositionInfo(); } - return remoteMediaClient.queueLoad( + MediaQueueItem[] mediaQueueItems = toMediaQueueItems(mediaItems); + timelineTracker.onMediaItemsSet(mediaItems, mediaQueueItems); + remoteMediaClient.queueLoad( mediaQueueItems, - min(startIndex, mediaQueueItems.length - 1), + min(startIndex, mediaItems.size() - 1), getCastRepeatMode(repeatMode), startPositionMs, /* customData= */ null); } - @Nullable - private PendingResult addMediaItemsInternal(MediaQueueItem[] items, int uid) { + private void addMediaItemsInternal(List mediaItems, int uid) { if (remoteMediaClient == null || getMediaStatus() == null) { - return null; + return; } - return remoteMediaClient.queueInsertItems(items, uid, /* customData= */ null); + MediaQueueItem[] itemsToInsert = toMediaQueueItems(mediaItems); + timelineTracker.onMediaItemsAdded(mediaItems, itemsToInsert); + remoteMediaClient.queueInsertItems(itemsToInsert, uid, /* customData= */ null); } - @Nullable - private PendingResult moveMediaItemsInternal( - int[] uids, int fromIndex, int newIndex) { + private void moveMediaItemsInternal(int[] uids, int fromIndex, int newIndex) { if (remoteMediaClient == null || getMediaStatus() == null) { - return null; + return; } int insertBeforeIndex = fromIndex < newIndex ? newIndex + uids.length : newIndex; int insertBeforeItemId = MediaQueueItem.INVALID_ITEM_ID; if (insertBeforeIndex < currentTimeline.getWindowCount()) { insertBeforeItemId = (int) currentTimeline.getWindow(insertBeforeIndex, window).uid; } - return remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null); + remoteMediaClient.queueReorderItems(uids, insertBeforeItemId, /* customData= */ null); } @Nullable diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java b/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java index 12e8ee5d2d..d21fca2608 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java @@ -15,13 +15,13 @@ */ package androidx.media3.cast; -import android.net.Uri; import android.util.SparseArray; import android.util.SparseIntArray; import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.Timeline; +import com.google.android.gms.cast.MediaInfo; import java.util.Arrays; /** A {@link Timeline} for Cast media queues. */ @@ -30,12 +30,16 @@ import java.util.Arrays; /** Holds {@link Timeline} related data for a Cast media item. */ public static final class ItemData { + /* package */ static final String UNKNOWN_CONTENT_ID = "UNKNOWN_CONTENT_ID"; + /** Holds no media information. */ public static final ItemData EMPTY = new ItemData( /* durationUs= */ C.TIME_UNSET, /* defaultPositionUs= */ C.TIME_UNSET, - /* isLive= */ false); + /* isLive= */ false, + MediaItem.EMPTY, + UNKNOWN_CONTENT_ID); /** The duration of the item in microseconds, or {@link C#TIME_UNSET} if unknown. */ public final long durationUs; @@ -45,6 +49,10 @@ import java.util.Arrays; public final long defaultPositionUs; /** Whether the item is live content, or {@code false} if unknown. */ public final boolean isLive; + /** The original media item that has been set or added to the playlist. */ + public final MediaItem mediaItem; + /** The {@linkplain MediaInfo#getContentId() content ID} of the cast media queue item. */ + public final String contentId; /** * Creates an instance. @@ -52,11 +60,20 @@ import java.util.Arrays; * @param durationUs See {@link #durationsUs}. * @param defaultPositionUs See {@link #defaultPositionUs}. * @param isLive See {@link #isLive}. + * @param mediaItem See {@link #mediaItem}. + * @param contentId See {@link #contentId}. */ - public ItemData(long durationUs, long defaultPositionUs, boolean isLive) { + public ItemData( + long durationUs, + long defaultPositionUs, + boolean isLive, + MediaItem mediaItem, + String contentId) { this.durationUs = durationUs; this.defaultPositionUs = defaultPositionUs; this.isLive = isLive; + this.mediaItem = mediaItem; + this.contentId = contentId; } /** @@ -66,14 +83,23 @@ import java.util.Arrays; * @param defaultPositionUs The default start position in microseconds, or {@link C#TIME_UNSET} * if unknown. * @param isLive Whether the item is live, or {@code false} if unknown. + * @param mediaItem The media item. + * @param contentId The content ID. */ - public ItemData copyWithNewValues(long durationUs, long defaultPositionUs, boolean isLive) { + public ItemData copyWithNewValues( + long durationUs, + long defaultPositionUs, + boolean isLive, + MediaItem mediaItem, + String contentId) { if (durationUs == this.durationUs && defaultPositionUs == this.defaultPositionUs - && isLive == this.isLive) { + && isLive == this.isLive + && contentId.equals(this.contentId) + && mediaItem.equals(this.mediaItem)) { return this; } - return new ItemData(durationUs, defaultPositionUs, isLive); + return new ItemData(durationUs, defaultPositionUs, isLive, mediaItem, contentId); } } @@ -82,6 +108,7 @@ import java.util.Arrays; new CastTimeline(new int[0], new SparseArray<>()); private final SparseIntArray idsToIndex; + private final MediaItem[] mediaItems; private final int[] ids; private final long[] durationsUs; private final long[] defaultPositionsUs; @@ -100,10 +127,12 @@ import java.util.Arrays; durationsUs = new long[itemCount]; defaultPositionsUs = new long[itemCount]; isLive = new boolean[itemCount]; + mediaItems = new MediaItem[itemCount]; for (int i = 0; i < ids.length; i++) { int id = ids[i]; idsToIndex.put(id, i); ItemData data = itemIdToData.get(id, ItemData.EMPTY); + mediaItems[i] = data.mediaItem.buildUpon().setTag(id).build(); durationsUs[i] = data.durationUs; defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs; isLive[i] = data.isLive; @@ -121,18 +150,16 @@ import java.util.Arrays; public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) { long durationUs = durationsUs[windowIndex]; boolean isDynamic = durationUs == C.TIME_UNSET; - MediaItem mediaItem = - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(ids[windowIndex]).build(); return window.set( /* uid= */ ids[windowIndex], - /* mediaItem= */ mediaItem, + /* mediaItem= */ mediaItems[windowIndex], /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* isSeekable= */ !isDynamic, isDynamic, - isLive[windowIndex] ? mediaItem.liveConfiguration : null, + isLive[windowIndex] ? mediaItems[windowIndex].liveConfiguration : null, defaultPositionsUs[windowIndex], durationUs, /* firstPeriodIndex= */ windowIndex, diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastTimelineTracker.java b/libraries/cast/src/main/java/androidx/media3/cast/CastTimelineTracker.java index e123495152..c955387ff4 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastTimelineTracker.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastTimelineTracker.java @@ -15,14 +15,23 @@ */ package androidx.media3.cast; +import static androidx.media3.cast.CastTimeline.ItemData.UNKNOWN_CONTENT_ID; +import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.common.util.Assertions.checkStateNotNull; + import android.util.SparseArray; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Player; import com.google.android.gms.cast.MediaInfo; import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.framework.media.RemoteMediaClient; +import java.util.HashMap; import java.util.HashSet; +import java.util.List; /** * Creates {@link CastTimeline CastTimelines} from cast receiver app status updates. @@ -33,9 +42,47 @@ import java.util.HashSet; /* package */ final class CastTimelineTracker { private final SparseArray itemIdToData; + private final MediaItemConverter mediaItemConverter; + @VisibleForTesting /* package */ final HashMap mediaItemsByContentId; - public CastTimelineTracker() { + /** + * Creates an instance. + * + * @param mediaItemConverter The converter used to convert from a {@link MediaQueueItem} to a + * {@link MediaItem}. + */ + public CastTimelineTracker(MediaItemConverter mediaItemConverter) { + this.mediaItemConverter = mediaItemConverter; itemIdToData = new SparseArray<>(); + mediaItemsByContentId = new HashMap<>(); + } + + /** + * Called when media items {@linkplain Player#setMediaItems have been set to the playlist} and are + * sent to the cast playback queue. A future queue update of the {@link RemoteMediaClient} will + * reflect this addition. + * + * @param mediaItems The media items that have been set. + * @param mediaQueueItems The corresponding media queue items. + */ + public void onMediaItemsSet(List mediaItems, MediaQueueItem[] mediaQueueItems) { + mediaItemsByContentId.clear(); + onMediaItemsAdded(mediaItems, mediaQueueItems); + } + + /** + * Called when media items {@linkplain Player#addMediaItems(List) have been added} and are sent to + * the cast playback queue. A future queue update of the {@link RemoteMediaClient} will reflect + * this addition. + * + * @param mediaItems The media items that have been added. + * @param mediaQueueItems The corresponding media queue items. + */ + public void onMediaItemsAdded(List mediaItems, MediaQueueItem[] mediaQueueItems) { + for (int i = 0; i < mediaItems.size(); i++) { + mediaItemsByContentId.put( + checkNotNull(mediaQueueItems[i].getMedia()).getContentId(), mediaItems.get(i)); + } } /** @@ -63,18 +110,36 @@ import java.util.HashSet; } int currentItemId = mediaStatus.getCurrentItemId(); + String currentContentId = checkStateNotNull(mediaStatus.getMediaInfo()).getContentId(); + MediaItem mediaItem = mediaItemsByContentId.get(currentContentId); updateItemData( - currentItemId, mediaStatus.getMediaInfo(), /* defaultPositionUs= */ C.TIME_UNSET); + currentItemId, + mediaItem != null ? mediaItem : MediaItem.EMPTY, + mediaStatus.getMediaInfo(), + currentContentId, + /* defaultPositionUs= */ C.TIME_UNSET); - for (MediaQueueItem item : mediaStatus.getQueueItems()) { - long defaultPositionUs = (long) (item.getStartTime() * C.MICROS_PER_SECOND); - updateItemData(item.getItemId(), item.getMedia(), defaultPositionUs); + for (MediaQueueItem queueItem : mediaStatus.getQueueItems()) { + long defaultPositionUs = (long) (queueItem.getStartTime() * C.MICROS_PER_SECOND); + @Nullable MediaInfo mediaInfo = queueItem.getMedia(); + String contentId = mediaInfo != null ? mediaInfo.getContentId() : UNKNOWN_CONTENT_ID; + mediaItem = mediaItemsByContentId.get(contentId); + updateItemData( + queueItem.getItemId(), + mediaItem != null ? mediaItem : mediaItemConverter.toMediaItem(queueItem), + mediaInfo, + contentId, + defaultPositionUs); } - return new CastTimeline(itemIds, itemIdToData); } - private void updateItemData(int itemId, @Nullable MediaInfo mediaInfo, long defaultPositionUs) { + private void updateItemData( + int itemId, + MediaItem mediaItem, + @Nullable MediaInfo mediaInfo, + String contentId, + long defaultPositionUs) { CastTimeline.ItemData previousData = itemIdToData.get(itemId, CastTimeline.ItemData.EMPTY); long durationUs = CastUtils.getStreamDurationUs(mediaInfo); if (durationUs == C.TIME_UNSET) { @@ -87,7 +152,10 @@ import java.util.HashSet; if (defaultPositionUs == C.TIME_UNSET) { defaultPositionUs = previousData.defaultPositionUs; } - itemIdToData.put(itemId, previousData.copyWithNewValues(durationUs, defaultPositionUs, isLive)); + itemIdToData.put( + itemId, + previousData.copyWithNewValues( + durationUs, defaultPositionUs, isLive, mediaItem, contentId)); } private void removeUnusedItemDataEntries(int[] itemIds) { @@ -99,6 +167,8 @@ import java.util.HashSet; int index = 0; while (index < itemIdToData.size()) { if (!scratchItemIds.contains(itemIdToData.keyAt(index))) { + CastTimeline.ItemData itemData = itemIdToData.valueAt(index); + mediaItemsByContentId.remove(itemData.contentId); itemIdToData.removeAt(index); } else { index++; diff --git a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java index 31b7afd87a..83273d2a9a 100644 --- a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java +++ b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java @@ -63,6 +63,7 @@ import static org.mockito.MockitoAnnotations.initMocks; import android.net.Uri; import androidx.media3.common.C; import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; @@ -126,6 +127,7 @@ public class CastPlayerTest { when(mockCastSession.getRemoteMediaClient()).thenReturn(mockRemoteMediaClient); when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); + when(mockMediaStatus.getMediaInfo()).thenReturn(new MediaInfo.Builder("contentId").build()); when(mockMediaQueue.getItemIds()).thenReturn(new int[0]); // Make the remote media client present the same default values as ExoPlayer: when(mockRemoteMediaClient.isPaused()).thenReturn(true); @@ -388,7 +390,7 @@ public class CastPlayerTest { mediaItems.add( new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); - castPlayer.setMediaItems(mediaItems, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L); + castPlayer.setMediaItems(mediaItems, /* startIndex= */ 1, /* startPositionMs= */ 2000L); verify(mockRemoteMediaClient) .queueLoad(queueItemsArgumentCaptor.capture(), eq(1), anyInt(), eq(2000L), any()); @@ -424,32 +426,42 @@ public class CastPlayerTest { String uri1 = "http://www.google.com/video1"; String uri2 = "http://www.google.com/video2"; firstPlaylist.add( - new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); + new MediaItem.Builder() + .setUri(uri1) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(1) + .build()); firstPlaylist.add( - new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); + new MediaItem.Builder() + .setUri(uri2) + .setMimeType(MimeTypes.APPLICATION_MP4) + .setTag(2) + .build()); ImmutableList secondPlaylist = ImmutableList.of( new MediaItem.Builder() .setUri(Uri.EMPTY) + .setTag(3) .setMimeType(MimeTypes.APPLICATION_MPD) .build()); - castPlayer.setMediaItems( - firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L); + castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 2000L); updateTimeLine( firstPlaylist, /* mediaQueueItemIds= */ new int[] {1, 2}, /* currentItemId= */ 2); // Replacing existing playlist. - castPlayer.setMediaItems( - secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L); + castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 1000L); updateTimeLine(secondPlaylist, /* mediaQueueItemIds= */ new int[] {3}, /* currentItemId= */ 3); InOrder inOrder = Mockito.inOrder(mockListener); inOrder - .verify(mockListener, times(2)) + .verify(mockListener) .onMediaItemTransition( - mediaItemCaptor.capture(), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + eq(firstPlaylist.get(1)), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + inOrder + .verify(mockListener) + .onMediaItemTransition( + eq(secondPlaylist.get(0)), eq(MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); - assertThat(mediaItemCaptor.getAllValues().get(1).localConfiguration.tag).isEqualTo(3); } @SuppressWarnings("deprecation") // Verifies deprecated callback being called correctly. @@ -459,18 +471,26 @@ public class CastPlayerTest { String uri1 = "http://www.google.com/video1"; String uri2 = "http://www.google.com/video2"; firstPlaylist.add( - new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); + new MediaItem.Builder() + .setUri(uri1) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(1) + .build()); firstPlaylist.add( - new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); + new MediaItem.Builder() + .setUri(uri2) + .setMimeType(MimeTypes.APPLICATION_MP4) + .setTag(2) + .build()); ImmutableList secondPlaylist = ImmutableList.of( new MediaItem.Builder() .setUri(Uri.EMPTY) .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(3) .build()); - castPlayer.setMediaItems( - firstPlaylist, /* startWindowIndex= */ 1, /* startPositionMs= */ 2000L); + castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 2000L); updateTimeLine( firstPlaylist, /* mediaQueueItemIds= */ new int[] {1, 2}, @@ -481,8 +501,7 @@ public class CastPlayerTest { /* durationsMs= */ new long[] {20_000, 20_000}, /* positionMs= */ 2000L); // Replacing existing playlist. - castPlayer.setMediaItems( - secondPlaylist, /* startWindowIndex= */ 0, /* startPositionMs= */ 1000L); + castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 1000L); updateTimeLine( secondPlaylist, /* mediaQueueItemIds= */ new int[] {3}, @@ -494,8 +513,8 @@ public class CastPlayerTest { Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 1, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 1, + firstPlaylist.get(1), /* periodUid= */ 2, /* periodIndex= */ 1, /* positionMs= */ 2000, @@ -505,8 +524,8 @@ public class CastPlayerTest { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 3, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(3).build(), + /* mediaItemIndex= */ 0, + secondPlaylist.get(0), /* periodUid= */ 3, /* periodIndex= */ 0, /* positionMs= */ 1000, @@ -720,10 +739,8 @@ public class CastPlayerTest { inOrder .verify(mockListener) .onMediaItemTransition( - mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + eq(mediaItem), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); - assertThat(mediaItemCaptor.getValue().localConfiguration.tag) - .isEqualTo(mediaItem.localConfiguration.tag); } @Test @@ -742,7 +759,8 @@ public class CastPlayerTest { InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItems.get(0)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder .verify(mockListener) .onMediaItemTransition( @@ -776,8 +794,8 @@ public class CastPlayerTest { Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 1234, @@ -787,7 +805,7 @@ public class CastPlayerTest { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ null, - /* windowIndex= */ 0, + /* mediaItemIndex= */ 0, /* mediaItem= */ null, /* periodUid= */ null, /* periodIndex= */ 0, @@ -827,10 +845,8 @@ public class CastPlayerTest { .onMediaItemTransition( mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); - assertThat(mediaItemCaptor.getAllValues().get(0).localConfiguration.tag) - .isEqualTo(mediaItem1.localConfiguration.tag); - assertThat(mediaItemCaptor.getAllValues().get(1).localConfiguration.tag) - .isEqualTo(mediaItem2.localConfiguration.tag); + assertThat(mediaItemCaptor.getAllValues().get(0)).isEqualTo(mediaItem1); + assertThat(mediaItemCaptor.getAllValues().get(1)).isEqualTo(mediaItem2); } @Test @@ -862,8 +878,8 @@ public class CastPlayerTest { Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItem1, /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 1234, @@ -873,8 +889,8 @@ public class CastPlayerTest { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 0, + mediaItem2, /* periodUid= */ 2, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -912,10 +928,8 @@ public class CastPlayerTest { mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); List capturedMediaItems = mediaItemCaptor.getAllValues(); - assertThat(capturedMediaItems.get(0).localConfiguration.tag) - .isEqualTo(mediaItem1.localConfiguration.tag); - assertThat(capturedMediaItems.get(1).localConfiguration.tag) - .isEqualTo(mediaItem2.localConfiguration.tag); + assertThat(capturedMediaItems.get(0)).isEqualTo(mediaItem1); + assertThat(capturedMediaItems.get(1)).isEqualTo(mediaItem2); } @Test @@ -945,8 +959,8 @@ public class CastPlayerTest { Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItem1, /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 0, // position at which we receive the timeline change @@ -956,8 +970,8 @@ public class CastPlayerTest { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 0, + mediaItem2, /* periodUid= */ 2, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -992,7 +1006,8 @@ public class CastPlayerTest { InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItem1), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); } @@ -1027,19 +1042,17 @@ public class CastPlayerTest { castPlayer.addMediaItems(mediaItems); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); - castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234); + castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1234); InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItem1), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder .verify(mockListener) - .onMediaItemTransition( - mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK)); + .onMediaItemTransition(eq(mediaItem2), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK)); inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt()); - assertThat(mediaItemCaptor.getValue().localConfiguration.tag) - .isEqualTo(mediaItem2.localConfiguration.tag); } @Test @@ -1054,13 +1067,13 @@ public class CastPlayerTest { castPlayer.addMediaItems(mediaItems); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); - castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1234); + castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1234); Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItem1, /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -1070,8 +1083,8 @@ public class CastPlayerTest { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 1, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 1, + mediaItem2, /* periodUid= */ 2, /* periodIndex= */ 1, /* positionMs= */ 1234, @@ -1097,12 +1110,13 @@ public class CastPlayerTest { castPlayer.addMediaItems(mediaItems); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1234); InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItems.get(0)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); } @@ -1115,14 +1129,13 @@ public class CastPlayerTest { castPlayer.addMediaItems(mediaItems); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 1234); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 1234); - MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(); Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -1132,8 +1145,8 @@ public class CastPlayerTest { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 1234, @@ -1164,13 +1177,12 @@ public class CastPlayerTest { InOrder inOrder = Mockito.inOrder(mockListener); inOrder .verify(mockListener) - .onMediaItemTransition(any(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); + .onMediaItemTransition( + eq(mediaItems.get(0)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED)); inOrder .verify(mockListener) - .onMediaItemTransition( - mediaItemCaptor.capture(), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO)); + .onMediaItemTransition(eq(mediaItems.get(1)), eq(Player.MEDIA_ITEM_TRANSITION_REASON_AUTO)); inOrder.verify(mockListener, never()).onMediaItemTransition(any(), anyInt()); - assertThat(mediaItemCaptor.getValue().localConfiguration.tag).isEqualTo(2); } @Test @@ -1203,8 +1215,8 @@ public class CastPlayerTest { Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(), + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 12500, @@ -1214,8 +1226,8 @@ public class CastPlayerTest { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 2, - /* windowIndex= */ 1, - new MediaItem.Builder().setUri(Uri.EMPTY).setTag(2).build(), + /* mediaItemIndex= */ 1, + mediaItems.get(1), /* periodUid= */ 2, /* periodIndex= */ 1, /* positionMs= */ 0, @@ -1250,12 +1262,11 @@ public class CastPlayerTest { mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs); castPlayer.seekBack(); - MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(); Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 2 * C.DEFAULT_SEEK_BACK_INCREMENT_MS, @@ -1265,8 +1276,8 @@ public class CastPlayerTest { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ C.DEFAULT_SEEK_BACK_INCREMENT_MS, @@ -1299,12 +1310,11 @@ public class CastPlayerTest { mediaItems, mediaQueueItemIds, currentItemId, streamTypes, durationsMs, positionMs); castPlayer.seekForward(); - MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).setTag(1).build(); Player.PositionInfo oldPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ 0, @@ -1314,8 +1324,8 @@ public class CastPlayerTest { Player.PositionInfo newPosition = new Player.PositionInfo( /* windowUid= */ 1, - /* windowIndex= */ 0, - mediaItem, + /* mediaItemIndex= */ 0, + mediaItems.get(0), /* periodUid= */ 1, /* periodIndex= */ 0, /* positionMs= */ C.DEFAULT_SEEK_FORWARD_INCREMENT_MS, @@ -1475,14 +1485,14 @@ public class CastPlayerTest { // Check that there were no other calls to onAvailableCommandsChanged. verify(mockListener).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow); verify(mockListener, times(2)).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0); verify(mockListener, times(2)).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 3, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 3, /* positionMs= */ 0); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousWindow); verify(mockListener, times(3)).onAvailableCommandsChanged(any()); } @@ -1509,14 +1519,14 @@ public class CastPlayerTest { // Check that there were no other calls to onAvailableCommandsChanged. verify(mockListener).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 2, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 0); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToPreviousAndNextWindow); verify(mockListener, times(2)).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 1, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 0); verify(mockListener, times(2)).onAvailableCommandsChanged(any()); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 0); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 0); verify(mockListener).onAvailableCommandsChanged(commandsWithSeekToNextWindow); verify(mockListener, times(3)).onAvailableCommandsChanged(any()); } @@ -1533,8 +1543,8 @@ public class CastPlayerTest { updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); verify(mockListener).onAvailableCommandsChanged(defaultCommands); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 200); - castPlayer.seekTo(/* windowIndex= */ 0, /* positionMs= */ 100); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 200); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 100); // Check that there were no other calls to onAvailableCommandsChanged. verify(mockListener).onAvailableCommandsChanged(any()); } @@ -1782,6 +1792,7 @@ public class CastPlayerTest { private MediaItem createMediaItem(int mediaQueueItemId) { return new MediaItem.Builder() .setUri("http://www.google.com/video" + mediaQueueItemId) + .setMediaMetadata(new MediaMetadata.Builder().setArtist("Foo Bar").build()) .setMimeType(MimeTypes.APPLICATION_MPD) .setTag(mediaQueueItemId) .build(); @@ -1821,8 +1832,12 @@ public class CastPlayerTest { int mediaQueueItemId = mediaQueueItemIds[i]; int streamType = streamTypes[i]; long durationMs = durationsMs[i]; + String contentId = + mediaItem.mediaId.equals(MediaItem.DEFAULT_MEDIA_ID) + ? mediaItem.localConfiguration.uri.toString() + : mediaItem.mediaId; MediaInfo.Builder mediaInfoBuilder = - new MediaInfo.Builder(mediaItem.localConfiguration.uri.toString()) + new MediaInfo.Builder(contentId) .setStreamType(streamType) .setContentType(mediaItem.localConfiguration.mimeType); if (durationMs != C.TIME_UNSET) { diff --git a/libraries/cast/src/test/java/androidx/media3/cast/CastTimelineTrackerTest.java b/libraries/cast/src/test/java/androidx/media3/cast/CastTimelineTrackerTest.java index 20fe12ac45..42747462a8 100644 --- a/libraries/cast/src/test/java/androidx/media3/cast/CastTimelineTrackerTest.java +++ b/libraries/cast/src/test/java/androidx/media3/cast/CastTimelineTrackerTest.java @@ -15,21 +15,30 @@ */ package androidx.media3.cast; +import static com.google.common.truth.Truth.assertThat; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import androidx.media3.common.C; +import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; +import androidx.media3.common.Timeline.Window; import androidx.media3.common.util.Util; import androidx.media3.test.utils.TimelineAsserts; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.gms.cast.MediaInfo; +import com.google.android.gms.cast.MediaQueueItem; import com.google.android.gms.cast.MediaStatus; import com.google.android.gms.cast.framework.media.MediaQueue; import com.google.android.gms.cast.framework.media.RemoteMediaClient; +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.List; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; /** Tests for {@link CastTimelineTracker}. */ @RunWith(AndroidJUnit4.class) @@ -40,10 +49,19 @@ public class CastTimelineTrackerTest { private static final long DURATION_4_MS = 4000; private static final long DURATION_5_MS = 5000; + private MediaItemConverter mediaItemConverter; + private CastTimelineTracker castTimelineTracker; + + @Before + public void init() { + mediaItemConverter = new DefaultMediaItemConverter(); + castTimelineTracker = new CastTimelineTracker(mediaItemConverter); + } + /** Tests that duration of the current media info is correctly propagated to the timeline. */ @Test public void getCastTimelinePersistsDuration() { - CastTimelineTracker tracker = new CastTimelineTracker(); + CastTimelineTracker tracker = new CastTimelineTracker(new DefaultMediaItemConverter()); RemoteMediaClient remoteMediaClient = mockRemoteMediaClient( @@ -104,10 +122,179 @@ public class CastTimelineTrackerTest { Util.msToUs(DURATION_5_MS)); } + @Test + public void getCastTimeline_onMediaItemsSet_correctMediaItemsInTimeline() { + RemoteMediaClient mockRemoteMediaClient = mock(RemoteMediaClient.class); + MediaQueue mockMediaQueue = mock(MediaQueue.class); + MediaStatus mockMediaStatus = mock(MediaStatus.class); + ImmutableList playlistMediaItems = + ImmutableList.of(createMediaItem(0), createMediaItem(1)); + MediaQueueItem[] playlistMediaQueueItems = + new MediaQueueItem[] { + createMediaQueueItem(playlistMediaItems.get(0), 0), + createMediaQueueItem(playlistMediaItems.get(1), 1) + }; + castTimelineTracker.onMediaItemsSet(playlistMediaItems, playlistMediaQueueItems); + // Mock remote media client state after adding two items. + when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1}); + when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); + when(mockMediaStatus.getCurrentItemId()).thenReturn(0); + when(mockMediaStatus.getMediaInfo()).thenReturn(playlistMediaQueueItems[0].getMedia()); + when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(playlistMediaQueueItems)); + + CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(2); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(0)); + assertThat(castTimeline.getWindow(/* windowIndex= */ 1, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(1)); + + MediaItem thirdMediaItem = createMediaItem(2); + MediaQueueItem thirdMediaQueueItem = createMediaQueueItem(thirdMediaItem, 2); + castTimelineTracker.onMediaItemsSet( + ImmutableList.of(thirdMediaItem), new MediaQueueItem[] {thirdMediaQueueItem}); + // Mock remote media client state after a single item overrides the previous playlist. + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {2}); + when(mockMediaStatus.getCurrentItemId()).thenReturn(2); + when(mockMediaStatus.getMediaInfo()).thenReturn(thirdMediaQueueItem.getMedia()); + when(mockMediaStatus.getQueueItems()).thenReturn(ImmutableList.of(thirdMediaQueueItem)); + + castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(1); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(thirdMediaItem); + } + + @Test + public void getCastTimeline_onMediaItemsAdded_correctMediaItemsInTimeline() { + RemoteMediaClient mockRemoteMediaClient = mock(RemoteMediaClient.class); + MediaQueue mockMediaQueue = mock(MediaQueue.class); + MediaStatus mockMediaStatus = mock(MediaStatus.class); + ImmutableList playlistMediaItems = + ImmutableList.of(createMediaItem(0), createMediaItem(1)); + MediaQueueItem[] playlistQueueItems = + new MediaQueueItem[] { + createMediaQueueItem(playlistMediaItems.get(0), /* uid= */ 0), + createMediaQueueItem(playlistMediaItems.get(1), /* uid= */ 1) + }; + ImmutableList secondPlaylistMediaItems = + new ImmutableList.Builder() + .addAll(playlistMediaItems) + .add(createMediaItem(2)) + .build(); + castTimelineTracker.onMediaItemsAdded(playlistMediaItems, playlistQueueItems); + when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); + when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); + // Mock remote media client state after two items have been added. + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1}); + when(mockMediaStatus.getCurrentItemId()).thenReturn(0); + when(mockMediaStatus.getMediaInfo()).thenReturn(playlistQueueItems[0].getMedia()); + when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(playlistQueueItems)); + + CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(2); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(0)); + assertThat(castTimeline.getWindow(/* windowIndex= */ 1, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(1)); + + // Mock remote media client state after adding a third item. + List playlistThreeQueueItems = + new ArrayList<>(Arrays.asList(playlistQueueItems)); + playlistThreeQueueItems.add(createMediaQueueItem(secondPlaylistMediaItems.get(2), 2)); + castTimelineTracker.onMediaItemsAdded( + secondPlaylistMediaItems, playlistThreeQueueItems.toArray(new MediaQueueItem[0])); + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1, 2}); + when(mockMediaStatus.getQueueItems()).thenReturn(playlistThreeQueueItems); + + castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(3); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(secondPlaylistMediaItems.get(0)); + assertThat(castTimeline.getWindow(/* windowIndex= */ 1, new Window()).mediaItem) + .isEqualTo(secondPlaylistMediaItems.get(1)); + assertThat(castTimeline.getWindow(/* windowIndex= */ 2, new Window()).mediaItem) + .isEqualTo(secondPlaylistMediaItems.get(2)); + } + + @Test + public void getCastTimeline_itemsRemoved_correctMediaItemsInTimelineAndMapCleanedUp() { + RemoteMediaClient mockRemoteMediaClient = mock(RemoteMediaClient.class); + MediaQueue mockMediaQueue = mock(MediaQueue.class); + MediaStatus mockMediaStatus = mock(MediaStatus.class); + ImmutableList playlistMediaItems = + ImmutableList.of(createMediaItem(0), createMediaItem(1)); + MediaQueueItem[] initialPlaylistTwoQueueItems = + new MediaQueueItem[] { + createMediaQueueItem(playlistMediaItems.get(0), 0), + createMediaQueueItem(playlistMediaItems.get(1), 1) + }; + castTimelineTracker.onMediaItemsSet(playlistMediaItems, initialPlaylistTwoQueueItems); + when(mockRemoteMediaClient.getMediaQueue()).thenReturn(mockMediaQueue); + when(mockRemoteMediaClient.getMediaStatus()).thenReturn(mockMediaStatus); + // Mock remote media client state with two items in the queue. + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {0, 1}); + when(mockMediaStatus.getCurrentItemId()).thenReturn(0); + when(mockMediaStatus.getMediaInfo()).thenReturn(initialPlaylistTwoQueueItems[0].getMedia()); + when(mockMediaStatus.getQueueItems()).thenReturn(Arrays.asList(initialPlaylistTwoQueueItems)); + + CastTimeline castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(2); + assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(2); + + // Mock remote media client state after the first item has been removed. + when(mockMediaQueue.getItemIds()).thenReturn(new int[] {1}); + when(mockMediaStatus.getCurrentItemId()).thenReturn(1); + when(mockMediaStatus.getMediaInfo()).thenReturn(initialPlaylistTwoQueueItems[1].getMedia()); + when(mockMediaStatus.getQueueItems()) + .thenReturn(ImmutableList.of(initialPlaylistTwoQueueItems[1])); + + castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(1); + assertThat(castTimeline.getWindow(/* windowIndex= */ 0, new Window()).mediaItem) + .isEqualTo(playlistMediaItems.get(1)); + // Assert that the removed item has been removed from the content ID map. + assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(1); + + // Mock remote media client state for empty queue. + when(mockRemoteMediaClient.getMediaStatus()).thenReturn(null); + when(mockMediaQueue.getItemIds()).thenReturn(new int[0]); + when(mockMediaStatus.getCurrentItemId()).thenReturn(MediaQueueItem.INVALID_ITEM_ID); + when(mockMediaStatus.getMediaInfo()).thenReturn(null); + when(mockMediaStatus.getQueueItems()).thenReturn(ImmutableList.of()); + + castTimeline = castTimelineTracker.getCastTimeline(mockRemoteMediaClient); + + assertThat(castTimeline.getWindowCount()).isEqualTo(0); + // Queue is not emptied when remote media client is empty. See [Internal ref: b/128825216]. + assertThat(castTimelineTracker.mediaItemsByContentId).hasSize(1); + } + + private MediaItem createMediaItem(int uid) { + return new MediaItem.Builder() + .setUri("http://www.google.com/" + uid) + .setMimeType(MimeTypes.AUDIO_MPEG) + .setTag(uid) + .build(); + } + + private MediaQueueItem createMediaQueueItem(MediaItem mediaItem, int uid) { + return new MediaQueueItem.Builder(mediaItemConverter.toMediaQueueItem(mediaItem)) + .setItemId(uid) + .build(); + } + private static RemoteMediaClient mockRemoteMediaClient( int[] itemIds, int currentItemId, long currentDurationMs) { - RemoteMediaClient remoteMediaClient = Mockito.mock(RemoteMediaClient.class); - MediaStatus status = Mockito.mock(MediaStatus.class); + RemoteMediaClient remoteMediaClient = mock(RemoteMediaClient.class); + MediaStatus status = mock(MediaStatus.class); when(status.getQueueItems()).thenReturn(Collections.emptyList()); when(remoteMediaClient.getMediaStatus()).thenReturn(status); when(status.getMediaInfo()).thenReturn(getMediaInfo(currentDurationMs)); @@ -118,7 +305,7 @@ public class CastTimelineTrackerTest { } private static MediaQueue mockMediaQueue(int[] itemIds) { - MediaQueue mediaQueue = Mockito.mock(MediaQueue.class); + MediaQueue mediaQueue = mock(MediaQueue.class); when(mediaQueue.getItemIds()).thenReturn(itemIds); return mediaQueue; } From a5ff4ef17f78b64735d82c6b16f0b69af5a570a3 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 12 Jul 2022 11:04:05 +0000 Subject: [PATCH 05/25] HDR: Check whether EXT_YUV_target extension is supported. This extension is needed for editing HDR input with OpenGL, as the ExternalTextureProcessor samples raw YUV values from the external texture for HDR and converts them to RGB itself rather than relying on the OpenGL driver to do this automatically as for SDR. PiperOrigin-RevId: 460424154 --- .../androidx/media3/common/util/GlUtil.java | 37 +++++++++++++++++++ .../transformer/GlEffectsFrameProcessor.java | 3 +- .../MatrixTransformationProcessor.java | 27 ++++++++------ .../VideoTranscodingSamplePipeline.java | 5 ++- 4 files changed, 57 insertions(+), 15 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 aa6abcad39..1c488a2459 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 @@ -63,6 +63,8 @@ public final class GlUtil { private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content"; // 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/OpenGL/extensions/EXT/EXT_YUV_target.txt + private static final String EXTENSION_YUV_TARGET = "GL_EXT_YUV_target"; private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_NONE = new int[] {EGL14.EGL_NONE}; private static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_8888 = @@ -170,6 +172,41 @@ public final class GlUtil { return eglExtensions != null && eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT); } + /** + * Returns whether the {@value #EXTENSION_YUV_TARGET} extension is supported. + * + *

This extension allows sampling raw YUV values from an external texture, which is required + * for HDR. + */ + public static boolean isYuvTargetExtensionSupported() { + if (Util.SDK_INT < 17) { + return false; + } + + @Nullable String glExtensions; + if (Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT)) { + // Create a placeholder context and make it current to allow calling GLES20.glGetString(). + try { + EGLDisplay eglDisplay = createEglDisplay(); + EGLContext eglContext = createEglContext(eglDisplay); + if (GlUtil.isSurfacelessContextExtensionSupported()) { + focusEglSurface( + eglDisplay, eglContext, EGL14.EGL_NO_SURFACE, /* width= */ 1, /* height= */ 1); + } else { + focusPlaceholderEglSurface(eglContext, eglDisplay); + } + glExtensions = GLES20.glGetString(GLES20.GL_EXTENSIONS); + destroyEglContext(eglDisplay, eglContext); + } catch (GlException e) { + return false; + } + } else { + glExtensions = GLES20.glGetString(GLES20.GL_EXTENSIONS); + } + + return glExtensions != null && glExtensions.contains(EXTENSION_YUV_TARGET); + } + /** Returns an initialized default {@link EGLDisplay}. */ @RequiresApi(17) public static EGLDisplay createEglDisplay() throws GlException { 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 3856a6bdee..46e4c298ed 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffectsFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffectsFrameProcessor.java @@ -53,7 +53,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * @param listener A {@link Listener}. * @param effects The {@link GlEffect GlEffects} to apply to each frame. * @param debugViewProvider A {@link DebugViewProvider}. - * @param useHdr Whether to process the input as an HDR signal. + * @param useHdr Whether to process the input as an HDR signal. Using HDR requires the {@code + * EXT_YUV_target} OpenGL extension. * @return A new instance. * @throws FrameProcessingException If reading shader files fails, or an OpenGL error occurs while * creating and configuring the OpenGL components. diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MatrixTransformationProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MatrixTransformationProcessor.java index c85bf100f1..7379cdf974 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MatrixTransformationProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MatrixTransformationProcessor.java @@ -109,7 +109,7 @@ import java.util.Arrays; context, ImmutableList.of(matrixTransformation), /* sampleFromExternalTexture= */ false, - /* enableExperimentalHdrEditing= */ false); + /* useHdr= */ false); } /** @@ -126,7 +126,7 @@ import java.util.Arrays; context, ImmutableList.of(matrixTransformation), /* sampleFromExternalTexture= */ false, - /* enableExperimentalHdrEditing= */ false); + /* useHdr= */ false); } /** @@ -138,15 +138,22 @@ import java.util.Arrays; * @param sampleFromExternalTexture Whether the input will be provided using an external texture. * If {@code true}, the caller should use {@link #setTextureTransformMatrix(float[])} to * provide the transformation matrix associated with the external texture. - * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. - * @throws FrameProcessingException If a problem occurs while reading shader files. + * @param useHdr Whether to process the input as an HDR signal. Using HDR requires the {@code + * EXT_YUV_target} OpenGL extension. + * @throws FrameProcessingException If a problem occurs while reading shader files or an OpenGL + * operation fails or is unsupported. */ public MatrixTransformationProcessor( Context context, ImmutableList matrixTransformations, boolean sampleFromExternalTexture, - boolean enableExperimentalHdrEditing) + boolean useHdr) throws FrameProcessingException { + if (sampleFromExternalTexture && useHdr && !GlUtil.isYuvTargetExtensionSupported()) { + throw new FrameProcessingException( + "The EXT_YUV_target extension is required for HDR editing."); + } + this.matrixTransformations = matrixTransformations; transformationMatrixCache = new float[matrixTransformations.size()][16]; @@ -159,13 +166,9 @@ import java.util.Arrays; String fragmentShaderFilePath; if (sampleFromExternalTexture) { vertexShaderFilePath = - enableExperimentalHdrEditing - ? VERTEX_SHADER_TRANSFORMATION_ES3_PATH - : VERTEX_SHADER_TRANSFORMATION_PATH; + useHdr ? VERTEX_SHADER_TRANSFORMATION_ES3_PATH : VERTEX_SHADER_TRANSFORMATION_PATH; fragmentShaderFilePath = - enableExperimentalHdrEditing - ? FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH - : FRAGMENT_SHADER_COPY_EXTERNAL_PATH; + useHdr ? FRAGMENT_SHADER_COPY_EXTERNAL_YUV_ES3_PATH : FRAGMENT_SHADER_COPY_EXTERNAL_PATH; } else { vertexShaderFilePath = VERTEX_SHADER_TRANSFORMATION_PATH; fragmentShaderFilePath = FRAGMENT_SHADER_COPY_PATH; @@ -177,7 +180,7 @@ import java.util.Arrays; throw new FrameProcessingException(e); } - if (enableExperimentalHdrEditing && sampleFromExternalTexture) { + if (useHdr && sampleFromExternalTexture) { // In HDR editing mode the decoder output is sampled in YUV. glProgram.setFloatsUniform("uColorTransform", MATRIX_YUV_TO_BT2020_COLOR_TRANSFORM); } 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 8b3ebe6bb2..0c97b1268f 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java @@ -111,8 +111,6 @@ import org.checkerframework.dataflow.qual.Pure; 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. useHdr = false; enableRequestSdrToneMapping = true; encoderWrapper.signalFallbackToSdr(); @@ -152,6 +150,9 @@ import org.checkerframework.dataflow.qual.Pure; streamOffsetUs, effectsListBuilder.build(), debugViewProvider, + // HDR is only used if the MediaCodec encoder supports FEATURE_HdrEditing. This + // implies that the OpenGL EXT_YUV_target extension is supported and hence the + // GlEffectsFrameProcessor also supports HDR. useHdr); } catch (FrameProcessingException e) { throw TransformationException.createForFrameProcessingException( From 66e12299881b4edbfa60d1fe717a9b6016fac2a5 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 12 Jul 2022 11:09:46 +0000 Subject: [PATCH 06/25] Set ColorInfo in decoder configuration format. Pass the color info and HDR static metadata when configuring the decoder using MediaFormatUtil.maybeSetColorInfo. PiperOrigin-RevId: 460424985 --- .../java/androidx/media3/transformer/DefaultDecoderFactory.java | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java index dcdcd6b927..277f00e261 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java @@ -79,6 +79,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; MediaFormatUtil.maybeSetInteger( mediaFormat, MediaFormat.KEY_MAX_INPUT_SIZE, format.maxInputSize); MediaFormatUtil.setCsdBuffers(mediaFormat, format.initializationData); + MediaFormatUtil.maybeSetColorInfo(mediaFormat, format.colorInfo); if (decoderSupportsKeyAllowFrameDrop) { // This key ensures no frame dropping when the decoder's output surface is full. This allows // transformer to decode as many frames as possible in one render cycle. From ad46cb1c81addfc20b7333a451741a717e85da18 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 12 Jul 2022 14:06:27 +0000 Subject: [PATCH 07/25] Group COMMAND_SET_MEDIA_ITEM and COMMAND_CHANGE_MEDIA_ITEMS together I don't think it's useful to keep these in numerical order, it makes more sense to keep them grouped into a 'logical' ordering. #minor-release PiperOrigin-RevId: 460453464 --- api.txt | 2 +- .../src/main/java/androidx/media3/cast/CastPlayer.java | 4 ++-- .../src/main/java/androidx/media3/common/Player.java | 8 ++++---- .../java/androidx/media3/exoplayer/ExoPlayerImpl.java | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api.txt b/api.txt index c4e42f5772..c2f1a94d1a 100644 --- a/api.txt +++ b/api.txt @@ -807,7 +807,7 @@ package androidx.media3.common { field public static final int TIMELINE_CHANGE_REASON_SOURCE_UPDATE = 1; // 0x1 } - @IntDef({androidx.media3.common.Player.COMMAND_INVALID, androidx.media3.common.Player.COMMAND_PLAY_PAUSE, androidx.media3.common.Player.COMMAND_PREPARE, androidx.media3.common.Player.COMMAND_STOP, androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION, androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT, androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_BACK, androidx.media3.common.Player.COMMAND_SEEK_FORWARD, androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH, androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE, androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE, androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_GET_TIMELINE, androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS, androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES, androidx.media3.common.Player.COMMAND_GET_VOLUME, androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE, androidx.media3.common.Player.COMMAND_GET_TEXT, androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, androidx.media3.common.Player.COMMAND_GET_TRACKS, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Command { + @IntDef({androidx.media3.common.Player.COMMAND_INVALID, androidx.media3.common.Player.COMMAND_PLAY_PAUSE, androidx.media3.common.Player.COMMAND_PREPARE, androidx.media3.common.Player.COMMAND_STOP, androidx.media3.common.Player.COMMAND_SEEK_TO_DEFAULT_POSITION, androidx.media3.common.Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT, androidx.media3.common.Player.COMMAND_SEEK_TO_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_SEEK_BACK, androidx.media3.common.Player.COMMAND_SEEK_FORWARD, androidx.media3.common.Player.COMMAND_SET_SPEED_AND_PITCH, androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE, androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE, androidx.media3.common.Player.COMMAND_GET_CURRENT_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_GET_TIMELINE, androidx.media3.common.Player.COMMAND_GET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA, androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM, androidx.media3.common.Player.COMMAND_CHANGE_MEDIA_ITEMS, androidx.media3.common.Player.COMMAND_GET_AUDIO_ATTRIBUTES, androidx.media3.common.Player.COMMAND_GET_VOLUME, androidx.media3.common.Player.COMMAND_GET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VOLUME, androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_ADJUST_DEVICE_VOLUME, androidx.media3.common.Player.COMMAND_SET_VIDEO_SURFACE, androidx.media3.common.Player.COMMAND_GET_TEXT, androidx.media3.common.Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, androidx.media3.common.Player.COMMAND_GET_TRACKS}) @java.lang.annotation.Documented @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) @java.lang.annotation.Target({java.lang.annotation.ElementType.FIELD, java.lang.annotation.ElementType.METHOD, java.lang.annotation.ElementType.PARAMETER, java.lang.annotation.ElementType.LOCAL_VARIABLE, java.lang.annotation.ElementType.TYPE_USE}) public static @interface Player.Command { } public static final class Player.Commands { diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index d55d73e7bd..9c7b9bcc1b 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -101,9 +101,9 @@ public final class CastPlayer extends BasePlayer { COMMAND_GET_TIMELINE, COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA, + COMMAND_SET_MEDIA_ITEM, COMMAND_CHANGE_MEDIA_ITEMS, - COMMAND_GET_TRACKS, - COMMAND_SET_MEDIA_ITEM) + COMMAND_GET_TRACKS) .build(); public static final float MIN_SPEED_SUPPORTED = 0.5f; diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java index 8cd90d2da1..4f2834b1ef 100644 --- a/libraries/common/src/main/java/androidx/media3/common/Player.java +++ b/libraries/common/src/main/java/androidx/media3/common/Player.java @@ -373,6 +373,7 @@ public interface Player { COMMAND_GET_TIMELINE, COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA, + COMMAND_SET_MEDIA_ITEM, COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_GET_AUDIO_ATTRIBUTES, COMMAND_GET_VOLUME, @@ -384,7 +385,6 @@ public interface Player { COMMAND_GET_TEXT, COMMAND_SET_TRACK_SELECTION_PARAMETERS, COMMAND_GET_TRACKS, - COMMAND_SET_MEDIA_ITEM, }; private final FlagSet.Builder flagsBuilder; @@ -1432,6 +1432,7 @@ public interface Player { COMMAND_GET_TIMELINE, COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA, + COMMAND_SET_MEDIA_ITEM, COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_GET_AUDIO_ATTRIBUTES, COMMAND_GET_VOLUME, @@ -1443,7 +1444,6 @@ public interface Player { COMMAND_GET_TEXT, COMMAND_SET_TRACK_SELECTION_PARAMETERS, COMMAND_GET_TRACKS, - COMMAND_SET_MEDIA_ITEM, }) @interface Command {} /** Command to start, pause or resume playback. */ @@ -1501,6 +1501,8 @@ public interface Player { int COMMAND_GET_MEDIA_ITEMS_METADATA = 18; /** Command to set the {@link MediaItem MediaItems} metadata. */ int COMMAND_SET_MEDIA_ITEMS_METADATA = 19; + /** Command to set a {@link MediaItem MediaItem}. */ + int COMMAND_SET_MEDIA_ITEM = 31; /** Command to change the {@link MediaItem MediaItems} in the playlist. */ int COMMAND_CHANGE_MEDIA_ITEMS = 20; /** Command to get the player current {@link AudioAttributes}. */ @@ -1523,8 +1525,6 @@ public interface Player { int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29; /** Command to get details of the current track selection. */ int COMMAND_GET_TRACKS = 30; - /** Command to set a {@link MediaItem MediaItem}. */ - int COMMAND_SET_MEDIA_ITEM = 31; /** Represents an invalid {@link Command}. */ int COMMAND_INVALID = -1; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index 5bd1058641..e95c465e9d 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -294,6 +294,7 @@ import java.util.concurrent.TimeoutException; COMMAND_GET_TIMELINE, COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA, + COMMAND_SET_MEDIA_ITEM, COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_GET_TRACKS, COMMAND_GET_AUDIO_ATTRIBUTES, @@ -303,8 +304,7 @@ import java.util.concurrent.TimeoutException; COMMAND_SET_DEVICE_VOLUME, COMMAND_ADJUST_DEVICE_VOLUME, COMMAND_SET_VIDEO_SURFACE, - COMMAND_GET_TEXT, - COMMAND_SET_MEDIA_ITEM) + COMMAND_GET_TEXT) .addIf( COMMAND_SET_TRACK_SELECTION_PARAMETERS, trackSelector.isSetParametersSupported()) .build(); From 776c8a5544357bed1c92399f3ab47fc245c0b878 Mon Sep 17 00:00:00 2001 From: claincly Date: Tue, 12 Jul 2022 14:41:02 +0000 Subject: [PATCH 08/25] Verified encoding performance, removing TODO. PiperOrigin-RevId: 460459378 --- .../java/androidx/media3/transformer/DefaultEncoderFactory.java | 1 - 1 file changed, 1 deletion(-) 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 60918c952c..38288d2872 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -476,7 +476,6 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { *

The adjustment is applied in-place to {@code mediaFormat}. */ private static void adjustMediaFormatForEncoderPerformanceSettings(MediaFormat mediaFormat) { - // TODO(b/213477153) Verify priority/operating rate settings work for non-AVC codecs. if (Util.SDK_INT < 25) { // Not setting priority and operating rate achieves better encoding performance. return; From 223922fb11f9af7510364f212939f1af65c3b5af Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 12 Jul 2022 15:54:18 +0000 Subject: [PATCH 09/25] Fix DefaultAudioSinkTest flakiness. Some calls to handleBuffer return false while a previous flush is still handled in the background. Fix this by either asserting the method returns true if we don't expect any delay, or calling it repeatedly until it returns true (within a timeout). PiperOrigin-RevId: 460474419 --- .../exoplayer/audio/DefaultAudioSinkTest.java | 163 +++++++++++++----- 1 file changed, 119 insertions(+), 44 deletions(-) diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java index 4b36260424..753434c7aa 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/audio/DefaultAudioSinkTest.java @@ -29,6 +29,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Arrays; +import java.util.concurrent.TimeoutException; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -38,6 +39,9 @@ import org.robolectric.annotation.Config; /** Unit tests for {@link DefaultAudioSink}. */ @RunWith(AndroidJUnit4.class) public final class DefaultAudioSinkTest { + + private static final long TIMEOUT_MS = 10_000; + private static final int CHANNEL_COUNT_MONO = 1; private static final int CHANNEL_COUNT_STEREO = 2; private static final int BYTES_PER_FRAME_16_BIT = 2; @@ -74,28 +78,44 @@ public final class DefaultAudioSinkTest { @Test public void handlesBufferAfterReset() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); // After reset and re-configure we can successfully queue more input. defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + retryUntilTrue( + () -> + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)); } @Test public void handlesBufferAfterReset_withPlaybackSpeed() throws Exception { defaultAudioSink.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.5f)); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); // After reset and re-configure we can successfully queue more input. defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + retryUntilTrue( + () -> + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)); assertThat(defaultAudioSink.getPlaybackParameters()) .isEqualTo(new PlaybackParameters(/* speed= */ 1.5f)); } @@ -103,28 +123,44 @@ public final class DefaultAudioSinkTest { @Test public void handlesBufferAfterReset_withFormatChange() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); // After reset and re-configure we can successfully queue more input. defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_MONO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + retryUntilTrue( + () -> + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)); } @Test public void handlesBufferAfterReset_withFormatChangeAndPlaybackSpeed() throws Exception { defaultAudioSink.setPlaybackParameters(new PlaybackParameters(/* speed= */ 1.5f)); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); // After reset and re-configure we can successfully queue more input. defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_MONO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + retryUntilTrue( + () -> + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)); assertThat(defaultAudioSink.getPlaybackParameters()) .isEqualTo(new PlaybackParameters(/* speed= */ 1.5f)); } @@ -135,8 +171,12 @@ public final class DefaultAudioSinkTest { CHANNEL_COUNT_STEREO, /* trimStartFrames= */ TRIM_100_MS_FRAME_COUNT, /* trimEndFrames= */ 0); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); assertThat(arrayAudioBufferSink.output) .hasLength( @@ -151,8 +191,12 @@ public final class DefaultAudioSinkTest { CHANNEL_COUNT_STEREO, /* trimStartFrames= */ 0, /* trimEndFrames= */ TRIM_10_MS_FRAME_COUNT); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); assertThat(arrayAudioBufferSink.output) .hasLength( @@ -167,8 +211,12 @@ public final class DefaultAudioSinkTest { CHANNEL_COUNT_STEREO, /* trimStartFrames= */ TRIM_100_MS_FRAME_COUNT, /* trimEndFrames= */ TRIM_10_MS_FRAME_COUNT); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); assertThat(arrayAudioBufferSink.output) .hasLength( @@ -180,19 +228,23 @@ public final class DefaultAudioSinkTest { @Test public void getCurrentPosition_returnsPositionFromFirstBuffer() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), - /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND, - /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); assertThat(defaultAudioSink.getCurrentPositionUs(/* sourceEnded= */ false)) .isEqualTo(5 * C.MICROS_PER_SECOND); defaultAudioSink.reset(); configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), - /* presentationTimeUs= */ 8 * C.MICROS_PER_SECOND, - /* encodedAccessUnitCount= */ 1); + retryUntilTrue( + () -> + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 8 * C.MICROS_PER_SECOND, + /* encodedAccessUnitCount= */ 1)); assertThat(defaultAudioSink.getCurrentPositionUs(/* sourceEnded= */ false)) .isEqualTo(8 * C.MICROS_PER_SECOND); } @@ -269,24 +321,32 @@ public final class DefaultAudioSinkTest { // This is demonstrating that no Exceptions are thrown as a result of handling a buffer after an // experimental flush. configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 0, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); // After the experimental flush we can successfully queue more input. defaultAudioSink.experimentalFlushWithoutAudioTrackRelease(); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), - /* presentationTimeUs= */ 5_000, - /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 5_000, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); } @Test public void getCurrentPosition_returnsUnset_afterExperimentalFlush() throws Exception { configureDefaultAudioSink(CHANNEL_COUNT_STEREO); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), - /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND, - /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); defaultAudioSink.experimentalFlushWithoutAudioTrackRelease(); assertThat(defaultAudioSink.getCurrentPositionUs(/* sourceEnded= */ false)) .isEqualTo(CURRENT_POSITION_NOT_SET); @@ -310,10 +370,12 @@ public final class DefaultAudioSinkTest { defaultAudioSink.enableTunnelingV21(); defaultAudioSink.setPlaybackParameters(new PlaybackParameters(2)); configureDefaultAudioSink(/* channelCount= */ 2); - defaultAudioSink.handleBuffer( - createDefaultSilenceBuffer(), - /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND, - /* encodedAccessUnitCount= */ 1); + assertThat( + defaultAudioSink.handleBuffer( + createDefaultSilenceBuffer(), + /* presentationTimeUs= */ 5 * C.MICROS_PER_SECOND, + /* encodedAccessUnitCount= */ 1)) + .isTrue(); assertThat(defaultAudioSink.getPlaybackParameters().speed).isEqualTo(1); } @@ -343,6 +405,19 @@ public final class DefaultAudioSinkTest { .order(ByteOrder.nativeOrder()); } + private interface ThrowingBooleanMethod { + boolean run() throws Exception; + } + + private static void retryUntilTrue(ThrowingBooleanMethod booleanMethod) throws Exception { + long timeoutTimeMs = System.currentTimeMillis() + TIMEOUT_MS; + while (!booleanMethod.run()) { + if (System.currentTimeMillis() >= timeoutTimeMs) { + throw new TimeoutException(); + } + } + } + private static final class ArrayAudioBufferSink implements TeeAudioProcessor.AudioBufferSink { private byte[] output; From 6922bd58ee844cc8293ef885918d01e2d0fbc02b Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 12 Jul 2022 16:04:45 +0000 Subject: [PATCH 10/25] Enable onMediaMetadataChanged in CastPlayer Issue: androidx/media#25 PiperOrigin-RevId: 460476841 --- RELEASENOTES.md | 2 + .../java/androidx/media3/cast/CastPlayer.java | 24 +++- .../androidx/media3/cast/CastPlayerTest.java | 112 +++++++++++++++++- 3 files changed, 134 insertions(+), 4 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cc4841b3fb..e01e5f6bee 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -40,6 +40,8 @@ `Window.mediaItem` in `CastTimeline` ([#25](https://github.com/androidx/media/issues/25), [#8212](https://github.com/google/ExoPlayer/issues/8212)). + * Support `Player.getMetadata()` and `Listener.onMediaMetadataChanged()` + with `CastPlayer` ([#25](https://github.com/androidx/media/issues/25)). ### 1.0.0-beta01 (2022-06-16) diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java index 9c7b9bcc1b..d7de97abdd 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java @@ -147,6 +147,7 @@ public final class CastPlayer extends BasePlayer { private int pendingSeekWindowIndex; private long pendingSeekPositionMs; @Nullable private PositionInfo pendingMediaItemRemovalPosition; + private MediaMetadata mediaMetadata; /** * Creates a new cast player. @@ -214,6 +215,7 @@ public final class CastPlayer extends BasePlayer { playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT); playbackState = STATE_IDLE; currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; + mediaMetadata = MediaMetadata.EMPTY; currentTracks = Tracks.EMPTY; availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build(); pendingSeekWindowIndex = C.INDEX_UNSET; @@ -427,6 +429,13 @@ public final class CastPlayer extends BasePlayer { Player.EVENT_MEDIA_ITEM_TRANSITION, listener -> listener.onMediaItemTransition(mediaItem, MEDIA_ITEM_TRANSITION_REASON_SEEK)); + MediaMetadata oldMediaMetadata = mediaMetadata; + mediaMetadata = getMediaMetadataInternal(); + if (!oldMediaMetadata.equals(mediaMetadata)) { + listeners.queueEvent( + Player.EVENT_MEDIA_METADATA_CHANGED, + listener -> listener.onMediaMetadataChanged(mediaMetadata)); + } } updateAvailableCommandsAndNotifyIfChanged(); } else if (pendingSeekCount == 0) { @@ -564,8 +573,12 @@ public final class CastPlayer extends BasePlayer { @Override public MediaMetadata getMediaMetadata() { - // CastPlayer does not currently support metadata. - return MediaMetadata.EMPTY; + return mediaMetadata; + } + + public MediaMetadata getMediaMetadataInternal() { + MediaItem currentMediaItem = getCurrentMediaItem(); + return currentMediaItem != null ? currentMediaItem.mediaMetadata : MediaMetadata.EMPTY; } @Override @@ -762,6 +775,7 @@ public final class CastPlayer extends BasePlayer { return; } int oldWindowIndex = this.currentWindowIndex; + MediaMetadata oldMediaMetadata = mediaMetadata; @Nullable Object oldPeriodUid = !getCurrentTimeline().isEmpty() @@ -773,6 +787,7 @@ public final class CastPlayer extends BasePlayer { boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged(); Timeline currentTimeline = getCurrentTimeline(); currentWindowIndex = fetchCurrentWindowIndex(remoteMediaClient, currentTimeline); + mediaMetadata = getMediaMetadataInternal(); @Nullable Object currentPeriodUid = !currentTimeline.isEmpty() @@ -826,6 +841,11 @@ public final class CastPlayer extends BasePlayer { listeners.queueEvent( Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(currentTracks)); } + if (!oldMediaMetadata.equals(mediaMetadata)) { + listeners.queueEvent( + Player.EVENT_MEDIA_METADATA_CHANGED, + listener -> listener.onMediaMetadataChanged(mediaMetadata)); + } updateAvailableCommandsAndNotifyIfChanged(); listeners.flushEvents(); } diff --git a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java index 83273d2a9a..11bbf97f79 100644 --- a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java +++ b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java @@ -67,6 +67,7 @@ import androidx.media3.common.MediaMetadata; import androidx.media3.common.MimeTypes; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; +import androidx.media3.common.Player.Listener; import androidx.media3.common.Timeline; import androidx.media3.common.util.Assertions; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -107,7 +108,7 @@ public class CastPlayerTest { @Mock private CastContext mockCastContext; @Mock private SessionManager mockSessionManager; @Mock private CastSession mockCastSession; - @Mock private Player.Listener mockListener; + @Mock private Listener mockListener; @Mock private PendingResult mockPendingResult; @Captor @@ -1042,7 +1043,9 @@ public class CastPlayerTest { castPlayer.addMediaItems(mediaItems); updateTimeLine(mediaItems, mediaQueueItemIds, /* currentItemId= */ 1); + MediaMetadata firstMediaMetadata = castPlayer.getMediaMetadata(); castPlayer.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1234); + MediaMetadata secondMediaMetadata = castPlayer.getMediaMetadata(); InOrder inOrder = Mockito.inOrder(mockListener); inOrder @@ -1053,6 +1056,8 @@ public class CastPlayerTest { .verify(mockListener) .onMediaItemTransition(eq(mediaItem2), eq(Player.MEDIA_ITEM_TRANSITION_REASON_SEEK)); inOrder.verify(mockListener, never()).onPositionDiscontinuity(any(), any(), anyInt()); + assertThat(firstMediaMetadata).isEqualTo(mediaItem1.mediaMetadata); + assertThat(secondMediaMetadata).isEqualTo(mediaItem2.mediaMetadata); } @Test @@ -1773,6 +1778,108 @@ public class CastPlayerTest { verify(mockListener).onAvailableCommandsChanged(any()); } + @Test + public void setMediaItems_doesNotifyOnMetadataChanged() { + when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null))) + .thenReturn(mockPendingResult); + ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(MediaMetadata.class); + String uri1 = "http://www.google.com/video1"; + String uri2 = "http://www.google.com/video2"; + ImmutableList firstPlaylist = + ImmutableList.of( + new MediaItem.Builder() + .setUri(uri1) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setMediaMetadata(new MediaMetadata.Builder().setArtist("foo").build()) + .setTag(1) + .build()); + ImmutableList secondPlaylist = + ImmutableList.of( + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setTag(2) + .setMediaMetadata(new MediaMetadata.Builder().setArtist("bar").build()) + .setMimeType(MimeTypes.APPLICATION_MPD) + .build(), + new MediaItem.Builder() + .setUri(uri2) + .setMimeType(MimeTypes.APPLICATION_MP4) + .setMediaMetadata(new MediaMetadata.Builder().setArtist("foobar").build()) + .setTag(3) + .build()); + castPlayer.addListener(mockListener); + + MediaMetadata intitalMetadata = castPlayer.getMediaMetadata(); + castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 2000L); + updateTimeLine(firstPlaylist, /* mediaQueueItemIds= */ new int[] {1}, /* currentItemId= */ 1); + MediaMetadata firstMetadata = castPlayer.getMediaMetadata(); + // Replacing existing playlist. + castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 0L); + updateTimeLine( + secondPlaylist, /* mediaQueueItemIds= */ new int[] {2, 3}, /* currentItemId= */ 3); + MediaMetadata secondMetadata = castPlayer.getMediaMetadata(); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 0); + MediaMetadata thirdMetadata = castPlayer.getMediaMetadata(); + + verify(mockListener, times(3)).onMediaItemTransition(mediaItemCaptor.capture(), anyInt()); + assertThat(mediaItemCaptor.getAllValues()) + .containsExactly(firstPlaylist.get(0), secondPlaylist.get(1), secondPlaylist.get(0)) + .inOrder(); + verify(mockListener, times(3)).onMediaMetadataChanged(metadataCaptor.capture()); + assertThat(metadataCaptor.getAllValues()) + .containsExactly( + firstPlaylist.get(0).mediaMetadata, + secondPlaylist.get(1).mediaMetadata, + secondPlaylist.get(0).mediaMetadata) + .inOrder(); + assertThat(intitalMetadata).isEqualTo(MediaMetadata.EMPTY); + assertThat(ImmutableList.of(firstMetadata, secondMetadata, thirdMetadata)) + .containsExactly( + firstPlaylist.get(0).mediaMetadata, + secondPlaylist.get(1).mediaMetadata, + secondPlaylist.get(0).mediaMetadata) + .inOrder(); + } + + @Test + public void setMediaItems_equalMetadata_doesNotNotifyOnMediaMetadataChanged() { + when(mockRemoteMediaClient.queueJumpToItem(anyInt(), anyLong(), eq(null))) + .thenReturn(mockPendingResult); + String uri1 = "http://www.google.com/video1"; + String uri2 = "http://www.google.com/video2"; + ImmutableList firstPlaylist = + ImmutableList.of( + new MediaItem.Builder() + .setUri(uri1) + .setMimeType(MimeTypes.APPLICATION_MPD) + .setTag(1) + .build()); + ImmutableList secondPlaylist = + ImmutableList.of( + new MediaItem.Builder() + .setMediaMetadata(MediaMetadata.EMPTY) + .setUri(Uri.EMPTY) + .setTag(2) + .setMimeType(MimeTypes.APPLICATION_MPD) + .build(), + new MediaItem.Builder() + .setUri(uri2) + .setMimeType(MimeTypes.APPLICATION_MP4) + .setTag(3) + .build()); + castPlayer.addListener(mockListener); + + castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 0, /* startPositionMs= */ 2000L); + updateTimeLine(firstPlaylist, /* mediaQueueItemIds= */ new int[] {1}, /* currentItemId= */ 1); + castPlayer.setMediaItems(secondPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 0L); + updateTimeLine( + secondPlaylist, /* mediaQueueItemIds= */ new int[] {2, 3}, /* currentItemId= */ 3); + castPlayer.seekTo(/* mediaItemIndex= */ 0, /* positionMs= */ 0); + + verify(mockListener, times(3)).onMediaItemTransition(any(), anyInt()); + verify(mockListener, never()).onMediaMetadataChanged(any()); + } + private int[] createMediaQueueItemIds(int numberOfIds) { int[] mediaQueueItemIds = new int[numberOfIds]; for (int i = 0; i < numberOfIds; i++) { @@ -1792,7 +1899,8 @@ public class CastPlayerTest { private MediaItem createMediaItem(int mediaQueueItemId) { return new MediaItem.Builder() .setUri("http://www.google.com/video" + mediaQueueItemId) - .setMediaMetadata(new MediaMetadata.Builder().setArtist("Foo Bar").build()) + .setMediaMetadata( + new MediaMetadata.Builder().setArtist("Foo Bar - " + mediaQueueItemId).build()) .setMimeType(MimeTypes.APPLICATION_MPD) .setTag(mediaQueueItemId) .build(); From 549496f1fa308fc229f539823573610bc0d06871 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 12 Jul 2022 16:31:38 +0000 Subject: [PATCH 11/25] Add method to check if tunneling is enabled. Issue: google/ExoPlayer#2518 PiperOrigin-RevId: 460482615 --- RELEASENOTES.md | 3 +++ .../java/androidx/media3/exoplayer/ExoPlayer.java | 10 ++++++++++ .../java/androidx/media3/exoplayer/ExoPlayerImpl.java | 11 +++++++++++ .../androidx/media3/exoplayer/SimpleExoPlayer.java | 6 ++++++ .../androidx/media3/test/utils/StubExoPlayer.java | 5 +++++ 5 files changed, 35 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e01e5f6bee..7460ba5075 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,9 @@ ([#9889](https://github.com/google/ExoPlayer/issues/9889)). * For progressive media, only include selected tracks in buffered position ([#10361](https://github.com/google/ExoPlayer/issues/10361)). + * Add `ExoPlayer.isTunnelingEnabled` to check if tunneling is enabled for + the currently selected tracks + ([#2518](https://github.com/google/ExoPlayer/issues/2518)). * Extractors: * Add support for AVI ([#2092](https://github.com/google/ExoPlayer/issues/2092)). diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java index 1efe41836c..422fc783e4 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayer.java @@ -1696,4 +1696,14 @@ public interface ExoPlayer extends Player { */ @UnstableApi boolean experimentalIsSleepingForOffload(); + + /** + * Returns whether tunneling is enabled for + * the currently selected tracks. + * + * @see Player.Listener#onTracksChanged(Tracks) + */ + @UnstableApi + boolean isTunnelingEnabled(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java index e95c465e9d..2327f53c34 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java @@ -1682,6 +1682,17 @@ import java.util.concurrent.TimeoutException; streamVolumeManager.setMuted(muted); } + @Override + public boolean isTunnelingEnabled() { + verifyApplicationThread(); + for (RendererConfiguration config : playbackInfo.trackSelectorResult.rendererConfigurations) { + if (config.tunneling) { + return true; + } + } + return false; + } + /* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) { this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java index 3dfd26d19b..233fd18eed 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/SimpleExoPlayer.java @@ -1258,6 +1258,12 @@ public class SimpleExoPlayer extends BasePlayer player.setDeviceMuted(muted); } + @Override + public boolean isTunnelingEnabled() { + blockUntilConstructorFinished(); + return player.isTunnelingEnabled(); + } + /* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) { blockUntilConstructorFinished(); player.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread); diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java index 28840d6ae0..0838b7e4ee 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/StubExoPlayer.java @@ -412,4 +412,9 @@ public class StubExoPlayer extends StubPlayer implements ExoPlayer { public boolean experimentalIsSleepingForOffload() { throw new UnsupportedOperationException(); } + + @Override + public boolean isTunnelingEnabled() { + throw new UnsupportedOperationException(); + } } From 704aa2531adac1a73cc284baa1a2f5e22f0c2a0b Mon Sep 17 00:00:00 2001 From: rohks Date: Tue, 12 Jul 2022 16:53:03 +0000 Subject: [PATCH 12/25] Ignore reserved bit in parsing NAL unit type `HevcConfig.parse` misreads reserved bit to determine NAL unit type. This is currently meant to be always set to 0, but could be given some kind of meaning in a future revision. Issue: google/ExoPlayer#10366 PiperOrigin-RevId: 460487613 --- .../androidx/media3/extractor/HevcConfig.java | 5 +- .../media3/extractor/HevcConfigTest.java | 90 +++++++++++++++++++ 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/HevcConfig.java b/libraries/extractor/src/main/java/androidx/media3/extractor/HevcConfig.java index ae22774b47..cbe14e9b16 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/HevcConfig.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/HevcConfig.java @@ -46,7 +46,7 @@ public final class HevcConfig { int csdLength = 0; int csdStartPosition = data.getPosition(); for (int i = 0; i < numberOfArrays; i++) { - data.skipBytes(1); // completeness (1), nal_unit_type (7) + data.skipBytes(1); // completeness (1), reserved (1), nal_unit_type (6) int numberOfNalUnits = data.readUnsignedShort(); for (int j = 0; j < numberOfNalUnits; j++) { int nalUnitLength = data.readUnsignedShort(); @@ -64,7 +64,8 @@ public final class HevcConfig { float pixelWidthHeightRatio = 1; @Nullable String codecs = null; for (int i = 0; i < numberOfArrays; i++) { - int nalUnitType = data.readUnsignedByte() & 0x7F; // completeness (1), nal_unit_type (7) + int nalUnitType = + data.readUnsignedByte() & 0x3F; // completeness (1), reserved (1), nal_unit_type (6) int numberOfNalUnits = data.readUnsignedShort(); for (int j = 0; j < numberOfNalUnits; j++) { int nalUnitLength = data.readUnsignedShort(); diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/HevcConfigTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/HevcConfigTest.java index cdcd7d8dd4..4068658a8b 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/HevcConfigTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/HevcConfigTest.java @@ -162,6 +162,86 @@ public final class HevcConfigTest { 64 }; + private static final byte[] HVCC_BOX_PAYLOAD_WITH_SET_RESERVED_BIT = + new byte[] { + // Header + 1, + 1, + 96, + 0, + 0, + 0, + -80, + 0, + 0, + 0, + 0, + 0, + -103, + -16, + 0, + -4, + -4, + -8, + -8, + 0, + 0, + 15, + + // Number of arrays + 1, + + // NAL unit type = SPS (Ignoring reserved bit) + // completeness (1), reserved (1), nal_unit_type (6) + 97, + // Number of NAL units + 0, + 1, + // NAL unit length + 0, + 39, + // NAL unit + 66, + 1, + 1, + 1, + 96, + 0, + 0, + 3, + 0, + -80, + 0, + 0, + 3, + 0, + 0, + 3, + 0, + -103, + -96, + 1, + -32, + 32, + 2, + 32, + 124, + 78, + 90, + -18, + 76, + -110, + -22, + 86, + 10, + 12, + 12, + 5, + -38, + 20, + 37 + }; + @Test public void parseHevcDecoderConfigurationRecord() throws Exception { ParsableByteArray data = new ParsableByteArray(HVCC_BOX_PAYLOAD); @@ -170,4 +250,14 @@ public final class HevcConfigTest { assertThat(hevcConfig.codecs).isEqualTo("hvc1.1.6.L153.B0"); assertThat(hevcConfig.nalUnitLengthFieldLength).isEqualTo(4); } + + /** https://github.com/google/ExoPlayer/issues/10366 */ + @Test + public void parseHevcDecoderConfigurationRecord_ignoresReservedBit() throws Exception { + ParsableByteArray data = new ParsableByteArray(HVCC_BOX_PAYLOAD_WITH_SET_RESERVED_BIT); + HevcConfig hevcConfig = HevcConfig.parse(data); + + assertThat(hevcConfig.codecs).isEqualTo("hvc1.1.6.L153.B0"); + assertThat(hevcConfig.nalUnitLengthFieldLength).isEqualTo(4); + } } From e56219f1f6ba4d3d3b7e579ba48c6a661761ee3f Mon Sep 17 00:00:00 2001 From: claincly Date: Tue, 12 Jul 2022 17:43:55 +0000 Subject: [PATCH 13/25] Fix a mis-match in encoder priority. PiperOrigin-RevId: 460500666 --- .../mh/analysis/EncoderPerformanceAnalysisTest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java index 1ef1566c1d..475d491f92 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java @@ -44,10 +44,12 @@ import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public class EncoderPerformanceAnalysisTest { - /** A non-realtime {@link MediaFormat#KEY_PRIORITY encoder priority}. */ - private static final int MEDIA_CODEC_PRIORITY_NON_REALTIME = 0; - /** A realtime {@link MediaFormat#KEY_PRIORITY encoder priority}. */ - private static final int MEDIA_CODEC_PRIORITY_REALTIME = 1; + /** A realtime {@linkplain MediaFormat#KEY_PRIORITY encoder priority}. */ + private static final int MEDIA_CODEC_PRIORITY_REALTIME = 0; + /** + * A non-realtime (as fast as possible) {@linkplain MediaFormat#KEY_PRIORITY encoder priority}. + */ + private static final int MEDIA_CODEC_PRIORITY_NON_REALTIME = 1; private static final ImmutableList INPUT_FILES = ImmutableList.of( From b61a06ba2f1aa4cc75dc1702ad94e483101adc51 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 12 Jul 2022 20:41:57 +0000 Subject: [PATCH 14/25] Don't set the tag in CastTimeline Leaving the media item that has been passed in unchanged, ensures that the media item in the timeline is equal to the media item that the user has passed into the player. The value of the tag is the uid of the window, meaning this is redundant information. #minor-release PiperOrigin-RevId: 460542246 --- .../androidx/media3/cast/CastTimeline.java | 2 +- .../androidx/media3/cast/CastPlayerTest.java | 65 +++++++------------ 2 files changed, 25 insertions(+), 42 deletions(-) diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java b/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java index d21fca2608..7cf90c5a4d 100644 --- a/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java +++ b/libraries/cast/src/main/java/androidx/media3/cast/CastTimeline.java @@ -132,7 +132,7 @@ import java.util.Arrays; int id = ids[i]; idsToIndex.put(id, i); ItemData data = itemIdToData.get(id, ItemData.EMPTY); - mediaItems[i] = data.mediaItem.buildUpon().setTag(id).build(); + mediaItems[i] = data.mediaItem; durationsUs[i] = data.durationUs; defaultPositionsUs[i] = data.defaultPositionUs == C.TIME_UNSET ? 0 : data.defaultPositionUs; isLive[i] = data.isLive; diff --git a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java index 11bbf97f79..0462878afa 100644 --- a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java +++ b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java @@ -100,6 +100,7 @@ import org.mockito.Mockito; public class CastPlayerTest { private CastPlayer castPlayer; + private DefaultMediaItemConverter mediaItemConverter; private RemoteMediaClient.Callback remoteMediaClientCallback; @Mock private RemoteMediaClient mockRemoteMediaClient; @@ -134,7 +135,8 @@ public class CastPlayerTest { when(mockRemoteMediaClient.isPaused()).thenReturn(true); when(mockMediaStatus.getQueueRepeatMode()).thenReturn(MediaStatus.REPEAT_MODE_REPEAT_OFF); when(mockMediaStatus.getPlaybackRate()).thenReturn(1.0d); - castPlayer = new CastPlayer(mockCastContext); + mediaItemConverter = new DefaultMediaItemConverter(); + castPlayer = new CastPlayer(mockCastContext, mediaItemConverter); castPlayer.addListener(mockListener); verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture()); remoteMediaClientCallback = callbackArgumentCaptor.getValue(); @@ -427,22 +429,13 @@ public class CastPlayerTest { String uri1 = "http://www.google.com/video1"; String uri2 = "http://www.google.com/video2"; firstPlaylist.add( - new MediaItem.Builder() - .setUri(uri1) - .setMimeType(MimeTypes.APPLICATION_MPD) - .setTag(1) - .build()); + new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); firstPlaylist.add( - new MediaItem.Builder() - .setUri(uri2) - .setMimeType(MimeTypes.APPLICATION_MP4) - .setTag(2) - .build()); + new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); ImmutableList secondPlaylist = ImmutableList.of( new MediaItem.Builder() .setUri(Uri.EMPTY) - .setTag(3) .setMimeType(MimeTypes.APPLICATION_MPD) .build()); @@ -472,23 +465,14 @@ public class CastPlayerTest { String uri1 = "http://www.google.com/video1"; String uri2 = "http://www.google.com/video2"; firstPlaylist.add( - new MediaItem.Builder() - .setUri(uri1) - .setMimeType(MimeTypes.APPLICATION_MPD) - .setTag(1) - .build()); + new MediaItem.Builder().setUri(uri1).setMimeType(MimeTypes.APPLICATION_MPD).build()); firstPlaylist.add( - new MediaItem.Builder() - .setUri(uri2) - .setMimeType(MimeTypes.APPLICATION_MP4) - .setTag(2) - .build()); + new MediaItem.Builder().setUri(uri2).setMimeType(MimeTypes.APPLICATION_MP4).build()); ImmutableList secondPlaylist = ImmutableList.of( new MediaItem.Builder() .setUri(Uri.EMPTY) .setMimeType(MimeTypes.APPLICATION_MPD) - .setTag(3) .build()); castPlayer.setMediaItems(firstPlaylist, /* startIndex= */ 1, /* startPositionMs= */ 2000L); @@ -556,34 +540,37 @@ public class CastPlayerTest { verify(mockRemoteMediaClient) .queueInsertItems( queueItemsArgumentCaptor.capture(), eq(MediaQueueItem.INVALID_ITEM_ID), any()); - MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri1); assertThat(mediaQueueItems[1].getMedia().getContentId()).isEqualTo(uri2); } - @SuppressWarnings("ConstantConditions") @Test public void addMediaItems_insertAtIndex_callsRemoteMediaClient() { int[] mediaQueueItemIds = createMediaQueueItemIds(/* numberOfIds= */ 2); List mediaItems = createMediaItems(mediaQueueItemIds); + // Add two items. addMediaItemsAndUpdateTimeline(mediaItems, mediaQueueItemIds); String uri = "http://www.google.com/video3"; MediaItem anotherMediaItem = new MediaItem.Builder().setUri(uri).setMimeType(MimeTypes.APPLICATION_MPD).build(); + int index = 1; + List newPlaylist = Collections.singletonList(anotherMediaItem); // Add another on position 1 - int index = 1; - castPlayer.addMediaItems(index, Collections.singletonList(anotherMediaItem)); + castPlayer.addMediaItems(index, newPlaylist); + updateTimeLine(newPlaylist, /* mediaQueueItemIds= */ new int[] {123}, /* currentItemId= */ 1); - verify(mockRemoteMediaClient) - .queueInsertItems( - queueItemsArgumentCaptor.capture(), - eq((int) mediaItems.get(index).localConfiguration.tag), - any()); - - MediaQueueItem[] mediaQueueItems = queueItemsArgumentCaptor.getValue(); - assertThat(mediaQueueItems[0].getMedia().getContentId()).isEqualTo(uri); + verify(mockRemoteMediaClient, times(2)) + .queueInsertItems(queueItemsArgumentCaptor.capture(), anyInt(), any()); + assertThat(queueItemsArgumentCaptor.getAllValues().get(1)[0]) + .isEqualTo(mediaItemConverter.toMediaQueueItem(anotherMediaItem)); + Timeline.Window currentWindow = + castPlayer + .getCurrentTimeline() + .getWindow(castPlayer.getCurrentMediaItemIndex(), new Timeline.Window()); + assertThat(currentWindow.uid).isEqualTo(123); + assertThat(currentWindow.mediaItem).isEqualTo(anotherMediaItem); } @Test @@ -722,8 +709,8 @@ public class CastPlayerTest { Timeline currentTimeline = castPlayer.getCurrentTimeline(); for (int i = 0; i < mediaItems.size(); i++) { - assertThat(currentTimeline.getWindow(/* windowIndex= */ i, window).uid) - .isEqualTo(mediaItems.get(i).localConfiguration.tag); + assertThat(currentTimeline.getWindow(/* windowIndex= */ i, window).mediaItem) + .isEqualTo(mediaItems.get(i)); } } @@ -1791,13 +1778,11 @@ public class CastPlayerTest { .setUri(uri1) .setMimeType(MimeTypes.APPLICATION_MPD) .setMediaMetadata(new MediaMetadata.Builder().setArtist("foo").build()) - .setTag(1) .build()); ImmutableList secondPlaylist = ImmutableList.of( new MediaItem.Builder() .setUri(Uri.EMPTY) - .setTag(2) .setMediaMetadata(new MediaMetadata.Builder().setArtist("bar").build()) .setMimeType(MimeTypes.APPLICATION_MPD) .build(), @@ -1805,7 +1790,6 @@ public class CastPlayerTest { .setUri(uri2) .setMimeType(MimeTypes.APPLICATION_MP4) .setMediaMetadata(new MediaMetadata.Builder().setArtist("foobar").build()) - .setTag(3) .build()); castPlayer.addListener(mockListener); @@ -1902,7 +1886,6 @@ public class CastPlayerTest { .setMediaMetadata( new MediaMetadata.Builder().setArtist("Foo Bar - " + mediaQueueItemId).build()) .setMimeType(MimeTypes.APPLICATION_MPD) - .setTag(mediaQueueItemId) .build(); } From f9a39201aafdb9b660d8c57fb8ffa051a9dc31c7 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 12 Jul 2022 20:43:24 +0000 Subject: [PATCH 15/25] Add migration script Note: This was already reviewed in . This doesn't mean we cannot apply further changes though. PiperOrigin-RevId: 460542835 --- github/media3-migration.sh | 386 +++++++++++++++++++++++++++++++++++++ 1 file changed, 386 insertions(+) create mode 100644 github/media3-migration.sh diff --git a/github/media3-migration.sh b/github/media3-migration.sh new file mode 100644 index 0000000000..f80ac4dfa3 --- /dev/null +++ b/github/media3-migration.sh @@ -0,0 +1,386 @@ +#!/bin/bash +# Copyright (C) 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. +## +shopt -s extglob + +PACKAGE_MAPPINGS='com.google.android.exoplayer2 androidx.media3.exoplayer +com.google.android.exoplayer2.analytics androidx.media3.exoplayer.analytics +com.google.android.exoplayer2.audio androidx.media3.exoplayer.audio +com.google.android.exoplayer2.castdemo androidx.media3.demo.cast +com.google.android.exoplayer2.database androidx.media3.database +com.google.android.exoplayer2.decoder androidx.media3.decoder +com.google.android.exoplayer2.demo androidx.media3.demo.main +com.google.android.exoplayer2.drm androidx.media3.exoplayer.drm +com.google.android.exoplayer2.ext.av1 androidx.media3.decoder.av1 +com.google.android.exoplayer2.ext.cast androidx.media3.cast +com.google.android.exoplayer2.ext.cronet androidx.media3.datasource.cronet +com.google.android.exoplayer2.ext.ffmpeg androidx.media3.decoder.ffmpeg +com.google.android.exoplayer2.ext.flac androidx.media3.decoder.flac +com.google.android.exoplayer2.ext.ima androidx.media3.exoplayer.ima +com.google.android.exoplayer2.ext.leanback androidx.media3.ui.leanback +com.google.android.exoplayer2.ext.okhttp androidx.media3.datasource.okhttp +com.google.android.exoplayer2.ext.opus androidx.media3.decoder.opus +com.google.android.exoplayer2.ext.rtmp androidx.media3.datasource.rtmp +com.google.android.exoplayer2.ext.vp9 androidx.media3.decoder.vp9 +com.google.android.exoplayer2.ext.workmanager androidx.media3.exoplayer.workmanager +com.google.android.exoplayer2.extractor androidx.media3.extractor +com.google.android.exoplayer2.gldemo androidx.media3.demo.gl +com.google.android.exoplayer2.mediacodec androidx.media3.exoplayer.mediacodec +com.google.android.exoplayer2.metadata androidx.media3.extractor.metadata +com.google.android.exoplayer2.offline androidx.media3.exoplayer.offline +com.google.android.exoplayer2.playbacktests androidx.media3.test.exoplayer.playback +com.google.android.exoplayer2.robolectric androidx.media3.test.utils.robolectric +com.google.android.exoplayer2.scheduler androidx.media3.exoplayer.scheduler +com.google.android.exoplayer2.source androidx.media3.exoplayer.source +com.google.android.exoplayer2.source.dash androidx.media3.exoplayer.dash +com.google.android.exoplayer2.source.hls androidx.media3.exoplayer.hls +com.google.android.exoplayer2.source.rtsp androidx.media3.exoplayer.rtsp +com.google.android.exoplayer2.source.smoothstreaming androidx.media3.exoplayer.smoothstreaming +com.google.android.exoplayer2.surfacedemo androidx.media3.demo.surface +com.google.android.exoplayer2.testdata androidx.media3.test.data +com.google.android.exoplayer2.testutil androidx.media3.test.utils +com.google.android.exoplayer2.text androidx.media3.extractor.text +com.google.android.exoplayer2.trackselection androidx.media3.exoplayer.trackselection +com.google.android.exoplayer2.transformer androidx.media3.transformer +com.google.android.exoplayer2.transformerdemo androidx.media3.demo.transformer +com.google.android.exoplayer2.ui androidx.media3.ui +com.google.android.exoplayer2.upstream androidx.media3.datasource +com.google.android.exoplayer2.upstream.cache androidx.media3.datasource.cache +com.google.android.exoplayer2.upstream.crypto androidx.media3.exoplayer.upstream.crypto +com.google.android.exoplayer2.util androidx.media3.common.util +com.google.android.exoplayer2.util androidx.media3.exoplayer.util +com.google.android.exoplayer2.video androidx.media3.exoplayer.video' + + +CLASS_RENAMINGS='com.google.android.exoplayer2.ui.StyledPlayerView androidx.media3.ui.PlayerView +StyledPlayerView PlayerView +com.google.android.exoplayer2.ui.StyledPlayerControlView androidx.media3.ui.PlayerControlView +StyledPlayerControlView PlayerControlView +com.google.android.exoplayer2.ExoPlayerLibraryInfo androidx.media3.common.MediaLibraryInfo +ExoPlayerLibraryInfo MediaLibraryInfo +com.google.android.exoplayer2.SimpleExoPlayer androidx.media3.exoplayer.ExoPlayer +SimpleExoPlayer ExoPlayer' + +CLASS_MAPPINGS='com.google.android.exoplayer2.text.span androidx.media3.common.text HorizontalTextInVerticalContextSpan LanguageFeatureSpan RubySpan SpanUtil TextAnnotation TextEmphasisSpan +com.google.android.exoplayer2.text androidx.media3.common.text CueGroup Cue +com.google.android.exoplayer2.text androidx.media3.exoplayer.text ExoplayerCuesDecoder SubtitleDecoderFactory TextOutput TextRenderer +com.google.android.exoplayer2.upstream.crypto androidx.media3.datasource AesCipherDataSource AesCipherDataSink AesFlushingCipher +com.google.android.exoplayer2.util androidx.media3.common.util AtomicFile Assertions BundleableUtil BundleUtil Clock ClosedSource CodecSpecificDataUtil ColorParser ConditionVariable Consumer CopyOnWriteMultiset EGLSurfaceTexture GlProgram GlUtil HandlerWrapper LibraryLoader ListenerSet Log LongArray MediaFormatUtil NetworkTypeObserver NonNullApi NotificationUtil ParsableBitArray ParsableByteArray RepeatModeUtil RunnableFutureTask SystemClock SystemHandlerWrapper TimedValueQueue TimestampAdjuster TraceUtil UnknownNull UnstableApi UriUtil Util XmlPullParserUtil +com.google.android.exoplayer2.util androidx.media3.common ErrorMessageProvider FlagSet FileTypes MimeTypes PriorityTaskManager +com.google.android.exoplayer2.metadata androidx.media3.common Metadata +com.google.android.exoplayer2.metadata androidx.media3.exoplayer.metadata MetadataDecoderFactory MetadataOutput MetadataRenderer +com.google.android.exoplayer2.audio androidx.media3.common AudioAttributes AuxEffectInfo +com.google.android.exoplayer2.ui androidx.media3.common AdOverlayInfo AdViewProvider +com.google.android.exoplayer2.source.ads androidx.media3.common AdPlaybackState +com.google.android.exoplayer2.source androidx.media3.common MediaPeriodId TrackGroup +com.google.android.exoplayer2.offline androidx.media3.common StreamKey +com.google.android.exoplayer2.ui androidx.media3.exoplayer.offline DownloadNotificationHelper +com.google.android.exoplayer2.trackselection androidx.media3.common TrackSelectionParameters TrackSelectionOverride +com.google.android.exoplayer2.video androidx.media3.common ColorInfo VideoSize +com.google.android.exoplayer2.upstream androidx.media3.common DataReader +com.google.android.exoplayer2.upstream androidx.media3.exoplayer.upstream Allocation Allocator BandwidthMeter CachedRegionTracker DefaultAllocator DefaultBandwidthMeter DefaultLoadErrorHandlingPolicy Loader LoaderErrorThrower ParsingLoadable SlidingPercentile TimeToFirstByteEstimator +com.google.android.exoplayer2.audio androidx.media3.extractor AacUtil Ac3Util Ac4Util DtsUtil MpegAudioUtil OpusUtil WavUtil +com.google.android.exoplayer2.util androidx.media3.extractor NalUnitUtil ParsableNalUnitBitArray +com.google.android.exoplayer2.video androidx.media3.extractor AvcConfig DolbyVisionConfig HevcConfig +com.google.android.exoplayer2.decoder androidx.media3.exoplayer DecoderCounters DecoderReuseEvaluation +com.google.android.exoplayer2.util androidx.media3.exoplayer MediaClock StandaloneMediaClock +com.google.android.exoplayer2 androidx.media3.exoplayer FormatHolder PlayerMessage +com.google.android.exoplayer2 androidx.media3.common BasePlayer BundleListRetriever Bundleable ControlDispatcher C DefaultControlDispatcher DeviceInfo ErrorMessageProvider ExoPlayerLibraryInfo Format ForwardingPlayer HeartRating IllegalSeekPositionException MediaItem MediaMetadata ParserException PercentageRating PlaybackException PlaybackParameters Player PositionInfo Rating StarRating ThumbRating Timeline Tracks +com.google.android.exoplayer2.drm androidx.media3.common DrmInitData' + +DEPENDENCY_MAPPINGS='exoplayer media3-exoplayer +exoplayer-common media3-common +exoplayer-core media3-exoplayer +exoplayer-dash media3-exoplayer-dash +exoplayer-database media3-database +exoplayer-datasource media-datasource +exoplayer-decoder media3-decoder +exoplayer-extractor media3-extractor +exoplayer-hls media3-exoplayer-hls +exoplayer-robolectricutils media3-test-utils-robolectric +exoplayer-rtsp media3-exoplayer-rtsp +exoplayer-smoothstreaming media3-exoplayer-smoothstreaming +exoplayer-testutils media3-test-utils +exoplayer-transformer media3-transformer +exoplayer-ui media3-ui +extension-cast media3-cast +extension-cronet media3-datasource-cronet +extension-ima media3-exoplayer-ima +extension-leanback media3-ui-leanback +extension-okhttp media3-datasource-okhttp +extension-rtmp media3-datasource-rtmp +extension-workmanager media3-exoplayer-workmanager' + +# Rewrites classes, packages and dependencies from the legacy ExoPlayer package structure +# to androidx.media3 structure. + +MEDIA3_VERSION="1.0.0-beta02" +LEGACY_PEER_VERSION="2.18.1" + +function usage() { + echo "usage: $0 [-p|-c|-d|-v]|[-m|-l [-x ] [-f] PROJECT_ROOT]" + echo " PROJECT_ROOT: path to your project root (location of 'gradlew')" + echo " -p: list package mappings and then exit" + echo " -c: list class mappings (precedence over package mappings) and then exit" + echo " -d: list dependency mappings and then exit" + echo " -m: migrate packages, classes and dependencies to AndroidX Media3" + echo " -l: list files that will be considered for rewrite and then exit" + echo " -x: exclude the path from the list of file to be changed: 'app/src/test'" + echo " -f: force the action even when validation fails" + echo " -v: print the exoplayer2/media3 version strings of this script and exit" + echo " --noclean : Do not call './gradlew clean' in project directory." + echo " -h, --help: show this help text" +} + +function print_pairs { + while read -r line; + do + IFS=' ' read -ra PAIR <<< "$line" + printf "%-55s %-30s\n" "${PAIR[0]}" "${PAIR[1]}" + done <<< "$(echo "$@")" +} + +function print_class_mappings { + while read -r mapping; + do + old=$(echo "$mapping" | cut -d ' ' -f1) + new=$(echo "$mapping" | cut -d ' ' -f2) + classes=$(echo "$mapping" | cut -d ' ' -f3-) + for clazz in $classes; + do + printf "%-80s %-30s\n" "$old.$clazz" "$new.$clazz" + done + done <<< "$(echo "$CLASS_MAPPINGS" | sort)" +} + +ERROR_COUNTER=0 +VALIDATION_ERRORS='' + +function add_validation_error { + let ERROR_COUNTER++ + VALIDATION_ERRORS+="\033[31m[$ERROR_COUNTER] ->\033[0m ${1}" +} + +function validate_exoplayer_version() { + has_exoplayer_dependency='' + while read -r file; + do + local version + version=$(grep -m 1 "com\.google\.android\.exoplayer:" "$file" | cut -d ":" -f3 | tr -d \" | tr -d \') + if [[ ! -z $version ]] && [[ ! "$version" =~ $LEGACY_PEER_VERSION ]]; + then + add_validation_error "The version does not match '$LEGACY_PEER_VERSION'. \ +Update to '$LEGACY_PEER_VERSION' or use the migration script matching your \ +current version. Current version '$version' found in\n $file\n" + fi + done <<< "$(find . -type f -name "build.gradle")" +} + +function validate_string_not_contained { + local pattern=$1 # regex + local failure_message=$2 + while read -r file; + do + if grep -q -e "$pattern" "$file"; + then + add_validation_error "$failure_message:\n $file\n" + fi + done <<< "$files" +} + +function validate_string_patterns { + validate_string_not_contained \ + 'com\.google\.android\.exoplayer2\..*\*' \ + 'Replace wildcard import statements with fully qualified import statements'; + validate_string_not_contained \ + 'com\.google\.android\.exoplayer2\.ui\.PlayerView' \ + 'Migrate PlayerView to StyledPlayerView before migrating'; + validate_string_not_contained \ + 'LegacyPlayerView' \ + 'Migrate LegacyPlayerView to StyledPlayerView before migrating'; + validate_string_not_contained \ + 'com\.google\.android\.exoplayer2\.ext\.mediasession' \ + 'The MediaSessionConnector is integrated in androidx.media3.session.MediaSession' +} + +SED_CMD_INPLACE='sed -i ' +if [[ "$OSTYPE" == "darwin"* ]]; then + SED_CMD_INPLACE="sed -i '' " +fi + +MIGRATE_FILES='1' +LIST_FILES_ONLY='1' +PRINT_CLASS_MAPPINGS='1' +PRINT_PACKAGE_MAPPINGS='1' +PRINT_DEPENDENCY_MAPPINGS='1' +PRINT_VERSION='1' +NO_CLEAN='1' +FORCE='1' +IGNORE_VERSION='1' +EXCLUDED_PATHS='' + +while [[ $1 =~ ^-.* ]]; +do + case "$1" in + -m ) MIGRATE_FILES='';; + -l ) LIST_FILES_ONLY='';; + -c ) PRINT_CLASS_MAPPINGS='';; + -p ) PRINT_PACKAGE_MAPPINGS='';; + -d ) PRINT_DEPENDENCY_MAPPINGS='';; + -v ) PRINT_VERSION='';; + -f ) FORCE='';; + -x ) shift; EXCLUDED_PATHS="$(printf "%s\n%s" $EXCLUDED_PATHS $1)";; + --noclean ) NO_CLEAN='';; + * ) usage && exit 1;; + esac + shift +done + +if [[ -z $PRINT_DEPENDENCY_MAPPINGS ]]; +then + print_pairs "$DEPENDENCY_MAPPINGS" + exit 0 +elif [[ -z $PRINT_PACKAGE_MAPPINGS ]]; +then + print_pairs "$PACKAGE_MAPPINGS" + exit 0 +elif [[ -z $PRINT_CLASS_MAPPINGS ]]; +then + print_class_mappings + exit 0 +elif [[ -z $PRINT_VERSION ]]; +then + echo "$LEGACY_PEER_VERSION -> $MEDIA3_VERSION. This script is written to migrate from ExoPlayer $LEGACY_PEER_VERSION to AndroidX Media3 $MEDIA3_VERSION" + exit 0 +elif [[ -z $1 ]]; +then + usage + exit 1 +fi + +if [[ ! -f $1/gradlew ]]; +then + echo "directory seems not to exist or is not a gradle project (missing 'gradlew')" + usage + exit 1 +fi + +PROJECT_ROOT=$1 +cd "$PROJECT_ROOT" + +# Create the set of files to transform +exclusion="/build/|/.idea/|/res/drawable|/res/color|/res/mipmap|/res/values|" +if [[ ! -z $EXCLUDED_PATHS ]]; +then + while read -r path; + do + exclusion="$exclusion./$path|" + done <<< "$EXCLUDED_PATHS" +fi +files=$(find . -name '*\.java' -o -name '*\.kt' -o -name '*\.xml' | grep -Ev "'$exclusion'") + +# Validate project and exit in case of validation errors +validate_string_patterns +validate_exoplayer_version "$PROJECT_ROOT" +if [[ ! -z $FORCE && ! -z "$VALIDATION_ERRORS" ]]; +then + echo "=============================================" + echo "Validation errors (use -f to force execution)" + echo "---------------------------------------------" + echo -e "$VALIDATION_ERRORS" + exit 1 +fi + +if [[ -z $LIST_FILES_ONLY ]]; +then + echo "$files" | cut -c 3- + find . -type f -name 'build\.gradle' | cut -c 3- + exit 0 +fi + +# start migration after successful validation or when forced to disregard validation +# errors + +if [[ ! -z "$MIGRATE_FILES" ]]; +then + echo "nothing to do" + usage + exit 0 +fi + +PWD=$(pwd) +if [[ ! -z $NO_CLEAN ]]; +then + cd "$PROJECT_ROOT" + ./gradlew clean + cd "$PWD" +fi + +# create expressions for class renamings +renaming_expressions='' +while read -r renaming; +do + src=$(echo "$renaming" | cut -d ' ' -f1 | sed -e 's/\./\\\./g') + dest=$(echo "$renaming" | cut -d ' ' -f2) + renaming_expressions+="-e s/$src/$dest/g " +done <<< "$CLASS_RENAMINGS" + +# create expressions for class mappings +classes_expressions='' +while read -r mapping; +do + src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g') + dest=$(echo "$mapping" | cut -d ' ' -f2) + classes=$(echo "$mapping" | cut -d ' ' -f3-) + for clazz in $classes; + do + classes_expressions+="-e s/$src\.$clazz/$dest.$clazz/g " + done +done <<< "$CLASS_MAPPINGS" + +# create expressions for package mappings +packages_expressions='' +while read -r mapping; +do + src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g') + dest=$(echo "$mapping" | cut -d ' ' -f2) + packages_expressions+="-e s/$src/$dest/g " +done <<< "$PACKAGE_MAPPINGS" + +# do search and replace with expressions in each selected file +while read -r file; +do + echo "migrating $file" + expr="$renaming_expressions $classes_expressions $packages_expressions" + $SED_CMD_INPLACE $expr $file +done <<< "$files" + +# create expressions for dependencies in gradle files +EXOPLAYER_GROUP="com\.google\.android\.exoplayer" +MEDIA3_GROUP="androidx.media3" +dependency_expressions="" +while read -r mapping +do + OLD=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g') + NEW=$(echo "$mapping" | cut -d ' ' -f2) + dependency_expressions="$dependency_expressions -e s/$EXOPLAYER_GROUP:$OLD:.*\"/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION\"/g -e s/$EXOPLAYER_GROUP:$OLD:.*'/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION'/" +done <<< "$DEPENDENCY_MAPPINGS" + +## do search and replace for dependencies in gradle files +while read -r build_file; +do + echo "migrating build file $build_file" + $SED_CMD_INPLACE $dependency_expressions $build_file +done <<< "$(find . -type f -name 'build\.gradle')" From 40fd3ffa6c3ea058e58e59b533fcaadb377c94d2 Mon Sep 17 00:00:00 2001 From: claincly Date: Wed, 13 Jul 2022 09:27:06 +0000 Subject: [PATCH 16/25] Fix two typos in RtpVp8Reader and test PiperOrigin-RevId: 460662425 --- .../androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java | 2 +- .../media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java index 31bc245f8e..46354e3ae1 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8Reader.java @@ -36,7 +36,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /* package */ final class RtpVp8Reader implements RtpPayloadReader { private static final String TAG = "RtpVP8Reader"; - /** VP9 uses a 90 KHz media clock (RFC7741 Section 4.1). */ + /** VP8 uses a 90 KHz media clock (RFC7741 Section 4.1). */ private static final long MEDIA_CLOCK_FREQUENCY = 90_000; private final RtpPayloadFormat payloadFormat; diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java index 61f80c6c2d..73ffe05fc5 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/reader/RtpVp8ReaderTest.java @@ -39,7 +39,7 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class RtpVp8ReaderTest { - /** VP9 uses a 90 KHz media clock (RFC7741 Section 4.1). */ + /** VP8 uses a 90 KHz media clock (RFC7741 Section 4.1). */ private static final long MEDIA_CLOCK_FREQUENCY = 90_000; private static final byte[] PARTITION_1 = getBytesFromHexString("000102030405060708090A0B0C0D0E"); @@ -61,7 +61,7 @@ public final class RtpVp8ReaderTest { new RtpPacket.Builder() .setTimestamp(PARTITION_1_RTP_TIMESTAMP) .setSequenceNumber(40290) - .setMarker(false) + .setMarker(true) .setPayloadData(Bytes.concat(getBytesFromHexString("00"), PARTITION_1_FRAGMENT_2)) .build(); From a88426ae58dc2ce31e1c4c0b7838f5e464107dcd Mon Sep 17 00:00:00 2001 From: samrobinson Date: Wed, 13 Jul 2022 12:16:59 +0000 Subject: [PATCH 17/25] Clarify format is supported by encoder. #cleanup #minor-release PiperOrigin-RevId: 460688226 --- .../transformer/DefaultEncoderFactory.java | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 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 38288d2872..8427f2cb4a 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -231,24 +231,34 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { } MediaCodecInfo encoderInfo = encoderAndClosestFormatSupport.encoder; - format = encoderAndClosestFormatSupport.supportedFormat; + Format encoderSupportedFormat = encoderAndClosestFormatSupport.supportedFormat; VideoEncoderSettings supportedVideoEncoderSettings = encoderAndClosestFormatSupport.supportedEncoderSettings; - String mimeType = checkNotNull(format.sampleMimeType); - MediaFormat mediaFormat = MediaFormat.createVideoFormat(mimeType, format.width, format.height); - mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, round(format.frameRate)); + String mimeType = checkNotNull(encoderSupportedFormat.sampleMimeType); + MediaFormat mediaFormat = + MediaFormat.createVideoFormat( + mimeType, encoderSupportedFormat.width, encoderSupportedFormat.height); + mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, round(encoderSupportedFormat.frameRate)); int bitrate; if (supportedVideoEncoderSettings.enableHighQualityTargeting) { bitrate = new DeviceMappedEncoderBitrateProvider() - .getBitrate(encoderInfo.getName(), format.width, format.height, format.frameRate); + .getBitrate( + encoderInfo.getName(), + encoderSupportedFormat.width, + encoderSupportedFormat.height, + encoderSupportedFormat.frameRate); } else if (supportedVideoEncoderSettings.bitrate != VideoEncoderSettings.NO_VALUE) { bitrate = supportedVideoEncoderSettings.bitrate; } else { - bitrate = getSuggestedBitrate(format.width, format.height, format.frameRate); + bitrate = + getSuggestedBitrate( + encoderSupportedFormat.width, + encoderSupportedFormat.height, + encoderSupportedFormat.frameRate); } mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); @@ -267,7 +277,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { adjustMediaFormatForH264EncoderSettings(mediaFormat, encoderInfo); } - MediaFormatUtil.maybeSetColorInfo(mediaFormat, format.colorInfo); + MediaFormatUtil.maybeSetColorInfo(mediaFormat, encoderSupportedFormat.colorInfo); mediaFormat.setInteger( MediaFormat.KEY_COLOR_FORMAT, supportedVideoEncoderSettings.colorProfile); @@ -303,7 +313,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { return new DefaultCodec( context, - format, + encoderSupportedFormat, mediaFormat, encoderInfo.getName(), /* isDecoder= */ false, @@ -396,7 +406,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { VideoEncoderSettings.NO_VALUE, VideoEncoderSettings.NO_VALUE); } - Format supportedEncoderFormat = + Format encoderSupportedFormat = requestedFormat .buildUpon() .setSampleMimeType(mimeType) @@ -405,7 +415,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { .setAverageBitrate(closestSupportedBitrate) .build(); return new VideoEncoderQueryResult( - pickedEncoderInfo, supportedEncoderFormat, supportedEncodingSettingBuilder.build()); + pickedEncoderInfo, encoderSupportedFormat, supportedEncodingSettingBuilder.build()); } /** Returns a list of encoders that support the requested resolution most closely. */ From f903869eb880508812c0dc2e1a8b01ecf0597f45 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Wed, 13 Jul 2022 12:48:02 +0000 Subject: [PATCH 18/25] Fix assertion error when using high quality targeting API. Add test that verifies SSIM with API enabled. #minor-release PiperOrigin-RevId: 460692420 --- .../transformer/mh/TranscodeQualityTest.java | 15 ++++-- .../transformer/DefaultEncoderFactory.java | 46 +++++++++++-------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java index ba8f173695..1568f9002d 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TranscodeQualityTest.java @@ -23,10 +23,12 @@ import android.net.Uri; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.transformer.AndroidTestUtil; +import androidx.media3.transformer.DefaultEncoderFactory; import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.TransformationTestResult; import androidx.media3.transformer.Transformer; import androidx.media3.transformer.TransformerAndroidTestRunner; +import androidx.media3.transformer.VideoEncoderSettings; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import org.junit.Test; @@ -36,9 +38,10 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class TranscodeQualityTest { @Test - public void transformWithDecodeEncode_ssimIsGreaterThan90Percent() throws Exception { + public void transformHighQualityTargetingAvcToAvc1920x1080_ssimIsGreaterThan95Percent() + throws Exception { Context context = ApplicationProvider.getApplicationContext(); - String testId = "transformWithDecodeEncode_ssim"; + String testId = "transformHighQualityTargetingAvcToAvc1920x1080_ssim"; if (AndroidTestUtil.skipAndLogIfInsufficientCodecSupport( context, @@ -52,7 +55,13 @@ public final class TranscodeQualityTest { new Transformer.Builder(context) .setTransformationRequest( new TransformationRequest.Builder().setVideoMimeType(MimeTypes.VIDEO_H264).build()) - .setEncoderFactory(new AndroidTestUtil.ForceEncodeEncoderFactory(context)) + .setEncoderFactory( + new DefaultEncoderFactory.Builder(context) + .setRequestedVideoEncoderSettings( + new VideoEncoderSettings.Builder() + .setEnableHighQualityTargeting(true) + .build()) + .build()) .setRemoveAudio(true) .build(); 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 8427f2cb4a..51d4d15782 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -241,27 +241,27 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { mimeType, encoderSupportedFormat.width, encoderSupportedFormat.height); mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, round(encoderSupportedFormat.frameRate)); - int bitrate; - if (supportedVideoEncoderSettings.enableHighQualityTargeting) { - bitrate = + int bitrate = new DeviceMappedEncoderBitrateProvider() .getBitrate( encoderInfo.getName(), encoderSupportedFormat.width, encoderSupportedFormat.height, encoderSupportedFormat.frameRate); - } else if (supportedVideoEncoderSettings.bitrate != VideoEncoderSettings.NO_VALUE) { - bitrate = supportedVideoEncoderSettings.bitrate; - } else { - bitrate = + encoderSupportedFormat = + encoderSupportedFormat.buildUpon().setAverageBitrate(bitrate).build(); + } else if (encoderSupportedFormat.bitrate == Format.NO_VALUE) { + int bitrate = getSuggestedBitrate( encoderSupportedFormat.width, encoderSupportedFormat.height, encoderSupportedFormat.frameRate); + encoderSupportedFormat = + encoderSupportedFormat.buildUpon().setAverageBitrate(bitrate).build(); } - mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); + mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, encoderSupportedFormat.averageBitrate); mediaFormat.setInteger(MediaFormat.KEY_BITRATE_MODE, supportedVideoEncoderSettings.bitrateMode); if (supportedVideoEncoderSettings.profile != VideoEncoderSettings.NO_VALUE @@ -391,11 +391,23 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { return null; } + // TODO(b/238094555): Check encoder supports bitrate targeted by high quality. MediaCodecInfo pickedEncoderInfo = filteredEncoderInfos.get(0); int closestSupportedBitrate = EncoderUtil.getSupportedBitrateRange(pickedEncoderInfo, mimeType).clamp(requestedBitrate); - VideoEncoderSettings.Builder supportedEncodingSettingBuilder = - videoEncoderSettings.buildUpon().setBitrate(closestSupportedBitrate); + + VideoEncoderSettings.Builder supportedEncodingSettingBuilder = videoEncoderSettings.buildUpon(); + Format.Builder encoderSupportedFormatBuilder = + requestedFormat + .buildUpon() + .setSampleMimeType(mimeType) + .setWidth(finalResolution.getWidth()) + .setHeight(finalResolution.getHeight()); + + if (!videoEncoderSettings.enableHighQualityTargeting) { + supportedEncodingSettingBuilder.setBitrate(closestSupportedBitrate); + encoderSupportedFormatBuilder.setAverageBitrate(closestSupportedBitrate); + } if (videoEncoderSettings.profile == VideoEncoderSettings.NO_VALUE || videoEncoderSettings.level == VideoEncoderSettings.NO_VALUE @@ -406,16 +418,10 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { VideoEncoderSettings.NO_VALUE, VideoEncoderSettings.NO_VALUE); } - Format encoderSupportedFormat = - requestedFormat - .buildUpon() - .setSampleMimeType(mimeType) - .setWidth(finalResolution.getWidth()) - .setHeight(finalResolution.getHeight()) - .setAverageBitrate(closestSupportedBitrate) - .build(); return new VideoEncoderQueryResult( - pickedEncoderInfo, encoderSupportedFormat, supportedEncodingSettingBuilder.build()); + pickedEncoderInfo, + encoderSupportedFormatBuilder.build(), + supportedEncodingSettingBuilder.build()); } /** Returns a list of encoders that support the requested resolution most closely. */ @@ -650,7 +656,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { * */ private static int getSuggestedBitrate(int width, int height, float frameRate) { - // TODO(b/210591626) Refactor into a BitrateProvider. + // TODO(b/238094555) Refactor into a BitrateProvider. // Assume medium motion factor. // 1080p60 -> 16.6Mbps, 720p30 -> 3.7Mbps. return (int) (width * height * frameRate * 0.07 * 2); From 9a616c0cee447b7bd809c0dfc4c9d864fc9fee56 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Jul 2022 13:24:04 +0000 Subject: [PATCH 19/25] Use SingleThreadExecutor to release AudioTracks We currently start a simple Thread to release AudioTracks asynchronously. If many AudioTracks are released at the same time, this may lead to OOM situations because we attempt to create multiple new threads. This can be improved by using a shared SingleThreadExecutor. In the simple case of one simmultaneous release, it's exactly the same behavior as before: create a thread and release it as soon as it's done. For multiple simultanous releases we get the advantage of sharing a single thread to avoid creating more than one at the same time. Issue: google/ExoPlayer#10057 PiperOrigin-RevId: 460698942 --- RELEASENOTES.md | 3 + .../exoplayer/audio/DefaultAudioSink.java | 58 ++++++++++++++----- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 425473041a..97aed03d91 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,9 @@ ([#2518](https://github.com/google/ExoPlayer/issues/2518)). * Allow custom logger for all ExoPlayer log output ([#9752](https://github.com/google/ExoPlayer/issues/9752)). + * Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid + OutOfMemory errors when releasing multiple players at the same time + ([#10057](https://github.com/google/ExoPlayer/issues/10057)). * Extractors: * Add support for AVI ([#2092](https://github.com/google/ExoPlayer/issues/2092)). 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 b53d79c47e..1704ed3ba3 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 @@ -33,6 +33,7 @@ import android.os.Handler; import android.os.SystemClock; import android.util.Pair; import androidx.annotation.DoNotInline; +import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -66,6 +67,7 @@ import java.nio.ByteOrder; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.concurrent.ExecutorService; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -472,6 +474,15 @@ public final class DefaultAudioSink implements AudioSink { */ public static boolean failOnSpuriousAudioTimestamp = false; + private static final Object releaseExecutorLock = new Object(); + + @GuardedBy("releaseExecutorLock") + @Nullable + private static ExecutorService releaseExecutor; + + @GuardedBy("releaseExecutorLock") + private static int pendingReleaseCount; + private final AudioCapabilities audioCapabilities; private final AudioProcessorChain audioProcessorChain; private final boolean enableFloatOutput; @@ -1424,9 +1435,6 @@ public final class DefaultAudioSink implements AudioSink { if (isOffloadedPlayback(audioTrack)) { checkNotNull(offloadStreamEventCallbackV29).unregister(audioTrack); } - // AudioTrack.release can take some time, so we call it on a background thread. - final AudioTrack toRelease = audioTrack; - audioTrack = null; if (Util.SDK_INT < 21 && !externalAudioSessionIdProvided) { // Prior to API level 21, audio sessions are not kept alive once there are no components // associated with them. If we generated the session ID internally, the only component @@ -1440,18 +1448,8 @@ public final class DefaultAudioSink implements AudioSink { pendingConfiguration = null; } audioTrackPositionTracker.reset(); - releasingConditionVariable.close(); - new Thread("ExoPlayer:AudioTrackReleaseThread") { - @Override - public void run() { - try { - toRelease.flush(); - toRelease.release(); - } finally { - releasingConditionVariable.open(); - } - } - }.start(); + releaseAudioTrackAsync(audioTrack, releasingConditionVariable); + audioTrack = null; } writeExceptionPendingExceptionHolder.clear(); initializationExceptionPendingExceptionHolder.clear(); @@ -1862,6 +1860,36 @@ public final class DefaultAudioSink implements AudioSink { } } + private static void releaseAudioTrackAsync( + AudioTrack audioTrack, ConditionVariable releasedConditionVariable) { + // AudioTrack.release can take some time, so we call it on a background thread. The background + // thread is shared statically to avoid creating many threads when multiple players are released + // at the same time. + releasedConditionVariable.close(); + synchronized (releaseExecutorLock) { + if (releaseExecutor == null) { + releaseExecutor = Util.newSingleThreadExecutor("ExoPlayer:AudioTrackReleaseThread"); + } + pendingReleaseCount++; + releaseExecutor.execute( + () -> { + try { + audioTrack.flush(); + audioTrack.release(); + } finally { + releasedConditionVariable.open(); + synchronized (releaseExecutorLock) { + pendingReleaseCount--; + if (pendingReleaseCount == 0) { + releaseExecutor.shutdown(); + releaseExecutor = null; + } + } + } + }); + } + } + @RequiresApi(29) private final class StreamEventCallbackV29 { private final Handler handler; From adc50515e93e6fdcf303d168e8388050503c46ef Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Jul 2022 15:27:55 +0000 Subject: [PATCH 20/25] Fix setDataSourceFactory handling in DefaultMediaSourceFactory The call doesn't currently reset the already loaded suppliers and factories. Also fix the supplier loading code to use a local copy of the current dataSourceFactory to avoid leaking an updated instance to a later invocation. Issue: androidx/media#116 #minor-release PiperOrigin-RevId: 460721541 --- RELEASENOTES.md | 3 ++ .../source/DefaultMediaSourceFactory.java | 13 ++--- .../dash/DefaultMediaSourceFactoryTest.java | 54 +++++++++++++++++++ .../hls/DefaultMediaSourceFactoryTest.java | 54 +++++++++++++++++++ .../exoplayer_smoothstreaming/build.gradle | 1 + .../DefaultMediaSourceFactoryTest.java | 54 +++++++++++++++++++ 6 files changed, 173 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 97aed03d91..78f6a3e7a8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,6 +17,9 @@ * Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid OutOfMemory errors when releasing multiple players at the same time ([#10057](https://github.com/google/ExoPlayer/issues/10057)). + * Fix implementation of `setDataSourceFactory` in + `DefaultMediaSourceFactory`, which was non-functional in some cases + ([#116](https://github.com/androidx/media/issues/116)). * Extractors: * Add support for AVI ([#2092](https://github.com/google/ExoPlayer/issues/2092)). diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java index f0a8cb1164..6a55a3a13e 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/DefaultMediaSourceFactory.java @@ -282,6 +282,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { */ public DefaultMediaSourceFactory setDataSourceFactory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; + delegateFactoryLoader.setDataSourceFactory(dataSourceFactory); return this; } @@ -594,6 +595,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { this.dataSourceFactory = dataSourceFactory; // TODO(b/233577470): Call MediaSource.Factory.setDataSourceFactory on each value when it // exists on the interface. + mediaSourceFactorySuppliers.clear(); mediaSourceFactories.clear(); } } @@ -627,6 +629,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } @Nullable Supplier mediaSourceFactorySupplier = null; + DataSource.Factory dataSourceFactory = checkNotNull(this.dataSourceFactory); try { Class clazz; switch (contentType) { @@ -634,19 +637,19 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { clazz = Class.forName("androidx.media3.exoplayer.dash.DashMediaSource$Factory") .asSubclass(MediaSource.Factory.class); - mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory)); + mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory); break; case C.CONTENT_TYPE_SS: clazz = Class.forName("androidx.media3.exoplayer.smoothstreaming.SsMediaSource$Factory") .asSubclass(MediaSource.Factory.class); - mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory)); + mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory); break; case C.CONTENT_TYPE_HLS: clazz = Class.forName("androidx.media3.exoplayer.hls.HlsMediaSource$Factory") .asSubclass(MediaSource.Factory.class); - mediaSourceFactorySupplier = () -> newInstance(clazz, checkNotNull(dataSourceFactory)); + mediaSourceFactorySupplier = () -> newInstance(clazz, dataSourceFactory); break; case C.CONTENT_TYPE_RTSP: clazz = @@ -656,9 +659,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { break; case C.CONTENT_TYPE_OTHER: mediaSourceFactorySupplier = - () -> - new ProgressiveMediaSource.Factory( - checkNotNull(dataSourceFactory), extractorsFactory); + () -> new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory); break; default: // Do nothing. diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultMediaSourceFactoryTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultMediaSourceFactoryTest.java index b6d4ac102f..77c1dde311 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultMediaSourceFactoryTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DefaultMediaSourceFactoryTest.java @@ -15,16 +15,21 @@ */ package androidx.media3.exoplayer.dash; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; +import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.test.utils.FakeDataSource; +import androidx.media3.test.utils.robolectric.RobolectricUtil; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -82,4 +87,53 @@ public class DefaultMediaSourceFactoryTest { assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_DASH); } + + @Test + public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception { + FakeDataSource fakeDataSource = new FakeDataSource(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()) + .setDataSourceFactory(() -> fakeDataSource); + + prepareDashUrlAndWaitForPrepareError(defaultMediaSourceFactory); + + assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty(); + } + + @Test + public void + createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory() + throws Exception { + FakeDataSource fakeDataSource = new FakeDataSource(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); + + // Use default DataSource.Factory first. + prepareDashUrlAndWaitForPrepareError(defaultMediaSourceFactory); + defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource); + prepareDashUrlAndWaitForPrepareError(defaultMediaSourceFactory); + + assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty(); + } + + private static void prepareDashUrlAndWaitForPrepareError( + DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception { + MediaSource mediaSource = + defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.mpd")); + getInstrumentation() + .runOnMainSync( + () -> + mediaSource.prepareSource( + (source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET)); + // We don't expect this to prepare successfully. + RobolectricUtil.runMainLooperUntil( + () -> { + try { + mediaSource.maybeThrowSourceInfoRefreshError(); + return false; + } catch (IOException e) { + return true; + } + }); + } } diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java index 2a2ff66b28..8062cff051 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/DefaultMediaSourceFactoryTest.java @@ -15,16 +15,21 @@ */ package androidx.media3.exoplayer.hls; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; +import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.test.utils.FakeDataSource; +import androidx.media3.test.utils.robolectric.RobolectricUtil; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -82,4 +87,53 @@ public class DefaultMediaSourceFactoryTest { assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_HLS); } + + @Test + public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception { + FakeDataSource fakeDataSource = new FakeDataSource(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()) + .setDataSourceFactory(() -> fakeDataSource); + + prepareHlsUrlAndWaitForPrepareError(defaultMediaSourceFactory); + + assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty(); + } + + @Test + public void + createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory() + throws Exception { + FakeDataSource fakeDataSource = new FakeDataSource(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); + + // Use default DataSource.Factory first. + prepareHlsUrlAndWaitForPrepareError(defaultMediaSourceFactory); + defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource); + prepareHlsUrlAndWaitForPrepareError(defaultMediaSourceFactory); + + assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty(); + } + + private static void prepareHlsUrlAndWaitForPrepareError( + DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception { + MediaSource mediaSource = + defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.m3u8")); + getInstrumentation() + .runOnMainSync( + () -> + mediaSource.prepareSource( + (source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET)); + // We don't expect this to prepare successfully. + RobolectricUtil.runMainLooperUntil( + () -> { + try { + mediaSource.maybeThrowSourceInfoRefreshError(); + return false; + } catch (IOException e) { + return true; + } + }); + } } diff --git a/libraries/exoplayer_smoothstreaming/build.gradle b/libraries/exoplayer_smoothstreaming/build.gradle index a379d25558..4b145ec6b3 100644 --- a/libraries/exoplayer_smoothstreaming/build.gradle +++ b/libraries/exoplayer_smoothstreaming/build.gradle @@ -29,6 +29,7 @@ dependencies { compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + testImplementation project(modulePrefix + 'test-utils-robolectric') testImplementation project(modulePrefix + 'test-utils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java index f5a205fcbe..4036fb9472 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/DefaultMediaSourceFactoryTest.java @@ -15,16 +15,21 @@ */ package androidx.media3.exoplayer.smoothstreaming; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.google.common.truth.Truth.assertThat; import android.content.Context; import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; +import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.MediaSource; +import androidx.media3.test.utils.FakeDataSource; +import androidx.media3.test.utils.robolectric.RobolectricUtil; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import java.io.IOException; import org.junit.Test; import org.junit.runner.RunWith; @@ -93,4 +98,53 @@ public class DefaultMediaSourceFactoryTest { assertThat(supportedTypes).asList().containsExactly(C.CONTENT_TYPE_OTHER, C.CONTENT_TYPE_SS); } + + @Test + public void createMediaSource_withSetDataSourceFactory_usesDataSourceFactory() throws Exception { + FakeDataSource fakeDataSource = new FakeDataSource(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()) + .setDataSourceFactory(() -> fakeDataSource); + + prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory); + + assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty(); + } + + @Test + public void + createMediaSource_usingDefaultDataSourceFactoryAndSetDataSourceFactory_usesUpdatesDataSourceFactory() + throws Exception { + FakeDataSource fakeDataSource = new FakeDataSource(); + DefaultMediaSourceFactory defaultMediaSourceFactory = + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); + + // Use default DataSource.Factory first. + prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory); + defaultMediaSourceFactory.setDataSourceFactory(() -> fakeDataSource); + prepareSsUrlAndWaitForPrepareError(defaultMediaSourceFactory); + + assertThat(fakeDataSource.getAndClearOpenedDataSpecs()).asList().isNotEmpty(); + } + + private static void prepareSsUrlAndWaitForPrepareError( + DefaultMediaSourceFactory defaultMediaSourceFactory) throws Exception { + MediaSource mediaSource = + defaultMediaSourceFactory.createMediaSource(MediaItem.fromUri(URI_MEDIA + "/file.ism")); + getInstrumentation() + .runOnMainSync( + () -> + mediaSource.prepareSource( + (source, timeline) -> {}, /* mediaTransferListener= */ null, PlayerId.UNSET)); + // We don't expect this to prepare successfully. + RobolectricUtil.runMainLooperUntil( + () -> { + try { + mediaSource.maybeThrowSourceInfoRefreshError(); + return false; + } catch (IOException e) { + return true; + } + }); + } } From 7954eeb3c2fa274d9343cbf51963a3cccf3270c7 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Wed, 13 Jul 2022 17:20:33 +0000 Subject: [PATCH 21/25] Use COLOR_Format32bitABGR2101010 for HDR encoder configuration. Also remove VideoEncoderSettings.colorProfile as there are no concrete use cases for customizing this and it clashes with picking the color format automatically based on SDR vs. HDR. PiperOrigin-RevId: 460746987 --- .../transformer/DefaultEncoderFactory.java | 16 +++++++++-- .../transformer/VideoEncoderSettings.java | 28 ------------------- 2 files changed, 14 insertions(+), 30 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 51d4d15782..b41e4e99ea 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -30,6 +30,7 @@ import android.media.MediaFormat; import android.util.Pair; import android.util.Size; import androidx.annotation.Nullable; +import androidx.media3.common.ColorInfo; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Log; @@ -278,8 +279,19 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { } MediaFormatUtil.maybeSetColorInfo(mediaFormat, encoderSupportedFormat.colorInfo); - mediaFormat.setInteger( - MediaFormat.KEY_COLOR_FORMAT, supportedVideoEncoderSettings.colorProfile); + if (Util.SDK_INT >= 31 && ColorInfo.isHdr(format.colorInfo)) { + if (EncoderUtil.getSupportedColorFormats(encoderInfo, mimeType) + .contains(MediaCodecInfo.CodecCapabilities.COLOR_Format32bitABGR2101010)) { + mediaFormat.setInteger( + MediaFormat.KEY_COLOR_FORMAT, + MediaCodecInfo.CodecCapabilities.COLOR_Format32bitABGR2101010); + } else { + throw createTransformationException(format); + } + } else { + mediaFormat.setInteger( + MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + } if (Util.SDK_INT >= 25) { mediaFormat.setFloat( 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 536e9fdb1c..d8c42784d1 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java @@ -41,9 +41,6 @@ public final class VideoEncoderSettings { /** A value for various fields to indicate that the field's value is unknown or not applicable. */ public static final int NO_VALUE = Format.NO_VALUE; - /** The default encoding color profile. */ - public static final int DEFAULT_COLOR_PROFILE = - MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; /** The default I-frame interval in seconds. */ public static final float DEFAULT_I_FRAME_INTERVAL_SECONDS = 1.0f; @@ -74,7 +71,6 @@ public final class VideoEncoderSettings { private @BitrateMode int bitrateMode; private int profile; private int level; - private int colorProfile; private float iFrameIntervalSeconds; private int operatingRate; private int priority; @@ -86,7 +82,6 @@ public final class VideoEncoderSettings { this.bitrateMode = BITRATE_MODE_VBR; this.profile = NO_VALUE; this.level = NO_VALUE; - this.colorProfile = DEFAULT_COLOR_PROFILE; this.iFrameIntervalSeconds = DEFAULT_I_FRAME_INTERVAL_SECONDS; this.operatingRate = NO_VALUE; this.priority = NO_VALUE; @@ -97,7 +92,6 @@ public final class VideoEncoderSettings { this.bitrateMode = videoEncoderSettings.bitrateMode; this.profile = videoEncoderSettings.profile; this.level = videoEncoderSettings.level; - this.colorProfile = videoEncoderSettings.colorProfile; this.iFrameIntervalSeconds = videoEncoderSettings.iFrameIntervalSeconds; this.operatingRate = videoEncoderSettings.operatingRate; this.priority = videoEncoderSettings.priority; @@ -152,21 +146,6 @@ public final class VideoEncoderSettings { return this; } - /** - * Sets {@link VideoEncoderSettings#colorProfile}. The default value is {@link - * #DEFAULT_COLOR_PROFILE}. - * - *

The value must be one of the {@code COLOR_*} constants defined in {@link - * MediaCodecInfo.CodecCapabilities}. - * - * @param colorProfile The {@link VideoEncoderSettings#colorProfile}. - * @return This builder. - */ - public Builder setColorProfile(int colorProfile) { - this.colorProfile = colorProfile; - return this; - } - /** * Sets {@link VideoEncoderSettings#iFrameIntervalSeconds}. The default value is {@link * #DEFAULT_I_FRAME_INTERVAL_SECONDS}. @@ -221,7 +200,6 @@ public final class VideoEncoderSettings { bitrateMode, profile, level, - colorProfile, iFrameIntervalSeconds, operatingRate, priority, @@ -237,8 +215,6 @@ public final class VideoEncoderSettings { public final int profile; /** The encoding level. */ public final int level; - /** The encoding color profile. */ - public final int colorProfile; /** The encoding I-Frame interval in seconds. */ public final float iFrameIntervalSeconds; /** The encoder {@link MediaFormat#KEY_OPERATING_RATE operating rate}. */ @@ -253,7 +229,6 @@ public final class VideoEncoderSettings { int bitrateMode, int profile, int level, - int colorProfile, float iFrameIntervalSeconds, int operatingRate, int priority, @@ -262,7 +237,6 @@ public final class VideoEncoderSettings { this.bitrateMode = bitrateMode; this.profile = profile; this.level = level; - this.colorProfile = colorProfile; this.iFrameIntervalSeconds = iFrameIntervalSeconds; this.operatingRate = operatingRate; this.priority = priority; @@ -289,7 +263,6 @@ public final class VideoEncoderSettings { && bitrateMode == that.bitrateMode && profile == that.profile && level == that.level - && colorProfile == that.colorProfile && iFrameIntervalSeconds == that.iFrameIntervalSeconds && operatingRate == that.operatingRate && priority == that.priority @@ -303,7 +276,6 @@ public final class VideoEncoderSettings { result = 31 * result + bitrateMode; result = 31 * result + profile; result = 31 * result + level; - result = 31 * result + colorProfile; result = 31 * result + Float.floatToIntBits(iFrameIntervalSeconds); result = 31 * result + operatingRate; result = 31 * result + priority; From b87fa45fea56a50b5c035c7ca18eaf19697e60bf Mon Sep 17 00:00:00 2001 From: samrobinson Date: Thu, 14 Jul 2022 12:27:05 +0000 Subject: [PATCH 22/25] Add additional video files. These are providing more variety and complexity. All files are okay to be public. PiperOrigin-RevId: 460935247 --- .../media3/transformer/AndroidTestUtil.java | 174 +++++++++++++++--- .../mh/analysis/BitrateAnalysisTest.java | 45 ++++- 2 files changed, 190 insertions(+), 29 deletions(-) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java index 9e6c3faf67..bddf874a40 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -15,6 +15,8 @@ */ package androidx.media3.transformer; +import static androidx.media3.common.MimeTypes.VIDEO_H264; +import static androidx.media3.common.MimeTypes.VIDEO_H265; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; @@ -39,11 +41,12 @@ import org.json.JSONObject; public final class AndroidTestUtil { private static final String TAG = "AndroidTestUtil"; - // TODO(b/228865104): Add device capability based test skipping. + // Format values are sourced from `mediainfo` command. + public static final String MP4_ASSET_URI_STRING = "asset:///media/mp4/sample.mp4"; public static final Format MP4_ASSET_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(1080) .setHeight(720) .setFrameRate(29.97f) @@ -53,7 +56,7 @@ public final class AndroidTestUtil { "asset:///media/mp4/sample_with_increasing_timestamps.mp4"; public static final Format MP4_ASSET_WITH_INCREASING_TIMESTAMPS_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(1920) .setHeight(1080) .setFrameRate(30.00f) @@ -65,7 +68,7 @@ public final class AndroidTestUtil { public static final Format MP4_ASSET_WITH_INCREASING_TIMESTAMPS_320W_240H_15S_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(320) .setHeight(240) .setFrameRate(30.00f) @@ -75,7 +78,7 @@ public final class AndroidTestUtil { "asset:///media/mp4/sample_sef_slow_motion.mp4"; public static final Format MP4_ASSET_SEF_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(320) .setHeight(240) .setFrameRate(30.472f) @@ -85,7 +88,7 @@ public final class AndroidTestUtil { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4"; public static final Format MP4_REMOTE_10_SECONDS_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(1280) .setHeight(720) .setFrameRate(29.97f) @@ -97,7 +100,7 @@ public final class AndroidTestUtil { public static final Format MP4_REMOTE_H264_MP3_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(1280) .setHeight(720) .setFrameRate(29.97f) @@ -107,7 +110,7 @@ public final class AndroidTestUtil { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4"; public static final Format MP4_REMOTE_4K60_PORTRAIT_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(3840) .setHeight(2160) .setFrameRate(57.39f) @@ -128,7 +131,7 @@ public final class AndroidTestUtil { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/1280w_720h_highmotion.mp4"; public static final Format MP4_REMOTE_1280W_720H_5_SECOND_HIGHMOTION_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(1280) .setHeight(720) .setAverageBitrate(8_939_000) @@ -139,7 +142,7 @@ public final class AndroidTestUtil { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/1440w_1440h_highmotion.mp4"; public static final Format MP4_REMOTE_1440W_1440H_5_SECOND_HIGHMOTION_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(1440) .setHeight(1440) .setAverageBitrate(17_000_000) @@ -150,7 +153,7 @@ public final class AndroidTestUtil { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_highmotion.mp4"; public static final Format MP4_REMOTE_1920W_1080H_5_SECOND_HIGHMOTION_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(1920) .setHeight(1080) .setAverageBitrate(17_100_000) @@ -161,7 +164,7 @@ public final class AndroidTestUtil { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/3840w_2160h_highmotion.mp4"; public static final Format MP4_REMOTE_3840W_2160H_5_SECOND_HIGHMOTION_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(3840) .setHeight(2160) .setAverageBitrate(48_300_000) @@ -172,7 +175,7 @@ public final class AndroidTestUtil { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/1280w_720h_30s_highmotion.mp4"; public static final Format MP4_REMOTE_1280W_720H_30_SECOND_HIGHMOTION_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(1280) .setHeight(720) .setAverageBitrate(9_962_000) @@ -183,7 +186,7 @@ public final class AndroidTestUtil { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_30s_highmotion.mp4"; public static final Format MP4_REMOTE_1920W_1080H_30_SECOND_HIGHMOTION_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(1920) .setHeight(1080) .setAverageBitrate(15_000_000) @@ -194,13 +197,122 @@ public final class AndroidTestUtil { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/3840w_2160h_32s_highmotion.mp4"; public static final Format MP4_REMOTE_3840W_2160H_32_SECOND_HIGHMOTION_FORMAT = new Format.Builder() - .setSampleMimeType(MimeTypes.VIDEO_H264) + .setSampleMimeType(VIDEO_H264) .setWidth(3840) .setHeight(2160) .setAverageBitrate(47_800_000) .setFrameRate(28.414f) .build(); + public static final String MP4_REMOTE_640W_480H_31_SECOND_ROOF_SONYXPERIAXZ3 = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/SonyXperiaXZ3_640w_480h_31s_roof.mp4"; + public static final Format MP4_REMOTE_640W_480H_31_SECOND_ROOF_SONYXPERIAXZ3_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(640) + .setHeight(480) + .setAverageBitrate(3_578_000) + .setFrameRate(30) + .build(); + + public static final String MP4_REMOTE_1280W_720H_30_SECOND_ROOF_ONEPLUSNORD2 = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/OnePlusNord2_1280w_720h_30s_roof.mp4"; + public static final Format MP4_REMOTE_1280W_720H_30_SECOND_ROOF_ONEPLUSNORD2_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(1280) + .setHeight(720) + .setAverageBitrate(8_966_000) + .setFrameRate(29.763f) + .build(); + + public static final String MP4_REMOTE_1280W_720H_32_SECOND_ROOF_REDMINOTE9 = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/RedmiNote9_1280w_720h_32s_roof.mp4"; + public static final Format MP4_REMOTE_1280W_720H_32_SECOND_ROOF_REDMINOTE9_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(1280) + .setHeight(720) + .setAverageBitrate(14_100_000) + .setFrameRate(30) + .build(); + + public static final String MP4_REMOTE_1440W_1440H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/SsS20Ultra5G_1440hw_31s_roof.mp4"; + public static final Format MP4_REMOTE_1440W_1440H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(1440) + .setHeight(1440) + .setAverageBitrate(16_300_000) + .setFrameRate(25.931f) + .build(); + + public static final String MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_ONEPLUSNORD2 = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/OnePlusNord2_1920w_1080h_60fr_30s_roof.mp4"; + public static final Format MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_ONEPLUSNORD2_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(1920) + .setHeight(1080) + .setAverageBitrate(20_000_000) + .setFrameRate(59.94f) + .build(); + + public static final String MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_REDMINOTE9 = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/RedmiNote9_1920w_1080h_60fps_30s_roof.mp4"; + public static final Format MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_REDMINOTE9_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(1920) + .setHeight(1080) + .setAverageBitrate(20_100_000) + .setFrameRate(61.069f) + .build(); + + public static final String MP4_REMOTE_2400W_1080H_34_SECOND_ROOF_SAMSUNGS20ULTRA5G = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/SsS20Ultra5G_2400w_1080h_34s_roof.mp4"; + public static final Format MP4_REMOTE_2400W_1080H_34_SECOND_ROOF_SAMSUNGS20ULTRA5G_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H265) + .setWidth(2400) + .setHeight(1080) + .setAverageBitrate(29_500_000) + .setFrameRate(27.472f) + .build(); + + public static final String MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_ONEPLUSNORD2 = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/OnePlusNord2_3840w_2160h_30s_roof.mp4"; + public static final Format MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_ONEPLUSNORD2_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(3840) + .setHeight(2160) + .setAverageBitrate(49_800_000) + .setFrameRate(29.802f) + .build(); + + public static final String MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_REDMINOTE9 = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/RedmiNote9_3840w_2160h_30s_roof.mp4"; + public static final Format MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_REDMINOTE9_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H264) + .setWidth(3840) + .setHeight(2160) + .setAverageBitrate(42_100_000) + .setFrameRate(30) + .build(); + + public static final String MP4_REMOTE_7680W_4320H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G = + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/device_videos/SsS20Ultra5G_7680w_4320h_31s_roof.mp4"; + public static final Format MP4_REMOTE_7680W_4320H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G_FORMAT = + new Format.Builder() + .setSampleMimeType(VIDEO_H265) + .setWidth(7680) + .setHeight(4320) + .setAverageBitrate(79_900_000) + .setFrameRate(23.163f) + .build(); /** * Log in logcat and in an analysis file that this test was skipped. * @@ -379,20 +491,40 @@ public final class AndroidTestUtil { return MP4_REMOTE_H264_MP3_FORMAT; case MP4_REMOTE_4K60_PORTRAIT_URI_STRING: return MP4_REMOTE_4K60_PORTRAIT_FORMAT; + case MP4_REMOTE_640W_480H_31_SECOND_ROOF_SONYXPERIAXZ3: + return MP4_REMOTE_640W_480H_31_SECOND_ROOF_SONYXPERIAXZ3_FORMAT; case MP4_REMOTE_1280W_720H_5_SECOND_HIGHMOTION: return MP4_REMOTE_1280W_720H_5_SECOND_HIGHMOTION_FORMAT; - case MP4_REMOTE_1440W_1440H_5_SECOND_HIGHMOTION: - return MP4_REMOTE_1440W_1440H_5_SECOND_HIGHMOTION_FORMAT; - case MP4_REMOTE_1920W_1080H_5_SECOND_HIGHMOTION: - return MP4_REMOTE_1920W_1080H_5_SECOND_HIGHMOTION_FORMAT; - case MP4_REMOTE_3840W_2160H_5_SECOND_HIGHMOTION: - return MP4_REMOTE_3840W_2160H_5_SECOND_HIGHMOTION_FORMAT; case MP4_REMOTE_1280W_720H_30_SECOND_HIGHMOTION: return MP4_REMOTE_1280W_720H_30_SECOND_HIGHMOTION_FORMAT; + case MP4_REMOTE_1280W_720H_30_SECOND_ROOF_ONEPLUSNORD2: + return MP4_REMOTE_1280W_720H_30_SECOND_ROOF_ONEPLUSNORD2_FORMAT; + case MP4_REMOTE_1280W_720H_32_SECOND_ROOF_REDMINOTE9: + return MP4_REMOTE_1280W_720H_32_SECOND_ROOF_REDMINOTE9_FORMAT; + case MP4_REMOTE_1440W_1440H_5_SECOND_HIGHMOTION: + return MP4_REMOTE_1440W_1440H_5_SECOND_HIGHMOTION_FORMAT; + case MP4_REMOTE_1440W_1440H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G: + return MP4_REMOTE_1440W_1440H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G_FORMAT; + case MP4_REMOTE_1920W_1080H_5_SECOND_HIGHMOTION: + return MP4_REMOTE_1920W_1080H_5_SECOND_HIGHMOTION_FORMAT; case MP4_REMOTE_1920W_1080H_30_SECOND_HIGHMOTION: return MP4_REMOTE_1920W_1080H_30_SECOND_HIGHMOTION_FORMAT; + case MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_ONEPLUSNORD2: + return MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_ONEPLUSNORD2_FORMAT; + case MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_REDMINOTE9: + return MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_REDMINOTE9_FORMAT; + case MP4_REMOTE_2400W_1080H_34_SECOND_ROOF_SAMSUNGS20ULTRA5G: + return MP4_REMOTE_2400W_1080H_34_SECOND_ROOF_SAMSUNGS20ULTRA5G_FORMAT; + case MP4_REMOTE_3840W_2160H_5_SECOND_HIGHMOTION: + return MP4_REMOTE_3840W_2160H_5_SECOND_HIGHMOTION_FORMAT; case MP4_REMOTE_3840W_2160H_32_SECOND_HIGHMOTION: return MP4_REMOTE_3840W_2160H_32_SECOND_HIGHMOTION_FORMAT; + case MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_ONEPLUSNORD2: + return MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_ONEPLUSNORD2_FORMAT; + case MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_REDMINOTE9: + return MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_REDMINOTE9_FORMAT; + case MP4_REMOTE_7680W_4320H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G: + return MP4_REMOTE_7680W_4320H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G_FORMAT; default: throw new IllegalArgumentException("The format for the given uri is not found."); } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java index 1b8837f137..bfe17ca98b 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java @@ -17,6 +17,24 @@ package androidx.media3.transformer.mh.analysis; import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR; import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1280W_720H_30_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1280W_720H_30_SECOND_ROOF_ONEPLUSNORD2; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1280W_720H_32_SECOND_ROOF_REDMINOTE9; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1280W_720H_5_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1440W_1440H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1440W_1440H_5_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1920W_1080H_30_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1920W_1080H_5_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_ONEPLUSNORD2; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_REDMINOTE9; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_2400W_1080H_34_SECOND_ROOF_SAMSUNGS20ULTRA5G; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_ONEPLUSNORD2; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_REDMINOTE9; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_3840W_2160H_32_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_3840W_2160H_5_SECOND_HIGHMOTION; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_640W_480H_31_SECOND_ROOF_SONYXPERIAXZ3; +import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_7680W_4320H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G; +import static androidx.media3.transformer.AndroidTestUtil.skipAndLogIfInsufficientCodecSupport; import android.content.Context; import android.net.Uri; @@ -47,13 +65,24 @@ import org.junit.runners.Parameterized.Parameters; public class BitrateAnalysisTest { private static final ImmutableList INPUT_FILES = ImmutableList.of( - AndroidTestUtil.MP4_REMOTE_1280W_720H_5_SECOND_HIGHMOTION, - AndroidTestUtil.MP4_REMOTE_1440W_1440H_5_SECOND_HIGHMOTION, - AndroidTestUtil.MP4_REMOTE_1920W_1080H_5_SECOND_HIGHMOTION, - AndroidTestUtil.MP4_REMOTE_3840W_2160H_5_SECOND_HIGHMOTION, - AndroidTestUtil.MP4_REMOTE_1280W_720H_30_SECOND_HIGHMOTION, - AndroidTestUtil.MP4_REMOTE_1920W_1080H_30_SECOND_HIGHMOTION, - AndroidTestUtil.MP4_REMOTE_3840W_2160H_32_SECOND_HIGHMOTION); + MP4_REMOTE_640W_480H_31_SECOND_ROOF_SONYXPERIAXZ3, + MP4_REMOTE_1280W_720H_5_SECOND_HIGHMOTION, + MP4_REMOTE_1280W_720H_30_SECOND_HIGHMOTION, + MP4_REMOTE_1280W_720H_30_SECOND_ROOF_ONEPLUSNORD2, + MP4_REMOTE_1280W_720H_32_SECOND_ROOF_REDMINOTE9, + MP4_REMOTE_1440W_1440H_5_SECOND_HIGHMOTION, + MP4_REMOTE_1440W_1440H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G, + MP4_REMOTE_1920W_1080H_5_SECOND_HIGHMOTION, + MP4_REMOTE_1920W_1080H_30_SECOND_HIGHMOTION, + MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_ONEPLUSNORD2, + MP4_REMOTE_1920W_1080H_60_FPS_30_SECOND_ROOF_REDMINOTE9, + MP4_REMOTE_2400W_1080H_34_SECOND_ROOF_SAMSUNGS20ULTRA5G, + MP4_REMOTE_3840W_2160H_5_SECOND_HIGHMOTION, + MP4_REMOTE_3840W_2160H_32_SECOND_HIGHMOTION, + MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_ONEPLUSNORD2, + MP4_REMOTE_3840W_2160H_30_SECOND_ROOF_REDMINOTE9, + MP4_REMOTE_7680W_4320H_31_SECOND_ROOF_SAMSUNGS20ULTRA5G); + private static final ImmutableList INPUT_BITRATE_MODES = ImmutableList.of(BITRATE_MODE_VBR, BITRATE_MODE_CBR); @@ -100,7 +129,7 @@ public class BitrateAnalysisTest { } Context context = ApplicationProvider.getApplicationContext(); - if (AndroidTestUtil.skipAndLogIfInsufficientCodecSupport( + if (skipAndLogIfInsufficientCodecSupport( context, testId, /* decodingFormat= */ AndroidTestUtil.getFormatForTestFile(fileUri), From be27daebc4e598a4c0d532779649a77f82650911 Mon Sep 17 00:00:00 2001 From: rohks Date: Fri, 15 Jul 2022 10:15:31 +0000 Subject: [PATCH 23/25] Version bump to exoplayer:2.18.1 and media3:1.0.0-beta02 #minor-release PiperOrigin-RevId: 461162552 --- .github/ISSUE_TEMPLATE/bug.yml | 1 + RELEASENOTES.md | 31 ++++++++++++------- constants.gradle | 4 +-- .../media3/common/MediaLibraryInfo.java | 6 ++-- 4 files changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index b29b2e92b0..f970c1ad12 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -17,6 +17,7 @@ body: label: Media3 Version description: What version of Media3 are you using? options: + - 1.0.0-beta02 - 1.0.0-beta01 - 1.0.0-alpha03 - 1.0.0-alpha02 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 78f6a3e7a8..5dcebc04be 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,25 @@ ### Unreleased changes +* Core library: + * Add `ExoPlayer.isTunnelingEnabled` to check if tunneling is enabled for + the currently selected tracks + ([#2518](https://github.com/google/ExoPlayer/issues/2518)). + * Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid + OutOfMemory errors when releasing multiple players at the same time + ([#10057](https://github.com/google/ExoPlayer/issues/10057)). +* Metadata: + * `MetadataRenderer` can now be configured to render metadata as soon as + they are available. Create an instance with + `MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory, + boolean)` to specify whether the renderer will output metadata early or + in sync with the player position. + +### 1.0.0-beta02 (2022-07-15) + +This release corresponds to the +[ExoPlayer 2.18.1 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.1). + * Core library: * Ensure that changing the `ShuffleOrder` with `ExoPlayer.setShuffleOrder` results in a call to `Player.Listener#onTimelineChanged` with @@ -9,14 +28,8 @@ ([#9889](https://github.com/google/ExoPlayer/issues/9889)). * For progressive media, only include selected tracks in buffered position ([#10361](https://github.com/google/ExoPlayer/issues/10361)). - * Add `ExoPlayer.isTunnelingEnabled` to check if tunneling is enabled for - the currently selected tracks - ([#2518](https://github.com/google/ExoPlayer/issues/2518)). * Allow custom logger for all ExoPlayer log output ([#9752](https://github.com/google/ExoPlayer/issues/9752)). - * Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid - OutOfMemory errors when releasing multiple players at the same time - ([#10057](https://github.com/google/ExoPlayer/issues/10057)). * Fix implementation of `setDataSourceFactory` in `DefaultMediaSourceFactory`, which was non-functional in some cases ([#116](https://github.com/androidx/media/issues/116)). @@ -27,12 +40,6 @@ ([#10316](https://github.com/google/ExoPlayer/issues/10316)). * Fix parsing of bitrates from `esds` boxes ([#10381](https://github.com/google/ExoPlayer/issues/10381)). -* Metadata: - * `MetadataRenderer` can now be configured to render metadata as soon as - they are available. Create an instance with - `MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory, - boolean)` to specify whether the renderer will output metadata early or - in sync with the player position. * DASH: * Parse ClearKey license URL from manifests ([#10246](https://github.com/google/ExoPlayer/issues/10246)). diff --git a/constants.gradle b/constants.gradle index 8752c6d8a9..abc8995fae 100644 --- a/constants.gradle +++ b/constants.gradle @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. project.ext { - releaseVersion = '1.0.0-beta01' - releaseVersionCode = 1_000_000_1_01 + releaseVersion = '1.0.0-beta02' + releaseVersionCode = 1_000_000_1_02 minSdkVersion = 16 appTargetSdkVersion = 29 // Upgrading this requires [Internal ref: b/193254928] to be fixed, or some diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java index 4e87f65806..62be209a9b 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java +++ b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java @@ -29,11 +29,11 @@ public final class MediaLibraryInfo { /** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. - public static final String VERSION = "1.0.0-beta01"; + public static final String VERSION = "1.0.0-beta02"; /** The version of the library expressed as {@code TAG + "/" + VERSION}. */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta01"; + public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta02"; /** * The version of the library expressed as an integer, for example 1002003300. @@ -47,7 +47,7 @@ public final class MediaLibraryInfo { * (123-045-006-3-00). */ // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. - public static final int VERSION_INT = 1_000_000_1_01; + public static final int VERSION_INT = 1_000_000_1_02; /** Whether the library was compiled with {@link Assertions} checks enabled. */ public static final boolean ASSERTIONS_ENABLED = true; From 9271572e950230a24da69a6ec5838dd838ea672d Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 15 Jul 2022 10:34:53 +0000 Subject: [PATCH 24/25] Add TODOs for registerReceiver calls without flag PiperOrigin-RevId: 461165173 --- .../src/main/java/androidx/media3/session/MediaSessionImpl.java | 1 + .../main/java/androidx/media3/ui/PlayerNotificationManager.java | 1 + 2 files changed, 2 insertions(+) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java index 6fce21470f..6f720629fe 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -210,6 +210,7 @@ import org.checkerframework.checker.initialization.qual.Initialized; broadcastReceiver = new MediaButtonReceiver(); IntentFilter filter = new IntentFilter(Intent.ACTION_MEDIA_BUTTON); filter.addDataScheme(castNonNull(sessionUri.getScheme())); + // TODO(b/197817693): Explicitly indicate whether the receiver should be exported. context.registerReceiver(broadcastReceiver, filter); } else { // Has MediaSessionService to revive playback after it's dead. diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java index f5c5009c2e..9502581c05 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerNotificationManager.java @@ -1164,6 +1164,7 @@ public class PlayerNotificationManager { Notification notification = builder.build(); notificationManager.notify(notificationId, notification); if (!isNotificationStarted) { + // TODO(b/197817693): Explicitly indicate whether the receiver should be exported. context.registerReceiver(notificationBroadcastReceiver, intentFilter); } if (notificationListener != null) { From 28e32072b679a7cde8a1cd8e6fad0e7e7d108093 Mon Sep 17 00:00:00 2001 From: Jorge Antonio Diaz-Benito Soriano Date: Thu, 21 Jul 2022 14:38:47 +0200 Subject: [PATCH 25/25] Cap concurrent removal DownloadManager.Tasks --- .../exoplayer/offline/DownloadManager.java | 11 +++- .../offline/DownloadManagerTest.java | 56 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadManager.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadManager.java index 2679e634f2..519db874b8 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadManager.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadManager.java @@ -174,6 +174,7 @@ public final class DownloadManager { private static final int MSG_RELEASE = 12; private static final String TAG = "DownloadManager"; + private static final int MAX_REMOVE_TASK_COUNT = 1; private final Context context; private final WritableDownloadIndex downloadIndex; @@ -709,6 +710,7 @@ public final class DownloadManager { private int maxParallelDownloads; private int minRetryCount; private int activeDownloadTaskCount; + private int activeRemoveTaskCount; public InternalHandler( HandlerThread thread, @@ -1060,6 +1062,10 @@ public final class DownloadManager { return; } + if (activeRemoveTaskCount >= MAX_REMOVE_TASK_COUNT) { + return; + } + // We can start a remove task. Downloader downloader = downloaderFactory.createDownloader(download.request); activeTask = @@ -1071,6 +1077,7 @@ public final class DownloadManager { minRetryCount, /* internalHandler= */ this); activeTasks.put(download.request.id, activeTask); + activeRemoveTaskCount++; activeTask.start(); } @@ -1100,7 +1107,9 @@ public final class DownloadManager { activeTasks.remove(downloadId); boolean isRemove = task.isRemove; - if (!isRemove && --activeDownloadTaskCount == 0) { + if (isRemove) { + activeRemoveTaskCount--; + } else if (--activeDownloadTaskCount == 0) { removeMessages(MSG_UPDATE_PROGRESS); } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadManagerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadManagerTest.java index 9cce8afbed..3cd1dcd83c 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadManagerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadManagerTest.java @@ -713,7 +713,61 @@ public class DownloadManagerTest { assertEqualIgnoringUpdateTime(mergedDownload, expectedDownload); } + @Test + public void remove_tasks_run_sequentially() + throws Throwable { + DefaultDownloadIndex defaultDownloadIndex = + new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider()); + defaultDownloadIndex.putDownload( + new Download( + new DownloadRequest.Builder(ID1, Uri.EMPTY).build(), + Download.STATE_REMOVING, + 0, + 1, + 2, + Download.STOP_REASON_NONE, + Download.FAILURE_REASON_NONE + ) + ); + defaultDownloadIndex.putDownload( + new Download( + new DownloadRequest.Builder(ID2, Uri.EMPTY).build(), + Download.STATE_RESTARTING, + 0, + 1, + 2, + Download.STOP_REASON_NONE, + Download.FAILURE_REASON_NONE + ) + ); + setupDownloadManager(100, defaultDownloadIndex); + + // The second removal should wait and the first one should be able to complete. + FakeDownloader downloader0 = getDownloaderAt(0); + assertNoDownloaderAt(1); + downloader0.assertId(ID1); + downloader0.assertRemoveStarted(); + downloader0.finish(); + assertRemoved(ID1); + + // The second removal can start once the first one has completed, and removes a download with + // state STATE_RESTARTING, so it should result in a new download being queued. + FakeDownloader downloader1 = getDownloaderAt(1); + downloader1.assertId(ID2); + downloader1.assertRemoveStarted(); + downloader1.finish(); + assertQueued(ID2); + } + private void setupDownloadManager(int maxParallelDownloads) throws Exception { + setupDownloadManager( + maxParallelDownloads, new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider()) + ); + } + + private void setupDownloadManager( + int maxParallelDownloads, WritableDownloadIndex writableDownloadIndex + ) throws Exception { if (downloadManager != null) { releaseDownloadManager(); } @@ -723,7 +777,7 @@ public class DownloadManagerTest { downloadManager = new DownloadManager( ApplicationProvider.getApplicationContext(), - new DefaultDownloadIndex(TestUtil.getInMemoryDatabaseProvider()), + writableDownloadIndex, new FakeDownloaderFactory()); downloadManager.setMaxParallelDownloads(maxParallelDownloads); downloadManager.setMinRetryCount(MIN_RETRY_COUNT);