From eee226ea40a02b188df748c6f0d5fa81ea560e47 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Fri, 21 Apr 2023 10:09:45 +0100 Subject: [PATCH 01/24] Render last frame even if have not read BUFFER_FLAG_END_OF_STREAM If the limited number of input buffers causes reading of all samples except the last one conveying end of stream, then the last frame will not be rendered. PiperOrigin-RevId: 525974445 (cherry picked from commit affbb7c57e73eb4f4f654f224c477fdd2e3ac9f2) --- RELEASENOTES.md | 9 + .../java/androidx/media3/decoder/Buffer.java | 5 + .../mediacodec/MediaCodecRenderer.java | 2 +- .../media3/exoplayer/source/SampleQueue.java | 3 + .../exoplayer/source/SampleQueueTest.java | 131 ++++++++++- .../video/MediaCodecVideoRendererTest.java | 213 +++++++++++++++++- .../media3/test/utils/FakeMediaPeriod.java | 3 +- .../media3/test/utils/FakeSampleStream.java | 6 +- 8 files changed, 362 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index acce6295ca..605c1c20f6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,14 @@ # Release notes +### Unreleased changes + +* Core library: + * Add `Buffer.isLastSample()` that denotes if `Buffer` contains flag + `C.BUFFER_FLAG_LAST_SAMPLE`. + * Fix issue where last frame may not be rendered if the last sample with + frames is dequeued without reading the 'end of stream' sample. + ([#11079](https://github.com/google/ExoPlayer/issues/11079)). + ### 1.0.1 (2023-04-18) This release corresponds to the diff --git a/libraries/decoder/src/main/java/androidx/media3/decoder/Buffer.java b/libraries/decoder/src/main/java/androidx/media3/decoder/Buffer.java index cf2be91c65..8d1c636da5 100644 --- a/libraries/decoder/src/main/java/androidx/media3/decoder/Buffer.java +++ b/libraries/decoder/src/main/java/androidx/media3/decoder/Buffer.java @@ -49,6 +49,11 @@ public abstract class Buffer { return getFlag(C.BUFFER_FLAG_KEY_FRAME); } + /** Returns whether the {@link C#BUFFER_FLAG_LAST_SAMPLE} flag is set. */ + public final boolean isLastSample() { + return getFlag(C.BUFFER_FLAG_LAST_SAMPLE); + } + /** Returns whether the {@link C#BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA} flag is set. */ public final boolean hasSupplementalData() { return getFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA); diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java index fddfea4e2a..c944762e0a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/mediacodec/MediaCodecRenderer.java @@ -1244,7 +1244,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { return true; } - if (hasReadStreamToEnd()) { + if (hasReadStreamToEnd() || buffer.isLastSample()) { // Notify output queue of the last buffer's timestamp. lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SampleQueue.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SampleQueue.java index 203c5f8b9b..a9d2c57caa 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SampleQueue.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SampleQueue.java @@ -716,6 +716,9 @@ public class SampleQueue implements TrackOutput { } buffer.setFlags(flags[relativeReadIndex]); + if (readPosition == (length - 1) && (loadingFinished || isLastSampleQueued)) { + buffer.addFlag(C.BUFFER_FLAG_LAST_SAMPLE); + } buffer.timeUs = timesUs[relativeReadIndex]; if (buffer.timeUs < startTimeUs) { buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/SampleQueueTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/SampleQueueTest.java index b7042b0457..a7f935ef17 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/SampleQueueTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/SampleQueueTest.java @@ -354,6 +354,32 @@ public final class SampleQueueTest { assertAllocationCount(0); } + @Test + public void readSingleSampleWithLoadingFinished() { + sampleQueue.sampleData(new ParsableByteArray(DATA), ALLOCATION_SIZE); + sampleQueue.format(FORMAT_1); + sampleQueue.sampleMetadata(1000, C.BUFFER_FLAG_KEY_FRAME, ALLOCATION_SIZE, 0, null); + + assertAllocationCount(1); + // If formatRequired, should read the format rather than the sample. + assertReadFormat(true, FORMAT_1); + // Otherwise should read the sample with loading finished. + assertReadLastSample( + 1000, + /* isKeyFrame= */ true, + /* isDecodeOnly= */ false, + /* isEncrypted= */ false, + DATA, + /* offset= */ 0, + ALLOCATION_SIZE); + // Allocation should still be held. + assertAllocationCount(1); + + sampleQueue.discardToRead(); + // The allocation should have been released. + assertAllocationCount(0); + } + @Test public void readMultiSamples() { writeTestData(); @@ -1642,13 +1668,27 @@ public final class SampleQueueTest { FLAG_OMIT_SAMPLE_DATA | FLAG_PEEK, /* loadingFinished= */ false); assertSampleBufferReadResult( - flagsOnlyBuffer, result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted); + flagsOnlyBuffer, + result, + timeUs, + isKeyFrame, + isDecodeOnly, + isEncrypted, + /* isLastSample= */ false); // Check that peek yields the expected values. clearFormatHolderAndInputBuffer(); result = sampleQueue.read(formatHolder, inputBuffer, FLAG_PEEK, /* loadingFinished= */ false); assertSampleBufferReadResult( - result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted, sampleData, offset, length); + result, + timeUs, + isKeyFrame, + isDecodeOnly, + isEncrypted, + /* isLastSample= */ false, + sampleData, + offset, + length); // Check that read yields the expected values. clearFormatHolderAndInputBuffer(); @@ -1656,7 +1696,85 @@ public final class SampleQueueTest { sampleQueue.read( formatHolder, inputBuffer, /* readFlags= */ 0, /* loadingFinished= */ false); assertSampleBufferReadResult( - result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted, sampleData, offset, length); + result, + timeUs, + isKeyFrame, + isDecodeOnly, + isEncrypted, + /* isLastSample= */ false, + sampleData, + offset, + length); + } + + /** + * Asserts {@link SampleQueue#read} returns {@link C#RESULT_BUFFER_READ} and that the buffer is + * filled with the specified sample data. Also asserts that being the last sample and loading is + * finished, that the {@link C#BUFFER_FLAG_LAST_SAMPLE} flag is set. + * + * @param timeUs The expected buffer timestamp. + * @param isKeyFrame The expected keyframe flag. + * @param isDecodeOnly The expected decodeOnly flag. + * @param isEncrypted The expected encrypted flag. + * @param sampleData An array containing the expected sample data. + * @param offset The offset in {@code sampleData} of the expected sample data. + * @param length The length of the expected sample data. + */ + private void assertReadLastSample( + long timeUs, + boolean isKeyFrame, + boolean isDecodeOnly, + boolean isEncrypted, + byte[] sampleData, + int offset, + int length) { + // Check that peek whilst omitting data yields the expected values. + formatHolder.format = null; + DecoderInputBuffer flagsOnlyBuffer = DecoderInputBuffer.newNoDataInstance(); + int result = + sampleQueue.read( + formatHolder, + flagsOnlyBuffer, + FLAG_OMIT_SAMPLE_DATA | FLAG_PEEK, + /* loadingFinished= */ true); + assertSampleBufferReadResult( + flagsOnlyBuffer, + result, + timeUs, + isKeyFrame, + isDecodeOnly, + isEncrypted, + /* isLastSample= */ true); + + // Check that peek yields the expected values. + clearFormatHolderAndInputBuffer(); + result = sampleQueue.read(formatHolder, inputBuffer, FLAG_PEEK, /* loadingFinished= */ true); + assertSampleBufferReadResult( + result, + timeUs, + isKeyFrame, + isDecodeOnly, + isEncrypted, + /* isLastSample= */ true, + sampleData, + offset, + length); + + // Check that read yields the expected values. + clearFormatHolderAndInputBuffer(); + result = + sampleQueue.read( + formatHolder, inputBuffer, /* readFlags= */ 0, /* loadingFinished= */ true); + assertSampleBufferReadResult( + result, + timeUs, + isKeyFrame, + isDecodeOnly, + isEncrypted, + /* isLastSample= */ true, + sampleData, + offset, + length); } private void assertSampleBufferReadResult( @@ -1665,7 +1783,8 @@ public final class SampleQueueTest { long timeUs, boolean isKeyFrame, boolean isDecodeOnly, - boolean isEncrypted) { + boolean isEncrypted, + boolean isLastSample) { assertThat(result).isEqualTo(RESULT_BUFFER_READ); // formatHolder should not be populated. assertThat(formatHolder.format).isNull(); @@ -1674,6 +1793,7 @@ public final class SampleQueueTest { assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyFrame); assertThat(inputBuffer.isDecodeOnly()).isEqualTo(isDecodeOnly); assertThat(inputBuffer.isEncrypted()).isEqualTo(isEncrypted); + assertThat(inputBuffer.isLastSample()).isEqualTo(isLastSample); } private void assertSampleBufferReadResult( @@ -1682,11 +1802,12 @@ public final class SampleQueueTest { boolean isKeyFrame, boolean isDecodeOnly, boolean isEncrypted, + boolean isLastSample, byte[] sampleData, int offset, int length) { assertSampleBufferReadResult( - inputBuffer, result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted); + inputBuffer, result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted, isLastSample); // inputBuffer should be populated with data. inputBuffer.flip(); assertThat(inputBuffer.data.limit()).isEqualTo(length); diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java index b7ad8dc6c2..5d6dac43d8 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/video/MediaCodecVideoRendererTest.java @@ -31,11 +31,14 @@ import static org.robolectric.Shadows.shadowOf; import android.content.Context; import android.graphics.SurfaceTexture; import android.hardware.display.DisplayManager; +import android.media.MediaCodec; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.CodecProfileLevel; import android.media.MediaFormat; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.os.PersistableBundle; import android.os.SystemClock; import android.view.Display; import android.view.Surface; @@ -44,6 +47,8 @@ import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.VideoSize; +import androidx.media3.decoder.CryptoInfo; +import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.RendererCapabilities; import androidx.media3.exoplayer.RendererCapabilities.Capabilities; @@ -51,13 +56,17 @@ import androidx.media3.exoplayer.RendererConfiguration; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionManager; +import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter; import androidx.media3.exoplayer.mediacodec.MediaCodecInfo; import androidx.media3.exoplayer.mediacodec.MediaCodecSelector; +import androidx.media3.exoplayer.mediacodec.SynchronousMediaCodecAdapter; import androidx.media3.exoplayer.upstream.DefaultAllocator; import androidx.media3.test.utils.FakeSampleStream; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; +import java.io.IOException; +import java.nio.ByteBuffer; import java.util.Collections; import java.util.List; import java.util.concurrent.TimeUnit; @@ -117,6 +126,7 @@ public class MediaCodecVideoRendererTest { private Looper testMainLooper; private Surface surface; private MediaCodecVideoRenderer mediaCodecVideoRenderer; + private MediaCodecSelector mediaCodecSelector; @Nullable private Format currentOutputFormat; @Mock private VideoRendererEventListener eventListener; @@ -124,7 +134,7 @@ public class MediaCodecVideoRendererTest { @Before public void setUp() throws Exception { testMainLooper = Looper.getMainLooper(); - MediaCodecSelector mediaCodecSelector = + mediaCodecSelector = (mimeType, requiresSecureDecoder, requiresTunnelingDecoder) -> Collections.singletonList( MediaCodecInfo.newInstance( @@ -207,6 +217,65 @@ public class MediaCodecVideoRendererTest { verify(eventListener).onDroppedFrames(eq(1), anyLong()); } + @Test + public void render_withBufferLimitEqualToNumberOfSamples_rendersLastFrameAfterEndOfStream() + throws Exception { + ArgumentCaptor argumentDecoderCounters = + ArgumentCaptor.forClass(DecoderCounters.class); + FakeSampleStream fakeSampleStream = + new FakeSampleStream( + new DefaultAllocator(/* trimOnReset= */ true, /* individualAllocationSize= */ 1024), + /* mediaSourceEventDispatcher= */ null, + DrmSessionManager.DRM_UNSUPPORTED, + new DrmSessionEventListener.EventDispatcher(), + /* initialFormat= */ VIDEO_H264, + ImmutableList.of( + oneByteSample(/* timeUs= */ 0, C.BUFFER_FLAG_KEY_FRAME), // First buffer. + oneByteSample(/* timeUs= */ 10_000), + oneByteSample(/* timeUs= */ 20_000), // Last buffer. + END_OF_STREAM_ITEM)); + fakeSampleStream.writeData(/* startPositionUs= */ 0); + // Seek to time after samples. + fakeSampleStream.seekToUs(30_000, /* allowTimeBeyondBuffer= */ true); + mediaCodecVideoRenderer = + new MediaCodecVideoRenderer( + ApplicationProvider.getApplicationContext(), + new ForwardingSynchronousMediaCodecAdapterWithBufferLimit.Factory(/* bufferLimit= */ 3), + mediaCodecSelector, + /* allowedJoiningTimeMs= */ 0, + /* enableDecoderFallback= */ false, + /* eventHandler= */ new Handler(testMainLooper), + /* eventListener= */ eventListener, + /* maxDroppedFramesToNotify= */ 1); + mediaCodecVideoRenderer.handleMessage(Renderer.MSG_SET_VIDEO_OUTPUT, surface); + mediaCodecVideoRenderer.enable( + RendererConfiguration.DEFAULT, + new Format[] {VIDEO_H264}, + fakeSampleStream, + /* positionUs= */ 0, + /* joining= */ false, + /* mayRenderStartOfStream= */ true, + /* startPositionUs= */ 0, + /* offsetUs= */ 0); + + mediaCodecVideoRenderer.start(); + mediaCodecVideoRenderer.setCurrentStreamFinal(); + mediaCodecVideoRenderer.render(0, SystemClock.elapsedRealtime() * 1000); + // Call to render should have read all samples up to but not including the END_OF_STREAM_ITEM. + assertThat(mediaCodecVideoRenderer.hasReadStreamToEnd()).isFalse(); + int posUs = 30_000; + while (!mediaCodecVideoRenderer.isEnded()) { + mediaCodecVideoRenderer.render(posUs, SystemClock.elapsedRealtime() * 1000); + posUs += 40_000; + } + shadowOf(testMainLooper).idle(); + + verify(eventListener).onRenderedFirstFrame(eq(surface), /* renderTimeMs= */ anyLong()); + verify(eventListener).onVideoEnabled(argumentDecoderCounters.capture()); + assertThat(argumentDecoderCounters.getValue().renderedOutputBufferCount).isEqualTo(1); + assertThat(argumentDecoderCounters.getValue().skippedOutputBufferCount).isEqualTo(2); + } + @Test public void render_sendsVideoSizeChangeWithCurrentFormatValues() throws Exception { FakeSampleStream fakeSampleStream = @@ -1194,4 +1263,146 @@ public class MediaCodecVideoRendererTest { .setHeight(height) .build(); } + + private static final class ForwardingSynchronousMediaCodecAdapterWithBufferLimit + extends ForwardingSynchronousMediaCodecAdapter { + /** A factory for {@link ForwardingSynchronousMediaCodecAdapterWithBufferLimit} instances. */ + public static final class Factory implements MediaCodecAdapter.Factory { + private final int bufferLimit; + + Factory(int bufferLimit) { + this.bufferLimit = bufferLimit; + } + + @Override + public MediaCodecAdapter createAdapter(Configuration configuration) throws IOException { + return new ForwardingSynchronousMediaCodecAdapterWithBufferLimit( + bufferLimit, new SynchronousMediaCodecAdapter.Factory().createAdapter(configuration)); + } + } + + private int bufferCounter; + + ForwardingSynchronousMediaCodecAdapterWithBufferLimit( + int bufferCounter, MediaCodecAdapter adapter) { + super(adapter); + this.bufferCounter = bufferCounter; + } + + @Override + public int dequeueInputBufferIndex() { + if (bufferCounter > 0) { + bufferCounter--; + return super.dequeueInputBufferIndex(); + } + return -1; + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + int outputIndex = super.dequeueOutputBufferIndex(bufferInfo); + if (outputIndex > 0) { + bufferCounter++; + } + return outputIndex; + } + } + + private abstract static class ForwardingSynchronousMediaCodecAdapter + implements MediaCodecAdapter { + private final MediaCodecAdapter adapter; + + ForwardingSynchronousMediaCodecAdapter(MediaCodecAdapter adapter) { + this.adapter = adapter; + } + + @Override + public int dequeueInputBufferIndex() { + return adapter.dequeueInputBufferIndex(); + } + + @Override + public int dequeueOutputBufferIndex(MediaCodec.BufferInfo bufferInfo) { + return adapter.dequeueOutputBufferIndex(bufferInfo); + } + + @Override + public MediaFormat getOutputFormat() { + return adapter.getOutputFormat(); + } + + @Nullable + @Override + public ByteBuffer getInputBuffer(int index) { + return adapter.getInputBuffer(index); + } + + @Nullable + @Override + public ByteBuffer getOutputBuffer(int index) { + return adapter.getOutputBuffer(index); + } + + @Override + public void queueInputBuffer( + int index, int offset, int size, long presentationTimeUs, int flags) { + adapter.queueInputBuffer(index, offset, size, presentationTimeUs, flags); + } + + @Override + public void queueSecureInputBuffer( + int index, int offset, CryptoInfo info, long presentationTimeUs, int flags) { + adapter.queueSecureInputBuffer(index, offset, info, presentationTimeUs, flags); + } + + @Override + public void releaseOutputBuffer(int index, boolean render) { + adapter.releaseOutputBuffer(index, render); + } + + @Override + public void releaseOutputBuffer(int index, long renderTimeStampNs) { + adapter.releaseOutputBuffer(index, renderTimeStampNs); + } + + @Override + public void flush() { + adapter.flush(); + } + + @Override + public void release() { + adapter.release(); + } + + @Override + public void setOnFrameRenderedListener(OnFrameRenderedListener listener, Handler handler) { + adapter.setOnFrameRenderedListener(listener, handler); + } + + @Override + public void setOutputSurface(Surface surface) { + adapter.setOutputSurface(surface); + } + + @Override + public void setParameters(Bundle params) { + adapter.setParameters(params); + } + + @Override + public void setVideoScalingMode(int scalingMode) { + adapter.setVideoScalingMode(scalingMode); + } + + @Override + public boolean needsReconfiguration() { + return adapter.needsReconfiguration(); + } + + @Override + public PersistableBundle getMetrics() { + return adapter.getMetrics(); + } + } } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java index 7ec904028e..aa1cc64bff 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaPeriod.java @@ -338,7 +338,8 @@ public class FakeMediaPeriod implements MediaPeriod { lastSeekPositionUs = seekPositionUs; boolean seekedInsideStreams = true; for (FakeSampleStream sampleStream : sampleStreams) { - seekedInsideStreams &= sampleStream.seekToUs(seekPositionUs); + seekedInsideStreams &= + sampleStream.seekToUs(seekPositionUs, /* allowTimeBeyondBuffer= */ false); } if (!seekedInsideStreams) { for (FakeSampleStream sampleStream : sampleStreams) { diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeSampleStream.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeSampleStream.java index d779b21109..b5a53a40c9 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeSampleStream.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeSampleStream.java @@ -204,10 +204,12 @@ public class FakeSampleStream implements SampleStream { * Seeks the stream to a new position using already available data in the queue. * * @param positionUs The new position, in microseconds. + * @param allowTimeBeyondBuffer Whether the operation can succeed if timeUs is beyond the end of + * the queue, by seeking to the last sample (or keyframe). * @return Whether seeking inside the available data was possible. */ - public boolean seekToUs(long positionUs) { - return sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false); + public boolean seekToUs(long positionUs, boolean allowTimeBeyondBuffer) { + return sampleQueue.seekTo(positionUs, allowTimeBeyondBuffer); } /** From 58cf3a7ba2ea784105052f1ca16904d8d060120a Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 21 Apr 2023 11:19:14 +0100 Subject: [PATCH 02/24] Remove unnecessary Activity method overrides in session demo app The PlayerView methods are documented to only be needed for sphrerical playbacks, which we are not using in the session demo app. PiperOrigin-RevId: 525986709 (cherry picked from commit 2de89ca2ce4c06bcbf9cb80dcc903f2cdd3ff733) --- .../androidx/media3/demo/session/PlayerActivity.kt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt index e4f09b8d33..452b901da5 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt @@ -88,16 +88,6 @@ class PlayerActivity : AppCompatActivity() { initializeController() } - override fun onResume() { - super.onResume() - playerView.onResume() - } - - override fun onPause() { - super.onPause() - playerView.onPause() - } - override fun onStop() { super.onStop() playerView.player = null From 0f6a1eb6b850f6039f12602316aee09fa9f747e7 Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 21 Apr 2023 12:35:39 +0100 Subject: [PATCH 03/24] Update available commands when MediaSessionCompat actions change This is a bug currently, where commands are created once but never updated again if the actions in MediaSessionCompat are changed. PiperOrigin-RevId: 525999084 (cherry picked from commit 79fab6783e07ea594410be347c8a3d6e1124707d) --- RELEASENOTES.md | 4 +++ .../session/MediaControllerImplLegacy.java | 12 ++++---- ...aControllerWithMediaSessionCompatTest.java | 30 +++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 605c1c20f6..7b2b47b91e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,10 @@ * Fix issue where last frame may not be rendered if the last sample with frames is dequeued without reading the 'end of stream' sample. ([#11079](https://github.com/google/ExoPlayer/issues/11079)). +* Session: + * Fix issue where `MediaController` doesn't update its available commands + when connected to a legacy `MediaSessionCompat` that updates its + actions. ### 1.0.1 (2023-04-18) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java index e01ffcbf49..f625821d2c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplLegacy.java @@ -1885,13 +1885,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ? newLegacyPlayerInfo.playbackInfoCompat.getVolumeControl() : VolumeProviderCompat.VOLUME_CONTROL_FIXED; availablePlayerCommands = - (oldControllerInfo.availablePlayerCommands == Commands.EMPTY) - ? MediaUtils.convertToPlayerCommands( - newLegacyPlayerInfo.playbackStateCompat, - volumeControlType, - sessionFlags, - isSessionReady) - : oldControllerInfo.availablePlayerCommands; + MediaUtils.convertToPlayerCommands( + newLegacyPlayerInfo.playbackStateCompat, + volumeControlType, + sessionFlags, + isSessionReady); PlaybackException playerError = MediaUtils.convertToPlaybackException(newLegacyPlayerInfo.playbackStateCompat); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index 8e4afdcdeb..f0eca11593 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -1502,6 +1502,36 @@ public class MediaControllerWithMediaSessionCompatTest { assertThat(errorFromGetterRef.get().getMessage()).isEqualTo(testConvertedErrorMessage); } + @Test + public void setPlaybackState_withActions_updatesAndNotifiesAvailableCommands() throws Exception { + MediaController controller = controllerTestRule.createController(session.getSessionToken()); + CountDownLatch latch = new CountDownLatch(1); + AtomicReference commandsFromParamRef = new AtomicReference<>(); + AtomicReference commandsFromGetterRef = new AtomicReference<>(); + Player.Listener listener = + new Player.Listener() { + @Override + public void onAvailableCommandsChanged(Player.Commands commands) { + commandsFromParamRef.set(commands); + commandsFromGetterRef.set(controller.getAvailableCommands()); + latch.countDown(); + } + }; + controller.addListener(listener); + + session.setPlaybackState( + new PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_FAST_FORWARD) + .build()); + + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(commandsFromParamRef.get().contains(Player.COMMAND_PLAY_PAUSE)).isTrue(); + assertThat(commandsFromParamRef.get().contains(Player.COMMAND_SEEK_FORWARD)).isTrue(); + assertThat(commandsFromGetterRef.get().contains(Player.COMMAND_PLAY_PAUSE)).isTrue(); + assertThat(commandsFromGetterRef.get().contains(Player.COMMAND_SEEK_FORWARD)).isTrue(); + } + @Test public void setPlaybackToRemote_notifiesDeviceInfoAndVolume() throws Exception { int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; From 3f5d777f3829ccd7d7f76ce13fe6cb9ba05ddf2e Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 21 Apr 2023 12:39:10 +0100 Subject: [PATCH 04/24] Clarify threading requirement for MediaController.releaseFuture And remove unnecessary check for isDone. Issue: androidx/media#345 PiperOrigin-RevId: 525999615 (cherry picked from commit 186f3d5c7767e65cc823846b045282c05ed30da5) --- .../java/androidx/media3/session/MediaController.java | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 44acda8293..163ad83f45 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -516,16 +516,19 @@ public class MediaController implements Player { /** * Releases the future controller returned by {@link Builder#buildAsync()}. It makes sure that the * controller is released by canceling the future if the future is not yet done. + * + *

Must be called on the {@linkplain #getApplicationLooper() application thread} of the media + * controller. */ public static void releaseFuture(Future controllerFuture) { - if (!controllerFuture.isDone()) { - controllerFuture.cancel(/* mayInterruptIfRunning= */ true); + if (controllerFuture.cancel(/* mayInterruptIfRunning= */ true)) { + // Successfully canceled the Future. The controller will be released by MediaControllerHolder. return; } MediaController controller; try { - controller = controllerFuture.get(); - } catch (CancellationException | ExecutionException | InterruptedException e) { + controller = Futures.getDone(controllerFuture); + } catch (CancellationException | ExecutionException e) { return; } controller.release(); From 20924724fc4fbe7f05d9656316c978ed37d5d592 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 24 Apr 2023 10:41:13 +0100 Subject: [PATCH 05/24] Ensure `DrmSessionManager.setPlayer()` is called before `prepare()` `prepare()` now logs a warning if it's called before `setPlayer()` because it's not possible to tell if it's being called on the wrong thread (since https://github.com/androidx/media/commit/3480a27994ef9e06bd7574bad4656eb8c7677971). This change finds all the places one is called immediately after the other and flips the order to be more correct. Issue: androidx/media#350 #minor-release PiperOrigin-RevId: 526582294 (cherry picked from commit 6aacbc6bbb11a5a55ec812cc93e0bb1b6810749e) --- .../source/ProgressiveMediaSource.java | 2 +- .../drm/DefaultDrmSessionManagerTest.java | 42 +++++++++---------- .../exoplayer/dash/DashMediaSource.java | 2 +- .../media3/exoplayer/hls/HlsMediaSource.java | 2 +- .../smoothstreaming/SsMediaSource.java | 2 +- .../media3/test/utils/FakeMediaSource.java | 2 +- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java index a97818f146..47757865ed 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaSource.java @@ -278,9 +278,9 @@ public final class ProgressiveMediaSource extends BaseMediaSource @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { transferListener = mediaTransferListener; - drmSessionManager.prepare(); drmSessionManager.setPlayer( /* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId()); + drmSessionManager.prepare(); notifySourceInfoRefreshed(); } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManagerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManagerTest.java index 3bd3545968..1103ee7178 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManagerTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/drm/DefaultDrmSessionManagerTest.java @@ -68,8 +68,8 @@ public class DefaultDrmSessionManagerTest { new DefaultDrmSessionManager.Builder() .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm()) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -91,8 +91,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(10_000) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -116,8 +116,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(C.TIME_UNSET) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -138,8 +138,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(10_000) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -162,8 +162,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(C.TIME_UNSET) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -188,8 +188,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(10_000) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -233,8 +233,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(10_000) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -272,8 +272,8 @@ public class DefaultDrmSessionManagerTest { .setMultiSession(true) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession firstDrmSession = checkNotNull( drmSessionManager.acquireSession( @@ -313,8 +313,8 @@ public class DefaultDrmSessionManagerTest { .setMultiSession(true) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSessionReference firstDrmSessionReference = checkNotNull( drmSessionManager.preacquireSession( @@ -358,8 +358,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(10_000) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession firstDrmSession = checkNotNull( drmSessionManager.acquireSession( @@ -405,8 +405,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(C.TIME_UNSET) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSessionReference sessionReference = drmSessionManager.preacquireSession(eventDispatcher, FORMAT_WITH_DRM_INIT_DATA); @@ -450,8 +450,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(C.TIME_UNSET) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSessionReference sessionReference = drmSessionManager.preacquireSession(/* eventDispatcher= */ null, FORMAT_WITH_DRM_INIT_DATA); @@ -486,8 +486,8 @@ public class DefaultDrmSessionManagerTest { .setSessionKeepaliveMs(C.TIME_UNSET) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSessionReference sessionReference = drmSessionManager.preacquireSession(/* eventDispatcher= */ null, FORMAT_WITH_DRM_INIT_DATA); @@ -530,8 +530,8 @@ public class DefaultDrmSessionManagerTest { .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm)) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DefaultDrmSession drmSession = (DefaultDrmSession) @@ -571,8 +571,8 @@ public class DefaultDrmSessionManagerTest { .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm)) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DefaultDrmSession drmSession = (DefaultDrmSession) @@ -615,8 +615,8 @@ public class DefaultDrmSessionManagerTest { DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm.Builder().setProvisionsRequired(1).build()) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -648,8 +648,8 @@ public class DefaultDrmSessionManagerTest { .throwNotProvisionedExceptionFromGetKeyRequest() .build()) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -674,8 +674,8 @@ public class DefaultDrmSessionManagerTest { DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm.Builder().setProvisionsRequired(2).build()) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -702,8 +702,8 @@ public class DefaultDrmSessionManagerTest { .setUuidAndExoMediaDrmProvider( DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm.Builder().build()) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -728,8 +728,8 @@ public class DefaultDrmSessionManagerTest { .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(mediaDrm)) .setSessionKeepaliveMs(C.TIME_UNSET) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( @@ -783,8 +783,8 @@ public class DefaultDrmSessionManagerTest { .setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm()) .build(/* mediaDrmCallback= */ licenseServer); - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET); + drmSessionManager.prepare(); DrmSession drmSession = checkNotNull( drmSessionManager.acquireSession( diff --git a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java index 00a96572a8..a2267f425e 100644 --- a/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java +++ b/libraries/exoplayer_dash/src/main/java/androidx/media3/exoplayer/dash/DashMediaSource.java @@ -449,8 +449,8 @@ public final class DashMediaSource extends BaseMediaSource { @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), getPlayerId()); + drmSessionManager.prepare(); if (sideloadedManifest) { processManifest(false); } else { diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java index 256cda9c79..57ad6b5d4c 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java @@ -417,9 +417,9 @@ public final class HlsMediaSource extends BaseMediaSource @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; - drmSessionManager.prepare(); drmSessionManager.setPlayer( /* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId()); + drmSessionManager.prepare(); MediaSourceEventListener.EventDispatcher eventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); playlistTracker.start( diff --git a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java index b92deea4ac..edbb43c61d 100644 --- a/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java +++ b/libraries/exoplayer_smoothstreaming/src/main/java/androidx/media3/exoplayer/smoothstreaming/SsMediaSource.java @@ -374,8 +374,8 @@ public final class SsMediaSource extends BaseMediaSource @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; - drmSessionManager.prepare(); drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), getPlayerId()); + drmSessionManager.prepare(); if (sideloadedManifest) { manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy(); processManifest(); diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaSource.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaSource.java index ee34c7db65..ccbdb5bbe6 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaSource.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/FakeMediaSource.java @@ -215,9 +215,9 @@ public class FakeMediaSource extends BaseMediaSource { public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { assertThat(preparedSource).isFalse(); transferListener = mediaTransferListener; - drmSessionManager.prepare(); drmSessionManager.setPlayer( /* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId()); + drmSessionManager.prepare(); preparedSource = true; releasedSource = false; sourceInfoRefreshHandler = Util.createHandlerForCurrentLooper(); From 31492031c11d113e7e882a672343aba84a740123 Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Wed, 26 Apr 2023 15:53:58 +0100 Subject: [PATCH 06/24] Merge pull request #313 from pengbins:fix_ts_h265reader_parse_sps PiperOrigin-RevId: 527259619 (cherry picked from commit fab134f0b3194349aaac702f4c582ee20356b1dc) --- RELEASENOTES.md | 4 + .../exoplayer/e2etest/TsPlaybackTest.java | 1 + .../media3/extractor/ts/H265Reader.java | 213 +----------------- .../media3/extractor/ts/TsExtractorTest.java | 6 + .../ts/sample_h265_rps_pred.ts.0.dump | 81 +++++++ .../ts/sample_h265_rps_pred.ts.1.dump | 65 ++++++ .../ts/sample_h265_rps_pred.ts.2.dump | 45 ++++ .../ts/sample_h265_rps_pred.ts.3.dump | 25 ++ ...ample_h265_rps_pred.ts.unknown_length.dump | 78 +++++++ .../assets/media/ts/sample_h265_rps_pred.ts | Bin 0 -> 15416 bytes .../ts/sample_h265_rps_pred.ts.dump | 0 11 files changed, 317 insertions(+), 201 deletions(-) create mode 100644 libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.0.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.1.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.2.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.3.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.unknown_length.dump create mode 100644 libraries/test_data/src/test/assets/media/ts/sample_h265_rps_pred.ts create mode 100644 libraries/test_data/src/test/assets/playbackdumps/ts/sample_h265_rps_pred.ts.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 7b2b47b91e..8eb95c496a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -8,6 +8,10 @@ * Fix issue where last frame may not be rendered if the last sample with frames is dequeued without reading the 'end of stream' sample. ([#11079](https://github.com/google/ExoPlayer/issues/11079)). +* Extractors: + * Fix parsing of H.265 SPS in MPEG-TS files by re-using the parsing logic + already used by RTSP and MP4 extractors + ([#303](https://github.com/androidx/media/issues/303)). * Session: * Fix issue where `MediaController` doesn't update its available commands when connected to a legacy `MediaSessionCompat` that updates its diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/TsPlaybackTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/TsPlaybackTest.java index c20a248b34..893630a8a2 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/TsPlaybackTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/e2etest/TsPlaybackTest.java @@ -63,6 +63,7 @@ public class TsPlaybackTest { "sample_h264_mpeg_audio.ts", "sample_h264_no_access_unit_delimiters.ts", "sample_h265.ts", + "sample_h265_rps_pred.ts", "sample_latm.ts", "sample_scte35.ts", "sample_with_id3.adts", diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H265Reader.java b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H265Reader.java index a0ab3aaa8d..17c0cc7281 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H265Reader.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/ts/H265Reader.java @@ -15,15 +15,12 @@ */ package androidx.media3.extractor.ts; -import static java.lang.Math.min; - import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Assertions; import androidx.media3.common.util.CodecSpecificDataUtil; -import androidx.media3.common.util.Log; import androidx.media3.common.util.ParsableByteArray; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; @@ -246,216 +243,30 @@ public final class H265Reader implements ElementaryStreamReader { System.arraycopy(sps.nalData, 0, csdData, vps.nalLength, sps.nalLength); System.arraycopy(pps.nalData, 0, csdData, vps.nalLength + sps.nalLength, pps.nalLength); - // Parse the SPS NAL unit, as per H.265/HEVC (2014) 7.3.2.2.1. - ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength); - bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id - int maxSubLayersMinus1 = bitArray.readBits(3); - bitArray.skipBit(); // sps_temporal_id_nesting_flag - int generalProfileSpace = bitArray.readBits(2); - boolean generalTierFlag = bitArray.readBit(); - int generalProfileIdc = bitArray.readBits(5); - int generalProfileCompatibilityFlags = 0; - for (int i = 0; i < 32; i++) { - if (bitArray.readBit()) { - generalProfileCompatibilityFlags |= (1 << i); - } - } - int[] constraintBytes = new int[6]; - for (int i = 0; i < constraintBytes.length; ++i) { - constraintBytes[i] = bitArray.readBits(8); - } - int generalLevelIdc = bitArray.readBits(8); - int toSkip = 0; - for (int i = 0; i < maxSubLayersMinus1; i++) { - if (bitArray.readBit()) { // sub_layer_profile_present_flag[i] - toSkip += 89; - } - if (bitArray.readBit()) { // sub_layer_level_present_flag[i] - toSkip += 8; - } - } - bitArray.skipBits(toSkip); - if (maxSubLayersMinus1 > 0) { - bitArray.skipBits(2 * (8 - maxSubLayersMinus1)); - } - - bitArray.readUnsignedExpGolombCodedInt(); // sps_seq_parameter_set_id - int chromaFormatIdc = bitArray.readUnsignedExpGolombCodedInt(); - if (chromaFormatIdc == 3) { - bitArray.skipBit(); // separate_colour_plane_flag - } - int picWidthInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); - int picHeightInLumaSamples = bitArray.readUnsignedExpGolombCodedInt(); - if (bitArray.readBit()) { // conformance_window_flag - int confWinLeftOffset = bitArray.readUnsignedExpGolombCodedInt(); - int confWinRightOffset = bitArray.readUnsignedExpGolombCodedInt(); - int confWinTopOffset = bitArray.readUnsignedExpGolombCodedInt(); - int confWinBottomOffset = bitArray.readUnsignedExpGolombCodedInt(); - // H.265/HEVC (2014) Table 6-1 - int subWidthC = chromaFormatIdc == 1 || chromaFormatIdc == 2 ? 2 : 1; - int subHeightC = chromaFormatIdc == 1 ? 2 : 1; - picWidthInLumaSamples -= subWidthC * (confWinLeftOffset + confWinRightOffset); - picHeightInLumaSamples -= subHeightC * (confWinTopOffset + confWinBottomOffset); - } - bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_luma_minus8 - bitArray.readUnsignedExpGolombCodedInt(); // bit_depth_chroma_minus8 - int log2MaxPicOrderCntLsbMinus4 = bitArray.readUnsignedExpGolombCodedInt(); - // for (i = sps_sub_layer_ordering_info_present_flag ? 0 : sps_max_sub_layers_minus1; ...) - for (int i = bitArray.readBit() ? 0 : maxSubLayersMinus1; i <= maxSubLayersMinus1; i++) { - bitArray.readUnsignedExpGolombCodedInt(); // sps_max_dec_pic_buffering_minus1[i] - bitArray.readUnsignedExpGolombCodedInt(); // sps_max_num_reorder_pics[i] - bitArray.readUnsignedExpGolombCodedInt(); // sps_max_latency_increase_plus1[i] - } - bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_coding_block_size_minus3 - bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_coding_block_size - bitArray.readUnsignedExpGolombCodedInt(); // log2_min_luma_transform_block_size_minus2 - bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_luma_transform_block_size - bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_inter - bitArray.readUnsignedExpGolombCodedInt(); // max_transform_hierarchy_depth_intra - // if (scaling_list_enabled_flag) { if (sps_scaling_list_data_present_flag) {...}} - boolean scalingListEnabled = bitArray.readBit(); - if (scalingListEnabled && bitArray.readBit()) { - skipScalingList(bitArray); - } - bitArray.skipBits(2); // amp_enabled_flag (1), sample_adaptive_offset_enabled_flag (1) - if (bitArray.readBit()) { // pcm_enabled_flag - // pcm_sample_bit_depth_luma_minus1 (4), pcm_sample_bit_depth_chroma_minus1 (4) - bitArray.skipBits(8); - bitArray.readUnsignedExpGolombCodedInt(); // log2_min_pcm_luma_coding_block_size_minus3 - bitArray.readUnsignedExpGolombCodedInt(); // log2_diff_max_min_pcm_luma_coding_block_size - bitArray.skipBit(); // pcm_loop_filter_disabled_flag - } - // Skips all short term reference picture sets. - skipShortTermRefPicSets(bitArray); - if (bitArray.readBit()) { // long_term_ref_pics_present_flag - // num_long_term_ref_pics_sps - for (int i = 0; i < bitArray.readUnsignedExpGolombCodedInt(); i++) { - int ltRefPicPocLsbSpsLength = log2MaxPicOrderCntLsbMinus4 + 4; - // lt_ref_pic_poc_lsb_sps[i], used_by_curr_pic_lt_sps_flag[i] - bitArray.skipBits(ltRefPicPocLsbSpsLength + 1); - } - } - bitArray.skipBits(2); // sps_temporal_mvp_enabled_flag, strong_intra_smoothing_enabled_flag - float pixelWidthHeightRatio = 1; - if (bitArray.readBit()) { // vui_parameters_present_flag - if (bitArray.readBit()) { // aspect_ratio_info_present_flag - int aspectRatioIdc = bitArray.readBits(8); - if (aspectRatioIdc == NalUnitUtil.EXTENDED_SAR) { - int sarWidth = bitArray.readBits(16); - int sarHeight = bitArray.readBits(16); - if (sarWidth != 0 && sarHeight != 0) { - pixelWidthHeightRatio = (float) sarWidth / sarHeight; - } - } else if (aspectRatioIdc < NalUnitUtil.ASPECT_RATIO_IDC_VALUES.length) { - pixelWidthHeightRatio = NalUnitUtil.ASPECT_RATIO_IDC_VALUES[aspectRatioIdc]; - } else { - Log.w(TAG, "Unexpected aspect_ratio_idc value: " + aspectRatioIdc); - } - } - if (bitArray.readBit()) { // overscan_info_present_flag - bitArray.skipBit(); // overscan_appropriate_flag - } - if (bitArray.readBit()) { // video_signal_type_present_flag - bitArray.skipBits(4); // video_format, video_full_range_flag - if (bitArray.readBit()) { // colour_description_present_flag - // colour_primaries, transfer_characteristics, matrix_coeffs - bitArray.skipBits(24); - } - } - if (bitArray.readBit()) { // chroma_loc_info_present_flag - bitArray.readUnsignedExpGolombCodedInt(); // chroma_sample_loc_type_top_field - bitArray.readUnsignedExpGolombCodedInt(); // chroma_sample_loc_type_bottom_field - } - bitArray.skipBit(); // neutral_chroma_indication_flag - if (bitArray.readBit()) { // field_seq_flag - // field_seq_flag equal to 1 indicates that the coded video sequence conveys pictures that - // represent fields, which means that frame height is double the picture height. - picHeightInLumaSamples *= 2; - } - } + // Skip the 3-byte NAL unit start code synthesised by the NalUnitTargetBuffer constructor. + NalUnitUtil.H265SpsData spsData = + NalUnitUtil.parseH265SpsNalUnit(sps.nalData, /* nalOffset= */ 3, sps.nalLength); String codecs = CodecSpecificDataUtil.buildHevcCodecString( - generalProfileSpace, - generalTierFlag, - generalProfileIdc, - generalProfileCompatibilityFlags, - constraintBytes, - generalLevelIdc); + spsData.generalProfileSpace, + spsData.generalTierFlag, + spsData.generalProfileIdc, + spsData.generalProfileCompatibilityFlags, + spsData.constraintBytes, + spsData.generalLevelIdc); return new Format.Builder() .setId(formatId) .setSampleMimeType(MimeTypes.VIDEO_H265) .setCodecs(codecs) - .setWidth(picWidthInLumaSamples) - .setHeight(picHeightInLumaSamples) - .setPixelWidthHeightRatio(pixelWidthHeightRatio) + .setWidth(spsData.width) + .setHeight(spsData.height) + .setPixelWidthHeightRatio(spsData.pixelWidthHeightRatio) .setInitializationData(Collections.singletonList(csdData)) .build(); } - /** Skips scaling_list_data(). See H.265/HEVC (2014) 7.3.4. */ - private static void skipScalingList(ParsableNalUnitBitArray bitArray) { - for (int sizeId = 0; sizeId < 4; sizeId++) { - for (int matrixId = 0; matrixId < 6; matrixId += sizeId == 3 ? 3 : 1) { - if (!bitArray.readBit()) { // scaling_list_pred_mode_flag[sizeId][matrixId] - // scaling_list_pred_matrix_id_delta[sizeId][matrixId] - bitArray.readUnsignedExpGolombCodedInt(); - } else { - int coefNum = min(64, 1 << (4 + (sizeId << 1))); - if (sizeId > 1) { - // scaling_list_dc_coef_minus8[sizeId - 2][matrixId] - bitArray.readSignedExpGolombCodedInt(); - } - for (int i = 0; i < coefNum; i++) { - bitArray.readSignedExpGolombCodedInt(); // scaling_list_delta_coef - } - } - } - } - } - - /** - * Reads the number of short term reference picture sets in a SPS as ue(v), then skips all of - * them. See H.265/HEVC (2014) 7.3.7. - */ - private static void skipShortTermRefPicSets(ParsableNalUnitBitArray bitArray) { - int numShortTermRefPicSets = bitArray.readUnsignedExpGolombCodedInt(); - boolean interRefPicSetPredictionFlag = false; - int numNegativePics; - int numPositivePics; - // As this method applies in a SPS, the only element of NumDeltaPocs accessed is the previous - // one, so we just keep track of that rather than storing the whole array. - // RefRpsIdx = stRpsIdx - (delta_idx_minus1 + 1) and delta_idx_minus1 is always zero in SPS. - int previousNumDeltaPocs = 0; - for (int stRpsIdx = 0; stRpsIdx < numShortTermRefPicSets; stRpsIdx++) { - if (stRpsIdx != 0) { - interRefPicSetPredictionFlag = bitArray.readBit(); - } - if (interRefPicSetPredictionFlag) { - bitArray.skipBit(); // delta_rps_sign - bitArray.readUnsignedExpGolombCodedInt(); // abs_delta_rps_minus1 - for (int j = 0; j <= previousNumDeltaPocs; j++) { - if (bitArray.readBit()) { // used_by_curr_pic_flag[j] - bitArray.skipBit(); // use_delta_flag[j] - } - } - } else { - numNegativePics = bitArray.readUnsignedExpGolombCodedInt(); - numPositivePics = bitArray.readUnsignedExpGolombCodedInt(); - previousNumDeltaPocs = numNegativePics + numPositivePics; - for (int i = 0; i < numNegativePics; i++) { - bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s0_minus1[i] - bitArray.skipBit(); // used_by_curr_pic_s0_flag[i] - } - for (int i = 0; i < numPositivePics; i++) { - bitArray.readUnsignedExpGolombCodedInt(); // delta_poc_s1_minus1[i] - bitArray.skipBit(); // used_by_curr_pic_s1_flag[i] - } - } - } - } - @EnsuresNonNull({"output", "sampleReader"}) private void assertTracksCreated() { Assertions.checkStateNotNull(output); diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/ts/TsExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/ts/TsExtractorTest.java index 3b372c63be..0cd2e9cd30 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/ts/TsExtractorTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/ts/TsExtractorTest.java @@ -92,6 +92,12 @@ public final class TsExtractorTest { ExtractorAsserts.assertBehavior(TsExtractor::new, "media/ts/sample_h265.ts", simulationConfig); } + @Test + public void sampleWithH265RpsPred() throws Exception { + ExtractorAsserts.assertBehavior( + TsExtractor::new, "media/ts/sample_h265_rps_pred.ts", simulationConfig); + } + @Test public void sampleWithScte35() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.0.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.0.dump new file mode 100644 index 0000000000..d4dc0a7863 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.0.dump @@ -0,0 +1,81 @@ +seekMap: + isSeekable = true + duration = 1000000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(500000) = [[timeUs=500000, position=7134]] + getPosition(1000000) = [[timeUs=1000000, position=14457]] +numberOfTracks = 1 +track 256: + total output bytes = 10004 + sample count = 15 + format 0: + id = 1/256 + sampleMimeType = video/hevc + codecs = hvc1.1.6.L63.90 + width = 914 + height = 686 + pixelWidthHeightRatio = 1.0003651 + initializationData: + data = length 146, hash 61554FEF + sample 0: + time = 266666 + flags = 1 + data = length 7464, hash EBF8518B + sample 1: + time = 1200000 + flags = 0 + data = length 1042, hash F69C93E1 + sample 2: + time = 733333 + flags = 0 + data = length 465, hash 2B469969 + sample 3: + time = 466666 + flags = 0 + data = length 177, hash 79777966 + sample 4: + time = 333333 + flags = 0 + data = length 65, hash 63DA4886 + sample 5: + time = 400000 + flags = 0 + data = length 33, hash EFE759C6 + sample 6: + time = 600000 + flags = 0 + data = length 88, hash 98333D02 + sample 7: + time = 533333 + flags = 0 + data = length 49, hash F9A023E1 + sample 8: + time = 666666 + flags = 0 + data = length 58, hash 74F1E9D9 + sample 9: + time = 933333 + flags = 0 + data = length 114, hash FA033C4D + sample 10: + time = 800000 + flags = 0 + data = length 87, hash 1A1C57E4 + sample 11: + time = 866666 + flags = 0 + data = length 65, hash 59F937BE + sample 12: + time = 1066666 + flags = 0 + data = length 94, hash 5D02AC81 + sample 13: + time = 1000000 + flags = 0 + data = length 57, hash 2750D207 + sample 14: + time = 1133333 + flags = 0 + data = length 46, hash CE770A40 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.1.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.1.dump new file mode 100644 index 0000000000..cfe07b53a9 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.1.dump @@ -0,0 +1,65 @@ +seekMap: + isSeekable = true + duration = 1000000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(500000) = [[timeUs=500000, position=7134]] + getPosition(1000000) = [[timeUs=1000000, position=14457]] +numberOfTracks = 1 +track 256: + total output bytes = 856 + sample count = 11 + format 0: + id = 1/256 + sampleMimeType = video/hevc + codecs = hvc1.1.6.L63.90 + width = 914 + height = 686 + pixelWidthHeightRatio = 1.0003651 + initializationData: + data = length 146, hash 61554FEF + sample 0: + time = 333333 + flags = 0 + data = length 65, hash 63DA4886 + sample 1: + time = 400000 + flags = 0 + data = length 33, hash EFE759C6 + sample 2: + time = 600000 + flags = 0 + data = length 88, hash 98333D02 + sample 3: + time = 533333 + flags = 0 + data = length 49, hash F9A023E1 + sample 4: + time = 666666 + flags = 0 + data = length 58, hash 74F1E9D9 + sample 5: + time = 933333 + flags = 0 + data = length 114, hash FA033C4D + sample 6: + time = 800000 + flags = 0 + data = length 87, hash 1A1C57E4 + sample 7: + time = 866666 + flags = 0 + data = length 65, hash 59F937BE + sample 8: + time = 1066666 + flags = 0 + data = length 94, hash 5D02AC81 + sample 9: + time = 1000000 + flags = 0 + data = length 57, hash 2750D207 + sample 10: + time = 1133333 + flags = 0 + data = length 46, hash CE770A40 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.2.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.2.dump new file mode 100644 index 0000000000..c3e8198155 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.2.dump @@ -0,0 +1,45 @@ +seekMap: + isSeekable = true + duration = 1000000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(500000) = [[timeUs=500000, position=7134]] + getPosition(1000000) = [[timeUs=1000000, position=14457]] +numberOfTracks = 1 +track 256: + total output bytes = 563 + sample count = 6 + format 0: + id = 1/256 + sampleMimeType = video/hevc + codecs = hvc1.1.6.L63.90 + width = 914 + height = 686 + pixelWidthHeightRatio = 1.0003651 + initializationData: + data = length 146, hash 61554FEF + sample 0: + time = 933333 + flags = 0 + data = length 114, hash FA033C4D + sample 1: + time = 800000 + flags = 0 + data = length 87, hash 1A1C57E4 + sample 2: + time = 866666 + flags = 0 + data = length 65, hash 59F937BE + sample 3: + time = 1066666 + flags = 0 + data = length 94, hash 5D02AC81 + sample 4: + time = 1000000 + flags = 0 + data = length 57, hash 2750D207 + sample 5: + time = 1133333 + flags = 0 + data = length 46, hash CE770A40 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.3.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.3.dump new file mode 100644 index 0000000000..d10958f482 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.3.dump @@ -0,0 +1,25 @@ +seekMap: + isSeekable = true + duration = 1000000 + getPosition(0) = [[timeUs=0, position=0]] + getPosition(1) = [[timeUs=1, position=0]] + getPosition(500000) = [[timeUs=500000, position=7134]] + getPosition(1000000) = [[timeUs=1000000, position=14457]] +numberOfTracks = 1 +track 256: + total output bytes = 146 + sample count = 1 + format 0: + id = 1/256 + sampleMimeType = video/hevc + codecs = hvc1.1.6.L63.90 + width = 914 + height = 686 + pixelWidthHeightRatio = 1.0003651 + initializationData: + data = length 146, hash 61554FEF + sample 0: + time = 1133333 + flags = 0 + data = length 46, hash CE770A40 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.unknown_length.dump new file mode 100644 index 0000000000..85ddd41279 --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/ts/sample_h265_rps_pred.ts.unknown_length.dump @@ -0,0 +1,78 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 256: + total output bytes = 10004 + sample count = 15 + format 0: + id = 1/256 + sampleMimeType = video/hevc + codecs = hvc1.1.6.L63.90 + width = 914 + height = 686 + pixelWidthHeightRatio = 1.0003651 + initializationData: + data = length 146, hash 61554FEF + sample 0: + time = 266666 + flags = 1 + data = length 7464, hash EBF8518B + sample 1: + time = 1200000 + flags = 0 + data = length 1042, hash F69C93E1 + sample 2: + time = 733333 + flags = 0 + data = length 465, hash 2B469969 + sample 3: + time = 466666 + flags = 0 + data = length 177, hash 79777966 + sample 4: + time = 333333 + flags = 0 + data = length 65, hash 63DA4886 + sample 5: + time = 400000 + flags = 0 + data = length 33, hash EFE759C6 + sample 6: + time = 600000 + flags = 0 + data = length 88, hash 98333D02 + sample 7: + time = 533333 + flags = 0 + data = length 49, hash F9A023E1 + sample 8: + time = 666666 + flags = 0 + data = length 58, hash 74F1E9D9 + sample 9: + time = 933333 + flags = 0 + data = length 114, hash FA033C4D + sample 10: + time = 800000 + flags = 0 + data = length 87, hash 1A1C57E4 + sample 11: + time = 866666 + flags = 0 + data = length 65, hash 59F937BE + sample 12: + time = 1066666 + flags = 0 + data = length 94, hash 5D02AC81 + sample 13: + time = 1000000 + flags = 0 + data = length 57, hash 2750D207 + sample 14: + time = 1133333 + flags = 0 + data = length 46, hash CE770A40 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/media/ts/sample_h265_rps_pred.ts b/libraries/test_data/src/test/assets/media/ts/sample_h265_rps_pred.ts new file mode 100644 index 0000000000000000000000000000000000000000..b5d336564d877dac33530f0583638681eb9af9db GIT binary patch literal 15416 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 X2SV8QBFg7-O|UAY@c1Dvz&QUe#l&M+ literal 0 HcmV?d00001 diff --git a/libraries/test_data/src/test/assets/playbackdumps/ts/sample_h265_rps_pred.ts.dump b/libraries/test_data/src/test/assets/playbackdumps/ts/sample_h265_rps_pred.ts.dump new file mode 100644 index 0000000000..e69de29bb2 From 3406334ee81a7f05f114310a26ecbf6a1deecc49 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 26 Apr 2023 16:42:28 +0100 Subject: [PATCH 07/24] Allow `MediaLibraryService` to reject the resumption notification To reliably reject the System UI playback resumption notification on all API levels (specifically API 30), the backward compatibility layer needs to return `null` for the library root. This is not possible in the Media3 implementation. This change allows an app to return a `LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)` that then is translated to return null by the backwards compatibility layer. Issue: androidx/media#355 Issue: androidx/media#167 Issue: androidx/media#27 See https://developer.android.com/guide/topics/media/media-controls#mediabrowserservice_implementation PiperOrigin-RevId: 527276529 (cherry picked from commit 7938978b5165a9cbb63a6ee1fe5209934e996c6e) --- RELEASENOTES.md | 4 ++++ .../java/androidx/media3/demo/session/PlaybackService.kt | 7 +++++++ .../media3/session/MediaLibraryServiceLegacyStub.java | 7 +++++-- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8eb95c496a..8b67555e01 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,10 @@ * Fix issue where `MediaController` doesn't update its available commands when connected to a legacy `MediaSessionCompat` that updates its actions. + * Fix bug that prevented the `MediaLibraryService` from returning null for + a call from System UI to `Callback.onGetLibraryRoot` with + `params.isRecent == true` on API 30 + ([#355](https://github.com/androidx/media/issues/355)). ### 1.0.1 (2023-04-18) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index 192499d4e1..58720bbbb3 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -29,6 +29,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.util.Util import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.* +import androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED import androidx.media3.session.MediaSession.ControllerInfo import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures @@ -142,6 +143,12 @@ class PlaybackService : MediaLibraryService() { browser: ControllerInfo, params: LibraryParams? ): ListenableFuture> { + if (params != null && params.isRecent) { + // The service currently does not support playback resumption. Tell System UI by returning + // an error of type 'RESULT_ERROR_NOT_SUPPORTED' for a `params.isRecent` request. See + // https://github.com/androidx/media/issues/355 + return Futures.immediateFuture(LibraryResult.ofError(RESULT_ERROR_NOT_SUPPORTED)) + } return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params)) } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java index 60de48cae1..51e81c1b6c 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibraryServiceLegacyStub.java @@ -126,8 +126,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; .putBoolean(BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, isSearchSessionCommandAvailable); return new BrowserRoot(result.value.mediaId, extras); } - // No library root, but keep browser compat connected to allow getting session. - return MediaUtils.defaultBrowserRoot; + // No library root, but keep browser compat connected to allow getting session unless the + // `Callback` implementation has not returned a `RESULT_SUCCESS`. + return result != null && result.resultCode != RESULT_SUCCESS + ? null + : MediaUtils.defaultBrowserRoot; } // TODO(b/192455639): Optimize potential multiple calls of From 40ef64ac3ad9de991c2412babc0e6bde7cb36a8e Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 27 Apr 2023 12:54:17 +0100 Subject: [PATCH 08/24] Fix leaks of media session service. References to the service are kept from MediaSessionStub and from a long-delayed Handler messages in ConnectionTimeoutHandler. Remove strong references from these places by making the timeout handler static and ensuring ConnectedControllersManager only keeps a weak reference to the service (as it's part of MediaSessionStub). Issue: androidx/media#346 PiperOrigin-RevId: 527543396 (cherry picked from commit 8c262d6c072304ed9f16feca64b70a18645cc908) --- RELEASENOTES.md | 2 ++ .../session/ConnectedControllersManager.java | 15 +++++++++++++-- .../media3/session/MediaSessionLegacyStub.java | 13 +++++++++---- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8b67555e01..ea2c005a39 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,8 @@ a call from System UI to `Callback.onGetLibraryRoot` with `params.isRecent == true` on API 30 ([#355](https://github.com/androidx/media/issues/355)). + * Fix memory leak of `MediaSessionService` or `MediaLibraryService` + ([#346](https://github.com/androidx/media/issues/346)). ### 1.0.1 (2023-04-18) diff --git a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java index 64b069effb..e20a05817a 100644 --- a/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java +++ b/libraries/session/src/main/java/androidx/media3/session/ConnectedControllersManager.java @@ -26,6 +26,7 @@ import androidx.media3.session.MediaSession.ControllerInfo; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import java.lang.ref.WeakReference; import java.util.ArrayDeque; import java.util.Deque; import java.util.concurrent.atomic.AtomicBoolean; @@ -62,14 +63,14 @@ import org.checkerframework.checker.nullness.qual.NonNull; private final ArrayMap> controllerRecords = new ArrayMap<>(); - private final MediaSessionImpl sessionImpl; + private final WeakReference sessionImpl; public ConnectedControllersManager(MediaSessionImpl session) { // Initialize default values. lock = new Object(); // Initialize members with params. - sessionImpl = session; + sessionImpl = new WeakReference<>(session); } public void addController( @@ -136,6 +137,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; } record.sequencedFutureManager.release(); + @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get(); + if (sessionImpl == null || sessionImpl.isReleased()) { + return; + } postOrRun( sessionImpl.getApplicationHandler(), () -> { @@ -214,8 +219,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; synchronized (lock) { info = controllerRecords.get(controllerInfo); } + @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get(); return info != null && info.playerCommands.contains(commandCode) + && sessionImpl != null && sessionImpl.getPlayerWrapper().getAvailableCommands().contains(commandCode); } @@ -248,6 +255,10 @@ import org.checkerframework.checker.nullness.qual.NonNull; @GuardedBy("lock") private void flushCommandQueue(ConnectedControllerRecord info) { + @Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get(); + if (sessionImpl == null) { + return; + } AtomicBoolean continueRunning = new AtomicBoolean(true); while (continueRunning.get()) { continueRunning.set(false); diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java index 4872383ebb..e554f9e3a3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionLegacyStub.java @@ -147,12 +147,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; appPackageName = context.getPackageName(); sessionManager = MediaSessionManager.getSessionManager(context); controllerLegacyCbForBroadcast = new ControllerLegacyCbForBroadcast(); - connectionTimeoutHandler = - new ConnectionTimeoutHandler(session.getApplicationHandler().getLooper()); mediaPlayPauseKeyHandler = new MediaPlayPauseKeyHandler(session.getApplicationHandler().getLooper()); connectedControllersManager = new ConnectedControllersManager<>(session); connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS; + connectionTimeoutHandler = + new ConnectionTimeoutHandler( + session.getApplicationHandler().getLooper(), connectedControllersManager); // Select a media button receiver component. ComponentName receiverComponentName = queryPackageManagerForMediaButtonReceiver(context); @@ -1372,12 +1373,16 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } - private class ConnectionTimeoutHandler extends Handler { + private static class ConnectionTimeoutHandler extends Handler { private static final int MSG_CONNECTION_TIMED_OUT = 1001; - public ConnectionTimeoutHandler(Looper looper) { + private final ConnectedControllersManager connectedControllersManager; + + public ConnectionTimeoutHandler( + Looper looper, ConnectedControllersManager connectedControllersManager) { super(looper); + this.connectedControllersManager = connectedControllersManager; } @Override From 179e35b3d10f4dbc14dcd26ad9b093e34d4e4675 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 28 Apr 2023 14:49:54 +0100 Subject: [PATCH 09/24] Add JavaDoc to some undocumented methods and move them PiperOrigin-RevId: 527870443 (cherry picked from commit 336d4b386f9691fff419728d901d0d6ee7a2ebf8) --- .../androidx/media3/session/MediaUtils.java | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 95f3e3bbf5..116faf7a2b 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -1411,13 +1411,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return new Pair<>(mergedPlayerInfo, mergedBundlingExclusions); } - private static byte[] convertToByteArray(Bitmap bitmap) throws IOException { - try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { - bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream); - return stream.toByteArray(); - } - } - + /** Generates an array of {@code n} indices. */ public static int[] generateUnshuffledIndices(int n) { int[] indices = new int[n]; for (int i = 0; i < n; i++) { @@ -1426,6 +1420,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; return indices; } + /** + * Calculates the buffered percentage of the given buffered position and the duration in + * milliseconds. + */ public static int calculateBufferedPercentage(long bufferedPositionMs, long durationMs) { return bufferedPositionMs == C.TIME_UNSET || durationMs == C.TIME_UNSET ? 0 @@ -1434,8 +1432,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; : Util.constrainValue((int) ((bufferedPositionMs * 100) / durationMs), 0, 100); } + /** + * Sets media items with start index and position for the given {@link Player} by honoring the + * available commands. + * + * @param player The player to set the media items. + * @param mediaItemsWithStartPosition The media items, the index and the position to set. + */ public static void setMediaItemsWithStartIndexAndPosition( - PlayerWrapper player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) { + Player player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) { if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET) { if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { player.setMediaItems(mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true); @@ -1443,17 +1448,22 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; player.setMediaItem( mediaItemsWithStartPosition.mediaItems.get(0), /* resetPosition= */ true); } - } else { - if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { - player.setMediaItems( - mediaItemsWithStartPosition.mediaItems, - mediaItemsWithStartPosition.startIndex, - mediaItemsWithStartPosition.startPositionMs); - } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { - player.setMediaItem( - mediaItemsWithStartPosition.mediaItems.get(0), - mediaItemsWithStartPosition.startPositionMs); - } + } else if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) { + player.setMediaItems( + mediaItemsWithStartPosition.mediaItems, + mediaItemsWithStartPosition.startIndex, + mediaItemsWithStartPosition.startPositionMs); + } else if (!mediaItemsWithStartPosition.mediaItems.isEmpty()) { + player.setMediaItem( + mediaItemsWithStartPosition.mediaItems.get(0), + mediaItemsWithStartPosition.startPositionMs); + } + } + + private static byte[] convertToByteArray(Bitmap bitmap) throws IOException { + try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { + bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream); + return stream.toByteArray(); } } From 841bdc6efe784ff3194813aa656d4cad6b0e9a39 Mon Sep 17 00:00:00 2001 From: michaelkatz Date: Fri, 28 Apr 2023 16:31:11 +0100 Subject: [PATCH 10/24] Add UTF-16 encoded subtitle support to SsaDecoder Issue: androidx/media#319 PiperOrigin-RevId: 527891646 (cherry picked from commit 06ac2f7990f0cdf691365cc304fade522c983761) --- RELEASENOTES.md | 3 + .../media3/common/util/ParsableByteArray.java | 70 +++++++++++++----- .../media3/extractor/text/ssa/SsaDecoder.java | 51 +++++++++---- .../extractor/text/ssa/SsaDecoderTest.java | 59 +++++++++++++++ .../src/test/assets/media/ssa/typical_utf16be | Bin 0 -> 1460 bytes .../src/test/assets/media/ssa/typical_utf16le | Bin 0 -> 1484 bytes 6 files changed, 149 insertions(+), 34 deletions(-) create mode 100644 libraries/test_data/src/test/assets/media/ssa/typical_utf16be create mode 100644 libraries/test_data/src/test/assets/media/ssa/typical_utf16le diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ea2c005a39..8e07b6f110 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -12,6 +12,9 @@ * Fix parsing of H.265 SPS in MPEG-TS files by re-using the parsing logic already used by RTSP and MP4 extractors ([#303](https://github.com/androidx/media/issues/303)). +* Text: + * SSA: Add support for UTF-16 files if they start with a byte order mark + ([#319](https://github.com/androidx/media/issues/319)). * Session: * Fix issue where `MediaController` doesn't update its available commands when connected to a legacy `MediaSessionCompat` that updates its diff --git a/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java b/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java index bd1117bc78..9e9e6ead0a 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/ParsableByteArray.java @@ -233,11 +233,28 @@ public final class ParsableByteArray { return (data[position] & 0xFF); } - /** Peeks at the next char. */ + /** + * Peeks at the next char. + * + *

Equivalent to passing {@link Charsets#UTF_16} or {@link Charsets#UTF_16BE} to {@link + * #peekChar(Charset)}. + */ public char peekChar() { return (char) ((data[position] & 0xFF) << 8 | (data[position + 1] & 0xFF)); } + /** + * Peeks at the next char (as decoded by {@code charset}) + * + * @throws IllegalArgumentException if charset is not supported. Only US_ASCII, UTF-8, UTF-16, + * UTF-16BE, and UTF-16LE are supported. + */ + public char peekChar(Charset charset) { + Assertions.checkArgument( + SUPPORTED_CHARSETS_FOR_READLINE.contains(charset), "Unsupported charset: " + charset); + return (char) (peekCharacterAndSize(charset) >> Short.SIZE); + } + /** Reads the next byte as an unsigned value. */ public int readUnsignedByte() { return (data[position++] & 0xFF); @@ -649,27 +666,42 @@ public final class ParsableByteArray { * UTF-8 and two bytes for UTF-16). */ private char readCharacterIfInList(Charset charset, char[] chars) { - char character; - int characterSize; - if ((charset.equals(Charsets.UTF_8) || charset.equals(Charsets.US_ASCII)) && bytesLeft() >= 1) { - character = Chars.checkedCast(UnsignedBytes.toInt(data[position])); - characterSize = 1; - } else if ((charset.equals(Charsets.UTF_16) || charset.equals(Charsets.UTF_16BE)) - && bytesLeft() >= 2) { - character = Chars.fromBytes(data[position], data[position + 1]); - characterSize = 2; - } else if (charset.equals(Charsets.UTF_16LE) && bytesLeft() >= 2) { - character = Chars.fromBytes(data[position + 1], data[position]); - characterSize = 2; - } else { - return 0; - } + int characterAndSize = peekCharacterAndSize(charset); - if (Chars.contains(chars, character)) { - position += characterSize; - return Chars.checkedCast(character); + if (characterAndSize != 0 && Chars.contains(chars, (char) (characterAndSize >> Short.SIZE))) { + position += characterAndSize & 0xFFFF; + return (char) (characterAndSize >> Short.SIZE); } else { return 0; } } + + /** + * Peeks at the character at {@link #position} (as decoded by {@code charset}), returns it and the + * number of bytes the character takes up within the array packed into an int. First four bytes + * are the character and the second four is the size in bytes it takes. Returns 0 if {@link + * #bytesLeft()} doesn't allow reading a whole character in {@code charset} or if the {@code + * charset} is not one of US_ASCII, UTF-8, UTF-16, UTF-16BE, or UTF-16LE. + * + *

Only supports characters that occupy a single code unit (i.e. one byte for UTF-8 and two + * bytes for UTF-16). + */ + private int peekCharacterAndSize(Charset charset) { + byte character; + short characterSize; + if ((charset.equals(Charsets.UTF_8) || charset.equals(Charsets.US_ASCII)) && bytesLeft() >= 1) { + character = (byte) Chars.checkedCast(UnsignedBytes.toInt(data[position])); + characterSize = 1; + } else if ((charset.equals(Charsets.UTF_16) || charset.equals(Charsets.UTF_16BE)) + && bytesLeft() >= 2) { + character = (byte) Chars.fromBytes(data[position], data[position + 1]); + characterSize = 2; + } else if (charset.equals(Charsets.UTF_16LE) && bytesLeft() >= 2) { + character = (byte) Chars.fromBytes(data[position + 1], data[position]); + characterSize = 2; + } else { + return 0; + } + return (Chars.checkedCast(character) << Short.SIZE) + characterSize; + } } diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDecoder.java b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDecoder.java index a981193f99..30017383e9 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDecoder.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/text/ssa/SsaDecoder.java @@ -37,6 +37,8 @@ import androidx.media3.common.util.Util; import androidx.media3.extractor.text.SimpleSubtitleDecoder; import androidx.media3.extractor.text.Subtitle; import com.google.common.base.Ascii; +import com.google.common.base.Charsets; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; @@ -98,11 +100,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { if (initializationData != null && !initializationData.isEmpty()) { haveInitializationData = true; + // Currently, construction with initialization data is only relevant to SSA subtitles muxed + // in a MKV. According to https://www.matroska.org/technical/subtitles.html, these muxed + // subtitles are always encoded in UTF-8. String formatLine = Util.fromUtf8Bytes(initializationData.get(0)); Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX)); dialogueFormatFromInitializationData = Assertions.checkNotNull(SsaDialogueFormat.fromFormatLine(formatLine)); - parseHeader(new ParsableByteArray(initializationData.get(1))); + parseHeader(new ParsableByteArray(initializationData.get(1)), Charsets.UTF_8); } else { haveInitializationData = false; dialogueFormatFromInitializationData = null; @@ -115,25 +120,37 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { List cueTimesUs = new ArrayList<>(); ParsableByteArray parsableData = new ParsableByteArray(data, length); + Charset charset = detectUtfCharset(parsableData); + if (!haveInitializationData) { - parseHeader(parsableData); + parseHeader(parsableData, charset); } - parseEventBody(parsableData, cues, cueTimesUs); + parseEventBody(parsableData, cues, cueTimesUs, charset); return new SsaSubtitle(cues, cueTimesUs); } + /** + * Determine UTF encoding of the byte array from a byte order mark (BOM), defaulting to UTF-8 if + * no BOM is found. + */ + private Charset detectUtfCharset(ParsableByteArray data) { + @Nullable Charset charset = data.readUtfCharsetFromBom(); + return charset != null ? charset : Charsets.UTF_8; + } + /** * Parses the header of the subtitle. * * @param data A {@link ParsableByteArray} from which the header should be read. + * @param charset The {@code Charset} of the encoding of {@code data}. */ - private void parseHeader(ParsableByteArray data) { + private void parseHeader(ParsableByteArray data, Charset charset) { @Nullable String currentLine; - while ((currentLine = data.readLine()) != null) { + while ((currentLine = data.readLine(charset)) != null) { if ("[Script Info]".equalsIgnoreCase(currentLine)) { - parseScriptInfo(data); + parseScriptInfo(data, charset); } else if ("[V4+ Styles]".equalsIgnoreCase(currentLine)) { - styles = parseStyles(data); + styles = parseStyles(data, charset); } else if ("[V4 Styles]".equalsIgnoreCase(currentLine)) { Log.i(TAG, "[V4 Styles] are not supported"); } else if ("[Events]".equalsIgnoreCase(currentLine)) { @@ -151,11 +168,12 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition() position} * set to the beginning of the first line after {@code [Script Info]}. + * @param charset The {@code Charset} of the encoding of {@code data}. */ - private void parseScriptInfo(ParsableByteArray data) { + private void parseScriptInfo(ParsableByteArray data, Charset charset) { @Nullable String currentLine; - while ((currentLine = data.readLine()) != null - && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + while ((currentLine = data.readLine(charset)) != null + && (data.bytesLeft() == 0 || data.peekChar(charset) != '[')) { String[] infoNameAndValue = currentLine.split(":"); if (infoNameAndValue.length != 2) { continue; @@ -187,13 +205,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * * @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition()} pointing * at the beginning of the first line after {@code [V4+ Styles]}. + * @param charset The {@code Charset} of the encoding of {@code data}. */ - private static Map parseStyles(ParsableByteArray data) { + private static Map parseStyles(ParsableByteArray data, Charset charset) { Map styles = new LinkedHashMap<>(); @Nullable SsaStyle.Format formatInfo = null; @Nullable String currentLine; - while ((currentLine = data.readLine()) != null - && (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) { + while ((currentLine = data.readLine(charset)) != null + && (data.bytesLeft() == 0 || data.peekChar(charset) != '[')) { if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { formatInfo = SsaStyle.Format.fromFormatLine(currentLine); } else if (currentLine.startsWith(STYLE_LINE_PREFIX)) { @@ -216,12 +235,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder { * @param data A {@link ParsableByteArray} from which the body should be read. * @param cues A list to which parsed cues will be added. * @param cueTimesUs A sorted list to which parsed cue timestamps will be added. + * @param charset The {@code Charset} of the encoding of {@code data}. */ - private void parseEventBody(ParsableByteArray data, List> cues, List cueTimesUs) { + private void parseEventBody( + ParsableByteArray data, List> cues, List cueTimesUs, Charset charset) { @Nullable SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null; @Nullable String currentLine; - while ((currentLine = data.readLine()) != null) { + while ((currentLine = data.readLine(charset)) != null) { if (currentLine.startsWith(FORMAT_LINE_PREFIX)) { format = SsaDialogueFormat.fromFormatLine(currentLine); } else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) { diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaDecoderTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaDecoderTest.java index 0c9cfff208..e831a0460c 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaDecoderTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/text/ssa/SsaDecoderTest.java @@ -30,6 +30,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.Iterables; import java.io.IOException; import java.util.ArrayList; +import java.util.Objects; import org.junit.Test; import org.junit.runner.RunWith; @@ -43,6 +44,8 @@ public final class SsaDecoderTest { private static final String TYPICAL_HEADER_ONLY = "media/ssa/typical_header"; private static final String TYPICAL_DIALOGUE_ONLY = "media/ssa/typical_dialogue"; private static final String TYPICAL_FORMAT_ONLY = "media/ssa/typical_format"; + private static final String TYPICAL_UTF16LE = "media/ssa/typical_utf16le"; + private static final String TYPICAL_UTF16BE = "media/ssa/typical_utf16be"; private static final String OVERLAPPING_TIMECODES = "media/ssa/overlapping_timecodes"; private static final String POSITIONS = "media/ssa/positioning"; private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes"; @@ -130,6 +133,58 @@ public final class SsaDecoderTest { assertTypicalCue3(subtitle, 4); } + @Test + public void decodeTypicalUtf16le() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16LE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + // Check position, line, anchors & alignment are set from Alignment Style (2 - bottom-center). + Cue firstCue = subtitle.getCues(subtitle.getEventTime(0)).get(0); + assertWithMessage("Cue.textAlignment") + .that(firstCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_CENTER); + assertWithMessage("Cue.positionAnchor") + .that(firstCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.95f); + + assertTypicalCue1(subtitle, 0); + assertTypicalCue2(subtitle, 2); + assertTypicalCue3(subtitle, 4); + } + + @Test + public void decodeTypicalUtf16be() throws IOException { + SsaDecoder decoder = new SsaDecoder(); + byte[] bytes = + TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), TYPICAL_UTF16BE); + Subtitle subtitle = decoder.decode(bytes, bytes.length, false); + + assertThat(subtitle.getEventTimeCount()).isEqualTo(6); + // Check position, line, anchors & alignment are set from Alignment Style (2 - bottom-center). + Cue firstCue = subtitle.getCues(subtitle.getEventTime(0)).get(0); + assertWithMessage("Cue.textAlignment") + .that(firstCue.textAlignment) + .isEqualTo(Layout.Alignment.ALIGN_CENTER); + assertWithMessage("Cue.positionAnchor") + .that(firstCue.positionAnchor) + .isEqualTo(Cue.ANCHOR_TYPE_MIDDLE); + assertThat(firstCue.position).isEqualTo(0.5f); + assertThat(firstCue.lineAnchor).isEqualTo(Cue.ANCHOR_TYPE_END); + assertThat(firstCue.lineType).isEqualTo(Cue.LINE_TYPE_FRACTION); + assertThat(firstCue.line).isEqualTo(0.95f); + + assertTypicalCue1(subtitle, 0); + assertTypicalCue2(subtitle, 2); + assertTypicalCue3(subtitle, 4); + } + @Test public void decodeOverlappingTimecodes() throws IOException { SsaDecoder decoder = new SsaDecoder(); @@ -438,6 +493,10 @@ public final class SsaDecoderTest { assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0); assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString()) .isEqualTo("This is the first subtitle."); + assertThat( + Objects.requireNonNull( + subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).textAlignment)) + .isEqualTo(Layout.Alignment.ALIGN_CENTER); assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(1230000); } diff --git a/libraries/test_data/src/test/assets/media/ssa/typical_utf16be b/libraries/test_data/src/test/assets/media/ssa/typical_utf16be new file mode 100644 index 0000000000000000000000000000000000000000..6b11ad0ed5760b3219046e3154f66f28f5fb1633 GIT binary patch literal 1460 zcmbW1TW`}q5QXPCzame(5USQmq2!H1D=HO=NT@)-Luxx0H3^lS5(4U9FMMa##x4$0 zL6Pmwz9UJ*wV)K#XeihhM+5(TWWoFEqu?dXAkijfN$KA zyXP?xnJsJt-pBr$)q=PVYuT3I%Pm+=h3|#m8)u;$8M&VmHRrizrz``!V$YQP#p4WL zv7J#VZNwiE72D+&Ty+w!aYFpaGsM1sN2Y{a+3&1@$;z2tVhJ?^8hdPN?+d=={zI|9 zat2~Iapak=)Lq3NqAe?`Ro#{4uC7pPO0I;6b&*M`C*)Jl)oXdZ!Y2b&#nT4zwt<{G za)h_~oOs-XQ?J{HjcV$IMtpN$r7)S<1$jbmyY?902lmo-`5o7O z6)H<6WgMZi;E`d{kL+vF%k2|> zirNhx<#cLtvZC|qVVJ0@cRG>!tr!yf4gVG$oe`^9pU_p^wRWMU?<&H3r%<$uUE1`P zlF0kgJe4^aH|QDKl>9oC@LsV`*^Sv};JWiUbuUc2Ss*?mpQT}9Hr;5QQEUxByiC;$Ke literal 0 HcmV?d00001 diff --git a/libraries/test_data/src/test/assets/media/ssa/typical_utf16le b/libraries/test_data/src/test/assets/media/ssa/typical_utf16le new file mode 100644 index 0000000000000000000000000000000000000000..da098604d0ebf8d4babfa5ebf7555c44de8a9b3d GIT binary patch literal 1484 zcmbW1-A~g{7{=dg6aNRUym1ofhT`r-Ad;XMH;D$lC~fxN# z8px?52WYF#soM>``xPJccAppnd@rerBAqZnimD#PeCNK);4-#zJca7E?K!$n?2T>l z9M+x+m86u`(FpW~ZCktMq14s4WzUede5aG%ZPxm?!X?|pU(kzcc9HG6rKUYh{DFNd zDth0fACvPMopMSwL3z;u)iO+0RY7&8{&)1T{f@rk?Nc$$-z8mJf~(qrch|EC>t>PJ z7INjNRT)KIpJsO^cwCcL-$wXXcfz{hoN*d(&cSsH%!zx>uKfG2*=epcn;E&>5a(Z+ zM(DP=F;wS@X&ba-JD?9|I)_7f2Q;~E*cX_tocoAeVqV<@EAkI_f;3cnbwzUJ6{?Uw nAg>PD?D#a-^V$V{Nv8jkmpG^gV)b_iG%vSvQU#_wal!dFI}zx_ literal 0 HcmV?d00001 From feb83c2d5924b84f81c88f4600ff175e4052aadc Mon Sep 17 00:00:00 2001 From: tofunmi Date: Wed, 3 May 2023 14:13:12 +0100 Subject: [PATCH 11/24] Update translations PiperOrigin-RevId: 529069808 (cherry picked from commit bba760f6e5ae8ba6db2cd2e45163698223335c2a) --- libraries/session/src/main/res/values-pt-rPT/strings.xml | 2 +- libraries/ui/src/main/res/values-am/strings.xml | 2 +- libraries/ui/src/main/res/values-ky/strings.xml | 4 ++-- libraries/ui/src/main/res/values-mk/strings.xml | 2 +- libraries/ui/src/main/res/values-th/strings.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/session/src/main/res/values-pt-rPT/strings.xml b/libraries/session/src/main/res/values-pt-rPT/strings.xml index d341ca707b..b7a4819fdf 100755 --- a/libraries/session/src/main/res/values-pt-rPT/strings.xml +++ b/libraries/session/src/main/res/values-pt-rPT/strings.xml @@ -5,7 +5,7 @@ Pausar Retroceder para o item anterior Avançar para o item seguinte - Retroceder + Anterior Avançar Autenticação necessária diff --git a/libraries/ui/src/main/res/values-am/strings.xml b/libraries/ui/src/main/res/values-am/strings.xml index 82802a43cd..9e1121dc6d 100644 --- a/libraries/ui/src/main/res/values-am/strings.xml +++ b/libraries/ui/src/main/res/values-am/strings.xml @@ -6,7 +6,7 @@ ቅንብሮች ተጨማሪ ቅንብሮችን ይደብቁ ተጨማሪ ቅንብሮችን ያሳዩ - ወደ ሙሉ ማያ ገጽ ግባ + ወደ ሙሉ ማያ ገፅ ግባ ከሙሉ ማያገጽ ውጣ ቀዳሚ ቀጣይ diff --git a/libraries/ui/src/main/res/values-ky/strings.xml b/libraries/ui/src/main/res/values-ky/strings.xml index 818f69e1ea..d7e785aa0c 100644 --- a/libraries/ui/src/main/res/values-ky/strings.xml +++ b/libraries/ui/src/main/res/values-ky/strings.xml @@ -4,8 +4,8 @@ Ойноткучту башкаруу элементтерин жашыруу Ойнотуу көрсөткүчү Параметрлер - Кошумча жөндөөлөрдү жашыруу - Кошумча жөндөөлөрдү көрсөтүү + Кошумча параметрлерди жашыруу + Кошумча параметрлерди көрсөтүү Толук экранга кирүү Толук экран режиминен чыгуу Мурунку diff --git a/libraries/ui/src/main/res/values-mk/strings.xml b/libraries/ui/src/main/res/values-mk/strings.xml index 95fe201eb9..33655cc6dc 100644 --- a/libraries/ui/src/main/res/values-mk/strings.xml +++ b/libraries/ui/src/main/res/values-mk/strings.xml @@ -1,7 +1,7 @@ Прикажи ги контролите на плеерот - Сокриј ги контролите на плеерот + Скриј ги контролите на плеерот Напредок на репродукцијата Поставки Сокријте ги дополнителните поставки diff --git a/libraries/ui/src/main/res/values-th/strings.xml b/libraries/ui/src/main/res/values-th/strings.xml index 5584dcf93c..b326cec9e9 100644 --- a/libraries/ui/src/main/res/values-th/strings.xml +++ b/libraries/ui/src/main/res/values-th/strings.xml @@ -45,7 +45,7 @@ เสียง ข้อความ ไม่มี - ยานยนต์ + อัตโนมัติ ไม่ทราบ %1$d × %2$d โมโน From b0b34def3d53742e1f87f45316fe6e8bff9d182c Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 3 May 2023 16:25:46 +0000 Subject: [PATCH 12/24] Fix demo app UnsafeOptInUsageError lint errors This change: * Adds missing `@OptIn` annotation to demo app's `ErrorMessageProvider` * Switches from `Util.SDK_INT` to `Build.VERSION.SDK_INT` in `SampleChooserActivity` (`PlayerActivity` is already using this). This code hasn't changed recently, and it doesn't fail on the `release` branch, but it failed when I checked the `main` branch just now - so I assume lint has updated to detect more cases where unstable APIs are being used without opt-in. I suspect the difference is due to different Android Gradle Plugin versions between the branches. #minor-release PiperOrigin-RevId: 529111669 (cherry picked from commit ebcdd983e2c5a9819d8a66873351bccbf93124c6) --- .../main/java/androidx/media3/demo/main/PlayerActivity.java | 1 + .../java/androidx/media3/demo/main/SampleChooserActivity.java | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index 22ee82e179..d812c2ffbf 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -514,6 +514,7 @@ public class PlayerActivity extends AppCompatActivity private class PlayerErrorMessageProvider implements ErrorMessageProvider { + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) @Override public Pair getErrorMessage(PlaybackException e) { String errorString = getString(R.string.error_generic); diff --git a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java index ef01b148ca..5afa2613e3 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java @@ -27,6 +27,7 @@ import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.net.Uri; import android.os.AsyncTask; +import android.os.Build; import android.os.Bundle; import android.text.TextUtils; import android.util.JsonReader; @@ -273,7 +274,7 @@ public class SampleChooserActivity extends AppCompatActivity Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) .show(); } else if (!notificationPermissionToastShown - && Util.SDK_INT >= 33 + && Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Api33.getPostNotificationPermissionString()) != PackageManager.PERMISSION_GRANTED) { downloadMediaItemWaitingForNotificationPermission = playlistHolder.mediaItems.get(0); From 375cdb2e22118240109eaab311db4255b5717a41 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 3 May 2023 16:29:35 +0000 Subject: [PATCH 13/24] Use a for-each loop instead of `forEach` in `PlaybackService.kt` The current code flags a lint error: ``` Error: Call requires API level 24 (current min is 16): java.lang.Iterable#forEach [NewApi] ``` I think this is a bit confusing because this is calling the Java [`Iterable.forEach`](https://developer.android.com/reference/java/lang/Iterable#forEach(java.util.function.Consumer%3C?%20super%20T%3E)) method which was added in Java 8 (and therefore is only available on API 24 and up), but there is **also** a Kotlin [`List.forEach`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/for-each.html) method which is available in all versions of Kotlin (and therefore all Android versions). Since this is a Kotlin file, at first glance you would assume this is the Kotlin method - but it's not. This also doesn't seem to be flagged by Android Studio, but is caught by Gradle lint on the command line. #minor-release PiperOrigin-RevId: 529112610 (cherry picked from commit 09b474a51936c9c83df32dfcaf816e37a72552ce) --- .../main/java/androidx/media3/demo/session/PlaybackService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index 58720bbbb3..eea7727ce4 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -96,7 +96,7 @@ class PlaybackService : MediaLibraryService() { ): MediaSession.ConnectionResult { val connectionResult = super.onConnect(session, controller) val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon() - customCommands.forEach { commandButton -> + for (commandButton in customCommands) { // Add custom command to available session commands. commandButton.sessionCommand?.let { availableSessionCommands.add(it) } } From 7a1d7bf5ea913182ea13567f561baed06a168e8c Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 4 May 2023 11:27:54 +0000 Subject: [PATCH 14/24] Temporarily suppress missing permission lint in session demo #minor-release PiperOrigin-RevId: 529370535 (cherry picked from commit 0f398d511dda9e89db64c841329a3938ea38bb62) --- .../main/java/androidx/media3/demo/session/PlaybackService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt index eea7727ce4..b2ac113fcb 100644 --- a/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt +++ b/demos/session/src/main/java/androidx/media3/demo/session/PlaybackService.kt @@ -15,6 +15,7 @@ */ package androidx.media3.demo.session +import android.annotation.SuppressLint import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent.* @@ -277,6 +278,7 @@ class PlaybackService : MediaLibraryService() { * by a media controller to resume playback when the {@link MediaSessionService} is in the * background. */ + @SuppressLint("MissingPermission") // TODO: b/280766358 - Request this permission at runtime. override fun onForegroundServiceStartNotAllowedException() { val notificationManagerCompat = NotificationManagerCompat.from(this@PlaybackService) ensureNotificationChannel(notificationManagerCompat) From 13191edd90da10ef4996a82eb139aac2a4776ab5 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 5 May 2023 09:53:52 +0000 Subject: [PATCH 15/24] Javadoc tweaks for `MediaSession.MediaItemsWithPosition` Also change some type parameter names in `MediaSession.BuilderBase` because `C` now clashes with the import of `androidx.media3.common.C`. #minor-release PiperOrigin-RevId: 529665698 (cherry picked from commit 78f20257bd9b1cdebea3f2968cb946046b1e34d8) --- .../androidx/media3/session/MediaSession.java | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java index 475e13020e..655a5457a7 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSession.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSession.java @@ -39,6 +39,7 @@ import androidx.annotation.VisibleForTesting; import androidx.core.app.NotificationCompat; import androidx.media.MediaSessionManager.RemoteUserInfo; import androidx.media3.common.AudioAttributes; +import androidx.media3.common.C; import androidx.media3.common.DeviceInfo; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaLibraryInfo; @@ -1161,9 +1162,9 @@ public class MediaSession { * the items directly by using Guava's {@link Futures#immediateFuture(Object)}. Once the {@link * MediaItemsWithStartPosition} has been resolved, the session will call {@link * Player#setMediaItems} as requested. If the resolved {@link - * MediaItemsWithStartPosition#startIndex startIndex} is {@link - * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} then the session will call {@link - * Player#setMediaItem(MediaItem, boolean)} with {@code resetPosition} set to {@code true}. + * MediaItemsWithStartPosition#startIndex startIndex} is {@link C#INDEX_UNSET C.INDEX_UNSET} + * then the session will call {@link Player#setMediaItem(MediaItem, boolean)} with {@code + * resetPosition} set to {@code true}. * *

Interoperability: This method will be called in response to the following {@link * MediaControllerCompat} methods: @@ -1188,19 +1189,18 @@ public class MediaSession { * @param controller The controller information. * @param mediaItems The list of requested {@linkplain MediaItem media items}. * @param startIndex The start index in the {@link MediaItem} list from which to start playing, - * or {@link androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} to start playing from the - * default index in the playlist. + * or {@link C#INDEX_UNSET C.INDEX_UNSET} to start playing from the default index in the + * playlist. * @param startPositionMs The starting position in the media item from where to start playing, - * or {@link androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} to start playing from the - * default position in the media item. This value is ignored if startIndex is C.INDEX_UNSET + * or {@link C#TIME_UNSET C.TIME_UNSET} to start playing from the default position in the + * media item. This value is ignored if startIndex is C.INDEX_UNSET * @return A {@link ListenableFuture} with a {@link MediaItemsWithStartPosition} containing a * list of resolved {@linkplain MediaItem media items}, and a starting index and position * that are playable by the underlying {@link Player}. If returned {@link - * MediaItemsWithStartPosition#startIndex} is {@link androidx.media3.common.C#INDEX_UNSET - * C.INDEX_UNSET} and {@link MediaItemsWithStartPosition#startPositionMs} is {@link - * androidx.media3.common.C#TIME_UNSET C.TIME_UNSET}, then {@linkplain - * Player#setMediaItems(List, boolean) Player#setMediaItems(List, true)} will be called to - * set media items with default index and position. + * MediaItemsWithStartPosition#startIndex} is {@link C#INDEX_UNSET C.INDEX_UNSET} and {@link + * MediaItemsWithStartPosition#startPositionMs} is {@link C#TIME_UNSET C.TIME_UNSET}, then + * {@linkplain Player#setMediaItems(List, boolean) Player#setMediaItems(List, true)} will be + * called to set media items with default index and position. */ @UnstableApi default ListenableFuture onSetMediaItems( @@ -1217,34 +1217,35 @@ public class MediaSession { } } - /** Representation of list of media items and where to start playing */ + /** Representation of a list of {@linkplain MediaItem media items} and where to start playing. */ @UnstableApi public static final class MediaItemsWithStartPosition { - /** List of {@link MediaItem media items}. */ + /** List of {@linkplain MediaItem media items}. */ public final ImmutableList mediaItems; /** - * Index to start playing at in {@link MediaItem} list. + * Index to start playing at in {@link #mediaItems}. * - *

The start index in the {@link MediaItem} list from which to start playing, or {@link - * androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} to start playing from the default index - * in the playlist. + *

The start index in {@link #mediaItems} from which to start playing, or {@link + * C#INDEX_UNSET} to start playing from the default index in the playlist. */ public final int startIndex; /** - * Position to start playing from in starting media item. + * Position in milliseconds to start playing from in the starting media item. * *

The starting position in the media item from where to start playing, or {@link - * androidx.media3.common.C#TIME_UNSET C.TIME_UNSET} to start playing from the default position - * in the media item. This value is ignored if startIndex is C.INDEX_UNSET + * C#TIME_UNSET} to start playing from the default position in the media item. This value is + * ignored if {@code startIndex} is {@link C#INDEX_UNSET}. */ public final long startPositionMs; /** - * Create an instance. + * Creates an instance. * - * @param mediaItems List of {@link MediaItem media items}. - * @param startIndex Index to start playing at in {@link MediaItem} list. - * @param startPositionMs Position to start playing from in starting media item. + * @param mediaItems List of {@linkplain MediaItem media items}. + * @param startIndex Index to start playing at in {@code mediaItems}, or {@link C#INDEX_UNSET} + * to start from the default index. + * @param startPositionMs Position in milliseconds to start playing from in the starting media + * item, or {@link C#TIME_UNSET} to start from the default position. */ public MediaItemsWithStartPosition( List mediaItems, int startIndex, long startPositionMs) { @@ -1473,17 +1474,19 @@ public class MediaSession { * applied to the subclasses. */ /* package */ abstract static class BuilderBase< - T extends MediaSession, U extends BuilderBase, C extends Callback> { + SessionT extends MediaSession, + BuilderT extends BuilderBase, + CallbackT extends Callback> { /* package */ final Context context; /* package */ final Player player; /* package */ String id; - /* package */ C callback; + /* package */ CallbackT callback; /* package */ @Nullable PendingIntent sessionActivity; /* package */ Bundle extras; /* package */ @MonotonicNonNull BitmapLoader bitmapLoader; - public BuilderBase(Context context, Player player, C callback) { + public BuilderBase(Context context, Player player, CallbackT callback) { this.context = checkNotNull(context); this.player = checkNotNull(player); checkArgument(player.canAdvertiseSession()); @@ -1493,35 +1496,35 @@ public class MediaSession { } @SuppressWarnings("unchecked") - public U setSessionActivity(PendingIntent pendingIntent) { + public BuilderT setSessionActivity(PendingIntent pendingIntent) { sessionActivity = checkNotNull(pendingIntent); - return (U) this; + return (BuilderT) this; } @SuppressWarnings("unchecked") - public U setId(String id) { + public BuilderT setId(String id) { this.id = checkNotNull(id); - return (U) this; + return (BuilderT) this; } @SuppressWarnings("unchecked") - /* package */ U setCallback(C callback) { + /* package */ BuilderT setCallback(CallbackT callback) { this.callback = checkNotNull(callback); - return (U) this; + return (BuilderT) this; } @SuppressWarnings("unchecked") - public U setExtras(Bundle extras) { + public BuilderT setExtras(Bundle extras) { this.extras = new Bundle(checkNotNull(extras)); - return (U) this; + return (BuilderT) this; } @SuppressWarnings("unchecked") - public U setBitmapLoader(BitmapLoader bitmapLoader) { + public BuilderT setBitmapLoader(BitmapLoader bitmapLoader) { this.bitmapLoader = bitmapLoader; - return (U) this; + return (BuilderT) this; } - public abstract T build(); + public abstract SessionT build(); } } From 3064bc9b376a3d140c4c7d4fe91f9768fbfea0c4 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 5 May 2023 15:01:58 +0000 Subject: [PATCH 16/24] Fix value type when unbundling LibraryResult without expected type Calling LibraryResult.toBundle() could have caused a CastClassException. This was because when unbundled with UNKNOWN_TYPE_CREATOR.fromBundle(Bundle), the valueType was set to VALUE_TYPE_ITEM_LIST for all types and the MediaItem was attempted to be casted to a list. PiperOrigin-RevId: 529717688 (cherry picked from commit f28a5888091c5caa685117d7a6653ae6c5a29f26) --- .../media3/session/LibraryResult.java | 2 +- .../media3/session/LibraryResultTest.java | 76 +++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java index 1d123d1107..637eb38a0f 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java @@ -388,7 +388,7 @@ public final class LibraryResult implements Bundleable { throw new IllegalStateException(); } - return new LibraryResult<>(resultCode, completionTimeMs, params, value, VALUE_TYPE_ITEM_LIST); + return new LibraryResult<>(resultCode, completionTimeMs, params, value, valueType); } @Documented diff --git a/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java b/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java index a4d7afc33c..9340766c09 100644 --- a/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java +++ b/libraries/session/src/test/java/androidx/media3/session/LibraryResultTest.java @@ -15,11 +15,17 @@ */ package androidx.media3.session; +import static androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED; +import static androidx.media3.session.LibraryResult.UNKNOWN_TYPE_CREATOR; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import android.os.Bundle; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; +import androidx.media3.session.MediaLibraryService.LibraryParams; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.collect.ImmutableList; import org.junit.Test; import org.junit.runner.RunWith; @@ -51,4 +57,74 @@ public class LibraryResultTest { assertThrows( IllegalArgumentException.class, () -> LibraryResult.ofItem(item, /* params= */ null)); } + + @Test + public void toBundle_mediaItemLibraryResultThatWasUnbundledAsAnUnknownType_noException() { + MediaItem mediaItem = + new MediaItem.Builder() + .setMediaId("rootMediaId") + .setMediaMetadata( + new MediaMetadata.Builder().setIsPlayable(false).setIsBrowsable(true).build()) + .build(); + LibraryParams params = new LibraryParams.Builder().build(); + LibraryResult libraryResult = LibraryResult.ofItem(mediaItem, params); + Bundle libraryResultBundle = libraryResult.toBundle(); + LibraryResult libraryResultFromUntyped = + UNKNOWN_TYPE_CREATOR.fromBundle(libraryResultBundle); + + Bundle bundleOfUntyped = libraryResultFromUntyped.toBundle(); + + assertThat(UNKNOWN_TYPE_CREATOR.fromBundle(bundleOfUntyped).value).isEqualTo(mediaItem); + } + + @Test + public void toBundle_mediaItemListLibraryResultThatWasUnbundledAsAnUnknownType_noException() { + MediaItem mediaItem = + new MediaItem.Builder() + .setMediaId("rootMediaId") + .setMediaMetadata( + new MediaMetadata.Builder().setIsPlayable(false).setIsBrowsable(true).build()) + .build(); + LibraryParams params = new LibraryParams.Builder().build(); + LibraryResult> libraryResult = + LibraryResult.ofItemList(ImmutableList.of(mediaItem), params); + Bundle libraryResultBundle = libraryResult.toBundle(); + LibraryResult mediaItemLibraryResultFromUntyped = + UNKNOWN_TYPE_CREATOR.fromBundle(libraryResultBundle); + + Bundle bundleOfUntyped = mediaItemLibraryResultFromUntyped.toBundle(); + + assertThat(UNKNOWN_TYPE_CREATOR.fromBundle(bundleOfUntyped).value) + .isEqualTo(ImmutableList.of(mediaItem)); + } + + @Test + public void toBundle_errorResultThatWasUnbundledAsAnUnknownType_noException() { + LibraryResult> libraryResult = + LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED); + Bundle errorLibraryResultBundle = libraryResult.toBundle(); + LibraryResult libraryResultFromUntyped = + UNKNOWN_TYPE_CREATOR.fromBundle(errorLibraryResultBundle); + + Bundle bundleOfUntyped = libraryResultFromUntyped.toBundle(); + + assertThat(UNKNOWN_TYPE_CREATOR.fromBundle(bundleOfUntyped).value).isNull(); + assertThat(UNKNOWN_TYPE_CREATOR.fromBundle(bundleOfUntyped).resultCode) + .isEqualTo(RESULT_ERROR_NOT_SUPPORTED); + } + + @Test + public void toBundle_voidResultThatWasUnbundledAsAnUnknownType_noException() { + LibraryResult> libraryResult = + LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED); + Bundle errorLibraryResultBundle = libraryResult.toBundle(); + LibraryResult libraryResultFromUntyped = + UNKNOWN_TYPE_CREATOR.fromBundle(errorLibraryResultBundle); + + Bundle bundleOfUntyped = libraryResultFromUntyped.toBundle(); + + assertThat(UNKNOWN_TYPE_CREATOR.fromBundle(bundleOfUntyped).value).isNull(); + assertThat(UNKNOWN_TYPE_CREATOR.fromBundle(bundleOfUntyped).resultCode) + .isEqualTo(RESULT_ERROR_NOT_SUPPORTED); + } } From a098f8672e784c2b5a5be70262e32d0184bab87f Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 5 May 2023 17:08:14 +0000 Subject: [PATCH 17/24] Add tests for `MediaLibraryInfo` version code consistency `VERSION_INT` is quite long with several sections, and it's easy to make a mistake when updating it - this should help since it checks it against `VERSION`, which is more easily human readable/writable. PiperOrigin-RevId: 529747023 (cherry picked from commit eb58d20067b258746f72127a601dd5ee40ce9c25) --- .../media3/common/MediaLibraryInfoTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 libraries/common/src/test/java/androidx/media3/common/MediaLibraryInfoTest.java diff --git a/libraries/common/src/test/java/androidx/media3/common/MediaLibraryInfoTest.java b/libraries/common/src/test/java/androidx/media3/common/MediaLibraryInfoTest.java new file mode 100644 index 0000000000..287eb18807 --- /dev/null +++ b/libraries/common/src/test/java/androidx/media3/common/MediaLibraryInfoTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2023 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.common; + +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.common.truth.Expect; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** Tests for {@link MediaLibraryInfo}. */ +@RunWith(AndroidJUnit4.class) +public class MediaLibraryInfoTest { + + private static final Pattern VERSION_PATTERN = + Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)(?:-(alpha|beta|rc)(\\d\\d))?"); + + @Rule public final Expect expect = Expect.create(); + + @Test + public void versionAndSlashyAreConsistent() { + assertThat(MediaLibraryInfo.VERSION_SLASHY) + .isEqualTo("AndroidXMedia3/" + MediaLibraryInfo.VERSION); + } + + @Test + public void versionIntIsSelfConsistentAndConsistentWithVersionString() { + // Use the Truth .matches() call so any failure has a clearer error message, then call + // Matcher#matches() below so the subsequent group(int) calls work. + assertThat(MediaLibraryInfo.VERSION).matches(VERSION_PATTERN); + Matcher matcher = VERSION_PATTERN.matcher(MediaLibraryInfo.VERSION); + checkState(matcher.matches()); + + int major = Integer.parseInt(matcher.group(1)); + int minor = Integer.parseInt(matcher.group(2)); + int bugfix = Integer.parseInt(matcher.group(3)); + String phase = matcher.group(4); + + expect.that(major).isAtLeast(1); + + int expectedVersionInt = 0; + expectedVersionInt += major * 1_000_000_000; + expectedVersionInt += minor * 1_000_000; + expectedVersionInt += bugfix * 1000; + + int phaseInt; + if (phase != null) { + expect.that(bugfix).isEqualTo(0); + switch (phase) { + case "alpha": + phaseInt = 0; + break; + case "beta": + phaseInt = 1; + break; + case "rc": + phaseInt = 2; + break; + default: + throw new AssertionError("Unrecognized phase: " + phase); + } + int phaseCount = Integer.parseInt(matcher.group(5)); + expect.that(phaseCount).isAtLeast(1); + expectedVersionInt += phaseCount; + } else { + // phase == null, so this is a stable or bugfix release. + phaseInt = 3; + } + expectedVersionInt += phaseInt * 100; + expect + .withMessage("VERSION_INT for " + MediaLibraryInfo.VERSION) + .that(MediaLibraryInfo.VERSION_INT) + .isEqualTo(expectedVersionInt); + } +} From f71370af75ac199ec9dc237e1d5f6d09a63655a6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 10 May 2023 16:53:20 +0000 Subject: [PATCH 18/24] Remove a copybara stripping tag #minor-release PiperOrigin-RevId: 530935437 (cherry picked from commit 17b183b11a3499ce0f86b53b9d60b66ba71f73cc) --- libraries/common/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/common/build.gradle b/libraries/common/build.gradle index 85169e2ec8..476d299f73 100644 --- a/libraries/common/build.gradle +++ b/libraries/common/build.gradle @@ -23,7 +23,7 @@ rootProject.allprojects.forEach { evaluationDependsOn(':' + it.name) } } -// copybara:media3-only + android { buildTypes { debug { From 0888dfbd05e1f22436137d93a67e56d06b8928cc Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 12 May 2023 10:31:39 +0000 Subject: [PATCH 19/24] Update the root project name check in `publish.gradle` The name was changed in https://github.com/androidx/media/commit/25581384e93bcb647a31678809421025385d72a0 and this check wasn't updated, meaning publishing no longer worked (it didn't publish anything, just printed lots of warnings like `Skipping task ':test-utils-robolectric:publish' as it has no actions.`). This change means the check is now using the same source-of-truth as the root project name, so it shouldn't go out of sync again. PiperOrigin-RevId: 531457952 (cherry picked from commit 4c1eb8aec7258324134913db521cd0a005ece31b) --- core_settings.gradle | 2 ++ publish.gradle | 2 +- settings.gradle | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/core_settings.gradle b/core_settings.gradle index b331d11b4d..0ef4e443c4 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -21,6 +21,8 @@ if (gradle.ext.has('androidxMediaModulePrefix')) { modulePrefix += gradle.ext.androidxMediaModulePrefix } +rootProject.name = gradle.ext.androidxMediaProjectName + include modulePrefix + 'lib-common' project(modulePrefix + 'lib-common').projectDir = new File(rootDir, 'libraries/common') diff --git a/publish.gradle b/publish.gradle index 4b93f3806b..366a380acd 100644 --- a/publish.gradle +++ b/publish.gradle @@ -16,7 +16,7 @@ apply plugin: 'maven-publish' apply from: "$gradle.ext.androidxMediaSettingsDir/missing_aar_type_workaround.gradle" afterEvaluate { - if (rootProject.name == "media3") { + if (rootProject.name == gradle.ext.androidxMediaProjectName) { publishing { repositories { maven { diff --git a/settings.gradle b/settings.gradle index 716f405a9a..f2cd7a6480 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,7 +18,7 @@ if (gradle.ext.has('androidxMediaModulePrefix')) { modulePrefix += gradle.ext.androidxMediaModulePrefix } -rootProject.name = 'media3' +gradle.ext.androidxMediaProjectName = 'media3' include modulePrefix + 'demo' project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main') From 20ba32543920813c9cc2f5583e0bd3cf4674622e Mon Sep 17 00:00:00 2001 From: tonihei Date: Mon, 15 May 2023 14:19:03 +0100 Subject: [PATCH 20/24] Add consistency check to sending and receiving position updates The periodic updates are only meant to happen while we are in the same period or ad. This was already guaranteed except for two cases: 1. The Player in a session has updated its state without yet calling its listeners 2. The session scheduled a PlayerInfo update that hasn't been sent yet ... and in both cases, the following happened: - The change updated the mediaItemIndex to an index that didn't exist in a previous Timeline known to the Controller - One of the period position updates happened to be sent at exactly this time This problem can be avoided by only scheduling the update if we are still in the same period/ad and haven't scheduled a normal PlayerInfo update already. Since new MediaControllers may still connect to old sessons with this bug, we need an equivalent change on the controller side to ignore such buggy updates. PiperOrigin-RevId: 532089328 (cherry picked from commit 96dd0ae5837a5fd82d7407623bedb5fd4d1e9252) --- RELEASENOTES.md | 3 ++ .../session/MediaControllerImplBase.java | 7 +++ .../media3/session/MediaSessionImpl.java | 19 ++++++-- .../androidx/media3/session/MediaUtils.java | 13 ++++++ .../media3/session/MediaControllerTest.java | 46 +++++++++++++++++++ 5 files changed, 85 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8e07b6f110..fc6632f300 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,9 @@ ([#355](https://github.com/androidx/media/issues/355)). * Fix memory leak of `MediaSessionService` or `MediaLibraryService` ([#346](https://github.com/androidx/media/issues/346)). + * Fix bug where a combined `Timeline` and position update in a + `MediaSession` may cause a `MediaController` to throw an + `IllegalStateException`. ### 1.0.1 (2023-04-18) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java index 8f224cb1ee..7ad8ce6b7e 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaControllerImplBase.java @@ -2500,6 +2500,13 @@ import org.checkerframework.checker.nullness.qual.NonNull; private void updateSessionPositionInfoIfNeeded(SessionPositionInfo sessionPositionInfo) { if (pendingMaskingSequencedFutureNumbers.isEmpty() && playerInfo.sessionPositionInfo.eventTimeMs < sessionPositionInfo.eventTimeMs) { + if (!MediaUtils.areSessionPositionInfosInSamePeriodOrAd( + sessionPositionInfo, playerInfo.sessionPositionInfo)) { + // MediaSessionImpl before version 1.0.2 has a bug that may send position info updates for + // new periods too early. Ignore these updates to avoid an inconsistent state (see + // [internal b/277301159]). + return; + } playerInfo = playerInfo.copyWithSessionPositionInfo(sessionPositionInfo); } } 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 ee865ba11d..8cc3a06f3d 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaSessionImpl.java @@ -733,7 +733,16 @@ import org.checkerframework.checker.initialization.qual.Initialized; } } SessionPositionInfo sessionPositionInfo = playerWrapper.createSessionPositionInfoForBundling(); - dispatchOnPeriodicSessionPositionInfoChanged(sessionPositionInfo); + if (!onPlayerInfoChangedHandler.hasPendingPlayerInfoChangedUpdate() + && MediaUtils.areSessionPositionInfosInSamePeriodOrAd( + sessionPositionInfo, playerInfo.sessionPositionInfo)) { + // Send a periodic position info only if a PlayerInfo update is not already already pending + // and the player state is still corresponding to the currently known PlayerInfo. Both + // conditions will soon trigger a new PlayerInfo update with the latest position info anyway + // and we also don't want to send a new position info early if the corresponding Timeline + // update hasn't been sent yet (see [internal b/277301159]). + dispatchOnPeriodicSessionPositionInfoChanged(sessionPositionInfo); + } schedulePeriodicSessionPositionInfoChanges(); } @@ -1288,11 +1297,15 @@ import org.checkerframework.checker.initialization.qual.Initialized; } } + public boolean hasPendingPlayerInfoChangedUpdate() { + return hasMessages(MSG_PLAYER_INFO_CHANGED); + } + public void sendPlayerInfoChangedMessage(boolean excludeTimeline, boolean excludeTracks) { this.excludeTimeline = this.excludeTimeline && excludeTimeline; this.excludeTracks = this.excludeTracks && excludeTracks; - if (!onPlayerInfoChangedHandler.hasMessages(MSG_PLAYER_INFO_CHANGED)) { - onPlayerInfoChangedHandler.sendEmptyMessage(MSG_PLAYER_INFO_CHANGED); + if (!hasMessages(MSG_PLAYER_INFO_CHANGED)) { + sendEmptyMessage(MSG_PLAYER_INFO_CHANGED); } } } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java index 116faf7a2b..d8627d4151 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaUtils.java @@ -1460,6 +1460,19 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } + /** + * Returns whether the two provided {@link SessionPositionInfo} describe a position in the same + * period or ad. + */ + public static boolean areSessionPositionInfosInSamePeriodOrAd( + SessionPositionInfo info1, SessionPositionInfo info2) { + // TODO: b/259220235 - Use UIDs instead of mediaItemIndex and periodIndex + return info1.positionInfo.mediaItemIndex == info2.positionInfo.mediaItemIndex + && info1.positionInfo.periodIndex == info2.positionInfo.periodIndex + && info1.positionInfo.adGroupIndex == info2.positionInfo.adGroupIndex + && info1.positionInfo.adIndexInAdGroup == info2.positionInfo.adIndexInAdGroup; + } + private static byte[] convertToByteArray(Bitmap bitmap) throws IOException { try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) { bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index 4a4c01db4f..5405c0f6d7 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -34,6 +34,7 @@ import android.app.PendingIntent; import android.content.Context; import android.os.Bundle; import android.os.RemoteException; +import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; import androidx.media3.common.Format; @@ -1064,6 +1065,51 @@ public class MediaControllerTest { assertThat(bufferedPositionAfterDelay.get()).isNotEqualTo(testBufferedPosition); } + @Test + public void + getCurrentMediaItemIndex_withPeriodicUpdateOverlappingTimelineChanges_updatesIndexCorrectly() + throws Exception { + Bundle playerConfig = + new RemoteMediaSession.MockPlayerConfigBuilder() + .setPlayWhenReady(true) + .setPlaybackState(Player.STATE_READY) + .build(); + remoteSession.setPlayer(playerConfig); + MediaController controller = controllerTestRule.createController(remoteSession.getToken()); + ArrayList transitionMediaItemIndices = new ArrayList<>(); + controller.addListener( + new Player.Listener() { + @Override + public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { + transitionMediaItemIndices.add(controller.getCurrentMediaItemIndex()); + } + }); + + // Intentionally trigger update often to ensure there is a likely overlap with Timeline updates. + remoteSession.setSessionPositionUpdateDelayMs(1L); + // Trigger many timeline and position updates that are incompatible with any previous updates. + for (int i = 1; i <= 100; i++) { + remoteSession.getMockPlayer().createAndSetFakeTimeline(/* windowCount= */ i); + remoteSession.getMockPlayer().setCurrentMediaItemIndex(i - 1); + remoteSession + .getMockPlayer() + .notifyTimelineChanged(Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED); + remoteSession + .getMockPlayer() + .notifyMediaItemTransition( + /* index= */ i - 1, Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED); + } + PollingCheck.waitFor(TIMEOUT_MS, () -> transitionMediaItemIndices.size() == 100); + + ImmutableList.Builder expectedMediaItemIndices = ImmutableList.builder(); + for (int i = 0; i < 100; i++) { + expectedMediaItemIndices.add(i); + } + assertThat(transitionMediaItemIndices) + .containsExactlyElementsIn(expectedMediaItemIndices.build()) + .inOrder(); + } + @Test public void getContentBufferedPosition_byDefault_returnsZero() throws Exception { MediaController controller = controllerTestRule.createController(remoteSession.getToken()); From 6a7a3763f440228feef50fabe702a580f53f9cb2 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 16 May 2023 11:48:31 +0100 Subject: [PATCH 21/24] Update release notes for Media3 1.0.2 PiperOrigin-RevId: 532404001 (cherry picked from commit 1a38a0c41ebad512e9baab14d562f06de934177f) --- RELEASENOTES.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index fc6632f300..fc703b1471 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,12 @@ # Release notes -### Unreleased changes +### 1.0.2 (2023-05-18) + +This release corresponds to the +[ExoPlayer 2.18.7 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.7). + +This release contains the following changes since the +[1.0.1 release](#101-2023-04-18): * Core library: * Add `Buffer.isLastSample()` that denotes if `Buffer` contains flag From c48471109e99e687023a916687150c1bbb46357a Mon Sep 17 00:00:00 2001 From: Ian Baker Date: Wed, 17 May 2023 10:15:34 +0100 Subject: [PATCH 22/24] Update media3 version number to 1.0.2 --- constants.gradle | 4 ++-- .../main/java/androidx/media3/common/MediaLibraryInfo.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/constants.gradle b/constants.gradle index dac2a21c37..e159877c50 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.1' - releaseVersionCode = 1_000_001_3_00 + releaseVersion = '1.0.2' + releaseVersionCode = 1_000_002_3_00 minSdkVersion = 16 appTargetSdkVersion = 33 // API version before restricting local file access. 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 3620406bfb..ed68279a77 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.1"; + public static final String VERSION = "1.0.2"; /** 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.1"; + public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.2"; /** * 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_001_3_00; + public static final int VERSION_INT = 1_000_002_3_00; /** Whether the library was compiled with {@link Assertions} checks enabled. */ public static final boolean ASSERTIONS_ENABLED = true; From 69879cd57f1caffe8d067d504a568ae2c6b0f1a1 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 17 May 2023 14:15:48 +0100 Subject: [PATCH 23/24] Add Media3 1.0.2 and ExoPlayer 2.18.7 to `bug.yml` template PiperOrigin-RevId: 532765549 (cherry picked from commit 4ede3d600718969e6c62e855fcdf73805352a323) --- .github/ISSUE_TEMPLATE/bug.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index e6dde5ad63..fd4047fc7e 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -20,6 +20,8 @@ body: label: Media3 Version description: What version of Media3 (or ExoPlayer) are you using? options: + - Media3 1.1.0-alpha01 + - Media3 1.0.2 - Media3 1.0.1 - Media3 1.0.0 - Media3 1.0.0-rc02 @@ -30,6 +32,7 @@ body: - Media3 1.0.0-alpha03 - Media3 1.0.0-alpha02 - Media3 1.0.0-alpha01 + - ExoPlayer 2.18.7 - ExoPlayer 2.18.6 - ExoPlayer 2.18.5 - ExoPlayer 2.18.4 From d77e79af49c0e766de9dd06128724bfdb2f7085f Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 17 May 2023 14:21:21 +0100 Subject: [PATCH 24/24] Add `main`/`dev-v2` branch options to `bug.yml` template #minor-release PiperOrigin-RevId: 532766676 (cherry picked from commit 84d0206c767526802aa3798dad7742ea807d5bc6) --- .github/ISSUE_TEMPLATE/bug.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index fd4047fc7e..c32439333a 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -32,6 +32,7 @@ body: - Media3 1.0.0-alpha03 - Media3 1.0.0-alpha02 - Media3 1.0.0-alpha01 + - Media3 `main` branch - ExoPlayer 2.18.7 - ExoPlayer 2.18.6 - ExoPlayer 2.18.5 @@ -49,6 +50,7 @@ body: - ExoPlayer 2.14.2 - ExoPlayer 2.14.1 - ExoPlayer 2.14.0 + - ExoPlayer `dev-v2` branch - Older (unsupported) validations: required: true