diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index e6dde5ad63..c32439333a 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,8 @@ 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 - ExoPlayer 2.18.4 @@ -46,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 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index acce6295ca..fc703b1471 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,40 @@ # Release notes +### 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 + `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)). +* 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)). +* 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 + 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)). + * 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) This release corresponds to the 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/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/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); 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..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.* @@ -29,6 +30,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 @@ -95,7 +97,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) } } @@ -142,6 +144,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)) } @@ -270,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) 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 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 { 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; 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/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); + } +} 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/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/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/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/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/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/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/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/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/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/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/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/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/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(); 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/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/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 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(); } } 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/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 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..d8627d4151 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,35 @@ 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); + } + } + + /** + * 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); + return stream.toByteArray(); } } 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/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); + } } 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/ssa/typical_utf16be b/libraries/test_data/src/test/assets/media/ssa/typical_utf16be new file mode 100644 index 0000000000..6b11ad0ed5 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/ssa/typical_utf16be differ 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 0000000000..da098604d0 Binary files /dev/null and b/libraries/test_data/src/test/assets/media/ssa/typical_utf16le differ 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 0000000000..b5d336564d Binary files /dev/null and b/libraries/test_data/src/test/assets/media/ts/sample_h265_rps_pred.ts differ 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 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()); 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; 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/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(); 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); } /** 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 โมโน 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')