mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
commit
2fc189d6a4
5
.github/ISSUE_TEMPLATE/bug.yml
vendored
5
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -20,6 +20,8 @@ body:
|
|||||||
label: Media3 Version
|
label: Media3 Version
|
||||||
description: What version of Media3 (or ExoPlayer) are you using?
|
description: What version of Media3 (or ExoPlayer) are you using?
|
||||||
options:
|
options:
|
||||||
|
- Media3 1.1.0-alpha01
|
||||||
|
- Media3 1.0.2
|
||||||
- Media3 1.0.1
|
- Media3 1.0.1
|
||||||
- Media3 1.0.0
|
- Media3 1.0.0
|
||||||
- Media3 1.0.0-rc02
|
- Media3 1.0.0-rc02
|
||||||
@ -30,6 +32,8 @@ body:
|
|||||||
- Media3 1.0.0-alpha03
|
- Media3 1.0.0-alpha03
|
||||||
- Media3 1.0.0-alpha02
|
- Media3 1.0.0-alpha02
|
||||||
- Media3 1.0.0-alpha01
|
- Media3 1.0.0-alpha01
|
||||||
|
- Media3 `main` branch
|
||||||
|
- ExoPlayer 2.18.7
|
||||||
- ExoPlayer 2.18.6
|
- ExoPlayer 2.18.6
|
||||||
- ExoPlayer 2.18.5
|
- ExoPlayer 2.18.5
|
||||||
- ExoPlayer 2.18.4
|
- ExoPlayer 2.18.4
|
||||||
@ -46,6 +50,7 @@ body:
|
|||||||
- ExoPlayer 2.14.2
|
- ExoPlayer 2.14.2
|
||||||
- ExoPlayer 2.14.1
|
- ExoPlayer 2.14.1
|
||||||
- ExoPlayer 2.14.0
|
- ExoPlayer 2.14.0
|
||||||
|
- ExoPlayer `dev-v2` branch
|
||||||
- Older (unsupported)
|
- Older (unsupported)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
@ -1,5 +1,40 @@
|
|||||||
# Release notes
|
# 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)
|
### 1.0.1 (2023-04-18)
|
||||||
|
|
||||||
This release corresponds to the
|
This release corresponds to the
|
||||||
|
@ -12,8 +12,8 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
project.ext {
|
project.ext {
|
||||||
releaseVersion = '1.0.1'
|
releaseVersion = '1.0.2'
|
||||||
releaseVersionCode = 1_000_001_3_00
|
releaseVersionCode = 1_000_002_3_00
|
||||||
minSdkVersion = 16
|
minSdkVersion = 16
|
||||||
appTargetSdkVersion = 33
|
appTargetSdkVersion = 33
|
||||||
// API version before restricting local file access.
|
// API version before restricting local file access.
|
||||||
|
@ -21,6 +21,8 @@ if (gradle.ext.has('androidxMediaModulePrefix')) {
|
|||||||
modulePrefix += gradle.ext.androidxMediaModulePrefix
|
modulePrefix += gradle.ext.androidxMediaModulePrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootProject.name = gradle.ext.androidxMediaProjectName
|
||||||
|
|
||||||
include modulePrefix + 'lib-common'
|
include modulePrefix + 'lib-common'
|
||||||
project(modulePrefix + 'lib-common').projectDir = new File(rootDir, 'libraries/common')
|
project(modulePrefix + 'lib-common').projectDir = new File(rootDir, 'libraries/common')
|
||||||
|
|
||||||
|
@ -514,6 +514,7 @@ public class PlayerActivity extends AppCompatActivity
|
|||||||
|
|
||||||
private class PlayerErrorMessageProvider implements ErrorMessageProvider<PlaybackException> {
|
private class PlayerErrorMessageProvider implements ErrorMessageProvider<PlaybackException> {
|
||||||
|
|
||||||
|
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
|
||||||
@Override
|
@Override
|
||||||
public Pair<Integer, String> getErrorMessage(PlaybackException e) {
|
public Pair<Integer, String> getErrorMessage(PlaybackException e) {
|
||||||
String errorString = getString(R.string.error_generic);
|
String errorString = getString(R.string.error_generic);
|
||||||
|
@ -27,6 +27,7 @@ import android.content.pm.PackageManager;
|
|||||||
import android.content.res.AssetManager;
|
import android.content.res.AssetManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.JsonReader;
|
import android.util.JsonReader;
|
||||||
@ -273,7 +274,7 @@ public class SampleChooserActivity extends AppCompatActivity
|
|||||||
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
|
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
|
||||||
.show();
|
.show();
|
||||||
} else if (!notificationPermissionToastShown
|
} else if (!notificationPermissionToastShown
|
||||||
&& Util.SDK_INT >= 33
|
&& Build.VERSION.SDK_INT >= 33
|
||||||
&& checkSelfPermission(Api33.getPostNotificationPermissionString())
|
&& checkSelfPermission(Api33.getPostNotificationPermissionString())
|
||||||
!= PackageManager.PERMISSION_GRANTED) {
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
downloadMediaItemWaitingForNotificationPermission = playlistHolder.mediaItems.get(0);
|
downloadMediaItemWaitingForNotificationPermission = playlistHolder.mediaItems.get(0);
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.demo.session
|
package androidx.media3.demo.session
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent.*
|
import android.app.PendingIntent.*
|
||||||
@ -29,6 +30,7 @@ import androidx.media3.common.MediaItem
|
|||||||
import androidx.media3.common.util.Util
|
import androidx.media3.common.util.Util
|
||||||
import androidx.media3.exoplayer.ExoPlayer
|
import androidx.media3.exoplayer.ExoPlayer
|
||||||
import androidx.media3.session.*
|
import androidx.media3.session.*
|
||||||
|
import androidx.media3.session.LibraryResult.RESULT_ERROR_NOT_SUPPORTED
|
||||||
import androidx.media3.session.MediaSession.ControllerInfo
|
import androidx.media3.session.MediaSession.ControllerInfo
|
||||||
import com.google.common.collect.ImmutableList
|
import com.google.common.collect.ImmutableList
|
||||||
import com.google.common.util.concurrent.Futures
|
import com.google.common.util.concurrent.Futures
|
||||||
@ -95,7 +97,7 @@ class PlaybackService : MediaLibraryService() {
|
|||||||
): MediaSession.ConnectionResult {
|
): MediaSession.ConnectionResult {
|
||||||
val connectionResult = super.onConnect(session, controller)
|
val connectionResult = super.onConnect(session, controller)
|
||||||
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||||
customCommands.forEach { commandButton ->
|
for (commandButton in customCommands) {
|
||||||
// Add custom command to available session commands.
|
// Add custom command to available session commands.
|
||||||
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
|
||||||
}
|
}
|
||||||
@ -142,6 +144,12 @@ class PlaybackService : MediaLibraryService() {
|
|||||||
browser: ControllerInfo,
|
browser: ControllerInfo,
|
||||||
params: LibraryParams?
|
params: LibraryParams?
|
||||||
): ListenableFuture<LibraryResult<MediaItem>> {
|
): 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))
|
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
|
* by a media controller to resume playback when the {@link MediaSessionService} is in the
|
||||||
* background.
|
* background.
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("MissingPermission") // TODO: b/280766358 - Request this permission at runtime.
|
||||||
override fun onForegroundServiceStartNotAllowedException() {
|
override fun onForegroundServiceStartNotAllowedException() {
|
||||||
val notificationManagerCompat = NotificationManagerCompat.from(this@PlaybackService)
|
val notificationManagerCompat = NotificationManagerCompat.from(this@PlaybackService)
|
||||||
ensureNotificationChannel(notificationManagerCompat)
|
ensureNotificationChannel(notificationManagerCompat)
|
||||||
|
@ -88,16 +88,6 @@ class PlayerActivity : AppCompatActivity() {
|
|||||||
initializeController()
|
initializeController()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
playerView.onResume()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
playerView.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
playerView.player = null
|
playerView.player = null
|
||||||
|
@ -23,7 +23,7 @@ rootProject.allprojects.forEach {
|
|||||||
evaluationDependsOn(':' + it.name)
|
evaluationDependsOn(':' + it.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// copybara:media3-only
|
|
||||||
android {
|
android {
|
||||||
buildTypes {
|
buildTypes {
|
||||||
debug {
|
debug {
|
||||||
|
@ -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". */
|
/** 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.
|
// 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}. */
|
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// 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.
|
* 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).
|
* (123-045-006-3-00).
|
||||||
*/
|
*/
|
||||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
// 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. */
|
/** Whether the library was compiled with {@link Assertions} checks enabled. */
|
||||||
public static final boolean ASSERTIONS_ENABLED = true;
|
public static final boolean ASSERTIONS_ENABLED = true;
|
||||||
|
@ -233,11 +233,28 @@ public final class ParsableByteArray {
|
|||||||
return (data[position] & 0xFF);
|
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() {
|
public char peekChar() {
|
||||||
return (char) ((data[position] & 0xFF) << 8 | (data[position + 1] & 0xFF));
|
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. */
|
/** Reads the next byte as an unsigned value. */
|
||||||
public int readUnsignedByte() {
|
public int readUnsignedByte() {
|
||||||
return (data[position++] & 0xFF);
|
return (data[position++] & 0xFF);
|
||||||
@ -649,27 +666,42 @@ public final class ParsableByteArray {
|
|||||||
* UTF-8 and two bytes for UTF-16).
|
* UTF-8 and two bytes for UTF-16).
|
||||||
*/
|
*/
|
||||||
private char readCharacterIfInList(Charset charset, char[] chars) {
|
private char readCharacterIfInList(Charset charset, char[] chars) {
|
||||||
char character;
|
int characterAndSize = peekCharacterAndSize(charset);
|
||||||
int characterSize;
|
|
||||||
|
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) {
|
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;
|
characterSize = 1;
|
||||||
} else if ((charset.equals(Charsets.UTF_16) || charset.equals(Charsets.UTF_16BE))
|
} else if ((charset.equals(Charsets.UTF_16) || charset.equals(Charsets.UTF_16BE))
|
||||||
&& bytesLeft() >= 2) {
|
&& bytesLeft() >= 2) {
|
||||||
character = Chars.fromBytes(data[position], data[position + 1]);
|
character = (byte) Chars.fromBytes(data[position], data[position + 1]);
|
||||||
characterSize = 2;
|
characterSize = 2;
|
||||||
} else if (charset.equals(Charsets.UTF_16LE) && bytesLeft() >= 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;
|
characterSize = 2;
|
||||||
} else {
|
} else {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
return (Chars.checkedCast(character) << Short.SIZE) + characterSize;
|
||||||
if (Chars.contains(chars, character)) {
|
|
||||||
position += characterSize;
|
|
||||||
return Chars.checkedCast(character);
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -49,6 +49,11 @@ public abstract class Buffer {
|
|||||||
return getFlag(C.BUFFER_FLAG_KEY_FRAME);
|
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. */
|
/** Returns whether the {@link C#BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA} flag is set. */
|
||||||
public final boolean hasSupplementalData() {
|
public final boolean hasSupplementalData() {
|
||||||
return getFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA);
|
return getFlag(C.BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA);
|
||||||
|
@ -1244,7 +1244,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasReadStreamToEnd()) {
|
if (hasReadStreamToEnd() || buffer.isLastSample()) {
|
||||||
// Notify output queue of the last buffer's timestamp.
|
// Notify output queue of the last buffer's timestamp.
|
||||||
lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs;
|
lastBufferInStreamPresentationTimeUs = largestQueuedPresentationTimeUs;
|
||||||
}
|
}
|
||||||
|
@ -278,9 +278,9 @@ public final class ProgressiveMediaSource extends BaseMediaSource
|
|||||||
@Override
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
transferListener = mediaTransferListener;
|
transferListener = mediaTransferListener;
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(
|
drmSessionManager.setPlayer(
|
||||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId());
|
/* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId());
|
||||||
|
drmSessionManager.prepare();
|
||||||
notifySourceInfoRefreshed();
|
notifySourceInfoRefreshed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -716,6 +716,9 @@ public class SampleQueue implements TrackOutput {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buffer.setFlags(flags[relativeReadIndex]);
|
buffer.setFlags(flags[relativeReadIndex]);
|
||||||
|
if (readPosition == (length - 1) && (loadingFinished || isLastSampleQueued)) {
|
||||||
|
buffer.addFlag(C.BUFFER_FLAG_LAST_SAMPLE);
|
||||||
|
}
|
||||||
buffer.timeUs = timesUs[relativeReadIndex];
|
buffer.timeUs = timesUs[relativeReadIndex];
|
||||||
if (buffer.timeUs < startTimeUs) {
|
if (buffer.timeUs < startTimeUs) {
|
||||||
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
buffer.addFlag(C.BUFFER_FLAG_DECODE_ONLY);
|
||||||
|
@ -68,8 +68,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
new DefaultDrmSessionManager.Builder()
|
new DefaultDrmSessionManager.Builder()
|
||||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
|
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -91,8 +91,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(10_000)
|
.setSessionKeepaliveMs(10_000)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -116,8 +116,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -138,8 +138,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(10_000)
|
.setSessionKeepaliveMs(10_000)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -162,8 +162,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -188,8 +188,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(10_000)
|
.setSessionKeepaliveMs(10_000)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -233,8 +233,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(10_000)
|
.setSessionKeepaliveMs(10_000)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -272,8 +272,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setMultiSession(true)
|
.setMultiSession(true)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession firstDrmSession =
|
DrmSession firstDrmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -313,8 +313,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setMultiSession(true)
|
.setMultiSession(true)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSessionReference firstDrmSessionReference =
|
DrmSessionReference firstDrmSessionReference =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.preacquireSession(
|
drmSessionManager.preacquireSession(
|
||||||
@ -358,8 +358,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(10_000)
|
.setSessionKeepaliveMs(10_000)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession firstDrmSession =
|
DrmSession firstDrmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -405,8 +405,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
|
|
||||||
DrmSessionReference sessionReference =
|
DrmSessionReference sessionReference =
|
||||||
drmSessionManager.preacquireSession(eventDispatcher, FORMAT_WITH_DRM_INIT_DATA);
|
drmSessionManager.preacquireSession(eventDispatcher, FORMAT_WITH_DRM_INIT_DATA);
|
||||||
@ -450,8 +450,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
|
|
||||||
DrmSessionReference sessionReference =
|
DrmSessionReference sessionReference =
|
||||||
drmSessionManager.preacquireSession(/* eventDispatcher= */ null, FORMAT_WITH_DRM_INIT_DATA);
|
drmSessionManager.preacquireSession(/* eventDispatcher= */ null, FORMAT_WITH_DRM_INIT_DATA);
|
||||||
@ -486,8 +486,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
|
|
||||||
DrmSessionReference sessionReference =
|
DrmSessionReference sessionReference =
|
||||||
drmSessionManager.preacquireSession(/* eventDispatcher= */ null, FORMAT_WITH_DRM_INIT_DATA);
|
drmSessionManager.preacquireSession(/* eventDispatcher= */ null, FORMAT_WITH_DRM_INIT_DATA);
|
||||||
@ -530,8 +530,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm))
|
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm))
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
|
|
||||||
DefaultDrmSession drmSession =
|
DefaultDrmSession drmSession =
|
||||||
(DefaultDrmSession)
|
(DefaultDrmSession)
|
||||||
@ -571,8 +571,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm))
|
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(exoMediaDrm))
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
|
|
||||||
DefaultDrmSession drmSession =
|
DefaultDrmSession drmSession =
|
||||||
(DefaultDrmSession)
|
(DefaultDrmSession)
|
||||||
@ -615,8 +615,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
DRM_SCHEME_UUID,
|
DRM_SCHEME_UUID,
|
||||||
uuid -> new FakeExoMediaDrm.Builder().setProvisionsRequired(1).build())
|
uuid -> new FakeExoMediaDrm.Builder().setProvisionsRequired(1).build())
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -648,8 +648,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.throwNotProvisionedExceptionFromGetKeyRequest()
|
.throwNotProvisionedExceptionFromGetKeyRequest()
|
||||||
.build())
|
.build())
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -674,8 +674,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
DRM_SCHEME_UUID,
|
DRM_SCHEME_UUID,
|
||||||
uuid -> new FakeExoMediaDrm.Builder().setProvisionsRequired(2).build())
|
uuid -> new FakeExoMediaDrm.Builder().setProvisionsRequired(2).build())
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -702,8 +702,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setUuidAndExoMediaDrmProvider(
|
.setUuidAndExoMediaDrmProvider(
|
||||||
DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm.Builder().build())
|
DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm.Builder().build())
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -728,8 +728,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(mediaDrm))
|
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, new AppManagedProvider(mediaDrm))
|
||||||
.setSessionKeepaliveMs(C.TIME_UNSET)
|
.setSessionKeepaliveMs(C.TIME_UNSET)
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
@ -783,8 +783,8 @@ public class DefaultDrmSessionManagerTest {
|
|||||||
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
|
.setUuidAndExoMediaDrmProvider(DRM_SCHEME_UUID, uuid -> new FakeExoMediaDrm())
|
||||||
.build(/* mediaDrmCallback= */ licenseServer);
|
.build(/* mediaDrmCallback= */ licenseServer);
|
||||||
|
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), PlayerId.UNSET);
|
||||||
|
drmSessionManager.prepare();
|
||||||
DrmSession drmSession =
|
DrmSession drmSession =
|
||||||
checkNotNull(
|
checkNotNull(
|
||||||
drmSessionManager.acquireSession(
|
drmSessionManager.acquireSession(
|
||||||
|
@ -63,6 +63,7 @@ public class TsPlaybackTest {
|
|||||||
"sample_h264_mpeg_audio.ts",
|
"sample_h264_mpeg_audio.ts",
|
||||||
"sample_h264_no_access_unit_delimiters.ts",
|
"sample_h264_no_access_unit_delimiters.ts",
|
||||||
"sample_h265.ts",
|
"sample_h265.ts",
|
||||||
|
"sample_h265_rps_pred.ts",
|
||||||
"sample_latm.ts",
|
"sample_latm.ts",
|
||||||
"sample_scte35.ts",
|
"sample_scte35.ts",
|
||||||
"sample_with_id3.adts",
|
"sample_with_id3.adts",
|
||||||
|
@ -354,6 +354,32 @@ public final class SampleQueueTest {
|
|||||||
assertAllocationCount(0);
|
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
|
@Test
|
||||||
public void readMultiSamples() {
|
public void readMultiSamples() {
|
||||||
writeTestData();
|
writeTestData();
|
||||||
@ -1642,13 +1668,27 @@ public final class SampleQueueTest {
|
|||||||
FLAG_OMIT_SAMPLE_DATA | FLAG_PEEK,
|
FLAG_OMIT_SAMPLE_DATA | FLAG_PEEK,
|
||||||
/* loadingFinished= */ false);
|
/* loadingFinished= */ false);
|
||||||
assertSampleBufferReadResult(
|
assertSampleBufferReadResult(
|
||||||
flagsOnlyBuffer, result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted);
|
flagsOnlyBuffer,
|
||||||
|
result,
|
||||||
|
timeUs,
|
||||||
|
isKeyFrame,
|
||||||
|
isDecodeOnly,
|
||||||
|
isEncrypted,
|
||||||
|
/* isLastSample= */ false);
|
||||||
|
|
||||||
// Check that peek yields the expected values.
|
// Check that peek yields the expected values.
|
||||||
clearFormatHolderAndInputBuffer();
|
clearFormatHolderAndInputBuffer();
|
||||||
result = sampleQueue.read(formatHolder, inputBuffer, FLAG_PEEK, /* loadingFinished= */ false);
|
result = sampleQueue.read(formatHolder, inputBuffer, FLAG_PEEK, /* loadingFinished= */ false);
|
||||||
assertSampleBufferReadResult(
|
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.
|
// Check that read yields the expected values.
|
||||||
clearFormatHolderAndInputBuffer();
|
clearFormatHolderAndInputBuffer();
|
||||||
@ -1656,7 +1696,85 @@ public final class SampleQueueTest {
|
|||||||
sampleQueue.read(
|
sampleQueue.read(
|
||||||
formatHolder, inputBuffer, /* readFlags= */ 0, /* loadingFinished= */ false);
|
formatHolder, inputBuffer, /* readFlags= */ 0, /* loadingFinished= */ false);
|
||||||
assertSampleBufferReadResult(
|
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(
|
private void assertSampleBufferReadResult(
|
||||||
@ -1665,7 +1783,8 @@ public final class SampleQueueTest {
|
|||||||
long timeUs,
|
long timeUs,
|
||||||
boolean isKeyFrame,
|
boolean isKeyFrame,
|
||||||
boolean isDecodeOnly,
|
boolean isDecodeOnly,
|
||||||
boolean isEncrypted) {
|
boolean isEncrypted,
|
||||||
|
boolean isLastSample) {
|
||||||
assertThat(result).isEqualTo(RESULT_BUFFER_READ);
|
assertThat(result).isEqualTo(RESULT_BUFFER_READ);
|
||||||
// formatHolder should not be populated.
|
// formatHolder should not be populated.
|
||||||
assertThat(formatHolder.format).isNull();
|
assertThat(formatHolder.format).isNull();
|
||||||
@ -1674,6 +1793,7 @@ public final class SampleQueueTest {
|
|||||||
assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyFrame);
|
assertThat(inputBuffer.isKeyFrame()).isEqualTo(isKeyFrame);
|
||||||
assertThat(inputBuffer.isDecodeOnly()).isEqualTo(isDecodeOnly);
|
assertThat(inputBuffer.isDecodeOnly()).isEqualTo(isDecodeOnly);
|
||||||
assertThat(inputBuffer.isEncrypted()).isEqualTo(isEncrypted);
|
assertThat(inputBuffer.isEncrypted()).isEqualTo(isEncrypted);
|
||||||
|
assertThat(inputBuffer.isLastSample()).isEqualTo(isLastSample);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertSampleBufferReadResult(
|
private void assertSampleBufferReadResult(
|
||||||
@ -1682,11 +1802,12 @@ public final class SampleQueueTest {
|
|||||||
boolean isKeyFrame,
|
boolean isKeyFrame,
|
||||||
boolean isDecodeOnly,
|
boolean isDecodeOnly,
|
||||||
boolean isEncrypted,
|
boolean isEncrypted,
|
||||||
|
boolean isLastSample,
|
||||||
byte[] sampleData,
|
byte[] sampleData,
|
||||||
int offset,
|
int offset,
|
||||||
int length) {
|
int length) {
|
||||||
assertSampleBufferReadResult(
|
assertSampleBufferReadResult(
|
||||||
inputBuffer, result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted);
|
inputBuffer, result, timeUs, isKeyFrame, isDecodeOnly, isEncrypted, isLastSample);
|
||||||
// inputBuffer should be populated with data.
|
// inputBuffer should be populated with data.
|
||||||
inputBuffer.flip();
|
inputBuffer.flip();
|
||||||
assertThat(inputBuffer.data.limit()).isEqualTo(length);
|
assertThat(inputBuffer.data.limit()).isEqualTo(length);
|
||||||
|
@ -31,11 +31,14 @@ import static org.robolectric.Shadows.shadowOf;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.SurfaceTexture;
|
import android.graphics.SurfaceTexture;
|
||||||
import android.hardware.display.DisplayManager;
|
import android.hardware.display.DisplayManager;
|
||||||
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||||
import android.media.MediaCodecInfo.CodecProfileLevel;
|
import android.media.MediaCodecInfo.CodecProfileLevel;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.os.PersistableBundle;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
@ -44,6 +47,8 @@ import androidx.media3.common.C;
|
|||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.VideoSize;
|
import androidx.media3.common.VideoSize;
|
||||||
|
import androidx.media3.decoder.CryptoInfo;
|
||||||
|
import androidx.media3.exoplayer.DecoderCounters;
|
||||||
import androidx.media3.exoplayer.Renderer;
|
import androidx.media3.exoplayer.Renderer;
|
||||||
import androidx.media3.exoplayer.RendererCapabilities;
|
import androidx.media3.exoplayer.RendererCapabilities;
|
||||||
import androidx.media3.exoplayer.RendererCapabilities.Capabilities;
|
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.analytics.PlayerId;
|
||||||
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
|
||||||
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
import androidx.media3.exoplayer.drm.DrmSessionManager;
|
||||||
|
import androidx.media3.exoplayer.mediacodec.MediaCodecAdapter;
|
||||||
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
|
import androidx.media3.exoplayer.mediacodec.MediaCodecInfo;
|
||||||
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
|
||||||
|
import androidx.media3.exoplayer.mediacodec.SynchronousMediaCodecAdapter;
|
||||||
import androidx.media3.exoplayer.upstream.DefaultAllocator;
|
import androidx.media3.exoplayer.upstream.DefaultAllocator;
|
||||||
import androidx.media3.test.utils.FakeSampleStream;
|
import androidx.media3.test.utils.FakeSampleStream;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -117,6 +126,7 @@ public class MediaCodecVideoRendererTest {
|
|||||||
private Looper testMainLooper;
|
private Looper testMainLooper;
|
||||||
private Surface surface;
|
private Surface surface;
|
||||||
private MediaCodecVideoRenderer mediaCodecVideoRenderer;
|
private MediaCodecVideoRenderer mediaCodecVideoRenderer;
|
||||||
|
private MediaCodecSelector mediaCodecSelector;
|
||||||
@Nullable private Format currentOutputFormat;
|
@Nullable private Format currentOutputFormat;
|
||||||
|
|
||||||
@Mock private VideoRendererEventListener eventListener;
|
@Mock private VideoRendererEventListener eventListener;
|
||||||
@ -124,7 +134,7 @@ public class MediaCodecVideoRendererTest {
|
|||||||
@Before
|
@Before
|
||||||
public void setUp() throws Exception {
|
public void setUp() throws Exception {
|
||||||
testMainLooper = Looper.getMainLooper();
|
testMainLooper = Looper.getMainLooper();
|
||||||
MediaCodecSelector mediaCodecSelector =
|
mediaCodecSelector =
|
||||||
(mimeType, requiresSecureDecoder, requiresTunnelingDecoder) ->
|
(mimeType, requiresSecureDecoder, requiresTunnelingDecoder) ->
|
||||||
Collections.singletonList(
|
Collections.singletonList(
|
||||||
MediaCodecInfo.newInstance(
|
MediaCodecInfo.newInstance(
|
||||||
@ -207,6 +217,65 @@ public class MediaCodecVideoRendererTest {
|
|||||||
verify(eventListener).onDroppedFrames(eq(1), anyLong());
|
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
|
@Test
|
||||||
public void render_sendsVideoSizeChangeWithCurrentFormatValues() throws Exception {
|
public void render_sendsVideoSizeChangeWithCurrentFormatValues() throws Exception {
|
||||||
FakeSampleStream fakeSampleStream =
|
FakeSampleStream fakeSampleStream =
|
||||||
@ -1194,4 +1263,146 @@ public class MediaCodecVideoRendererTest {
|
|||||||
.setHeight(height)
|
.setHeight(height)
|
||||||
.build();
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -449,8 +449,8 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
@Override
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
this.mediaTransferListener = mediaTransferListener;
|
this.mediaTransferListener = mediaTransferListener;
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), getPlayerId());
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), getPlayerId());
|
||||||
|
drmSessionManager.prepare();
|
||||||
if (sideloadedManifest) {
|
if (sideloadedManifest) {
|
||||||
processManifest(false);
|
processManifest(false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -417,9 +417,9 @@ public final class HlsMediaSource extends BaseMediaSource
|
|||||||
@Override
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
this.mediaTransferListener = mediaTransferListener;
|
this.mediaTransferListener = mediaTransferListener;
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(
|
drmSessionManager.setPlayer(
|
||||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId());
|
/* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId());
|
||||||
|
drmSessionManager.prepare();
|
||||||
MediaSourceEventListener.EventDispatcher eventDispatcher =
|
MediaSourceEventListener.EventDispatcher eventDispatcher =
|
||||||
createEventDispatcher(/* mediaPeriodId= */ null);
|
createEventDispatcher(/* mediaPeriodId= */ null);
|
||||||
playlistTracker.start(
|
playlistTracker.start(
|
||||||
|
@ -374,8 +374,8 @@ public final class SsMediaSource extends BaseMediaSource
|
|||||||
@Override
|
@Override
|
||||||
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
this.mediaTransferListener = mediaTransferListener;
|
this.mediaTransferListener = mediaTransferListener;
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), getPlayerId());
|
drmSessionManager.setPlayer(/* playbackLooper= */ Looper.myLooper(), getPlayerId());
|
||||||
|
drmSessionManager.prepare();
|
||||||
if (sideloadedManifest) {
|
if (sideloadedManifest) {
|
||||||
manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy();
|
manifestLoaderErrorThrower = new LoaderErrorThrower.Dummy();
|
||||||
processManifest();
|
processManifest();
|
||||||
|
@ -37,6 +37,8 @@ import androidx.media3.common.util.Util;
|
|||||||
import androidx.media3.extractor.text.SimpleSubtitleDecoder;
|
import androidx.media3.extractor.text.SimpleSubtitleDecoder;
|
||||||
import androidx.media3.extractor.text.Subtitle;
|
import androidx.media3.extractor.text.Subtitle;
|
||||||
import com.google.common.base.Ascii;
|
import com.google.common.base.Ascii;
|
||||||
|
import com.google.common.base.Charsets;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@ -98,11 +100,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
|||||||
|
|
||||||
if (initializationData != null && !initializationData.isEmpty()) {
|
if (initializationData != null && !initializationData.isEmpty()) {
|
||||||
haveInitializationData = true;
|
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));
|
String formatLine = Util.fromUtf8Bytes(initializationData.get(0));
|
||||||
Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX));
|
Assertions.checkArgument(formatLine.startsWith(FORMAT_LINE_PREFIX));
|
||||||
dialogueFormatFromInitializationData =
|
dialogueFormatFromInitializationData =
|
||||||
Assertions.checkNotNull(SsaDialogueFormat.fromFormatLine(formatLine));
|
Assertions.checkNotNull(SsaDialogueFormat.fromFormatLine(formatLine));
|
||||||
parseHeader(new ParsableByteArray(initializationData.get(1)));
|
parseHeader(new ParsableByteArray(initializationData.get(1)), Charsets.UTF_8);
|
||||||
} else {
|
} else {
|
||||||
haveInitializationData = false;
|
haveInitializationData = false;
|
||||||
dialogueFormatFromInitializationData = null;
|
dialogueFormatFromInitializationData = null;
|
||||||
@ -115,25 +120,37 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
|||||||
List<Long> cueTimesUs = new ArrayList<>();
|
List<Long> cueTimesUs = new ArrayList<>();
|
||||||
|
|
||||||
ParsableByteArray parsableData = new ParsableByteArray(data, length);
|
ParsableByteArray parsableData = new ParsableByteArray(data, length);
|
||||||
|
Charset charset = detectUtfCharset(parsableData);
|
||||||
|
|
||||||
if (!haveInitializationData) {
|
if (!haveInitializationData) {
|
||||||
parseHeader(parsableData);
|
parseHeader(parsableData, charset);
|
||||||
}
|
}
|
||||||
parseEventBody(parsableData, cues, cueTimesUs);
|
parseEventBody(parsableData, cues, cueTimesUs, charset);
|
||||||
return new SsaSubtitle(cues, cueTimesUs);
|
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.
|
* Parses the header of the subtitle.
|
||||||
*
|
*
|
||||||
* @param data A {@link ParsableByteArray} from which the header should be read.
|
* @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;
|
@Nullable String currentLine;
|
||||||
while ((currentLine = data.readLine()) != null) {
|
while ((currentLine = data.readLine(charset)) != null) {
|
||||||
if ("[Script Info]".equalsIgnoreCase(currentLine)) {
|
if ("[Script Info]".equalsIgnoreCase(currentLine)) {
|
||||||
parseScriptInfo(data);
|
parseScriptInfo(data, charset);
|
||||||
} else if ("[V4+ Styles]".equalsIgnoreCase(currentLine)) {
|
} else if ("[V4+ Styles]".equalsIgnoreCase(currentLine)) {
|
||||||
styles = parseStyles(data);
|
styles = parseStyles(data, charset);
|
||||||
} else if ("[V4 Styles]".equalsIgnoreCase(currentLine)) {
|
} else if ("[V4 Styles]".equalsIgnoreCase(currentLine)) {
|
||||||
Log.i(TAG, "[V4 Styles] are not supported");
|
Log.i(TAG, "[V4 Styles] are not supported");
|
||||||
} else if ("[Events]".equalsIgnoreCase(currentLine)) {
|
} 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}
|
* @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition() position}
|
||||||
* set to the beginning of the first line after {@code [Script Info]}.
|
* 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;
|
@Nullable String currentLine;
|
||||||
while ((currentLine = data.readLine()) != null
|
while ((currentLine = data.readLine(charset)) != null
|
||||||
&& (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) {
|
&& (data.bytesLeft() == 0 || data.peekChar(charset) != '[')) {
|
||||||
String[] infoNameAndValue = currentLine.split(":");
|
String[] infoNameAndValue = currentLine.split(":");
|
||||||
if (infoNameAndValue.length != 2) {
|
if (infoNameAndValue.length != 2) {
|
||||||
continue;
|
continue;
|
||||||
@ -187,13 +205,14 @@ public final class SsaDecoder extends SimpleSubtitleDecoder {
|
|||||||
*
|
*
|
||||||
* @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition()} pointing
|
* @param data A {@link ParsableByteArray} with {@link ParsableByteArray#getPosition()} pointing
|
||||||
* at the beginning of the first line after {@code [V4+ Styles]}.
|
* 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<>();
|
Map<String, SsaStyle> styles = new LinkedHashMap<>();
|
||||||
@Nullable SsaStyle.Format formatInfo = null;
|
@Nullable SsaStyle.Format formatInfo = null;
|
||||||
@Nullable String currentLine;
|
@Nullable String currentLine;
|
||||||
while ((currentLine = data.readLine()) != null
|
while ((currentLine = data.readLine(charset)) != null
|
||||||
&& (data.bytesLeft() == 0 || data.peekUnsignedByte() != '[')) {
|
&& (data.bytesLeft() == 0 || data.peekChar(charset) != '[')) {
|
||||||
if (currentLine.startsWith(FORMAT_LINE_PREFIX)) {
|
if (currentLine.startsWith(FORMAT_LINE_PREFIX)) {
|
||||||
formatInfo = SsaStyle.Format.fromFormatLine(currentLine);
|
formatInfo = SsaStyle.Format.fromFormatLine(currentLine);
|
||||||
} else if (currentLine.startsWith(STYLE_LINE_PREFIX)) {
|
} 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 data A {@link ParsableByteArray} from which the body should be read.
|
||||||
* @param cues A list to which parsed cues will be added.
|
* @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 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
|
@Nullable
|
||||||
SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null;
|
SsaDialogueFormat format = haveInitializationData ? dialogueFormatFromInitializationData : null;
|
||||||
@Nullable String currentLine;
|
@Nullable String currentLine;
|
||||||
while ((currentLine = data.readLine()) != null) {
|
while ((currentLine = data.readLine(charset)) != null) {
|
||||||
if (currentLine.startsWith(FORMAT_LINE_PREFIX)) {
|
if (currentLine.startsWith(FORMAT_LINE_PREFIX)) {
|
||||||
format = SsaDialogueFormat.fromFormatLine(currentLine);
|
format = SsaDialogueFormat.fromFormatLine(currentLine);
|
||||||
} else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) {
|
} else if (currentLine.startsWith(DIALOGUE_LINE_PREFIX)) {
|
||||||
|
@ -15,15 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.extractor.ts;
|
package androidx.media3.extractor.ts;
|
||||||
|
|
||||||
import static java.lang.Math.min;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.Assertions;
|
import androidx.media3.common.util.Assertions;
|
||||||
import androidx.media3.common.util.CodecSpecificDataUtil;
|
import androidx.media3.common.util.CodecSpecificDataUtil;
|
||||||
import androidx.media3.common.util.Log;
|
|
||||||
import androidx.media3.common.util.ParsableByteArray;
|
import androidx.media3.common.util.ParsableByteArray;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.media3.common.util.Util;
|
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(sps.nalData, 0, csdData, vps.nalLength, sps.nalLength);
|
||||||
System.arraycopy(pps.nalData, 0, csdData, vps.nalLength + sps.nalLength, pps.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.
|
// Skip the 3-byte NAL unit start code synthesised by the NalUnitTargetBuffer constructor.
|
||||||
ParsableNalUnitBitArray bitArray = new ParsableNalUnitBitArray(sps.nalData, 0, sps.nalLength);
|
NalUnitUtil.H265SpsData spsData =
|
||||||
bitArray.skipBits(40 + 4); // NAL header, sps_video_parameter_set_id
|
NalUnitUtil.parseH265SpsNalUnit(sps.nalData, /* nalOffset= */ 3, sps.nalLength);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String codecs =
|
String codecs =
|
||||||
CodecSpecificDataUtil.buildHevcCodecString(
|
CodecSpecificDataUtil.buildHevcCodecString(
|
||||||
generalProfileSpace,
|
spsData.generalProfileSpace,
|
||||||
generalTierFlag,
|
spsData.generalTierFlag,
|
||||||
generalProfileIdc,
|
spsData.generalProfileIdc,
|
||||||
generalProfileCompatibilityFlags,
|
spsData.generalProfileCompatibilityFlags,
|
||||||
constraintBytes,
|
spsData.constraintBytes,
|
||||||
generalLevelIdc);
|
spsData.generalLevelIdc);
|
||||||
|
|
||||||
return new Format.Builder()
|
return new Format.Builder()
|
||||||
.setId(formatId)
|
.setId(formatId)
|
||||||
.setSampleMimeType(MimeTypes.VIDEO_H265)
|
.setSampleMimeType(MimeTypes.VIDEO_H265)
|
||||||
.setCodecs(codecs)
|
.setCodecs(codecs)
|
||||||
.setWidth(picWidthInLumaSamples)
|
.setWidth(spsData.width)
|
||||||
.setHeight(picHeightInLumaSamples)
|
.setHeight(spsData.height)
|
||||||
.setPixelWidthHeightRatio(pixelWidthHeightRatio)
|
.setPixelWidthHeightRatio(spsData.pixelWidthHeightRatio)
|
||||||
.setInitializationData(Collections.singletonList(csdData))
|
.setInitializationData(Collections.singletonList(csdData))
|
||||||
.build();
|
.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"})
|
@EnsuresNonNull({"output", "sampleReader"})
|
||||||
private void assertTracksCreated() {
|
private void assertTracksCreated() {
|
||||||
Assertions.checkStateNotNull(output);
|
Assertions.checkStateNotNull(output);
|
||||||
|
@ -30,6 +30,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Objects;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
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_HEADER_ONLY = "media/ssa/typical_header";
|
||||||
private static final String TYPICAL_DIALOGUE_ONLY = "media/ssa/typical_dialogue";
|
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_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 OVERLAPPING_TIMECODES = "media/ssa/overlapping_timecodes";
|
||||||
private static final String POSITIONS = "media/ssa/positioning";
|
private static final String POSITIONS = "media/ssa/positioning";
|
||||||
private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes";
|
private static final String INVALID_TIMECODES = "media/ssa/invalid_timecodes";
|
||||||
@ -130,6 +133,58 @@ public final class SsaDecoderTest {
|
|||||||
assertTypicalCue3(subtitle, 4);
|
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
|
@Test
|
||||||
public void decodeOverlappingTimecodes() throws IOException {
|
public void decodeOverlappingTimecodes() throws IOException {
|
||||||
SsaDecoder decoder = new SsaDecoder();
|
SsaDecoder decoder = new SsaDecoder();
|
||||||
@ -438,6 +493,10 @@ public final class SsaDecoderTest {
|
|||||||
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
|
assertThat(subtitle.getEventTime(eventIndex)).isEqualTo(0);
|
||||||
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
|
assertThat(subtitle.getCues(subtitle.getEventTime(eventIndex)).get(0).text.toString())
|
||||||
.isEqualTo("This is the first subtitle.");
|
.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);
|
assertThat(subtitle.getEventTime(eventIndex + 1)).isEqualTo(1230000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +92,12 @@ public final class TsExtractorTest {
|
|||||||
ExtractorAsserts.assertBehavior(TsExtractor::new, "media/ts/sample_h265.ts", simulationConfig);
|
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
|
@Test
|
||||||
public void sampleWithScte35() throws Exception {
|
public void sampleWithScte35() throws Exception {
|
||||||
ExtractorAsserts.assertBehavior(
|
ExtractorAsserts.assertBehavior(
|
||||||
|
@ -26,6 +26,7 @@ import androidx.media3.session.MediaSession.ControllerInfo;
|
|||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
import com.google.common.util.concurrent.MoreExecutors;
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayDeque;
|
import java.util.ArrayDeque;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
@ -62,14 +63,14 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
private final ArrayMap<ControllerInfo, ConnectedControllerRecord<T>> controllerRecords =
|
private final ArrayMap<ControllerInfo, ConnectedControllerRecord<T>> controllerRecords =
|
||||||
new ArrayMap<>();
|
new ArrayMap<>();
|
||||||
|
|
||||||
private final MediaSessionImpl sessionImpl;
|
private final WeakReference<MediaSessionImpl> sessionImpl;
|
||||||
|
|
||||||
public ConnectedControllersManager(MediaSessionImpl session) {
|
public ConnectedControllersManager(MediaSessionImpl session) {
|
||||||
// Initialize default values.
|
// Initialize default values.
|
||||||
lock = new Object();
|
lock = new Object();
|
||||||
|
|
||||||
// Initialize members with params.
|
// Initialize members with params.
|
||||||
sessionImpl = session;
|
sessionImpl = new WeakReference<>(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addController(
|
public void addController(
|
||||||
@ -136,6 +137,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
}
|
}
|
||||||
|
|
||||||
record.sequencedFutureManager.release();
|
record.sequencedFutureManager.release();
|
||||||
|
@Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
|
||||||
|
if (sessionImpl == null || sessionImpl.isReleased()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
postOrRun(
|
postOrRun(
|
||||||
sessionImpl.getApplicationHandler(),
|
sessionImpl.getApplicationHandler(),
|
||||||
() -> {
|
() -> {
|
||||||
@ -214,8 +219,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
info = controllerRecords.get(controllerInfo);
|
info = controllerRecords.get(controllerInfo);
|
||||||
}
|
}
|
||||||
|
@Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
|
||||||
return info != null
|
return info != null
|
||||||
&& info.playerCommands.contains(commandCode)
|
&& info.playerCommands.contains(commandCode)
|
||||||
|
&& sessionImpl != null
|
||||||
&& sessionImpl.getPlayerWrapper().getAvailableCommands().contains(commandCode);
|
&& sessionImpl.getPlayerWrapper().getAvailableCommands().contains(commandCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -248,6 +255,10 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
|
|
||||||
@GuardedBy("lock")
|
@GuardedBy("lock")
|
||||||
private void flushCommandQueue(ConnectedControllerRecord<T> info) {
|
private void flushCommandQueue(ConnectedControllerRecord<T> info) {
|
||||||
|
@Nullable MediaSessionImpl sessionImpl = this.sessionImpl.get();
|
||||||
|
if (sessionImpl == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
AtomicBoolean continueRunning = new AtomicBoolean(true);
|
AtomicBoolean continueRunning = new AtomicBoolean(true);
|
||||||
while (continueRunning.get()) {
|
while (continueRunning.get()) {
|
||||||
continueRunning.set(false);
|
continueRunning.set(false);
|
||||||
|
@ -388,7 +388,7 @@ public final class LibraryResult<V> implements Bundleable {
|
|||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LibraryResult<>(resultCode, completionTimeMs, params, value, VALUE_TYPE_ITEM_LIST);
|
return new LibraryResult<>(resultCode, completionTimeMs, params, value, valueType);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Documented
|
@Documented
|
||||||
|
@ -516,16 +516,19 @@ public class MediaController implements Player {
|
|||||||
/**
|
/**
|
||||||
* Releases the future controller returned by {@link Builder#buildAsync()}. It makes sure that the
|
* 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.
|
* 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) {
|
public static void releaseFuture(Future<? extends MediaController> controllerFuture) {
|
||||||
if (!controllerFuture.isDone()) {
|
if (controllerFuture.cancel(/* mayInterruptIfRunning= */ true)) {
|
||||||
controllerFuture.cancel(/* mayInterruptIfRunning= */ true);
|
// Successfully canceled the Future. The controller will be released by MediaControllerHolder.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MediaController controller;
|
MediaController controller;
|
||||||
try {
|
try {
|
||||||
controller = controllerFuture.get();
|
controller = Futures.getDone(controllerFuture);
|
||||||
} catch (CancellationException | ExecutionException | InterruptedException e) {
|
} catch (CancellationException | ExecutionException e) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
controller.release();
|
controller.release();
|
||||||
|
@ -2500,6 +2500,13 @@ import org.checkerframework.checker.nullness.qual.NonNull;
|
|||||||
private void updateSessionPositionInfoIfNeeded(SessionPositionInfo sessionPositionInfo) {
|
private void updateSessionPositionInfoIfNeeded(SessionPositionInfo sessionPositionInfo) {
|
||||||
if (pendingMaskingSequencedFutureNumbers.isEmpty()
|
if (pendingMaskingSequencedFutureNumbers.isEmpty()
|
||||||
&& playerInfo.sessionPositionInfo.eventTimeMs < sessionPositionInfo.eventTimeMs) {
|
&& 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);
|
playerInfo = playerInfo.copyWithSessionPositionInfo(sessionPositionInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1885,13 +1885,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
? newLegacyPlayerInfo.playbackInfoCompat.getVolumeControl()
|
? newLegacyPlayerInfo.playbackInfoCompat.getVolumeControl()
|
||||||
: VolumeProviderCompat.VOLUME_CONTROL_FIXED;
|
: VolumeProviderCompat.VOLUME_CONTROL_FIXED;
|
||||||
availablePlayerCommands =
|
availablePlayerCommands =
|
||||||
(oldControllerInfo.availablePlayerCommands == Commands.EMPTY)
|
MediaUtils.convertToPlayerCommands(
|
||||||
? MediaUtils.convertToPlayerCommands(
|
|
||||||
newLegacyPlayerInfo.playbackStateCompat,
|
newLegacyPlayerInfo.playbackStateCompat,
|
||||||
volumeControlType,
|
volumeControlType,
|
||||||
sessionFlags,
|
sessionFlags,
|
||||||
isSessionReady)
|
isSessionReady);
|
||||||
: oldControllerInfo.availablePlayerCommands;
|
|
||||||
|
|
||||||
PlaybackException playerError =
|
PlaybackException playerError =
|
||||||
MediaUtils.convertToPlaybackException(newLegacyPlayerInfo.playbackStateCompat);
|
MediaUtils.convertToPlaybackException(newLegacyPlayerInfo.playbackStateCompat);
|
||||||
|
@ -126,8 +126,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
.putBoolean(BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, isSearchSessionCommandAvailable);
|
.putBoolean(BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, isSearchSessionCommandAvailable);
|
||||||
return new BrowserRoot(result.value.mediaId, extras);
|
return new BrowserRoot(result.value.mediaId, extras);
|
||||||
}
|
}
|
||||||
// No library root, but keep browser compat connected to allow getting session.
|
// No library root, but keep browser compat connected to allow getting session unless the
|
||||||
return MediaUtils.defaultBrowserRoot;
|
// `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
|
// TODO(b/192455639): Optimize potential multiple calls of
|
||||||
|
@ -39,6 +39,7 @@ import androidx.annotation.VisibleForTesting;
|
|||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.media.MediaSessionManager.RemoteUserInfo;
|
import androidx.media.MediaSessionManager.RemoteUserInfo;
|
||||||
import androidx.media3.common.AudioAttributes;
|
import androidx.media3.common.AudioAttributes;
|
||||||
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.DeviceInfo;
|
import androidx.media3.common.DeviceInfo;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaLibraryInfo;
|
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
|
* the items directly by using Guava's {@link Futures#immediateFuture(Object)}. Once the {@link
|
||||||
* MediaItemsWithStartPosition} has been resolved, the session will call {@link
|
* MediaItemsWithStartPosition} has been resolved, the session will call {@link
|
||||||
* Player#setMediaItems} as requested. If the resolved {@link
|
* Player#setMediaItems} as requested. If the resolved {@link
|
||||||
* MediaItemsWithStartPosition#startIndex startIndex} is {@link
|
* MediaItemsWithStartPosition#startIndex startIndex} is {@link C#INDEX_UNSET C.INDEX_UNSET}
|
||||||
* androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} then the session will call {@link
|
* then the session will call {@link Player#setMediaItem(MediaItem, boolean)} with {@code
|
||||||
* Player#setMediaItem(MediaItem, boolean)} with {@code resetPosition} set to {@code true}.
|
* resetPosition} set to {@code true}.
|
||||||
*
|
*
|
||||||
* <p>Interoperability: This method will be called in response to the following {@link
|
* <p>Interoperability: This method will be called in response to the following {@link
|
||||||
* MediaControllerCompat} methods:
|
* MediaControllerCompat} methods:
|
||||||
@ -1188,19 +1189,18 @@ public class MediaSession {
|
|||||||
* @param controller The controller information.
|
* @param controller The controller information.
|
||||||
* @param mediaItems The list of requested {@linkplain MediaItem media items}.
|
* @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,
|
* @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
|
* or {@link C#INDEX_UNSET C.INDEX_UNSET} to start playing from the default index in the
|
||||||
* default index in the playlist.
|
* playlist.
|
||||||
* @param startPositionMs The starting position in the media item from where to start playing,
|
* @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
|
* or {@link C#TIME_UNSET C.TIME_UNSET} to start playing from the default position in the
|
||||||
* default position in the media item. This value is ignored if startIndex is C.INDEX_UNSET
|
* media item. This value is ignored if startIndex is C.INDEX_UNSET
|
||||||
* @return A {@link ListenableFuture} with a {@link MediaItemsWithStartPosition} containing a
|
* @return A {@link ListenableFuture} with a {@link MediaItemsWithStartPosition} containing a
|
||||||
* list of resolved {@linkplain MediaItem media items}, and a starting index and position
|
* list of resolved {@linkplain MediaItem media items}, and a starting index and position
|
||||||
* that are playable by the underlying {@link Player}. If returned {@link
|
* that are playable by the underlying {@link Player}. If returned {@link
|
||||||
* MediaItemsWithStartPosition#startIndex} is {@link androidx.media3.common.C#INDEX_UNSET
|
* MediaItemsWithStartPosition#startIndex} is {@link C#INDEX_UNSET C.INDEX_UNSET} and {@link
|
||||||
* C.INDEX_UNSET} and {@link MediaItemsWithStartPosition#startPositionMs} is {@link
|
* MediaItemsWithStartPosition#startPositionMs} is {@link C#TIME_UNSET C.TIME_UNSET}, then
|
||||||
* androidx.media3.common.C#TIME_UNSET C.TIME_UNSET}, then {@linkplain
|
* {@linkplain Player#setMediaItems(List, boolean) Player#setMediaItems(List, true)} will be
|
||||||
* Player#setMediaItems(List, boolean) Player#setMediaItems(List, true)} will be called to
|
* called to set media items with default index and position.
|
||||||
* set media items with default index and position.
|
|
||||||
*/
|
*/
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
default ListenableFuture<MediaItemsWithStartPosition> onSetMediaItems(
|
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
|
@UnstableApi
|
||||||
public static final class MediaItemsWithStartPosition {
|
public static final class MediaItemsWithStartPosition {
|
||||||
/** List of {@link MediaItem media items}. */
|
/** List of {@linkplain MediaItem media items}. */
|
||||||
public final ImmutableList<MediaItem> mediaItems;
|
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
|
* <p>The start index in {@link #mediaItems} from which to start playing, or {@link
|
||||||
* androidx.media3.common.C#INDEX_UNSET C.INDEX_UNSET} to start playing from the default index
|
* C#INDEX_UNSET} to start playing from the default index in the playlist.
|
||||||
* in the playlist.
|
|
||||||
*/
|
*/
|
||||||
public final int startIndex;
|
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
|
* <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
|
* C#TIME_UNSET} to start playing from the default position in the media item. This value is
|
||||||
* in the media item. This value is ignored if startIndex is C.INDEX_UNSET
|
* ignored if {@code startIndex} is {@link C#INDEX_UNSET}.
|
||||||
*/
|
*/
|
||||||
public final long startPositionMs;
|
public final long startPositionMs;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an instance.
|
* Creates an instance.
|
||||||
*
|
*
|
||||||
* @param mediaItems List of {@link MediaItem media items}.
|
* @param mediaItems List of {@linkplain MediaItem media items}.
|
||||||
* @param startIndex Index to start playing at in {@link MediaItem} list.
|
* @param startIndex Index to start playing at in {@code mediaItems}, or {@link C#INDEX_UNSET}
|
||||||
* @param startPositionMs Position to start playing from in starting media item.
|
* 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(
|
public MediaItemsWithStartPosition(
|
||||||
List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
|
||||||
@ -1473,17 +1474,19 @@ public class MediaSession {
|
|||||||
* applied to the subclasses.
|
* applied to the subclasses.
|
||||||
*/
|
*/
|
||||||
/* package */ abstract static class BuilderBase<
|
/* 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 Context context;
|
||||||
/* package */ final Player player;
|
/* package */ final Player player;
|
||||||
/* package */ String id;
|
/* package */ String id;
|
||||||
/* package */ C callback;
|
/* package */ CallbackT callback;
|
||||||
/* package */ @Nullable PendingIntent sessionActivity;
|
/* package */ @Nullable PendingIntent sessionActivity;
|
||||||
/* package */ Bundle extras;
|
/* package */ Bundle extras;
|
||||||
/* package */ @MonotonicNonNull BitmapLoader bitmapLoader;
|
/* 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.context = checkNotNull(context);
|
||||||
this.player = checkNotNull(player);
|
this.player = checkNotNull(player);
|
||||||
checkArgument(player.canAdvertiseSession());
|
checkArgument(player.canAdvertiseSession());
|
||||||
@ -1493,35 +1496,35 @@ public class MediaSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public U setSessionActivity(PendingIntent pendingIntent) {
|
public BuilderT setSessionActivity(PendingIntent pendingIntent) {
|
||||||
sessionActivity = checkNotNull(pendingIntent);
|
sessionActivity = checkNotNull(pendingIntent);
|
||||||
return (U) this;
|
return (BuilderT) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public U setId(String id) {
|
public BuilderT setId(String id) {
|
||||||
this.id = checkNotNull(id);
|
this.id = checkNotNull(id);
|
||||||
return (U) this;
|
return (BuilderT) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
/* package */ U setCallback(C callback) {
|
/* package */ BuilderT setCallback(CallbackT callback) {
|
||||||
this.callback = checkNotNull(callback);
|
this.callback = checkNotNull(callback);
|
||||||
return (U) this;
|
return (BuilderT) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public U setExtras(Bundle extras) {
|
public BuilderT setExtras(Bundle extras) {
|
||||||
this.extras = new Bundle(checkNotNull(extras));
|
this.extras = new Bundle(checkNotNull(extras));
|
||||||
return (U) this;
|
return (BuilderT) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public U setBitmapLoader(BitmapLoader bitmapLoader) {
|
public BuilderT setBitmapLoader(BitmapLoader bitmapLoader) {
|
||||||
this.bitmapLoader = bitmapLoader;
|
this.bitmapLoader = bitmapLoader;
|
||||||
return (U) this;
|
return (BuilderT) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract T build();
|
public abstract SessionT build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -733,7 +733,16 @@ import org.checkerframework.checker.initialization.qual.Initialized;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SessionPositionInfo sessionPositionInfo = playerWrapper.createSessionPositionInfoForBundling();
|
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);
|
dispatchOnPeriodicSessionPositionInfoChanged(sessionPositionInfo);
|
||||||
|
}
|
||||||
schedulePeriodicSessionPositionInfoChanges();
|
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) {
|
public void sendPlayerInfoChangedMessage(boolean excludeTimeline, boolean excludeTracks) {
|
||||||
this.excludeTimeline = this.excludeTimeline && excludeTimeline;
|
this.excludeTimeline = this.excludeTimeline && excludeTimeline;
|
||||||
this.excludeTracks = this.excludeTracks && excludeTracks;
|
this.excludeTracks = this.excludeTracks && excludeTracks;
|
||||||
if (!onPlayerInfoChangedHandler.hasMessages(MSG_PLAYER_INFO_CHANGED)) {
|
if (!hasMessages(MSG_PLAYER_INFO_CHANGED)) {
|
||||||
onPlayerInfoChangedHandler.sendEmptyMessage(MSG_PLAYER_INFO_CHANGED);
|
sendEmptyMessage(MSG_PLAYER_INFO_CHANGED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,12 +147,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
appPackageName = context.getPackageName();
|
appPackageName = context.getPackageName();
|
||||||
sessionManager = MediaSessionManager.getSessionManager(context);
|
sessionManager = MediaSessionManager.getSessionManager(context);
|
||||||
controllerLegacyCbForBroadcast = new ControllerLegacyCbForBroadcast();
|
controllerLegacyCbForBroadcast = new ControllerLegacyCbForBroadcast();
|
||||||
connectionTimeoutHandler =
|
|
||||||
new ConnectionTimeoutHandler(session.getApplicationHandler().getLooper());
|
|
||||||
mediaPlayPauseKeyHandler =
|
mediaPlayPauseKeyHandler =
|
||||||
new MediaPlayPauseKeyHandler(session.getApplicationHandler().getLooper());
|
new MediaPlayPauseKeyHandler(session.getApplicationHandler().getLooper());
|
||||||
connectedControllersManager = new ConnectedControllersManager<>(session);
|
connectedControllersManager = new ConnectedControllersManager<>(session);
|
||||||
connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS;
|
connectionTimeoutMs = DEFAULT_CONNECTION_TIMEOUT_MS;
|
||||||
|
connectionTimeoutHandler =
|
||||||
|
new ConnectionTimeoutHandler(
|
||||||
|
session.getApplicationHandler().getLooper(), connectedControllersManager);
|
||||||
|
|
||||||
// Select a media button receiver component.
|
// Select a media button receiver component.
|
||||||
ComponentName receiverComponentName = queryPackageManagerForMediaButtonReceiver(context);
|
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;
|
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);
|
super(looper);
|
||||||
|
this.connectedControllersManager = connectedControllersManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -1411,13 +1411,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
return new Pair<>(mergedPlayerInfo, mergedBundlingExclusions);
|
return new Pair<>(mergedPlayerInfo, mergedBundlingExclusions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] convertToByteArray(Bitmap bitmap) throws IOException {
|
/** Generates an array of {@code n} indices. */
|
||||||
try (ByteArrayOutputStream stream = new ByteArrayOutputStream()) {
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, /* ignored */ 0, stream);
|
|
||||||
return stream.toByteArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int[] generateUnshuffledIndices(int n) {
|
public static int[] generateUnshuffledIndices(int n) {
|
||||||
int[] indices = new int[n];
|
int[] indices = new int[n];
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
@ -1426,6 +1420,10 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
return indices;
|
return indices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the buffered percentage of the given buffered position and the duration in
|
||||||
|
* milliseconds.
|
||||||
|
*/
|
||||||
public static int calculateBufferedPercentage(long bufferedPositionMs, long durationMs) {
|
public static int calculateBufferedPercentage(long bufferedPositionMs, long durationMs) {
|
||||||
return bufferedPositionMs == C.TIME_UNSET || durationMs == C.TIME_UNSET
|
return bufferedPositionMs == C.TIME_UNSET || durationMs == C.TIME_UNSET
|
||||||
? 0
|
? 0
|
||||||
@ -1434,8 +1432,15 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
: Util.constrainValue((int) ((bufferedPositionMs * 100) / durationMs), 0, 100);
|
: 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(
|
public static void setMediaItemsWithStartIndexAndPosition(
|
||||||
PlayerWrapper player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) {
|
Player player, MediaSession.MediaItemsWithStartPosition mediaItemsWithStartPosition) {
|
||||||
if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET) {
|
if (mediaItemsWithStartPosition.startIndex == C.INDEX_UNSET) {
|
||||||
if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
||||||
player.setMediaItems(mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true);
|
player.setMediaItems(mediaItemsWithStartPosition.mediaItems, /* resetPosition= */ true);
|
||||||
@ -1443,8 +1448,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
player.setMediaItem(
|
player.setMediaItem(
|
||||||
mediaItemsWithStartPosition.mediaItems.get(0), /* resetPosition= */ true);
|
mediaItemsWithStartPosition.mediaItems.get(0), /* resetPosition= */ true);
|
||||||
}
|
}
|
||||||
} else {
|
} else if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
||||||
if (player.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)) {
|
|
||||||
player.setMediaItems(
|
player.setMediaItems(
|
||||||
mediaItemsWithStartPosition.mediaItems,
|
mediaItemsWithStartPosition.mediaItems,
|
||||||
mediaItemsWithStartPosition.startIndex,
|
mediaItemsWithStartPosition.startIndex,
|
||||||
@ -1455,6 +1459,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
|||||||
mediaItemsWithStartPosition.startPositionMs);
|
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() {}
|
private MediaUtils() {}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<string name="media3_controls_pause_description">Pausar</string>
|
<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_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_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="media3_controls_seek_forward_description">Avançar</string>
|
||||||
<string name="authentication_required">Autenticação necessária</string>
|
<string name="authentication_required">Autenticação necessária</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -15,11 +15,17 @@
|
|||||||
*/
|
*/
|
||||||
package androidx.media3.session;
|
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 static org.junit.Assert.assertThrows;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
|
import androidx.media3.session.MediaLibraryService.LibraryParams;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
@ -51,4 +57,74 @@ public class LibraryResultTest {
|
|||||||
assertThrows(
|
assertThrows(
|
||||||
IllegalArgumentException.class, () -> LibraryResult.ofItem(item, /* params= */ null));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
BIN
libraries/test_data/src/test/assets/media/ssa/typical_utf16be
Normal file
BIN
libraries/test_data/src/test/assets/media/ssa/typical_utf16be
Normal file
Binary file not shown.
BIN
libraries/test_data/src/test/assets/media/ssa/typical_utf16le
Normal file
BIN
libraries/test_data/src/test/assets/media/ssa/typical_utf16le
Normal file
Binary file not shown.
Binary file not shown.
@ -34,6 +34,7 @@ import android.app.PendingIntent;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.media3.common.AudioAttributes;
|
import androidx.media3.common.AudioAttributes;
|
||||||
import androidx.media3.common.C;
|
import androidx.media3.common.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
@ -1064,6 +1065,51 @@ public class MediaControllerTest {
|
|||||||
assertThat(bufferedPositionAfterDelay.get()).isNotEqualTo(testBufferedPosition);
|
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
|
@Test
|
||||||
public void getContentBufferedPosition_byDefault_returnsZero() throws Exception {
|
public void getContentBufferedPosition_byDefault_returnsZero() throws Exception {
|
||||||
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
|
MediaController controller = controllerTestRule.createController(remoteSession.getToken());
|
||||||
|
@ -1502,6 +1502,36 @@ public class MediaControllerWithMediaSessionCompatTest {
|
|||||||
assertThat(errorFromGetterRef.get().getMessage()).isEqualTo(testConvertedErrorMessage);
|
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
|
@Test
|
||||||
public void setPlaybackToRemote_notifiesDeviceInfoAndVolume() throws Exception {
|
public void setPlaybackToRemote_notifiesDeviceInfoAndVolume() throws Exception {
|
||||||
int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
|
int volumeControlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
|
||||||
|
@ -338,7 +338,8 @@ public class FakeMediaPeriod implements MediaPeriod {
|
|||||||
lastSeekPositionUs = seekPositionUs;
|
lastSeekPositionUs = seekPositionUs;
|
||||||
boolean seekedInsideStreams = true;
|
boolean seekedInsideStreams = true;
|
||||||
for (FakeSampleStream sampleStream : sampleStreams) {
|
for (FakeSampleStream sampleStream : sampleStreams) {
|
||||||
seekedInsideStreams &= sampleStream.seekToUs(seekPositionUs);
|
seekedInsideStreams &=
|
||||||
|
sampleStream.seekToUs(seekPositionUs, /* allowTimeBeyondBuffer= */ false);
|
||||||
}
|
}
|
||||||
if (!seekedInsideStreams) {
|
if (!seekedInsideStreams) {
|
||||||
for (FakeSampleStream sampleStream : sampleStreams) {
|
for (FakeSampleStream sampleStream : sampleStreams) {
|
||||||
|
@ -215,9 +215,9 @@ public class FakeMediaSource extends BaseMediaSource {
|
|||||||
public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
public synchronized void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
|
||||||
assertThat(preparedSource).isFalse();
|
assertThat(preparedSource).isFalse();
|
||||||
transferListener = mediaTransferListener;
|
transferListener = mediaTransferListener;
|
||||||
drmSessionManager.prepare();
|
|
||||||
drmSessionManager.setPlayer(
|
drmSessionManager.setPlayer(
|
||||||
/* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId());
|
/* playbackLooper= */ checkNotNull(Looper.myLooper()), getPlayerId());
|
||||||
|
drmSessionManager.prepare();
|
||||||
preparedSource = true;
|
preparedSource = true;
|
||||||
releasedSource = false;
|
releasedSource = false;
|
||||||
sourceInfoRefreshHandler = Util.createHandlerForCurrentLooper();
|
sourceInfoRefreshHandler = Util.createHandlerForCurrentLooper();
|
||||||
|
@ -204,10 +204,12 @@ public class FakeSampleStream implements SampleStream {
|
|||||||
* Seeks the stream to a new position using already available data in the queue.
|
* Seeks the stream to a new position using already available data in the queue.
|
||||||
*
|
*
|
||||||
* @param positionUs The new position, in microseconds.
|
* @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.
|
* @return Whether seeking inside the available data was possible.
|
||||||
*/
|
*/
|
||||||
public boolean seekToUs(long positionUs) {
|
public boolean seekToUs(long positionUs, boolean allowTimeBeyondBuffer) {
|
||||||
return sampleQueue.seekTo(positionUs, /* allowTimeBeyondBuffer= */ false);
|
return sampleQueue.seekTo(positionUs, allowTimeBeyondBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<string name="exo_controls_settings_description">ቅንብሮች</string>
|
<string name="exo_controls_settings_description">ቅንብሮች</string>
|
||||||
<string name="exo_controls_overflow_hide_description">ተጨማሪ ቅንብሮችን ይደብቁ</string>
|
<string name="exo_controls_overflow_hide_description">ተጨማሪ ቅንብሮችን ይደብቁ</string>
|
||||||
<string name="exo_controls_overflow_show_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_fullscreen_exit_description">ከሙሉ ማያገጽ ውጣ</string>
|
||||||
<string name="exo_controls_previous_description">ቀዳሚ</string>
|
<string name="exo_controls_previous_description">ቀዳሚ</string>
|
||||||
<string name="exo_controls_next_description">ቀጣይ</string>
|
<string name="exo_controls_next_description">ቀጣይ</string>
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
<string name="exo_controls_hide">Ойноткучту башкаруу элементтерин жашыруу</string>
|
<string name="exo_controls_hide">Ойноткучту башкаруу элементтерин жашыруу</string>
|
||||||
<string name="exo_controls_seek_bar_description">Ойнотуу көрсөткүчү</string>
|
<string name="exo_controls_seek_bar_description">Ойнотуу көрсөткүчү</string>
|
||||||
<string name="exo_controls_settings_description">Параметрлер</string>
|
<string name="exo_controls_settings_description">Параметрлер</string>
|
||||||
<string name="exo_controls_overflow_hide_description">Кошумча жөндөөлөрдү жашыруу</string>
|
<string name="exo_controls_overflow_hide_description">Кошумча параметрлерди жашыруу</string>
|
||||||
<string name="exo_controls_overflow_show_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_fullscreen_exit_description">Толук экран режиминен чыгуу</string>
|
||||||
<string name="exo_controls_previous_description">Мурунку</string>
|
<string name="exo_controls_previous_description">Мурунку</string>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="exo_controls_show">Прикажи ги контролите на плеерот</string>
|
<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_seek_bar_description">Напредок на репродукцијата</string>
|
||||||
<string name="exo_controls_settings_description">Поставки</string>
|
<string name="exo_controls_settings_description">Поставки</string>
|
||||||
<string name="exo_controls_overflow_hide_description">Сокријте ги дополнителните поставки</string>
|
<string name="exo_controls_overflow_hide_description">Сокријте ги дополнителните поставки</string>
|
||||||
|
@ -45,7 +45,7 @@
|
|||||||
<string name="exo_track_selection_title_audio">เสียง</string>
|
<string name="exo_track_selection_title_audio">เสียง</string>
|
||||||
<string name="exo_track_selection_title_text">ข้อความ</string>
|
<string name="exo_track_selection_title_text">ข้อความ</string>
|
||||||
<string name="exo_track_selection_none">ไม่มี</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_unknown">ไม่ทราบ</string>
|
||||||
<string name="exo_track_resolution">%1$d × %2$d</string>
|
<string name="exo_track_resolution">%1$d × %2$d</string>
|
||||||
<string name="exo_track_mono">โมโน</string>
|
<string name="exo_track_mono">โมโน</string>
|
||||||
|
@ -16,7 +16,7 @@ apply plugin: 'maven-publish'
|
|||||||
apply from: "$gradle.ext.androidxMediaSettingsDir/missing_aar_type_workaround.gradle"
|
apply from: "$gradle.ext.androidxMediaSettingsDir/missing_aar_type_workaround.gradle"
|
||||||
|
|
||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
if (rootProject.name == "media3") {
|
if (rootProject.name == gradle.ext.androidxMediaProjectName) {
|
||||||
publishing {
|
publishing {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
|
@ -18,7 +18,7 @@ if (gradle.ext.has('androidxMediaModulePrefix')) {
|
|||||||
modulePrefix += gradle.ext.androidxMediaModulePrefix
|
modulePrefix += gradle.ext.androidxMediaModulePrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
rootProject.name = 'media3'
|
gradle.ext.androidxMediaProjectName = 'media3'
|
||||||
|
|
||||||
include modulePrefix + 'demo'
|
include modulePrefix + 'demo'
|
||||||
project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main')
|
project(modulePrefix + 'demo').projectDir = new File(rootDir, 'demos/main')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user