Commit f17e846d by christosts Committed by GitHub

Merge pull request #251 from androidx/release-1.0.0-rc01

1.0.0 rc01
parents c2cbb637 98bf30d2
Showing with 1947 additions and 913 deletions
......@@ -17,6 +17,7 @@ body:
label: Media3 Version
description: What version of Media3 are you using?
options:
- 1.0.0-rc01
- 1.0.0-beta03
- 1.0.0-beta02
- 1.0.0-beta01
......
......@@ -5,15 +5,13 @@ Android, including local playback (via ExoPlayer) and media sessions.
## Current status
AndroidX Media is currently in beta and we welcome your feedback via the
[issue tracker][]. Please consult the [release notes][] for more details about
the beta release.
AndroidX Media is currently in release candidate and we welcome your feedback
via the [issue tracker][]. Please consult the [release notes][] for more details
about the current release.
ExoPlayer's new home will be in AndroidX Media, but for now we are publishing it
both in AndroidX Media and via the existing [ExoPlayer project][]. While
AndroidX Media is in beta we recommend that production apps using ExoPlayer
continue to depend on the existing ExoPlayer project. We are still handling
ExoPlayer issues on the [ExoPlayer issue tracker][].
both in AndroidX Media and via the existing [ExoPlayer project][] and we are
still handling ExoPlayer issues on the [ExoPlayer issue tracker][].
You'll find some [Media3 documentation on developer.android.com][], including a
[migration guide for existing ExoPlayer and MediaSession users][].
......
Release notes
# Release notes
### 1.0.0-rc01 (2023-02-16)
This release corresponds to the
[ExoPlayer 2.18.3 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.3).
* Core library:
* Tweak the renderer's decoder ordering logic to uphold the
`MediaCodecSelector`'s preferences, even if a decoder reports it may not
be able to play the media performantly. For example with default
selector, hardware decoder with only functional support will be
preferred over software decoder that fully supports the format
([#10604](https://github.com/google/ExoPlayer/issues/10604)).
* Add `ExoPlayer.Builder.setPlaybackLooper` that sets a pre-existing
playback thread for a new ExoPlayer instance.
* Allow download manager helpers to be cleared
([#10776](https://github.com/google/ExoPlayer/issues/10776)).
* Add parameter to `BasePlayer.seekTo` to also indicate the command used
for seeking.
* Use theme when loading drawables on API 21+
([#220](https://github.com/androidx/media/issues/220)).
* Add `ConcatenatingMediaSource2` that allows combining multiple media
items into a single window
([#247](https://github.com/androidx/media/issues/247)).
* Extractors:
* Throw a `ParserException` instead of a `NullPointerException` if the
sample table (stbl) is missing a required sample description (stsd) when
parsing trak atoms.
* Correctly skip samples when seeking directly to a sync frame in fMP4
([#10941](https://github.com/google/ExoPlayer/issues/10941)).
* Audio:
* Use the compressed audio format bitrate to calculate the min buffer size
for `AudioTrack` in direct playbacks (passthrough).
* Text:
* Fix `TextRenderer` passing an invalid (negative) index to
`Subtitle.getEventTime` if a subtitle file contains no cues.
* SubRip: Add support for UTF-16 files if they start with a byte order
mark.
* Metadata:
* Parse multiple null-separated values from ID3 frames, as permitted by
ID3 v2.4.
* Add `MediaMetadata.mediaType` to denote the type of content or the type
of folder described by the metadata.
* Add `MediaMetadata.isBrowsable` as a replacement for
`MediaMetadata.folderType`. The folder type will be deprecated in the
next release.
* DASH:
* Add full parsing for image adaptation sets, including tile counts
([#3752](https://github.com/google/ExoPlayer/issues/3752)).
* UI:
* Fix the deprecated
`PlayerView.setControllerVisibilityListener(PlayerControlView.VisibilityListener)`
to ensure visibility changes are passed to the registered listener
([#229](https://github.com/androidx/media/issues/229)).
* Fix the ordering of the center player controls in `PlayerView` when
using a right-to-left (RTL) layout
([#227](https://github.com/androidx/media/issues/227)).
* Session:
* Add abstract `SimpleBasePlayer` to help implement the `Player` interface
for custom players.
* Add helper method to convert platform session token to Media3
`SessionToken` ([#171](https://github.com/androidx/media/issues/171)).
* Use `onMediaMetadataChanged` to trigger updates of the platform media
session ([#219](https://github.com/androidx/media/issues/219)).
* Add the media session as an argument of `getMediaButtons()` of the
`DefaultMediaNotificationProvider` and use immutable lists for clarity
([#216](https://github.com/androidx/media/issues/216)).
* Add `onSetMediaItems` callback listener to provide means to modify/set
`MediaItem` list, starting index and position by session before setting
onto Player ([#156](https://github.com/androidx/media/issues/156)).
* Avoid double tap detection for non-Bluetooth media button events
([#233](https://github.com/androidx/media/issues/233)).
* Make `QueueTimeline` more robust in case of a shady legacy session state
([#241](https://github.com/androidx/media/issues/241)).
* Metadata:
* Parse multiple null-separated values from ID3 frames, as permitted by
ID3 v2.4.
* Add `MediaMetadata.mediaType` to denote the type of content or the type
of folder described by the metadata.
* Add `MediaMetadata.isBrowsable` as a replacement for
`MediaMetadata.folderType`. The folder type will be deprecated in the
next release.
* Cast extension:
* Bump Cast SDK version to 21.2.0.
* IMA extension:
* Remove player listener of the `ImaServerSideAdInsertionMediaSource` on
the application thread to avoid threading issues.
* Add a property `focusSkipButtonWhenAvailable` to the
`ImaServerSideAdInsertionMediaSource.AdsLoader.Builder` to request
focusing the skip button on TV devices and set it to true by default.
* Add a method `focusSkipButton()` to the
`ImaServerSideAdInsertionMediaSource.AdsLoader` to programmatically
request to focus the skip button.
* Bump IMA SDK version to 3.29.0.
* Demo app:
* Request notification permission for download notifications at runtime
([#10884](https://github.com/google/ExoPlayer/issues/10884)).
### 1.0.0-beta03 (2022-11-22)
......@@ -259,15 +356,15 @@ This release corresponds to the
* Query the platform (API 29+) or assume the audio encoding channel count
for audio passthrough when the format audio channel count is unset,
which occurs with HLS chunkless preparation
([10204](https://github.com/google/ExoPlayer/issues/10204)).
([#10204](https://github.com/google/ExoPlayer/issues/10204)).
* Configure `AudioTrack` with channel mask
`AudioFormat.CHANNEL_OUT_7POINT1POINT4` if the decoder outputs 12
channel PCM audio
([#10322](#https://github.com/google/ExoPlayer/pull/10322).
([#10322](#https://github.com/google/ExoPlayer/pull/10322)).
* DRM
* Ensure the DRM session is always correctly updated when seeking
immediately after a format change
([10274](https://github.com/google/ExoPlayer/issues/10274)).
([#10274](https://github.com/google/ExoPlayer/issues/10274)).
* Text:
* Change `Player.getCurrentCues()` to return `CueGroup` instead of
`List<Cue>`.
......
......@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
project.ext {
releaseVersion = '1.0.0-beta03'
releaseVersionCode = 1_000_000_1_03
releaseVersion = '1.0.0-rc01'
releaseVersionCode = 1_000_000_2_01
minSdkVersion = 16
appTargetSdkVersion = 33
// API version before restricting local file access.
......
......@@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-feature android:name="android.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
......@@ -35,6 +36,7 @@
android:largeHeap="true"
android:allowBackup="false"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:name="androidx.multidex.MultiDexApplication"
tools:targetApi="29">
......
......@@ -19,6 +19,7 @@ import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
......@@ -41,8 +42,10 @@ import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaItem.ClippingConfiguration;
......@@ -76,6 +79,7 @@ public class SampleChooserActivity extends AppCompatActivity
private static final String TAG = "SampleChooserActivity";
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position";
private static final String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position";
private static final int POST_NOTIFICATION_PERMISSION_REQUEST_CODE = 100;
private String[] uris;
private boolean useExtensionRenderers;
......@@ -83,6 +87,8 @@ public class SampleChooserActivity extends AppCompatActivity
private SampleAdapter sampleAdapter;
private MenuItem preferExtensionDecodersMenuItem;
private ExpandableListView sampleListView;
@Nullable private MediaItem downloadMediaItemWaitingForNotificationPermission;
private boolean notificationPermissionToastShown;
@Override
public void onCreate(Bundle savedInstanceState) {
......@@ -172,12 +178,34 @@ public class SampleChooserActivity extends AppCompatActivity
public void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == POST_NOTIFICATION_PERMISSION_REQUEST_CODE) {
handlePostNotificationPermissionGrantResults(grantResults);
} else {
handleExternalStoragePermissionGrantResults(grantResults);
}
}
private void handlePostNotificationPermissionGrantResults(int[] grantResults) {
if (!notificationPermissionToastShown
&& (grantResults.length == 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED)) {
Toast.makeText(
getApplicationContext(), R.string.post_notification_not_granted, Toast.LENGTH_LONG)
.show();
notificationPermissionToastShown = true;
}
if (downloadMediaItemWaitingForNotificationPermission != null) {
// Download with or without permission to post notifications.
toggleDownload(downloadMediaItemWaitingForNotificationPermission);
downloadMediaItemWaitingForNotificationPermission = null;
}
}
private void handleExternalStoragePermissionGrantResults(int[] grantResults) {
if (grantResults.length == 0) {
// Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case.
return;
}
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
} else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadSample();
} else {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
......@@ -244,15 +272,26 @@ public class SampleChooserActivity extends AppCompatActivity
if (downloadUnsupportedStringId != 0) {
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
.show();
} else if (!notificationPermissionToastShown
&& Util.SDK_INT >= 33
&& checkSelfPermission(Api33.getPostNotificationPermissionString())
!= PackageManager.PERMISSION_GRANTED) {
downloadMediaItemWaitingForNotificationPermission = playlistHolder.mediaItems.get(0);
requestPermissions(
new String[] {Api33.getPostNotificationPermissionString()},
/* requestCode= */ POST_NOTIFICATION_PERMISSION_REQUEST_CODE);
} else {
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory(
/* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload(
getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory);
toggleDownload(playlistHolder.mediaItems.get(0));
}
}
private void toggleDownload(MediaItem mediaItem) {
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory(
/* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload(getSupportFragmentManager(), mediaItem, renderersFactory);
}
private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) {
if (playlistHolder.mediaItems.size() > 1) {
return R.string.download_playlist_unsupported;
......@@ -630,4 +669,13 @@ public class SampleChooserActivity extends AppCompatActivity
this.playlists = new ArrayList<>();
}
}
@RequiresApi(33)
private static class Api33 {
@DoNotInline
public static String getPostNotificationPermissionString() {
return Manifest.permission.POST_NOTIFICATIONS;
}
}
}
......@@ -45,6 +45,8 @@
<string name="sample_list_load_error">One or more sample lists failed to load</string>
<string name="post_notification_not_granted">Notifications suppressed. Grant permission to see download notifications.</string>
<string name="download_start_error">Failed to start download</string>
<string name="download_start_error_offline_license">Failed to obtain offline license</string>
......
......@@ -31,7 +31,7 @@ android {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
}
......
......@@ -38,7 +38,7 @@ import com.google.common.util.concurrent.ListenableFuture
class MainActivity : AppCompatActivity() {
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
private val browser: MediaBrowser?
get() = if (browserFuture.isDone) browserFuture.get() else null
get() = if (browserFuture.isDone && !browserFuture.isCancelled) browserFuture.get() else null
private lateinit var mediaListAdapter: FolderMediaItemArrayAdapter
private lateinit var mediaListView: ListView
......
......@@ -20,11 +20,6 @@ import android.net.Uri
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_GENRES
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE
import androidx.media3.common.util.Util
import com.google.common.collect.ImmutableList
import org.json.JSONObject
......@@ -67,7 +62,8 @@ object MediaItemTree {
title: String,
mediaId: String,
isPlayable: Boolean,
@MediaMetadata.FolderType folderType: Int,
isBrowsable: Boolean,
mediaType: @MediaMetadata.MediaType Int,
subtitleConfigurations: List<SubtitleConfiguration> = mutableListOf(),
album: String? = null,
artist: String? = null,
......@@ -81,9 +77,10 @@ object MediaItemTree {
.setTitle(title)
.setArtist(artist)
.setGenre(genre)
.setFolderType(folderType)
.setIsBrowsable(isBrowsable)
.setIsPlayable(isPlayable)
.setArtworkUri(imageUri)
.setMediaType(mediaType)
.build()
return MediaItem.Builder()
......@@ -109,7 +106,8 @@ object MediaItemTree {
title = "Root Folder",
mediaId = ROOT_ID,
isPlayable = false,
folderType = FOLDER_TYPE_MIXED
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
)
)
treeNodes[ALBUM_ID] =
......@@ -118,7 +116,8 @@ object MediaItemTree {
title = "Album Folder",
mediaId = ALBUM_ID,
isPlayable = false,
folderType = FOLDER_TYPE_MIXED
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
)
)
treeNodes[ARTIST_ID] =
......@@ -127,7 +126,8 @@ object MediaItemTree {
title = "Artist Folder",
mediaId = ARTIST_ID,
isPlayable = false,
folderType = FOLDER_TYPE_MIXED
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS
)
)
treeNodes[GENRE_ID] =
......@@ -136,7 +136,8 @@ object MediaItemTree {
title = "Genre Folder",
mediaId = GENRE_ID,
isPlayable = false,
folderType = FOLDER_TYPE_MIXED
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_GENRES
)
)
treeNodes[ROOT_ID]!!.addChild(ALBUM_ID)
......@@ -188,7 +189,8 @@ object MediaItemTree {
title = title,
mediaId = idInTree,
isPlayable = true,
folderType = FOLDER_TYPE_NONE,
isBrowsable = false,
mediaType = MediaMetadata.MEDIA_TYPE_MUSIC,
subtitleConfigurations,
album = album,
artist = artist,
......@@ -207,7 +209,8 @@ object MediaItemTree {
title = album,
mediaId = albumFolderIdInTree,
isPlayable = true,
folderType = FOLDER_TYPE_ALBUMS,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_ALBUM,
subtitleConfigurations
)
)
......@@ -223,7 +226,8 @@ object MediaItemTree {
title = artist,
mediaId = artistFolderIdInTree,
isPlayable = true,
folderType = FOLDER_TYPE_ARTISTS,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_ARTIST,
subtitleConfigurations
)
)
......@@ -239,7 +243,8 @@ object MediaItemTree {
title = genre,
mediaId = genreFolderIdInTree,
isPlayable = true,
folderType = FOLDER_TYPE_GENRES,
isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_GENRE,
subtitleConfigurations
)
)
......@@ -262,7 +267,7 @@ object MediaItemTree {
fun getRandomItem(): MediaItem {
var curRoot = getRootItem()
while (curRoot.mediaMetadata.folderType != FOLDER_TYPE_NONE) {
while (curRoot.mediaMetadata.isBrowsable == true) {
val children = getChildren(curRoot.mediaId)!!
curRoot = children.random()
}
......
......@@ -15,22 +15,21 @@
*/
package androidx.media3.demo.session
import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent.*
import android.app.TaskStackBuilder
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media3.common.AudioAttributes
import androidx.media3.common.MediaItem
import androidx.media3.common.util.Util
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
......@@ -51,6 +50,8 @@ class PlaybackService : MediaLibraryService() {
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_OFF"
private const val NOTIFICATION_ID = 123
private const val CHANNEL_ID = "demo_session_notification_channel_id"
}
override fun onCreate() {
......@@ -66,15 +67,23 @@ class PlaybackService : MediaLibraryService() {
)
customLayout = ImmutableList.of(customCommands[0])
initializeSessionAndPlayer()
setListener(MediaSessionServiceListener())
}
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
override fun onTaskRemoved(rootIntent: Intent?) {
if (!player.playWhenReady) {
stopSelf()
}
}
override fun onDestroy() {
player.release()
mediaLibrarySession.release()
clearListener()
super.onDestroy()
}
......@@ -253,4 +262,49 @@ class PlaybackService : MediaLibraryService() {
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */
}
private inner class MediaSessionServiceListener : Listener {
/**
* This method is only required to be implemented on Android 12 or above when an attempt is made
* by a media controller to resume playback when the {@link MediaSessionService} is in the
* background.
*/
override fun onForegroundServiceStartNotAllowedException() {
val notificationManagerCompat = NotificationManagerCompat.from(this@PlaybackService)
ensureNotificationChannel(notificationManagerCompat)
val pendingIntent =
TaskStackBuilder.create(this@PlaybackService).run {
addNextIntent(Intent(this@PlaybackService, MainActivity::class.java))
val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
}
val builder =
NotificationCompat.Builder(this@PlaybackService, CHANNEL_ID)
.setContentIntent(pendingIntent)
.setSmallIcon(R.drawable.media3_notification_small_icon)
.setContentTitle(getString(R.string.notification_content_title))
.setStyle(
NotificationCompat.BigTextStyle().bigText(getString(R.string.notification_content_text))
)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setAutoCancel(true)
notificationManagerCompat.notify(NOTIFICATION_ID, builder.build())
}
}
private fun ensureNotificationChannel(notificationManagerCompat: NotificationManagerCompat) {
if (Util.SDK_INT < 26 || notificationManagerCompat.getNotificationChannel(CHANNEL_ID) != null) {
return
}
val channel =
NotificationChannel(
CHANNEL_ID,
getString(R.string.notification_channel_name),
NotificationManager.IMPORTANCE_DEFAULT
)
notificationManagerCompat.createNotificationChannel(channel)
}
}
......@@ -24,4 +24,9 @@
<string name="no_item_prompt">
"! No media in the play list !\nPlease try to add more from browser"
</string>
<string name="notification_content_title">Playback cannot be resumed</string>
<string name="notification_content_text">Press on the play button on the media notification if it
is still present, otherwise please open the app to start the playback and re-connect the session
to the controller</string>
<string name="notification_channel_name">Playback cannot be resumed</string>
</resources>
......@@ -27,3 +27,9 @@ Create a `CastPlayer` and use it to control a Cast receiver app. Since
`CastPlayer` implements the `Player` interface, it can be passed to all media
components that accept a `Player`, including the UI components provided by the
UI module.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -14,7 +14,7 @@
apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle"
dependencies {
api 'com.google.android.gms:play-services-cast-framework:21.0.1'
api 'com.google.android.gms:play-services-cast-framework:21.2.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'lib-common')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
......
......@@ -15,6 +15,7 @@
*/
package androidx.media3.cast;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.min;
......@@ -43,7 +44,6 @@ import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Log;
......@@ -295,7 +295,7 @@ public final class CastPlayer extends BasePlayer {
@Override
public void addMediaItems(int index, List<MediaItem> mediaItems) {
Assertions.checkArgument(index >= 0);
checkArgument(index >= 0);
int uid = MediaQueueItem.INVALID_ITEM_ID;
if (index < currentTimeline.getWindowCount()) {
uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
......@@ -305,14 +305,11 @@ public final class CastPlayer extends BasePlayer {
@Override
public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
Assertions.checkArgument(
fromIndex >= 0
&& fromIndex <= toIndex
&& toIndex <= currentTimeline.getWindowCount()
&& newIndex >= 0
&& newIndex < currentTimeline.getWindowCount());
newIndex = min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex));
if (fromIndex == toIndex || fromIndex == newIndex) {
checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newIndex >= 0);
int playlistSize = currentTimeline.getWindowCount();
toIndex = min(toIndex, playlistSize);
newIndex = min(newIndex, playlistSize - (toIndex - fromIndex));
if (fromIndex >= playlistSize || fromIndex == toIndex || fromIndex == newIndex) {
// Do nothing.
return;
}
......@@ -325,9 +322,10 @@ public final class CastPlayer extends BasePlayer {
@Override
public void removeMediaItems(int fromIndex, int toIndex) {
Assertions.checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
toIndex = min(toIndex, currentTimeline.getWindowCount());
if (fromIndex == toIndex) {
checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
int playlistSize = currentTimeline.getWindowCount();
toIndex = min(toIndex, playlistSize);
if (fromIndex >= playlistSize || fromIndex == toIndex) {
// Do nothing.
return;
}
......@@ -399,7 +397,16 @@ public final class CastPlayer extends BasePlayer {
// don't implement onPositionDiscontinuity().
@SuppressWarnings("deprecation")
@Override
public void seekTo(int mediaItemIndex, long positionMs) {
@VisibleForTesting(otherwise = PROTECTED)
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
checkArgument(mediaItemIndex >= 0);
if (!currentTimeline.isEmpty() && mediaItemIndex >= currentTimeline.getWindowCount()) {
return;
}
MediaStatus mediaStatus = getMediaStatus();
// We assume the default position is 0. There is no support for seeking to the default position
// in RemoteMediaClient.
......
......@@ -26,10 +26,6 @@ import com.google.android.gms.cast.MediaTrack;
/** Utility methods for Cast integration. */
/* package */ final class CastUtils {
/** The duration returned by {@link MediaInfo#getStreamDuration()} for live streams. */
// TODO: Remove once [Internal ref: b/171657375] is fixed.
private static final long LIVE_STREAM_DURATION = -1000;
/**
* Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
* unknown or not applicable.
......@@ -42,9 +38,7 @@ import com.google.android.gms.cast.MediaTrack;
return C.TIME_UNSET;
}
long durationMs = mediaInfo.getStreamDuration();
return durationMs != MediaInfo.UNKNOWN_DURATION && durationMs != LIVE_STREAM_DURATION
? Util.msToUs(durationMs)
: C.TIME_UNSET;
return durationMs != MediaInfo.UNKNOWN_DURATION ? Util.msToUs(durationMs) : C.TIME_UNSET;
}
/**
......
......@@ -2,3 +2,9 @@
Provides common code and utilities used by other media modules. Application code
will not normally need to depend on this module directly.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -459,44 +459,29 @@ public final class AdPlaybackState implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_TIME_US,
FIELD_COUNT,
FIELD_URIS,
FIELD_STATES,
FIELD_DURATIONS_US,
FIELD_CONTENT_RESUME_OFFSET_US,
FIELD_IS_SERVER_SIDE_INSERTED,
FIELD_ORIGINAL_COUNT
})
private @interface FieldNumber {}
private static final int FIELD_TIME_US = 0;
private static final int FIELD_COUNT = 1;
private static final int FIELD_URIS = 2;
private static final int FIELD_STATES = 3;
private static final int FIELD_DURATIONS_US = 4;
private static final int FIELD_CONTENT_RESUME_OFFSET_US = 5;
private static final int FIELD_IS_SERVER_SIDE_INSERTED = 6;
private static final int FIELD_ORIGINAL_COUNT = 7;
private static final String FIELD_TIME_US = Util.intToStringMaxRadix(0);
private static final String FIELD_COUNT = Util.intToStringMaxRadix(1);
private static final String FIELD_URIS = Util.intToStringMaxRadix(2);
private static final String FIELD_STATES = Util.intToStringMaxRadix(3);
private static final String FIELD_DURATIONS_US = Util.intToStringMaxRadix(4);
private static final String FIELD_CONTENT_RESUME_OFFSET_US = Util.intToStringMaxRadix(5);
private static final String FIELD_IS_SERVER_SIDE_INSERTED = Util.intToStringMaxRadix(6);
private static final String FIELD_ORIGINAL_COUNT = Util.intToStringMaxRadix(7);
// putParcelableArrayList actually supports null elements.
@SuppressWarnings("nullness:argument")
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putLong(keyForField(FIELD_TIME_US), timeUs);
bundle.putInt(keyForField(FIELD_COUNT), count);
bundle.putInt(keyForField(FIELD_ORIGINAL_COUNT), originalCount);
bundle.putLong(FIELD_TIME_US, timeUs);
bundle.putInt(FIELD_COUNT, count);
bundle.putInt(FIELD_ORIGINAL_COUNT, originalCount);
bundle.putParcelableArrayList(
keyForField(FIELD_URIS), new ArrayList<@NullableType Uri>(Arrays.asList(uris)));
bundle.putIntArray(keyForField(FIELD_STATES), states);
bundle.putLongArray(keyForField(FIELD_DURATIONS_US), durationsUs);
bundle.putLong(keyForField(FIELD_CONTENT_RESUME_OFFSET_US), contentResumeOffsetUs);
bundle.putBoolean(keyForField(FIELD_IS_SERVER_SIDE_INSERTED), isServerSideInserted);
FIELD_URIS, new ArrayList<@NullableType Uri>(Arrays.asList(uris)));
bundle.putIntArray(FIELD_STATES, states);
bundle.putLongArray(FIELD_DURATIONS_US, durationsUs);
bundle.putLong(FIELD_CONTENT_RESUME_OFFSET_US, contentResumeOffsetUs);
bundle.putBoolean(FIELD_IS_SERVER_SIDE_INSERTED, isServerSideInserted);
return bundle;
}
......@@ -506,18 +491,16 @@ public final class AdPlaybackState implements Bundleable {
// getParcelableArrayList may have null elements.
@SuppressWarnings("nullness:type.argument")
private static AdGroup fromBundle(Bundle bundle) {
long timeUs = bundle.getLong(keyForField(FIELD_TIME_US));
int count = bundle.getInt(keyForField(FIELD_COUNT), /* defaultValue= */ C.LENGTH_UNSET);
int originalCount =
bundle.getInt(keyForField(FIELD_ORIGINAL_COUNT), /* defaultValue= */ C.LENGTH_UNSET);
@Nullable
ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(keyForField(FIELD_URIS));
long timeUs = bundle.getLong(FIELD_TIME_US);
int count = bundle.getInt(FIELD_COUNT);
int originalCount = bundle.getInt(FIELD_ORIGINAL_COUNT);
@Nullable ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(FIELD_URIS);
@Nullable
@AdState
int[] states = bundle.getIntArray(keyForField(FIELD_STATES));
@Nullable long[] durationsUs = bundle.getLongArray(keyForField(FIELD_DURATIONS_US));
long contentResumeOffsetUs = bundle.getLong(keyForField(FIELD_CONTENT_RESUME_OFFSET_US));
boolean isServerSideInserted = bundle.getBoolean(keyForField(FIELD_IS_SERVER_SIDE_INSERTED));
int[] states = bundle.getIntArray(FIELD_STATES);
@Nullable long[] durationsUs = bundle.getLongArray(FIELD_DURATIONS_US);
long contentResumeOffsetUs = bundle.getLong(FIELD_CONTENT_RESUME_OFFSET_US);
boolean isServerSideInserted = bundle.getBoolean(FIELD_IS_SERVER_SIDE_INSERTED);
return new AdGroup(
timeUs,
count,
......@@ -528,10 +511,6 @@ public final class AdPlaybackState implements Bundleable {
contentResumeOffsetUs,
isServerSideInserted);
}
private static String keyForField(@AdGroup.FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
/**
......@@ -1122,21 +1101,10 @@ public final class AdPlaybackState implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_AD_GROUPS,
FIELD_AD_RESUME_POSITION_US,
FIELD_CONTENT_DURATION_US,
FIELD_REMOVED_AD_GROUP_COUNT
})
private @interface FieldNumber {}
private static final int FIELD_AD_GROUPS = 1;
private static final int FIELD_AD_RESUME_POSITION_US = 2;
private static final int FIELD_CONTENT_DURATION_US = 3;
private static final int FIELD_REMOVED_AD_GROUP_COUNT = 4;
private static final String FIELD_AD_GROUPS = Util.intToStringMaxRadix(1);
private static final String FIELD_AD_RESUME_POSITION_US = Util.intToStringMaxRadix(2);
private static final String FIELD_CONTENT_DURATION_US = Util.intToStringMaxRadix(3);
private static final String FIELD_REMOVED_AD_GROUP_COUNT = Util.intToStringMaxRadix(4);
/**
* {@inheritDoc}
......@@ -1152,10 +1120,18 @@ public final class AdPlaybackState implements Bundleable {
for (AdGroup adGroup : adGroups) {
adGroupBundleList.add(adGroup.toBundle());
}
bundle.putParcelableArrayList(keyForField(FIELD_AD_GROUPS), adGroupBundleList);
bundle.putLong(keyForField(FIELD_AD_RESUME_POSITION_US), adResumePositionUs);
bundle.putLong(keyForField(FIELD_CONTENT_DURATION_US), contentDurationUs);
bundle.putInt(keyForField(FIELD_REMOVED_AD_GROUP_COUNT), removedAdGroupCount);
if (!adGroupBundleList.isEmpty()) {
bundle.putParcelableArrayList(FIELD_AD_GROUPS, adGroupBundleList);
}
if (adResumePositionUs != NONE.adResumePositionUs) {
bundle.putLong(FIELD_AD_RESUME_POSITION_US, adResumePositionUs);
}
if (contentDurationUs != NONE.contentDurationUs) {
bundle.putLong(FIELD_CONTENT_DURATION_US, contentDurationUs);
}
if (removedAdGroupCount != NONE.removedAdGroupCount) {
bundle.putInt(FIELD_REMOVED_AD_GROUP_COUNT, removedAdGroupCount);
}
return bundle;
}
......@@ -1167,9 +1143,7 @@ public final class AdPlaybackState implements Bundleable {
public static final Bundleable.Creator<AdPlaybackState> CREATOR = AdPlaybackState::fromBundle;
private static AdPlaybackState fromBundle(Bundle bundle) {
@Nullable
ArrayList<Bundle> adGroupBundleList =
bundle.getParcelableArrayList(keyForField(FIELD_AD_GROUPS));
@Nullable ArrayList<Bundle> adGroupBundleList = bundle.getParcelableArrayList(FIELD_AD_GROUPS);
@Nullable AdGroup[] adGroups;
if (adGroupBundleList == null) {
adGroups = new AdGroup[0];
......@@ -1180,18 +1154,15 @@ public final class AdPlaybackState implements Bundleable {
}
}
long adResumePositionUs =
bundle.getLong(keyForField(FIELD_AD_RESUME_POSITION_US), /* defaultValue= */ 0);
bundle.getLong(FIELD_AD_RESUME_POSITION_US, /* defaultValue= */ NONE.adResumePositionUs);
long contentDurationUs =
bundle.getLong(keyForField(FIELD_CONTENT_DURATION_US), /* defaultValue= */ C.TIME_UNSET);
int removedAdGroupCount = bundle.getInt(keyForField(FIELD_REMOVED_AD_GROUP_COUNT));
bundle.getLong(FIELD_CONTENT_DURATION_US, /* defaultValue= */ NONE.contentDurationUs);
int removedAdGroupCount =
bundle.getInt(FIELD_REMOVED_AD_GROUP_COUNT, /* defaultValue= */ NONE.removedAdGroupCount);
return new AdPlaybackState(
/* adsId= */ null, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
private static AdGroup[] createEmptyAdGroups(long[] adGroupTimesUs) {
AdGroup[] adGroups = new AdGroup[adGroupTimesUs.length];
for (int i = 0; i < adGroups.length; i++) {
......
......@@ -15,20 +15,13 @@
*/
package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Attributes for audio playback, which configure the underlying platform {@link
......@@ -205,33 +198,21 @@ public final class AudioAttributes implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_CONTENT_TYPE,
FIELD_FLAGS,
FIELD_USAGE,
FIELD_ALLOWED_CAPTURE_POLICY,
FIELD_SPATIALIZATION_BEHAVIOR
})
private @interface FieldNumber {}
private static final int FIELD_CONTENT_TYPE = 0;
private static final int FIELD_FLAGS = 1;
private static final int FIELD_USAGE = 2;
private static final int FIELD_ALLOWED_CAPTURE_POLICY = 3;
private static final int FIELD_SPATIALIZATION_BEHAVIOR = 4;
private static final String FIELD_CONTENT_TYPE = Util.intToStringMaxRadix(0);
private static final String FIELD_FLAGS = Util.intToStringMaxRadix(1);
private static final String FIELD_USAGE = Util.intToStringMaxRadix(2);
private static final String FIELD_ALLOWED_CAPTURE_POLICY = Util.intToStringMaxRadix(3);
private static final String FIELD_SPATIALIZATION_BEHAVIOR = Util.intToStringMaxRadix(4);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_CONTENT_TYPE), contentType);
bundle.putInt(keyForField(FIELD_FLAGS), flags);
bundle.putInt(keyForField(FIELD_USAGE), usage);
bundle.putInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY), allowedCapturePolicy);
bundle.putInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR), spatializationBehavior);
bundle.putInt(FIELD_CONTENT_TYPE, contentType);
bundle.putInt(FIELD_FLAGS, flags);
bundle.putInt(FIELD_USAGE, usage);
bundle.putInt(FIELD_ALLOWED_CAPTURE_POLICY, allowedCapturePolicy);
bundle.putInt(FIELD_SPATIALIZATION_BEHAVIOR, spatializationBehavior);
return bundle;
}
......@@ -240,29 +221,24 @@ public final class AudioAttributes implements Bundleable {
public static final Creator<AudioAttributes> CREATOR =
bundle -> {
Builder builder = new Builder();
if (bundle.containsKey(keyForField(FIELD_CONTENT_TYPE))) {
builder.setContentType(bundle.getInt(keyForField(FIELD_CONTENT_TYPE)));
if (bundle.containsKey(FIELD_CONTENT_TYPE)) {
builder.setContentType(bundle.getInt(FIELD_CONTENT_TYPE));
}
if (bundle.containsKey(keyForField(FIELD_FLAGS))) {
builder.setFlags(bundle.getInt(keyForField(FIELD_FLAGS)));
if (bundle.containsKey(FIELD_FLAGS)) {
builder.setFlags(bundle.getInt(FIELD_FLAGS));
}
if (bundle.containsKey(keyForField(FIELD_USAGE))) {
builder.setUsage(bundle.getInt(keyForField(FIELD_USAGE)));
if (bundle.containsKey(FIELD_USAGE)) {
builder.setUsage(bundle.getInt(FIELD_USAGE));
}
if (bundle.containsKey(keyForField(FIELD_ALLOWED_CAPTURE_POLICY))) {
builder.setAllowedCapturePolicy(bundle.getInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY)));
if (bundle.containsKey(FIELD_ALLOWED_CAPTURE_POLICY)) {
builder.setAllowedCapturePolicy(bundle.getInt(FIELD_ALLOWED_CAPTURE_POLICY));
}
if (bundle.containsKey(keyForField(FIELD_SPATIALIZATION_BEHAVIOR))) {
builder.setSpatializationBehavior(
bundle.getInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR)));
if (bundle.containsKey(FIELD_SPATIALIZATION_BEHAVIOR)) {
builder.setSpatializationBehavior(bundle.getInt(FIELD_SPATIALIZATION_BEHAVIOR));
}
return builder.build();
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
@RequiresApi(29)
private static final class Api29 {
@DoNotInline
......
......@@ -15,14 +15,15 @@
*/
package androidx.media3.common;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static java.lang.Math.max;
import static java.lang.Math.min;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.ForOverride;
import java.util.List;
/** Abstract base {@link Player} which implements common implementation independent methods. */
......@@ -121,27 +122,23 @@ public abstract class BasePlayer implements Player {
@Override
public final void seekToDefaultPosition() {
seekToDefaultPosition(getCurrentMediaItemIndex());
seekToDefaultPositionInternal(
getCurrentMediaItemIndex(), Player.COMMAND_SEEK_TO_DEFAULT_POSITION);
}
@Override
public final void seekToDefaultPosition(int mediaItemIndex) {
seekTo(mediaItemIndex, /* positionMs= */ C.TIME_UNSET);
}
@Override
public final void seekTo(long positionMs) {
seekTo(getCurrentMediaItemIndex(), positionMs);
seekToDefaultPositionInternal(mediaItemIndex, Player.COMMAND_SEEK_TO_MEDIA_ITEM);
}
@Override
public final void seekBack() {
seekToOffset(-getSeekBackIncrement());
seekToOffset(-getSeekBackIncrement(), Player.COMMAND_SEEK_BACK);
}
@Override
public final void seekForward() {
seekToOffset(getSeekForwardIncrement());
seekToOffset(getSeekForwardIncrement(), Player.COMMAND_SEEK_FORWARD);
}
/**
......@@ -187,15 +184,7 @@ public abstract class BasePlayer implements Player {
@Override
public final void seekToPreviousMediaItem() {
int previousMediaItemIndex = getPreviousMediaItemIndex();
if (previousMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (previousMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem();
} else {
seekToDefaultPosition(previousMediaItemIndex);
}
seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
}
@Override
......@@ -207,12 +196,12 @@ public abstract class BasePlayer implements Player {
boolean hasPreviousMediaItem = hasPreviousMediaItem();
if (isCurrentMediaItemLive() && !isCurrentMediaItemSeekable()) {
if (hasPreviousMediaItem) {
seekToPreviousMediaItem();
seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS);
}
} else if (hasPreviousMediaItem && getCurrentPosition() <= getMaxSeekToPreviousPosition()) {
seekToPreviousMediaItem();
seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS);
} else {
seekTo(/* positionMs= */ 0);
seekToCurrentItem(/* positionMs= */ 0, Player.COMMAND_SEEK_TO_PREVIOUS);
}
}
......@@ -259,15 +248,7 @@ public abstract class BasePlayer implements Player {
@Override
public final void seekToNextMediaItem() {
int nextMediaItemIndex = getNextMediaItemIndex();
if (nextMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (nextMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem();
} else {
seekToDefaultPosition(nextMediaItemIndex);
}
seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
}
@Override
......@@ -277,13 +258,43 @@ public abstract class BasePlayer implements Player {
return;
}
if (hasNextMediaItem()) {
seekToNextMediaItem();
seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT);
} else if (isCurrentMediaItemLive() && isCurrentMediaItemDynamic()) {
seekToDefaultPosition();
seekToDefaultPositionInternal(getCurrentMediaItemIndex(), Player.COMMAND_SEEK_TO_NEXT);
}
}
@Override
public final void seekTo(long positionMs) {
seekToCurrentItem(positionMs, Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM);
}
@Override
public final void seekTo(int mediaItemIndex, long positionMs) {
seekTo(
mediaItemIndex,
positionMs,
Player.COMMAND_SEEK_TO_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
/**
* Seeks to a position in the specified {@link MediaItem}.
*
* @param mediaItemIndex The index of the {@link MediaItem}.
* @param positionMs The seek position in the specified {@link MediaItem} in milliseconds, or
* {@link C#TIME_UNSET} to seek to the media item's default position.
* @param seekCommand The {@link Player.Command} used to trigger the seek.
* @param isRepeatingCurrentItem Whether this seeks repeats the current item.
*/
@VisibleForTesting(otherwise = PROTECTED)
public abstract void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem);
@Override
public final void setPlaybackSpeed(float speed) {
setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
}
......@@ -437,29 +448,63 @@ public abstract class BasePlayer implements Player {
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
}
/**
* Repeat the current media item.
*
* <p>The default implementation seeks to the default position in the current item, which can be
* overridden for additional handling.
*/
@ForOverride
protected void repeatCurrentMediaItem() {
seekToDefaultPosition();
}
private @RepeatMode int getRepeatModeForNavigation() {
@RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;
}
private void seekToOffset(long offsetMs) {
private void seekToCurrentItem(long positionMs, @Player.Command int seekCommand) {
seekTo(
getCurrentMediaItemIndex(), positionMs, seekCommand, /* isRepeatingCurrentItem= */ false);
}
private void seekToOffset(long offsetMs, @Player.Command int seekCommand) {
long positionMs = getCurrentPosition() + offsetMs;
long durationMs = getDuration();
if (durationMs != C.TIME_UNSET) {
positionMs = min(positionMs, durationMs);
}
positionMs = max(positionMs, 0);
seekTo(positionMs);
seekToCurrentItem(positionMs, seekCommand);
}
private void seekToDefaultPositionInternal(int mediaItemIndex, @Player.Command int seekCommand) {
seekTo(
mediaItemIndex,
/* positionMs= */ C.TIME_UNSET,
seekCommand,
/* isRepeatingCurrentItem= */ false);
}
private void seekToNextMediaItemInternal(@Player.Command int seekCommand) {
int nextMediaItemIndex = getNextMediaItemIndex();
if (nextMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (nextMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem(seekCommand);
} else {
seekToDefaultPositionInternal(nextMediaItemIndex, seekCommand);
}
}
private void seekToPreviousMediaItemInternal(@Player.Command int seekCommand) {
int previousMediaItemIndex = getPreviousMediaItemIndex();
if (previousMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (previousMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem(seekCommand);
} else {
seekToDefaultPositionInternal(previousMediaItemIndex, seekCommand);
}
}
private void repeatCurrentMediaItem(@Player.Command int seekCommand) {
seekTo(
getCurrentMediaItemIndex(),
/* positionMs= */ C.TIME_UNSET,
seekCommand,
/* isRepeatingCurrentItem= */ true);
}
}
......@@ -196,7 +196,7 @@ public final class C {
* #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
* {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link
* #ENCODING_E_AC3}, {@link #ENCODING_E_AC3_JOC}, {@link #ENCODING_AC4}, {@link #ENCODING_DTS},
* {@link #ENCODING_DTS_HD} or {@link #ENCODING_DOLBY_TRUEHD}.
* {@link #ENCODING_DTS_HD}, {@link #ENCODING_DOLBY_TRUEHD} or {@link #ENCODING_OPUS}.
*/
@UnstableApi
@Documented
......@@ -224,7 +224,8 @@ public final class C {
ENCODING_AC4,
ENCODING_DTS,
ENCODING_DTS_HD,
ENCODING_DOLBY_TRUEHD
ENCODING_DOLBY_TRUEHD,
ENCODING_OPUS,
})
public @interface Encoding {}
......@@ -325,6 +326,10 @@ public final class C {
* @see AudioFormat#ENCODING_DOLBY_TRUEHD
*/
@UnstableApi public static final int ENCODING_DOLBY_TRUEHD = AudioFormat.ENCODING_DOLBY_TRUEHD;
/**
* @see AudioFormat#ENCODING_OPUS
*/
@UnstableApi public static final int ENCODING_OPUS = AudioFormat.ENCODING_OPUS;
/** Represents the behavior affecting whether spatialization will be used. */
@Documented
......
......@@ -15,16 +15,10 @@
*/
package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import androidx.media3.common.util.Util;
import java.util.Arrays;
import org.checkerframework.dataflow.qual.Pure;
......@@ -183,41 +177,26 @@ public final class ColorInfo implements Bundleable {
// Bundleable implementation
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_COLOR_SPACE,
FIELD_COLOR_RANGE,
FIELD_COLOR_TRANSFER,
FIELD_HDR_STATIC_INFO,
})
private @interface FieldNumber {}
private static final int FIELD_COLOR_SPACE = 0;
private static final int FIELD_COLOR_RANGE = 1;
private static final int FIELD_COLOR_TRANSFER = 2;
private static final int FIELD_HDR_STATIC_INFO = 3;
private static final String FIELD_COLOR_SPACE = Util.intToStringMaxRadix(0);
private static final String FIELD_COLOR_RANGE = Util.intToStringMaxRadix(1);
private static final String FIELD_COLOR_TRANSFER = Util.intToStringMaxRadix(2);
private static final String FIELD_HDR_STATIC_INFO = Util.intToStringMaxRadix(3);
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_COLOR_SPACE), colorSpace);
bundle.putInt(keyForField(FIELD_COLOR_RANGE), colorRange);
bundle.putInt(keyForField(FIELD_COLOR_TRANSFER), colorTransfer);
bundle.putByteArray(keyForField(FIELD_HDR_STATIC_INFO), hdrStaticInfo);
bundle.putInt(FIELD_COLOR_SPACE, colorSpace);
bundle.putInt(FIELD_COLOR_RANGE, colorRange);
bundle.putInt(FIELD_COLOR_TRANSFER, colorTransfer);
bundle.putByteArray(FIELD_HDR_STATIC_INFO, hdrStaticInfo);
return bundle;
}
public static final Creator<ColorInfo> CREATOR =
bundle ->
new ColorInfo(
bundle.getInt(keyForField(FIELD_COLOR_SPACE), Format.NO_VALUE),
bundle.getInt(keyForField(FIELD_COLOR_RANGE), Format.NO_VALUE),
bundle.getInt(keyForField(FIELD_COLOR_TRANSFER), Format.NO_VALUE),
bundle.getByteArray(keyForField(FIELD_HDR_STATIC_INFO)));
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
bundle.getInt(FIELD_COLOR_SPACE, Format.NO_VALUE),
bundle.getInt(FIELD_COLOR_RANGE, Format.NO_VALUE),
bundle.getInt(FIELD_COLOR_TRANSFER, Format.NO_VALUE),
bundle.getByteArray(FIELD_HDR_STATIC_INFO));
}
......@@ -21,6 +21,7 @@ import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
......@@ -87,23 +88,17 @@ public final class DeviceInfo implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_PLAYBACK_TYPE, FIELD_MIN_VOLUME, FIELD_MAX_VOLUME})
private @interface FieldNumber {}
private static final int FIELD_PLAYBACK_TYPE = 0;
private static final int FIELD_MIN_VOLUME = 1;
private static final int FIELD_MAX_VOLUME = 2;
private static final String FIELD_PLAYBACK_TYPE = Util.intToStringMaxRadix(0);
private static final String FIELD_MIN_VOLUME = Util.intToStringMaxRadix(1);
private static final String FIELD_MAX_VOLUME = Util.intToStringMaxRadix(2);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_PLAYBACK_TYPE), playbackType);
bundle.putInt(keyForField(FIELD_MIN_VOLUME), minVolume);
bundle.putInt(keyForField(FIELD_MAX_VOLUME), maxVolume);
bundle.putInt(FIELD_PLAYBACK_TYPE, playbackType);
bundle.putInt(FIELD_MIN_VOLUME, minVolume);
bundle.putInt(FIELD_MAX_VOLUME, maxVolume);
return bundle;
}
......@@ -112,14 +107,9 @@ public final class DeviceInfo implements Bundleable {
public static final Creator<DeviceInfo> CREATOR =
bundle -> {
int playbackType =
bundle.getInt(
keyForField(FIELD_PLAYBACK_TYPE), /* defaultValue= */ PLAYBACK_TYPE_LOCAL);
int minVolume = bundle.getInt(keyForField(FIELD_MIN_VOLUME), /* defaultValue= */ 0);
int maxVolume = bundle.getInt(keyForField(FIELD_MAX_VOLUME), /* defaultValue= */ 0);
bundle.getInt(FIELD_PLAYBACK_TYPE, /* defaultValue= */ PLAYBACK_TYPE_LOCAL);
int minVolume = bundle.getInt(FIELD_MIN_VOLUME, /* defaultValue= */ 0);
int maxVolume = bundle.getInt(FIELD_MAX_VOLUME, /* defaultValue= */ 0);
return new DeviceInfo(playbackType, minVolume, maxVolume);
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -16,17 +16,12 @@
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* A rating expressed as "heart" or "no heart". It can be used to indicate whether the content is a
......@@ -81,22 +76,16 @@ public final class HeartRating extends Rating {
private static final @RatingType int TYPE = RATING_TYPE_HEART;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_RATING_TYPE, FIELD_RATED, FIELD_IS_HEART})
private @interface FieldNumber {}
private static final int FIELD_RATED = 1;
private static final int FIELD_IS_HEART = 2;
private static final String FIELD_RATED = Util.intToStringMaxRadix(1);
private static final String FIELD_IS_HEART = Util.intToStringMaxRadix(2);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE);
bundle.putBoolean(keyForField(FIELD_RATED), rated);
bundle.putBoolean(keyForField(FIELD_IS_HEART), isHeart);
bundle.putInt(FIELD_RATING_TYPE, TYPE);
bundle.putBoolean(FIELD_RATED, rated);
bundle.putBoolean(FIELD_IS_HEART, isHeart);
return bundle;
}
......@@ -104,16 +93,10 @@ public final class HeartRating extends Rating {
@UnstableApi public static final Creator<HeartRating> CREATOR = HeartRating::fromBundle;
private static HeartRating fromBundle(Bundle bundle) {
checkArgument(
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET)
== TYPE);
boolean isRated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false);
checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE);
boolean isRated = bundle.getBoolean(FIELD_RATED, /* defaultValue= */ false);
return isRated
? new HeartRating(bundle.getBoolean(keyForField(FIELD_IS_HEART), /* defaultValue= */ false))
? new HeartRating(bundle.getBoolean(FIELD_IS_HEART, /* defaultValue= */ false))
: new HeartRating();
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -29,11 +29,11 @@ public final class MediaLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "1.0.0-beta03";
public static final String VERSION = "1.0.0-rc01";
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta03";
public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-rc01";
/**
* The version of the library expressed as an integer, for example 1002003300.
......@@ -47,7 +47,7 @@ public final class MediaLibraryInfo {
* (123-045-006-3-00).
*/
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 1_000_000_1_03;
public static final int VERSION_INT = 1_000_000_2_01;
/** Whether the library was compiled with {@link Assertions} checks enabled. */
public static final boolean ASSERTIONS_ENABLED = true;
......
......@@ -50,11 +50,8 @@ public final class Metadata implements Parcelable {
}
/**
* Updates the {@link MediaMetadata.Builder} with the type specific values stored in this Entry.
*
* <p>The order of the {@link Entry} objects in the {@link Metadata} matters. If two {@link
* Entry} entries attempt to populate the same {@link MediaMetadata} field, then the last one in
* the list is used.
* Updates the {@link MediaMetadata.Builder} with the type-specific values stored in this {@code
* Entry}.
*
* @param builder The builder to be updated.
*/
......
......@@ -587,6 +587,8 @@ public final class MimeTypes {
return C.ENCODING_DTS_HD;
case MimeTypes.AUDIO_TRUEHD:
return C.ENCODING_DOLBY_TRUEHD;
case MimeTypes.AUDIO_OPUS:
return C.ENCODING_OPUS;
default:
return C.ENCODING_INVALID;
}
......
......@@ -16,18 +16,13 @@
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** A rating expressed as a percentage. */
public final class PercentageRating extends Rating {
......@@ -79,20 +74,14 @@ public final class PercentageRating extends Rating {
private static final @RatingType int TYPE = RATING_TYPE_PERCENTAGE;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_RATING_TYPE, FIELD_PERCENT})
private @interface FieldNumber {}
private static final int FIELD_PERCENT = 1;
private static final String FIELD_PERCENT = Util.intToStringMaxRadix(1);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE);
bundle.putFloat(keyForField(FIELD_PERCENT), percent);
bundle.putInt(FIELD_RATING_TYPE, TYPE);
bundle.putFloat(FIELD_PERCENT, percent);
return bundle;
}
......@@ -100,14 +89,8 @@ public final class PercentageRating extends Rating {
@UnstableApi public static final Creator<PercentageRating> CREATOR = PercentageRating::fromBundle;
private static PercentageRating fromBundle(Bundle bundle) {
checkArgument(
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET)
== TYPE);
float percent = bundle.getFloat(keyForField(FIELD_PERCENT), /* defaultValue= */ RATING_UNSET);
checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE);
float percent = bundle.getFloat(FIELD_PERCENT, /* defaultValue= */ RATING_UNSET);
return percent == RATING_UNSET ? new PercentageRating() : new PercentageRating(percent);
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -346,13 +346,12 @@ public class PlaybackException extends Exception implements Bundleable {
@UnstableApi
protected PlaybackException(Bundle bundle) {
this(
/* message= */ bundle.getString(keyForField(FIELD_STRING_MESSAGE)),
/* message= */ bundle.getString(FIELD_STRING_MESSAGE),
/* cause= */ getCauseFromBundle(bundle),
/* errorCode= */ bundle.getInt(
keyForField(FIELD_INT_ERROR_CODE), /* defaultValue= */ ERROR_CODE_UNSPECIFIED),
FIELD_INT_ERROR_CODE, /* defaultValue= */ ERROR_CODE_UNSPECIFIED),
/* timestampMs= */ bundle.getLong(
keyForField(FIELD_LONG_TIMESTAMP_MS),
/* defaultValue= */ SystemClock.elapsedRealtime()));
FIELD_LONG_TIMESTAMP_MS, /* defaultValue= */ SystemClock.elapsedRealtime()));
}
/** Creates a new instance using the given values. */
......@@ -401,18 +400,18 @@ public class PlaybackException extends Exception implements Bundleable {
// Bundleable implementation.
private static final int FIELD_INT_ERROR_CODE = 0;
private static final int FIELD_LONG_TIMESTAMP_MS = 1;
private static final int FIELD_STRING_MESSAGE = 2;
private static final int FIELD_STRING_CAUSE_CLASS_NAME = 3;
private static final int FIELD_STRING_CAUSE_MESSAGE = 4;
private static final String FIELD_INT_ERROR_CODE = Util.intToStringMaxRadix(0);
private static final String FIELD_LONG_TIMESTAMP_MS = Util.intToStringMaxRadix(1);
private static final String FIELD_STRING_MESSAGE = Util.intToStringMaxRadix(2);
private static final String FIELD_STRING_CAUSE_CLASS_NAME = Util.intToStringMaxRadix(3);
private static final String FIELD_STRING_CAUSE_MESSAGE = Util.intToStringMaxRadix(4);
/**
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
* and {@link Bundleable.Creator}.
*
* <p>Subclasses should obtain their {@link Bundle Bundle's} field keys by applying a non-negative
* offset on this constant and passing the result to {@link #keyForField(int)}.
* offset on this constant and passing the result to {@link Util#intToStringMaxRadix(int)}.
*/
@UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000;
......@@ -424,29 +423,17 @@ public class PlaybackException extends Exception implements Bundleable {
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_INT_ERROR_CODE), errorCode);
bundle.putLong(keyForField(FIELD_LONG_TIMESTAMP_MS), timestampMs);
bundle.putString(keyForField(FIELD_STRING_MESSAGE), getMessage());
bundle.putInt(FIELD_INT_ERROR_CODE, errorCode);
bundle.putLong(FIELD_LONG_TIMESTAMP_MS, timestampMs);
bundle.putString(FIELD_STRING_MESSAGE, getMessage());
@Nullable Throwable cause = getCause();
if (cause != null) {
bundle.putString(keyForField(FIELD_STRING_CAUSE_CLASS_NAME), cause.getClass().getName());
bundle.putString(keyForField(FIELD_STRING_CAUSE_MESSAGE), cause.getMessage());
bundle.putString(FIELD_STRING_CAUSE_CLASS_NAME, cause.getClass().getName());
bundle.putString(FIELD_STRING_CAUSE_MESSAGE, cause.getMessage());
}
return bundle;
}
/**
* Converts the given field number to a string which can be used as a field key when implementing
* {@link #toBundle()} and {@link Bundleable.Creator}.
*
* <p>Subclasses should use {@code field} values greater than or equal to {@link
* #FIELD_CUSTOM_ID_BASE}.
*/
@UnstableApi
protected static String keyForField(int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
// Creates a new {@link Throwable} with possibly {@code null} message.
@SuppressWarnings("nullness:argument")
private static Throwable createThrowable(Class<?> clazz, @Nullable String message)
......@@ -462,8 +449,8 @@ public class PlaybackException extends Exception implements Bundleable {
@Nullable
private static Throwable getCauseFromBundle(Bundle bundle) {
@Nullable String causeClassName = bundle.getString(keyForField(FIELD_STRING_CAUSE_CLASS_NAME));
@Nullable String causeMessage = bundle.getString(keyForField(FIELD_STRING_CAUSE_MESSAGE));
@Nullable String causeClassName = bundle.getString(FIELD_STRING_CAUSE_CLASS_NAME);
@Nullable String causeMessage = bundle.getString(FIELD_STRING_CAUSE_MESSAGE);
@Nullable Throwable cause = null;
if (!TextUtils.isEmpty(causeClassName)) {
try {
......
......@@ -15,20 +15,13 @@
*/
package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.CheckResult;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Parameters that apply to playback, including speed setting. */
public final class PlaybackParameters implements Bundleable {
......@@ -122,21 +115,15 @@ public final class PlaybackParameters implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_SPEED, FIELD_PITCH})
private @interface FieldNumber {}
private static final int FIELD_SPEED = 0;
private static final int FIELD_PITCH = 1;
private static final String FIELD_SPEED = Util.intToStringMaxRadix(0);
private static final String FIELD_PITCH = Util.intToStringMaxRadix(1);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putFloat(keyForField(FIELD_SPEED), speed);
bundle.putFloat(keyForField(FIELD_PITCH), pitch);
bundle.putFloat(FIELD_SPEED, speed);
bundle.putFloat(FIELD_PITCH, pitch);
return bundle;
}
......@@ -144,12 +131,8 @@ public final class PlaybackParameters implements Bundleable {
@UnstableApi
public static final Creator<PlaybackParameters> CREATOR =
bundle -> {
float speed = bundle.getFloat(keyForField(FIELD_SPEED), /* defaultValue= */ 1f);
float pitch = bundle.getFloat(keyForField(FIELD_PITCH), /* defaultValue= */ 1f);
float speed = bundle.getFloat(FIELD_SPEED, /* defaultValue= */ 1f);
float pitch = bundle.getFloat(FIELD_PITCH, /* defaultValue= */ 1f);
return new PlaybackParameters(speed, pitch);
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -20,6 +20,7 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
......@@ -60,21 +61,14 @@ public abstract class Rating implements Bundleable {
/* package */ static final int RATING_TYPE_STAR = 2;
/* package */ static final int RATING_TYPE_THUMB = 3;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_RATING_TYPE})
private @interface FieldNumber {}
/* package */ static final int FIELD_RATING_TYPE = 0;
/* package */ static final String FIELD_RATING_TYPE = Util.intToStringMaxRadix(0);
/** Object that can restore a {@link Rating} from a {@link Bundle}. */
@UnstableApi public static final Creator<Rating> CREATOR = Rating::fromBundle;
private static Rating fromBundle(Bundle bundle) {
@RatingType
int ratingType =
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET);
int ratingType = bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET);
switch (ratingType) {
case RATING_TYPE_HEART:
return HeartRating.CREATOR.fromBundle(bundle);
......@@ -89,8 +83,4 @@ public abstract class Rating implements Bundleable {
throw new IllegalArgumentException("Unknown RatingType: " + ratingType);
}
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -16,19 +16,14 @@
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** A rating expressed as a fractional number of stars. */
public final class StarRating extends Rating {
......@@ -106,22 +101,16 @@ public final class StarRating extends Rating {
private static final @RatingType int TYPE = RATING_TYPE_STAR;
private static final int MAX_STARS_DEFAULT = 5;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_RATING_TYPE, FIELD_MAX_STARS, FIELD_STAR_RATING})
private @interface FieldNumber {}
private static final int FIELD_MAX_STARS = 1;
private static final int FIELD_STAR_RATING = 2;
private static final String FIELD_MAX_STARS = Util.intToStringMaxRadix(1);
private static final String FIELD_STAR_RATING = Util.intToStringMaxRadix(2);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE);
bundle.putInt(keyForField(FIELD_MAX_STARS), maxStars);
bundle.putFloat(keyForField(FIELD_STAR_RATING), starRating);
bundle.putInt(FIELD_RATING_TYPE, TYPE);
bundle.putInt(FIELD_MAX_STARS, maxStars);
bundle.putFloat(FIELD_STAR_RATING, starRating);
return bundle;
}
......@@ -129,19 +118,11 @@ public final class StarRating extends Rating {
@UnstableApi public static final Creator<StarRating> CREATOR = StarRating::fromBundle;
private static StarRating fromBundle(Bundle bundle) {
checkArgument(
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET)
== TYPE);
int maxStars =
bundle.getInt(keyForField(FIELD_MAX_STARS), /* defaultValue= */ MAX_STARS_DEFAULT);
float starRating =
bundle.getFloat(keyForField(FIELD_STAR_RATING), /* defaultValue= */ RATING_UNSET);
checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE);
int maxStars = bundle.getInt(FIELD_MAX_STARS, /* defaultValue= */ MAX_STARS_DEFAULT);
float starRating = bundle.getFloat(FIELD_STAR_RATING, /* defaultValue= */ RATING_UNSET);
return starRating == RATING_UNSET
? new StarRating(maxStars)
: new StarRating(maxStars, starRating);
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -16,17 +16,12 @@
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** A rating expressed as "thumbs up" or "thumbs down". */
public final class ThumbRating extends Rating {
......@@ -78,22 +73,16 @@ public final class ThumbRating extends Rating {
private static final @RatingType int TYPE = RATING_TYPE_THUMB;
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_RATING_TYPE, FIELD_RATED, FIELD_IS_THUMBS_UP})
private @interface FieldNumber {}
private static final int FIELD_RATED = 1;
private static final int FIELD_IS_THUMBS_UP = 2;
private static final String FIELD_RATED = Util.intToStringMaxRadix(1);
private static final String FIELD_IS_THUMBS_UP = Util.intToStringMaxRadix(2);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE);
bundle.putBoolean(keyForField(FIELD_RATED), rated);
bundle.putBoolean(keyForField(FIELD_IS_THUMBS_UP), isThumbsUp);
bundle.putInt(FIELD_RATING_TYPE, TYPE);
bundle.putBoolean(FIELD_RATED, rated);
bundle.putBoolean(FIELD_IS_THUMBS_UP, isThumbsUp);
return bundle;
}
......@@ -101,17 +90,10 @@ public final class ThumbRating extends Rating {
@UnstableApi public static final Creator<ThumbRating> CREATOR = ThumbRating::fromBundle;
private static ThumbRating fromBundle(Bundle bundle) {
checkArgument(
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET)
== TYPE);
boolean rated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false);
checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE);
boolean rated = bundle.getBoolean(FIELD_RATED, /* defaultValue= */ false);
return rated
? new ThumbRating(
bundle.getBoolean(keyForField(FIELD_IS_THUMBS_UP), /* defaultValue= */ false))
? new ThumbRating(bundle.getBoolean(FIELD_IS_THUMBS_UP, /* defaultValue= */ false))
: new ThumbRating();
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -16,20 +16,15 @@
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.CheckResult;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
......@@ -165,15 +160,8 @@ public final class TrackGroup implements Bundleable {
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_FORMATS, FIELD_ID})
private @interface FieldNumber {}
private static final int FIELD_FORMATS = 0;
private static final int FIELD_ID = 1;
private static final String FIELD_FORMATS = Util.intToStringMaxRadix(0);
private static final String FIELD_ID = Util.intToStringMaxRadix(1);
@UnstableApi
@Override
......@@ -183,8 +171,8 @@ public final class TrackGroup implements Bundleable {
for (Format format : formats) {
arrayList.add(format.toBundle(/* excludeMetadata= */ true));
}
bundle.putParcelableArrayList(keyForField(FIELD_FORMATS), arrayList);
bundle.putString(keyForField(FIELD_ID), id);
bundle.putParcelableArrayList(FIELD_FORMATS, arrayList);
bundle.putString(FIELD_ID, id);
return bundle;
}
......@@ -192,20 +180,15 @@ public final class TrackGroup implements Bundleable {
@UnstableApi
public static final Creator<TrackGroup> CREATOR =
bundle -> {
@Nullable
List<Bundle> formatBundles = bundle.getParcelableArrayList(keyForField(FIELD_FORMATS));
@Nullable List<Bundle> formatBundles = bundle.getParcelableArrayList(FIELD_FORMATS);
List<Format> formats =
formatBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(Format.CREATOR, formatBundles);
String id = bundle.getString(keyForField(FIELD_ID), /* defaultValue= */ "");
String id = bundle.getString(FIELD_ID, /* defaultValue= */ "");
return new TrackGroup(id, formats.toArray(new Format[0]));
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
private void verifyCorrectness() {
// TrackGroups should only contain tracks with exactly the same content (but in different
// qualities). We only log an error instead of throwing to not break backwards-compatibility for
......
......@@ -20,14 +20,11 @@ import static java.util.Collections.max;
import static java.util.Collections.min;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
......@@ -54,16 +51,8 @@ public final class TrackSelectionOverride implements Bundleable {
/** The indices of tracks in a {@link TrackGroup} to be selected. */
public final ImmutableList<Integer> trackIndices;
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FIELD_TRACK_GROUP,
FIELD_TRACKS,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUP = 0;
private static final int FIELD_TRACKS = 1;
private static final String FIELD_TRACK_GROUP = Util.intToStringMaxRadix(0);
private static final String FIELD_TRACKS = Util.intToStringMaxRadix(1);
/**
* Constructs an instance to force {@code trackIndex} in {@code trackGroup} to be selected.
......@@ -119,8 +108,8 @@ public final class TrackSelectionOverride implements Bundleable {
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle());
bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndices));
bundle.putBundle(FIELD_TRACK_GROUP, mediaTrackGroup.toBundle());
bundle.putIntArray(FIELD_TRACKS, Ints.toArray(trackIndices));
return bundle;
}
......@@ -128,13 +117,9 @@ public final class TrackSelectionOverride implements Bundleable {
@UnstableApi
public static final Creator<TrackSelectionOverride> CREATOR =
bundle -> {
Bundle trackGroupBundle = checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP)));
Bundle trackGroupBundle = checkNotNull(bundle.getBundle(FIELD_TRACK_GROUP));
TrackGroup mediaTrackGroup = TrackGroup.CREATOR.fromBundle(trackGroupBundle);
int[] tracks = checkNotNull(bundle.getIntArray(keyForField(FIELD_TRACKS)));
int[] tracks = checkNotNull(bundle.getIntArray(FIELD_TRACKS));
return new TrackSelectionOverride(mediaTrackGroup, Ints.asList(tracks));
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -18,20 +18,15 @@ package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.BundleableUtil.toBundleArrayList;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Booleans;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.List;
......@@ -221,29 +216,19 @@ public final class Tracks implements Bundleable {
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_TRACK_GROUP,
FIELD_TRACK_SUPPORT,
FIELD_TRACK_SELECTED,
FIELD_ADAPTIVE_SUPPORTED,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUP = 0;
private static final int FIELD_TRACK_SUPPORT = 1;
private static final int FIELD_TRACK_SELECTED = 3;
private static final int FIELD_ADAPTIVE_SUPPORTED = 4;
private static final String FIELD_TRACK_GROUP = Util.intToStringMaxRadix(0);
private static final String FIELD_TRACK_SUPPORT = Util.intToStringMaxRadix(1);
private static final String FIELD_TRACK_SELECTED = Util.intToStringMaxRadix(3);
private static final String FIELD_ADAPTIVE_SUPPORTED = Util.intToStringMaxRadix(4);
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle());
bundle.putIntArray(keyForField(FIELD_TRACK_SUPPORT), trackSupport);
bundle.putBooleanArray(keyForField(FIELD_TRACK_SELECTED), trackSelected);
bundle.putBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), adaptiveSupported);
bundle.putBundle(FIELD_TRACK_GROUP, mediaTrackGroup.toBundle());
bundle.putIntArray(FIELD_TRACK_SUPPORT, trackSupport);
bundle.putBooleanArray(FIELD_TRACK_SELECTED, trackSelected);
bundle.putBoolean(FIELD_ADAPTIVE_SUPPORTED, adaptiveSupported);
return bundle;
}
......@@ -253,23 +238,16 @@ public final class Tracks implements Bundleable {
bundle -> {
// Can't create a Tracks.Group without a TrackGroup
TrackGroup trackGroup =
TrackGroup.CREATOR.fromBundle(
checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP))));
TrackGroup.CREATOR.fromBundle(checkNotNull(bundle.getBundle(FIELD_TRACK_GROUP)));
final @C.FormatSupport int[] trackSupport =
MoreObjects.firstNonNull(
bundle.getIntArray(keyForField(FIELD_TRACK_SUPPORT)), new int[trackGroup.length]);
bundle.getIntArray(FIELD_TRACK_SUPPORT), new int[trackGroup.length]);
boolean[] selected =
MoreObjects.firstNonNull(
bundle.getBooleanArray(keyForField(FIELD_TRACK_SELECTED)),
new boolean[trackGroup.length]);
boolean adaptiveSupported =
bundle.getBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), false);
bundle.getBooleanArray(FIELD_TRACK_SELECTED), new boolean[trackGroup.length]);
boolean adaptiveSupported = bundle.getBoolean(FIELD_ADAPTIVE_SUPPORTED, false);
return new Group(trackGroup, adaptiveSupported, trackSupport, selected);
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
/** Empty tracks. */
......@@ -385,21 +363,13 @@ public final class Tracks implements Bundleable {
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_TRACK_GROUPS,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUPS = 0;
private static final String FIELD_TRACK_GROUPS = Util.intToStringMaxRadix(0);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(keyForField(FIELD_TRACK_GROUPS), toBundleArrayList(groups));
bundle.putParcelableArrayList(FIELD_TRACK_GROUPS, toBundleArrayList(groups));
return bundle;
}
......@@ -407,16 +377,11 @@ public final class Tracks implements Bundleable {
@UnstableApi
public static final Creator<Tracks> CREATOR =
bundle -> {
@Nullable
List<Bundle> groupBundles = bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS));
@Nullable List<Bundle> groupBundles = bundle.getParcelableArrayList(FIELD_TRACK_GROUPS);
List<Group> groups =
groupBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(Group.CREATOR, groupBundles);
return new Tracks(groups);
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -15,18 +15,12 @@
*/
package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import androidx.media3.common.util.Util;
/** Represents the video size. */
public final class VideoSize implements Bundleable {
......@@ -132,48 +126,32 @@ public final class VideoSize implements Bundleable {
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_WIDTH,
FIELD_HEIGHT,
FIELD_UNAPPLIED_ROTATION_DEGREES,
FIELD_PIXEL_WIDTH_HEIGHT_RATIO,
})
private @interface FieldNumber {}
private static final int FIELD_WIDTH = 0;
private static final int FIELD_HEIGHT = 1;
private static final int FIELD_UNAPPLIED_ROTATION_DEGREES = 2;
private static final int FIELD_PIXEL_WIDTH_HEIGHT_RATIO = 3;
private static final String FIELD_WIDTH = Util.intToStringMaxRadix(0);
private static final String FIELD_HEIGHT = Util.intToStringMaxRadix(1);
private static final String FIELD_UNAPPLIED_ROTATION_DEGREES = Util.intToStringMaxRadix(2);
private static final String FIELD_PIXEL_WIDTH_HEIGHT_RATIO = Util.intToStringMaxRadix(3);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_WIDTH), width);
bundle.putInt(keyForField(FIELD_HEIGHT), height);
bundle.putInt(keyForField(FIELD_UNAPPLIED_ROTATION_DEGREES), unappliedRotationDegrees);
bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio);
bundle.putInt(FIELD_WIDTH, width);
bundle.putInt(FIELD_HEIGHT, height);
bundle.putInt(FIELD_UNAPPLIED_ROTATION_DEGREES, unappliedRotationDegrees);
bundle.putFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio);
return bundle;
}
@UnstableApi
public static final Creator<VideoSize> CREATOR =
bundle -> {
int width = bundle.getInt(keyForField(FIELD_WIDTH), DEFAULT_WIDTH);
int height = bundle.getInt(keyForField(FIELD_HEIGHT), DEFAULT_HEIGHT);
int width = bundle.getInt(FIELD_WIDTH, DEFAULT_WIDTH);
int height = bundle.getInt(FIELD_HEIGHT, DEFAULT_HEIGHT);
int unappliedRotationDegrees =
bundle.getInt(
keyForField(FIELD_UNAPPLIED_ROTATION_DEGREES), DEFAULT_UNAPPLIED_ROTATION_DEGREES);
bundle.getInt(FIELD_UNAPPLIED_ROTATION_DEGREES, DEFAULT_UNAPPLIED_ROTATION_DEGREES);
float pixelWidthHeightRatio =
bundle.getFloat(
keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO);
bundle.getFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO);
return new VideoSize(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -35,6 +35,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
......@@ -977,69 +978,45 @@ public final class Cue implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_TEXT,
FIELD_TEXT_ALIGNMENT,
FIELD_MULTI_ROW_ALIGNMENT,
FIELD_BITMAP,
FIELD_LINE,
FIELD_LINE_TYPE,
FIELD_LINE_ANCHOR,
FIELD_POSITION,
FIELD_POSITION_ANCHOR,
FIELD_TEXT_SIZE_TYPE,
FIELD_TEXT_SIZE,
FIELD_SIZE,
FIELD_BITMAP_HEIGHT,
FIELD_WINDOW_COLOR,
FIELD_WINDOW_COLOR_SET,
FIELD_VERTICAL_TYPE,
FIELD_SHEAR_DEGREES
})
private @interface FieldNumber {}
private static final int FIELD_TEXT = 0;
private static final int FIELD_TEXT_ALIGNMENT = 1;
private static final int FIELD_MULTI_ROW_ALIGNMENT = 2;
private static final int FIELD_BITMAP = 3;
private static final int FIELD_LINE = 4;
private static final int FIELD_LINE_TYPE = 5;
private static final int FIELD_LINE_ANCHOR = 6;
private static final int FIELD_POSITION = 7;
private static final int FIELD_POSITION_ANCHOR = 8;
private static final int FIELD_TEXT_SIZE_TYPE = 9;
private static final int FIELD_TEXT_SIZE = 10;
private static final int FIELD_SIZE = 11;
private static final int FIELD_BITMAP_HEIGHT = 12;
private static final int FIELD_WINDOW_COLOR = 13;
private static final int FIELD_WINDOW_COLOR_SET = 14;
private static final int FIELD_VERTICAL_TYPE = 15;
private static final int FIELD_SHEAR_DEGREES = 16;
private static final String FIELD_TEXT = Util.intToStringMaxRadix(0);
private static final String FIELD_TEXT_ALIGNMENT = Util.intToStringMaxRadix(1);
private static final String FIELD_MULTI_ROW_ALIGNMENT = Util.intToStringMaxRadix(2);
private static final String FIELD_BITMAP = Util.intToStringMaxRadix(3);
private static final String FIELD_LINE = Util.intToStringMaxRadix(4);
private static final String FIELD_LINE_TYPE = Util.intToStringMaxRadix(5);
private static final String FIELD_LINE_ANCHOR = Util.intToStringMaxRadix(6);
private static final String FIELD_POSITION = Util.intToStringMaxRadix(7);
private static final String FIELD_POSITION_ANCHOR = Util.intToStringMaxRadix(8);
private static final String FIELD_TEXT_SIZE_TYPE = Util.intToStringMaxRadix(9);
private static final String FIELD_TEXT_SIZE = Util.intToStringMaxRadix(10);
private static final String FIELD_SIZE = Util.intToStringMaxRadix(11);
private static final String FIELD_BITMAP_HEIGHT = Util.intToStringMaxRadix(12);
private static final String FIELD_WINDOW_COLOR = Util.intToStringMaxRadix(13);
private static final String FIELD_WINDOW_COLOR_SET = Util.intToStringMaxRadix(14);
private static final String FIELD_VERTICAL_TYPE = Util.intToStringMaxRadix(15);
private static final String FIELD_SHEAR_DEGREES = Util.intToStringMaxRadix(16);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putCharSequence(keyForField(FIELD_TEXT), text);
bundle.putSerializable(keyForField(FIELD_TEXT_ALIGNMENT), textAlignment);
bundle.putSerializable(keyForField(FIELD_MULTI_ROW_ALIGNMENT), multiRowAlignment);
bundle.putParcelable(keyForField(FIELD_BITMAP), bitmap);
bundle.putFloat(keyForField(FIELD_LINE), line);
bundle.putInt(keyForField(FIELD_LINE_TYPE), lineType);
bundle.putInt(keyForField(FIELD_LINE_ANCHOR), lineAnchor);
bundle.putFloat(keyForField(FIELD_POSITION), position);
bundle.putInt(keyForField(FIELD_POSITION_ANCHOR), positionAnchor);
bundle.putInt(keyForField(FIELD_TEXT_SIZE_TYPE), textSizeType);
bundle.putFloat(keyForField(FIELD_TEXT_SIZE), textSize);
bundle.putFloat(keyForField(FIELD_SIZE), size);
bundle.putFloat(keyForField(FIELD_BITMAP_HEIGHT), bitmapHeight);
bundle.putBoolean(keyForField(FIELD_WINDOW_COLOR_SET), windowColorSet);
bundle.putInt(keyForField(FIELD_WINDOW_COLOR), windowColor);
bundle.putInt(keyForField(FIELD_VERTICAL_TYPE), verticalType);
bundle.putFloat(keyForField(FIELD_SHEAR_DEGREES), shearDegrees);
bundle.putCharSequence(FIELD_TEXT, text);
bundle.putSerializable(FIELD_TEXT_ALIGNMENT, textAlignment);
bundle.putSerializable(FIELD_MULTI_ROW_ALIGNMENT, multiRowAlignment);
bundle.putParcelable(FIELD_BITMAP, bitmap);
bundle.putFloat(FIELD_LINE, line);
bundle.putInt(FIELD_LINE_TYPE, lineType);
bundle.putInt(FIELD_LINE_ANCHOR, lineAnchor);
bundle.putFloat(FIELD_POSITION, position);
bundle.putInt(FIELD_POSITION_ANCHOR, positionAnchor);
bundle.putInt(FIELD_TEXT_SIZE_TYPE, textSizeType);
bundle.putFloat(FIELD_TEXT_SIZE, textSize);
bundle.putFloat(FIELD_SIZE, size);
bundle.putFloat(FIELD_BITMAP_HEIGHT, bitmapHeight);
bundle.putBoolean(FIELD_WINDOW_COLOR_SET, windowColorSet);
bundle.putInt(FIELD_WINDOW_COLOR, windowColor);
bundle.putInt(FIELD_VERTICAL_TYPE, verticalType);
bundle.putFloat(FIELD_SHEAR_DEGREES, shearDegrees);
return bundle;
}
......@@ -1047,67 +1024,56 @@ public final class Cue implements Bundleable {
private static final Cue fromBundle(Bundle bundle) {
Builder builder = new Builder();
@Nullable CharSequence text = bundle.getCharSequence(keyForField(FIELD_TEXT));
@Nullable CharSequence text = bundle.getCharSequence(FIELD_TEXT);
if (text != null) {
builder.setText(text);
}
@Nullable
Alignment textAlignment = (Alignment) bundle.getSerializable(keyForField(FIELD_TEXT_ALIGNMENT));
@Nullable Alignment textAlignment = (Alignment) bundle.getSerializable(FIELD_TEXT_ALIGNMENT);
if (textAlignment != null) {
builder.setTextAlignment(textAlignment);
}
@Nullable
Alignment multiRowAlignment =
(Alignment) bundle.getSerializable(keyForField(FIELD_MULTI_ROW_ALIGNMENT));
Alignment multiRowAlignment = (Alignment) bundle.getSerializable(FIELD_MULTI_ROW_ALIGNMENT);
if (multiRowAlignment != null) {
builder.setMultiRowAlignment(multiRowAlignment);
}
@Nullable Bitmap bitmap = bundle.getParcelable(keyForField(FIELD_BITMAP));
@Nullable Bitmap bitmap = bundle.getParcelable(FIELD_BITMAP);
if (bitmap != null) {
builder.setBitmap(bitmap);
}
if (bundle.containsKey(keyForField(FIELD_LINE))
&& bundle.containsKey(keyForField(FIELD_LINE_TYPE))) {
builder.setLine(
bundle.getFloat(keyForField(FIELD_LINE)), bundle.getInt(keyForField(FIELD_LINE_TYPE)));
if (bundle.containsKey(FIELD_LINE) && bundle.containsKey(FIELD_LINE_TYPE)) {
builder.setLine(bundle.getFloat(FIELD_LINE), bundle.getInt(FIELD_LINE_TYPE));
}
if (bundle.containsKey(keyForField(FIELD_LINE_ANCHOR))) {
builder.setLineAnchor(bundle.getInt(keyForField(FIELD_LINE_ANCHOR)));
if (bundle.containsKey(FIELD_LINE_ANCHOR)) {
builder.setLineAnchor(bundle.getInt(FIELD_LINE_ANCHOR));
}
if (bundle.containsKey(keyForField(FIELD_POSITION))) {
builder.setPosition(bundle.getFloat(keyForField(FIELD_POSITION)));
if (bundle.containsKey(FIELD_POSITION)) {
builder.setPosition(bundle.getFloat(FIELD_POSITION));
}
if (bundle.containsKey(keyForField(FIELD_POSITION_ANCHOR))) {
builder.setPositionAnchor(bundle.getInt(keyForField(FIELD_POSITION_ANCHOR)));
if (bundle.containsKey(FIELD_POSITION_ANCHOR)) {
builder.setPositionAnchor(bundle.getInt(FIELD_POSITION_ANCHOR));
}
if (bundle.containsKey(keyForField(FIELD_TEXT_SIZE))
&& bundle.containsKey(keyForField(FIELD_TEXT_SIZE_TYPE))) {
builder.setTextSize(
bundle.getFloat(keyForField(FIELD_TEXT_SIZE)),
bundle.getInt(keyForField(FIELD_TEXT_SIZE_TYPE)));
if (bundle.containsKey(FIELD_TEXT_SIZE) && bundle.containsKey(FIELD_TEXT_SIZE_TYPE)) {
builder.setTextSize(bundle.getFloat(FIELD_TEXT_SIZE), bundle.getInt(FIELD_TEXT_SIZE_TYPE));
}
if (bundle.containsKey(keyForField(FIELD_SIZE))) {
builder.setSize(bundle.getFloat(keyForField(FIELD_SIZE)));
if (bundle.containsKey(FIELD_SIZE)) {
builder.setSize(bundle.getFloat(FIELD_SIZE));
}
if (bundle.containsKey(keyForField(FIELD_BITMAP_HEIGHT))) {
builder.setBitmapHeight(bundle.getFloat(keyForField(FIELD_BITMAP_HEIGHT)));
if (bundle.containsKey(FIELD_BITMAP_HEIGHT)) {
builder.setBitmapHeight(bundle.getFloat(FIELD_BITMAP_HEIGHT));
}
if (bundle.containsKey(keyForField(FIELD_WINDOW_COLOR))) {
builder.setWindowColor(bundle.getInt(keyForField(FIELD_WINDOW_COLOR)));
if (bundle.containsKey(FIELD_WINDOW_COLOR)) {
builder.setWindowColor(bundle.getInt(FIELD_WINDOW_COLOR));
}
if (!bundle.getBoolean(keyForField(FIELD_WINDOW_COLOR_SET), /* defaultValue= */ false)) {
if (!bundle.getBoolean(FIELD_WINDOW_COLOR_SET, /* defaultValue= */ false)) {
builder.clearWindowColor();
}
if (bundle.containsKey(keyForField(FIELD_VERTICAL_TYPE))) {
builder.setVerticalType(bundle.getInt(keyForField(FIELD_VERTICAL_TYPE)));
if (bundle.containsKey(FIELD_VERTICAL_TYPE)) {
builder.setVerticalType(bundle.getInt(FIELD_VERTICAL_TYPE));
}
if (bundle.containsKey(keyForField(FIELD_SHEAR_DEGREES))) {
builder.setShearDegrees(bundle.getFloat(keyForField(FIELD_SHEAR_DEGREES)));
if (bundle.containsKey(FIELD_SHEAR_DEGREES)) {
builder.setShearDegrees(bundle.getFloat(FIELD_SHEAR_DEGREES));
}
return builder.build();
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -15,21 +15,15 @@
*/
package androidx.media3.common.text;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.graphics.Bitmap;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
......@@ -66,41 +60,31 @@ public final class CueGroup implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_CUES, FIELD_PRESENTATION_TIME_US})
private @interface FieldNumber {}
private static final int FIELD_CUES = 0;
private static final int FIELD_PRESENTATION_TIME_US = 1;
private static final String FIELD_CUES = Util.intToStringMaxRadix(0);
private static final String FIELD_PRESENTATION_TIME_US = Util.intToStringMaxRadix(1);
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_CUES), BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues)));
bundle.putLong(keyForField(FIELD_PRESENTATION_TIME_US), presentationTimeUs);
FIELD_CUES, BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues)));
bundle.putLong(FIELD_PRESENTATION_TIME_US, presentationTimeUs);
return bundle;
}
@UnstableApi public static final Creator<CueGroup> CREATOR = CueGroup::fromBundle;
private static final CueGroup fromBundle(Bundle bundle) {
@Nullable ArrayList<Bundle> cueBundles = bundle.getParcelableArrayList(keyForField(FIELD_CUES));
@Nullable ArrayList<Bundle> cueBundles = bundle.getParcelableArrayList(FIELD_CUES);
List<Cue> cues =
cueBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(Cue.CREATOR, cueBundles);
long presentationTimeUs = bundle.getLong(keyForField(FIELD_PRESENTATION_TIME_US));
long presentationTimeUs = bundle.getLong(FIELD_PRESENTATION_TIME_US);
return new CueGroup(cues, presentationTimeUs);
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
/**
* Filters out {@link Cue} objects containing {@link Bitmap}. It is used when transferring cues
* between processes to prevent transferring too much data.
......
......@@ -15,9 +15,12 @@
*/
package androidx.media3.common.util;
import static androidx.media3.common.util.Assertions.checkState;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.CheckResult;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.FlagSet;
......@@ -34,6 +37,9 @@ import org.checkerframework.checker.nullness.qual.NonNull;
* <p>Events are also guaranteed to be only sent to the listeners registered at the time the event
* was enqueued and haven't been removed since.
*
* <p>All methods must be called on the {@link Looper} passed to the constructor unless indicated
* otherwise.
*
* @param <T> The listener type.
*/
@UnstableApi
......@@ -76,14 +82,18 @@ public final class ListenerSet<T extends @NonNull Object> {
private final CopyOnWriteArraySet<ListenerHolder<T>> listeners;
private final ArrayDeque<Runnable> flushingEvents;
private final ArrayDeque<Runnable> queuedEvents;
private final Object releasedLock;
@GuardedBy("releasedLock")
private boolean released;
private boolean throwsWhenUsingWrongThread;
/**
* Creates a new listener set.
*
* @param looper A {@link Looper} used to call listeners on. The same {@link Looper} must be used
* to call all other methods of this class.
* to call all other methods of this class unless indicated otherwise.
* @param clock A {@link Clock}.
* @param iterationFinishedEvent An {@link IterationFinishedEvent} sent when all other events sent
* during one {@link Looper} message queue iteration were handled by the listeners.
......@@ -100,17 +110,21 @@ public final class ListenerSet<T extends @NonNull Object> {
this.clock = clock;
this.listeners = listeners;
this.iterationFinishedEvent = iterationFinishedEvent;
releasedLock = new Object();
flushingEvents = new ArrayDeque<>();
queuedEvents = new ArrayDeque<>();
// It's safe to use "this" because we don't send a message before exiting the constructor.
@SuppressWarnings("nullness:methodref.receiver.bound")
HandlerWrapper handler = clock.createHandler(looper, this::handleMessage);
this.handler = handler;
throwsWhenUsingWrongThread = true;
}
/**
* Copies the listener set.
*
* <p>This method can be called from any thread.
*
* @param looper The new {@link Looper} for the copied listener set.
* @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events
* sent during one {@link Looper} message queue iteration were handled by the listeners.
......@@ -124,6 +138,8 @@ public final class ListenerSet<T extends @NonNull Object> {
/**
* Copies the listener set.
*
* <p>This method can be called from any thread.
*
* @param looper The new {@link Looper} for the copied listener set.
* @param clock The new {@link Clock} for the copied listener set.
* @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events
......@@ -141,14 +157,18 @@ public final class ListenerSet<T extends @NonNull Object> {
*
* <p>If a listener is already present, it will not be added again.
*
* <p>This method can be called from any thread.
*
* @param listener The listener to be added.
*/
public void add(T listener) {
if (released) {
return;
}
Assertions.checkNotNull(listener);
listeners.add(new ListenerHolder<>(listener));
synchronized (releasedLock) {
if (released) {
return;
}
listeners.add(new ListenerHolder<>(listener));
}
}
/**
......@@ -159,6 +179,7 @@ public final class ListenerSet<T extends @NonNull Object> {
* @param listener The listener to be removed.
*/
public void remove(T listener) {
verifyCurrentThread();
for (ListenerHolder<T> listenerHolder : listeners) {
if (listenerHolder.listener.equals(listener)) {
listenerHolder.release(iterationFinishedEvent);
......@@ -169,11 +190,13 @@ public final class ListenerSet<T extends @NonNull Object> {
/** Removes all listeners from the set. */
public void clear() {
verifyCurrentThread();
listeners.clear();
}
/** Returns the number of added listeners. */
public int size() {
verifyCurrentThread();
return listeners.size();
}
......@@ -185,6 +208,7 @@ public final class ListenerSet<T extends @NonNull Object> {
* @param event The event.
*/
public void queueEvent(int eventFlag, Event<T> event) {
verifyCurrentThread();
CopyOnWriteArraySet<ListenerHolder<T>> listenerSnapshot = new CopyOnWriteArraySet<>(listeners);
queuedEvents.add(
() -> {
......@@ -196,6 +220,7 @@ public final class ListenerSet<T extends @NonNull Object> {
/** Notifies listeners of events previously enqueued with {@link #queueEvent(int, Event)}. */
public void flushEvents() {
verifyCurrentThread();
if (queuedEvents.isEmpty()) {
return;
}
......@@ -234,11 +259,27 @@ public final class ListenerSet<T extends @NonNull Object> {
* <p>This will ensure no events are sent to any listener after this method has been called.
*/
public void release() {
verifyCurrentThread();
synchronized (releasedLock) {
released = true;
}
for (ListenerHolder<T> listenerHolder : listeners) {
listenerHolder.release(iterationFinishedEvent);
}
listeners.clear();
released = true;
}
/**
* Sets whether methods throw when using the wrong thread.
*
* <p>Do not use this method unless to support legacy use cases.
*
* @param throwsWhenUsingWrongThread Whether to throw when using the wrong thread.
* @deprecated Do not use this method and ensure all calls are made from the correct thread.
*/
@Deprecated
public void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) {
this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread;
}
private boolean handleMessage(Message message) {
......@@ -254,6 +295,13 @@ public final class ListenerSet<T extends @NonNull Object> {
return true;
}
private void verifyCurrentThread() {
if (!throwsWhenUsingWrongThread) {
return;
}
checkState(Thread.currentThread() == handler.getLooper().getThread());
}
private static final class ListenerHolder<T extends @NonNull Object> {
public final T listener;
......
......@@ -17,6 +17,9 @@ package androidx.media3.common.util;
import androidx.annotation.Nullable;
import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Chars;
import com.google.common.primitives.UnsignedBytes;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
......@@ -28,6 +31,12 @@ import java.util.Arrays;
@UnstableApi
public final class ParsableByteArray {
private static final char[] CR_AND_LF = {'\r', '\n'};
private static final char[] LF = {'\n'};
private static final ImmutableSet<Charset> SUPPORTED_CHARSETS_FOR_READLINE =
ImmutableSet.of(
Charsets.US_ASCII, Charsets.UTF_8, Charsets.UTF_16, Charsets.UTF_16BE, Charsets.UTF_16LE);
private byte[] data;
private int position;
// TODO(internal b/147657250): Enforce this limit on all read methods.
......@@ -490,45 +499,47 @@ public final class ParsableByteArray {
}
/**
* Reads a line of text.
* Reads a line of text in UTF-8.
*
* <p>Equivalent to passing {@link Charsets#UTF_8} to {@link #readLine(Charset)}.
*/
@Nullable
public String readLine() {
return readLine(Charsets.UTF_8);
}
/**
* Reads a line of text in {@code charset}.
*
* <p>A line is considered to be terminated by any one of a carriage return ('\r'), a line feed
* ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). The UTF-8 charset is
* used. This method discards leading UTF-8 byte order marks, if present.
* ('\n'), or a carriage return followed immediately by a line feed ('\r\n'). This method discards
* leading UTF byte order marks (BOM), if present.
*
* <p>The {@linkplain #getPosition() position} is advanced to start of the next line (i.e. any
* line terminators are skipped).
*
* @param charset The charset used to interpret the bytes as a {@link String}.
* @return The line not including any line-termination characters, or null if the end of the data
* has already been reached.
* @throws IllegalArgumentException if charset is not supported. Only US_ASCII, UTF-8, UTF-16,
* UTF-16BE, and UTF-16LE are supported.
*/
@Nullable
public String readLine() {
public String readLine(Charset charset) {
Assertions.checkArgument(
SUPPORTED_CHARSETS_FOR_READLINE.contains(charset), "Unsupported charset: " + charset);
if (bytesLeft() == 0) {
return null;
}
int lineLimit = position;
while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) {
lineLimit++;
if (!charset.equals(Charsets.US_ASCII)) {
readUtfCharsetFromBom(); // Skip BOM if present
}
if (lineLimit - position >= 3
&& data[position] == (byte) 0xEF
&& data[position + 1] == (byte) 0xBB
&& data[position + 2] == (byte) 0xBF) {
// There's a UTF-8 byte order mark at the start of the line. Discard it.
position += 3;
}
String line = Util.fromUtf8Bytes(data, position, lineLimit - position);
position = lineLimit;
int lineLimit = findNextLineTerminator(charset);
String line = readString(lineLimit - position, charset);
if (position == limit) {
return line;
}
if (data[position] == '\r') {
position++;
if (position == limit) {
return line;
}
}
if (data[position] == '\n') {
position++;
}
skipLineTerminator(charset);
return line;
}
......@@ -566,4 +577,99 @@ public final class ParsableByteArray {
position += length;
return value;
}
/**
* Reads a UTF byte order mark (BOM) and returns the UTF {@link Charset} it represents. Returns
* {@code null} without advancing {@link #getPosition() position} if no BOM is found.
*/
@Nullable
public Charset readUtfCharsetFromBom() {
if (bytesLeft() >= 3
&& data[position] == (byte) 0xEF
&& data[position + 1] == (byte) 0xBB
&& data[position + 2] == (byte) 0xBF) {
position += 3;
return Charsets.UTF_8;
} else if (bytesLeft() >= 2) {
if (data[position] == (byte) 0xFE && data[position + 1] == (byte) 0xFF) {
position += 2;
return Charsets.UTF_16BE;
} else if (data[position] == (byte) 0xFF && data[position + 1] == (byte) 0xFE) {
position += 2;
return Charsets.UTF_16LE;
}
}
return null;
}
/**
* Returns the index of the next occurrence of '\n' or '\r', or {@link #limit} if none is found.
*/
private int findNextLineTerminator(Charset charset) {
int stride;
if (charset.equals(Charsets.UTF_8) || charset.equals(Charsets.US_ASCII)) {
stride = 1;
} else if (charset.equals(Charsets.UTF_16)
|| charset.equals(Charsets.UTF_16LE)
|| charset.equals(Charsets.UTF_16BE)) {
stride = 2;
} else {
throw new IllegalArgumentException("Unsupported charset: " + charset);
}
for (int i = position; i < limit - (stride - 1); i += stride) {
if ((charset.equals(Charsets.UTF_8) || charset.equals(Charsets.US_ASCII))
&& Util.isLinebreak(data[i])) {
return i;
} else if ((charset.equals(Charsets.UTF_16) || charset.equals(Charsets.UTF_16BE))
&& data[i] == 0x00
&& Util.isLinebreak(data[i + 1])) {
return i;
} else if (charset.equals(Charsets.UTF_16LE)
&& data[i + 1] == 0x00
&& Util.isLinebreak(data[i])) {
return i;
}
}
return limit;
}
private void skipLineTerminator(Charset charset) {
if (readCharacterIfInList(charset, CR_AND_LF) == '\r') {
readCharacterIfInList(charset, LF);
}
}
/**
* Peeks at the character at {@link #position} (as decoded by {@code charset}), returns it and
* advances {@link #position} past it if it's in {@code chars}, otherwise returns {@code 0}
* without advancing {@link #position}. Returns {@code 0} if {@link #bytesLeft()} doesn't allow
* reading a whole character in {@code charset}.
*
* <p>Only supports characters in {@code chars} that occupy a single code unit (i.e. one byte for
* UTF-8 and two bytes for UTF-16).
*/
private char readCharacterIfInList(Charset charset, char[] chars) {
char character;
int characterSize;
if ((charset.equals(Charsets.UTF_8) || charset.equals(Charsets.US_ASCII)) && bytesLeft() >= 1) {
character = Chars.checkedCast(UnsignedBytes.toInt(data[position]));
characterSize = 1;
} else if ((charset.equals(Charsets.UTF_16) || charset.equals(Charsets.UTF_16BE))
&& bytesLeft() >= 2) {
character = Chars.fromBytes(data[position], data[position + 1]);
characterSize = 2;
} else if (charset.equals(Charsets.UTF_16LE) && bytesLeft() >= 2) {
character = Chars.fromBytes(data[position + 1], data[position]);
characterSize = 2;
} else {
return 0;
}
if (Chars.contains(chars, character)) {
position += characterSize;
return Chars.checkedCast(character);
} else {
return 0;
}
}
}
......@@ -29,6 +29,9 @@ public final class Size {
public static final Size UNKNOWN =
new Size(/* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET);
/* A static instance to represent a size of zero height and width. */
public static final Size ZERO = new Size(/* width= */ 0, /* height= */ 0);
private final int width;
private final int height;
......
......@@ -47,6 +47,7 @@ import android.content.res.Resources;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.media.AudioFormat;
import android.media.AudioManager;
......@@ -66,6 +67,8 @@ import android.util.SparseLongArray;
import android.view.Display;
import android.view.SurfaceView;
import android.view.WindowManager;
import androidx.annotation.DoNotInline;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
......@@ -2864,6 +2867,33 @@ public final class Util {
return sum;
}
/**
* Returns a {@link Drawable} for the given resource or throws a {@link
* Resources.NotFoundException} if not found.
*
* @param context The context to get the theme from starting with API 21.
* @param resources The resources to load the drawable from.
* @param drawableRes The drawable resource int.
* @return The loaded {@link Drawable}.
*/
@UnstableApi
public static Drawable getDrawable(
Context context, Resources resources, @DrawableRes int drawableRes) {
return SDK_INT >= 21
? Api21.getDrawable(context, resources, drawableRes)
: resources.getDrawable(drawableRes);
}
/**
* Returns a string representation of the integer using radix value {@link Character#MAX_RADIX}.
*
* @param i An integer to be converted to String.
*/
@UnstableApi
public static String intToStringMaxRadix(int i) {
return Integer.toString(i, Character.MAX_RADIX);
}
@Nullable
private static String getSystemProperty(String name) {
try {
......@@ -3100,4 +3130,12 @@ public final class Util {
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4,
0xF3
};
@RequiresApi(21)
private static final class Api21 {
@DoNotInline
public static Drawable getDrawable(Context context, Resources resources, @DrawableRes int res) {
return resources.getDrawable(res, context.getTheme());
}
}
}
......@@ -24,6 +24,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;
import android.net.Uri;
import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
......@@ -402,7 +403,43 @@ public class AdPlaybackStateTest {
}
@Test
public void roundTripViaBundle_yieldsEqualFieldsExceptAdsId() {
public void adPlaybackStateWithNoAds_checkValues() {
AdPlaybackState adPlaybackStateWithNoAds = AdPlaybackState.NONE;
// Please refrain from altering these values since doing so would cause issues with backwards
// compatibility.
assertThat(adPlaybackStateWithNoAds.adsId).isNull();
assertThat(adPlaybackStateWithNoAds.adGroupCount).isEqualTo(0);
assertThat(adPlaybackStateWithNoAds.adResumePositionUs).isEqualTo(0);
assertThat(adPlaybackStateWithNoAds.contentDurationUs).isEqualTo(C.TIME_UNSET);
assertThat(adPlaybackStateWithNoAds.removedAdGroupCount).isEqualTo(0);
}
@Test
public void adPlaybackStateWithNoAds_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
AdPlaybackState adPlaybackStateWithNoAds = AdPlaybackState.NONE;
Bundle adPlaybackStateWithNoAdsBundle = adPlaybackStateWithNoAds.toBundle();
// Check that default values are skipped when bundling.
assertThat(adPlaybackStateWithNoAdsBundle.keySet()).isEmpty();
AdPlaybackState adPlaybackStateWithNoAdsFromBundle =
AdPlaybackState.CREATOR.fromBundle(adPlaybackStateWithNoAdsBundle);
assertThat(adPlaybackStateWithNoAdsFromBundle.adsId).isEqualTo(adPlaybackStateWithNoAds.adsId);
assertThat(adPlaybackStateWithNoAdsFromBundle.adGroupCount)
.isEqualTo(adPlaybackStateWithNoAds.adGroupCount);
assertThat(adPlaybackStateWithNoAdsFromBundle.adResumePositionUs)
.isEqualTo(adPlaybackStateWithNoAds.adResumePositionUs);
assertThat(adPlaybackStateWithNoAdsFromBundle.contentDurationUs)
.isEqualTo(adPlaybackStateWithNoAds.contentDurationUs);
assertThat(adPlaybackStateWithNoAdsFromBundle.removedAdGroupCount)
.isEqualTo(adPlaybackStateWithNoAds.removedAdGroupCount);
}
@Test
public void createAdPlaybackState_roundTripViaBundle_yieldsEqualFieldsExceptAdsId() {
AdPlaybackState originalState =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US)
.withRemovedAdGroupCount(1)
......
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import androidx.media3.test.utils.FakeTimeline;
import androidx.media3.test.utils.StubPlayer;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link BasePlayer}. */
@RunWith(AndroidJUnit4.class)
public class BasePlayerTest {
@Test
public void seekTo_withIndexAndPosition_usesCommandSeekToMediaItem() {
BasePlayer player = spy(new TestBasePlayer());
player.seekTo(/* mediaItemIndex= */ 2, /* positionMs= */ 4000);
verify(player)
.seekTo(
/* mediaItemIndex= */ 2,
/* positionMs= */ 4000,
Player.COMMAND_SEEK_TO_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekTo_withPosition_usesCommandSeekInCurrentMediaItem() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekTo(/* positionMs= */ 4000);
verify(player)
.seekTo(
/* mediaItemIndex= */ 1,
/* positionMs= */ 4000,
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToDefaultPosition_withIndex_usesCommandSeekToMediaItem() {
BasePlayer player = spy(new TestBasePlayer());
player.seekToDefaultPosition(/* mediaItemIndex= */ 2);
verify(player)
.seekTo(
/* mediaItemIndex= */ 2,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToDefaultPosition_withoutIndex_usesCommandSeekToDefaultPosition() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekToDefaultPosition();
verify(player)
.seekTo(
/* mediaItemIndex= */ 1,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_DEFAULT_POSITION,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToNext_usesCommandSeekToNext() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekToNext();
verify(player)
.seekTo(
/* mediaItemIndex= */ 2,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_NEXT,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToNextMediaItem_usesCommandSeekToNextMediaItem() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekToNextMediaItem();
verify(player)
.seekTo(
/* mediaItemIndex= */ 2,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekForward_usesCommandSeekForward() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public long getSeekForwardIncrement() {
return 2000;
}
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
@Override
public long getCurrentPosition() {
return 5000;
}
});
player.seekForward();
verify(player)
.seekTo(
/* mediaItemIndex= */ 1,
/* positionMs= */ 7000,
Player.COMMAND_SEEK_FORWARD,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToPrevious_usesCommandSeekToPrevious() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
@Override
public long getMaxSeekToPreviousPosition() {
return 4000;
}
@Override
public long getCurrentPosition() {
return 2000;
}
});
player.seekToPrevious();
verify(player)
.seekTo(
/* mediaItemIndex= */ 0,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_PREVIOUS,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekToPreviousMediaItem_usesCommandSeekToPreviousMediaItem() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
});
player.seekToPreviousMediaItem();
verify(player)
.seekTo(
/* mediaItemIndex= */ 0,
/* positionMs= */ C.TIME_UNSET,
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM,
/* isRepeatingCurrentItem= */ false);
}
@Test
public void seekBack_usesCommandSeekBack() {
BasePlayer player =
spy(
new TestBasePlayer() {
@Override
public long getSeekBackIncrement() {
return 2000;
}
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
@Override
public long getCurrentPosition() {
return 5000;
}
});
player.seekBack();
verify(player)
.seekTo(
/* mediaItemIndex= */ 1,
/* positionMs= */ 3000,
Player.COMMAND_SEEK_BACK,
/* isRepeatingCurrentItem= */ false);
}
private static class TestBasePlayer extends StubPlayer {
@Override
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
// Do nothing.
}
@Override
public long getSeekBackIncrement() {
return 2000;
}
@Override
public long getSeekForwardIncrement() {
return 2000;
}
@Override
public long getMaxSeekToPreviousPosition() {
return 2000;
}
@Override
public Timeline getCurrentTimeline() {
return new FakeTimeline(/* windowCount= */ 3);
}
@Override
public int getCurrentMediaItemIndex() {
return 1;
}
@Override
public long getCurrentPosition() {
return 5000;
}
@Override
public long getDuration() {
return 20000;
}
@Override
public boolean isPlayingAd() {
return false;
}
@Override
public int getRepeatMode() {
return Player.REPEAT_MODE_OFF;
}
@Override
public boolean getShuffleModeEnabled() {
return false;
}
}
}
......@@ -111,6 +111,8 @@ public final class FormatTest {
.setEncoderPadding(1002)
.setAccessibilityChannel(2)
.setCryptoType(C.CRYPTO_TYPE_CUSTOM_BASE)
.setTileCountHorizontal(20)
.setTileCountVertical(40)
.build();
}
......
......@@ -360,10 +360,12 @@ public class MediaItemTest {
}
@Test
public void clippingConfigurationDefaults() {
public void createDefaultClippingConfigurationInstance_checksDefaultValues() {
MediaItem.ClippingConfiguration clippingConfiguration =
new MediaItem.ClippingConfiguration.Builder().build();
// Please refrain from altering default values since doing so would cause issues with backwards
// compatibility.
assertThat(clippingConfiguration.startPositionMs).isEqualTo(0L);
assertThat(clippingConfiguration.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE);
assertThat(clippingConfiguration.relativeToLiveWindow).isFalse();
......@@ -373,6 +375,38 @@ public class MediaItemTest {
}
@Test
public void
createDefaultClippingConfigurationInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
MediaItem.ClippingConfiguration clippingConfiguration =
new MediaItem.ClippingConfiguration.Builder().build();
Bundle clippingConfigurationBundle = clippingConfiguration.toBundle();
// Check that default values are skipped when bundling.
assertThat(clippingConfigurationBundle.keySet()).isEmpty();
MediaItem.ClippingConfiguration clippingConfigurationFromBundle =
MediaItem.ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle);
assertThat(clippingConfigurationFromBundle).isEqualTo(clippingConfiguration);
}
@Test
public void createClippingConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() {
// Creates instance by setting some non-default values
MediaItem.ClippingConfiguration clippingConfiguration =
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(1000L)
.setStartsAtKeyFrame(true)
.build();
MediaItem.ClippingConfiguration clippingConfigurationFromBundle =
MediaItem.ClippingConfiguration.CREATOR.fromBundle(clippingConfiguration.toBundle());
assertThat(clippingConfigurationFromBundle).isEqualTo(clippingConfiguration);
}
@Test
public void clippingConfigurationBuilder_throwsOnInvalidValues() {
MediaItem.ClippingConfiguration.Builder clippingConfigurationBuilder =
new MediaItem.ClippingConfiguration.Builder();
......@@ -515,6 +549,53 @@ public class MediaItemTest {
}
@Test
public void createDefaultLiveConfigurationInstance_checksDefaultValues() {
MediaItem.LiveConfiguration liveConfiguration =
new MediaItem.LiveConfiguration.Builder().build();
// Please refrain from altering default values since doing so would cause issues with backwards
// compatibility.
assertThat(liveConfiguration.targetOffsetMs).isEqualTo(C.TIME_UNSET);
assertThat(liveConfiguration.minOffsetMs).isEqualTo(C.TIME_UNSET);
assertThat(liveConfiguration.maxOffsetMs).isEqualTo(C.TIME_UNSET);
assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET);
assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET);
assertThat(liveConfiguration).isEqualTo(MediaItem.LiveConfiguration.UNSET);
}
@Test
public void
createDefaultLiveConfigurationInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
MediaItem.LiveConfiguration liveConfiguration =
new MediaItem.LiveConfiguration.Builder().build();
Bundle liveConfigurationBundle = liveConfiguration.toBundle();
// Check that default values are skipped when bundling.
assertThat(liveConfigurationBundle.keySet()).isEmpty();
MediaItem.LiveConfiguration liveConfigurationFromBundle =
MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle);
assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration);
}
@Test
public void createLiveConfigurationInstance_roundTripViaBundle_yieldsEqualInstance() {
// Creates instance by setting some non-default values
MediaItem.LiveConfiguration liveConfiguration =
new MediaItem.LiveConfiguration.Builder()
.setTargetOffsetMs(10_000)
.setMaxPlaybackSpeed(2f)
.build();
MediaItem.LiveConfiguration liveConfigurationFromBundle =
MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfiguration.toBundle());
assertThat(liveConfigurationFromBundle).isEqualTo(liveConfiguration);
}
@Test
public void builderSetLiveConfiguration() {
MediaItem mediaItem =
new MediaItem.Builder()
......@@ -747,4 +828,62 @@ public class MediaItemTest {
assertThat(mediaItem.localConfiguration).isNotNull();
assertThat(MediaItem.CREATOR.fromBundle(mediaItem.toBundle()).localConfiguration).isNull();
}
@Test
public void createDefaultMediaItemInstance_checksDefaultValues() {
MediaItem mediaItem = new MediaItem.Builder().build();
// Please refrain from altering default values since doing so would cause issues with backwards
// compatibility.
assertThat(mediaItem.mediaId).isEqualTo(MediaItem.DEFAULT_MEDIA_ID);
assertThat(mediaItem.liveConfiguration).isEqualTo(MediaItem.LiveConfiguration.UNSET);
assertThat(mediaItem.mediaMetadata).isEqualTo(MediaMetadata.EMPTY);
assertThat(mediaItem.clippingConfiguration).isEqualTo(MediaItem.ClippingConfiguration.UNSET);
assertThat(mediaItem.requestMetadata).isEqualTo(RequestMetadata.EMPTY);
assertThat(mediaItem).isEqualTo(MediaItem.EMPTY);
}
@Test
public void createDefaultMediaItemInstance_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
MediaItem mediaItem = new MediaItem.Builder().build();
Bundle mediaItemBundle = mediaItem.toBundle();
// Check that default values are skipped when bundling.
assertThat(mediaItemBundle.keySet()).isEmpty();
MediaItem mediaItemFromBundle = MediaItem.CREATOR.fromBundle(mediaItem.toBundle());
assertThat(mediaItemFromBundle).isEqualTo(mediaItem);
}
@Test
public void createMediaItemInstance_roundTripViaBundle_yieldsEqualInstance() {
Bundle extras = new Bundle();
extras.putString("key", "value");
// Creates instance by setting some non-default values
MediaItem mediaItem =
new MediaItem.Builder()
.setLiveConfiguration(
new MediaItem.LiveConfiguration.Builder()
.setTargetOffsetMs(20_000)
.setMinOffsetMs(2_222)
.setMaxOffsetMs(4_444)
.setMinPlaybackSpeed(.9f)
.setMaxPlaybackSpeed(1.1f)
.build())
.setRequestMetadata(
new RequestMetadata.Builder()
.setMediaUri(Uri.parse("http://test.test"))
.setSearchQuery("search")
.setExtras(extras)
.build())
.build();
MediaItem mediaItemFromBundle = MediaItem.CREATOR.fromBundle(mediaItem.toBundle());
assertThat(mediaItemFromBundle).isEqualTo(mediaItem);
assertThat(mediaItemFromBundle.requestMetadata.extras)
.isEqualTo(mediaItem.requestMetadata.extras);
}
}
......@@ -49,6 +49,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.trackNumber).isNull();
assertThat(mediaMetadata.totalTrackCount).isNull();
assertThat(mediaMetadata.folderType).isNull();
assertThat(mediaMetadata.isBrowsable).isNull();
assertThat(mediaMetadata.isPlayable).isNull();
assertThat(mediaMetadata.recordingYear).isNull();
assertThat(mediaMetadata.recordingMonth).isNull();
......@@ -64,6 +65,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.genre).isNull();
assertThat(mediaMetadata.compilation).isNull();
assertThat(mediaMetadata.station).isNull();
assertThat(mediaMetadata.mediaType).isNull();
assertThat(mediaMetadata.extras).isNull();
}
......@@ -105,13 +107,86 @@ public class MediaMetadataTest {
}
@Test
public void roundTripViaBundle_yieldsEqualInstance() {
public void toBundleSkipsDefaultValues_fromBundleRestoresThem() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().build();
Bundle mediaMetadataBundle = mediaMetadata.toBundle();
// Check that default values are skipped when bundling.
assertThat(mediaMetadataBundle.keySet()).isEmpty();
MediaMetadata mediaMetadataFromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle);
assertThat(mediaMetadataFromBundle).isEqualTo(mediaMetadata);
// Extras is not implemented in MediaMetadata.equals(Object o).
assertThat(mediaMetadataFromBundle.extras).isNull();
}
@Test
public void createFullyPopulatedMediaMetadata_roundTripViaBundle_yieldsEqualInstance() {
MediaMetadata mediaMetadata = getFullyPopulatedMediaMetadata();
MediaMetadata fromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle());
assertThat(fromBundle).isEqualTo(mediaMetadata);
MediaMetadata mediaMetadataFromBundle =
MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle());
assertThat(mediaMetadataFromBundle).isEqualTo(mediaMetadata);
// Extras is not implemented in MediaMetadata.equals(Object o).
assertThat(fromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE);
assertThat(mediaMetadataFromBundle.extras.getString(EXTRAS_KEY)).isEqualTo(EXTRAS_VALUE);
}
@Test
public void builderSetFolderType_toNone_setsIsBrowsableToFalse() {
MediaMetadata mediaMetadata =
new MediaMetadata.Builder().setFolderType(MediaMetadata.FOLDER_TYPE_NONE).build();
assertThat(mediaMetadata.isBrowsable).isFalse();
}
@Test
public void builderSetFolderType_toNotNone_setsIsBrowsableToTrueAndMatchingMediaType() {
MediaMetadata mediaMetadata =
new MediaMetadata.Builder().setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS).build();
assertThat(mediaMetadata.isBrowsable).isTrue();
assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS);
}
@Test
public void
builderSetFolderType_toNotNoneWithManualMediaType_setsIsBrowsableToTrueAndDoesNotOverrideMediaType() {
MediaMetadata mediaMetadata =
new MediaMetadata.Builder()
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS)
.setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS)
.build();
assertThat(mediaMetadata.isBrowsable).isTrue();
assertThat(mediaMetadata.mediaType).isEqualTo(MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS);
}
@Test
public void builderSetIsBrowsable_toTrueWithoutMediaType_setsFolderTypeToMixed() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(true).build();
assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_MIXED);
}
@Test
public void builderSetIsBrowsable_toTrueWithMediaType_setsFolderTypeToMatchMediaType() {
MediaMetadata mediaMetadata =
new MediaMetadata.Builder()
.setIsBrowsable(true)
.setMediaType(MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS)
.build();
assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_ARTISTS);
}
@Test
public void builderSetFolderType_toFalse_setsFolderTypeToNone() {
MediaMetadata mediaMetadata = new MediaMetadata.Builder().setIsBrowsable(false).build();
assertThat(mediaMetadata.folderType).isEqualTo(MediaMetadata.FOLDER_TYPE_NONE);
}
private static MediaMetadata getFullyPopulatedMediaMetadata() {
......@@ -134,6 +209,7 @@ public class MediaMetadataTest {
.setTrackNumber(4)
.setTotalTrackCount(12)
.setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS)
.setIsBrowsable(true)
.setIsPlayable(true)
.setRecordingYear(2000)
.setRecordingMonth(11)
......@@ -149,6 +225,7 @@ public class MediaMetadataTest {
.setGenre("Pop")
.setCompilation("Amazing songs.")
.setStation("radio station")
.setMediaType(MediaMetadata.MEDIA_TYPE_MIXED)
.setExtras(extras)
.build();
}
......
......@@ -17,6 +17,7 @@ package androidx.media3.common;
import static com.google.common.truth.Truth.assertThat;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem.LiveConfiguration;
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
......@@ -267,7 +268,9 @@ public class TimelineTest {
/* durationUs= */ 2,
/* defaultPositionUs= */ 22,
/* windowOffsetInFirstPeriodUs= */ 222,
ImmutableList.of(AdPlaybackState.NONE),
ImmutableList.of(
new AdPlaybackState(
/* adsId= */ null, /* adGroupTimesUs...= */ 10_000, 20_000)),
new MediaItem.Builder().setMediaId("mediaId2").build()),
new TimelineWindowDefinition(
/* periodCount= */ 3,
......@@ -335,6 +338,29 @@ public class TimelineTest {
}
@Test
public void window_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
Timeline.Window window = new Timeline.Window();
// Please refrain from altering these default values since doing so would cause issues with
// backwards compatibility.
window.presentationStartTimeMs = C.TIME_UNSET;
window.windowStartTimeMs = C.TIME_UNSET;
window.elapsedRealtimeEpochOffsetMs = C.TIME_UNSET;
window.durationUs = C.TIME_UNSET;
window.mediaItem = new MediaItem.Builder().build();
Bundle windowBundle = window.toBundle();
// Check that default values are skipped when bundling.
assertThat(windowBundle.keySet()).isEmpty();
Timeline.Window restoredWindow = Timeline.Window.CREATOR.fromBundle(windowBundle);
assertThat(restoredWindow.manifest).isNull();
TimelineAsserts.assertWindowEqualsExceptUidAndManifest(
/* expectedWindow= */ window, /* actualWindow= */ restoredWindow);
}
@Test
public void roundTripViaBundle_ofWindow_yieldsEqualInstanceExceptUidAndManifest() {
Timeline.Window window = new Timeline.Window();
window.uid = new Object();
......@@ -368,6 +394,26 @@ public class TimelineTest {
}
@Test
public void period_toBundleSkipsDefaultValues_fromBundleRestoresThem() {
Timeline.Period period = new Timeline.Period();
// Please refrain from altering these default values since doing so would cause issues with
// backwards compatibility.
period.durationUs = C.TIME_UNSET;
Bundle periodBundle = period.toBundle();
// Check that default values are skipped when bundling.
assertThat(periodBundle.keySet()).isEmpty();
Timeline.Period restoredPeriod = Timeline.Period.CREATOR.fromBundle(periodBundle);
assertThat(restoredPeriod.id).isNull();
assertThat(restoredPeriod.uid).isNull();
TimelineAsserts.assertPeriodEqualsExceptIds(
/* expectedPeriod= */ period, /* actualPeriod= */ restoredPeriod);
}
@Test
public void roundTripViaBundle_ofPeriod_yieldsEqualInstanceExceptIds() {
Timeline.Period period = new Timeline.Period();
period.id = new Object();
......
......@@ -5,7 +5,6 @@ will not normally need to depend on this module directly.
## Links
* [Javadoc][]: Classes matching `androidx.media3.database.*` belong to this
module.
* [Javadoc][]
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -3,3 +3,9 @@
Provides a `DataSource` abstraction and a number of concrete implementations for
reading data from different sources. Application code will not normally need to
depend on this module directly.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -119,3 +119,9 @@ whilst still using Cronet Fallback for other networking performed by your
application.
[Send a simple request]: https://developer.android.com/guide/topics/connectivity/cronet/start
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -48,3 +48,9 @@ new DefaultDataSourceFactory(
...
/* baseDataSourceFactory= */ new OkHttpDataSource.Factory(...));
```
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -45,3 +45,9 @@ application code are required. Alternatively, if you know that your application
doesn't need to handle any other protocols, you can update any
`DataSource.Factory` instantiations in your application code to use
`RtmpDataSource.Factory` directly.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -2,3 +2,9 @@
Provides a decoder abstraction. Application code will not normally need to
depend on this module directly.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -123,3 +123,11 @@ gets from the libgav1 decoder:
Note: Although the default option uses `ANativeWindow`, based on our testing the
GL rendering mode has better performance, so should be preferred
## Links
<!-- TODO(b/204738828): Add link to 'troubleshooting using decoding extensions' media3 guide entry when it's published on developer.android.com -->
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -116,3 +116,11 @@ then implement your own logic to use the renderer for a given track.
[Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[ExoPlayer issue 2781]: https://github.com/google/ExoPlayer/issues/2781
[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension
## Links
<!-- TODO(b/204738828): Add link to 'troubleshooting using extensions' media3 guide entry when it's published on developer.android.com -->
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
set -eu
FFMPEG_MODULE_PATH=$1
NDK_PATH=$2
......
......@@ -95,3 +95,11 @@ Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation,
so you need to make sure you are passing an `LibflacAudioRenderer` to the
player, then implement your own logic to use the renderer for a given track.
## Links
<!-- TODO(b/204738828): Add link to 'troubleshooting decoding using extensions' media3 guide entry when it's published on developer.android.com -->
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -99,3 +99,11 @@ Note: These instructions assume you're using `DefaultTrackSelector`. If you have
a custom track selector the choice of `Renderer` is up to your implementation,
so you need to make sure you are passing an `LibopusAudioRenderer` to the
player, then implement your own logic to use the renderer for a given track.
## Links
<!-- TODO(b/204738828): Add link to 'troubleshooting using decoding extensions' media3 guide entry when it's published on developer.android.com -->
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -15,7 +15,7 @@
# limitations under the License.
#
set -e
set -eu
ASM_CONVERTER="./libopus/celt/arm/arm2gnu.pl"
if [[ ! -x "${ASM_CONVERTER}" ]]; then
......
......@@ -136,3 +136,11 @@ gets from the libvpx decoder:
Note: Although the default option uses `ANativeWindow`, based on our testing the
GL rendering mode has better performance, so should be preferred.
## Links
<!-- TODO(b/204738828): Add link to 'troubleshooting using decoding extensions' media3 guide entry when it's published on developer.android.com -->
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -18,7 +18,7 @@
# a bash script that generates the necessary config files for libvpx android ndk
# builds.
set -e
set -eu
if [ $# -ne 0 ]; then
echo "Usage: ${0}"
......
......@@ -17,3 +17,9 @@ Alternatively, you can clone this GitHub project and depend on the module
locally. Instructions for doing this can be found in the [top level README][].
[top level README]: ../../README.md
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -18,3 +18,9 @@ Alternatively, you can clone this GitHub project and depend on the module
locally. Instructions for doing this can be found in the [top level README][].
[top level README]: ../../README.md
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages
......@@ -250,17 +250,15 @@ public final class ExoPlaybackException extends PlaybackException {
private ExoPlaybackException(Bundle bundle) {
super(bundle);
type = bundle.getInt(keyForField(FIELD_TYPE), /* defaultValue= */ TYPE_UNEXPECTED);
rendererName = bundle.getString(keyForField(FIELD_RENDERER_NAME));
rendererIndex =
bundle.getInt(keyForField(FIELD_RENDERER_INDEX), /* defaultValue= */ C.INDEX_UNSET);
@Nullable Bundle rendererFormatBundle = bundle.getBundle(keyForField(FIELD_RENDERER_FORMAT));
type = bundle.getInt(FIELD_TYPE, /* defaultValue= */ TYPE_UNEXPECTED);
rendererName = bundle.getString(FIELD_RENDERER_NAME);
rendererIndex = bundle.getInt(FIELD_RENDERER_INDEX, /* defaultValue= */ C.INDEX_UNSET);
@Nullable Bundle rendererFormatBundle = bundle.getBundle(FIELD_RENDERER_FORMAT);
rendererFormat =
rendererFormatBundle == null ? null : Format.CREATOR.fromBundle(rendererFormatBundle);
rendererFormatSupport =
bundle.getInt(
keyForField(FIELD_RENDERER_FORMAT_SUPPORT), /* defaultValue= */ C.FORMAT_HANDLED);
isRecoverable = bundle.getBoolean(keyForField(FIELD_IS_RECOVERABLE), /* defaultValue= */ false);
bundle.getInt(FIELD_RENDERER_FORMAT_SUPPORT, /* defaultValue= */ C.FORMAT_HANDLED);
isRecoverable = bundle.getBoolean(FIELD_IS_RECOVERABLE, /* defaultValue= */ false);
mediaPeriodId = null;
}
......@@ -403,12 +401,17 @@ public final class ExoPlaybackException extends PlaybackException {
@UnstableApi
public static final Creator<ExoPlaybackException> CREATOR = ExoPlaybackException::new;
private static final int FIELD_TYPE = FIELD_CUSTOM_ID_BASE + 1;
private static final int FIELD_RENDERER_NAME = FIELD_CUSTOM_ID_BASE + 2;
private static final int FIELD_RENDERER_INDEX = FIELD_CUSTOM_ID_BASE + 3;
private static final int FIELD_RENDERER_FORMAT = FIELD_CUSTOM_ID_BASE + 4;
private static final int FIELD_RENDERER_FORMAT_SUPPORT = FIELD_CUSTOM_ID_BASE + 5;
private static final int FIELD_IS_RECOVERABLE = FIELD_CUSTOM_ID_BASE + 6;
private static final String FIELD_TYPE = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 1);
private static final String FIELD_RENDERER_NAME =
Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 2);
private static final String FIELD_RENDERER_INDEX =
Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 3);
private static final String FIELD_RENDERER_FORMAT =
Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 4);
private static final String FIELD_RENDERER_FORMAT_SUPPORT =
Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 5);
private static final String FIELD_IS_RECOVERABLE =
Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 6);
/**
* {@inheritDoc}
......@@ -420,14 +423,14 @@ public final class ExoPlaybackException extends PlaybackException {
@Override
public Bundle toBundle() {
Bundle bundle = super.toBundle();
bundle.putInt(keyForField(FIELD_TYPE), type);
bundle.putString(keyForField(FIELD_RENDERER_NAME), rendererName);
bundle.putInt(keyForField(FIELD_RENDERER_INDEX), rendererIndex);
bundle.putInt(FIELD_TYPE, type);
bundle.putString(FIELD_RENDERER_NAME, rendererName);
bundle.putInt(FIELD_RENDERER_INDEX, rendererIndex);
if (rendererFormat != null) {
bundle.putBundle(keyForField(FIELD_RENDERER_FORMAT), rendererFormat.toBundle());
bundle.putBundle(FIELD_RENDERER_FORMAT, rendererFormat.toBundle());
}
bundle.putInt(keyForField(FIELD_RENDERER_FORMAT_SUPPORT), rendererFormatSupport);
bundle.putBoolean(keyForField(FIELD_IS_RECOVERABLE), isRecoverable);
bundle.putInt(FIELD_RENDERER_FORMAT_SUPPORT, rendererFormatSupport);
bundle.putBoolean(FIELD_IS_RECOVERABLE, isRecoverable);
return bundle;
}
}
......@@ -24,6 +24,7 @@ import android.media.AudioDeviceInfo;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.os.Looper;
import android.os.Process;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
......@@ -131,15 +132,15 @@ import java.util.List;
* threading model">
*
* <ul>
* <li>ExoPlayer instances must be accessed from a single application thread. For the vast
* majority of cases this should be the application's main thread. Using the application's
* main thread is also a requirement when using ExoPlayer's UI components or the IMA
* extension. The thread on which an ExoPlayer instance must be accessed can be explicitly
* specified by passing a `Looper` when creating the player. If no `Looper` is specified, then
* the `Looper` of the thread that the player is created on is used, or if that thread does
* not have a `Looper`, the `Looper` of the application's main thread is used. In all cases
* the `Looper` of the thread from which the player must be accessed can be queried using
* {@link #getApplicationLooper()}.
* <li>ExoPlayer instances must be accessed from a single application thread unless indicated
* otherwise. For the vast majority of cases this should be the application's main thread.
* Using the application's main thread is also a requirement when using ExoPlayer's UI
* components or the IMA extension. The thread on which an ExoPlayer instance must be accessed
* can be explicitly specified by passing a `Looper` when creating the player. If no `Looper`
* is specified, then the `Looper` of the thread that the player is created on is used, or if
* that thread does not have a `Looper`, the `Looper` of the application's main thread is
* used. In all cases the `Looper` of the thread from which the player must be accessed can be
* queried using {@link #getApplicationLooper()}.
* <li>Registered listeners are called on the thread associated with {@link
* #getApplicationLooper()}. Note that this means registered listeners are called on the same
* thread which must be used to access the player.
......@@ -485,6 +486,7 @@ public interface ExoPlayer extends Player {
/* package */ long detachSurfaceTimeoutMs;
/* package */ boolean pauseAtEndOfMediaItems;
/* package */ boolean usePlatformDiagnostics;
@Nullable /* package */ Looper playbackLooper;
/* package */ boolean buildCalled;
/**
......@@ -527,6 +529,7 @@ public interface ExoPlayer extends Player {
* <li>{@code pauseAtEndOfMediaItems}: {@code false}
* <li>{@code usePlatformDiagnostics}: {@code true}
* <li>{@link Clock}: {@link Clock#DEFAULT}
* <li>{@code playbackLooper}: {@code null} (create new thread)
* </ul>
*
* @param context A {@link Context}.
......@@ -1135,6 +1138,24 @@ public interface ExoPlayer extends Player {
}
/**
* Sets the {@link Looper} that will be used for playback.
*
* <p>The backing thread should run with priority {@link Process#THREAD_PRIORITY_AUDIO} and
* should handle messages within 10ms.
*
* @param playbackLooper A {@link looper}.
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
@UnstableApi
public Builder setPlaybackLooper(Looper playbackLooper) {
checkState(!buildCalled);
this.playbackLooper = playbackLooper;
return this;
}
/**
* Builds an {@link ExoPlayer} instance.
*
* @throws IllegalStateException If this method has already been called.
......@@ -1208,6 +1229,8 @@ public interface ExoPlayer extends Player {
/**
* Adds a listener to receive audio offload events.
*
* <p>This method can be called from any thread.
*
* @param listener The listener to register.
*/
@UnstableApi
......@@ -1228,6 +1251,8 @@ public interface ExoPlayer extends Player {
/**
* Adds an {@link AnalyticsListener} to receive analytics events.
*
* <p>This method can be called from any thread.
*
* @param listener The listener to be added.
*/
void addAnalyticsListener(AnalyticsListener listener);
......@@ -1293,11 +1318,19 @@ public interface ExoPlayer extends Player {
@Deprecated
TrackSelectionArray getCurrentTrackSelections();
/** Returns the {@link Looper} associated with the playback thread. */
/**
* Returns the {@link Looper} associated with the playback thread.
*
* <p>This method may be called from any thread.
*/
@UnstableApi
Looper getPlaybackLooper();
/** Returns the {@link Clock} used for playback. */
/**
* Returns the {@link Clock} used for playback.
*
* <p>This method can be called from any thread.
*/
@UnstableApi
Clock getClock();
......
......@@ -189,7 +189,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final LoadControl loadControl;
private final BandwidthMeter bandwidthMeter;
private final HandlerWrapper handler;
private final HandlerThread internalPlaybackThread;
@Nullable private final HandlerThread internalPlaybackThread;
private final Looper playbackLooper;
private final Timeline.Window window;
private final Timeline.Period period;
......@@ -244,7 +244,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
Looper applicationLooper,
Clock clock,
PlaybackInfoUpdateListener playbackInfoUpdateListener,
PlayerId playerId) {
PlayerId playerId,
Looper playbackLooper) {
this.playbackInfoUpdateListener = playbackInfoUpdateListener;
this.renderers = renderers;
this.trackSelector = trackSelector;
......@@ -280,17 +281,23 @@ import java.util.concurrent.atomic.AtomicBoolean;
deliverPendingMessageAtStartPositionRequired = true;
Handler eventHandler = new Handler(applicationLooper);
HandlerWrapper eventHandler = clock.createHandler(applicationLooper, /* callback= */ null);
queue = new MediaPeriodQueue(analyticsCollector, eventHandler);
mediaSourceList =
new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId);
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start();
playbackLooper = internalPlaybackThread.getLooper();
handler = clock.createHandler(playbackLooper, this);
if (playbackLooper != null) {
internalPlaybackThread = null;
this.playbackLooper = playbackLooper;
} else {
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
// not normally change to this priority" is incorrect.
internalPlaybackThread =
new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO);
internalPlaybackThread.start();
this.playbackLooper = internalPlaybackThread.getLooper();
}
handler = clock.createHandler(this.playbackLooper, this);
}
public void experimentalSetForegroundModeTimeoutMs(long setForegroundModeTimeoutMs) {
......@@ -393,7 +400,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Override
public synchronized void sendMessage(PlayerMessage message) {
if (released || !internalPlaybackThread.isAlive()) {
if (released || !playbackLooper.getThread().isAlive()) {
Log.w(TAG, "Ignoring messages sent after release.");
message.markAsProcessed(/* isDelivered= */ false);
return;
......@@ -408,7 +415,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @return Whether the operations succeeded. If false, the operation timed out.
*/
public synchronized boolean setForegroundMode(boolean foregroundMode) {
if (released || !internalPlaybackThread.isAlive()) {
if (released || !playbackLooper.getThread().isAlive()) {
return true;
}
if (foregroundMode) {
......@@ -430,7 +437,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @return Whether the release succeeded. If false, the release timed out.
*/
public synchronized boolean release() {
if (released || !internalPlaybackThread.isAlive()) {
if (released || !playbackLooper.getThread().isAlive()) {
return true;
}
handler.sendEmptyMessage(MSG_RELEASE);
......@@ -1382,7 +1389,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* resetError= */ false);
loadControl.onReleased();
setState(Player.STATE_IDLE);
internalPlaybackThread.quit();
if (internalPlaybackThread != null) {
internalPlaybackThread.quit();
}
synchronized (this) {
released = true;
notifyAll();
......
......@@ -26,6 +26,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Player.RepeatMode;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
......@@ -71,7 +72,7 @@ import com.google.common.collect.ImmutableList;
private final Timeline.Period period;
private final Timeline.Window window;
private final AnalyticsCollector analyticsCollector;
private final Handler analyticsCollectorHandler;
private final HandlerWrapper analyticsCollectorHandler;
private long nextWindowSequenceNumber;
private @RepeatMode int repeatMode;
......@@ -91,7 +92,7 @@ import com.google.common.collect.ImmutableList;
* on.
*/
public MediaPeriodQueue(
AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) {
AnalyticsCollector analyticsCollector, HandlerWrapper analyticsCollectorHandler) {
this.analyticsCollector = analyticsCollector;
this.analyticsCollectorHandler = analyticsCollectorHandler;
period = new Timeline.Period();
......
......@@ -144,13 +144,13 @@ public interface RendererCapabilities {
/** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */
int MODE_SUPPORT_MASK = 0b11 << 7;
/**
* The renderer will use a decoder for fallback mimetype if possible as format's MIME type is
* unsupported
* The format's MIME type is unsupported and the renderer may use a decoder for a fallback MIME
* type.
*/
int DECODER_SUPPORT_FALLBACK_MIMETYPE = 0b10 << 7;
/** The renderer is able to use the primary decoder for the format's MIME type. */
int DECODER_SUPPORT_PRIMARY = 0b1 << 7;
/** The renderer will use a fallback decoder. */
/** The format exceeds the primary decoder's capabilities but is supported by fallback decoder */
int DECODER_SUPPORT_FALLBACK = 0;
/**
......
......@@ -15,6 +15,8 @@
*/
package androidx.media3.exoplayer;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import android.content.Context;
import android.media.AudioDeviceInfo;
import android.os.Looper;
......@@ -1004,10 +1006,16 @@ public class SimpleExoPlayer extends BasePlayer
return player.isLoading();
}
@SuppressWarnings("ForOverride") // Forwarding to ForOverride method in ExoPlayerImpl.
@Override
public void seekTo(int mediaItemIndex, long positionMs) {
@VisibleForTesting(otherwise = PROTECTED)
public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
blockUntilConstructorFinished();
player.seekTo(mediaItemIndex, positionMs);
player.seekTo(mediaItemIndex, positionMs, seekCommand, isRepeatingCurrentItem);
}
@Override
......
......@@ -96,6 +96,20 @@ public class DefaultAnalyticsCollector implements AnalyticsCollector {
eventTimes = new SparseArray<>();
}
/**
* Sets whether methods throw when using the wrong thread.
*
* <p>Do not use this method unless to support legacy use cases.
*
* @param throwsWhenUsingWrongThread Whether to throw when using the wrong thread.
* @deprecated Do not use this method and ensure all calls are made from the correct thread.
*/
@SuppressWarnings("deprecation") // Calling deprecated method.
@Deprecated
public void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) {
listeners.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread);
}
@Override
@CallSuper
public void addListener(AnalyticsListener listener) {
......
......@@ -60,6 +60,7 @@ import androidx.media3.extractor.Ac3Util;
import androidx.media3.extractor.Ac4Util;
import androidx.media3.extractor.DtsUtil;
import androidx.media3.extractor.MpegAudioUtil;
import androidx.media3.extractor.OpusUtil;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe;
import com.google.errorprone.annotations.InlineMeValidationDisabled;
......@@ -203,6 +204,8 @@ public final class DefaultAudioSink implements AudioSink {
* @param pcmFrameSize The size of the PCM frames if the {@code encoding} is PCM, 1 otherwise,
* in bytes.
* @param sampleRate The sample rate of the format, in Hz.
* @param bitrate The bitrate of the audio stream if the stream is compressed, or {@link
* Format#NO_VALUE} if {@code encoding} is PCM or the bitrate is not known.
* @param maxAudioTrackPlaybackSpeed The maximum speed the content will be played using {@link
* AudioTrack#setPlaybackParams}. 0.5 is 2x slow motion, 1 is real time, 2 is 2x fast
* forward, etc. This will be {@code 1} unless {@link
......@@ -217,6 +220,7 @@ public final class DefaultAudioSink implements AudioSink {
@OutputMode int outputMode,
int pcmFrameSize,
int sampleRate,
int bitrate,
double maxAudioTrackPlaybackSpeed);
}
......@@ -788,8 +792,9 @@ public final class DefaultAudioSink implements AudioSink {
getAudioTrackMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding),
outputEncoding,
outputMode,
outputPcmFrameSize,
outputPcmFrameSize != C.LENGTH_UNSET ? outputPcmFrameSize : 1,
outputSampleRate,
inputFormat.bitrate,
enableAudioTrackPlaybackParams ? MAX_PLAYBACK_SPEED : DEFAULT_PLAYBACK_SPEED);
offloadDisabledUntilNextConfiguration = false;
......@@ -1000,9 +1005,11 @@ public final class DefaultAudioSink implements AudioSink {
getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount());
if (!startMediaTimeUsNeedsSync
&& Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) {
listener.onAudioSinkError(
new AudioSink.UnexpectedDiscontinuityException(
presentationTimeUs, expectedPresentationTimeUs));
if (listener != null) {
listener.onAudioSinkError(
new AudioSink.UnexpectedDiscontinuityException(
presentationTimeUs, expectedPresentationTimeUs));
}
startMediaTimeUsNeedsSync = true;
}
if (startMediaTimeUsNeedsSync) {
......@@ -1785,6 +1792,8 @@ public final class DefaultAudioSink implements AudioSink {
? 0
: (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset)
* Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT);
case C.ENCODING_OPUS:
return OpusUtil.parsePacketAudioSampleCount(buffer);
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
case C.ENCODING_PCM_24BIT:
......
......@@ -19,6 +19,7 @@ import static androidx.media3.common.util.Util.constrainValue;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_OFFLOAD;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PCM;
import static com.google.common.math.IntMath.divide;
import static com.google.common.primitives.Ints.checkedCast;
import static java.lang.Math.max;
......@@ -32,7 +33,9 @@ import androidx.media3.extractor.Ac3Util;
import androidx.media3.extractor.Ac4Util;
import androidx.media3.extractor.DtsUtil;
import androidx.media3.extractor.MpegAudioUtil;
import androidx.media3.extractor.OpusUtil;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.math.RoundingMode;
/** Provide the buffer size to use when creating an {@link AudioTrack}. */
@UnstableApi
......@@ -173,10 +176,11 @@ public class DefaultAudioTrackBufferSizeProvider
@OutputMode int outputMode,
int pcmFrameSize,
int sampleRate,
int bitrate,
double maxAudioTrackPlaybackSpeed) {
int bufferSize =
get1xBufferSizeInBytes(
minBufferSizeInBytes, encoding, outputMode, pcmFrameSize, sampleRate);
minBufferSizeInBytes, encoding, outputMode, pcmFrameSize, sampleRate, bitrate);
// Maintain the buffer duration by scaling the size accordingly.
bufferSize = (int) (bufferSize * maxAudioTrackPlaybackSpeed);
// Buffer size must not be lower than the AudioTrack min buffer size for this format.
......@@ -187,12 +191,17 @@ public class DefaultAudioTrackBufferSizeProvider
/** Returns the buffer size for playback at 1x speed. */
protected int get1xBufferSizeInBytes(
int minBufferSizeInBytes, int encoding, int outputMode, int pcmFrameSize, int sampleRate) {
int minBufferSizeInBytes,
int encoding,
int outputMode,
int pcmFrameSize,
int sampleRate,
int bitrate) {
switch (outputMode) {
case OUTPUT_MODE_PCM:
return getPcmBufferSizeInBytes(minBufferSizeInBytes, sampleRate, pcmFrameSize);
case OUTPUT_MODE_PASSTHROUGH:
return getPassthroughBufferSizeInBytes(encoding);
return getPassthroughBufferSizeInBytes(encoding, bitrate);
case OUTPUT_MODE_OFFLOAD:
return getOffloadBufferSizeInBytes(encoding);
default:
......@@ -209,13 +218,16 @@ public class DefaultAudioTrackBufferSizeProvider
}
/** Returns the buffer size for passthrough playback. */
protected int getPassthroughBufferSizeInBytes(@C.Encoding int encoding) {
protected int getPassthroughBufferSizeInBytes(@C.Encoding int encoding, int bitrate) {
int bufferSizeUs = passthroughBufferDurationUs;
if (encoding == C.ENCODING_AC3) {
bufferSizeUs *= ac3BufferMultiplicationFactor;
}
int maxByteRate = getMaximumEncodedRateBytesPerSecond(encoding);
return checkedCast((long) bufferSizeUs * maxByteRate / C.MICROS_PER_SECOND);
int byteRate =
bitrate != Format.NO_VALUE
? divide(bitrate, 8, RoundingMode.CEILING)
: getMaximumEncodedRateBytesPerSecond(encoding);
return checkedCast((long) bufferSizeUs * byteRate / C.MICROS_PER_SECOND);
}
/** Returns the buffer size for offload playback. */
......@@ -255,6 +267,8 @@ public class DefaultAudioTrackBufferSizeProvider
return DtsUtil.DTS_HD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DOLBY_TRUEHD:
return Ac3Util.TRUEHD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_OPUS:
return OpusUtil.MAX_BYTES_PER_SECOND;
case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
case C.ENCODING_PCM_24BIT:
......
......@@ -245,7 +245,8 @@ public final class MediaCodecInfo {
}
/**
* Returns whether the decoder may support decoding the given {@code format}.
* Returns whether the decoder may support decoding the given {@code format} both functionally and
* performantly.
*
* @param format The input media format.
* @return Whether the decoder may support decoding the given {@code format}.
......@@ -256,7 +257,7 @@ public final class MediaCodecInfo {
return false;
}
if (!isCodecProfileAndLevelSupported(format)) {
if (!isCodecProfileAndLevelSupported(format, /* checkPerformanceCapabilities= */ true)) {
return false;
}
......@@ -283,15 +284,24 @@ public final class MediaCodecInfo {
}
}
/**
* Returns whether the decoder may functionally support decoding the given {@code format}.
*
* @param format The input media format.
* @return Whether the decoder may functionally support decoding the given {@code format}.
*/
public boolean isFormatFunctionallySupported(Format format) {
return isSampleMimeTypeSupported(format)
&& isCodecProfileAndLevelSupported(format, /* checkPerformanceCapabilities= */ false);
}
private boolean isSampleMimeTypeSupported(Format format) {
return mimeType.equals(format.sampleMimeType)
|| mimeType.equals(MediaCodecUtil.getAlternativeCodecMimeType(format));
}
private boolean isCodecProfileAndLevelSupported(Format format) {
if (format.codecs == null) {
return true;
}
private boolean isCodecProfileAndLevelSupported(
Format format, boolean checkPerformanceCapabilities) {
Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format);
if (codecProfileAndLevel == null) {
// If we don't know any better, we assume that the profile and level are supported.
......@@ -327,7 +337,7 @@ public final class MediaCodecInfo {
for (CodecProfileLevel profileLevel : profileLevels) {
if (profileLevel.profile == profile
&& profileLevel.level >= level
&& (profileLevel.level >= level || !checkPerformanceCapabilities)
&& !needsProfileExcludedWorkaround(mimeType, profile)) {
return true;
}
......
......@@ -1113,6 +1113,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
}
codecInitializedTimestamp = SystemClock.elapsedRealtime();
if (!codecInfo.isFormatSupported(inputFormat)) {
Log.w(
TAG,
Util.formatInvariant(
"Format exceeds selected codec's capabilities [%s, %s]",
Format.toLogString(inputFormat), codecName));
}
this.codecInfo = codecInfo;
this.codecOperatingRate = codecOperatingRate;
codecInputFormat = inputFormat;
......@@ -2425,7 +2433,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
|| (Util.SDK_INT <= 17 && "OMX.allwinner.video.decoder.avc".equals(name))
|| (Util.SDK_INT <= 29
&& ("OMX.broadcom.video_decoder.tunnel".equals(name)
|| "OMX.broadcom.video_decoder.tunnel.secure".equals(name)))
|| "OMX.broadcom.video_decoder.tunnel.secure".equals(name)
|| "OMX.bcm.vdec.avc.tunnel".equals(name)
|| "OMX.bcm.vdec.avc.tunnel.secure".equals(name)
|| "OMX.bcm.vdec.hevc.tunnel".equals(name)
|| "OMX.bcm.vdec.hevc.tunnel.secure".equals(name)))
|| ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
}
......
......@@ -190,22 +190,15 @@ public final class MediaCodecUtil {
}
/**
* Returns a copy of the provided decoder list sorted such that decoders with format support are
* listed first. The returned list is modifiable for convenience.
* Returns a copy of the provided decoder list sorted such that decoders with functional format
* support are listed first. The returned list is modifiable for convenience.
*/
@CheckResult
public static List<MediaCodecInfo> getDecoderInfosSortedByFormatSupport(
List<MediaCodecInfo> decoderInfos, Format format) {
decoderInfos = new ArrayList<>(decoderInfos);
sortByScore(
decoderInfos,
decoderInfo -> {
try {
return decoderInfo.isFormatSupported(format) ? 1 : 0;
} catch (DecoderQueryException e) {
return -1;
}
});
decoderInfos, decoderInfo -> decoderInfo.isFormatFunctionallySupported(format) ? 1 : 0);
return decoderInfos;
}
......
......@@ -40,7 +40,16 @@ import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** A {@link Service} for downloading media. */
/**
* A {@link Service} for downloading media.
*
* <p>Apps with target SDK 33 and greater need to add the {@code
* android.permission.POST_NOTIFICATIONS} permission to the manifest and request the permission at
* runtime before starting downloads. Without that permission granted by the user, notifications
* posted by this service are not displayed. See <a
* href="https://developer.android.com/develop/ui/views/notifications/notification-permission">the
* official UI guide</a> for more detailed information.
*/
@UnstableApi
public abstract class DownloadService extends Service {
......@@ -574,6 +583,17 @@ public abstract class DownloadService extends Service {
Util.startForegroundService(context, intent);
}
/**
* Clear all {@linkplain DownloadManagerHelper download manager helpers} before restarting the
* service.
*
* <p>Calling this method is normally only required if an app supports downloading content for
* multiple users for which different download directories should be used.
*/
public static void clearDownloadManagerHelpers() {
downloadManagerHelpers.clear();
}
@Override
public void onCreate() {
if (channelId != null) {
......
......@@ -61,7 +61,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
private static final int MSG_UPDATE_TIMELINE = 4;
private static final int MSG_ON_COMPLETION = 5;
private static final MediaItem EMPTY_MEDIA_ITEM =
private static final MediaItem PLACEHOLDER_MEDIA_ITEM =
new MediaItem.Builder().setUri(Uri.EMPTY).build();
// Accessed on any thread.
......@@ -451,7 +451,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
public MediaItem getMediaItem() {
// This method is actually never called because getInitialTimeline is implemented and hence the
// MaskingMediaSource does not need to create a placeholder timeline for this media source.
return EMPTY_MEDIA_ITEM;
return PLACEHOLDER_MEDIA_ITEM;
}
@Override
......@@ -1018,7 +1018,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
@Override
public MediaItem getMediaItem() {
return EMPTY_MEDIA_ITEM;
return PLACEHOLDER_MEDIA_ITEM;
}
@Override
......
......@@ -118,17 +118,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
for (int i = 0; i < selections.length; i++) {
Integer streamChildIndex = streams[i] == null ? null : streamPeriodIndices.get(streams[i]);
streamChildIndices[i] = streamChildIndex == null ? C.INDEX_UNSET : streamChildIndex;
selectionChildIndices[i] = C.INDEX_UNSET;
if (selections[i] != null) {
TrackGroup mergedTrackGroup = selections[i].getTrackGroup();
TrackGroup childTrackGroup =
checkNotNull(childTrackGroupByMergedTrackGroup.get(mergedTrackGroup));
for (int j = 0; j < periods.length; j++) {
if (periods[j].getTrackGroups().indexOf(childTrackGroup) != C.INDEX_UNSET) {
selectionChildIndices[i] = j;
break;
}
}
// mergedTrackGroup.id is 'periods array index' + ":" + childTrackGroup.id
selectionChildIndices[i] =
Integer.parseInt(mergedTrackGroup.id.substring(0, mergedTrackGroup.id.indexOf(":")));
} else {
selectionChildIndices[i] = C.INDEX_UNSET;
}
}
streamPeriodIndices.clear();
......
......@@ -72,7 +72,7 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
}
private static final int PERIOD_COUNT_UNSET = -1;
private static final MediaItem EMPTY_MEDIA_ITEM =
private static final MediaItem PLACEHOLDER_MEDIA_ITEM =
new MediaItem.Builder().setMediaId("MergingMediaSource").build();
private final boolean adjustPeriodTimeOffsets;
......@@ -163,7 +163,7 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
@Override
public MediaItem getMediaItem() {
return mediaSources.length > 0 ? mediaSources[0].getMediaItem() : EMPTY_MEDIA_ITEM;
return mediaSources.length > 0 ? mediaSources[0].getMediaItem() : PLACEHOLDER_MEDIA_ITEM;
}
@Override
......
......@@ -15,10 +15,7 @@
*/
package androidx.media3.exoplayer.source;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
import androidx.media3.common.C;
......@@ -26,11 +23,8 @@ import androidx.media3.common.TrackGroup;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;
/**
......@@ -118,21 +112,13 @@ public final class TrackGroupArray implements Bundleable {
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_TRACK_GROUPS,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUPS = 0;
private static final String FIELD_TRACK_GROUPS = Util.intToStringMaxRadix(0);
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_TRACK_GROUPS), BundleableUtil.toBundleArrayList(trackGroups));
FIELD_TRACK_GROUPS, BundleableUtil.toBundleArrayList(trackGroups));
return bundle;
}
......@@ -140,8 +126,7 @@ public final class TrackGroupArray implements Bundleable {
public static final Creator<TrackGroupArray> CREATOR =
bundle -> {
@Nullable
List<Bundle> trackGroupBundles =
bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS));
List<Bundle> trackGroupBundles = bundle.getParcelableArrayList(FIELD_TRACK_GROUPS);
if (trackGroupBundles == null) {
return new TrackGroupArray();
}
......@@ -163,8 +148,4 @@ public final class TrackGroupArray implements Bundleable {
}
}
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
......@@ -427,7 +427,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
@SideEffectFree
private long getCurrentEventTimeUs(long positionUs) {
int nextEventTimeIndex = subtitle.getNextEventTimeIndex(positionUs);
if (nextEventTimeIndex == 0) {
if (nextEventTimeIndex == 0 || subtitle.getEventTimeCount() == 0) {
return subtitle.timeUs;
}
......
......@@ -2050,7 +2050,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
}
private void handleFrameRendered(long presentationTimeUs) {
if (this != tunnelingOnFrameRenderedListener) {
if (this != tunnelingOnFrameRenderedListener || getCodec() == null) {
// Stale event.
return;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment