Merge pull request #408 from androidx/release-1.0.2

1.0.2
This commit is contained in:
Ian Baker 2023-05-17 17:26:12 +01:00 committed by GitHub
commit 2fc189d6a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1300 additions and 378 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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')

View File

@ -514,6 +514,7 @@ public class PlayerActivity extends AppCompatActivity
private class PlayerErrorMessageProvider implements ErrorMessageProvider<PlaybackException> {
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
@Override
public Pair<Integer, String> getErrorMessage(PlaybackException e) {
String errorString = getString(R.string.error_generic);

View File

@ -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);

View File

@ -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<LibraryResult<MediaItem>> {
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)

View File

@ -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

View File

@ -23,7 +23,7 @@ rootProject.allprojects.forEach {
evaluationDependsOn(':' + it.name)
}
}
// copybara:media3-only
android {
buildTypes {
debug {

View File

@ -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;

View File

@ -233,11 +233,28 @@ public final class ParsableByteArray {
return (data[position] & 0xFF);
}
/** Peeks at the next char. */
/**
* Peeks at the next char.
*
* <p>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;
int characterAndSize = peekCharacterAndSize(charset);
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.
*
* <p>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 = Chars.checkedCast(UnsignedBytes.toInt(data[position]));
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 = Chars.fromBytes(data[position], data[position + 1]);
character = (byte) 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]);
character = (byte) Chars.fromBytes(data[position + 1], data[position]);
characterSize = 2;
} else {
return 0;
}
if (Chars.contains(chars, character)) {
position += characterSize;
return Chars.checkedCast(character);
} else {
return 0;
}
return (Chars.checkedCast(character) << Short.SIZE) + characterSize;
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;
}

View File

@ -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();
}

View File

@ -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);

View File

@ -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(

View File

@ -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",

View File

@ -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);

View File

@ -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<DecoderCounters> 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();
}
}
}

View File

@ -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 {

View File

@ -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(

View File

@ -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();

View File

@ -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<Long> 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<String, SsaStyle> parseStyles(ParsableByteArray data) {
private static Map<String, SsaStyle> parseStyles(ParsableByteArray data, Charset charset) {
Map<String, SsaStyle> 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<List<Cue>> cues, List<Long> cueTimesUs) {
private void parseEventBody(
ParsableByteArray data, List<List<Cue>> cues, List<Long> 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)) {

View File

@ -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);

View File

@ -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);
}

View File

@ -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(

View File

@ -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<ControllerInfo, ConnectedControllerRecord<T>> controllerRecords =
new ArrayMap<>();
private final MediaSessionImpl sessionImpl;
private final WeakReference<MediaSessionImpl> 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<T> info) {
@Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
if (sessionImpl == null) {
return;
}
AtomicBoolean continueRunning = new AtomicBoolean(true);
while (continueRunning.get()) {
continueRunning.set(false);

View File

@ -388,7 +388,7 @@ public final class LibraryResult<V> 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

View File

@ -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.
*
* <p>Must be called on the {@linkplain #getApplicationLooper() application thread} of the media
* controller.
*/
public static void releaseFuture(Future<? extends MediaController> 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();

View File

@ -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);
}
}

View File

@ -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(
MediaUtils.convertToPlayerCommands(
newLegacyPlayerInfo.playbackStateCompat,
volumeControlType,
sessionFlags,
isSessionReady)
: oldControllerInfo.availablePlayerCommands;
isSessionReady);
PlaybackException playerError =
MediaUtils.convertToPlaybackException(newLegacyPlayerInfo.playbackStateCompat);

View File

@ -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

View File

@ -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}.
*
* <p>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<MediaItemsWithStartPosition> 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<MediaItem> mediaItems;
/**
* Index to start playing at in {@link MediaItem} list.
* Index to start playing at in {@link #mediaItems}.
*
* <p>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.
* <p>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.
*
* <p>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<MediaItem> 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<T, U, C>, C extends Callback> {
SessionT extends MediaSession,
BuilderT extends BuilderBase<SessionT, BuilderT, CallbackT>,
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();
}
}

View File

@ -733,7 +733,16 @@ import org.checkerframework.checker.initialization.qual.Initialized;
}
}
SessionPositionInfo sessionPositionInfo = playerWrapper.createSessionPositionInfoForBundling();
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);
}
}
}

View File

@ -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<RemoteUserInfo> connectedControllersManager;
public ConnectionTimeoutHandler(
Looper looper, ConnectedControllersManager<RemoteUserInfo> connectedControllersManager) {
super(looper);
this.connectedControllersManager = connectedControllersManager;
}
@Override

View File

@ -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,8 +1448,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
player.setMediaItem(
mediaItemsWithStartPosition.mediaItems.get(0), /* resetPosition= */ true);
}
} else {
if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
} else if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
player.setMediaItems(
mediaItemsWithStartPosition.mediaItems,
mediaItemsWithStartPosition.startIndex,
@ -1455,6 +1459,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
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();
}
}
private MediaUtils() {}

View File

@ -5,7 +5,7 @@
<string name="media3_controls_pause_description">Pausar</string>
<string name="media3_controls_seek_to_previous_description">Retroceder para o item anterior</string>
<string name="media3_controls_seek_to_next_description">Avançar para o item seguinte</string>
<string name="media3_controls_seek_back_description">Retroceder</string>
<string name="media3_controls_seek_back_description">Anterior</string>
<string name="media3_controls_seek_forward_description">Avançar</string>
<string name="authentication_required">Autenticação necessária</string>
</resources>

View File

@ -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<MediaItem> 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<ImmutableList<MediaItem>> 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<ImmutableList<Error>> 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<ImmutableList<Error>> 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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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<Integer> 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<Integer> 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());

View File

@ -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<Player.Commands> commandsFromParamRef = new AtomicReference<>();
AtomicReference<Player.Commands> 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;

View File

@ -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) {

View File

@ -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();

View File

@ -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);
}
/**

View File

@ -6,7 +6,7 @@
<string name="exo_controls_settings_description">ቅንብሮች</string>
<string name="exo_controls_overflow_hide_description">ተጨማሪ ቅንብሮችን ይደብቁ</string>
<string name="exo_controls_overflow_show_description">ተጨማሪ ቅንብሮችን ያሳዩ</string>
<string name="exo_controls_fullscreen_enter_description">ወደ ሙሉ ማያ ገ ግባ</string>
<string name="exo_controls_fullscreen_enter_description">ወደ ሙሉ ማያ ገ ግባ</string>
<string name="exo_controls_fullscreen_exit_description">ከሙሉ ማያገጽ ውጣ</string>
<string name="exo_controls_previous_description">ቀዳሚ</string>
<string name="exo_controls_next_description">ቀጣይ</string>

View File

@ -4,8 +4,8 @@
<string name="exo_controls_hide">Ойноткучту башкаруу элементтерин жашыруу</string>
<string name="exo_controls_seek_bar_description">Ойнотуу көрсөткүчү</string>
<string name="exo_controls_settings_description">Параметрлер</string>
<string name="exo_controls_overflow_hide_description">Кошумча жөндөөлөрдү жашыруу</string>
<string name="exo_controls_overflow_show_description">Кошумча жөндөөлөрдү көрсөтүү</string>
<string name="exo_controls_overflow_hide_description">Кошумча параметрлерди жашыруу</string>
<string name="exo_controls_overflow_show_description">Кошумча параметрлерди көрсөтүү</string>
<string name="exo_controls_fullscreen_enter_description">Толук экранга кирүү</string>
<string name="exo_controls_fullscreen_exit_description">Толук экран режиминен чыгуу</string>
<string name="exo_controls_previous_description">Мурунку</string>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="exo_controls_show">Прикажи ги контролите на плеерот</string>
<string name="exo_controls_hide">Сокриј ги контролите на плеерот</string>
<string name="exo_controls_hide">Скриј ги контролите на плеерот</string>
<string name="exo_controls_seek_bar_description">Напредок на репродукцијата</string>
<string name="exo_controls_settings_description">Поставки</string>
<string name="exo_controls_overflow_hide_description">Сокријте ги дополнителните поставки</string>

View File

@ -45,7 +45,7 @@
<string name="exo_track_selection_title_audio">เสียง</string>
<string name="exo_track_selection_title_text">ข้อความ</string>
<string name="exo_track_selection_none">ไม่มี</string>
<string name="exo_track_selection_auto">ยานยนต์</string>
<string name="exo_track_selection_auto">อัตโนมัติ</string>
<string name="exo_track_unknown">ไม่ทราบ</string>
<string name="exo_track_resolution">%1$d × %2$d</string>
<string name="exo_track_mono">โมโน</string>

View File

@ -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 {

View File

@ -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')