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

1.0.0 rc01
This commit is contained in:
christosts 2023-02-16 17:03:01 +00:00 committed by GitHub
commit f17e846d3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
347 changed files with 27215 additions and 5828 deletions

View File

@ -17,6 +17,7 @@ body:
label: Media3 Version label: Media3 Version
description: What version of Media3 are you using? description: What version of Media3 are you using?
options: options:
- 1.0.0-rc01
- 1.0.0-beta03 - 1.0.0-beta03
- 1.0.0-beta02 - 1.0.0-beta02
- 1.0.0-beta01 - 1.0.0-beta01

View File

@ -5,15 +5,13 @@ Android, including local playback (via ExoPlayer) and media sessions.
## Current status ## Current status
AndroidX Media is currently in beta and we welcome your feedback via the AndroidX Media is currently in release candidate and we welcome your feedback
[issue tracker][]. Please consult the [release notes][] for more details about via the [issue tracker][]. Please consult the [release notes][] for more details
the beta release. about the current release.
ExoPlayer's new home will be in AndroidX Media, but for now we are publishing it 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 both in AndroidX Media and via the existing [ExoPlayer project][] and we are
AndroidX Media is in beta we recommend that production apps using ExoPlayer still handling ExoPlayer issues on the [ExoPlayer issue tracker][].
continue to depend on the existing ExoPlayer project. We are still handling
ExoPlayer issues on the [ExoPlayer issue tracker][].
You'll find some [Media3 documentation on developer.android.com][], including a You'll find some [Media3 documentation on developer.android.com][], including a
[migration guide for existing ExoPlayer and MediaSession users][]. [migration guide for existing ExoPlayer and MediaSession users][].

View File

@ -1,4 +1,101 @@
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) ### 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 * Query the platform (API 29+) or assume the audio encoding channel count
for audio passthrough when the format audio channel count is unset, for audio passthrough when the format audio channel count is unset,
which occurs with HLS chunkless preparation 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 * Configure `AudioTrack` with channel mask
`AudioFormat.CHANNEL_OUT_7POINT1POINT4` if the decoder outputs 12 `AudioFormat.CHANNEL_OUT_7POINT1POINT4` if the decoder outputs 12
channel PCM audio channel PCM audio
([#10322](#https://github.com/google/ExoPlayer/pull/10322). ([#10322](#https://github.com/google/ExoPlayer/pull/10322)).
* DRM * DRM
* Ensure the DRM session is always correctly updated when seeking * Ensure the DRM session is always correctly updated when seeking
immediately after a format change immediately after a format change
([10274](https://github.com/google/ExoPlayer/issues/10274)). ([#10274](https://github.com/google/ExoPlayer/issues/10274)).
* Text: * Text:
* Change `Player.getCurrentCues()` to return `CueGroup` instead of * Change `Player.getCurrentCues()` to return `CueGroup` instead of
`List<Cue>`. `List<Cue>`.

View File

@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
project.ext { project.ext {
releaseVersion = '1.0.0-beta03' releaseVersion = '1.0.0-rc01'
releaseVersionCode = 1_000_000_1_03 releaseVersionCode = 1_000_000_2_01
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 33 appTargetSdkVersion = 33
// API version before restricting local file access. // API version before restricting local file access.

View File

@ -23,6 +23,7 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <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.software.leanback" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/> <uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
@ -35,6 +36,7 @@
android:largeHeap="true" android:largeHeap="true"
android:allowBackup="false" android:allowBackup="false"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:name="androidx.multidex.MultiDexApplication" android:name="androidx.multidex.MultiDexApplication"
tools:targetApi="29"> tools:targetApi="29">

View File

@ -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.checkNotNull;
import static com.google.common.base.Preconditions.checkState; import static com.google.common.base.Preconditions.checkState;
import android.Manifest;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
@ -41,8 +42,10 @@ import android.widget.ExpandableListView.OnChildClickListener;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn; import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaItem.ClippingConfiguration; 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 TAG = "SampleChooserActivity";
private static final String GROUP_POSITION_PREFERENCE_KEY = "sample_chooser_group_position"; 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 String CHILD_POSITION_PREFERENCE_KEY = "sample_chooser_child_position";
private static final int POST_NOTIFICATION_PERMISSION_REQUEST_CODE = 100;
private String[] uris; private String[] uris;
private boolean useExtensionRenderers; private boolean useExtensionRenderers;
@ -83,6 +87,8 @@ public class SampleChooserActivity extends AppCompatActivity
private SampleAdapter sampleAdapter; private SampleAdapter sampleAdapter;
private MenuItem preferExtensionDecodersMenuItem; private MenuItem preferExtensionDecodersMenuItem;
private ExpandableListView sampleListView; private ExpandableListView sampleListView;
@Nullable private MediaItem downloadMediaItemWaitingForNotificationPermission;
private boolean notificationPermissionToastShown;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -172,12 +178,34 @@ public class SampleChooserActivity extends AppCompatActivity
public void onRequestPermissionsResult( public void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) { int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, 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) { if (grantResults.length == 0) {
// Empty results are triggered if a permission is requested while another request was already // Empty results are triggered if a permission is requested while another request was already
// pending and can be safely ignored in this case. // pending and can be safely ignored in this case.
return; return;
} } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
loadSample(); loadSample();
} else { } else {
Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG) Toast.makeText(getApplicationContext(), R.string.sample_list_load_error, Toast.LENGTH_LONG)
@ -244,15 +272,26 @@ public class SampleChooserActivity extends AppCompatActivity
if (downloadUnsupportedStringId != 0) { if (downloadUnsupportedStringId != 0) {
Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG) Toast.makeText(getApplicationContext(), downloadUnsupportedStringId, Toast.LENGTH_LONG)
.show(); .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 { } else {
RenderersFactory renderersFactory = toggleDownload(playlistHolder.mediaItems.get(0));
DemoUtil.buildRenderersFactory(
/* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload(
getSupportFragmentManager(), playlistHolder.mediaItems.get(0), renderersFactory);
} }
} }
private void toggleDownload(MediaItem mediaItem) {
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory(
/* context= */ this, isNonNullAndChecked(preferExtensionDecodersMenuItem));
downloadTracker.toggleDownload(getSupportFragmentManager(), mediaItem, renderersFactory);
}
private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) { private int getDownloadUnsupportedStringId(PlaylistHolder playlistHolder) {
if (playlistHolder.mediaItems.size() > 1) { if (playlistHolder.mediaItems.size() > 1) {
return R.string.download_playlist_unsupported; return R.string.download_playlist_unsupported;
@ -630,4 +669,13 @@ public class SampleChooserActivity extends AppCompatActivity
this.playlists = new ArrayList<>(); this.playlists = new ArrayList<>();
} }
} }
@RequiresApi(33)
private static class Api33 {
@DoNotInline
public static String getPostNotificationPermissionString() {
return Manifest.permission.POST_NOTIFICATIONS;
}
}
} }

View File

@ -45,6 +45,8 @@
<string name="sample_list_load_error">One or more sample lists failed to load</string> <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">Failed to start download</string>
<string name="download_start_error_offline_license">Failed to obtain offline license</string> <string name="download_start_error_offline_license">Failed to obtain offline license</string>

View File

@ -31,7 +31,7 @@ android {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true multiDexEnabled true
} }

View File

@ -38,7 +38,7 @@ import com.google.common.util.concurrent.ListenableFuture
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var browserFuture: ListenableFuture<MediaBrowser> private lateinit var browserFuture: ListenableFuture<MediaBrowser>
private val browser: 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 mediaListAdapter: FolderMediaItemArrayAdapter
private lateinit var mediaListView: ListView private lateinit var mediaListView: ListView

View File

@ -20,11 +20,6 @@ import android.net.Uri
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.SubtitleConfiguration import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.MediaMetadata 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 androidx.media3.common.util.Util
import com.google.common.collect.ImmutableList import com.google.common.collect.ImmutableList
import org.json.JSONObject import org.json.JSONObject
@ -67,7 +62,8 @@ object MediaItemTree {
title: String, title: String,
mediaId: String, mediaId: String,
isPlayable: Boolean, isPlayable: Boolean,
@MediaMetadata.FolderType folderType: Int, isBrowsable: Boolean,
mediaType: @MediaMetadata.MediaType Int,
subtitleConfigurations: List<SubtitleConfiguration> = mutableListOf(), subtitleConfigurations: List<SubtitleConfiguration> = mutableListOf(),
album: String? = null, album: String? = null,
artist: String? = null, artist: String? = null,
@ -81,9 +77,10 @@ object MediaItemTree {
.setTitle(title) .setTitle(title)
.setArtist(artist) .setArtist(artist)
.setGenre(genre) .setGenre(genre)
.setFolderType(folderType) .setIsBrowsable(isBrowsable)
.setIsPlayable(isPlayable) .setIsPlayable(isPlayable)
.setArtworkUri(imageUri) .setArtworkUri(imageUri)
.setMediaType(mediaType)
.build() .build()
return MediaItem.Builder() return MediaItem.Builder()
@ -109,7 +106,8 @@ object MediaItemTree {
title = "Root Folder", title = "Root Folder",
mediaId = ROOT_ID, mediaId = ROOT_ID,
isPlayable = false, isPlayable = false,
folderType = FOLDER_TYPE_MIXED isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
) )
) )
treeNodes[ALBUM_ID] = treeNodes[ALBUM_ID] =
@ -118,7 +116,8 @@ object MediaItemTree {
title = "Album Folder", title = "Album Folder",
mediaId = ALBUM_ID, mediaId = ALBUM_ID,
isPlayable = false, isPlayable = false,
folderType = FOLDER_TYPE_MIXED isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
) )
) )
treeNodes[ARTIST_ID] = treeNodes[ARTIST_ID] =
@ -127,7 +126,8 @@ object MediaItemTree {
title = "Artist Folder", title = "Artist Folder",
mediaId = ARTIST_ID, mediaId = ARTIST_ID,
isPlayable = false, isPlayable = false,
folderType = FOLDER_TYPE_MIXED isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS
) )
) )
treeNodes[GENRE_ID] = treeNodes[GENRE_ID] =
@ -136,7 +136,8 @@ object MediaItemTree {
title = "Genre Folder", title = "Genre Folder",
mediaId = GENRE_ID, mediaId = GENRE_ID,
isPlayable = false, isPlayable = false,
folderType = FOLDER_TYPE_MIXED isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_GENRES
) )
) )
treeNodes[ROOT_ID]!!.addChild(ALBUM_ID) treeNodes[ROOT_ID]!!.addChild(ALBUM_ID)
@ -188,7 +189,8 @@ object MediaItemTree {
title = title, title = title,
mediaId = idInTree, mediaId = idInTree,
isPlayable = true, isPlayable = true,
folderType = FOLDER_TYPE_NONE, isBrowsable = false,
mediaType = MediaMetadata.MEDIA_TYPE_MUSIC,
subtitleConfigurations, subtitleConfigurations,
album = album, album = album,
artist = artist, artist = artist,
@ -207,7 +209,8 @@ object MediaItemTree {
title = album, title = album,
mediaId = albumFolderIdInTree, mediaId = albumFolderIdInTree,
isPlayable = true, isPlayable = true,
folderType = FOLDER_TYPE_ALBUMS, isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_ALBUM,
subtitleConfigurations subtitleConfigurations
) )
) )
@ -223,7 +226,8 @@ object MediaItemTree {
title = artist, title = artist,
mediaId = artistFolderIdInTree, mediaId = artistFolderIdInTree,
isPlayable = true, isPlayable = true,
folderType = FOLDER_TYPE_ARTISTS, isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_ARTIST,
subtitleConfigurations subtitleConfigurations
) )
) )
@ -239,7 +243,8 @@ object MediaItemTree {
title = genre, title = genre,
mediaId = genreFolderIdInTree, mediaId = genreFolderIdInTree,
isPlayable = true, isPlayable = true,
folderType = FOLDER_TYPE_GENRES, isBrowsable = true,
mediaType = MediaMetadata.MEDIA_TYPE_GENRE,
subtitleConfigurations subtitleConfigurations
) )
) )
@ -262,7 +267,7 @@ object MediaItemTree {
fun getRandomItem(): MediaItem { fun getRandomItem(): MediaItem {
var curRoot = getRootItem() var curRoot = getRootItem()
while (curRoot.mediaMetadata.folderType != FOLDER_TYPE_NONE) { while (curRoot.mediaMetadata.isBrowsable == true) {
val children = getChildren(curRoot.mediaId)!! val children = getChildren(curRoot.mediaId)!!
curRoot = children.random() curRoot = children.random()
} }

View File

@ -15,22 +15,21 @@
*/ */
package androidx.media3.demo.session package androidx.media3.demo.session
import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.NotificationChannel
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.NotificationManager
import android.app.PendingIntent.*
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.util.Util
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton import androidx.media3.session.*
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.ControllerInfo 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.collect.ImmutableList
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
@ -51,6 +50,8 @@ class PlaybackService : MediaLibraryService() {
"android.media3.session.demo.SHUFFLE_ON" "android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF = private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_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() { override fun onCreate() {
@ -66,15 +67,23 @@ class PlaybackService : MediaLibraryService() {
) )
customLayout = ImmutableList.of(customCommands[0]) customLayout = ImmutableList.of(customCommands[0])
initializeSessionAndPlayer() initializeSessionAndPlayer()
setListener(MediaSessionServiceListener())
} }
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession { override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
return mediaLibrarySession return mediaLibrarySession
} }
override fun onTaskRemoved(rootIntent: Intent?) {
if (!player.playWhenReady) {
stopSelf()
}
}
override fun onDestroy() { override fun onDestroy() {
player.release() player.release()
mediaLibrarySession.release() mediaLibrarySession.release()
clearListener()
super.onDestroy() super.onDestroy()
} }
@ -253,4 +262,49 @@ class PlaybackService : MediaLibraryService() {
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) { private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */ /* 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)
}
} }

View File

@ -24,4 +24,9 @@
<string name="no_item_prompt"> <string name="no_item_prompt">
"! No media in the play list !\nPlease try to add more from browser" "! No media in the play list !\nPlease try to add more from browser"
</string> </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> </resources>

View File

@ -1,386 +0,0 @@
#!/bin/bash
# Copyright (C) 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.
##
shopt -s extglob
PACKAGE_MAPPINGS='com.google.android.exoplayer2 androidx.media3.exoplayer
com.google.android.exoplayer2.analytics androidx.media3.exoplayer.analytics
com.google.android.exoplayer2.audio androidx.media3.exoplayer.audio
com.google.android.exoplayer2.castdemo androidx.media3.demo.cast
com.google.android.exoplayer2.database androidx.media3.database
com.google.android.exoplayer2.decoder androidx.media3.decoder
com.google.android.exoplayer2.demo androidx.media3.demo.main
com.google.android.exoplayer2.drm androidx.media3.exoplayer.drm
com.google.android.exoplayer2.ext.av1 androidx.media3.decoder.av1
com.google.android.exoplayer2.ext.cast androidx.media3.cast
com.google.android.exoplayer2.ext.cronet androidx.media3.datasource.cronet
com.google.android.exoplayer2.ext.ffmpeg androidx.media3.decoder.ffmpeg
com.google.android.exoplayer2.ext.flac androidx.media3.decoder.flac
com.google.android.exoplayer2.ext.ima androidx.media3.exoplayer.ima
com.google.android.exoplayer2.ext.leanback androidx.media3.ui.leanback
com.google.android.exoplayer2.ext.okhttp androidx.media3.datasource.okhttp
com.google.android.exoplayer2.ext.opus androidx.media3.decoder.opus
com.google.android.exoplayer2.ext.rtmp androidx.media3.datasource.rtmp
com.google.android.exoplayer2.ext.vp9 androidx.media3.decoder.vp9
com.google.android.exoplayer2.ext.workmanager androidx.media3.exoplayer.workmanager
com.google.android.exoplayer2.extractor androidx.media3.extractor
com.google.android.exoplayer2.gldemo androidx.media3.demo.gl
com.google.android.exoplayer2.mediacodec androidx.media3.exoplayer.mediacodec
com.google.android.exoplayer2.metadata androidx.media3.extractor.metadata
com.google.android.exoplayer2.offline androidx.media3.exoplayer.offline
com.google.android.exoplayer2.playbacktests androidx.media3.test.exoplayer.playback
com.google.android.exoplayer2.robolectric androidx.media3.test.utils.robolectric
com.google.android.exoplayer2.scheduler androidx.media3.exoplayer.scheduler
com.google.android.exoplayer2.source androidx.media3.exoplayer.source
com.google.android.exoplayer2.source.dash androidx.media3.exoplayer.dash
com.google.android.exoplayer2.source.hls androidx.media3.exoplayer.hls
com.google.android.exoplayer2.source.rtsp androidx.media3.exoplayer.rtsp
com.google.android.exoplayer2.source.smoothstreaming androidx.media3.exoplayer.smoothstreaming
com.google.android.exoplayer2.surfacedemo androidx.media3.demo.surface
com.google.android.exoplayer2.testdata androidx.media3.test.data
com.google.android.exoplayer2.testutil androidx.media3.test.utils
com.google.android.exoplayer2.text androidx.media3.extractor.text
com.google.android.exoplayer2.trackselection androidx.media3.exoplayer.trackselection
com.google.android.exoplayer2.transformer androidx.media3.transformer
com.google.android.exoplayer2.transformerdemo androidx.media3.demo.transformer
com.google.android.exoplayer2.ui androidx.media3.ui
com.google.android.exoplayer2.upstream androidx.media3.datasource
com.google.android.exoplayer2.upstream.cache androidx.media3.datasource.cache
com.google.android.exoplayer2.upstream.crypto androidx.media3.exoplayer.upstream.crypto
com.google.android.exoplayer2.util androidx.media3.common.util
com.google.android.exoplayer2.util androidx.media3.exoplayer.util
com.google.android.exoplayer2.video androidx.media3.exoplayer.video'
CLASS_RENAMINGS='com.google.android.exoplayer2.ui.StyledPlayerView androidx.media3.ui.PlayerView
StyledPlayerView PlayerView
com.google.android.exoplayer2.ui.StyledPlayerControlView androidx.media3.ui.PlayerControlView
StyledPlayerControlView PlayerControlView
com.google.android.exoplayer2.ExoPlayerLibraryInfo androidx.media3.common.MediaLibraryInfo
ExoPlayerLibraryInfo MediaLibraryInfo
com.google.android.exoplayer2.SimpleExoPlayer androidx.media3.exoplayer.ExoPlayer
SimpleExoPlayer ExoPlayer'
CLASS_MAPPINGS='com.google.android.exoplayer2.text.span androidx.media3.common.text HorizontalTextInVerticalContextSpan LanguageFeatureSpan RubySpan SpanUtil TextAnnotation TextEmphasisSpan
com.google.android.exoplayer2.text androidx.media3.common.text CueGroup Cue
com.google.android.exoplayer2.text androidx.media3.exoplayer.text ExoplayerCuesDecoder SubtitleDecoderFactory TextOutput TextRenderer
com.google.android.exoplayer2.upstream.crypto androidx.media3.datasource AesCipherDataSource AesCipherDataSink AesFlushingCipher
com.google.android.exoplayer2.util androidx.media3.common.util AtomicFile Assertions BundleableUtil BundleUtil Clock ClosedSource CodecSpecificDataUtil ColorParser ConditionVariable Consumer CopyOnWriteMultiset EGLSurfaceTexture GlProgram GlUtil HandlerWrapper LibraryLoader ListenerSet Log LongArray MediaFormatUtil NetworkTypeObserver NonNullApi NotificationUtil ParsableBitArray ParsableByteArray RepeatModeUtil RunnableFutureTask SystemClock SystemHandlerWrapper TimedValueQueue TimestampAdjuster TraceUtil UnknownNull UnstableApi UriUtil Util XmlPullParserUtil
com.google.android.exoplayer2.util androidx.media3.common ErrorMessageProvider FlagSet FileTypes MimeTypes PriorityTaskManager
com.google.android.exoplayer2.metadata androidx.media3.common Metadata
com.google.android.exoplayer2.metadata androidx.media3.exoplayer.metadata MetadataDecoderFactory MetadataOutput MetadataRenderer
com.google.android.exoplayer2.audio androidx.media3.common AudioAttributes AuxEffectInfo
com.google.android.exoplayer2.ui androidx.media3.common AdOverlayInfo AdViewProvider
com.google.android.exoplayer2.source.ads androidx.media3.common AdPlaybackState
com.google.android.exoplayer2.source androidx.media3.common MediaPeriodId TrackGroup
com.google.android.exoplayer2.offline androidx.media3.common StreamKey
com.google.android.exoplayer2.ui androidx.media3.exoplayer.offline DownloadNotificationHelper
com.google.android.exoplayer2.trackselection androidx.media3.common TrackSelectionParameters TrackSelectionOverride
com.google.android.exoplayer2.video androidx.media3.common ColorInfo VideoSize
com.google.android.exoplayer2.upstream androidx.media3.common DataReader
com.google.android.exoplayer2.upstream androidx.media3.exoplayer.upstream Allocation Allocator BandwidthMeter CachedRegionTracker DefaultAllocator DefaultBandwidthMeter DefaultLoadErrorHandlingPolicy Loader LoaderErrorThrower ParsingLoadable SlidingPercentile TimeToFirstByteEstimator
com.google.android.exoplayer2.audio androidx.media3.extractor AacUtil Ac3Util Ac4Util DtsUtil MpegAudioUtil OpusUtil WavUtil
com.google.android.exoplayer2.util androidx.media3.extractor NalUnitUtil ParsableNalUnitBitArray
com.google.android.exoplayer2.video androidx.media3.extractor AvcConfig DolbyVisionConfig HevcConfig
com.google.android.exoplayer2.decoder androidx.media3.exoplayer DecoderCounters DecoderReuseEvaluation
com.google.android.exoplayer2.util androidx.media3.exoplayer MediaClock StandaloneMediaClock
com.google.android.exoplayer2 androidx.media3.exoplayer FormatHolder PlayerMessage
com.google.android.exoplayer2 androidx.media3.common BasePlayer BundleListRetriever Bundleable ControlDispatcher C DefaultControlDispatcher DeviceInfo ErrorMessageProvider ExoPlayerLibraryInfo Format ForwardingPlayer HeartRating IllegalSeekPositionException MediaItem MediaMetadata ParserException PercentageRating PlaybackException PlaybackParameters Player PositionInfo Rating StarRating ThumbRating Timeline Tracks
com.google.android.exoplayer2.drm androidx.media3.common DrmInitData'
DEPENDENCY_MAPPINGS='exoplayer media3-exoplayer
exoplayer-common media3-common
exoplayer-core media3-exoplayer
exoplayer-dash media3-exoplayer-dash
exoplayer-database media3-database
exoplayer-datasource media-datasource
exoplayer-decoder media3-decoder
exoplayer-extractor media3-extractor
exoplayer-hls media3-exoplayer-hls
exoplayer-robolectricutils media3-test-utils-robolectric
exoplayer-rtsp media3-exoplayer-rtsp
exoplayer-smoothstreaming media3-exoplayer-smoothstreaming
exoplayer-testutils media3-test-utils
exoplayer-transformer media3-transformer
exoplayer-ui media3-ui
extension-cast media3-cast
extension-cronet media3-datasource-cronet
extension-ima media3-exoplayer-ima
extension-leanback media3-ui-leanback
extension-okhttp media3-datasource-okhttp
extension-rtmp media3-datasource-rtmp
extension-workmanager media3-exoplayer-workmanager'
# Rewrites classes, packages and dependencies from the legacy ExoPlayer package structure
# to androidx.media3 structure.
MEDIA3_VERSION="1.0.0-beta02"
LEGACY_PEER_VERSION="2.18.1"
function usage() {
echo "usage: $0 [-p|-c|-d|-v]|[-m|-l [-x <path>] [-f] PROJECT_ROOT]"
echo " PROJECT_ROOT: path to your project root (location of 'gradlew')"
echo " -p: list package mappings and then exit"
echo " -c: list class mappings (precedence over package mappings) and then exit"
echo " -d: list dependency mappings and then exit"
echo " -m: migrate packages, classes and dependencies to AndroidX Media3"
echo " -l: list files that will be considered for rewrite and then exit"
echo " -x: exclude the path from the list of file to be changed: 'app/src/test'"
echo " -f: force the action even when validation fails"
echo " -v: print the exoplayer2/media3 version strings of this script and exit"
echo " --noclean : Do not call './gradlew clean' in project directory."
echo " -h, --help: show this help text"
}
function print_pairs {
while read -r line;
do
IFS=' ' read -ra PAIR <<< "$line"
printf "%-55s %-30s\n" "${PAIR[0]}" "${PAIR[1]}"
done <<< "$(echo "$@")"
}
function print_class_mappings {
while read -r mapping;
do
old=$(echo "$mapping" | cut -d ' ' -f1)
new=$(echo "$mapping" | cut -d ' ' -f2)
classes=$(echo "$mapping" | cut -d ' ' -f3-)
for clazz in $classes;
do
printf "%-80s %-30s\n" "$old.$clazz" "$new.$clazz"
done
done <<< "$(echo "$CLASS_MAPPINGS" | sort)"
}
ERROR_COUNTER=0
VALIDATION_ERRORS=''
function add_validation_error {
let ERROR_COUNTER++
VALIDATION_ERRORS+="\033[31m[$ERROR_COUNTER] ->\033[0m ${1}"
}
function validate_exoplayer_version() {
has_exoplayer_dependency=''
while read -r file;
do
local version
version=$(grep -m 1 "com\.google\.android\.exoplayer:" "$file" | cut -d ":" -f3 | tr -d \" | tr -d \')
if [[ ! -z $version ]] && [[ ! "$version" =~ $LEGACY_PEER_VERSION ]];
then
add_validation_error "The version does not match '$LEGACY_PEER_VERSION'. \
Update to '$LEGACY_PEER_VERSION' or use the migration script matching your \
current version. Current version '$version' found in\n $file\n"
fi
done <<< "$(find . -type f -name "build.gradle")"
}
function validate_string_not_contained {
local pattern=$1 # regex
local failure_message=$2
while read -r file;
do
if grep -q -e "$pattern" "$file";
then
add_validation_error "$failure_message:\n $file\n"
fi
done <<< "$files"
}
function validate_string_patterns {
validate_string_not_contained \
'com\.google\.android\.exoplayer2\..*\*' \
'Replace wildcard import statements with fully qualified import statements';
validate_string_not_contained \
'com\.google\.android\.exoplayer2\.ui\.PlayerView' \
'Migrate PlayerView to StyledPlayerView before migrating';
validate_string_not_contained \
'LegacyPlayerView' \
'Migrate LegacyPlayerView to StyledPlayerView before migrating';
validate_string_not_contained \
'com\.google\.android\.exoplayer2\.ext\.mediasession' \
'The MediaSessionConnector is integrated in androidx.media3.session.MediaSession'
}
SED_CMD_INPLACE='sed -i '
if [[ "$OSTYPE" == "darwin"* ]]; then
SED_CMD_INPLACE="sed -i '' "
fi
MIGRATE_FILES='1'
LIST_FILES_ONLY='1'
PRINT_CLASS_MAPPINGS='1'
PRINT_PACKAGE_MAPPINGS='1'
PRINT_DEPENDENCY_MAPPINGS='1'
PRINT_VERSION='1'
NO_CLEAN='1'
FORCE='1'
IGNORE_VERSION='1'
EXCLUDED_PATHS=''
while [[ $1 =~ ^-.* ]];
do
case "$1" in
-m ) MIGRATE_FILES='';;
-l ) LIST_FILES_ONLY='';;
-c ) PRINT_CLASS_MAPPINGS='';;
-p ) PRINT_PACKAGE_MAPPINGS='';;
-d ) PRINT_DEPENDENCY_MAPPINGS='';;
-v ) PRINT_VERSION='';;
-f ) FORCE='';;
-x ) shift; EXCLUDED_PATHS="$(printf "%s\n%s" $EXCLUDED_PATHS $1)";;
--noclean ) NO_CLEAN='';;
* ) usage && exit 1;;
esac
shift
done
if [[ -z $PRINT_DEPENDENCY_MAPPINGS ]];
then
print_pairs "$DEPENDENCY_MAPPINGS"
exit 0
elif [[ -z $PRINT_PACKAGE_MAPPINGS ]];
then
print_pairs "$PACKAGE_MAPPINGS"
exit 0
elif [[ -z $PRINT_CLASS_MAPPINGS ]];
then
print_class_mappings
exit 0
elif [[ -z $PRINT_VERSION ]];
then
echo "$LEGACY_PEER_VERSION -> $MEDIA3_VERSION. This script is written to migrate from ExoPlayer $LEGACY_PEER_VERSION to AndroidX Media3 $MEDIA3_VERSION"
exit 0
elif [[ -z $1 ]];
then
usage
exit 1
fi
if [[ ! -f $1/gradlew ]];
then
echo "directory seems not to exist or is not a gradle project (missing 'gradlew')"
usage
exit 1
fi
PROJECT_ROOT=$1
cd "$PROJECT_ROOT"
# Create the set of files to transform
exclusion="/build/|/.idea/|/res/drawable|/res/color|/res/mipmap|/res/values|"
if [[ ! -z $EXCLUDED_PATHS ]];
then
while read -r path;
do
exclusion="$exclusion./$path|"
done <<< "$EXCLUDED_PATHS"
fi
files=$(find . -name '*\.java' -o -name '*\.kt' -o -name '*\.xml' | grep -Ev "'$exclusion'")
# Validate project and exit in case of validation errors
validate_string_patterns
validate_exoplayer_version "$PROJECT_ROOT"
if [[ ! -z $FORCE && ! -z "$VALIDATION_ERRORS" ]];
then
echo "============================================="
echo "Validation errors (use -f to force execution)"
echo "---------------------------------------------"
echo -e "$VALIDATION_ERRORS"
exit 1
fi
if [[ -z $LIST_FILES_ONLY ]];
then
echo "$files" | cut -c 3-
find . -type f -name 'build\.gradle' | cut -c 3-
exit 0
fi
# start migration after successful validation or when forced to disregard validation
# errors
if [[ ! -z "$MIGRATE_FILES" ]];
then
echo "nothing to do"
usage
exit 0
fi
PWD=$(pwd)
if [[ ! -z $NO_CLEAN ]];
then
cd "$PROJECT_ROOT"
./gradlew clean
cd "$PWD"
fi
# create expressions for class renamings
renaming_expressions=''
while read -r renaming;
do
src=$(echo "$renaming" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
dest=$(echo "$renaming" | cut -d ' ' -f2)
renaming_expressions+="-e s/$src/$dest/g "
done <<< "$CLASS_RENAMINGS"
# create expressions for class mappings
classes_expressions=''
while read -r mapping;
do
src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
dest=$(echo "$mapping" | cut -d ' ' -f2)
classes=$(echo "$mapping" | cut -d ' ' -f3-)
for clazz in $classes;
do
classes_expressions+="-e s/$src\.$clazz/$dest.$clazz/g "
done
done <<< "$CLASS_MAPPINGS"
# create expressions for package mappings
packages_expressions=''
while read -r mapping;
do
src=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
dest=$(echo "$mapping" | cut -d ' ' -f2)
packages_expressions+="-e s/$src/$dest/g "
done <<< "$PACKAGE_MAPPINGS"
# do search and replace with expressions in each selected file
while read -r file;
do
echo "migrating $file"
expr="$renaming_expressions $classes_expressions $packages_expressions"
$SED_CMD_INPLACE $expr $file
done <<< "$files"
# create expressions for dependencies in gradle files
EXOPLAYER_GROUP="com\.google\.android\.exoplayer"
MEDIA3_GROUP="androidx.media3"
dependency_expressions=""
while read -r mapping
do
OLD=$(echo "$mapping" | cut -d ' ' -f1 | sed -e 's/\./\\\./g')
NEW=$(echo "$mapping" | cut -d ' ' -f2)
dependency_expressions="$dependency_expressions -e s/$EXOPLAYER_GROUP:$OLD:.*\"/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION\"/g -e s/$EXOPLAYER_GROUP:$OLD:.*'/$MEDIA3_GROUP:$NEW:$MEDIA3_VERSION'/"
done <<< "$DEPENDENCY_MAPPINGS"
## do search and replace for dependencies in gradle files
while read -r build_file;
do
echo "migrating build file $build_file"
$SED_CMD_INPLACE $dependency_expressions $build_file
done <<< "$(find . -type f -name 'build\.gradle')"

View File

@ -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 `CastPlayer` implements the `Player` interface, it can be passed to all media
components that accept a `Player`, including the UI components provided by the components that accept a `Player`, including the UI components provided by the
UI module. UI module.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -14,7 +14,7 @@
apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle" apply from: "$gradle.ext.androidxMediaSettingsDir/common_library_config.gradle"
dependencies { 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 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation project(modulePrefix + 'lib-common') implementation project(modulePrefix + 'lib-common')
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.cast; package androidx.media3.cast;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.castNonNull;
import static java.lang.Math.min; import static java.lang.Math.min;
@ -43,7 +44,6 @@ import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks; import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.text.CueGroup; import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
@ -295,7 +295,7 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void addMediaItems(int index, List<MediaItem> mediaItems) { public void addMediaItems(int index, List<MediaItem> mediaItems) {
Assertions.checkArgument(index >= 0); checkArgument(index >= 0);
int uid = MediaQueueItem.INVALID_ITEM_ID; int uid = MediaQueueItem.INVALID_ITEM_ID;
if (index < currentTimeline.getWindowCount()) { if (index < currentTimeline.getWindowCount()) {
uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid; uid = (int) currentTimeline.getWindow(/* windowIndex= */ index, window).uid;
@ -305,14 +305,11 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { public void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
Assertions.checkArgument( checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newIndex >= 0);
fromIndex >= 0 int playlistSize = currentTimeline.getWindowCount();
&& fromIndex <= toIndex toIndex = min(toIndex, playlistSize);
&& toIndex <= currentTimeline.getWindowCount() newIndex = min(newIndex, playlistSize - (toIndex - fromIndex));
&& newIndex >= 0 if (fromIndex >= playlistSize || fromIndex == toIndex || fromIndex == newIndex) {
&& newIndex < currentTimeline.getWindowCount());
newIndex = min(newIndex, currentTimeline.getWindowCount() - (toIndex - fromIndex));
if (fromIndex == toIndex || fromIndex == newIndex) {
// Do nothing. // Do nothing.
return; return;
} }
@ -325,9 +322,10 @@ public final class CastPlayer extends BasePlayer {
@Override @Override
public void removeMediaItems(int fromIndex, int toIndex) { public void removeMediaItems(int fromIndex, int toIndex) {
Assertions.checkArgument(fromIndex >= 0 && toIndex >= fromIndex); checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
toIndex = min(toIndex, currentTimeline.getWindowCount()); int playlistSize = currentTimeline.getWindowCount();
if (fromIndex == toIndex) { toIndex = min(toIndex, playlistSize);
if (fromIndex >= playlistSize || fromIndex == toIndex) {
// Do nothing. // Do nothing.
return; return;
} }
@ -399,7 +397,16 @@ public final class CastPlayer extends BasePlayer {
// don't implement onPositionDiscontinuity(). // don't implement onPositionDiscontinuity().
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Override @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(); MediaStatus mediaStatus = getMediaStatus();
// We assume the default position is 0. There is no support for seeking to the default position // We assume the default position is 0. There is no support for seeking to the default position
// in RemoteMediaClient. // in RemoteMediaClient.

View File

@ -26,10 +26,6 @@ import com.google.android.gms.cast.MediaTrack;
/** Utility methods for Cast integration. */ /** Utility methods for Cast integration. */
/* package */ final class CastUtils { /* 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 * Returns the duration in microseconds advertised by a media info, or {@link C#TIME_UNSET} if
* unknown or not applicable. * unknown or not applicable.
@ -42,9 +38,7 @@ import com.google.android.gms.cast.MediaTrack;
return C.TIME_UNSET; return C.TIME_UNSET;
} }
long durationMs = mediaInfo.getStreamDuration(); long durationMs = mediaInfo.getStreamDuration();
return durationMs != MediaInfo.UNKNOWN_DURATION && durationMs != LIVE_STREAM_DURATION return durationMs != MediaInfo.UNKNOWN_DURATION ? Util.msToUs(durationMs) : C.TIME_UNSET;
? Util.msToUs(durationMs)
: C.TIME_UNSET;
} }
/** /**

View File

@ -2,3 +2,9 @@
Provides common code and utilities used by other media modules. Application code Provides common code and utilities used by other media modules. Application code
will not normally need to depend on this module directly. will not normally need to depend on this module directly.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -459,44 +459,29 @@ public final class AdPlaybackState implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_TIME_US = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_COUNT = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_URIS = Util.intToStringMaxRadix(2);
@IntDef({ private static final String FIELD_STATES = Util.intToStringMaxRadix(3);
FIELD_TIME_US, private static final String FIELD_DURATIONS_US = Util.intToStringMaxRadix(4);
FIELD_COUNT, private static final String FIELD_CONTENT_RESUME_OFFSET_US = Util.intToStringMaxRadix(5);
FIELD_URIS, private static final String FIELD_IS_SERVER_SIDE_INSERTED = Util.intToStringMaxRadix(6);
FIELD_STATES, private static final String FIELD_ORIGINAL_COUNT = Util.intToStringMaxRadix(7);
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;
// putParcelableArrayList actually supports null elements. // putParcelableArrayList actually supports null elements.
@SuppressWarnings("nullness:argument") @SuppressWarnings("nullness:argument")
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putLong(keyForField(FIELD_TIME_US), timeUs); bundle.putLong(FIELD_TIME_US, timeUs);
bundle.putInt(keyForField(FIELD_COUNT), count); bundle.putInt(FIELD_COUNT, count);
bundle.putInt(keyForField(FIELD_ORIGINAL_COUNT), originalCount); bundle.putInt(FIELD_ORIGINAL_COUNT, originalCount);
bundle.putParcelableArrayList( bundle.putParcelableArrayList(
keyForField(FIELD_URIS), new ArrayList<@NullableType Uri>(Arrays.asList(uris))); FIELD_URIS, new ArrayList<@NullableType Uri>(Arrays.asList(uris)));
bundle.putIntArray(keyForField(FIELD_STATES), states); bundle.putIntArray(FIELD_STATES, states);
bundle.putLongArray(keyForField(FIELD_DURATIONS_US), durationsUs); bundle.putLongArray(FIELD_DURATIONS_US, durationsUs);
bundle.putLong(keyForField(FIELD_CONTENT_RESUME_OFFSET_US), contentResumeOffsetUs); bundle.putLong(FIELD_CONTENT_RESUME_OFFSET_US, contentResumeOffsetUs);
bundle.putBoolean(keyForField(FIELD_IS_SERVER_SIDE_INSERTED), isServerSideInserted); bundle.putBoolean(FIELD_IS_SERVER_SIDE_INSERTED, isServerSideInserted);
return bundle; return bundle;
} }
@ -506,18 +491,16 @@ public final class AdPlaybackState implements Bundleable {
// getParcelableArrayList may have null elements. // getParcelableArrayList may have null elements.
@SuppressWarnings("nullness:type.argument") @SuppressWarnings("nullness:type.argument")
private static AdGroup fromBundle(Bundle bundle) { private static AdGroup fromBundle(Bundle bundle) {
long timeUs = bundle.getLong(keyForField(FIELD_TIME_US)); long timeUs = bundle.getLong(FIELD_TIME_US);
int count = bundle.getInt(keyForField(FIELD_COUNT), /* defaultValue= */ C.LENGTH_UNSET); int count = bundle.getInt(FIELD_COUNT);
int originalCount = int originalCount = bundle.getInt(FIELD_ORIGINAL_COUNT);
bundle.getInt(keyForField(FIELD_ORIGINAL_COUNT), /* defaultValue= */ C.LENGTH_UNSET); @Nullable ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(FIELD_URIS);
@Nullable
ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(keyForField(FIELD_URIS));
@Nullable @Nullable
@AdState @AdState
int[] states = bundle.getIntArray(keyForField(FIELD_STATES)); int[] states = bundle.getIntArray(FIELD_STATES);
@Nullable long[] durationsUs = bundle.getLongArray(keyForField(FIELD_DURATIONS_US)); @Nullable long[] durationsUs = bundle.getLongArray(FIELD_DURATIONS_US);
long contentResumeOffsetUs = bundle.getLong(keyForField(FIELD_CONTENT_RESUME_OFFSET_US)); long contentResumeOffsetUs = bundle.getLong(FIELD_CONTENT_RESUME_OFFSET_US);
boolean isServerSideInserted = bundle.getBoolean(keyForField(FIELD_IS_SERVER_SIDE_INSERTED)); boolean isServerSideInserted = bundle.getBoolean(FIELD_IS_SERVER_SIDE_INSERTED);
return new AdGroup( return new AdGroup(
timeUs, timeUs,
count, count,
@ -528,10 +511,6 @@ public final class AdPlaybackState implements Bundleable {
contentResumeOffsetUs, contentResumeOffsetUs,
isServerSideInserted); 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. // Bundleable implementation.
@Documented private static final String FIELD_AD_GROUPS = Util.intToStringMaxRadix(1);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_AD_RESUME_POSITION_US = Util.intToStringMaxRadix(2);
@Target(TYPE_USE) private static final String FIELD_CONTENT_DURATION_US = Util.intToStringMaxRadix(3);
@IntDef({ private static final String FIELD_REMOVED_AD_GROUP_COUNT = Util.intToStringMaxRadix(4);
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;
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -1152,10 +1120,18 @@ public final class AdPlaybackState implements Bundleable {
for (AdGroup adGroup : adGroups) { for (AdGroup adGroup : adGroups) {
adGroupBundleList.add(adGroup.toBundle()); adGroupBundleList.add(adGroup.toBundle());
} }
bundle.putParcelableArrayList(keyForField(FIELD_AD_GROUPS), adGroupBundleList); if (!adGroupBundleList.isEmpty()) {
bundle.putLong(keyForField(FIELD_AD_RESUME_POSITION_US), adResumePositionUs); bundle.putParcelableArrayList(FIELD_AD_GROUPS, adGroupBundleList);
bundle.putLong(keyForField(FIELD_CONTENT_DURATION_US), contentDurationUs); }
bundle.putInt(keyForField(FIELD_REMOVED_AD_GROUP_COUNT), removedAdGroupCount); 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; return bundle;
} }
@ -1167,9 +1143,7 @@ public final class AdPlaybackState implements Bundleable {
public static final Bundleable.Creator<AdPlaybackState> CREATOR = AdPlaybackState::fromBundle; public static final Bundleable.Creator<AdPlaybackState> CREATOR = AdPlaybackState::fromBundle;
private static AdPlaybackState fromBundle(Bundle bundle) { private static AdPlaybackState fromBundle(Bundle bundle) {
@Nullable @Nullable ArrayList<Bundle> adGroupBundleList = bundle.getParcelableArrayList(FIELD_AD_GROUPS);
ArrayList<Bundle> adGroupBundleList =
bundle.getParcelableArrayList(keyForField(FIELD_AD_GROUPS));
@Nullable AdGroup[] adGroups; @Nullable AdGroup[] adGroups;
if (adGroupBundleList == null) { if (adGroupBundleList == null) {
adGroups = new AdGroup[0]; adGroups = new AdGroup[0];
@ -1180,18 +1154,15 @@ public final class AdPlaybackState implements Bundleable {
} }
} }
long adResumePositionUs = long adResumePositionUs =
bundle.getLong(keyForField(FIELD_AD_RESUME_POSITION_US), /* defaultValue= */ 0); bundle.getLong(FIELD_AD_RESUME_POSITION_US, /* defaultValue= */ NONE.adResumePositionUs);
long contentDurationUs = long contentDurationUs =
bundle.getLong(keyForField(FIELD_CONTENT_DURATION_US), /* defaultValue= */ C.TIME_UNSET); bundle.getLong(FIELD_CONTENT_DURATION_US, /* defaultValue= */ NONE.contentDurationUs);
int removedAdGroupCount = bundle.getInt(keyForField(FIELD_REMOVED_AD_GROUP_COUNT)); int removedAdGroupCount =
bundle.getInt(FIELD_REMOVED_AD_GROUP_COUNT, /* defaultValue= */ NONE.removedAdGroupCount);
return new AdPlaybackState( return new AdPlaybackState(
/* adsId= */ null, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); /* 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) { private static AdGroup[] createEmptyAdGroups(long[] adGroupTimesUs) {
AdGroup[] adGroups = new AdGroup[adGroupTimesUs.length]; AdGroup[] adGroups = new AdGroup[adGroupTimesUs.length];
for (int i = 0; i < adGroups.length; i++) { for (int i = 0; i < adGroups.length; i++) {

View File

@ -15,20 +15,13 @@
*/ */
package androidx.media3.common; package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.DoNotInline; import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue; 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 * Attributes for audio playback, which configure the underlying platform {@link
@ -205,33 +198,21 @@ public final class AudioAttributes implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_CONTENT_TYPE = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_FLAGS = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_USAGE = Util.intToStringMaxRadix(2);
@IntDef({ private static final String FIELD_ALLOWED_CAPTURE_POLICY = Util.intToStringMaxRadix(3);
FIELD_CONTENT_TYPE, private static final String FIELD_SPATIALIZATION_BEHAVIOR = Util.intToStringMaxRadix(4);
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;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_CONTENT_TYPE), contentType); bundle.putInt(FIELD_CONTENT_TYPE, contentType);
bundle.putInt(keyForField(FIELD_FLAGS), flags); bundle.putInt(FIELD_FLAGS, flags);
bundle.putInt(keyForField(FIELD_USAGE), usage); bundle.putInt(FIELD_USAGE, usage);
bundle.putInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY), allowedCapturePolicy); bundle.putInt(FIELD_ALLOWED_CAPTURE_POLICY, allowedCapturePolicy);
bundle.putInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR), spatializationBehavior); bundle.putInt(FIELD_SPATIALIZATION_BEHAVIOR, spatializationBehavior);
return bundle; return bundle;
} }
@ -240,29 +221,24 @@ public final class AudioAttributes implements Bundleable {
public static final Creator<AudioAttributes> CREATOR = public static final Creator<AudioAttributes> CREATOR =
bundle -> { bundle -> {
Builder builder = new Builder(); Builder builder = new Builder();
if (bundle.containsKey(keyForField(FIELD_CONTENT_TYPE))) { if (bundle.containsKey(FIELD_CONTENT_TYPE)) {
builder.setContentType(bundle.getInt(keyForField(FIELD_CONTENT_TYPE))); builder.setContentType(bundle.getInt(FIELD_CONTENT_TYPE));
} }
if (bundle.containsKey(keyForField(FIELD_FLAGS))) { if (bundle.containsKey(FIELD_FLAGS)) {
builder.setFlags(bundle.getInt(keyForField(FIELD_FLAGS))); builder.setFlags(bundle.getInt(FIELD_FLAGS));
} }
if (bundle.containsKey(keyForField(FIELD_USAGE))) { if (bundle.containsKey(FIELD_USAGE)) {
builder.setUsage(bundle.getInt(keyForField(FIELD_USAGE))); builder.setUsage(bundle.getInt(FIELD_USAGE));
} }
if (bundle.containsKey(keyForField(FIELD_ALLOWED_CAPTURE_POLICY))) { if (bundle.containsKey(FIELD_ALLOWED_CAPTURE_POLICY)) {
builder.setAllowedCapturePolicy(bundle.getInt(keyForField(FIELD_ALLOWED_CAPTURE_POLICY))); builder.setAllowedCapturePolicy(bundle.getInt(FIELD_ALLOWED_CAPTURE_POLICY));
} }
if (bundle.containsKey(keyForField(FIELD_SPATIALIZATION_BEHAVIOR))) { if (bundle.containsKey(FIELD_SPATIALIZATION_BEHAVIOR)) {
builder.setSpatializationBehavior( builder.setSpatializationBehavior(bundle.getInt(FIELD_SPATIALIZATION_BEHAVIOR));
bundle.getInt(keyForField(FIELD_SPATIALIZATION_BEHAVIOR)));
} }
return builder.build(); return builder.build();
}; };
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
@RequiresApi(29) @RequiresApi(29)
private static final class Api29 { private static final class Api29 {
@DoNotInline @DoNotInline

View File

@ -15,14 +15,15 @@
*/ */
package androidx.media3.common; package androidx.media3.common;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.ForOverride;
import java.util.List; import java.util.List;
/** Abstract base {@link Player} which implements common implementation independent methods. */ /** Abstract base {@link Player} which implements common implementation independent methods. */
@ -121,27 +122,23 @@ public abstract class BasePlayer implements Player {
@Override @Override
public final void seekToDefaultPosition() { public final void seekToDefaultPosition() {
seekToDefaultPosition(getCurrentMediaItemIndex()); seekToDefaultPositionInternal(
getCurrentMediaItemIndex(), Player.COMMAND_SEEK_TO_DEFAULT_POSITION);
} }
@Override @Override
public final void seekToDefaultPosition(int mediaItemIndex) { public final void seekToDefaultPosition(int mediaItemIndex) {
seekTo(mediaItemIndex, /* positionMs= */ C.TIME_UNSET); seekToDefaultPositionInternal(mediaItemIndex, Player.COMMAND_SEEK_TO_MEDIA_ITEM);
}
@Override
public final void seekTo(long positionMs) {
seekTo(getCurrentMediaItemIndex(), positionMs);
} }
@Override @Override
public final void seekBack() { public final void seekBack() {
seekToOffset(-getSeekBackIncrement()); seekToOffset(-getSeekBackIncrement(), Player.COMMAND_SEEK_BACK);
} }
@Override @Override
public final void seekForward() { public final void seekForward() {
seekToOffset(getSeekForwardIncrement()); seekToOffset(getSeekForwardIncrement(), Player.COMMAND_SEEK_FORWARD);
} }
/** /**
@ -187,15 +184,7 @@ public abstract class BasePlayer implements Player {
@Override @Override
public final void seekToPreviousMediaItem() { public final void seekToPreviousMediaItem() {
int previousMediaItemIndex = getPreviousMediaItemIndex(); seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM);
if (previousMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (previousMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem();
} else {
seekToDefaultPosition(previousMediaItemIndex);
}
} }
@Override @Override
@ -207,12 +196,12 @@ public abstract class BasePlayer implements Player {
boolean hasPreviousMediaItem = hasPreviousMediaItem(); boolean hasPreviousMediaItem = hasPreviousMediaItem();
if (isCurrentMediaItemLive() && !isCurrentMediaItemSeekable()) { if (isCurrentMediaItemLive() && !isCurrentMediaItemSeekable()) {
if (hasPreviousMediaItem) { if (hasPreviousMediaItem) {
seekToPreviousMediaItem(); seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS);
} }
} else if (hasPreviousMediaItem && getCurrentPosition() <= getMaxSeekToPreviousPosition()) { } else if (hasPreviousMediaItem && getCurrentPosition() <= getMaxSeekToPreviousPosition()) {
seekToPreviousMediaItem(); seekToPreviousMediaItemInternal(Player.COMMAND_SEEK_TO_PREVIOUS);
} else { } else {
seekTo(/* positionMs= */ 0); seekToCurrentItem(/* positionMs= */ 0, Player.COMMAND_SEEK_TO_PREVIOUS);
} }
} }
@ -259,15 +248,7 @@ public abstract class BasePlayer implements Player {
@Override @Override
public final void seekToNextMediaItem() { public final void seekToNextMediaItem() {
int nextMediaItemIndex = getNextMediaItemIndex(); seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM);
if (nextMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (nextMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem();
} else {
seekToDefaultPosition(nextMediaItemIndex);
}
} }
@Override @Override
@ -277,12 +258,42 @@ public abstract class BasePlayer implements Player {
return; return;
} }
if (hasNextMediaItem()) { if (hasNextMediaItem()) {
seekToNextMediaItem(); seekToNextMediaItemInternal(Player.COMMAND_SEEK_TO_NEXT);
} else if (isCurrentMediaItemLive() && isCurrentMediaItemDynamic()) { } 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 @Override
public final void setPlaybackSpeed(float speed) { public final void setPlaybackSpeed(float speed) {
setPlaybackParameters(getPlaybackParameters().withSpeed(speed)); setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
@ -437,29 +448,63 @@ public abstract class BasePlayer implements Player {
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs(); : 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() { private @RepeatMode int getRepeatModeForNavigation() {
@RepeatMode int repeatMode = getRepeatMode(); @RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode; 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 positionMs = getCurrentPosition() + offsetMs;
long durationMs = getDuration(); long durationMs = getDuration();
if (durationMs != C.TIME_UNSET) { if (durationMs != C.TIME_UNSET) {
positionMs = min(positionMs, durationMs); positionMs = min(positionMs, durationMs);
} }
positionMs = max(positionMs, 0); 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);
} }
} }

View File

@ -196,7 +196,7 @@ public final class C {
* #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT}, * #ENCODING_PCM_16BIT_BIG_ENDIAN}, {@link #ENCODING_PCM_24BIT}, {@link #ENCODING_PCM_32BIT},
* {@link #ENCODING_PCM_FLOAT}, {@link #ENCODING_MP3}, {@link #ENCODING_AC3}, {@link * {@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}, * #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 @UnstableApi
@Documented @Documented
@ -224,7 +224,8 @@ public final class C {
ENCODING_AC4, ENCODING_AC4,
ENCODING_DTS, ENCODING_DTS,
ENCODING_DTS_HD, ENCODING_DTS_HD,
ENCODING_DOLBY_TRUEHD ENCODING_DOLBY_TRUEHD,
ENCODING_OPUS,
}) })
public @interface Encoding {} public @interface Encoding {}
@ -325,6 +326,10 @@ public final class C {
* @see AudioFormat#ENCODING_DOLBY_TRUEHD * @see AudioFormat#ENCODING_DOLBY_TRUEHD
*/ */
@UnstableApi public static final int ENCODING_DOLBY_TRUEHD = 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. */ /** Represents the behavior affecting whether spatialization will be used. */
@Documented @Documented

View File

@ -15,16 +15,10 @@
*/ */
package androidx.media3.common; package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import java.lang.annotation.Documented; import androidx.media3.common.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays; import java.util.Arrays;
import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.Pure;
@ -183,41 +177,26 @@ public final class ColorInfo implements Bundleable {
// Bundleable implementation // Bundleable implementation
@Documented private static final String FIELD_COLOR_SPACE = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_COLOR_RANGE = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_COLOR_TRANSFER = Util.intToStringMaxRadix(2);
@IntDef({ private static final String FIELD_HDR_STATIC_INFO = Util.intToStringMaxRadix(3);
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;
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_COLOR_SPACE), colorSpace); bundle.putInt(FIELD_COLOR_SPACE, colorSpace);
bundle.putInt(keyForField(FIELD_COLOR_RANGE), colorRange); bundle.putInt(FIELD_COLOR_RANGE, colorRange);
bundle.putInt(keyForField(FIELD_COLOR_TRANSFER), colorTransfer); bundle.putInt(FIELD_COLOR_TRANSFER, colorTransfer);
bundle.putByteArray(keyForField(FIELD_HDR_STATIC_INFO), hdrStaticInfo); bundle.putByteArray(FIELD_HDR_STATIC_INFO, hdrStaticInfo);
return bundle; return bundle;
} }
public static final Creator<ColorInfo> CREATOR = public static final Creator<ColorInfo> CREATOR =
bundle -> bundle ->
new ColorInfo( new ColorInfo(
bundle.getInt(keyForField(FIELD_COLOR_SPACE), Format.NO_VALUE), bundle.getInt(FIELD_COLOR_SPACE, Format.NO_VALUE),
bundle.getInt(keyForField(FIELD_COLOR_RANGE), Format.NO_VALUE), bundle.getInt(FIELD_COLOR_RANGE, Format.NO_VALUE),
bundle.getInt(keyForField(FIELD_COLOR_TRANSFER), Format.NO_VALUE), bundle.getInt(FIELD_COLOR_TRANSFER, Format.NO_VALUE),
bundle.getByteArray(keyForField(FIELD_HDR_STATIC_INFO))); bundle.getByteArray(FIELD_HDR_STATIC_INFO));
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -21,6 +21,7 @@ import android.os.Bundle;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
@ -87,23 +88,17 @@ public final class DeviceInfo implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_PLAYBACK_TYPE = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_MIN_VOLUME = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_MAX_VOLUME = Util.intToStringMaxRadix(2);
@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;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_PLAYBACK_TYPE), playbackType); bundle.putInt(FIELD_PLAYBACK_TYPE, playbackType);
bundle.putInt(keyForField(FIELD_MIN_VOLUME), minVolume); bundle.putInt(FIELD_MIN_VOLUME, minVolume);
bundle.putInt(keyForField(FIELD_MAX_VOLUME), maxVolume); bundle.putInt(FIELD_MAX_VOLUME, maxVolume);
return bundle; return bundle;
} }
@ -112,14 +107,9 @@ public final class DeviceInfo implements Bundleable {
public static final Creator<DeviceInfo> CREATOR = public static final Creator<DeviceInfo> CREATOR =
bundle -> { bundle -> {
int playbackType = int playbackType =
bundle.getInt( bundle.getInt(FIELD_PLAYBACK_TYPE, /* defaultValue= */ PLAYBACK_TYPE_LOCAL);
keyForField(FIELD_PLAYBACK_TYPE), /* defaultValue= */ PLAYBACK_TYPE_LOCAL); int minVolume = bundle.getInt(FIELD_MIN_VOLUME, /* defaultValue= */ 0);
int minVolume = bundle.getInt(keyForField(FIELD_MIN_VOLUME), /* defaultValue= */ 0); int maxVolume = bundle.getInt(FIELD_MAX_VOLUME, /* defaultValue= */ 0);
int maxVolume = bundle.getInt(keyForField(FIELD_MAX_VOLUME), /* defaultValue= */ 0);
return new DeviceInfo(playbackType, minVolume, maxVolume); return new DeviceInfo(playbackType, minVolume, maxVolume);
}; };
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -15,20 +15,13 @@
*/ */
package androidx.media3.common; package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.errorprone.annotations.CanIgnoreReturnValue; 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;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -112,6 +105,13 @@ import java.util.UUID;
* <ul> * <ul>
* <li>{@link #accessibilityChannel} * <li>{@link #accessibilityChannel}
* </ul> * </ul>
*
* <h2 id="image-formats">Fields relevant to image formats</h2>
*
* <ul>
* <li>{@link #tileCountHorizontal}
* <li>{@link #tileCountVertical}
* </ul>
*/ */
public final class Format implements Bundleable { public final class Format implements Bundleable {
@ -172,6 +172,11 @@ public final class Format implements Bundleable {
private int accessibilityChannel; private int accessibilityChannel;
// Image specific
private int tileCountHorizontal;
private int tileCountVertical;
// Provided by the source. // Provided by the source.
private @C.CryptoType int cryptoType; private @C.CryptoType int cryptoType;
@ -195,6 +200,9 @@ public final class Format implements Bundleable {
pcmEncoding = NO_VALUE; pcmEncoding = NO_VALUE;
// Text specific. // Text specific.
accessibilityChannel = NO_VALUE; accessibilityChannel = NO_VALUE;
// Image specific.
tileCountHorizontal = NO_VALUE;
tileCountVertical = NO_VALUE;
// Provided by the source. // Provided by the source.
cryptoType = C.CRYPTO_TYPE_NONE; cryptoType = C.CRYPTO_TYPE_NONE;
} }
@ -239,6 +247,9 @@ public final class Format implements Bundleable {
this.encoderPadding = format.encoderPadding; this.encoderPadding = format.encoderPadding;
// Text specific. // Text specific.
this.accessibilityChannel = format.accessibilityChannel; this.accessibilityChannel = format.accessibilityChannel;
// Image specific.
this.tileCountHorizontal = format.tileCountHorizontal;
this.tileCountVertical = format.tileCountVertical;
// Provided by the source. // Provided by the source.
this.cryptoType = format.cryptoType; this.cryptoType = format.cryptoType;
} }
@ -614,6 +625,32 @@ public final class Format implements Bundleable {
return this; return this;
} }
// Image specific.
/**
* Sets {@link Format#tileCountHorizontal}. The default value is {@link #NO_VALUE}.
*
* @param tileCountHorizontal The {@link Format#accessibilityChannel}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setTileCountHorizontal(int tileCountHorizontal) {
this.tileCountHorizontal = tileCountHorizontal;
return this;
}
/**
* Sets {@link Format#tileCountVertical}. The default value is {@link #NO_VALUE}.
*
* @param tileCountVertical The {@link Format#accessibilityChannel}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setTileCountVertical(int tileCountVertical) {
this.tileCountVertical = tileCountVertical;
return this;
}
// Provided by source. // Provided by source.
/** /**
@ -786,6 +823,15 @@ public final class Format implements Bundleable {
/** The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */ /** The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */
@UnstableApi public final int accessibilityChannel; @UnstableApi public final int accessibilityChannel;
// Image specific.
/**
* The number of horizontal tiles in an image, or {@link #NO_VALUE} if not known or applicable.
*/
@UnstableApi public final int tileCountHorizontal;
/** The number of vertical tiles in an image, or {@link #NO_VALUE} if not known or applicable. */
@UnstableApi public final int tileCountVertical;
// Provided by source. // Provided by source.
/** /**
@ -1015,6 +1061,9 @@ public final class Format implements Bundleable {
encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding; encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding;
// Text specific. // Text specific.
accessibilityChannel = builder.accessibilityChannel; accessibilityChannel = builder.accessibilityChannel;
// Image specific.
tileCountHorizontal = builder.tileCountHorizontal;
tileCountVertical = builder.tileCountVertical;
// Provided by source. // Provided by source.
if (builder.cryptoType == C.CRYPTO_TYPE_NONE && drmInitData != null) { if (builder.cryptoType == C.CRYPTO_TYPE_NONE && drmInitData != null) {
// Encrypted content cannot use CRYPTO_TYPE_NONE. // Encrypted content cannot use CRYPTO_TYPE_NONE.
@ -1275,6 +1324,9 @@ public final class Format implements Bundleable {
result = 31 * result + encoderPadding; result = 31 * result + encoderPadding;
// Text specific. // Text specific.
result = 31 * result + accessibilityChannel; result = 31 * result + accessibilityChannel;
// Image specific.
result = 31 * result + tileCountHorizontal;
result = 31 * result + tileCountVertical;
// Provided by the source. // Provided by the source.
result = 31 * result + cryptoType; result = 31 * result + cryptoType;
hashCode = result; hashCode = result;
@ -1311,6 +1363,8 @@ public final class Format implements Bundleable {
&& encoderDelay == other.encoderDelay && encoderDelay == other.encoderDelay
&& encoderPadding == other.encoderPadding && encoderPadding == other.encoderPadding
&& accessibilityChannel == other.accessibilityChannel && accessibilityChannel == other.accessibilityChannel
&& tileCountHorizontal == other.tileCountHorizontal
&& tileCountVertical == other.tileCountVertical
&& cryptoType == other.cryptoType && cryptoType == other.cryptoType
&& Float.compare(frameRate, other.frameRate) == 0 && Float.compare(frameRate, other.frameRate) == 0
&& Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0 && Float.compare(pixelWidthHeightRatio, other.pixelWidthHeightRatio) == 0
@ -1476,73 +1530,39 @@ public final class Format implements Bundleable {
} }
// Bundleable implementation. // Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_ID,
FIELD_LABEL,
FIELD_LANGUAGE,
FIELD_SELECTION_FLAGS,
FIELD_ROLE_FLAGS,
FIELD_AVERAGE_BITRATE,
FIELD_PEAK_BITRATE,
FIELD_CODECS,
FIELD_METADATA,
FIELD_CONTAINER_MIME_TYPE,
FIELD_SAMPLE_MIME_TYPE,
FIELD_MAX_INPUT_SIZE,
FIELD_INITIALIZATION_DATA,
FIELD_DRM_INIT_DATA,
FIELD_SUBSAMPLE_OFFSET_US,
FIELD_WIDTH,
FIELD_HEIGHT,
FIELD_FRAME_RATE,
FIELD_ROTATION_DEGREES,
FIELD_PIXEL_WIDTH_HEIGHT_RATIO,
FIELD_PROJECTION_DATA,
FIELD_STEREO_MODE,
FIELD_COLOR_INFO,
FIELD_CHANNEL_COUNT,
FIELD_SAMPLE_RATE,
FIELD_PCM_ENCODING,
FIELD_ENCODER_DELAY,
FIELD_ENCODER_PADDING,
FIELD_ACCESSIBILITY_CHANNEL,
FIELD_CRYPTO_TYPE,
})
private @interface FieldNumber {}
private static final int FIELD_ID = 0; private static final String FIELD_ID = Util.intToStringMaxRadix(0);
private static final int FIELD_LABEL = 1; private static final String FIELD_LABEL = Util.intToStringMaxRadix(1);
private static final int FIELD_LANGUAGE = 2; private static final String FIELD_LANGUAGE = Util.intToStringMaxRadix(2);
private static final int FIELD_SELECTION_FLAGS = 3; private static final String FIELD_SELECTION_FLAGS = Util.intToStringMaxRadix(3);
private static final int FIELD_ROLE_FLAGS = 4; private static final String FIELD_ROLE_FLAGS = Util.intToStringMaxRadix(4);
private static final int FIELD_AVERAGE_BITRATE = 5; private static final String FIELD_AVERAGE_BITRATE = Util.intToStringMaxRadix(5);
private static final int FIELD_PEAK_BITRATE = 6; private static final String FIELD_PEAK_BITRATE = Util.intToStringMaxRadix(6);
private static final int FIELD_CODECS = 7; private static final String FIELD_CODECS = Util.intToStringMaxRadix(7);
private static final int FIELD_METADATA = 8; private static final String FIELD_METADATA = Util.intToStringMaxRadix(8);
private static final int FIELD_CONTAINER_MIME_TYPE = 9; private static final String FIELD_CONTAINER_MIME_TYPE = Util.intToStringMaxRadix(9);
private static final int FIELD_SAMPLE_MIME_TYPE = 10; private static final String FIELD_SAMPLE_MIME_TYPE = Util.intToStringMaxRadix(10);
private static final int FIELD_MAX_INPUT_SIZE = 11; private static final String FIELD_MAX_INPUT_SIZE = Util.intToStringMaxRadix(11);
private static final int FIELD_INITIALIZATION_DATA = 12; private static final String FIELD_INITIALIZATION_DATA = Util.intToStringMaxRadix(12);
private static final int FIELD_DRM_INIT_DATA = 13; private static final String FIELD_DRM_INIT_DATA = Util.intToStringMaxRadix(13);
private static final int FIELD_SUBSAMPLE_OFFSET_US = 14; private static final String FIELD_SUBSAMPLE_OFFSET_US = Util.intToStringMaxRadix(14);
private static final int FIELD_WIDTH = 15; private static final String FIELD_WIDTH = Util.intToStringMaxRadix(15);
private static final int FIELD_HEIGHT = 16; private static final String FIELD_HEIGHT = Util.intToStringMaxRadix(16);
private static final int FIELD_FRAME_RATE = 17; private static final String FIELD_FRAME_RATE = Util.intToStringMaxRadix(17);
private static final int FIELD_ROTATION_DEGREES = 18; private static final String FIELD_ROTATION_DEGREES = Util.intToStringMaxRadix(18);
private static final int FIELD_PIXEL_WIDTH_HEIGHT_RATIO = 19; private static final String FIELD_PIXEL_WIDTH_HEIGHT_RATIO = Util.intToStringMaxRadix(19);
private static final int FIELD_PROJECTION_DATA = 20; private static final String FIELD_PROJECTION_DATA = Util.intToStringMaxRadix(20);
private static final int FIELD_STEREO_MODE = 21; private static final String FIELD_STEREO_MODE = Util.intToStringMaxRadix(21);
private static final int FIELD_COLOR_INFO = 22; private static final String FIELD_COLOR_INFO = Util.intToStringMaxRadix(22);
private static final int FIELD_CHANNEL_COUNT = 23; private static final String FIELD_CHANNEL_COUNT = Util.intToStringMaxRadix(23);
private static final int FIELD_SAMPLE_RATE = 24; private static final String FIELD_SAMPLE_RATE = Util.intToStringMaxRadix(24);
private static final int FIELD_PCM_ENCODING = 25; private static final String FIELD_PCM_ENCODING = Util.intToStringMaxRadix(25);
private static final int FIELD_ENCODER_DELAY = 26; private static final String FIELD_ENCODER_DELAY = Util.intToStringMaxRadix(26);
private static final int FIELD_ENCODER_PADDING = 27; private static final String FIELD_ENCODER_PADDING = Util.intToStringMaxRadix(27);
private static final int FIELD_ACCESSIBILITY_CHANNEL = 28; private static final String FIELD_ACCESSIBILITY_CHANNEL = Util.intToStringMaxRadix(28);
private static final int FIELD_CRYPTO_TYPE = 29; private static final String FIELD_CRYPTO_TYPE = Util.intToStringMaxRadix(29);
private static final String FIELD_TILE_COUNT_HORIZONTAL = Util.intToStringMaxRadix(30);
private static final String FIELD_TILE_COUNT_VERTICAL = Util.intToStringMaxRadix(31);
@UnstableApi @UnstableApi
@Override @Override
@ -1557,51 +1577,54 @@ public final class Format implements Bundleable {
@UnstableApi @UnstableApi
public Bundle toBundle(boolean excludeMetadata) { public Bundle toBundle(boolean excludeMetadata) {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString(keyForField(FIELD_ID), id); bundle.putString(FIELD_ID, id);
bundle.putString(keyForField(FIELD_LABEL), label); bundle.putString(FIELD_LABEL, label);
bundle.putString(keyForField(FIELD_LANGUAGE), language); bundle.putString(FIELD_LANGUAGE, language);
bundle.putInt(keyForField(FIELD_SELECTION_FLAGS), selectionFlags); bundle.putInt(FIELD_SELECTION_FLAGS, selectionFlags);
bundle.putInt(keyForField(FIELD_ROLE_FLAGS), roleFlags); bundle.putInt(FIELD_ROLE_FLAGS, roleFlags);
bundle.putInt(keyForField(FIELD_AVERAGE_BITRATE), averageBitrate); bundle.putInt(FIELD_AVERAGE_BITRATE, averageBitrate);
bundle.putInt(keyForField(FIELD_PEAK_BITRATE), peakBitrate); bundle.putInt(FIELD_PEAK_BITRATE, peakBitrate);
bundle.putString(keyForField(FIELD_CODECS), codecs); bundle.putString(FIELD_CODECS, codecs);
if (!excludeMetadata) { if (!excludeMetadata) {
// TODO (internal ref: b/239701618) // TODO (internal ref: b/239701618)
bundle.putParcelable(keyForField(FIELD_METADATA), metadata); bundle.putParcelable(FIELD_METADATA, metadata);
} }
// Container specific. // Container specific.
bundle.putString(keyForField(FIELD_CONTAINER_MIME_TYPE), containerMimeType); bundle.putString(FIELD_CONTAINER_MIME_TYPE, containerMimeType);
// Sample specific. // Sample specific.
bundle.putString(keyForField(FIELD_SAMPLE_MIME_TYPE), sampleMimeType); bundle.putString(FIELD_SAMPLE_MIME_TYPE, sampleMimeType);
bundle.putInt(keyForField(FIELD_MAX_INPUT_SIZE), maxInputSize); bundle.putInt(FIELD_MAX_INPUT_SIZE, maxInputSize);
for (int i = 0; i < initializationData.size(); i++) { for (int i = 0; i < initializationData.size(); i++) {
bundle.putByteArray(keyForInitializationData(i), initializationData.get(i)); bundle.putByteArray(keyForInitializationData(i), initializationData.get(i));
} }
// DrmInitData doesn't need to be Bundleable as it's only used in the playing process to // DrmInitData doesn't need to be Bundleable as it's only used in the playing process to
// initialize the decoder. // initialize the decoder.
bundle.putParcelable(keyForField(FIELD_DRM_INIT_DATA), drmInitData); bundle.putParcelable(FIELD_DRM_INIT_DATA, drmInitData);
bundle.putLong(keyForField(FIELD_SUBSAMPLE_OFFSET_US), subsampleOffsetUs); bundle.putLong(FIELD_SUBSAMPLE_OFFSET_US, subsampleOffsetUs);
// Video specific. // Video specific.
bundle.putInt(keyForField(FIELD_WIDTH), width); bundle.putInt(FIELD_WIDTH, width);
bundle.putInt(keyForField(FIELD_HEIGHT), height); bundle.putInt(FIELD_HEIGHT, height);
bundle.putFloat(keyForField(FIELD_FRAME_RATE), frameRate); bundle.putFloat(FIELD_FRAME_RATE, frameRate);
bundle.putInt(keyForField(FIELD_ROTATION_DEGREES), rotationDegrees); bundle.putInt(FIELD_ROTATION_DEGREES, rotationDegrees);
bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio); bundle.putFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio);
bundle.putByteArray(keyForField(FIELD_PROJECTION_DATA), projectionData); bundle.putByteArray(FIELD_PROJECTION_DATA, projectionData);
bundle.putInt(keyForField(FIELD_STEREO_MODE), stereoMode); bundle.putInt(FIELD_STEREO_MODE, stereoMode);
if (colorInfo != null) { if (colorInfo != null) {
bundle.putBundle(keyForField(FIELD_COLOR_INFO), colorInfo.toBundle()); bundle.putBundle(FIELD_COLOR_INFO, colorInfo.toBundle());
} }
// Audio specific. // Audio specific.
bundle.putInt(keyForField(FIELD_CHANNEL_COUNT), channelCount); bundle.putInt(FIELD_CHANNEL_COUNT, channelCount);
bundle.putInt(keyForField(FIELD_SAMPLE_RATE), sampleRate); bundle.putInt(FIELD_SAMPLE_RATE, sampleRate);
bundle.putInt(keyForField(FIELD_PCM_ENCODING), pcmEncoding); bundle.putInt(FIELD_PCM_ENCODING, pcmEncoding);
bundle.putInt(keyForField(FIELD_ENCODER_DELAY), encoderDelay); bundle.putInt(FIELD_ENCODER_DELAY, encoderDelay);
bundle.putInt(keyForField(FIELD_ENCODER_PADDING), encoderPadding); bundle.putInt(FIELD_ENCODER_PADDING, encoderPadding);
// Text specific. // Text specific.
bundle.putInt(keyForField(FIELD_ACCESSIBILITY_CHANNEL), accessibilityChannel); bundle.putInt(FIELD_ACCESSIBILITY_CHANNEL, accessibilityChannel);
// Image specific.
bundle.putInt(FIELD_TILE_COUNT_HORIZONTAL, tileCountHorizontal);
bundle.putInt(FIELD_TILE_COUNT_VERTICAL, tileCountVertical);
// Source specific. // Source specific.
bundle.putInt(keyForField(FIELD_CRYPTO_TYPE), cryptoType); bundle.putInt(FIELD_CRYPTO_TYPE, cryptoType);
return bundle; return bundle;
} }
@ -1612,28 +1635,22 @@ public final class Format implements Bundleable {
Builder builder = new Builder(); Builder builder = new Builder();
BundleableUtil.ensureClassLoader(bundle); BundleableUtil.ensureClassLoader(bundle);
builder builder
.setId(defaultIfNull(bundle.getString(keyForField(FIELD_ID)), DEFAULT.id)) .setId(defaultIfNull(bundle.getString(FIELD_ID), DEFAULT.id))
.setLabel(defaultIfNull(bundle.getString(keyForField(FIELD_LABEL)), DEFAULT.label)) .setLabel(defaultIfNull(bundle.getString(FIELD_LABEL), DEFAULT.label))
.setLanguage(defaultIfNull(bundle.getString(keyForField(FIELD_LANGUAGE)), DEFAULT.language)) .setLanguage(defaultIfNull(bundle.getString(FIELD_LANGUAGE), DEFAULT.language))
.setSelectionFlags( .setSelectionFlags(bundle.getInt(FIELD_SELECTION_FLAGS, DEFAULT.selectionFlags))
bundle.getInt(keyForField(FIELD_SELECTION_FLAGS), DEFAULT.selectionFlags)) .setRoleFlags(bundle.getInt(FIELD_ROLE_FLAGS, DEFAULT.roleFlags))
.setRoleFlags(bundle.getInt(keyForField(FIELD_ROLE_FLAGS), DEFAULT.roleFlags)) .setAverageBitrate(bundle.getInt(FIELD_AVERAGE_BITRATE, DEFAULT.averageBitrate))
.setAverageBitrate( .setPeakBitrate(bundle.getInt(FIELD_PEAK_BITRATE, DEFAULT.peakBitrate))
bundle.getInt(keyForField(FIELD_AVERAGE_BITRATE), DEFAULT.averageBitrate)) .setCodecs(defaultIfNull(bundle.getString(FIELD_CODECS), DEFAULT.codecs))
.setPeakBitrate(bundle.getInt(keyForField(FIELD_PEAK_BITRATE), DEFAULT.peakBitrate)) .setMetadata(defaultIfNull(bundle.getParcelable(FIELD_METADATA), DEFAULT.metadata))
.setCodecs(defaultIfNull(bundle.getString(keyForField(FIELD_CODECS)), DEFAULT.codecs))
.setMetadata(
defaultIfNull(bundle.getParcelable(keyForField(FIELD_METADATA)), DEFAULT.metadata))
// Container specific. // Container specific.
.setContainerMimeType( .setContainerMimeType(
defaultIfNull( defaultIfNull(bundle.getString(FIELD_CONTAINER_MIME_TYPE), DEFAULT.containerMimeType))
bundle.getString(keyForField(FIELD_CONTAINER_MIME_TYPE)),
DEFAULT.containerMimeType))
// Sample specific. // Sample specific.
.setSampleMimeType( .setSampleMimeType(
defaultIfNull( defaultIfNull(bundle.getString(FIELD_SAMPLE_MIME_TYPE), DEFAULT.sampleMimeType))
bundle.getString(keyForField(FIELD_SAMPLE_MIME_TYPE)), DEFAULT.sampleMimeType)) .setMaxInputSize(bundle.getInt(FIELD_MAX_INPUT_SIZE, DEFAULT.maxInputSize));
.setMaxInputSize(bundle.getInt(keyForField(FIELD_MAX_INPUT_SIZE), DEFAULT.maxInputSize));
List<byte[]> initializationData = new ArrayList<>(); List<byte[]> initializationData = new ArrayList<>();
for (int i = 0; ; i++) { for (int i = 0; ; i++) {
@ -1645,51 +1662,55 @@ public final class Format implements Bundleable {
} }
builder builder
.setInitializationData(initializationData) .setInitializationData(initializationData)
.setDrmInitData(bundle.getParcelable(keyForField(FIELD_DRM_INIT_DATA))) .setDrmInitData(bundle.getParcelable(FIELD_DRM_INIT_DATA))
.setSubsampleOffsetUs( .setSubsampleOffsetUs(bundle.getLong(FIELD_SUBSAMPLE_OFFSET_US, DEFAULT.subsampleOffsetUs))
bundle.getLong(keyForField(FIELD_SUBSAMPLE_OFFSET_US), DEFAULT.subsampleOffsetUs))
// Video specific. // Video specific.
.setWidth(bundle.getInt(keyForField(FIELD_WIDTH), DEFAULT.width)) .setWidth(bundle.getInt(FIELD_WIDTH, DEFAULT.width))
.setHeight(bundle.getInt(keyForField(FIELD_HEIGHT), DEFAULT.height)) .setHeight(bundle.getInt(FIELD_HEIGHT, DEFAULT.height))
.setFrameRate(bundle.getFloat(keyForField(FIELD_FRAME_RATE), DEFAULT.frameRate)) .setFrameRate(bundle.getFloat(FIELD_FRAME_RATE, DEFAULT.frameRate))
.setRotationDegrees( .setRotationDegrees(bundle.getInt(FIELD_ROTATION_DEGREES, DEFAULT.rotationDegrees))
bundle.getInt(keyForField(FIELD_ROTATION_DEGREES), DEFAULT.rotationDegrees))
.setPixelWidthHeightRatio( .setPixelWidthHeightRatio(
bundle.getFloat( bundle.getFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, DEFAULT.pixelWidthHeightRatio))
keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT.pixelWidthHeightRatio)) .setProjectionData(bundle.getByteArray(FIELD_PROJECTION_DATA))
.setProjectionData(bundle.getByteArray(keyForField(FIELD_PROJECTION_DATA))) .setStereoMode(bundle.getInt(FIELD_STEREO_MODE, DEFAULT.stereoMode));
.setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode)); Bundle colorInfoBundle = bundle.getBundle(FIELD_COLOR_INFO);
Bundle colorInfoBundle = bundle.getBundle(keyForField(FIELD_COLOR_INFO));
if (colorInfoBundle != null) { if (colorInfoBundle != null) {
builder.setColorInfo(ColorInfo.CREATOR.fromBundle(colorInfoBundle)); builder.setColorInfo(ColorInfo.CREATOR.fromBundle(colorInfoBundle));
} }
// Audio specific. // Audio specific.
builder builder
.setChannelCount(bundle.getInt(keyForField(FIELD_CHANNEL_COUNT), DEFAULT.channelCount)) .setChannelCount(bundle.getInt(FIELD_CHANNEL_COUNT, DEFAULT.channelCount))
.setSampleRate(bundle.getInt(keyForField(FIELD_SAMPLE_RATE), DEFAULT.sampleRate)) .setSampleRate(bundle.getInt(FIELD_SAMPLE_RATE, DEFAULT.sampleRate))
.setPcmEncoding(bundle.getInt(keyForField(FIELD_PCM_ENCODING), DEFAULT.pcmEncoding)) .setPcmEncoding(bundle.getInt(FIELD_PCM_ENCODING, DEFAULT.pcmEncoding))
.setEncoderDelay(bundle.getInt(keyForField(FIELD_ENCODER_DELAY), DEFAULT.encoderDelay)) .setEncoderDelay(bundle.getInt(FIELD_ENCODER_DELAY, DEFAULT.encoderDelay))
.setEncoderPadding( .setEncoderPadding(bundle.getInt(FIELD_ENCODER_PADDING, DEFAULT.encoderPadding))
bundle.getInt(keyForField(FIELD_ENCODER_PADDING), DEFAULT.encoderPadding))
// Text specific. // Text specific.
.setAccessibilityChannel( .setAccessibilityChannel(
bundle.getInt(keyForField(FIELD_ACCESSIBILITY_CHANNEL), DEFAULT.accessibilityChannel)) bundle.getInt(FIELD_ACCESSIBILITY_CHANNEL, DEFAULT.accessibilityChannel))
// Image specific.
.setTileCountHorizontal(
bundle.getInt(FIELD_TILE_COUNT_HORIZONTAL, DEFAULT.tileCountHorizontal))
.setTileCountVertical(bundle.getInt(FIELD_TILE_COUNT_VERTICAL, DEFAULT.tileCountVertical))
// Source specific. // Source specific.
.setCryptoType(bundle.getInt(keyForField(FIELD_CRYPTO_TYPE), DEFAULT.cryptoType)); .setCryptoType(bundle.getInt(FIELD_CRYPTO_TYPE, DEFAULT.cryptoType));
return builder.build(); return builder.build();
} }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
private static String keyForInitializationData(int initialisationDataIndex) { private static String keyForInitializationData(int initialisationDataIndex) {
return keyForField(FIELD_INITIALIZATION_DATA) return FIELD_INITIALIZATION_DATA
+ "_" + "_"
+ Integer.toString(initialisationDataIndex, Character.MAX_RADIX); + Integer.toString(initialisationDataIndex, Character.MAX_RADIX);
} }
/**
* Utility method to get {@code defaultValue} if {@code value} is {@code null}. {@code
* defaultValue} can be {@code null}.
*
* <p>Note: Current implementations of getters in {@link Bundle}, for example {@link
* Bundle#getString(String, String)} does not allow the defaultValue to be {@code null}, hence the
* need for this method.
*/
@Nullable @Nullable
private static <T> T defaultIfNull(@Nullable T value, @Nullable T defaultValue) { private static <T> T defaultIfNull(@Nullable T value, @Nullable T defaultValue) {
return value != null ? value : defaultValue; return value != null ? value : defaultValue;

View File

@ -16,17 +16,12 @@
package androidx.media3.common; package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects; 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 * 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; private static final @RatingType int TYPE = RATING_TYPE_HEART;
@Documented private static final String FIELD_RATED = Util.intToStringMaxRadix(1);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_IS_HEART = Util.intToStringMaxRadix(2);
@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;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE); bundle.putInt(FIELD_RATING_TYPE, TYPE);
bundle.putBoolean(keyForField(FIELD_RATED), rated); bundle.putBoolean(FIELD_RATED, rated);
bundle.putBoolean(keyForField(FIELD_IS_HEART), isHeart); bundle.putBoolean(FIELD_IS_HEART, isHeart);
return bundle; return bundle;
} }
@ -104,16 +93,10 @@ public final class HeartRating extends Rating {
@UnstableApi public static final Creator<HeartRating> CREATOR = HeartRating::fromBundle; @UnstableApi public static final Creator<HeartRating> CREATOR = HeartRating::fromBundle;
private static HeartRating fromBundle(Bundle bundle) { private static HeartRating fromBundle(Bundle bundle) {
checkArgument( checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE);
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET) boolean isRated = bundle.getBoolean(FIELD_RATED, /* defaultValue= */ false);
== TYPE);
boolean isRated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false);
return isRated return isRated
? new HeartRating(bundle.getBoolean(keyForField(FIELD_IS_HEART), /* defaultValue= */ false)) ? new HeartRating(bundle.getBoolean(FIELD_IS_HEART, /* defaultValue= */ false))
: new HeartRating(); : new HeartRating();
} }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -17,11 +17,9 @@ package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
@ -31,10 +29,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe; import com.google.errorprone.annotations.InlineMe;
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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@ -1122,7 +1116,7 @@ public final class MediaItem implements Bundleable {
private float minPlaybackSpeed; private float minPlaybackSpeed;
private float maxPlaybackSpeed; private float maxPlaybackSpeed;
/** Constructs an instance. */ /** Creates a new instance with default values. */
public Builder() { public Builder() {
this.targetOffsetMs = C.TIME_UNSET; this.targetOffsetMs = C.TIME_UNSET;
this.minOffsetMs = C.TIME_UNSET; this.minOffsetMs = C.TIME_UNSET;
@ -1304,33 +1298,31 @@ public final class MediaItem implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_TARGET_OFFSET_MS = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_MIN_OFFSET_MS = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_MAX_OFFSET_MS = Util.intToStringMaxRadix(2);
@IntDef({ private static final String FIELD_MIN_PLAYBACK_SPEED = Util.intToStringMaxRadix(3);
FIELD_TARGET_OFFSET_MS, private static final String FIELD_MAX_PLAYBACK_SPEED = Util.intToStringMaxRadix(4);
FIELD_MIN_OFFSET_MS,
FIELD_MAX_OFFSET_MS,
FIELD_MIN_PLAYBACK_SPEED,
FIELD_MAX_PLAYBACK_SPEED
})
private @interface FieldNumber {}
private static final int FIELD_TARGET_OFFSET_MS = 0;
private static final int FIELD_MIN_OFFSET_MS = 1;
private static final int FIELD_MAX_OFFSET_MS = 2;
private static final int FIELD_MIN_PLAYBACK_SPEED = 3;
private static final int FIELD_MAX_PLAYBACK_SPEED = 4;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putLong(keyForField(FIELD_TARGET_OFFSET_MS), targetOffsetMs); if (targetOffsetMs != UNSET.targetOffsetMs) {
bundle.putLong(keyForField(FIELD_MIN_OFFSET_MS), minOffsetMs); bundle.putLong(FIELD_TARGET_OFFSET_MS, targetOffsetMs);
bundle.putLong(keyForField(FIELD_MAX_OFFSET_MS), maxOffsetMs); }
bundle.putFloat(keyForField(FIELD_MIN_PLAYBACK_SPEED), minPlaybackSpeed); if (minOffsetMs != UNSET.minOffsetMs) {
bundle.putFloat(keyForField(FIELD_MAX_PLAYBACK_SPEED), maxPlaybackSpeed); bundle.putLong(FIELD_MIN_OFFSET_MS, minOffsetMs);
}
if (maxOffsetMs != UNSET.maxOffsetMs) {
bundle.putLong(FIELD_MAX_OFFSET_MS, maxOffsetMs);
}
if (minPlaybackSpeed != UNSET.minPlaybackSpeed) {
bundle.putFloat(FIELD_MIN_PLAYBACK_SPEED, minPlaybackSpeed);
}
if (maxPlaybackSpeed != UNSET.maxPlaybackSpeed) {
bundle.putFloat(FIELD_MAX_PLAYBACK_SPEED, maxPlaybackSpeed);
}
return bundle; return bundle;
} }
@ -1339,18 +1331,13 @@ public final class MediaItem implements Bundleable {
public static final Creator<LiveConfiguration> CREATOR = public static final Creator<LiveConfiguration> CREATOR =
bundle -> bundle ->
new LiveConfiguration( new LiveConfiguration(
bundle.getLong( bundle.getLong(FIELD_TARGET_OFFSET_MS, /* defaultValue= */ UNSET.targetOffsetMs),
keyForField(FIELD_TARGET_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET), bundle.getLong(FIELD_MIN_OFFSET_MS, /* defaultValue= */ UNSET.minOffsetMs),
bundle.getLong(keyForField(FIELD_MIN_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET), bundle.getLong(FIELD_MAX_OFFSET_MS, /* defaultValue= */ UNSET.maxOffsetMs),
bundle.getLong(keyForField(FIELD_MAX_OFFSET_MS), /* defaultValue= */ C.TIME_UNSET),
bundle.getFloat( bundle.getFloat(
keyForField(FIELD_MIN_PLAYBACK_SPEED), /* defaultValue= */ C.RATE_UNSET), FIELD_MIN_PLAYBACK_SPEED, /* defaultValue= */ UNSET.minPlaybackSpeed),
bundle.getFloat( bundle.getFloat(
keyForField(FIELD_MAX_PLAYBACK_SPEED), /* defaultValue= */ C.RATE_UNSET)); FIELD_MAX_PLAYBACK_SPEED, /* defaultValue= */ UNSET.maxPlaybackSpeed));
private static String keyForField(@LiveConfiguration.FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }
/** Properties for a text track. */ /** Properties for a text track. */
@ -1589,7 +1576,7 @@ public final class MediaItem implements Bundleable {
private boolean relativeToDefaultPosition; private boolean relativeToDefaultPosition;
private boolean startsAtKeyFrame; private boolean startsAtKeyFrame;
/** Constructs an instance. */ /** Creates a new instance with default values. */
public Builder() { public Builder() {
endPositionMs = C.TIME_END_OF_SOURCE; endPositionMs = C.TIME_END_OF_SOURCE;
} }
@ -1742,33 +1729,31 @@ public final class MediaItem implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_START_POSITION_MS = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_END_POSITION_MS = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_RELATIVE_TO_LIVE_WINDOW = Util.intToStringMaxRadix(2);
@IntDef({ private static final String FIELD_RELATIVE_TO_DEFAULT_POSITION = Util.intToStringMaxRadix(3);
FIELD_START_POSITION_MS, private static final String FIELD_STARTS_AT_KEY_FRAME = Util.intToStringMaxRadix(4);
FIELD_END_POSITION_MS,
FIELD_RELATIVE_TO_LIVE_WINDOW,
FIELD_RELATIVE_TO_DEFAULT_POSITION,
FIELD_STARTS_AT_KEY_FRAME
})
private @interface FieldNumber {}
private static final int FIELD_START_POSITION_MS = 0;
private static final int FIELD_END_POSITION_MS = 1;
private static final int FIELD_RELATIVE_TO_LIVE_WINDOW = 2;
private static final int FIELD_RELATIVE_TO_DEFAULT_POSITION = 3;
private static final int FIELD_STARTS_AT_KEY_FRAME = 4;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putLong(keyForField(FIELD_START_POSITION_MS), startPositionMs); if (startPositionMs != UNSET.startPositionMs) {
bundle.putLong(keyForField(FIELD_END_POSITION_MS), endPositionMs); bundle.putLong(FIELD_START_POSITION_MS, startPositionMs);
bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), relativeToLiveWindow); }
bundle.putBoolean(keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), relativeToDefaultPosition); if (endPositionMs != UNSET.endPositionMs) {
bundle.putBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), startsAtKeyFrame); bundle.putLong(FIELD_END_POSITION_MS, endPositionMs);
}
if (relativeToLiveWindow != UNSET.relativeToLiveWindow) {
bundle.putBoolean(FIELD_RELATIVE_TO_LIVE_WINDOW, relativeToLiveWindow);
}
if (relativeToDefaultPosition != UNSET.relativeToDefaultPosition) {
bundle.putBoolean(FIELD_RELATIVE_TO_DEFAULT_POSITION, relativeToDefaultPosition);
}
if (startsAtKeyFrame != UNSET.startsAtKeyFrame) {
bundle.putBoolean(FIELD_STARTS_AT_KEY_FRAME, startsAtKeyFrame);
}
return bundle; return bundle;
} }
@ -1778,22 +1763,22 @@ public final class MediaItem implements Bundleable {
bundle -> bundle ->
new ClippingConfiguration.Builder() new ClippingConfiguration.Builder()
.setStartPositionMs( .setStartPositionMs(
bundle.getLong(keyForField(FIELD_START_POSITION_MS), /* defaultValue= */ 0))
.setEndPositionMs(
bundle.getLong( bundle.getLong(
keyForField(FIELD_END_POSITION_MS), FIELD_START_POSITION_MS, /* defaultValue= */ UNSET.startPositionMs))
/* defaultValue= */ C.TIME_END_OF_SOURCE)) .setEndPositionMs(
bundle.getLong(FIELD_END_POSITION_MS, /* defaultValue= */ UNSET.endPositionMs))
.setRelativeToLiveWindow( .setRelativeToLiveWindow(
bundle.getBoolean(keyForField(FIELD_RELATIVE_TO_LIVE_WINDOW), false)) bundle.getBoolean(
FIELD_RELATIVE_TO_LIVE_WINDOW,
/* defaultValue= */ UNSET.relativeToLiveWindow))
.setRelativeToDefaultPosition( .setRelativeToDefaultPosition(
bundle.getBoolean(keyForField(FIELD_RELATIVE_TO_DEFAULT_POSITION), false)) bundle.getBoolean(
FIELD_RELATIVE_TO_DEFAULT_POSITION,
/* defaultValue= */ UNSET.relativeToDefaultPosition))
.setStartsAtKeyFrame( .setStartsAtKeyFrame(
bundle.getBoolean(keyForField(FIELD_STARTS_AT_KEY_FRAME), false)) bundle.getBoolean(
FIELD_STARTS_AT_KEY_FRAME, /* defaultValue= */ UNSET.startsAtKeyFrame))
.buildClippingProperties(); .buildClippingProperties();
private static String keyForField(@ClippingConfiguration.FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }
/** /**
@ -1912,28 +1897,22 @@ public final class MediaItem implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_MEDIA_URI = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_SEARCH_QUERY = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(2);
@IntDef({FIELD_MEDIA_URI, FIELD_SEARCH_QUERY, FIELD_EXTRAS})
private @interface FieldNumber {}
private static final int FIELD_MEDIA_URI = 0;
private static final int FIELD_SEARCH_QUERY = 1;
private static final int FIELD_EXTRAS = 2;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
if (mediaUri != null) { if (mediaUri != null) {
bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri); bundle.putParcelable(FIELD_MEDIA_URI, mediaUri);
} }
if (searchQuery != null) { if (searchQuery != null) {
bundle.putString(keyForField(FIELD_SEARCH_QUERY), searchQuery); bundle.putString(FIELD_SEARCH_QUERY, searchQuery);
} }
if (extras != null) { if (extras != null) {
bundle.putBundle(keyForField(FIELD_EXTRAS), extras); bundle.putBundle(FIELD_EXTRAS, extras);
} }
return bundle; return bundle;
} }
@ -1943,14 +1922,10 @@ public final class MediaItem implements Bundleable {
public static final Creator<RequestMetadata> CREATOR = public static final Creator<RequestMetadata> CREATOR =
bundle -> bundle ->
new RequestMetadata.Builder() new RequestMetadata.Builder()
.setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI))) .setMediaUri(bundle.getParcelable(FIELD_MEDIA_URI))
.setSearchQuery(bundle.getString(keyForField(FIELD_SEARCH_QUERY))) .setSearchQuery(bundle.getString(FIELD_SEARCH_QUERY))
.setExtras(bundle.getBundle(keyForField(FIELD_EXTRAS))) .setExtras(bundle.getBundle(FIELD_EXTRAS))
.build(); .build();
private static String keyForField(@RequestMetadata.FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }
/** /**
@ -2046,24 +2021,11 @@ public final class MediaItem implements Bundleable {
} }
// Bundleable implementation. // Bundleable implementation.
private static final String FIELD_MEDIA_ID = Util.intToStringMaxRadix(0);
@Documented private static final String FIELD_LIVE_CONFIGURATION = Util.intToStringMaxRadix(1);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_MEDIA_METADATA = Util.intToStringMaxRadix(2);
@Target(TYPE_USE) private static final String FIELD_CLIPPING_PROPERTIES = Util.intToStringMaxRadix(3);
@IntDef({ private static final String FIELD_REQUEST_METADATA = Util.intToStringMaxRadix(4);
FIELD_MEDIA_ID,
FIELD_LIVE_CONFIGURATION,
FIELD_MEDIA_METADATA,
FIELD_CLIPPING_PROPERTIES,
FIELD_REQUEST_METADATA
})
private @interface FieldNumber {}
private static final int FIELD_MEDIA_ID = 0;
private static final int FIELD_LIVE_CONFIGURATION = 1;
private static final int FIELD_MEDIA_METADATA = 2;
private static final int FIELD_CLIPPING_PROPERTIES = 3;
private static final int FIELD_REQUEST_METADATA = 4;
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -2075,11 +2037,21 @@ public final class MediaItem implements Bundleable {
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putString(keyForField(FIELD_MEDIA_ID), mediaId); if (!mediaId.equals(DEFAULT_MEDIA_ID)) {
bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle()); bundle.putString(FIELD_MEDIA_ID, mediaId);
bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle()); }
bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingConfiguration.toBundle()); if (!liveConfiguration.equals(LiveConfiguration.UNSET)) {
bundle.putBundle(keyForField(FIELD_REQUEST_METADATA), requestMetadata.toBundle()); bundle.putBundle(FIELD_LIVE_CONFIGURATION, liveConfiguration.toBundle());
}
if (!mediaMetadata.equals(MediaMetadata.EMPTY)) {
bundle.putBundle(FIELD_MEDIA_METADATA, mediaMetadata.toBundle());
}
if (!clippingConfiguration.equals(ClippingConfiguration.UNSET)) {
bundle.putBundle(FIELD_CLIPPING_PROPERTIES, clippingConfiguration.toBundle());
}
if (!requestMetadata.equals(RequestMetadata.EMPTY)) {
bundle.putBundle(FIELD_REQUEST_METADATA, requestMetadata.toBundle());
}
return bundle; return bundle;
} }
@ -2092,31 +2064,29 @@ public final class MediaItem implements Bundleable {
@SuppressWarnings("deprecation") // Unbundling to ClippingProperties while it still exists. @SuppressWarnings("deprecation") // Unbundling to ClippingProperties while it still exists.
private static MediaItem fromBundle(Bundle bundle) { private static MediaItem fromBundle(Bundle bundle) {
String mediaId = checkNotNull(bundle.getString(keyForField(FIELD_MEDIA_ID), DEFAULT_MEDIA_ID)); String mediaId = checkNotNull(bundle.getString(FIELD_MEDIA_ID, DEFAULT_MEDIA_ID));
@Nullable @Nullable Bundle liveConfigurationBundle = bundle.getBundle(FIELD_LIVE_CONFIGURATION);
Bundle liveConfigurationBundle = bundle.getBundle(keyForField(FIELD_LIVE_CONFIGURATION));
LiveConfiguration liveConfiguration; LiveConfiguration liveConfiguration;
if (liveConfigurationBundle == null) { if (liveConfigurationBundle == null) {
liveConfiguration = LiveConfiguration.UNSET; liveConfiguration = LiveConfiguration.UNSET;
} else { } else {
liveConfiguration = LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle); liveConfiguration = LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle);
} }
@Nullable Bundle mediaMetadataBundle = bundle.getBundle(keyForField(FIELD_MEDIA_METADATA)); @Nullable Bundle mediaMetadataBundle = bundle.getBundle(FIELD_MEDIA_METADATA);
MediaMetadata mediaMetadata; MediaMetadata mediaMetadata;
if (mediaMetadataBundle == null) { if (mediaMetadataBundle == null) {
mediaMetadata = MediaMetadata.EMPTY; mediaMetadata = MediaMetadata.EMPTY;
} else { } else {
mediaMetadata = MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle); mediaMetadata = MediaMetadata.CREATOR.fromBundle(mediaMetadataBundle);
} }
@Nullable @Nullable Bundle clippingConfigurationBundle = bundle.getBundle(FIELD_CLIPPING_PROPERTIES);
Bundle clippingConfigurationBundle = bundle.getBundle(keyForField(FIELD_CLIPPING_PROPERTIES));
ClippingProperties clippingConfiguration; ClippingProperties clippingConfiguration;
if (clippingConfigurationBundle == null) { if (clippingConfigurationBundle == null) {
clippingConfiguration = ClippingProperties.UNSET; clippingConfiguration = ClippingProperties.UNSET;
} else { } else {
clippingConfiguration = ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle); clippingConfiguration = ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle);
} }
@Nullable Bundle requestMetadataBundle = bundle.getBundle(keyForField(FIELD_REQUEST_METADATA)); @Nullable Bundle requestMetadataBundle = bundle.getBundle(FIELD_REQUEST_METADATA);
RequestMetadata requestMetadata; RequestMetadata requestMetadata;
if (requestMetadataBundle == null) { if (requestMetadataBundle == null) {
requestMetadata = RequestMetadata.EMPTY; requestMetadata = RequestMetadata.EMPTY;
@ -2131,8 +2101,4 @@ public final class MediaItem implements Bundleable {
mediaMetadata, mediaMetadata,
requestMetadata); requestMetadata);
} }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -29,11 +29,11 @@ public final class MediaLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */ /** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
public static final String VERSION = "1.0.0-beta03"; public static final String VERSION = "1.0.0-rc01";
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */ /** The version of the library expressed as {@code TAG + "/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.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. * The version of the library expressed as an integer, for example 1002003300.
@ -47,7 +47,7 @@ public final class MediaLibraryInfo {
* (123-045-006-3-00). * (123-045-006-3-00).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
public static final int VERSION_INT = 1_000_000_1_03; public static final int VERSION_INT = 1_000_000_2_01;
/** Whether the library was compiled with {@link Assertions} checks enabled. */ /** Whether the library was compiled with {@link Assertions} checks enabled. */
public static final boolean ASSERTIONS_ENABLED = true; public static final boolean ASSERTIONS_ENABLED = true;

View File

@ -61,6 +61,7 @@ public final class MediaMetadata implements Bundleable {
@Nullable private Integer trackNumber; @Nullable private Integer trackNumber;
@Nullable private Integer totalTrackCount; @Nullable private Integer totalTrackCount;
@Nullable private @FolderType Integer folderType; @Nullable private @FolderType Integer folderType;
@Nullable private Boolean isBrowsable;
@Nullable private Boolean isPlayable; @Nullable private Boolean isPlayable;
@Nullable private Integer recordingYear; @Nullable private Integer recordingYear;
@Nullable private Integer recordingMonth; @Nullable private Integer recordingMonth;
@ -76,6 +77,7 @@ public final class MediaMetadata implements Bundleable {
@Nullable private CharSequence genre; @Nullable private CharSequence genre;
@Nullable private CharSequence compilation; @Nullable private CharSequence compilation;
@Nullable private CharSequence station; @Nullable private CharSequence station;
@Nullable private @MediaType Integer mediaType;
@Nullable private Bundle extras; @Nullable private Bundle extras;
public Builder() {} public Builder() {}
@ -96,6 +98,7 @@ public final class MediaMetadata implements Bundleable {
this.trackNumber = mediaMetadata.trackNumber; this.trackNumber = mediaMetadata.trackNumber;
this.totalTrackCount = mediaMetadata.totalTrackCount; this.totalTrackCount = mediaMetadata.totalTrackCount;
this.folderType = mediaMetadata.folderType; this.folderType = mediaMetadata.folderType;
this.isBrowsable = mediaMetadata.isBrowsable;
this.isPlayable = mediaMetadata.isPlayable; this.isPlayable = mediaMetadata.isPlayable;
this.recordingYear = mediaMetadata.recordingYear; this.recordingYear = mediaMetadata.recordingYear;
this.recordingMonth = mediaMetadata.recordingMonth; this.recordingMonth = mediaMetadata.recordingMonth;
@ -111,6 +114,7 @@ public final class MediaMetadata implements Bundleable {
this.genre = mediaMetadata.genre; this.genre = mediaMetadata.genre;
this.compilation = mediaMetadata.compilation; this.compilation = mediaMetadata.compilation;
this.station = mediaMetadata.station; this.station = mediaMetadata.station;
this.mediaType = mediaMetadata.mediaType;
this.extras = mediaMetadata.extras; this.extras = mediaMetadata.extras;
} }
@ -244,13 +248,26 @@ public final class MediaMetadata implements Bundleable {
return this; return this;
} }
/** Sets the {@link FolderType}. */ /**
* Sets the {@link FolderType}.
*
* <p>This method will be deprecated. Use {@link #setIsBrowsable} to indicate if an item is a
* browsable folder and use {@link #setMediaType} to indicate the type of the folder.
*/
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setFolderType(@Nullable @FolderType Integer folderType) { public Builder setFolderType(@Nullable @FolderType Integer folderType) {
this.folderType = folderType; this.folderType = folderType;
return this; return this;
} }
/** Sets whether the media is a browsable folder. */
@UnstableApi
@CanIgnoreReturnValue
public Builder setIsBrowsable(@Nullable Boolean isBrowsable) {
this.isBrowsable = isBrowsable;
return this;
}
/** Sets whether the media is playable. */ /** Sets whether the media is playable. */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setIsPlayable(@Nullable Boolean isPlayable) { public Builder setIsPlayable(@Nullable Boolean isPlayable) {
@ -383,6 +400,14 @@ public final class MediaMetadata implements Bundleable {
return this; return this;
} }
/** Sets the {@link MediaType}. */
@CanIgnoreReturnValue
@UnstableApi
public Builder setMediaType(@Nullable @MediaType Integer mediaType) {
this.mediaType = mediaType;
return this;
}
/** Sets the extras {@link Bundle}. */ /** Sets the extras {@link Bundle}. */
@CanIgnoreReturnValue @CanIgnoreReturnValue
public Builder setExtras(@Nullable Bundle extras) { public Builder setExtras(@Nullable Bundle extras) {
@ -481,6 +506,9 @@ public final class MediaMetadata implements Bundleable {
if (mediaMetadata.folderType != null) { if (mediaMetadata.folderType != null) {
setFolderType(mediaMetadata.folderType); setFolderType(mediaMetadata.folderType);
} }
if (mediaMetadata.isBrowsable != null) {
setIsBrowsable(mediaMetadata.isBrowsable);
}
if (mediaMetadata.isPlayable != null) { if (mediaMetadata.isPlayable != null) {
setIsPlayable(mediaMetadata.isPlayable); setIsPlayable(mediaMetadata.isPlayable);
} }
@ -529,6 +557,9 @@ public final class MediaMetadata implements Bundleable {
if (mediaMetadata.station != null) { if (mediaMetadata.station != null) {
setStation(mediaMetadata.station); setStation(mediaMetadata.station);
} }
if (mediaMetadata.mediaType != null) {
setMediaType(mediaMetadata.mediaType);
}
if (mediaMetadata.extras != null) { if (mediaMetadata.extras != null) {
setExtras(mediaMetadata.extras); setExtras(mediaMetadata.extras);
} }
@ -542,12 +573,186 @@ public final class MediaMetadata implements Bundleable {
} }
} }
/**
* The type of content described by the media item.
*
* <p>One of {@link #MEDIA_TYPE_MIXED}, {@link #MEDIA_TYPE_MUSIC}, {@link
* #MEDIA_TYPE_AUDIO_BOOK_CHAPTER}, {@link #MEDIA_TYPE_PODCAST_EPISODE}, {@link
* #MEDIA_TYPE_RADIO_STATION}, {@link #MEDIA_TYPE_NEWS}, {@link #MEDIA_TYPE_VIDEO}, {@link
* #MEDIA_TYPE_TRAILER}, {@link #MEDIA_TYPE_MOVIE}, {@link #MEDIA_TYPE_TV_SHOW}, {@link
* #MEDIA_TYPE_ALBUM}, {@link #MEDIA_TYPE_ARTIST}, {@link #MEDIA_TYPE_GENRE}, {@link
* #MEDIA_TYPE_PLAYLIST}, {@link #MEDIA_TYPE_YEAR}, {@link #MEDIA_TYPE_AUDIO_BOOK}, {@link
* #MEDIA_TYPE_PODCAST}, {@link #MEDIA_TYPE_TV_CHANNEL}, {@link #MEDIA_TYPE_TV_SERIES}, {@link
* #MEDIA_TYPE_TV_SEASON}, {@link #MEDIA_TYPE_FOLDER_MIXED}, {@link #MEDIA_TYPE_FOLDER_ALBUMS},
* {@link #MEDIA_TYPE_FOLDER_ARTISTS}, {@link #MEDIA_TYPE_FOLDER_GENRES}, {@link
* #MEDIA_TYPE_FOLDER_PLAYLISTS}, {@link #MEDIA_TYPE_FOLDER_YEARS}, {@link
* #MEDIA_TYPE_FOLDER_AUDIO_BOOKS}, {@link #MEDIA_TYPE_FOLDER_PODCASTS}, {@link
* #MEDIA_TYPE_FOLDER_TV_CHANNELS}, {@link #MEDIA_TYPE_FOLDER_TV_SERIES}, {@link
* #MEDIA_TYPE_FOLDER_TV_SHOWS}, {@link #MEDIA_TYPE_FOLDER_RADIO_STATIONS}, {@link
* #MEDIA_TYPE_FOLDER_NEWS}, {@link #MEDIA_TYPE_FOLDER_VIDEOS}, {@link
* #MEDIA_TYPE_FOLDER_TRAILERS} or {@link #MEDIA_TYPE_FOLDER_MOVIES}.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@UnstableApi
@IntDef({
MEDIA_TYPE_MIXED,
MEDIA_TYPE_MUSIC,
MEDIA_TYPE_AUDIO_BOOK_CHAPTER,
MEDIA_TYPE_PODCAST_EPISODE,
MEDIA_TYPE_RADIO_STATION,
MEDIA_TYPE_NEWS,
MEDIA_TYPE_VIDEO,
MEDIA_TYPE_TRAILER,
MEDIA_TYPE_MOVIE,
MEDIA_TYPE_TV_SHOW,
MEDIA_TYPE_ALBUM,
MEDIA_TYPE_ARTIST,
MEDIA_TYPE_GENRE,
MEDIA_TYPE_PLAYLIST,
MEDIA_TYPE_YEAR,
MEDIA_TYPE_AUDIO_BOOK,
MEDIA_TYPE_PODCAST,
MEDIA_TYPE_TV_CHANNEL,
MEDIA_TYPE_TV_SERIES,
MEDIA_TYPE_TV_SEASON,
MEDIA_TYPE_FOLDER_MIXED,
MEDIA_TYPE_FOLDER_ALBUMS,
MEDIA_TYPE_FOLDER_ARTISTS,
MEDIA_TYPE_FOLDER_GENRES,
MEDIA_TYPE_FOLDER_PLAYLISTS,
MEDIA_TYPE_FOLDER_YEARS,
MEDIA_TYPE_FOLDER_AUDIO_BOOKS,
MEDIA_TYPE_FOLDER_PODCASTS,
MEDIA_TYPE_FOLDER_TV_CHANNELS,
MEDIA_TYPE_FOLDER_TV_SERIES,
MEDIA_TYPE_FOLDER_TV_SHOWS,
MEDIA_TYPE_FOLDER_RADIO_STATIONS,
MEDIA_TYPE_FOLDER_NEWS,
MEDIA_TYPE_FOLDER_VIDEOS,
MEDIA_TYPE_FOLDER_TRAILERS,
MEDIA_TYPE_FOLDER_MOVIES,
})
public @interface MediaType {}
/** Media of undetermined type or a mix of multiple {@linkplain MediaType media types}. */
@UnstableApi public static final int MEDIA_TYPE_MIXED = 0;
/** {@link MediaType} for music. */
@UnstableApi public static final int MEDIA_TYPE_MUSIC = 1;
/** {@link MediaType} for an audio book chapter. */
@UnstableApi public static final int MEDIA_TYPE_AUDIO_BOOK_CHAPTER = 2;
/** {@link MediaType} for a podcast episode. */
@UnstableApi public static final int MEDIA_TYPE_PODCAST_EPISODE = 3;
/** {@link MediaType} for a radio station. */
@UnstableApi public static final int MEDIA_TYPE_RADIO_STATION = 4;
/** {@link MediaType} for news. */
@UnstableApi public static final int MEDIA_TYPE_NEWS = 5;
/** {@link MediaType} for a video. */
@UnstableApi public static final int MEDIA_TYPE_VIDEO = 6;
/** {@link MediaType} for a movie trailer. */
@UnstableApi public static final int MEDIA_TYPE_TRAILER = 7;
/** {@link MediaType} for a movie. */
@UnstableApi public static final int MEDIA_TYPE_MOVIE = 8;
/** {@link MediaType} for a TV show. */
@UnstableApi public static final int MEDIA_TYPE_TV_SHOW = 9;
/**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) belonging to an
* album.
*/
@UnstableApi public static final int MEDIA_TYPE_ALBUM = 10;
/**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) from the same
* artist.
*/
@UnstableApi public static final int MEDIA_TYPE_ARTIST = 11;
/**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) of the same
* genre.
*/
@UnstableApi public static final int MEDIA_TYPE_GENRE = 12;
/**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) forming a
* playlist.
*/
@UnstableApi public static final int MEDIA_TYPE_PLAYLIST = 13;
/**
* {@link MediaType} for a group of items (e.g., {@link #MEDIA_TYPE_MUSIC music}) from the same
* year.
*/
@UnstableApi public static final int MEDIA_TYPE_YEAR = 14;
/**
* {@link MediaType} for a group of items forming an audio book. Items in this group are typically
* of type {@link #MEDIA_TYPE_AUDIO_BOOK_CHAPTER}.
*/
@UnstableApi public static final int MEDIA_TYPE_AUDIO_BOOK = 15;
/**
* {@link MediaType} for a group of items belonging to a podcast. Items in this group are
* typically of type {@link #MEDIA_TYPE_PODCAST_EPISODE}.
*/
@UnstableApi public static final int MEDIA_TYPE_PODCAST = 16;
/**
* {@link MediaType} for a group of items that are part of a TV channel. Items in this group are
* typically of type {@link #MEDIA_TYPE_TV_SHOW}, {@link #MEDIA_TYPE_TV_SERIES} or {@link
* #MEDIA_TYPE_MOVIE}.
*/
@UnstableApi public static final int MEDIA_TYPE_TV_CHANNEL = 17;
/**
* {@link MediaType} for a group of items that are part of a TV series. Items in this group are
* typically of type {@link #MEDIA_TYPE_TV_SHOW} or {@link #MEDIA_TYPE_TV_SEASON}.
*/
@UnstableApi public static final int MEDIA_TYPE_TV_SERIES = 18;
/**
* {@link MediaType} for a group of items that are part of a TV series. Items in this group are
* typically of type {@link #MEDIA_TYPE_TV_SHOW}.
*/
@UnstableApi public static final int MEDIA_TYPE_TV_SEASON = 19;
/** {@link MediaType} for a folder with mixed or undetermined content. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_MIXED = 20;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_ALBUM albums}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_ALBUMS = 21;
/** {@link MediaType} for a folder containing {@linkplain #FIELD_ARTIST artists}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_ARTISTS = 22;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_GENRE genres}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_GENRES = 23;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_PLAYLIST playlists}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_PLAYLISTS = 24;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_YEAR years}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_YEARS = 25;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_AUDIO_BOOK audio books}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_AUDIO_BOOKS = 26;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_PODCAST podcasts}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_PODCASTS = 27;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_CHANNEL TV channels}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_TV_CHANNELS = 28;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_SERIES TV series}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_TV_SERIES = 29;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TV_SHOW TV shows}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_TV_SHOWS = 30;
/**
* {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_RADIO_STATION radio
* stations}.
*/
@UnstableApi public static final int MEDIA_TYPE_FOLDER_RADIO_STATIONS = 31;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_NEWS news}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_NEWS = 32;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_VIDEO videos}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_VIDEOS = 33;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_TRAILER movie trailers}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_TRAILERS = 34;
/** {@link MediaType} for a folder containing {@linkplain #MEDIA_TYPE_MOVIE movies}. */
@UnstableApi public static final int MEDIA_TYPE_FOLDER_MOVIES = 35;
/** /**
* The folder type of the media item. * The folder type of the media item.
* *
* <p>This can be used as the type of a browsable bluetooth folder (see section 6.10.2.2 of the <a * <p>This can be used as the type of a browsable bluetooth folder (see section 6.10.2.2 of the <a
* href="https://www.bluetooth.com/specifications/specs/a-v-remote-control-profile-1-6-2/">Bluetooth * href="https://www.bluetooth.com/specifications/specs/a-v-remote-control-profile-1-6-2/">Bluetooth
* AVRCP 1.6.2</a>). * AVRCP 1.6.2</a>).
*
* <p>One of {@link #FOLDER_TYPE_NONE}, {@link #FOLDER_TYPE_MIXED}, {@link #FOLDER_TYPE_TITLES},
* {@link #FOLDER_TYPE_ALBUMS}, {@link #FOLDER_TYPE_ARTISTS}, {@link #FOLDER_TYPE_GENRES}, {@link
* #FOLDER_TYPE_PLAYLISTS} or {@link #FOLDER_TYPE_YEARS}.
*/ */
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added. // with Kotlin usages from before TYPE_USE was added.
@ -588,6 +793,17 @@ public final class MediaMetadata implements Bundleable {
* *
* <p>Values sourced from the ID3 v2.4 specification (See section 4.14 of * <p>Values sourced from the ID3 v2.4 specification (See section 4.14 of
* https://id3.org/id3v2.4.0-frames). * https://id3.org/id3v2.4.0-frames).
*
* <p>One of {@link #PICTURE_TYPE_OTHER}, {@link #PICTURE_TYPE_FILE_ICON}, {@link
* #PICTURE_TYPE_FILE_ICON_OTHER}, {@link #PICTURE_TYPE_FRONT_COVER}, {@link
* #PICTURE_TYPE_BACK_COVER}, {@link #PICTURE_TYPE_LEAFLET_PAGE}, {@link #PICTURE_TYPE_MEDIA},
* {@link #PICTURE_TYPE_LEAD_ARTIST_PERFORMER}, {@link #PICTURE_TYPE_ARTIST_PERFORMER}, {@link
* #PICTURE_TYPE_CONDUCTOR}, {@link #PICTURE_TYPE_BAND_ORCHESTRA}, {@link #PICTURE_TYPE_COMPOSER},
* {@link #PICTURE_TYPE_LYRICIST}, {@link #PICTURE_TYPE_RECORDING_LOCATION}, {@link
* #PICTURE_TYPE_DURING_RECORDING}, {@link #PICTURE_TYPE_DURING_PERFORMANCE}, {@link
* #PICTURE_TYPE_MOVIE_VIDEO_SCREEN_CAPTURE}, {@link #PICTURE_TYPE_A_BRIGHT_COLORED_FISH}, {@link
* #PICTURE_TYPE_ILLUSTRATION}, {@link #PICTURE_TYPE_BAND_ARTIST_LOGO} or {@link
* #PICTURE_TYPE_PUBLISHER_STUDIO_LOGO}.
*/ */
// @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
// with Kotlin usages from before TYPE_USE was added. // with Kotlin usages from before TYPE_USE was added.
@ -676,9 +892,16 @@ public final class MediaMetadata implements Bundleable {
@Nullable public final Integer trackNumber; @Nullable public final Integer trackNumber;
/** Optional total number of tracks. */ /** Optional total number of tracks. */
@Nullable public final Integer totalTrackCount; @Nullable public final Integer totalTrackCount;
/** Optional {@link FolderType}. */ /**
* Optional {@link FolderType}.
*
* <p>This field will be deprecated. Use {@link #isBrowsable} to indicate if an item is a
* browsable folder and use {@link #mediaType} to indicate the type of the folder.
*/
@Nullable public final @FolderType Integer folderType; @Nullable public final @FolderType Integer folderType;
/** Optional boolean for media playability. */ /** Optional boolean to indicate that the media is a browsable folder. */
@UnstableApi @Nullable public final Boolean isBrowsable;
/** Optional boolean to indicate that the media is playable. */
@Nullable public final Boolean isPlayable; @Nullable public final Boolean isPlayable;
/** /**
* @deprecated Use {@link #recordingYear} instead. * @deprecated Use {@link #recordingYear} instead.
@ -729,6 +952,8 @@ public final class MediaMetadata implements Bundleable {
@Nullable public final CharSequence compilation; @Nullable public final CharSequence compilation;
/** Optional name of the station streaming the media. */ /** Optional name of the station streaming the media. */
@Nullable public final CharSequence station; @Nullable public final CharSequence station;
/** Optional {@link MediaType}. */
@UnstableApi @Nullable public final @MediaType Integer mediaType;
/** /**
* Optional extras {@link Bundle}. * Optional extras {@link Bundle}.
@ -739,6 +964,22 @@ public final class MediaMetadata implements Bundleable {
@Nullable public final Bundle extras; @Nullable public final Bundle extras;
private MediaMetadata(Builder builder) { private MediaMetadata(Builder builder) {
// Handle compatibility for deprecated fields.
@Nullable Boolean isBrowsable = builder.isBrowsable;
@Nullable Integer folderType = builder.folderType;
@Nullable Integer mediaType = builder.mediaType;
if (isBrowsable != null) {
if (!isBrowsable) {
folderType = FOLDER_TYPE_NONE;
} else if (folderType == null || folderType == FOLDER_TYPE_NONE) {
folderType = mediaType != null ? getFolderTypeFromMediaType(mediaType) : FOLDER_TYPE_MIXED;
}
} else if (folderType != null) {
isBrowsable = folderType != FOLDER_TYPE_NONE;
if (isBrowsable && mediaType == null) {
mediaType = getMediaTypeFromFolderType(folderType);
}
}
this.title = builder.title; this.title = builder.title;
this.artist = builder.artist; this.artist = builder.artist;
this.albumTitle = builder.albumTitle; this.albumTitle = builder.albumTitle;
@ -753,7 +994,8 @@ public final class MediaMetadata implements Bundleable {
this.artworkUri = builder.artworkUri; this.artworkUri = builder.artworkUri;
this.trackNumber = builder.trackNumber; this.trackNumber = builder.trackNumber;
this.totalTrackCount = builder.totalTrackCount; this.totalTrackCount = builder.totalTrackCount;
this.folderType = builder.folderType; this.folderType = folderType;
this.isBrowsable = isBrowsable;
this.isPlayable = builder.isPlayable; this.isPlayable = builder.isPlayable;
this.year = builder.recordingYear; this.year = builder.recordingYear;
this.recordingYear = builder.recordingYear; this.recordingYear = builder.recordingYear;
@ -770,6 +1012,7 @@ public final class MediaMetadata implements Bundleable {
this.genre = builder.genre; this.genre = builder.genre;
this.compilation = builder.compilation; this.compilation = builder.compilation;
this.station = builder.station; this.station = builder.station;
this.mediaType = mediaType;
this.extras = builder.extras; this.extras = builder.extras;
} }
@ -802,6 +1045,7 @@ public final class MediaMetadata implements Bundleable {
&& Util.areEqual(trackNumber, that.trackNumber) && Util.areEqual(trackNumber, that.trackNumber)
&& Util.areEqual(totalTrackCount, that.totalTrackCount) && Util.areEqual(totalTrackCount, that.totalTrackCount)
&& Util.areEqual(folderType, that.folderType) && Util.areEqual(folderType, that.folderType)
&& Util.areEqual(isBrowsable, that.isBrowsable)
&& Util.areEqual(isPlayable, that.isPlayable) && Util.areEqual(isPlayable, that.isPlayable)
&& Util.areEqual(recordingYear, that.recordingYear) && Util.areEqual(recordingYear, that.recordingYear)
&& Util.areEqual(recordingMonth, that.recordingMonth) && Util.areEqual(recordingMonth, that.recordingMonth)
@ -816,7 +1060,8 @@ public final class MediaMetadata implements Bundleable {
&& Util.areEqual(totalDiscCount, that.totalDiscCount) && Util.areEqual(totalDiscCount, that.totalDiscCount)
&& Util.areEqual(genre, that.genre) && Util.areEqual(genre, that.genre)
&& Util.areEqual(compilation, that.compilation) && Util.areEqual(compilation, that.compilation)
&& Util.areEqual(station, that.station); && Util.areEqual(station, that.station)
&& Util.areEqual(mediaType, that.mediaType);
} }
@Override @Override
@ -837,6 +1082,7 @@ public final class MediaMetadata implements Bundleable {
trackNumber, trackNumber,
totalTrackCount, totalTrackCount,
folderType, folderType,
isBrowsable,
isPlayable, isPlayable,
recordingYear, recordingYear,
recordingMonth, recordingMonth,
@ -851,150 +1097,149 @@ public final class MediaMetadata implements Bundleable {
totalDiscCount, totalDiscCount,
genre, genre,
compilation, compilation,
station); station,
mediaType);
} }
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_TITLE = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_ARTIST = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_ALBUM_TITLE = Util.intToStringMaxRadix(2);
@IntDef({ private static final String FIELD_ALBUM_ARTIST = Util.intToStringMaxRadix(3);
FIELD_TITLE, private static final String FIELD_DISPLAY_TITLE = Util.intToStringMaxRadix(4);
FIELD_ARTIST, private static final String FIELD_SUBTITLE = Util.intToStringMaxRadix(5);
FIELD_ALBUM_TITLE, private static final String FIELD_DESCRIPTION = Util.intToStringMaxRadix(6);
FIELD_ALBUM_ARTIST, // 7 is reserved to maintain backward compatibility for a previously defined field.
FIELD_DISPLAY_TITLE, private static final String FIELD_USER_RATING = Util.intToStringMaxRadix(8);
FIELD_SUBTITLE, private static final String FIELD_OVERALL_RATING = Util.intToStringMaxRadix(9);
FIELD_DESCRIPTION, private static final String FIELD_ARTWORK_DATA = Util.intToStringMaxRadix(10);
FIELD_MEDIA_URI, private static final String FIELD_ARTWORK_URI = Util.intToStringMaxRadix(11);
FIELD_USER_RATING, private static final String FIELD_TRACK_NUMBER = Util.intToStringMaxRadix(12);
FIELD_OVERALL_RATING, private static final String FIELD_TOTAL_TRACK_COUNT = Util.intToStringMaxRadix(13);
FIELD_ARTWORK_DATA, private static final String FIELD_FOLDER_TYPE = Util.intToStringMaxRadix(14);
FIELD_ARTWORK_DATA_TYPE, private static final String FIELD_IS_PLAYABLE = Util.intToStringMaxRadix(15);
FIELD_ARTWORK_URI, private static final String FIELD_RECORDING_YEAR = Util.intToStringMaxRadix(16);
FIELD_TRACK_NUMBER, private static final String FIELD_RECORDING_MONTH = Util.intToStringMaxRadix(17);
FIELD_TOTAL_TRACK_COUNT, private static final String FIELD_RECORDING_DAY = Util.intToStringMaxRadix(18);
FIELD_FOLDER_TYPE, private static final String FIELD_RELEASE_YEAR = Util.intToStringMaxRadix(19);
FIELD_IS_PLAYABLE, private static final String FIELD_RELEASE_MONTH = Util.intToStringMaxRadix(20);
FIELD_RECORDING_YEAR, private static final String FIELD_RELEASE_DAY = Util.intToStringMaxRadix(21);
FIELD_RECORDING_MONTH, private static final String FIELD_WRITER = Util.intToStringMaxRadix(22);
FIELD_RECORDING_DAY, private static final String FIELD_COMPOSER = Util.intToStringMaxRadix(23);
FIELD_RELEASE_YEAR, private static final String FIELD_CONDUCTOR = Util.intToStringMaxRadix(24);
FIELD_RELEASE_MONTH, private static final String FIELD_DISC_NUMBER = Util.intToStringMaxRadix(25);
FIELD_RELEASE_DAY, private static final String FIELD_TOTAL_DISC_COUNT = Util.intToStringMaxRadix(26);
FIELD_WRITER, private static final String FIELD_GENRE = Util.intToStringMaxRadix(27);
FIELD_COMPOSER, private static final String FIELD_COMPILATION = Util.intToStringMaxRadix(28);
FIELD_CONDUCTOR, private static final String FIELD_ARTWORK_DATA_TYPE = Util.intToStringMaxRadix(29);
FIELD_DISC_NUMBER, private static final String FIELD_STATION = Util.intToStringMaxRadix(30);
FIELD_TOTAL_DISC_COUNT, private static final String FIELD_MEDIA_TYPE = Util.intToStringMaxRadix(31);
FIELD_GENRE, private static final String FIELD_IS_BROWSABLE = Util.intToStringMaxRadix(32);
FIELD_COMPILATION, private static final String FIELD_EXTRAS = Util.intToStringMaxRadix(1000);
FIELD_STATION,
FIELD_EXTRAS
})
private @interface FieldNumber {}
private static final int FIELD_TITLE = 0;
private static final int FIELD_ARTIST = 1;
private static final int FIELD_ALBUM_TITLE = 2;
private static final int FIELD_ALBUM_ARTIST = 3;
private static final int FIELD_DISPLAY_TITLE = 4;
private static final int FIELD_SUBTITLE = 5;
private static final int FIELD_DESCRIPTION = 6;
private static final int FIELD_MEDIA_URI = 7;
private static final int FIELD_USER_RATING = 8;
private static final int FIELD_OVERALL_RATING = 9;
private static final int FIELD_ARTWORK_DATA = 10;
private static final int FIELD_ARTWORK_URI = 11;
private static final int FIELD_TRACK_NUMBER = 12;
private static final int FIELD_TOTAL_TRACK_COUNT = 13;
private static final int FIELD_FOLDER_TYPE = 14;
private static final int FIELD_IS_PLAYABLE = 15;
private static final int FIELD_RECORDING_YEAR = 16;
private static final int FIELD_RECORDING_MONTH = 17;
private static final int FIELD_RECORDING_DAY = 18;
private static final int FIELD_RELEASE_YEAR = 19;
private static final int FIELD_RELEASE_MONTH = 20;
private static final int FIELD_RELEASE_DAY = 21;
private static final int FIELD_WRITER = 22;
private static final int FIELD_COMPOSER = 23;
private static final int FIELD_CONDUCTOR = 24;
private static final int FIELD_DISC_NUMBER = 25;
private static final int FIELD_TOTAL_DISC_COUNT = 26;
private static final int FIELD_GENRE = 27;
private static final int FIELD_COMPILATION = 28;
private static final int FIELD_ARTWORK_DATA_TYPE = 29;
private static final int FIELD_STATION = 30;
private static final int FIELD_EXTRAS = 1000;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putCharSequence(keyForField(FIELD_TITLE), title); if (title != null) {
bundle.putCharSequence(keyForField(FIELD_ARTIST), artist); bundle.putCharSequence(FIELD_TITLE, title);
bundle.putCharSequence(keyForField(FIELD_ALBUM_TITLE), albumTitle); }
bundle.putCharSequence(keyForField(FIELD_ALBUM_ARTIST), albumArtist); if (artist != null) {
bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle); bundle.putCharSequence(FIELD_ARTIST, artist);
bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle); }
bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description); if (albumTitle != null) {
bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData); bundle.putCharSequence(FIELD_ALBUM_TITLE, albumTitle);
bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri); }
bundle.putCharSequence(keyForField(FIELD_WRITER), writer); if (albumArtist != null) {
bundle.putCharSequence(keyForField(FIELD_COMPOSER), composer); bundle.putCharSequence(FIELD_ALBUM_ARTIST, albumArtist);
bundle.putCharSequence(keyForField(FIELD_CONDUCTOR), conductor); }
bundle.putCharSequence(keyForField(FIELD_GENRE), genre); if (displayTitle != null) {
bundle.putCharSequence(keyForField(FIELD_COMPILATION), compilation); bundle.putCharSequence(FIELD_DISPLAY_TITLE, displayTitle);
bundle.putCharSequence(keyForField(FIELD_STATION), station); }
if (subtitle != null) {
bundle.putCharSequence(FIELD_SUBTITLE, subtitle);
}
if (description != null) {
bundle.putCharSequence(FIELD_DESCRIPTION, description);
}
if (artworkData != null) {
bundle.putByteArray(FIELD_ARTWORK_DATA, artworkData);
}
if (artworkUri != null) {
bundle.putParcelable(FIELD_ARTWORK_URI, artworkUri);
}
if (writer != null) {
bundle.putCharSequence(FIELD_WRITER, writer);
}
if (composer != null) {
bundle.putCharSequence(FIELD_COMPOSER, composer);
}
if (conductor != null) {
bundle.putCharSequence(FIELD_CONDUCTOR, conductor);
}
if (genre != null) {
bundle.putCharSequence(FIELD_GENRE, genre);
}
if (compilation != null) {
bundle.putCharSequence(FIELD_COMPILATION, compilation);
}
if (station != null) {
bundle.putCharSequence(FIELD_STATION, station);
}
if (userRating != null) { if (userRating != null) {
bundle.putBundle(keyForField(FIELD_USER_RATING), userRating.toBundle()); bundle.putBundle(FIELD_USER_RATING, userRating.toBundle());
} }
if (overallRating != null) { if (overallRating != null) {
bundle.putBundle(keyForField(FIELD_OVERALL_RATING), overallRating.toBundle()); bundle.putBundle(FIELD_OVERALL_RATING, overallRating.toBundle());
} }
if (trackNumber != null) { if (trackNumber != null) {
bundle.putInt(keyForField(FIELD_TRACK_NUMBER), trackNumber); bundle.putInt(FIELD_TRACK_NUMBER, trackNumber);
} }
if (totalTrackCount != null) { if (totalTrackCount != null) {
bundle.putInt(keyForField(FIELD_TOTAL_TRACK_COUNT), totalTrackCount); bundle.putInt(FIELD_TOTAL_TRACK_COUNT, totalTrackCount);
} }
if (folderType != null) { if (folderType != null) {
bundle.putInt(keyForField(FIELD_FOLDER_TYPE), folderType); bundle.putInt(FIELD_FOLDER_TYPE, folderType);
}
if (isBrowsable != null) {
bundle.putBoolean(FIELD_IS_BROWSABLE, isBrowsable);
} }
if (isPlayable != null) { if (isPlayable != null) {
bundle.putBoolean(keyForField(FIELD_IS_PLAYABLE), isPlayable); bundle.putBoolean(FIELD_IS_PLAYABLE, isPlayable);
} }
if (recordingYear != null) { if (recordingYear != null) {
bundle.putInt(keyForField(FIELD_RECORDING_YEAR), recordingYear); bundle.putInt(FIELD_RECORDING_YEAR, recordingYear);
} }
if (recordingMonth != null) { if (recordingMonth != null) {
bundle.putInt(keyForField(FIELD_RECORDING_MONTH), recordingMonth); bundle.putInt(FIELD_RECORDING_MONTH, recordingMonth);
} }
if (recordingDay != null) { if (recordingDay != null) {
bundle.putInt(keyForField(FIELD_RECORDING_DAY), recordingDay); bundle.putInt(FIELD_RECORDING_DAY, recordingDay);
} }
if (releaseYear != null) { if (releaseYear != null) {
bundle.putInt(keyForField(FIELD_RELEASE_YEAR), releaseYear); bundle.putInt(FIELD_RELEASE_YEAR, releaseYear);
} }
if (releaseMonth != null) { if (releaseMonth != null) {
bundle.putInt(keyForField(FIELD_RELEASE_MONTH), releaseMonth); bundle.putInt(FIELD_RELEASE_MONTH, releaseMonth);
} }
if (releaseDay != null) { if (releaseDay != null) {
bundle.putInt(keyForField(FIELD_RELEASE_DAY), releaseDay); bundle.putInt(FIELD_RELEASE_DAY, releaseDay);
} }
if (discNumber != null) { if (discNumber != null) {
bundle.putInt(keyForField(FIELD_DISC_NUMBER), discNumber); bundle.putInt(FIELD_DISC_NUMBER, discNumber);
} }
if (totalDiscCount != null) { if (totalDiscCount != null) {
bundle.putInt(keyForField(FIELD_TOTAL_DISC_COUNT), totalDiscCount); bundle.putInt(FIELD_TOTAL_DISC_COUNT, totalDiscCount);
} }
if (artworkDataType != null) { if (artworkDataType != null) {
bundle.putInt(keyForField(FIELD_ARTWORK_DATA_TYPE), artworkDataType); bundle.putInt(FIELD_ARTWORK_DATA_TYPE, artworkDataType);
}
if (mediaType != null) {
bundle.putInt(FIELD_MEDIA_TYPE, mediaType);
} }
if (extras != null) { if (extras != null) {
bundle.putBundle(keyForField(FIELD_EXTRAS), extras); bundle.putBundle(FIELD_EXTRAS, extras);
} }
return bundle; return bundle;
} }
@ -1005,80 +1250,152 @@ public final class MediaMetadata implements Bundleable {
private static MediaMetadata fromBundle(Bundle bundle) { private static MediaMetadata fromBundle(Bundle bundle) {
Builder builder = new Builder(); Builder builder = new Builder();
builder builder
.setTitle(bundle.getCharSequence(keyForField(FIELD_TITLE))) .setTitle(bundle.getCharSequence(FIELD_TITLE))
.setArtist(bundle.getCharSequence(keyForField(FIELD_ARTIST))) .setArtist(bundle.getCharSequence(FIELD_ARTIST))
.setAlbumTitle(bundle.getCharSequence(keyForField(FIELD_ALBUM_TITLE))) .setAlbumTitle(bundle.getCharSequence(FIELD_ALBUM_TITLE))
.setAlbumArtist(bundle.getCharSequence(keyForField(FIELD_ALBUM_ARTIST))) .setAlbumArtist(bundle.getCharSequence(FIELD_ALBUM_ARTIST))
.setDisplayTitle(bundle.getCharSequence(keyForField(FIELD_DISPLAY_TITLE))) .setDisplayTitle(bundle.getCharSequence(FIELD_DISPLAY_TITLE))
.setSubtitle(bundle.getCharSequence(keyForField(FIELD_SUBTITLE))) .setSubtitle(bundle.getCharSequence(FIELD_SUBTITLE))
.setDescription(bundle.getCharSequence(keyForField(FIELD_DESCRIPTION))) .setDescription(bundle.getCharSequence(FIELD_DESCRIPTION))
.setArtworkData( .setArtworkData(
bundle.getByteArray(keyForField(FIELD_ARTWORK_DATA)), bundle.getByteArray(FIELD_ARTWORK_DATA),
bundle.containsKey(keyForField(FIELD_ARTWORK_DATA_TYPE)) bundle.containsKey(FIELD_ARTWORK_DATA_TYPE)
? bundle.getInt(keyForField(FIELD_ARTWORK_DATA_TYPE)) ? bundle.getInt(FIELD_ARTWORK_DATA_TYPE)
: null) : null)
.setArtworkUri(bundle.getParcelable(keyForField(FIELD_ARTWORK_URI))) .setArtworkUri(bundle.getParcelable(FIELD_ARTWORK_URI))
.setWriter(bundle.getCharSequence(keyForField(FIELD_WRITER))) .setWriter(bundle.getCharSequence(FIELD_WRITER))
.setComposer(bundle.getCharSequence(keyForField(FIELD_COMPOSER))) .setComposer(bundle.getCharSequence(FIELD_COMPOSER))
.setConductor(bundle.getCharSequence(keyForField(FIELD_CONDUCTOR))) .setConductor(bundle.getCharSequence(FIELD_CONDUCTOR))
.setGenre(bundle.getCharSequence(keyForField(FIELD_GENRE))) .setGenre(bundle.getCharSequence(FIELD_GENRE))
.setCompilation(bundle.getCharSequence(keyForField(FIELD_COMPILATION))) .setCompilation(bundle.getCharSequence(FIELD_COMPILATION))
.setStation(bundle.getCharSequence(keyForField(FIELD_STATION))) .setStation(bundle.getCharSequence(FIELD_STATION))
.setExtras(bundle.getBundle(keyForField(FIELD_EXTRAS))); .setExtras(bundle.getBundle(FIELD_EXTRAS));
if (bundle.containsKey(keyForField(FIELD_USER_RATING))) { if (bundle.containsKey(FIELD_USER_RATING)) {
@Nullable Bundle fieldBundle = bundle.getBundle(keyForField(FIELD_USER_RATING)); @Nullable Bundle fieldBundle = bundle.getBundle(FIELD_USER_RATING);
if (fieldBundle != null) { if (fieldBundle != null) {
builder.setUserRating(Rating.CREATOR.fromBundle(fieldBundle)); builder.setUserRating(Rating.CREATOR.fromBundle(fieldBundle));
} }
} }
if (bundle.containsKey(keyForField(FIELD_OVERALL_RATING))) { if (bundle.containsKey(FIELD_OVERALL_RATING)) {
@Nullable Bundle fieldBundle = bundle.getBundle(keyForField(FIELD_OVERALL_RATING)); @Nullable Bundle fieldBundle = bundle.getBundle(FIELD_OVERALL_RATING);
if (fieldBundle != null) { if (fieldBundle != null) {
builder.setOverallRating(Rating.CREATOR.fromBundle(fieldBundle)); builder.setOverallRating(Rating.CREATOR.fromBundle(fieldBundle));
} }
} }
if (bundle.containsKey(keyForField(FIELD_TRACK_NUMBER))) { if (bundle.containsKey(FIELD_TRACK_NUMBER)) {
builder.setTrackNumber(bundle.getInt(keyForField(FIELD_TRACK_NUMBER))); builder.setTrackNumber(bundle.getInt(FIELD_TRACK_NUMBER));
} }
if (bundle.containsKey(keyForField(FIELD_TOTAL_TRACK_COUNT))) { if (bundle.containsKey(FIELD_TOTAL_TRACK_COUNT)) {
builder.setTotalTrackCount(bundle.getInt(keyForField(FIELD_TOTAL_TRACK_COUNT))); builder.setTotalTrackCount(bundle.getInt(FIELD_TOTAL_TRACK_COUNT));
} }
if (bundle.containsKey(keyForField(FIELD_FOLDER_TYPE))) { if (bundle.containsKey(FIELD_FOLDER_TYPE)) {
builder.setFolderType(bundle.getInt(keyForField(FIELD_FOLDER_TYPE))); builder.setFolderType(bundle.getInt(FIELD_FOLDER_TYPE));
} }
if (bundle.containsKey(keyForField(FIELD_IS_PLAYABLE))) { if (bundle.containsKey(FIELD_IS_BROWSABLE)) {
builder.setIsPlayable(bundle.getBoolean(keyForField(FIELD_IS_PLAYABLE))); builder.setIsBrowsable(bundle.getBoolean(FIELD_IS_BROWSABLE));
} }
if (bundle.containsKey(keyForField(FIELD_RECORDING_YEAR))) { if (bundle.containsKey(FIELD_IS_PLAYABLE)) {
builder.setRecordingYear(bundle.getInt(keyForField(FIELD_RECORDING_YEAR))); builder.setIsPlayable(bundle.getBoolean(FIELD_IS_PLAYABLE));
} }
if (bundle.containsKey(keyForField(FIELD_RECORDING_MONTH))) { if (bundle.containsKey(FIELD_RECORDING_YEAR)) {
builder.setRecordingMonth(bundle.getInt(keyForField(FIELD_RECORDING_MONTH))); builder.setRecordingYear(bundle.getInt(FIELD_RECORDING_YEAR));
} }
if (bundle.containsKey(keyForField(FIELD_RECORDING_DAY))) { if (bundle.containsKey(FIELD_RECORDING_MONTH)) {
builder.setRecordingDay(bundle.getInt(keyForField(FIELD_RECORDING_DAY))); builder.setRecordingMonth(bundle.getInt(FIELD_RECORDING_MONTH));
} }
if (bundle.containsKey(keyForField(FIELD_RELEASE_YEAR))) { if (bundle.containsKey(FIELD_RECORDING_DAY)) {
builder.setReleaseYear(bundle.getInt(keyForField(FIELD_RELEASE_YEAR))); builder.setRecordingDay(bundle.getInt(FIELD_RECORDING_DAY));
} }
if (bundle.containsKey(keyForField(FIELD_RELEASE_MONTH))) { if (bundle.containsKey(FIELD_RELEASE_YEAR)) {
builder.setReleaseMonth(bundle.getInt(keyForField(FIELD_RELEASE_MONTH))); builder.setReleaseYear(bundle.getInt(FIELD_RELEASE_YEAR));
} }
if (bundle.containsKey(keyForField(FIELD_RELEASE_DAY))) { if (bundle.containsKey(FIELD_RELEASE_MONTH)) {
builder.setReleaseDay(bundle.getInt(keyForField(FIELD_RELEASE_DAY))); builder.setReleaseMonth(bundle.getInt(FIELD_RELEASE_MONTH));
} }
if (bundle.containsKey(keyForField(FIELD_DISC_NUMBER))) { if (bundle.containsKey(FIELD_RELEASE_DAY)) {
builder.setDiscNumber(bundle.getInt(keyForField(FIELD_DISC_NUMBER))); builder.setReleaseDay(bundle.getInt(FIELD_RELEASE_DAY));
} }
if (bundle.containsKey(keyForField(FIELD_TOTAL_DISC_COUNT))) { if (bundle.containsKey(FIELD_DISC_NUMBER)) {
builder.setTotalDiscCount(bundle.getInt(keyForField(FIELD_TOTAL_DISC_COUNT))); builder.setDiscNumber(bundle.getInt(FIELD_DISC_NUMBER));
}
if (bundle.containsKey(FIELD_TOTAL_DISC_COUNT)) {
builder.setTotalDiscCount(bundle.getInt(FIELD_TOTAL_DISC_COUNT));
}
if (bundle.containsKey(FIELD_MEDIA_TYPE)) {
builder.setMediaType(bundle.getInt(FIELD_MEDIA_TYPE));
} }
return builder.build(); return builder.build();
} }
private static String keyForField(@FieldNumber int field) { private static @FolderType int getFolderTypeFromMediaType(@MediaType int mediaType) {
return Integer.toString(field, Character.MAX_RADIX); switch (mediaType) {
case MEDIA_TYPE_ALBUM:
case MEDIA_TYPE_ARTIST:
case MEDIA_TYPE_AUDIO_BOOK:
case MEDIA_TYPE_AUDIO_BOOK_CHAPTER:
case MEDIA_TYPE_FOLDER_MOVIES:
case MEDIA_TYPE_FOLDER_NEWS:
case MEDIA_TYPE_FOLDER_RADIO_STATIONS:
case MEDIA_TYPE_FOLDER_TRAILERS:
case MEDIA_TYPE_FOLDER_VIDEOS:
case MEDIA_TYPE_GENRE:
case MEDIA_TYPE_MOVIE:
case MEDIA_TYPE_MUSIC:
case MEDIA_TYPE_NEWS:
case MEDIA_TYPE_PLAYLIST:
case MEDIA_TYPE_PODCAST:
case MEDIA_TYPE_PODCAST_EPISODE:
case MEDIA_TYPE_RADIO_STATION:
case MEDIA_TYPE_TRAILER:
case MEDIA_TYPE_TV_CHANNEL:
case MEDIA_TYPE_TV_SEASON:
case MEDIA_TYPE_TV_SERIES:
case MEDIA_TYPE_TV_SHOW:
case MEDIA_TYPE_VIDEO:
case MEDIA_TYPE_YEAR:
return FOLDER_TYPE_TITLES;
case MEDIA_TYPE_FOLDER_ALBUMS:
return FOLDER_TYPE_ALBUMS;
case MEDIA_TYPE_FOLDER_ARTISTS:
return FOLDER_TYPE_ARTISTS;
case MEDIA_TYPE_FOLDER_GENRES:
return FOLDER_TYPE_GENRES;
case MEDIA_TYPE_FOLDER_PLAYLISTS:
return FOLDER_TYPE_PLAYLISTS;
case MEDIA_TYPE_FOLDER_YEARS:
return FOLDER_TYPE_YEARS;
case MEDIA_TYPE_FOLDER_AUDIO_BOOKS:
case MEDIA_TYPE_FOLDER_MIXED:
case MEDIA_TYPE_FOLDER_TV_CHANNELS:
case MEDIA_TYPE_FOLDER_TV_SERIES:
case MEDIA_TYPE_FOLDER_TV_SHOWS:
case MEDIA_TYPE_FOLDER_PODCASTS:
case MEDIA_TYPE_MIXED:
default:
return FOLDER_TYPE_MIXED;
}
}
private static @MediaType int getMediaTypeFromFolderType(@FolderType int folderType) {
switch (folderType) {
case FOLDER_TYPE_ALBUMS:
return MEDIA_TYPE_FOLDER_ALBUMS;
case FOLDER_TYPE_ARTISTS:
return MEDIA_TYPE_FOLDER_ARTISTS;
case FOLDER_TYPE_GENRES:
return MEDIA_TYPE_FOLDER_GENRES;
case FOLDER_TYPE_PLAYLISTS:
return MEDIA_TYPE_FOLDER_PLAYLISTS;
case FOLDER_TYPE_TITLES:
return MEDIA_TYPE_MIXED;
case FOLDER_TYPE_YEARS:
return MEDIA_TYPE_FOLDER_YEARS;
case FOLDER_TYPE_MIXED:
case FOLDER_TYPE_NONE:
default:
return MEDIA_TYPE_FOLDER_MIXED;
}
} }
} }

View File

@ -50,11 +50,8 @@ public final class Metadata implements Parcelable {
} }
/** /**
* Updates the {@link MediaMetadata.Builder} with the type specific values stored in this Entry. * Updates the {@link MediaMetadata.Builder} with the type-specific values stored in this {@code
* * 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.
* *
* @param builder The builder to be updated. * @param builder The builder to be updated.
*/ */

View File

@ -587,6 +587,8 @@ public final class MimeTypes {
return C.ENCODING_DTS_HD; return C.ENCODING_DTS_HD;
case MimeTypes.AUDIO_TRUEHD: case MimeTypes.AUDIO_TRUEHD:
return C.ENCODING_DOLBY_TRUEHD; return C.ENCODING_DOLBY_TRUEHD;
case MimeTypes.AUDIO_OPUS:
return C.ENCODING_OPUS;
default: default:
return C.ENCODING_INVALID; return C.ENCODING_INVALID;
} }

View File

@ -16,18 +16,13 @@
package androidx.media3.common; package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.FloatRange; import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects; 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. */ /** A rating expressed as a percentage. */
public final class PercentageRating extends Rating { 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; private static final @RatingType int TYPE = RATING_TYPE_PERCENTAGE;
@Documented private static final String FIELD_PERCENT = Util.intToStringMaxRadix(1);
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_RATING_TYPE, FIELD_PERCENT})
private @interface FieldNumber {}
private static final int FIELD_PERCENT = 1;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE); bundle.putInt(FIELD_RATING_TYPE, TYPE);
bundle.putFloat(keyForField(FIELD_PERCENT), percent); bundle.putFloat(FIELD_PERCENT, percent);
return bundle; return bundle;
} }
@ -100,14 +89,8 @@ public final class PercentageRating extends Rating {
@UnstableApi public static final Creator<PercentageRating> CREATOR = PercentageRating::fromBundle; @UnstableApi public static final Creator<PercentageRating> CREATOR = PercentageRating::fromBundle;
private static PercentageRating fromBundle(Bundle bundle) { private static PercentageRating fromBundle(Bundle bundle) {
checkArgument( checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE);
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET) float percent = bundle.getFloat(FIELD_PERCENT, /* defaultValue= */ RATING_UNSET);
== TYPE);
float percent = bundle.getFloat(keyForField(FIELD_PERCENT), /* defaultValue= */ RATING_UNSET);
return percent == RATING_UNSET ? new PercentageRating() : new PercentageRating(percent); return percent == RATING_UNSET ? new PercentageRating() : new PercentageRating(percent);
} }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -346,13 +346,12 @@ public class PlaybackException extends Exception implements Bundleable {
@UnstableApi @UnstableApi
protected PlaybackException(Bundle bundle) { protected PlaybackException(Bundle bundle) {
this( this(
/* message= */ bundle.getString(keyForField(FIELD_STRING_MESSAGE)), /* message= */ bundle.getString(FIELD_STRING_MESSAGE),
/* cause= */ getCauseFromBundle(bundle), /* cause= */ getCauseFromBundle(bundle),
/* errorCode= */ bundle.getInt( /* errorCode= */ bundle.getInt(
keyForField(FIELD_INT_ERROR_CODE), /* defaultValue= */ ERROR_CODE_UNSPECIFIED), FIELD_INT_ERROR_CODE, /* defaultValue= */ ERROR_CODE_UNSPECIFIED),
/* timestampMs= */ bundle.getLong( /* timestampMs= */ bundle.getLong(
keyForField(FIELD_LONG_TIMESTAMP_MS), FIELD_LONG_TIMESTAMP_MS, /* defaultValue= */ SystemClock.elapsedRealtime()));
/* defaultValue= */ SystemClock.elapsedRealtime()));
} }
/** Creates a new instance using the given values. */ /** Creates a new instance using the given values. */
@ -401,18 +400,18 @@ public class PlaybackException extends Exception implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
private static final int FIELD_INT_ERROR_CODE = 0; private static final String FIELD_INT_ERROR_CODE = Util.intToStringMaxRadix(0);
private static final int FIELD_LONG_TIMESTAMP_MS = 1; private static final String FIELD_LONG_TIMESTAMP_MS = Util.intToStringMaxRadix(1);
private static final int FIELD_STRING_MESSAGE = 2; private static final String FIELD_STRING_MESSAGE = Util.intToStringMaxRadix(2);
private static final int FIELD_STRING_CAUSE_CLASS_NAME = 3; private static final String FIELD_STRING_CAUSE_CLASS_NAME = Util.intToStringMaxRadix(3);
private static final int FIELD_STRING_CAUSE_MESSAGE = 4; 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()} * Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
* and {@link Bundleable.Creator}. * and {@link Bundleable.Creator}.
* *
* <p>Subclasses should obtain their {@link Bundle Bundle's} field keys by applying a non-negative * <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; @UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000;
@ -424,29 +423,17 @@ public class PlaybackException extends Exception implements Bundleable {
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_INT_ERROR_CODE), errorCode); bundle.putInt(FIELD_INT_ERROR_CODE, errorCode);
bundle.putLong(keyForField(FIELD_LONG_TIMESTAMP_MS), timestampMs); bundle.putLong(FIELD_LONG_TIMESTAMP_MS, timestampMs);
bundle.putString(keyForField(FIELD_STRING_MESSAGE), getMessage()); bundle.putString(FIELD_STRING_MESSAGE, getMessage());
@Nullable Throwable cause = getCause(); @Nullable Throwable cause = getCause();
if (cause != null) { if (cause != null) {
bundle.putString(keyForField(FIELD_STRING_CAUSE_CLASS_NAME), cause.getClass().getName()); bundle.putString(FIELD_STRING_CAUSE_CLASS_NAME, cause.getClass().getName());
bundle.putString(keyForField(FIELD_STRING_CAUSE_MESSAGE), cause.getMessage()); bundle.putString(FIELD_STRING_CAUSE_MESSAGE, cause.getMessage());
} }
return bundle; 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. // Creates a new {@link Throwable} with possibly {@code null} message.
@SuppressWarnings("nullness:argument") @SuppressWarnings("nullness:argument")
private static Throwable createThrowable(Class<?> clazz, @Nullable String message) private static Throwable createThrowable(Class<?> clazz, @Nullable String message)
@ -462,8 +449,8 @@ public class PlaybackException extends Exception implements Bundleable {
@Nullable @Nullable
private static Throwable getCauseFromBundle(Bundle bundle) { private static Throwable getCauseFromBundle(Bundle bundle) {
@Nullable String causeClassName = bundle.getString(keyForField(FIELD_STRING_CAUSE_CLASS_NAME)); @Nullable String causeClassName = bundle.getString(FIELD_STRING_CAUSE_CLASS_NAME);
@Nullable String causeMessage = bundle.getString(keyForField(FIELD_STRING_CAUSE_MESSAGE)); @Nullable String causeMessage = bundle.getString(FIELD_STRING_CAUSE_MESSAGE);
@Nullable Throwable cause = null; @Nullable Throwable cause = null;
if (!TextUtils.isEmpty(causeClassName)) { if (!TextUtils.isEmpty(causeClassName)) {
try { try {

View File

@ -15,20 +15,13 @@
*/ */
package androidx.media3.common; package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.FloatRange; import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; 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. */ /** Parameters that apply to playback, including speed setting. */
public final class PlaybackParameters implements Bundleable { public final class PlaybackParameters implements Bundleable {
@ -122,21 +115,15 @@ public final class PlaybackParameters implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_SPEED = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_PITCH = Util.intToStringMaxRadix(1);
@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;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putFloat(keyForField(FIELD_SPEED), speed); bundle.putFloat(FIELD_SPEED, speed);
bundle.putFloat(keyForField(FIELD_PITCH), pitch); bundle.putFloat(FIELD_PITCH, pitch);
return bundle; return bundle;
} }
@ -144,12 +131,8 @@ public final class PlaybackParameters implements Bundleable {
@UnstableApi @UnstableApi
public static final Creator<PlaybackParameters> CREATOR = public static final Creator<PlaybackParameters> CREATOR =
bundle -> { bundle -> {
float speed = bundle.getFloat(keyForField(FIELD_SPEED), /* defaultValue= */ 1f); float speed = bundle.getFloat(FIELD_SPEED, /* defaultValue= */ 1f);
float pitch = bundle.getFloat(keyForField(FIELD_PITCH), /* defaultValue= */ 1f); float pitch = bundle.getFloat(FIELD_PITCH, /* defaultValue= */ 1f);
return new PlaybackParameters(speed, pitch); return new PlaybackParameters(speed, pitch);
}; };
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -20,6 +20,7 @@ import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef; import androidx.annotation.IntDef;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; 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_STAR = 2;
/* package */ static final int RATING_TYPE_THUMB = 3; /* package */ static final int RATING_TYPE_THUMB = 3;
@Documented /* package */ static final String FIELD_RATING_TYPE = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_RATING_TYPE})
private @interface FieldNumber {}
/* package */ static final int FIELD_RATING_TYPE = 0;
/** Object that can restore a {@link Rating} from a {@link Bundle}. */ /** Object that can restore a {@link Rating} from a {@link Bundle}. */
@UnstableApi public static final Creator<Rating> CREATOR = Rating::fromBundle; @UnstableApi public static final Creator<Rating> CREATOR = Rating::fromBundle;
private static Rating fromBundle(Bundle bundle) { private static Rating fromBundle(Bundle bundle) {
@RatingType @RatingType
int ratingType = int ratingType = bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET);
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET);
switch (ratingType) { switch (ratingType) {
case RATING_TYPE_HEART: case RATING_TYPE_HEART:
return HeartRating.CREATOR.fromBundle(bundle); return HeartRating.CREATOR.fromBundle(bundle);
@ -89,8 +83,4 @@ public abstract class Rating implements Bundleable {
throw new IllegalArgumentException("Unknown RatingType: " + ratingType); throw new IllegalArgumentException("Unknown RatingType: " + ratingType);
} }
} }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -16,19 +16,14 @@
package androidx.media3.common; package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.FloatRange; import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects; 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. */ /** A rating expressed as a fractional number of stars. */
public final class StarRating extends Rating { 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 @RatingType int TYPE = RATING_TYPE_STAR;
private static final int MAX_STARS_DEFAULT = 5; private static final int MAX_STARS_DEFAULT = 5;
@Documented private static final String FIELD_MAX_STARS = Util.intToStringMaxRadix(1);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_STAR_RATING = Util.intToStringMaxRadix(2);
@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;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE); bundle.putInt(FIELD_RATING_TYPE, TYPE);
bundle.putInt(keyForField(FIELD_MAX_STARS), maxStars); bundle.putInt(FIELD_MAX_STARS, maxStars);
bundle.putFloat(keyForField(FIELD_STAR_RATING), starRating); bundle.putFloat(FIELD_STAR_RATING, starRating);
return bundle; return bundle;
} }
@ -129,19 +118,11 @@ public final class StarRating extends Rating {
@UnstableApi public static final Creator<StarRating> CREATOR = StarRating::fromBundle; @UnstableApi public static final Creator<StarRating> CREATOR = StarRating::fromBundle;
private static StarRating fromBundle(Bundle bundle) { private static StarRating fromBundle(Bundle bundle) {
checkArgument( checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE);
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET) int maxStars = bundle.getInt(FIELD_MAX_STARS, /* defaultValue= */ MAX_STARS_DEFAULT);
== TYPE); float starRating = bundle.getFloat(FIELD_STAR_RATING, /* defaultValue= */ RATING_UNSET);
int maxStars =
bundle.getInt(keyForField(FIELD_MAX_STARS), /* defaultValue= */ MAX_STARS_DEFAULT);
float starRating =
bundle.getFloat(keyForField(FIELD_STAR_RATING), /* defaultValue= */ RATING_UNSET);
return starRating == RATING_UNSET return starRating == RATING_UNSET
? new StarRating(maxStars) ? new StarRating(maxStars)
: new StarRating(maxStars, starRating); : new StarRating(maxStars, starRating);
} }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -16,17 +16,12 @@
package androidx.media3.common; package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects; 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". */ /** A rating expressed as "thumbs up" or "thumbs down". */
public final class ThumbRating extends Rating { 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; private static final @RatingType int TYPE = RATING_TYPE_THUMB;
@Documented private static final String FIELD_RATED = Util.intToStringMaxRadix(1);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_IS_THUMBS_UP = Util.intToStringMaxRadix(2);
@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;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_RATING_TYPE), TYPE); bundle.putInt(FIELD_RATING_TYPE, TYPE);
bundle.putBoolean(keyForField(FIELD_RATED), rated); bundle.putBoolean(FIELD_RATED, rated);
bundle.putBoolean(keyForField(FIELD_IS_THUMBS_UP), isThumbsUp); bundle.putBoolean(FIELD_IS_THUMBS_UP, isThumbsUp);
return bundle; return bundle;
} }
@ -101,17 +90,10 @@ public final class ThumbRating extends Rating {
@UnstableApi public static final Creator<ThumbRating> CREATOR = ThumbRating::fromBundle; @UnstableApi public static final Creator<ThumbRating> CREATOR = ThumbRating::fromBundle;
private static ThumbRating fromBundle(Bundle bundle) { private static ThumbRating fromBundle(Bundle bundle) {
checkArgument( checkArgument(bundle.getInt(FIELD_RATING_TYPE, /* defaultValue= */ RATING_TYPE_UNSET) == TYPE);
bundle.getInt(keyForField(FIELD_RATING_TYPE), /* defaultValue= */ RATING_TYPE_UNSET) boolean rated = bundle.getBoolean(FIELD_RATED, /* defaultValue= */ false);
== TYPE);
boolean rated = bundle.getBoolean(keyForField(FIELD_RATED), /* defaultValue= */ false);
return rated return rated
? new ThumbRating( ? new ThumbRating(bundle.getBoolean(FIELD_IS_THUMBS_UP, /* defaultValue= */ false))
bundle.getBoolean(keyForField(FIELD_IS_THUMBS_UP), /* defaultValue= */ false))
: new ThumbRating(); : new ThumbRating();
} }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -20,14 +20,12 @@ import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.BundleUtil; import androidx.media3.common.util.BundleUtil;
@ -36,10 +34,6 @@ import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe; import com.google.errorprone.annotations.InlineMe;
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.ArrayList;
import java.util.List; import java.util.List;
@ -158,7 +152,7 @@ public abstract class Timeline implements Bundleable {
private static final Object FAKE_WINDOW_UID = new Object(); private static final Object FAKE_WINDOW_UID = new Object();
private static final MediaItem EMPTY_MEDIA_ITEM = private static final MediaItem PLACEHOLDER_MEDIA_ITEM =
new MediaItem.Builder() new MediaItem.Builder()
.setMediaId("androidx.media3.common.Timeline") .setMediaId("androidx.media3.common.Timeline")
.setUri(Uri.EMPTY) .setUri(Uri.EMPTY)
@ -258,7 +252,7 @@ public abstract class Timeline implements Bundleable {
/** Creates window. */ /** Creates window. */
public Window() { public Window() {
uid = SINGLE_WINDOW_UID; uid = SINGLE_WINDOW_UID;
mediaItem = EMPTY_MEDIA_ITEM; mediaItem = PLACEHOLDER_MEDIA_ITEM;
} }
/** Sets the data held by this window. */ /** Sets the data held by this window. */
@ -281,7 +275,7 @@ public abstract class Timeline implements Bundleable {
int lastPeriodIndex, int lastPeriodIndex,
long positionInFirstPeriodUs) { long positionInFirstPeriodUs) {
this.uid = uid; this.uid = uid;
this.mediaItem = mediaItem != null ? mediaItem : EMPTY_MEDIA_ITEM; this.mediaItem = mediaItem != null ? mediaItem : PLACEHOLDER_MEDIA_ITEM;
this.tag = this.tag =
mediaItem != null && mediaItem.localConfiguration != null mediaItem != null && mediaItem.localConfiguration != null
? mediaItem.localConfiguration.tag ? mediaItem.localConfiguration.tag
@ -420,63 +414,20 @@ public abstract class Timeline implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_MEDIA_ITEM = Util.intToStringMaxRadix(1);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_PRESENTATION_START_TIME_MS = Util.intToStringMaxRadix(2);
@Target(TYPE_USE) private static final String FIELD_WINDOW_START_TIME_MS = Util.intToStringMaxRadix(3);
@IntDef({ private static final String FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS =
FIELD_MEDIA_ITEM, Util.intToStringMaxRadix(4);
FIELD_PRESENTATION_START_TIME_MS, private static final String FIELD_IS_SEEKABLE = Util.intToStringMaxRadix(5);
FIELD_WINDOW_START_TIME_MS, private static final String FIELD_IS_DYNAMIC = Util.intToStringMaxRadix(6);
FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS, private static final String FIELD_LIVE_CONFIGURATION = Util.intToStringMaxRadix(7);
FIELD_IS_SEEKABLE, private static final String FIELD_IS_PLACEHOLDER = Util.intToStringMaxRadix(8);
FIELD_IS_DYNAMIC, private static final String FIELD_DEFAULT_POSITION_US = Util.intToStringMaxRadix(9);
FIELD_LIVE_CONFIGURATION, private static final String FIELD_DURATION_US = Util.intToStringMaxRadix(10);
FIELD_IS_PLACEHOLDER, private static final String FIELD_FIRST_PERIOD_INDEX = Util.intToStringMaxRadix(11);
FIELD_DEFAULT_POSITION_US, private static final String FIELD_LAST_PERIOD_INDEX = Util.intToStringMaxRadix(12);
FIELD_DURATION_US, private static final String FIELD_POSITION_IN_FIRST_PERIOD_US = Util.intToStringMaxRadix(13);
FIELD_FIRST_PERIOD_INDEX,
FIELD_LAST_PERIOD_INDEX,
FIELD_POSITION_IN_FIRST_PERIOD_US,
})
private @interface FieldNumber {}
private static final int FIELD_MEDIA_ITEM = 1;
private static final int FIELD_PRESENTATION_START_TIME_MS = 2;
private static final int FIELD_WINDOW_START_TIME_MS = 3;
private static final int FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS = 4;
private static final int FIELD_IS_SEEKABLE = 5;
private static final int FIELD_IS_DYNAMIC = 6;
private static final int FIELD_LIVE_CONFIGURATION = 7;
private static final int FIELD_IS_PLACEHOLDER = 8;
private static final int FIELD_DEFAULT_POSITION_US = 9;
private static final int FIELD_DURATION_US = 10;
private static final int FIELD_FIRST_PERIOD_INDEX = 11;
private static final int FIELD_LAST_PERIOD_INDEX = 12;
private static final int FIELD_POSITION_IN_FIRST_PERIOD_US = 13;
private final Bundle toBundle(boolean excludeMediaItem) {
Bundle bundle = new Bundle();
bundle.putBundle(
keyForField(FIELD_MEDIA_ITEM),
excludeMediaItem ? MediaItem.EMPTY.toBundle() : mediaItem.toBundle());
bundle.putLong(keyForField(FIELD_PRESENTATION_START_TIME_MS), presentationStartTimeMs);
bundle.putLong(keyForField(FIELD_WINDOW_START_TIME_MS), windowStartTimeMs);
bundle.putLong(
keyForField(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS), elapsedRealtimeEpochOffsetMs);
bundle.putBoolean(keyForField(FIELD_IS_SEEKABLE), isSeekable);
bundle.putBoolean(keyForField(FIELD_IS_DYNAMIC), isDynamic);
@Nullable MediaItem.LiveConfiguration liveConfiguration = this.liveConfiguration;
if (liveConfiguration != null) {
bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle());
}
bundle.putBoolean(keyForField(FIELD_IS_PLACEHOLDER), isPlaceholder);
bundle.putLong(keyForField(FIELD_DEFAULT_POSITION_US), defaultPositionUs);
bundle.putLong(keyForField(FIELD_DURATION_US), durationUs);
bundle.putInt(keyForField(FIELD_FIRST_PERIOD_INDEX), firstPeriodIndex);
bundle.putInt(keyForField(FIELD_LAST_PERIOD_INDEX), lastPeriodIndex);
bundle.putLong(keyForField(FIELD_POSITION_IN_FIRST_PERIOD_US), positionInFirstPeriodUs);
return bundle;
}
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -485,11 +436,52 @@ public abstract class Timeline implements Bundleable {
* restored by {@link #CREATOR} will be a fake {@link Object} and the {@link #manifest} of the * restored by {@link #CREATOR} will be a fake {@link Object} and the {@link #manifest} of the
* instance will be {@code null}. * instance will be {@code null}.
*/ */
// TODO(b/166765820): See if missing fields would be okay and add them to the Bundle otherwise.
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
return toBundle(/* excludeMediaItem= */ false); Bundle bundle = new Bundle();
if (!MediaItem.EMPTY.equals(mediaItem)) {
bundle.putBundle(FIELD_MEDIA_ITEM, mediaItem.toBundle());
}
if (presentationStartTimeMs != C.TIME_UNSET) {
bundle.putLong(FIELD_PRESENTATION_START_TIME_MS, presentationStartTimeMs);
}
if (windowStartTimeMs != C.TIME_UNSET) {
bundle.putLong(FIELD_WINDOW_START_TIME_MS, windowStartTimeMs);
}
if (elapsedRealtimeEpochOffsetMs != C.TIME_UNSET) {
bundle.putLong(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS, elapsedRealtimeEpochOffsetMs);
}
if (isSeekable) {
bundle.putBoolean(FIELD_IS_SEEKABLE, isSeekable);
}
if (isDynamic) {
bundle.putBoolean(FIELD_IS_DYNAMIC, isDynamic);
}
@Nullable MediaItem.LiveConfiguration liveConfiguration = this.liveConfiguration;
if (liveConfiguration != null) {
bundle.putBundle(FIELD_LIVE_CONFIGURATION, liveConfiguration.toBundle());
}
if (isPlaceholder) {
bundle.putBoolean(FIELD_IS_PLACEHOLDER, isPlaceholder);
}
if (defaultPositionUs != 0) {
bundle.putLong(FIELD_DEFAULT_POSITION_US, defaultPositionUs);
}
if (durationUs != C.TIME_UNSET) {
bundle.putLong(FIELD_DURATION_US, durationUs);
}
if (firstPeriodIndex != 0) {
bundle.putInt(FIELD_FIRST_PERIOD_INDEX, firstPeriodIndex);
}
if (lastPeriodIndex != 0) {
bundle.putInt(FIELD_LAST_PERIOD_INDEX, lastPeriodIndex);
}
if (positionInFirstPeriodUs != 0) {
bundle.putLong(FIELD_POSITION_IN_FIRST_PERIOD_US, positionInFirstPeriodUs);
}
return bundle;
} }
/** /**
@ -501,42 +493,31 @@ public abstract class Timeline implements Bundleable {
@UnstableApi public static final Creator<Window> CREATOR = Window::fromBundle; @UnstableApi public static final Creator<Window> CREATOR = Window::fromBundle;
private static Window fromBundle(Bundle bundle) { private static Window fromBundle(Bundle bundle) {
@Nullable Bundle mediaItemBundle = bundle.getBundle(keyForField(FIELD_MEDIA_ITEM)); @Nullable Bundle mediaItemBundle = bundle.getBundle(FIELD_MEDIA_ITEM);
@Nullable @Nullable
MediaItem mediaItem = MediaItem mediaItem =
mediaItemBundle != null ? MediaItem.CREATOR.fromBundle(mediaItemBundle) : null; mediaItemBundle != null ? MediaItem.CREATOR.fromBundle(mediaItemBundle) : MediaItem.EMPTY;
long presentationStartTimeMs = long presentationStartTimeMs =
bundle.getLong( bundle.getLong(FIELD_PRESENTATION_START_TIME_MS, /* defaultValue= */ C.TIME_UNSET);
keyForField(FIELD_PRESENTATION_START_TIME_MS), /* defaultValue= */ C.TIME_UNSET);
long windowStartTimeMs = long windowStartTimeMs =
bundle.getLong(keyForField(FIELD_WINDOW_START_TIME_MS), /* defaultValue= */ C.TIME_UNSET); bundle.getLong(FIELD_WINDOW_START_TIME_MS, /* defaultValue= */ C.TIME_UNSET);
long elapsedRealtimeEpochOffsetMs = long elapsedRealtimeEpochOffsetMs =
bundle.getLong( bundle.getLong(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS, /* defaultValue= */ C.TIME_UNSET);
keyForField(FIELD_ELAPSED_REALTIME_EPOCH_OFFSET_MS), boolean isSeekable = bundle.getBoolean(FIELD_IS_SEEKABLE, /* defaultValue= */ false);
/* defaultValue= */ C.TIME_UNSET); boolean isDynamic = bundle.getBoolean(FIELD_IS_DYNAMIC, /* defaultValue= */ false);
boolean isSeekable = @Nullable Bundle liveConfigurationBundle = bundle.getBundle(FIELD_LIVE_CONFIGURATION);
bundle.getBoolean(keyForField(FIELD_IS_SEEKABLE), /* defaultValue= */ false);
boolean isDynamic =
bundle.getBoolean(keyForField(FIELD_IS_DYNAMIC), /* defaultValue= */ false);
@Nullable
Bundle liveConfigurationBundle = bundle.getBundle(keyForField(FIELD_LIVE_CONFIGURATION));
@Nullable @Nullable
MediaItem.LiveConfiguration liveConfiguration = MediaItem.LiveConfiguration liveConfiguration =
liveConfigurationBundle != null liveConfigurationBundle != null
? MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle) ? MediaItem.LiveConfiguration.CREATOR.fromBundle(liveConfigurationBundle)
: null; : null;
boolean isPlaceHolder = boolean isPlaceHolder = bundle.getBoolean(FIELD_IS_PLACEHOLDER, /* defaultValue= */ false);
bundle.getBoolean(keyForField(FIELD_IS_PLACEHOLDER), /* defaultValue= */ false); long defaultPositionUs = bundle.getLong(FIELD_DEFAULT_POSITION_US, /* defaultValue= */ 0);
long defaultPositionUs = long durationUs = bundle.getLong(FIELD_DURATION_US, /* defaultValue= */ C.TIME_UNSET);
bundle.getLong(keyForField(FIELD_DEFAULT_POSITION_US), /* defaultValue= */ 0); int firstPeriodIndex = bundle.getInt(FIELD_FIRST_PERIOD_INDEX, /* defaultValue= */ 0);
long durationUs = int lastPeriodIndex = bundle.getInt(FIELD_LAST_PERIOD_INDEX, /* defaultValue= */ 0);
bundle.getLong(keyForField(FIELD_DURATION_US), /* defaultValue= */ C.TIME_UNSET);
int firstPeriodIndex =
bundle.getInt(keyForField(FIELD_FIRST_PERIOD_INDEX), /* defaultValue= */ 0);
int lastPeriodIndex =
bundle.getInt(keyForField(FIELD_LAST_PERIOD_INDEX), /* defaultValue= */ 0);
long positionInFirstPeriodUs = long positionInFirstPeriodUs =
bundle.getLong(keyForField(FIELD_POSITION_IN_FIRST_PERIOD_US), /* defaultValue= */ 0); bundle.getLong(FIELD_POSITION_IN_FIRST_PERIOD_US, /* defaultValue= */ 0);
Window window = new Window(); Window window = new Window();
window.set( window.set(
@ -557,10 +538,6 @@ public abstract class Timeline implements Bundleable {
window.isPlaceholder = isPlaceHolder; window.isPlaceholder = isPlaceHolder;
return window; return window;
} }
private static String keyForField(@Window.FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }
/** /**
@ -912,23 +889,11 @@ public abstract class Timeline implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_WINDOW_INDEX = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_DURATION_US = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_POSITION_IN_WINDOW_US = Util.intToStringMaxRadix(2);
@IntDef({ private static final String FIELD_PLACEHOLDER = Util.intToStringMaxRadix(3);
FIELD_WINDOW_INDEX, private static final String FIELD_AD_PLAYBACK_STATE = Util.intToStringMaxRadix(4);
FIELD_DURATION_US,
FIELD_POSITION_IN_WINDOW_US,
FIELD_PLACEHOLDER,
FIELD_AD_PLAYBACK_STATE
})
private @interface FieldNumber {}
private static final int FIELD_WINDOW_INDEX = 0;
private static final int FIELD_DURATION_US = 1;
private static final int FIELD_POSITION_IN_WINDOW_US = 2;
private static final int FIELD_PLACEHOLDER = 3;
private static final int FIELD_AD_PLAYBACK_STATE = 4;
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -936,16 +901,25 @@ public abstract class Timeline implements Bundleable {
* <p>It omits the {@link #id} and {@link #uid} fields so these fields of an instance restored * <p>It omits the {@link #id} and {@link #uid} fields so these fields of an instance restored
* by {@link #CREATOR} will always be {@code null}. * by {@link #CREATOR} will always be {@code null}.
*/ */
// TODO(b/166765820): See if missing fields would be okay and add them to the Bundle otherwise.
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_WINDOW_INDEX), windowIndex); if (windowIndex != 0) {
bundle.putLong(keyForField(FIELD_DURATION_US), durationUs); bundle.putInt(FIELD_WINDOW_INDEX, windowIndex);
bundle.putLong(keyForField(FIELD_POSITION_IN_WINDOW_US), positionInWindowUs); }
bundle.putBoolean(keyForField(FIELD_PLACEHOLDER), isPlaceholder); if (durationUs != C.TIME_UNSET) {
bundle.putBundle(keyForField(FIELD_AD_PLAYBACK_STATE), adPlaybackState.toBundle()); bundle.putLong(FIELD_DURATION_US, durationUs);
}
if (positionInWindowUs != 0) {
bundle.putLong(FIELD_POSITION_IN_WINDOW_US, positionInWindowUs);
}
if (isPlaceholder) {
bundle.putBoolean(FIELD_PLACEHOLDER, isPlaceholder);
}
if (!adPlaybackState.equals(AdPlaybackState.NONE)) {
bundle.putBundle(FIELD_AD_PLAYBACK_STATE, adPlaybackState.toBundle());
}
return bundle; return bundle;
} }
@ -957,14 +931,11 @@ public abstract class Timeline implements Bundleable {
@UnstableApi public static final Creator<Period> CREATOR = Period::fromBundle; @UnstableApi public static final Creator<Period> CREATOR = Period::fromBundle;
private static Period fromBundle(Bundle bundle) { private static Period fromBundle(Bundle bundle) {
int windowIndex = bundle.getInt(keyForField(FIELD_WINDOW_INDEX), /* defaultValue= */ 0); int windowIndex = bundle.getInt(FIELD_WINDOW_INDEX, /* defaultValue= */ 0);
long durationUs = long durationUs = bundle.getLong(FIELD_DURATION_US, /* defaultValue= */ C.TIME_UNSET);
bundle.getLong(keyForField(FIELD_DURATION_US), /* defaultValue= */ C.TIME_UNSET); long positionInWindowUs = bundle.getLong(FIELD_POSITION_IN_WINDOW_US, /* defaultValue= */ 0);
long positionInWindowUs = boolean isPlaceholder = bundle.getBoolean(FIELD_PLACEHOLDER, /* defaultValue= */ false);
bundle.getLong(keyForField(FIELD_POSITION_IN_WINDOW_US), /* defaultValue= */ 0); @Nullable Bundle adPlaybackStateBundle = bundle.getBundle(FIELD_AD_PLAYBACK_STATE);
boolean isPlaceholder = bundle.getBoolean(keyForField(FIELD_PLACEHOLDER));
@Nullable
Bundle adPlaybackStateBundle = bundle.getBundle(keyForField(FIELD_AD_PLAYBACK_STATE));
AdPlaybackState adPlaybackState = AdPlaybackState adPlaybackState =
adPlaybackStateBundle != null adPlaybackStateBundle != null
? AdPlaybackState.CREATOR.fromBundle(adPlaybackStateBundle) ? AdPlaybackState.CREATOR.fromBundle(adPlaybackStateBundle)
@ -981,10 +952,6 @@ public abstract class Timeline implements Bundleable {
isPlaceholder); isPlaceholder);
return period; return period;
} }
private static String keyForField(@Period.FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }
/** An empty timeline. */ /** An empty timeline. */
@ -1404,19 +1371,9 @@ public abstract class Timeline implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_WINDOWS = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_PERIODS = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_SHUFFLED_WINDOW_INDICES = Util.intToStringMaxRadix(2);
@IntDef({
FIELD_WINDOWS,
FIELD_PERIODS,
FIELD_SHUFFLED_WINDOW_INDICES,
})
private @interface FieldNumber {}
private static final int FIELD_WINDOWS = 0;
private static final int FIELD_PERIODS = 1;
private static final int FIELD_SHUFFLED_WINDOW_INDICES = 2;
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -1424,18 +1381,15 @@ public abstract class Timeline implements Bundleable {
* <p>The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of * <p>The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of
* an instance restored by {@link #CREATOR} may have missing fields as described in {@link * an instance restored by {@link #CREATOR} may have missing fields as described in {@link
* Window#toBundle()} and {@link Period#toBundle()}. * Window#toBundle()} and {@link Period#toBundle()}.
*
* @param excludeMediaItems Whether to exclude all {@link Window#mediaItem media items} of windows
* in the timeline.
*/ */
@UnstableApi @UnstableApi
public final Bundle toBundle(boolean excludeMediaItems) { @Override
public final Bundle toBundle() {
List<Bundle> windowBundles = new ArrayList<>(); List<Bundle> windowBundles = new ArrayList<>();
int windowCount = getWindowCount(); int windowCount = getWindowCount();
Window window = new Window(); Window window = new Window();
for (int i = 0; i < windowCount; i++) { for (int i = 0; i < windowCount; i++) {
windowBundles.add( windowBundles.add(getWindow(i, window, /* defaultPositionProjectionUs= */ 0).toBundle());
getWindow(i, window, /* defaultPositionProjectionUs= */ 0).toBundle(excludeMediaItems));
} }
List<Bundle> periodBundles = new ArrayList<>(); List<Bundle> periodBundles = new ArrayList<>();
@ -1456,25 +1410,43 @@ public abstract class Timeline implements Bundleable {
} }
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
BundleUtil.putBinder( BundleUtil.putBinder(bundle, FIELD_WINDOWS, new BundleListRetriever(windowBundles));
bundle, keyForField(FIELD_WINDOWS), new BundleListRetriever(windowBundles)); BundleUtil.putBinder(bundle, FIELD_PERIODS, new BundleListRetriever(periodBundles));
BundleUtil.putBinder( bundle.putIntArray(FIELD_SHUFFLED_WINDOW_INDICES, shuffledWindowIndices);
bundle, keyForField(FIELD_PERIODS), new BundleListRetriever(periodBundles));
bundle.putIntArray(keyForField(FIELD_SHUFFLED_WINDOW_INDICES), shuffledWindowIndices);
return bundle; return bundle;
} }
/** /**
* {@inheritDoc} * Returns a {@link Bundle} containing just the specified {@link Window}.
* *
* <p>The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of * <p>The {@link #getWindow(int, Window)} windows} and {@link #getPeriod(int, Period) periods} of
* an instance restored by {@link #CREATOR} may have missing fields as described in {@link * an instance restored by {@link #CREATOR} may have missing fields as described in {@link
* Window#toBundle()} and {@link Period#toBundle()}. * Window#toBundle()} and {@link Period#toBundle()}.
*
* @param windowIndex The index of the {@link Window} to include in the {@link Bundle}.
*/ */
@UnstableApi @UnstableApi
@Override public final Bundle toBundleWithOneWindowOnly(int windowIndex) {
public final Bundle toBundle() { Window window = getWindow(windowIndex, new Window(), /* defaultPositionProjectionUs= */ 0);
return toBundle(/* excludeMediaItems= */ false);
List<Bundle> periodBundles = new ArrayList<>();
Period period = new Period();
for (int i = window.firstPeriodIndex; i <= window.lastPeriodIndex; i++) {
getPeriod(i, period, /* setIds= */ false);
period.windowIndex = 0;
periodBundles.add(period.toBundle());
}
window.lastPeriodIndex = window.lastPeriodIndex - window.firstPeriodIndex;
window.firstPeriodIndex = 0;
Bundle windowBundle = window.toBundle();
Bundle bundle = new Bundle();
BundleUtil.putBinder(
bundle, FIELD_WINDOWS, new BundleListRetriever(ImmutableList.of(windowBundle)));
BundleUtil.putBinder(bundle, FIELD_PERIODS, new BundleListRetriever(periodBundles));
bundle.putIntArray(FIELD_SHUFFLED_WINDOW_INDICES, new int[] {0});
return bundle;
} }
/** /**
@ -1488,13 +1460,10 @@ public abstract class Timeline implements Bundleable {
private static Timeline fromBundle(Bundle bundle) { private static Timeline fromBundle(Bundle bundle) {
ImmutableList<Window> windows = ImmutableList<Window> windows =
fromBundleListRetriever( fromBundleListRetriever(Window.CREATOR, BundleUtil.getBinder(bundle, FIELD_WINDOWS));
Window.CREATOR, BundleUtil.getBinder(bundle, keyForField(FIELD_WINDOWS)));
ImmutableList<Period> periods = ImmutableList<Period> periods =
fromBundleListRetriever( fromBundleListRetriever(Period.CREATOR, BundleUtil.getBinder(bundle, FIELD_PERIODS));
Period.CREATOR, BundleUtil.getBinder(bundle, keyForField(FIELD_PERIODS))); @Nullable int[] shuffledWindowIndices = bundle.getIntArray(FIELD_SHUFFLED_WINDOW_INDICES);
@Nullable
int[] shuffledWindowIndices = bundle.getIntArray(keyForField(FIELD_SHUFFLED_WINDOW_INDICES));
return new RemotableTimeline( return new RemotableTimeline(
windows, windows,
periods, periods,
@ -1516,10 +1485,6 @@ public abstract class Timeline implements Bundleable {
return builder.build(); return builder.build();
} }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
private static int[] generateUnshuffledIndices(int n) { private static int[] generateUnshuffledIndices(int n) {
int[] indices = new int[n]; int[] indices = new int[n];
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {

View File

@ -16,20 +16,15 @@
package androidx.media3.common; package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; 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.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -165,15 +160,8 @@ public final class TrackGroup implements Bundleable {
} }
// Bundleable implementation. // Bundleable implementation.
private static final String FIELD_FORMATS = Util.intToStringMaxRadix(0);
@Documented private static final String FIELD_ID = Util.intToStringMaxRadix(1);
@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;
@UnstableApi @UnstableApi
@Override @Override
@ -183,8 +171,8 @@ public final class TrackGroup implements Bundleable {
for (Format format : formats) { for (Format format : formats) {
arrayList.add(format.toBundle(/* excludeMetadata= */ true)); arrayList.add(format.toBundle(/* excludeMetadata= */ true));
} }
bundle.putParcelableArrayList(keyForField(FIELD_FORMATS), arrayList); bundle.putParcelableArrayList(FIELD_FORMATS, arrayList);
bundle.putString(keyForField(FIELD_ID), id); bundle.putString(FIELD_ID, id);
return bundle; return bundle;
} }
@ -192,20 +180,15 @@ public final class TrackGroup implements Bundleable {
@UnstableApi @UnstableApi
public static final Creator<TrackGroup> CREATOR = public static final Creator<TrackGroup> CREATOR =
bundle -> { bundle -> {
@Nullable @Nullable List<Bundle> formatBundles = bundle.getParcelableArrayList(FIELD_FORMATS);
List<Bundle> formatBundles = bundle.getParcelableArrayList(keyForField(FIELD_FORMATS));
List<Format> formats = List<Format> formats =
formatBundles == null formatBundles == null
? ImmutableList.of() ? ImmutableList.of()
: BundleableUtil.fromBundleList(Format.CREATOR, formatBundles); : 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])); 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() { private void verifyCorrectness() {
// TrackGroups should only contain tracks with exactly the same content (but in different // 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 // qualities). We only log an error instead of throwing to not break backwards-compatibility for

View File

@ -20,14 +20,11 @@ import static java.util.Collections.max;
import static java.util.Collections.min; import static java.util.Collections.min;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints; 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; 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. */ /** The indices of tracks in a {@link TrackGroup} to be selected. */
public final ImmutableList<Integer> trackIndices; public final ImmutableList<Integer> trackIndices;
@Documented private static final String FIELD_TRACK_GROUP = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_TRACKS = Util.intToStringMaxRadix(1);
@IntDef({
FIELD_TRACK_GROUP,
FIELD_TRACKS,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUP = 0;
private static final int FIELD_TRACKS = 1;
/** /**
* Constructs an instance to force {@code trackIndex} in {@code trackGroup} to be selected. * Constructs an instance to force {@code trackIndex} in {@code trackGroup} to be selected.
@ -119,8 +108,8 @@ public final class TrackSelectionOverride implements Bundleable {
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle()); bundle.putBundle(FIELD_TRACK_GROUP, mediaTrackGroup.toBundle());
bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndices)); bundle.putIntArray(FIELD_TRACKS, Ints.toArray(trackIndices));
return bundle; return bundle;
} }
@ -128,13 +117,9 @@ public final class TrackSelectionOverride implements Bundleable {
@UnstableApi @UnstableApi
public static final Creator<TrackSelectionOverride> CREATOR = public static final Creator<TrackSelectionOverride> CREATOR =
bundle -> { bundle -> {
Bundle trackGroupBundle = checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP))); Bundle trackGroupBundle = checkNotNull(bundle.getBundle(FIELD_TRACK_GROUP));
TrackGroup mediaTrackGroup = TrackGroup.CREATOR.fromBundle(trackGroupBundle); 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)); return new TrackSelectionOverride(mediaTrackGroup, Ints.asList(tracks));
}; };
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -158,95 +158,71 @@ public class TrackSelectionParameters implements Bundleable {
@UnstableApi @UnstableApi
protected Builder(Bundle bundle) { protected Builder(Bundle bundle) {
// Video // Video
maxVideoWidth = maxVideoWidth = bundle.getInt(FIELD_MAX_VIDEO_WIDTH, DEFAULT_WITHOUT_CONTEXT.maxVideoWidth);
bundle.getInt(keyForField(FIELD_MAX_VIDEO_WIDTH), DEFAULT_WITHOUT_CONTEXT.maxVideoWidth);
maxVideoHeight = maxVideoHeight =
bundle.getInt( bundle.getInt(FIELD_MAX_VIDEO_HEIGHT, DEFAULT_WITHOUT_CONTEXT.maxVideoHeight);
keyForField(FIELD_MAX_VIDEO_HEIGHT), DEFAULT_WITHOUT_CONTEXT.maxVideoHeight);
maxVideoFrameRate = maxVideoFrameRate =
bundle.getInt( bundle.getInt(FIELD_MAX_VIDEO_FRAMERATE, DEFAULT_WITHOUT_CONTEXT.maxVideoFrameRate);
keyForField(FIELD_MAX_VIDEO_FRAMERATE), DEFAULT_WITHOUT_CONTEXT.maxVideoFrameRate);
maxVideoBitrate = maxVideoBitrate =
bundle.getInt( bundle.getInt(FIELD_MAX_VIDEO_BITRATE, DEFAULT_WITHOUT_CONTEXT.maxVideoBitrate);
keyForField(FIELD_MAX_VIDEO_BITRATE), DEFAULT_WITHOUT_CONTEXT.maxVideoBitrate); minVideoWidth = bundle.getInt(FIELD_MIN_VIDEO_WIDTH, DEFAULT_WITHOUT_CONTEXT.minVideoWidth);
minVideoWidth =
bundle.getInt(keyForField(FIELD_MIN_VIDEO_WIDTH), DEFAULT_WITHOUT_CONTEXT.minVideoWidth);
minVideoHeight = minVideoHeight =
bundle.getInt( bundle.getInt(FIELD_MIN_VIDEO_HEIGHT, DEFAULT_WITHOUT_CONTEXT.minVideoHeight);
keyForField(FIELD_MIN_VIDEO_HEIGHT), DEFAULT_WITHOUT_CONTEXT.minVideoHeight);
minVideoFrameRate = minVideoFrameRate =
bundle.getInt( bundle.getInt(FIELD_MIN_VIDEO_FRAMERATE, DEFAULT_WITHOUT_CONTEXT.minVideoFrameRate);
keyForField(FIELD_MIN_VIDEO_FRAMERATE), DEFAULT_WITHOUT_CONTEXT.minVideoFrameRate);
minVideoBitrate = minVideoBitrate =
bundle.getInt( bundle.getInt(FIELD_MIN_VIDEO_BITRATE, DEFAULT_WITHOUT_CONTEXT.minVideoBitrate);
keyForField(FIELD_MIN_VIDEO_BITRATE), DEFAULT_WITHOUT_CONTEXT.minVideoBitrate); viewportWidth = bundle.getInt(FIELD_VIEWPORT_WIDTH, DEFAULT_WITHOUT_CONTEXT.viewportWidth);
viewportWidth = viewportHeight = bundle.getInt(FIELD_VIEWPORT_HEIGHT, DEFAULT_WITHOUT_CONTEXT.viewportHeight);
bundle.getInt(keyForField(FIELD_VIEWPORT_WIDTH), DEFAULT_WITHOUT_CONTEXT.viewportWidth);
viewportHeight =
bundle.getInt(keyForField(FIELD_VIEWPORT_HEIGHT), DEFAULT_WITHOUT_CONTEXT.viewportHeight);
viewportOrientationMayChange = viewportOrientationMayChange =
bundle.getBoolean( bundle.getBoolean(
keyForField(FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE), FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE,
DEFAULT_WITHOUT_CONTEXT.viewportOrientationMayChange); DEFAULT_WITHOUT_CONTEXT.viewportOrientationMayChange);
preferredVideoMimeTypes = preferredVideoMimeTypes =
ImmutableList.copyOf( ImmutableList.copyOf(
firstNonNull( firstNonNull(bundle.getStringArray(FIELD_PREFERRED_VIDEO_MIMETYPES), new String[0]));
bundle.getStringArray(keyForField(FIELD_PREFERRED_VIDEO_MIMETYPES)),
new String[0]));
preferredVideoRoleFlags = preferredVideoRoleFlags =
bundle.getInt( bundle.getInt(
keyForField(FIELD_PREFERRED_VIDEO_ROLE_FLAGS), FIELD_PREFERRED_VIDEO_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredVideoRoleFlags);
DEFAULT_WITHOUT_CONTEXT.preferredVideoRoleFlags);
// Audio // Audio
String[] preferredAudioLanguages1 = String[] preferredAudioLanguages1 =
firstNonNull( firstNonNull(bundle.getStringArray(FIELD_PREFERRED_AUDIO_LANGUAGES), new String[0]);
bundle.getStringArray(keyForField(FIELD_PREFERRED_AUDIO_LANGUAGES)), new String[0]);
preferredAudioLanguages = normalizeLanguageCodes(preferredAudioLanguages1); preferredAudioLanguages = normalizeLanguageCodes(preferredAudioLanguages1);
preferredAudioRoleFlags = preferredAudioRoleFlags =
bundle.getInt( bundle.getInt(
keyForField(FIELD_PREFERRED_AUDIO_ROLE_FLAGS), FIELD_PREFERRED_AUDIO_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredAudioRoleFlags);
DEFAULT_WITHOUT_CONTEXT.preferredAudioRoleFlags);
maxAudioChannelCount = maxAudioChannelCount =
bundle.getInt( bundle.getInt(
keyForField(FIELD_MAX_AUDIO_CHANNEL_COUNT), FIELD_MAX_AUDIO_CHANNEL_COUNT, DEFAULT_WITHOUT_CONTEXT.maxAudioChannelCount);
DEFAULT_WITHOUT_CONTEXT.maxAudioChannelCount);
maxAudioBitrate = maxAudioBitrate =
bundle.getInt( bundle.getInt(FIELD_MAX_AUDIO_BITRATE, DEFAULT_WITHOUT_CONTEXT.maxAudioBitrate);
keyForField(FIELD_MAX_AUDIO_BITRATE), DEFAULT_WITHOUT_CONTEXT.maxAudioBitrate);
preferredAudioMimeTypes = preferredAudioMimeTypes =
ImmutableList.copyOf( ImmutableList.copyOf(
firstNonNull( firstNonNull(bundle.getStringArray(FIELD_PREFERRED_AUDIO_MIME_TYPES), new String[0]));
bundle.getStringArray(keyForField(FIELD_PREFERRED_AUDIO_MIME_TYPES)),
new String[0]));
// Text // Text
preferredTextLanguages = preferredTextLanguages =
normalizeLanguageCodes( normalizeLanguageCodes(
firstNonNull( firstNonNull(bundle.getStringArray(FIELD_PREFERRED_TEXT_LANGUAGES), new String[0]));
bundle.getStringArray(keyForField(FIELD_PREFERRED_TEXT_LANGUAGES)),
new String[0]));
preferredTextRoleFlags = preferredTextRoleFlags =
bundle.getInt( bundle.getInt(
keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS), FIELD_PREFERRED_TEXT_ROLE_FLAGS, DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags);
DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags);
ignoredTextSelectionFlags = ignoredTextSelectionFlags =
bundle.getInt( bundle.getInt(
keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS), FIELD_IGNORED_TEXT_SELECTION_FLAGS,
DEFAULT_WITHOUT_CONTEXT.ignoredTextSelectionFlags); DEFAULT_WITHOUT_CONTEXT.ignoredTextSelectionFlags);
selectUndeterminedTextLanguage = selectUndeterminedTextLanguage =
bundle.getBoolean( bundle.getBoolean(
keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE), FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
DEFAULT_WITHOUT_CONTEXT.selectUndeterminedTextLanguage); DEFAULT_WITHOUT_CONTEXT.selectUndeterminedTextLanguage);
// General // General
forceLowestBitrate = forceLowestBitrate =
bundle.getBoolean( bundle.getBoolean(FIELD_FORCE_LOWEST_BITRATE, DEFAULT_WITHOUT_CONTEXT.forceLowestBitrate);
keyForField(FIELD_FORCE_LOWEST_BITRATE), DEFAULT_WITHOUT_CONTEXT.forceLowestBitrate);
forceHighestSupportedBitrate = forceHighestSupportedBitrate =
bundle.getBoolean( bundle.getBoolean(
keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE), FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE,
DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate); DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate);
@Nullable @Nullable
List<Bundle> overrideBundleList = List<Bundle> overrideBundleList = bundle.getParcelableArrayList(FIELD_SELECTION_OVERRIDES);
bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDES));
List<TrackSelectionOverride> overrideList = List<TrackSelectionOverride> overrideList =
overrideBundleList == null overrideBundleList == null
? ImmutableList.of() ? ImmutableList.of()
@ -257,7 +233,7 @@ public class TrackSelectionParameters implements Bundleable {
overrides.put(override.mediaTrackGroup, override); overrides.put(override.mediaTrackGroup, override);
} }
int[] disabledTrackTypeArray = int[] disabledTrackTypeArray =
firstNonNull(bundle.getIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE)), new int[0]); firstNonNull(bundle.getIntArray(FIELD_DISABLED_TRACK_TYPE), new int[0]);
disabledTrackTypes = new HashSet<>(); disabledTrackTypes = new HashSet<>();
for (@C.TrackType int disabledTrackType : disabledTrackTypeArray) { for (@C.TrackType int disabledTrackType : disabledTrackTypeArray) {
disabledTrackTypes.add(disabledTrackType); disabledTrackTypes.add(disabledTrackType);
@ -1103,39 +1079,40 @@ public class TrackSelectionParameters implements Bundleable {
// Bundleable implementation // Bundleable implementation
private static final int FIELD_PREFERRED_AUDIO_LANGUAGES = 1; private static final String FIELD_PREFERRED_AUDIO_LANGUAGES = Util.intToStringMaxRadix(1);
private static final int FIELD_PREFERRED_AUDIO_ROLE_FLAGS = 2; private static final String FIELD_PREFERRED_AUDIO_ROLE_FLAGS = Util.intToStringMaxRadix(2);
private static final int FIELD_PREFERRED_TEXT_LANGUAGES = 3; private static final String FIELD_PREFERRED_TEXT_LANGUAGES = Util.intToStringMaxRadix(3);
private static final int FIELD_PREFERRED_TEXT_ROLE_FLAGS = 4; private static final String FIELD_PREFERRED_TEXT_ROLE_FLAGS = Util.intToStringMaxRadix(4);
private static final int FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE = 5; private static final String FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE = Util.intToStringMaxRadix(5);
private static final int FIELD_MAX_VIDEO_WIDTH = 6; private static final String FIELD_MAX_VIDEO_WIDTH = Util.intToStringMaxRadix(6);
private static final int FIELD_MAX_VIDEO_HEIGHT = 7; private static final String FIELD_MAX_VIDEO_HEIGHT = Util.intToStringMaxRadix(7);
private static final int FIELD_MAX_VIDEO_FRAMERATE = 8; private static final String FIELD_MAX_VIDEO_FRAMERATE = Util.intToStringMaxRadix(8);
private static final int FIELD_MAX_VIDEO_BITRATE = 9; private static final String FIELD_MAX_VIDEO_BITRATE = Util.intToStringMaxRadix(9);
private static final int FIELD_MIN_VIDEO_WIDTH = 10; private static final String FIELD_MIN_VIDEO_WIDTH = Util.intToStringMaxRadix(10);
private static final int FIELD_MIN_VIDEO_HEIGHT = 11; private static final String FIELD_MIN_VIDEO_HEIGHT = Util.intToStringMaxRadix(11);
private static final int FIELD_MIN_VIDEO_FRAMERATE = 12; private static final String FIELD_MIN_VIDEO_FRAMERATE = Util.intToStringMaxRadix(12);
private static final int FIELD_MIN_VIDEO_BITRATE = 13; private static final String FIELD_MIN_VIDEO_BITRATE = Util.intToStringMaxRadix(13);
private static final int FIELD_VIEWPORT_WIDTH = 14; private static final String FIELD_VIEWPORT_WIDTH = Util.intToStringMaxRadix(14);
private static final int FIELD_VIEWPORT_HEIGHT = 15; private static final String FIELD_VIEWPORT_HEIGHT = Util.intToStringMaxRadix(15);
private static final int FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE = 16; private static final String FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE = Util.intToStringMaxRadix(16);
private static final int FIELD_PREFERRED_VIDEO_MIMETYPES = 17; private static final String FIELD_PREFERRED_VIDEO_MIMETYPES = Util.intToStringMaxRadix(17);
private static final int FIELD_MAX_AUDIO_CHANNEL_COUNT = 18; private static final String FIELD_MAX_AUDIO_CHANNEL_COUNT = Util.intToStringMaxRadix(18);
private static final int FIELD_MAX_AUDIO_BITRATE = 19; private static final String FIELD_MAX_AUDIO_BITRATE = Util.intToStringMaxRadix(19);
private static final int FIELD_PREFERRED_AUDIO_MIME_TYPES = 20; private static final String FIELD_PREFERRED_AUDIO_MIME_TYPES = Util.intToStringMaxRadix(20);
private static final int FIELD_FORCE_LOWEST_BITRATE = 21; private static final String FIELD_FORCE_LOWEST_BITRATE = Util.intToStringMaxRadix(21);
private static final int FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE = 22; private static final String FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE = Util.intToStringMaxRadix(22);
private static final int FIELD_SELECTION_OVERRIDES = 23; private static final String FIELD_SELECTION_OVERRIDES = Util.intToStringMaxRadix(23);
private static final int FIELD_DISABLED_TRACK_TYPE = 24; private static final String FIELD_DISABLED_TRACK_TYPE = Util.intToStringMaxRadix(24);
private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 25; private static final String FIELD_PREFERRED_VIDEO_ROLE_FLAGS = Util.intToStringMaxRadix(25);
private static final int FIELD_IGNORED_TEXT_SELECTION_FLAGS = 26; private static final String FIELD_IGNORED_TEXT_SELECTION_FLAGS = Util.intToStringMaxRadix(26);
/** /**
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()} * Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
* and {@link Bundleable.Creator}. * and {@link Bundleable.Creator}.
* *
* <p>Subclasses should obtain keys for their {@link Bundle} representation by applying a * <p>Subclasses should obtain keys for their {@link Bundle} representation by applying a
* non-negative offset on this constant and passing the result to {@link #keyForField(int)}. * non-negative offset on this constant and passing the result to {@link
* Util#intToStringMaxRadix(int)}.
*/ */
@UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000; @UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000;
@ -1144,46 +1121,39 @@ public class TrackSelectionParameters implements Bundleable {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
// Video // Video
bundle.putInt(keyForField(FIELD_MAX_VIDEO_WIDTH), maxVideoWidth); bundle.putInt(FIELD_MAX_VIDEO_WIDTH, maxVideoWidth);
bundle.putInt(keyForField(FIELD_MAX_VIDEO_HEIGHT), maxVideoHeight); bundle.putInt(FIELD_MAX_VIDEO_HEIGHT, maxVideoHeight);
bundle.putInt(keyForField(FIELD_MAX_VIDEO_FRAMERATE), maxVideoFrameRate); bundle.putInt(FIELD_MAX_VIDEO_FRAMERATE, maxVideoFrameRate);
bundle.putInt(keyForField(FIELD_MAX_VIDEO_BITRATE), maxVideoBitrate); bundle.putInt(FIELD_MAX_VIDEO_BITRATE, maxVideoBitrate);
bundle.putInt(keyForField(FIELD_MIN_VIDEO_WIDTH), minVideoWidth); bundle.putInt(FIELD_MIN_VIDEO_WIDTH, minVideoWidth);
bundle.putInt(keyForField(FIELD_MIN_VIDEO_HEIGHT), minVideoHeight); bundle.putInt(FIELD_MIN_VIDEO_HEIGHT, minVideoHeight);
bundle.putInt(keyForField(FIELD_MIN_VIDEO_FRAMERATE), minVideoFrameRate); bundle.putInt(FIELD_MIN_VIDEO_FRAMERATE, minVideoFrameRate);
bundle.putInt(keyForField(FIELD_MIN_VIDEO_BITRATE), minVideoBitrate); bundle.putInt(FIELD_MIN_VIDEO_BITRATE, minVideoBitrate);
bundle.putInt(keyForField(FIELD_VIEWPORT_WIDTH), viewportWidth); bundle.putInt(FIELD_VIEWPORT_WIDTH, viewportWidth);
bundle.putInt(keyForField(FIELD_VIEWPORT_HEIGHT), viewportHeight); bundle.putInt(FIELD_VIEWPORT_HEIGHT, viewportHeight);
bundle.putBoolean( bundle.putBoolean(FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE, viewportOrientationMayChange);
keyForField(FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE), viewportOrientationMayChange);
bundle.putStringArray( bundle.putStringArray(
keyForField(FIELD_PREFERRED_VIDEO_MIMETYPES), FIELD_PREFERRED_VIDEO_MIMETYPES, preferredVideoMimeTypes.toArray(new String[0]));
preferredVideoMimeTypes.toArray(new String[0])); bundle.putInt(FIELD_PREFERRED_VIDEO_ROLE_FLAGS, preferredVideoRoleFlags);
bundle.putInt(keyForField(FIELD_PREFERRED_VIDEO_ROLE_FLAGS), preferredVideoRoleFlags);
// Audio // Audio
bundle.putStringArray( bundle.putStringArray(
keyForField(FIELD_PREFERRED_AUDIO_LANGUAGES), FIELD_PREFERRED_AUDIO_LANGUAGES, preferredAudioLanguages.toArray(new String[0]));
preferredAudioLanguages.toArray(new String[0])); bundle.putInt(FIELD_PREFERRED_AUDIO_ROLE_FLAGS, preferredAudioRoleFlags);
bundle.putInt(keyForField(FIELD_PREFERRED_AUDIO_ROLE_FLAGS), preferredAudioRoleFlags); bundle.putInt(FIELD_MAX_AUDIO_CHANNEL_COUNT, maxAudioChannelCount);
bundle.putInt(keyForField(FIELD_MAX_AUDIO_CHANNEL_COUNT), maxAudioChannelCount); bundle.putInt(FIELD_MAX_AUDIO_BITRATE, maxAudioBitrate);
bundle.putInt(keyForField(FIELD_MAX_AUDIO_BITRATE), maxAudioBitrate);
bundle.putStringArray( bundle.putStringArray(
keyForField(FIELD_PREFERRED_AUDIO_MIME_TYPES), FIELD_PREFERRED_AUDIO_MIME_TYPES, preferredAudioMimeTypes.toArray(new String[0]));
preferredAudioMimeTypes.toArray(new String[0]));
// Text // Text
bundle.putStringArray( bundle.putStringArray(
keyForField(FIELD_PREFERRED_TEXT_LANGUAGES), preferredTextLanguages.toArray(new String[0])); FIELD_PREFERRED_TEXT_LANGUAGES, preferredTextLanguages.toArray(new String[0]));
bundle.putInt(keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS), preferredTextRoleFlags); bundle.putInt(FIELD_PREFERRED_TEXT_ROLE_FLAGS, preferredTextRoleFlags);
bundle.putInt(keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS), ignoredTextSelectionFlags); bundle.putInt(FIELD_IGNORED_TEXT_SELECTION_FLAGS, ignoredTextSelectionFlags);
bundle.putBoolean( bundle.putBoolean(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE, selectUndeterminedTextLanguage);
keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE), selectUndeterminedTextLanguage);
// General // General
bundle.putBoolean(keyForField(FIELD_FORCE_LOWEST_BITRATE), forceLowestBitrate); bundle.putBoolean(FIELD_FORCE_LOWEST_BITRATE, forceLowestBitrate);
bundle.putBoolean( bundle.putBoolean(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE, forceHighestSupportedBitrate);
keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE), forceHighestSupportedBitrate); bundle.putParcelableArrayList(FIELD_SELECTION_OVERRIDES, toBundleArrayList(overrides.values()));
bundle.putParcelableArrayList( bundle.putIntArray(FIELD_DISABLED_TRACK_TYPE, Ints.toArray(disabledTrackTypes));
keyForField(FIELD_SELECTION_OVERRIDES), toBundleArrayList(overrides.values()));
bundle.putIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE), Ints.toArray(disabledTrackTypes));
return bundle; return bundle;
} }
@ -1199,16 +1169,4 @@ public class TrackSelectionParameters implements Bundleable {
@UnstableApi @Deprecated @UnstableApi @Deprecated
public static final Creator<TrackSelectionParameters> CREATOR = public static final Creator<TrackSelectionParameters> CREATOR =
TrackSelectionParameters::fromBundle; TrackSelectionParameters::fromBundle;
/**
* 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);
}
} }

View File

@ -18,20 +18,15 @@ package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.BundleableUtil.toBundleArrayList; import static androidx.media3.common.util.BundleableUtil.toBundleArrayList;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Booleans; 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.Arrays;
import java.util.List; import java.util.List;
@ -221,29 +216,19 @@ public final class Tracks implements Bundleable {
} }
// Bundleable implementation. // 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 String FIELD_TRACK_GROUP = Util.intToStringMaxRadix(0);
private static final int FIELD_TRACK_SUPPORT = 1; private static final String FIELD_TRACK_SUPPORT = Util.intToStringMaxRadix(1);
private static final int FIELD_TRACK_SELECTED = 3; private static final String FIELD_TRACK_SELECTED = Util.intToStringMaxRadix(3);
private static final int FIELD_ADAPTIVE_SUPPORTED = 4; private static final String FIELD_ADAPTIVE_SUPPORTED = Util.intToStringMaxRadix(4);
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle()); bundle.putBundle(FIELD_TRACK_GROUP, mediaTrackGroup.toBundle());
bundle.putIntArray(keyForField(FIELD_TRACK_SUPPORT), trackSupport); bundle.putIntArray(FIELD_TRACK_SUPPORT, trackSupport);
bundle.putBooleanArray(keyForField(FIELD_TRACK_SELECTED), trackSelected); bundle.putBooleanArray(FIELD_TRACK_SELECTED, trackSelected);
bundle.putBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), adaptiveSupported); bundle.putBoolean(FIELD_ADAPTIVE_SUPPORTED, adaptiveSupported);
return bundle; return bundle;
} }
@ -253,23 +238,16 @@ public final class Tracks implements Bundleable {
bundle -> { bundle -> {
// Can't create a Tracks.Group without a TrackGroup // Can't create a Tracks.Group without a TrackGroup
TrackGroup trackGroup = TrackGroup trackGroup =
TrackGroup.CREATOR.fromBundle( TrackGroup.CREATOR.fromBundle(checkNotNull(bundle.getBundle(FIELD_TRACK_GROUP)));
checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP))));
final @C.FormatSupport int[] trackSupport = final @C.FormatSupport int[] trackSupport =
MoreObjects.firstNonNull( MoreObjects.firstNonNull(
bundle.getIntArray(keyForField(FIELD_TRACK_SUPPORT)), new int[trackGroup.length]); bundle.getIntArray(FIELD_TRACK_SUPPORT), new int[trackGroup.length]);
boolean[] selected = boolean[] selected =
MoreObjects.firstNonNull( MoreObjects.firstNonNull(
bundle.getBooleanArray(keyForField(FIELD_TRACK_SELECTED)), bundle.getBooleanArray(FIELD_TRACK_SELECTED), new boolean[trackGroup.length]);
new boolean[trackGroup.length]); boolean adaptiveSupported = bundle.getBoolean(FIELD_ADAPTIVE_SUPPORTED, false);
boolean adaptiveSupported =
bundle.getBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), false);
return new Group(trackGroup, adaptiveSupported, trackSupport, selected); return new Group(trackGroup, adaptiveSupported, trackSupport, selected);
}; };
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }
/** Empty tracks. */ /** Empty tracks. */
@ -385,21 +363,13 @@ public final class Tracks implements Bundleable {
} }
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_TRACK_GROUPS = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_TRACK_GROUPS,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUPS = 0;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelableArrayList(keyForField(FIELD_TRACK_GROUPS), toBundleArrayList(groups)); bundle.putParcelableArrayList(FIELD_TRACK_GROUPS, toBundleArrayList(groups));
return bundle; return bundle;
} }
@ -407,16 +377,11 @@ public final class Tracks implements Bundleable {
@UnstableApi @UnstableApi
public static final Creator<Tracks> CREATOR = public static final Creator<Tracks> CREATOR =
bundle -> { bundle -> {
@Nullable @Nullable List<Bundle> groupBundles = bundle.getParcelableArrayList(FIELD_TRACK_GROUPS);
List<Bundle> groupBundles = bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS));
List<Group> groups = List<Group> groups =
groupBundles == null groupBundles == null
? ImmutableList.of() ? ImmutableList.of()
: BundleableUtil.fromBundleList(Group.CREATOR, groupBundles); : BundleableUtil.fromBundleList(Group.CREATOR, groupBundles);
return new Tracks(groups); return new Tracks(groups);
}; };
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -15,18 +15,12 @@
*/ */
package androidx.media3.common; package androidx.media3.common;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.FloatRange; import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import java.lang.annotation.Documented; import androidx.media3.common.util.Util;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** Represents the video size. */ /** Represents the video size. */
public final class VideoSize implements Bundleable { public final class VideoSize implements Bundleable {
@ -132,48 +126,32 @@ public final class VideoSize implements Bundleable {
} }
// Bundleable implementation. // 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 String FIELD_WIDTH = Util.intToStringMaxRadix(0);
private static final int FIELD_HEIGHT = 1; private static final String FIELD_HEIGHT = Util.intToStringMaxRadix(1);
private static final int FIELD_UNAPPLIED_ROTATION_DEGREES = 2; private static final String FIELD_UNAPPLIED_ROTATION_DEGREES = Util.intToStringMaxRadix(2);
private static final int FIELD_PIXEL_WIDTH_HEIGHT_RATIO = 3; private static final String FIELD_PIXEL_WIDTH_HEIGHT_RATIO = Util.intToStringMaxRadix(3);
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_WIDTH), width); bundle.putInt(FIELD_WIDTH, width);
bundle.putInt(keyForField(FIELD_HEIGHT), height); bundle.putInt(FIELD_HEIGHT, height);
bundle.putInt(keyForField(FIELD_UNAPPLIED_ROTATION_DEGREES), unappliedRotationDegrees); bundle.putInt(FIELD_UNAPPLIED_ROTATION_DEGREES, unappliedRotationDegrees);
bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio); bundle.putFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, pixelWidthHeightRatio);
return bundle; return bundle;
} }
@UnstableApi @UnstableApi
public static final Creator<VideoSize> CREATOR = public static final Creator<VideoSize> CREATOR =
bundle -> { bundle -> {
int width = bundle.getInt(keyForField(FIELD_WIDTH), DEFAULT_WIDTH); int width = bundle.getInt(FIELD_WIDTH, DEFAULT_WIDTH);
int height = bundle.getInt(keyForField(FIELD_HEIGHT), DEFAULT_HEIGHT); int height = bundle.getInt(FIELD_HEIGHT, DEFAULT_HEIGHT);
int unappliedRotationDegrees = int unappliedRotationDegrees =
bundle.getInt( bundle.getInt(FIELD_UNAPPLIED_ROTATION_DEGREES, DEFAULT_UNAPPLIED_ROTATION_DEGREES);
keyForField(FIELD_UNAPPLIED_ROTATION_DEGREES), DEFAULT_UNAPPLIED_ROTATION_DEGREES);
float pixelWidthHeightRatio = float pixelWidthHeightRatio =
bundle.getFloat( bundle.getFloat(FIELD_PIXEL_WIDTH_HEIGHT_RATIO, DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO);
keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT_PIXEL_WIDTH_HEIGHT_RATIO);
return new VideoSize(width, height, unappliedRotationDegrees, pixelWidthHeightRatio); return new VideoSize(width, height, unappliedRotationDegrees, pixelWidthHeightRatio);
}; };
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -35,6 +35,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable; import androidx.media3.common.Bundleable;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
@ -977,69 +978,45 @@ public final class Cue implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_TEXT = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_TEXT_ALIGNMENT = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_MULTI_ROW_ALIGNMENT = Util.intToStringMaxRadix(2);
@IntDef({ private static final String FIELD_BITMAP = Util.intToStringMaxRadix(3);
FIELD_TEXT, private static final String FIELD_LINE = Util.intToStringMaxRadix(4);
FIELD_TEXT_ALIGNMENT, private static final String FIELD_LINE_TYPE = Util.intToStringMaxRadix(5);
FIELD_MULTI_ROW_ALIGNMENT, private static final String FIELD_LINE_ANCHOR = Util.intToStringMaxRadix(6);
FIELD_BITMAP, private static final String FIELD_POSITION = Util.intToStringMaxRadix(7);
FIELD_LINE, private static final String FIELD_POSITION_ANCHOR = Util.intToStringMaxRadix(8);
FIELD_LINE_TYPE, private static final String FIELD_TEXT_SIZE_TYPE = Util.intToStringMaxRadix(9);
FIELD_LINE_ANCHOR, private static final String FIELD_TEXT_SIZE = Util.intToStringMaxRadix(10);
FIELD_POSITION, private static final String FIELD_SIZE = Util.intToStringMaxRadix(11);
FIELD_POSITION_ANCHOR, private static final String FIELD_BITMAP_HEIGHT = Util.intToStringMaxRadix(12);
FIELD_TEXT_SIZE_TYPE, private static final String FIELD_WINDOW_COLOR = Util.intToStringMaxRadix(13);
FIELD_TEXT_SIZE, private static final String FIELD_WINDOW_COLOR_SET = Util.intToStringMaxRadix(14);
FIELD_SIZE, private static final String FIELD_VERTICAL_TYPE = Util.intToStringMaxRadix(15);
FIELD_BITMAP_HEIGHT, private static final String FIELD_SHEAR_DEGREES = Util.intToStringMaxRadix(16);
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;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putCharSequence(keyForField(FIELD_TEXT), text); bundle.putCharSequence(FIELD_TEXT, text);
bundle.putSerializable(keyForField(FIELD_TEXT_ALIGNMENT), textAlignment); bundle.putSerializable(FIELD_TEXT_ALIGNMENT, textAlignment);
bundle.putSerializable(keyForField(FIELD_MULTI_ROW_ALIGNMENT), multiRowAlignment); bundle.putSerializable(FIELD_MULTI_ROW_ALIGNMENT, multiRowAlignment);
bundle.putParcelable(keyForField(FIELD_BITMAP), bitmap); bundle.putParcelable(FIELD_BITMAP, bitmap);
bundle.putFloat(keyForField(FIELD_LINE), line); bundle.putFloat(FIELD_LINE, line);
bundle.putInt(keyForField(FIELD_LINE_TYPE), lineType); bundle.putInt(FIELD_LINE_TYPE, lineType);
bundle.putInt(keyForField(FIELD_LINE_ANCHOR), lineAnchor); bundle.putInt(FIELD_LINE_ANCHOR, lineAnchor);
bundle.putFloat(keyForField(FIELD_POSITION), position); bundle.putFloat(FIELD_POSITION, position);
bundle.putInt(keyForField(FIELD_POSITION_ANCHOR), positionAnchor); bundle.putInt(FIELD_POSITION_ANCHOR, positionAnchor);
bundle.putInt(keyForField(FIELD_TEXT_SIZE_TYPE), textSizeType); bundle.putInt(FIELD_TEXT_SIZE_TYPE, textSizeType);
bundle.putFloat(keyForField(FIELD_TEXT_SIZE), textSize); bundle.putFloat(FIELD_TEXT_SIZE, textSize);
bundle.putFloat(keyForField(FIELD_SIZE), size); bundle.putFloat(FIELD_SIZE, size);
bundle.putFloat(keyForField(FIELD_BITMAP_HEIGHT), bitmapHeight); bundle.putFloat(FIELD_BITMAP_HEIGHT, bitmapHeight);
bundle.putBoolean(keyForField(FIELD_WINDOW_COLOR_SET), windowColorSet); bundle.putBoolean(FIELD_WINDOW_COLOR_SET, windowColorSet);
bundle.putInt(keyForField(FIELD_WINDOW_COLOR), windowColor); bundle.putInt(FIELD_WINDOW_COLOR, windowColor);
bundle.putInt(keyForField(FIELD_VERTICAL_TYPE), verticalType); bundle.putInt(FIELD_VERTICAL_TYPE, verticalType);
bundle.putFloat(keyForField(FIELD_SHEAR_DEGREES), shearDegrees); bundle.putFloat(FIELD_SHEAR_DEGREES, shearDegrees);
return bundle; return bundle;
} }
@ -1047,67 +1024,56 @@ public final class Cue implements Bundleable {
private static final Cue fromBundle(Bundle bundle) { private static final Cue fromBundle(Bundle bundle) {
Builder builder = new Builder(); Builder builder = new Builder();
@Nullable CharSequence text = bundle.getCharSequence(keyForField(FIELD_TEXT)); @Nullable CharSequence text = bundle.getCharSequence(FIELD_TEXT);
if (text != null) { if (text != null) {
builder.setText(text); builder.setText(text);
} }
@Nullable @Nullable Alignment textAlignment = (Alignment) bundle.getSerializable(FIELD_TEXT_ALIGNMENT);
Alignment textAlignment = (Alignment) bundle.getSerializable(keyForField(FIELD_TEXT_ALIGNMENT));
if (textAlignment != null) { if (textAlignment != null) {
builder.setTextAlignment(textAlignment); builder.setTextAlignment(textAlignment);
} }
@Nullable @Nullable
Alignment multiRowAlignment = Alignment multiRowAlignment = (Alignment) bundle.getSerializable(FIELD_MULTI_ROW_ALIGNMENT);
(Alignment) bundle.getSerializable(keyForField(FIELD_MULTI_ROW_ALIGNMENT));
if (multiRowAlignment != null) { if (multiRowAlignment != null) {
builder.setMultiRowAlignment(multiRowAlignment); builder.setMultiRowAlignment(multiRowAlignment);
} }
@Nullable Bitmap bitmap = bundle.getParcelable(keyForField(FIELD_BITMAP)); @Nullable Bitmap bitmap = bundle.getParcelable(FIELD_BITMAP);
if (bitmap != null) { if (bitmap != null) {
builder.setBitmap(bitmap); builder.setBitmap(bitmap);
} }
if (bundle.containsKey(keyForField(FIELD_LINE)) if (bundle.containsKey(FIELD_LINE) && bundle.containsKey(FIELD_LINE_TYPE)) {
&& bundle.containsKey(keyForField(FIELD_LINE_TYPE))) { builder.setLine(bundle.getFloat(FIELD_LINE), bundle.getInt(FIELD_LINE_TYPE));
builder.setLine(
bundle.getFloat(keyForField(FIELD_LINE)), bundle.getInt(keyForField(FIELD_LINE_TYPE)));
} }
if (bundle.containsKey(keyForField(FIELD_LINE_ANCHOR))) { if (bundle.containsKey(FIELD_LINE_ANCHOR)) {
builder.setLineAnchor(bundle.getInt(keyForField(FIELD_LINE_ANCHOR))); builder.setLineAnchor(bundle.getInt(FIELD_LINE_ANCHOR));
} }
if (bundle.containsKey(keyForField(FIELD_POSITION))) { if (bundle.containsKey(FIELD_POSITION)) {
builder.setPosition(bundle.getFloat(keyForField(FIELD_POSITION))); builder.setPosition(bundle.getFloat(FIELD_POSITION));
} }
if (bundle.containsKey(keyForField(FIELD_POSITION_ANCHOR))) { if (bundle.containsKey(FIELD_POSITION_ANCHOR)) {
builder.setPositionAnchor(bundle.getInt(keyForField(FIELD_POSITION_ANCHOR))); builder.setPositionAnchor(bundle.getInt(FIELD_POSITION_ANCHOR));
} }
if (bundle.containsKey(keyForField(FIELD_TEXT_SIZE)) if (bundle.containsKey(FIELD_TEXT_SIZE) && bundle.containsKey(FIELD_TEXT_SIZE_TYPE)) {
&& bundle.containsKey(keyForField(FIELD_TEXT_SIZE_TYPE))) { builder.setTextSize(bundle.getFloat(FIELD_TEXT_SIZE), bundle.getInt(FIELD_TEXT_SIZE_TYPE));
builder.setTextSize(
bundle.getFloat(keyForField(FIELD_TEXT_SIZE)),
bundle.getInt(keyForField(FIELD_TEXT_SIZE_TYPE)));
} }
if (bundle.containsKey(keyForField(FIELD_SIZE))) { if (bundle.containsKey(FIELD_SIZE)) {
builder.setSize(bundle.getFloat(keyForField(FIELD_SIZE))); builder.setSize(bundle.getFloat(FIELD_SIZE));
} }
if (bundle.containsKey(keyForField(FIELD_BITMAP_HEIGHT))) { if (bundle.containsKey(FIELD_BITMAP_HEIGHT)) {
builder.setBitmapHeight(bundle.getFloat(keyForField(FIELD_BITMAP_HEIGHT))); builder.setBitmapHeight(bundle.getFloat(FIELD_BITMAP_HEIGHT));
} }
if (bundle.containsKey(keyForField(FIELD_WINDOW_COLOR))) { if (bundle.containsKey(FIELD_WINDOW_COLOR)) {
builder.setWindowColor(bundle.getInt(keyForField(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(); builder.clearWindowColor();
} }
if (bundle.containsKey(keyForField(FIELD_VERTICAL_TYPE))) { if (bundle.containsKey(FIELD_VERTICAL_TYPE)) {
builder.setVerticalType(bundle.getInt(keyForField(FIELD_VERTICAL_TYPE))); builder.setVerticalType(bundle.getInt(FIELD_VERTICAL_TYPE));
} }
if (bundle.containsKey(keyForField(FIELD_SHEAR_DEGREES))) { if (bundle.containsKey(FIELD_SHEAR_DEGREES)) {
builder.setShearDegrees(bundle.getFloat(keyForField(FIELD_SHEAR_DEGREES))); builder.setShearDegrees(bundle.getFloat(FIELD_SHEAR_DEGREES));
} }
return builder.build(); return builder.build();
} }
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }

View File

@ -15,21 +15,15 @@
*/ */
package androidx.media3.common.text; package androidx.media3.common.text;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable; import androidx.media3.common.Bundleable;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; 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.ArrayList;
import java.util.List; import java.util.List;
@ -66,41 +60,31 @@ public final class CueGroup implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_CUES = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_PRESENTATION_TIME_US = Util.intToStringMaxRadix(1);
@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;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelableArrayList( bundle.putParcelableArrayList(
keyForField(FIELD_CUES), BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues))); FIELD_CUES, BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues)));
bundle.putLong(keyForField(FIELD_PRESENTATION_TIME_US), presentationTimeUs); bundle.putLong(FIELD_PRESENTATION_TIME_US, presentationTimeUs);
return bundle; return bundle;
} }
@UnstableApi public static final Creator<CueGroup> CREATOR = CueGroup::fromBundle; @UnstableApi public static final Creator<CueGroup> CREATOR = CueGroup::fromBundle;
private static final CueGroup fromBundle(Bundle bundle) { 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 = List<Cue> cues =
cueBundles == null cueBundles == null
? ImmutableList.of() ? ImmutableList.of()
: BundleableUtil.fromBundleList(Cue.CREATOR, cueBundles); : 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); 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 * Filters out {@link Cue} objects containing {@link Bitmap}. It is used when transferring cues
* between processes to prevent transferring too much data. * between processes to prevent transferring too much data.

View File

@ -15,9 +15,12 @@
*/ */
package androidx.media3.common.util; package androidx.media3.common.util;
import static androidx.media3.common.util.Assertions.checkState;
import android.os.Looper; import android.os.Looper;
import android.os.Message; import android.os.Message;
import androidx.annotation.CheckResult; import androidx.annotation.CheckResult;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.FlagSet; 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 * <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. * 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. * @param <T> The listener type.
*/ */
@UnstableApi @UnstableApi
@ -76,14 +82,18 @@ public final class ListenerSet<T extends @NonNull Object> {
private final CopyOnWriteArraySet<ListenerHolder<T>> listeners; private final CopyOnWriteArraySet<ListenerHolder<T>> listeners;
private final ArrayDeque<Runnable> flushingEvents; private final ArrayDeque<Runnable> flushingEvents;
private final ArrayDeque<Runnable> queuedEvents; private final ArrayDeque<Runnable> queuedEvents;
private final Object releasedLock;
@GuardedBy("releasedLock")
private boolean released; private boolean released;
private boolean throwsWhenUsingWrongThread;
/** /**
* Creates a new listener set. * Creates a new listener set.
* *
* @param looper A {@link Looper} used to call listeners on. The same {@link Looper} must be used * @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 clock A {@link Clock}.
* @param iterationFinishedEvent An {@link IterationFinishedEvent} sent when all other events sent * @param iterationFinishedEvent An {@link IterationFinishedEvent} sent when all other events sent
* during one {@link Looper} message queue iteration were handled by the listeners. * 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.clock = clock;
this.listeners = listeners; this.listeners = listeners;
this.iterationFinishedEvent = iterationFinishedEvent; this.iterationFinishedEvent = iterationFinishedEvent;
releasedLock = new Object();
flushingEvents = new ArrayDeque<>(); flushingEvents = new ArrayDeque<>();
queuedEvents = new ArrayDeque<>(); queuedEvents = new ArrayDeque<>();
// It's safe to use "this" because we don't send a message before exiting the constructor. // It's safe to use "this" because we don't send a message before exiting the constructor.
@SuppressWarnings("nullness:methodref.receiver.bound") @SuppressWarnings("nullness:methodref.receiver.bound")
HandlerWrapper handler = clock.createHandler(looper, this::handleMessage); HandlerWrapper handler = clock.createHandler(looper, this::handleMessage);
this.handler = handler; this.handler = handler;
throwsWhenUsingWrongThread = true;
} }
/** /**
* Copies the listener set. * 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 looper The new {@link Looper} for the copied listener set.
* @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events * @param iterationFinishedEvent The new {@link IterationFinishedEvent} sent when all other events
* sent during one {@link Looper} message queue iteration were handled by the listeners. * 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. * 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 looper The new {@link Looper} for the copied listener set.
* @param clock The new {@link Clock} 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 * @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>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. * @param listener The listener to be added.
*/ */
public void add(T listener) { public void add(T listener) {
if (released) {
return;
}
Assertions.checkNotNull(listener); 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. * @param listener The listener to be removed.
*/ */
public void remove(T listener) { public void remove(T listener) {
verifyCurrentThread();
for (ListenerHolder<T> listenerHolder : listeners) { for (ListenerHolder<T> listenerHolder : listeners) {
if (listenerHolder.listener.equals(listener)) { if (listenerHolder.listener.equals(listener)) {
listenerHolder.release(iterationFinishedEvent); listenerHolder.release(iterationFinishedEvent);
@ -169,11 +190,13 @@ public final class ListenerSet<T extends @NonNull Object> {
/** Removes all listeners from the set. */ /** Removes all listeners from the set. */
public void clear() { public void clear() {
verifyCurrentThread();
listeners.clear(); listeners.clear();
} }
/** Returns the number of added listeners. */ /** Returns the number of added listeners. */
public int size() { public int size() {
verifyCurrentThread();
return listeners.size(); return listeners.size();
} }
@ -185,6 +208,7 @@ public final class ListenerSet<T extends @NonNull Object> {
* @param event The event. * @param event The event.
*/ */
public void queueEvent(int eventFlag, Event<T> event) { public void queueEvent(int eventFlag, Event<T> event) {
verifyCurrentThread();
CopyOnWriteArraySet<ListenerHolder<T>> listenerSnapshot = new CopyOnWriteArraySet<>(listeners); CopyOnWriteArraySet<ListenerHolder<T>> listenerSnapshot = new CopyOnWriteArraySet<>(listeners);
queuedEvents.add( 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)}. */ /** Notifies listeners of events previously enqueued with {@link #queueEvent(int, Event)}. */
public void flushEvents() { public void flushEvents() {
verifyCurrentThread();
if (queuedEvents.isEmpty()) { if (queuedEvents.isEmpty()) {
return; 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. * <p>This will ensure no events are sent to any listener after this method has been called.
*/ */
public void release() { public void release() {
verifyCurrentThread();
synchronized (releasedLock) {
released = true;
}
for (ListenerHolder<T> listenerHolder : listeners) { for (ListenerHolder<T> listenerHolder : listeners) {
listenerHolder.release(iterationFinishedEvent); listenerHolder.release(iterationFinishedEvent);
} }
listeners.clear(); 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) { private boolean handleMessage(Message message) {
@ -254,6 +295,13 @@ public final class ListenerSet<T extends @NonNull Object> {
return true; return true;
} }
private void verifyCurrentThread() {
if (!throwsWhenUsingWrongThread) {
return;
}
checkState(Thread.currentThread() == handler.getLooper().getThread());
}
private static final class ListenerHolder<T extends @NonNull Object> { private static final class ListenerHolder<T extends @NonNull Object> {
public final T listener; public final T listener;

View File

@ -17,6 +17,9 @@ package androidx.media3.common.util;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.google.common.base.Charsets; 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.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Arrays; import java.util.Arrays;
@ -28,6 +31,12 @@ import java.util.Arrays;
@UnstableApi @UnstableApi
public final class ParsableByteArray { 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 byte[] data;
private int position; private int position;
// TODO(internal b/147657250): Enforce this limit on all read methods. // 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>A line is considered to be terminated by any one of a carriage return ('\r'), a line feed * <p>Equivalent to passing {@link Charsets#UTF_8} to {@link #readLine(Charset)}.
* ('\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.
*
* @return The line not including any line-termination characters, or null if the end of the data
* has already been reached.
*/ */
@Nullable @Nullable
public String readLine() { 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'). 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(Charset charset) {
Assertions.checkArgument(
SUPPORTED_CHARSETS_FOR_READLINE.contains(charset), "Unsupported charset: " + charset);
if (bytesLeft() == 0) { if (bytesLeft() == 0) {
return null; return null;
} }
int lineLimit = position; if (!charset.equals(Charsets.US_ASCII)) {
while (lineLimit < limit && !Util.isLinebreak(data[lineLimit])) { readUtfCharsetFromBom(); // Skip BOM if present
lineLimit++;
} }
if (lineLimit - position >= 3 int lineLimit = findNextLineTerminator(charset);
&& data[position] == (byte) 0xEF String line = readString(lineLimit - position, charset);
&& 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;
if (position == limit) { if (position == limit) {
return line; return line;
} }
if (data[position] == '\r') { skipLineTerminator(charset);
position++;
if (position == limit) {
return line;
}
}
if (data[position] == '\n') {
position++;
}
return line; return line;
} }
@ -566,4 +577,99 @@ public final class ParsableByteArray {
position += length; position += length;
return value; 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;
}
}
} }

View File

@ -29,6 +29,9 @@ public final class Size {
public static final Size UNKNOWN = public static final Size UNKNOWN =
new Size(/* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET); 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 width;
private final int height; private final int height;

View File

@ -47,6 +47,7 @@ import android.content.res.Resources;
import android.database.DatabaseUtils; import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.graphics.Point; import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager; import android.hardware.display.DisplayManager;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioManager; import android.media.AudioManager;
@ -66,6 +67,8 @@ import android.util.SparseLongArray;
import android.view.Display; import android.view.Display;
import android.view.SurfaceView; import android.view.SurfaceView;
import android.view.WindowManager; import android.view.WindowManager;
import androidx.annotation.DoNotInline;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -2864,6 +2867,33 @@ public final class Util {
return sum; 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 @Nullable
private static String getSystemProperty(String name) { private static String getSystemProperty(String name) {
try { try {
@ -3100,4 +3130,12 @@ public final class Util {
0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4,
0xF3 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());
}
}
} }

View File

@ -24,6 +24,7 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
@ -402,7 +403,43 @@ public class AdPlaybackStateTest {
} }
@Test @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 = AdPlaybackState originalState =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US) new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US)
.withRemovedAdGroupCount(1) .withRemovedAdGroupCount(1)

View File

@ -0,0 +1,318 @@
/*
* 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;
}
}
}

View File

@ -111,6 +111,8 @@ public final class FormatTest {
.setEncoderPadding(1002) .setEncoderPadding(1002)
.setAccessibilityChannel(2) .setAccessibilityChannel(2)
.setCryptoType(C.CRYPTO_TYPE_CUSTOM_BASE) .setCryptoType(C.CRYPTO_TYPE_CUSTOM_BASE)
.setTileCountHorizontal(20)
.setTileCountVertical(40)
.build(); .build();
} }

View File

@ -360,10 +360,12 @@ public class MediaItemTest {
} }
@Test @Test
public void clippingConfigurationDefaults() { public void createDefaultClippingConfigurationInstance_checksDefaultValues() {
MediaItem.ClippingConfiguration clippingConfiguration = MediaItem.ClippingConfiguration clippingConfiguration =
new MediaItem.ClippingConfiguration.Builder().build(); 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.startPositionMs).isEqualTo(0L);
assertThat(clippingConfiguration.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE); assertThat(clippingConfiguration.endPositionMs).isEqualTo(C.TIME_END_OF_SOURCE);
assertThat(clippingConfiguration.relativeToLiveWindow).isFalse(); assertThat(clippingConfiguration.relativeToLiveWindow).isFalse();
@ -372,6 +374,38 @@ public class MediaItemTest {
assertThat(clippingConfiguration).isEqualTo(MediaItem.ClippingConfiguration.UNSET); assertThat(clippingConfiguration).isEqualTo(MediaItem.ClippingConfiguration.UNSET);
} }
@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 @Test
public void clippingConfigurationBuilder_throwsOnInvalidValues() { public void clippingConfigurationBuilder_throwsOnInvalidValues() {
MediaItem.ClippingConfiguration.Builder clippingConfigurationBuilder = MediaItem.ClippingConfiguration.Builder clippingConfigurationBuilder =
@ -514,6 +548,53 @@ public class MediaItemTest {
assertThat(mediaItem.mediaMetadata).isEqualTo(mediaMetadata); assertThat(mediaItem.mediaMetadata).isEqualTo(mediaMetadata);
} }
@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 @Test
public void builderSetLiveConfiguration() { public void builderSetLiveConfiguration() {
MediaItem mediaItem = MediaItem mediaItem =
@ -747,4 +828,62 @@ public class MediaItemTest {
assertThat(mediaItem.localConfiguration).isNotNull(); assertThat(mediaItem.localConfiguration).isNotNull();
assertThat(MediaItem.CREATOR.fromBundle(mediaItem.toBundle()).localConfiguration).isNull(); 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);
}
} }

View File

@ -49,6 +49,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.trackNumber).isNull(); assertThat(mediaMetadata.trackNumber).isNull();
assertThat(mediaMetadata.totalTrackCount).isNull(); assertThat(mediaMetadata.totalTrackCount).isNull();
assertThat(mediaMetadata.folderType).isNull(); assertThat(mediaMetadata.folderType).isNull();
assertThat(mediaMetadata.isBrowsable).isNull();
assertThat(mediaMetadata.isPlayable).isNull(); assertThat(mediaMetadata.isPlayable).isNull();
assertThat(mediaMetadata.recordingYear).isNull(); assertThat(mediaMetadata.recordingYear).isNull();
assertThat(mediaMetadata.recordingMonth).isNull(); assertThat(mediaMetadata.recordingMonth).isNull();
@ -64,6 +65,7 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.genre).isNull(); assertThat(mediaMetadata.genre).isNull();
assertThat(mediaMetadata.compilation).isNull(); assertThat(mediaMetadata.compilation).isNull();
assertThat(mediaMetadata.station).isNull(); assertThat(mediaMetadata.station).isNull();
assertThat(mediaMetadata.mediaType).isNull();
assertThat(mediaMetadata.extras).isNull(); assertThat(mediaMetadata.extras).isNull();
} }
@ -105,13 +107,86 @@ public class MediaMetadataTest {
} }
@Test @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 mediaMetadata = getFullyPopulatedMediaMetadata();
MediaMetadata fromBundle = MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle()); MediaMetadata mediaMetadataFromBundle =
assertThat(fromBundle).isEqualTo(mediaMetadata); MediaMetadata.CREATOR.fromBundle(mediaMetadata.toBundle());
assertThat(mediaMetadataFromBundle).isEqualTo(mediaMetadata);
// Extras is not implemented in MediaMetadata.equals(Object o). // 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() { private static MediaMetadata getFullyPopulatedMediaMetadata() {
@ -134,6 +209,7 @@ public class MediaMetadataTest {
.setTrackNumber(4) .setTrackNumber(4)
.setTotalTrackCount(12) .setTotalTrackCount(12)
.setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS) .setFolderType(MediaMetadata.FOLDER_TYPE_PLAYLISTS)
.setIsBrowsable(true)
.setIsPlayable(true) .setIsPlayable(true)
.setRecordingYear(2000) .setRecordingYear(2000)
.setRecordingMonth(11) .setRecordingMonth(11)
@ -149,6 +225,7 @@ public class MediaMetadataTest {
.setGenre("Pop") .setGenre("Pop")
.setCompilation("Amazing songs.") .setCompilation("Amazing songs.")
.setStation("radio station") .setStation("radio station")
.setMediaType(MediaMetadata.MEDIA_TYPE_MIXED)
.setExtras(extras) .setExtras(extras)
.build(); .build();
} }

View File

@ -17,6 +17,7 @@ package androidx.media3.common;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.MediaItem.LiveConfiguration; import androidx.media3.common.MediaItem.LiveConfiguration;
import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder; import androidx.media3.exoplayer.source.ShuffleOrder.DefaultShuffleOrder;
@ -267,7 +268,9 @@ public class TimelineTest {
/* durationUs= */ 2, /* durationUs= */ 2,
/* defaultPositionUs= */ 22, /* defaultPositionUs= */ 22,
/* windowOffsetInFirstPeriodUs= */ 222, /* windowOffsetInFirstPeriodUs= */ 222,
ImmutableList.of(AdPlaybackState.NONE), ImmutableList.of(
new AdPlaybackState(
/* adsId= */ null, /* adGroupTimesUs...= */ 10_000, 20_000)),
new MediaItem.Builder().setMediaId("mediaId2").build()), new MediaItem.Builder().setMediaId("mediaId2").build()),
new TimelineWindowDefinition( new TimelineWindowDefinition(
/* periodCount= */ 3, /* periodCount= */ 3,
@ -334,6 +337,29 @@ public class TimelineTest {
TimelineAsserts.assertEmpty(Timeline.CREATOR.fromBundle(Timeline.EMPTY.toBundle())); TimelineAsserts.assertEmpty(Timeline.CREATOR.fromBundle(Timeline.EMPTY.toBundle()));
} }
@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 @Test
public void roundTripViaBundle_ofWindow_yieldsEqualInstanceExceptUidAndManifest() { public void roundTripViaBundle_ofWindow_yieldsEqualInstanceExceptUidAndManifest() {
Timeline.Window window = new Timeline.Window(); Timeline.Window window = new Timeline.Window();
@ -367,6 +393,26 @@ public class TimelineTest {
/* expectedWindow= */ window, /* actualWindow= */ restoredWindow); /* expectedWindow= */ window, /* actualWindow= */ restoredWindow);
} }
@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 @Test
public void roundTripViaBundle_ofPeriod_yieldsEqualInstanceExceptIds() { public void roundTripViaBundle_ofPeriod_yieldsEqualInstanceExceptIds() {
Timeline.Period period = new Timeline.Period(); Timeline.Period period = new Timeline.Period();

View File

@ -15,11 +15,13 @@
*/ */
package androidx.media3.common.util; package androidx.media3.common.util;
import static androidx.media3.test.utils.TestUtil.createByteArray;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.Charset.forName; import static java.nio.charset.Charset.forName;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.base.Charsets;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.Arrays; import java.util.Arrays;
@ -330,6 +332,7 @@ public final class ParsableByteArrayTest {
public void readLittleEndianLong() { public void readLittleEndianLong() {
ParsableByteArray byteArray = ParsableByteArray byteArray =
new ParsableByteArray(new byte[] {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFF}); new ParsableByteArray(new byte[] {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, (byte) 0xFF});
assertThat(byteArray.readLittleEndianLong()).isEqualTo(0xFF00000000000001L); assertThat(byteArray.readLittleEndianLong()).isEqualTo(0xFF00000000000001L);
assertThat(byteArray.getPosition()).isEqualTo(8); assertThat(byteArray.getPosition()).isEqualTo(8);
} }
@ -337,6 +340,7 @@ public final class ParsableByteArrayTest {
@Test @Test
public void readLittleEndianUnsignedInt() { public void readLittleEndianUnsignedInt() {
ParsableByteArray byteArray = new ParsableByteArray(new byte[] {0x10, 0x00, 0x00, (byte) 0xFF}); ParsableByteArray byteArray = new ParsableByteArray(new byte[] {0x10, 0x00, 0x00, (byte) 0xFF});
assertThat(byteArray.readLittleEndianUnsignedInt()).isEqualTo(0xFF000010L); assertThat(byteArray.readLittleEndianUnsignedInt()).isEqualTo(0xFF000010L);
assertThat(byteArray.getPosition()).isEqualTo(4); assertThat(byteArray.getPosition()).isEqualTo(4);
} }
@ -344,6 +348,7 @@ public final class ParsableByteArrayTest {
@Test @Test
public void readLittleEndianInt() { public void readLittleEndianInt() {
ParsableByteArray byteArray = new ParsableByteArray(new byte[] {0x01, 0x00, 0x00, (byte) 0xFF}); ParsableByteArray byteArray = new ParsableByteArray(new byte[] {0x01, 0x00, 0x00, (byte) 0xFF});
assertThat(byteArray.readLittleEndianInt()).isEqualTo(0xFF000001); assertThat(byteArray.readLittleEndianInt()).isEqualTo(0xFF000001);
assertThat(byteArray.getPosition()).isEqualTo(4); assertThat(byteArray.getPosition()).isEqualTo(4);
} }
@ -352,6 +357,7 @@ public final class ParsableByteArrayTest {
public void readLittleEndianUnsignedInt24() { public void readLittleEndianUnsignedInt24() {
byte[] data = {0x01, 0x02, (byte) 0xFF}; byte[] data = {0x01, 0x02, (byte) 0xFF};
ParsableByteArray byteArray = new ParsableByteArray(data); ParsableByteArray byteArray = new ParsableByteArray(data);
assertThat(byteArray.readLittleEndianUnsignedInt24()).isEqualTo(0xFF0201); assertThat(byteArray.readLittleEndianUnsignedInt24()).isEqualTo(0xFF0201);
assertThat(byteArray.getPosition()).isEqualTo(3); assertThat(byteArray.getPosition()).isEqualTo(3);
} }
@ -360,6 +366,7 @@ public final class ParsableByteArrayTest {
public void readInt24Positive() { public void readInt24Positive() {
byte[] data = {0x01, 0x02, (byte) 0xFF}; byte[] data = {0x01, 0x02, (byte) 0xFF};
ParsableByteArray byteArray = new ParsableByteArray(data); ParsableByteArray byteArray = new ParsableByteArray(data);
assertThat(byteArray.readInt24()).isEqualTo(0x0102FF); assertThat(byteArray.readInt24()).isEqualTo(0x0102FF);
assertThat(byteArray.getPosition()).isEqualTo(3); assertThat(byteArray.getPosition()).isEqualTo(3);
} }
@ -368,6 +375,7 @@ public final class ParsableByteArrayTest {
public void readInt24Negative() { public void readInt24Negative() {
byte[] data = {(byte) 0xFF, 0x02, (byte) 0x01}; byte[] data = {(byte) 0xFF, 0x02, (byte) 0x01};
ParsableByteArray byteArray = new ParsableByteArray(data); ParsableByteArray byteArray = new ParsableByteArray(data);
assertThat(byteArray.readInt24()).isEqualTo(0xFFFF0201); assertThat(byteArray.readInt24()).isEqualTo(0xFFFF0201);
assertThat(byteArray.getPosition()).isEqualTo(3); assertThat(byteArray.getPosition()).isEqualTo(3);
} }
@ -376,6 +384,7 @@ public final class ParsableByteArrayTest {
public void readLittleEndianUnsignedShort() { public void readLittleEndianUnsignedShort() {
ParsableByteArray byteArray = ParsableByteArray byteArray =
new ParsableByteArray(new byte[] {0x01, (byte) 0xFF, 0x02, (byte) 0xFF}); new ParsableByteArray(new byte[] {0x01, (byte) 0xFF, 0x02, (byte) 0xFF});
assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF01); assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF01);
assertThat(byteArray.getPosition()).isEqualTo(2); assertThat(byteArray.getPosition()).isEqualTo(2);
assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF02); assertThat(byteArray.readLittleEndianUnsignedShort()).isEqualTo(0xFF02);
@ -386,6 +395,7 @@ public final class ParsableByteArrayTest {
public void readLittleEndianShort() { public void readLittleEndianShort() {
ParsableByteArray byteArray = ParsableByteArray byteArray =
new ParsableByteArray(new byte[] {0x01, (byte) 0xFF, 0x02, (byte) 0xFF}); new ParsableByteArray(new byte[] {0x01, (byte) 0xFF, 0x02, (byte) 0xFF});
assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF01); assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF01);
assertThat(byteArray.getPosition()).isEqualTo(2); assertThat(byteArray.getPosition()).isEqualTo(2);
assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF02); assertThat(byteArray.readLittleEndianShort()).isEqualTo((short) 0xFF02);
@ -420,6 +430,7 @@ public final class ParsableByteArrayTest {
(byte) 0x20, (byte) 0x20,
}; };
ParsableByteArray byteArray = new ParsableByteArray(data); ParsableByteArray byteArray = new ParsableByteArray(data);
assertThat(byteArray.readString(data.length)).isEqualTo("ä ö ® π √ ± 谢 "); assertThat(byteArray.readString(data.length)).isEqualTo("ä ö ® π √ ± 谢 ");
assertThat(byteArray.getPosition()).isEqualTo(data.length); assertThat(byteArray.getPosition()).isEqualTo(data.length);
} }
@ -428,6 +439,7 @@ public final class ParsableByteArrayTest {
public void readAsciiString() { public void readAsciiString() {
byte[] data = new byte[] {'t', 'e', 's', 't'}; byte[] data = new byte[] {'t', 'e', 's', 't'};
ParsableByteArray testArray = new ParsableByteArray(data); ParsableByteArray testArray = new ParsableByteArray(data);
assertThat(testArray.readString(data.length, forName("US-ASCII"))).isEqualTo("test"); assertThat(testArray.readString(data.length, forName("US-ASCII"))).isEqualTo("test");
assertThat(testArray.getPosition()).isEqualTo(data.length); assertThat(testArray.getPosition()).isEqualTo(data.length);
} }
@ -436,6 +448,7 @@ public final class ParsableByteArrayTest {
public void readStringOutOfBoundsDoesNotMovePosition() { public void readStringOutOfBoundsDoesNotMovePosition() {
byte[] data = {(byte) 0xC3, (byte) 0xA4, (byte) 0x20}; byte[] data = {(byte) 0xC3, (byte) 0xA4, (byte) 0x20};
ParsableByteArray byteArray = new ParsableByteArray(data); ParsableByteArray byteArray = new ParsableByteArray(data);
try { try {
byteArray.readString(data.length + 1); byteArray.readString(data.length + 1);
fail(); fail();
@ -452,17 +465,22 @@ public final class ParsableByteArrayTest {
} }
@Test @Test
public void readNullTerminatedStringWithLengths() { public void readNullTerminatedStringWithLengths_readLengthsMatchNullPositions() {
byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0}; byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0};
// Test with lengths that match NUL byte positions.
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo"); assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readNullTerminatedString(4)).isEqualTo("bar"); assertThat(parser.readNullTerminatedString(4)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readNullTerminatedString()).isNull(); assertThat(parser.readNullTerminatedString()).isNull();
// Test with lengths that do not match NUL byte positions. }
parser = new ParsableByteArray(bytes);
@Test
public void readNullTerminatedStringWithLengths_readLengthsDontMatchNullPositions() {
byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0};
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readNullTerminatedString(2)).isEqualTo("fo"); assertThat(parser.readNullTerminatedString(2)).isEqualTo("fo");
assertThat(parser.getPosition()).isEqualTo(2); assertThat(parser.getPosition()).isEqualTo(2);
assertThat(parser.readNullTerminatedString(2)).isEqualTo("o"); assertThat(parser.readNullTerminatedString(2)).isEqualTo("o");
@ -472,13 +490,23 @@ public final class ParsableByteArrayTest {
assertThat(parser.readNullTerminatedString(1)).isEqualTo(""); assertThat(parser.readNullTerminatedString(1)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readNullTerminatedString()).isNull(); assertThat(parser.readNullTerminatedString()).isNull();
// Test with limit at NUL }
parser = new ParsableByteArray(bytes, 4);
@Test
public void readNullTerminatedStringWithLengths_limitAtNull() {
byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0};
ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 4);
assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo"); assertThat(parser.readNullTerminatedString(4)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readNullTerminatedString()).isNull(); assertThat(parser.readNullTerminatedString()).isNull();
// Test with limit before NUL }
parser = new ParsableByteArray(bytes, 3);
@Test
public void readNullTerminatedStringWithLengths_limitBeforeNull() {
byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0};
ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 3);
assertThat(parser.readNullTerminatedString(3)).isEqualTo("foo"); assertThat(parser.readNullTerminatedString(3)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(3); assertThat(parser.getPosition()).isEqualTo(3);
assertThat(parser.readNullTerminatedString()).isNull(); assertThat(parser.readNullTerminatedString()).isNull();
@ -487,20 +515,30 @@ public final class ParsableByteArrayTest {
@Test @Test
public void readNullTerminatedString() { public void readNullTerminatedString() {
byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0}; byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0};
// Test normal case.
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); assertThat(parser.readNullTerminatedString()).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readNullTerminatedString()).isEqualTo("bar"); assertThat(parser.readNullTerminatedString()).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readNullTerminatedString()).isNull(); assertThat(parser.readNullTerminatedString()).isNull();
// Test with limit at NUL. }
parser = new ParsableByteArray(bytes, 4);
@Test
public void readNullTerminatedString_withLimitAtNull() {
byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0};
ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 4);
assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); assertThat(parser.readNullTerminatedString()).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readNullTerminatedString()).isNull(); assertThat(parser.readNullTerminatedString()).isNull();
// Test with limit before NUL. }
parser = new ParsableByteArray(bytes, 3);
@Test
public void readNullTerminatedString_withLimitBeforeNull() {
byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r', 0};
ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 3);
assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); assertThat(parser.readNullTerminatedString()).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(3); assertThat(parser.getPosition()).isEqualTo(3);
assertThat(parser.readNullTerminatedString()).isNull(); assertThat(parser.readNullTerminatedString()).isNull();
@ -510,6 +548,7 @@ public final class ParsableByteArrayTest {
public void readNullTerminatedStringWithoutEndingNull() { public void readNullTerminatedStringWithoutEndingNull() {
byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r'}; byte[] bytes = new byte[] {'f', 'o', 'o', 0, 'b', 'a', 'r'};
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readNullTerminatedString()).isEqualTo("foo"); assertThat(parser.readNullTerminatedString()).isEqualTo("foo");
assertThat(parser.readNullTerminatedString()).isEqualTo("bar"); assertThat(parser.readNullTerminatedString()).isEqualTo("bar");
assertThat(parser.readNullTerminatedString()).isNull(); assertThat(parser.readNullTerminatedString()).isNull();
@ -518,78 +557,364 @@ public final class ParsableByteArrayTest {
@Test @Test
public void readDelimiterTerminatedString() { public void readDelimiterTerminatedString() {
byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r', '*'}; byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r', '*'};
// Test normal case.
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo"); assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("bar"); assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(8); assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readDelimiterTerminatedString('*')).isNull(); assertThat(parser.readDelimiterTerminatedString('*')).isNull();
}
@Test
public void readDelimiterTerminatedString_limitAtDelimiter() {
byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r', '*'};
ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 4);
// Test with limit at delimiter.
parser = new ParsableByteArray(bytes, 4);
assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo"); assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4); assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readDelimiterTerminatedString('*')).isNull(); assertThat(parser.readDelimiterTerminatedString('*')).isNull();
// Test with limit before delimiter. }
parser = new ParsableByteArray(bytes, 3);
@Test
public void readDelimiterTerminatedString_limitBeforeDelimiter() {
byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r', '*'};
ParsableByteArray parser = new ParsableByteArray(bytes, /* limit= */ 3);
assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo"); assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(3); assertThat(parser.getPosition()).isEqualTo(3);
assertThat(parser.readDelimiterTerminatedString('*')).isNull(); assertThat(parser.readDelimiterTerminatedString('*')).isNull();
} }
@Test @Test
public void readDelimiterTerminatedStringWithoutEndingDelimiter() { public void readDelimiterTerminatedStringW_noDelimiter() {
byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r'}; byte[] bytes = new byte[] {'f', 'o', 'o', '*', 'b', 'a', 'r'};
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo"); assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("foo");
assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("bar"); assertThat(parser.readDelimiterTerminatedString('*')).isEqualTo("bar");
assertThat(parser.readDelimiterTerminatedString('*')).isNull(); assertThat(parser.readDelimiterTerminatedString('*')).isNull();
} }
@Test @Test
public void readSingleLineWithoutEndingTrail() { public void readSingleLineWithoutEndingTrail_ascii() {
byte[] bytes = new byte[] {'f', 'o', 'o'}; byte[] bytes = "foo".getBytes(Charsets.US_ASCII);
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(3);
assertThat(parser.readLine(Charsets.US_ASCII)).isNull();
}
@Test
public void readSingleLineWithEndingLf_ascii() {
byte[] bytes = "foo\n".getBytes(Charsets.US_ASCII);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readLine(Charsets.US_ASCII)).isNull();
}
@Test
public void readTwoLinesWithCrFollowedByLf_ascii() {
byte[] bytes = "foo\r\nbar".getBytes(Charsets.US_ASCII);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(5);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readLine(Charsets.US_ASCII)).isNull();
}
@Test
public void readThreeLinesWithEmptyLine_ascii() {
byte[] bytes = "foo\r\n\rbar".getBytes(Charsets.US_ASCII);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(5);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(6);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(9);
assertThat(parser.readLine(Charsets.US_ASCII)).isNull();
}
@Test
public void readFourLinesWithLfFollowedByCr_ascii() {
byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.US_ASCII);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(5);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(6);
assertThat(parser.readLine(Charsets.US_ASCII)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(11);
assertThat(parser.readLine(Charsets.US_ASCII)).isNull();
}
@Test
public void readSingleLineWithoutEndingTrail_utf8() {
byte[] bytes = "foo".getBytes(Charsets.UTF_8);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine()).isEqualTo("foo"); assertThat(parser.readLine()).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(3);
assertThat(parser.readLine()).isNull(); assertThat(parser.readLine()).isNull();
} }
@Test @Test
public void readSingleLineWithEndingLf() { public void readSingleLineWithEndingLf_utf8() {
byte[] bytes = new byte[] {'f', 'o', 'o', '\n'}; byte[] bytes = "foo\n".getBytes(Charsets.UTF_8);
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine()).isEqualTo("foo"); assertThat(parser.readLine()).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readLine()).isNull(); assertThat(parser.readLine()).isNull();
} }
@Test @Test
public void readTwoLinesWithCrFollowedByLf() { public void readTwoLinesWithCrFollowedByLf_utf8() {
byte[] bytes = new byte[] {'f', 'o', 'o', '\r', '\n', 'b', 'a', 'r'}; byte[] bytes = "foo\r\nbar".getBytes(Charsets.UTF_8);
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine()).isEqualTo("foo"); assertThat(parser.readLine()).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(5);
assertThat(parser.readLine()).isEqualTo("bar"); assertThat(parser.readLine()).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readLine()).isNull(); assertThat(parser.readLine()).isNull();
} }
@Test @Test
public void readThreeLinesWithEmptyLine() { public void readThreeLinesWithEmptyLineAndLeadingBom_utf8() {
byte[] bytes = new byte[] {'f', 'o', 'o', '\r', '\n', '\r', 'b', 'a', 'r'}; byte[] bytes =
Bytes.concat(createByteArray(0xEF, 0xBB, 0xBF), "foo\r\n\rbar".getBytes(Charsets.UTF_8));
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine()).isEqualTo("foo"); assertThat(parser.readLine()).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readLine()).isEqualTo(""); assertThat(parser.readLine()).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(9);
assertThat(parser.readLine()).isEqualTo("bar"); assertThat(parser.readLine()).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(12);
assertThat(parser.readLine()).isNull(); assertThat(parser.readLine()).isNull();
} }
@Test @Test
public void readFourLinesWithLfFollowedByCr() { public void readFourLinesWithLfFollowedByCr_utf8() {
byte[] bytes = new byte[] {'f', 'o', 'o', '\n', '\r', '\r', 'b', 'a', 'r', '\r', '\n'}; byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.UTF_8);
ParsableByteArray parser = new ParsableByteArray(bytes); ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine()).isEqualTo("foo"); assertThat(parser.readLine()).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(4);
assertThat(parser.readLine()).isEqualTo(""); assertThat(parser.readLine()).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(5);
assertThat(parser.readLine()).isEqualTo(""); assertThat(parser.readLine()).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(6);
assertThat(parser.readLine()).isEqualTo("bar"); assertThat(parser.readLine()).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(11);
assertThat(parser.readLine()).isNull(); assertThat(parser.readLine()).isNull();
} }
@Test
public void readSingleLineWithoutEndingTrail_utf16() {
// Use UTF_16BE because we don't want the leading BOM that's added by getBytes(UTF_16). We
// explicitly test with a BOM elsewhere.
byte[] bytes = "foo".getBytes(Charsets.UTF_16BE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(6);
assertThat(parser.readLine(Charsets.UTF_16)).isNull();
}
@Test
public void readSingleLineWithEndingLf_utf16() {
// Use UTF_16BE because we don't want the leading BOM that's added by getBytes(UTF_16). We
// explicitly test with a BOM elsewhere.
byte[] bytes = "foo\n".getBytes(Charsets.UTF_16BE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readLine(Charsets.UTF_16)).isNull();
}
@Test
public void readTwoLinesWithCrFollowedByLf_utf16() {
// Use UTF_16BE because we don't want the leading BOM that's added by getBytes(UTF_16). We
// explicitly test with a BOM elsewhere.
byte[] bytes = "foo\r\nbar".getBytes(Charsets.UTF_16BE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(10);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(16);
assertThat(parser.readLine(Charsets.UTF_16)).isNull();
}
@Test
public void readThreeLinesWithEmptyLineAndLeadingBom_utf16() {
// getBytes(UTF_16) always adds the leading BOM.
byte[] bytes = "foo\r\n\rbar".getBytes(Charsets.UTF_16);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(12);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(14);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(20);
assertThat(parser.readLine(Charsets.UTF_16)).isNull();
}
@Test
public void readFourLinesWithLfFollowedByCr_utf16() {
// Use UTF_16BE because we don't want the leading BOM that's added by getBytes(UTF_16). We
// explicitly test with a BOM elsewhere.
byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.UTF_16BE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(10);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(12);
assertThat(parser.readLine(Charsets.UTF_16)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(22);
assertThat(parser.readLine(Charsets.UTF_16)).isNull();
}
@Test
public void readSingleLineWithoutEndingTrail_utf16be() {
byte[] bytes = "foo".getBytes(Charsets.UTF_16BE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(6);
assertThat(parser.readLine(Charsets.UTF_16BE)).isNull();
}
@Test
public void readSingleLineWithEndingLf_utf16be() {
byte[] bytes = "foo\n".getBytes(Charsets.UTF_16BE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readLine(Charsets.UTF_16BE)).isNull();
}
@Test
public void readTwoLinesWithCrFollowedByLf_utf16be() {
byte[] bytes = "foo\r\nbar".getBytes(Charsets.UTF_16BE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(10);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(16);
assertThat(parser.readLine(Charsets.UTF_16BE)).isNull();
}
@Test
public void readThreeLinesWithEmptyLineAndLeadingBom_utf16be() {
byte[] bytes =
Bytes.concat(createByteArray(0xFE, 0xFF), "foo\r\n\rbar".getBytes(Charsets.UTF_16BE));
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(12);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(14);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(20);
assertThat(parser.readLine(Charsets.UTF_16BE)).isNull();
}
@Test
public void readFourLinesWithLfFollowedByCr_utf16be() {
byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.UTF_16BE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(10);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(12);
assertThat(parser.readLine(Charsets.UTF_16BE)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(22);
assertThat(parser.readLine(Charsets.UTF_16BE)).isNull();
}
@Test
public void readSingleLineWithoutEndingTrail_utf16le() {
byte[] bytes = "foo".getBytes(Charsets.UTF_16LE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(6);
assertThat(parser.readLine(Charsets.UTF_16LE)).isNull();
}
@Test
public void readSingleLineWithEndingLf_utf16le() {
byte[] bytes = "foo\n".getBytes(Charsets.UTF_16LE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readLine(Charsets.UTF_16LE)).isNull();
}
@Test
public void readTwoLinesWithCrFollowedByLf_utf16le() {
byte[] bytes = "foo\r\nbar".getBytes(Charsets.UTF_16LE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(10);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(16);
assertThat(parser.readLine(Charsets.UTF_16LE)).isNull();
}
@Test
public void readThreeLinesWithEmptyLineAndLeadingBom_utf16le() {
byte[] bytes =
Bytes.concat(createByteArray(0xFF, 0xFE), "foo\r\n\rbar".getBytes(Charsets.UTF_16LE));
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(12);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(14);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(20);
assertThat(parser.readLine(Charsets.UTF_16LE)).isNull();
}
@Test
public void readFourLinesWithLfFollowedByCr_utf16le() {
byte[] bytes = "foo\n\r\rbar\r\n".getBytes(Charsets.UTF_16LE);
ParsableByteArray parser = new ParsableByteArray(bytes);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("foo");
assertThat(parser.getPosition()).isEqualTo(8);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(10);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("");
assertThat(parser.getPosition()).isEqualTo(12);
assertThat(parser.readLine(Charsets.UTF_16LE)).isEqualTo("bar");
assertThat(parser.getPosition()).isEqualTo(22);
assertThat(parser.readLine(Charsets.UTF_16LE)).isNull();
}
} }

View File

@ -5,7 +5,6 @@ will not normally need to depend on this module directly.
## Links ## Links
* [Javadoc][]: Classes matching `androidx.media3.database.*` belong to this * [Javadoc][]
module.
[Javadoc]: https://exoplayer.dev/doc/reference/index.html [Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -3,3 +3,9 @@
Provides a `DataSource` abstraction and a number of concrete implementations for Provides a `DataSource` abstraction and a number of concrete implementations for
reading data from different sources. Application code will not normally need to reading data from different sources. Application code will not normally need to
depend on this module directly. depend on this module directly.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -119,3 +119,9 @@ whilst still using Cronet Fallback for other networking performed by your
application. application.
[Send a simple request]: https://developer.android.com/guide/topics/connectivity/cronet/start [Send a simple request]: https://developer.android.com/guide/topics/connectivity/cronet/start
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -48,3 +48,9 @@ new DefaultDataSourceFactory(
... ...
/* baseDataSourceFactory= */ new OkHttpDataSource.Factory(...)); /* baseDataSourceFactory= */ new OkHttpDataSource.Factory(...));
``` ```
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -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 doesn't need to handle any other protocols, you can update any
`DataSource.Factory` instantiations in your application code to use `DataSource.Factory` instantiations in your application code to use
`RtmpDataSource.Factory` directly. `RtmpDataSource.Factory` directly.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -2,3 +2,9 @@
Provides a decoder abstraction. Application code will not normally need to Provides a decoder abstraction. Application code will not normally need to
depend on this module directly. depend on this module directly.
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -123,3 +123,11 @@ gets from the libgav1 decoder:
Note: Although the default option uses `ANativeWindow`, based on our testing the Note: Although the default option uses `ANativeWindow`, based on our testing the
GL rendering mode has better performance, so should be preferred 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

View File

@ -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 [Android NDK]: https://developer.android.com/tools/sdk/ndk/index.html
[ExoPlayer issue 2781]: https://github.com/google/ExoPlayer/issues/2781 [ExoPlayer issue 2781]: https://github.com/google/ExoPlayer/issues/2781
[Supported formats]: https://exoplayer.dev/supported-formats.html#ffmpeg-extension [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

View File

@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
set -eu
FFMPEG_MODULE_PATH=$1 FFMPEG_MODULE_PATH=$1
NDK_PATH=$2 NDK_PATH=$2

View File

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

View File

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

View File

@ -15,7 +15,7 @@
# limitations under the License. # limitations under the License.
# #
set -e set -eu
ASM_CONVERTER="./libopus/celt/arm/arm2gnu.pl" ASM_CONVERTER="./libopus/celt/arm/arm2gnu.pl"
if [[ ! -x "${ASM_CONVERTER}" ]]; then if [[ ! -x "${ASM_CONVERTER}" ]]; then

View File

@ -136,3 +136,11 @@ gets from the libvpx decoder:
Note: Although the default option uses `ANativeWindow`, based on our testing the Note: Although the default option uses `ANativeWindow`, based on our testing the
GL rendering mode has better performance, so should be preferred. 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

View File

@ -18,7 +18,7 @@
# a bash script that generates the necessary config files for libvpx android ndk # a bash script that generates the necessary config files for libvpx android ndk
# builds. # builds.
set -e set -eu
if [ $# -ne 0 ]; then if [ $# -ne 0 ]; then
echo "Usage: ${0}" echo "Usage: ${0}"

View File

@ -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][]. locally. Instructions for doing this can be found in the [top level README][].
[top level README]: ../../README.md [top level README]: ../../README.md
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -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][]. locally. Instructions for doing this can be found in the [top level README][].
[top level README]: ../../README.md [top level README]: ../../README.md
## Links
* [Javadoc][]
[Javadoc]: https://developer.android.com/reference/androidx/media3/packages

View File

@ -250,17 +250,15 @@ public final class ExoPlaybackException extends PlaybackException {
private ExoPlaybackException(Bundle bundle) { private ExoPlaybackException(Bundle bundle) {
super(bundle); super(bundle);
type = bundle.getInt(keyForField(FIELD_TYPE), /* defaultValue= */ TYPE_UNEXPECTED); type = bundle.getInt(FIELD_TYPE, /* defaultValue= */ TYPE_UNEXPECTED);
rendererName = bundle.getString(keyForField(FIELD_RENDERER_NAME)); rendererName = bundle.getString(FIELD_RENDERER_NAME);
rendererIndex = rendererIndex = bundle.getInt(FIELD_RENDERER_INDEX, /* defaultValue= */ C.INDEX_UNSET);
bundle.getInt(keyForField(FIELD_RENDERER_INDEX), /* defaultValue= */ C.INDEX_UNSET); @Nullable Bundle rendererFormatBundle = bundle.getBundle(FIELD_RENDERER_FORMAT);
@Nullable Bundle rendererFormatBundle = bundle.getBundle(keyForField(FIELD_RENDERER_FORMAT));
rendererFormat = rendererFormat =
rendererFormatBundle == null ? null : Format.CREATOR.fromBundle(rendererFormatBundle); rendererFormatBundle == null ? null : Format.CREATOR.fromBundle(rendererFormatBundle);
rendererFormatSupport = rendererFormatSupport =
bundle.getInt( bundle.getInt(FIELD_RENDERER_FORMAT_SUPPORT, /* defaultValue= */ C.FORMAT_HANDLED);
keyForField(FIELD_RENDERER_FORMAT_SUPPORT), /* defaultValue= */ C.FORMAT_HANDLED); isRecoverable = bundle.getBoolean(FIELD_IS_RECOVERABLE, /* defaultValue= */ false);
isRecoverable = bundle.getBoolean(keyForField(FIELD_IS_RECOVERABLE), /* defaultValue= */ false);
mediaPeriodId = null; mediaPeriodId = null;
} }
@ -403,12 +401,17 @@ public final class ExoPlaybackException extends PlaybackException {
@UnstableApi @UnstableApi
public static final Creator<ExoPlaybackException> CREATOR = ExoPlaybackException::new; public static final Creator<ExoPlaybackException> CREATOR = ExoPlaybackException::new;
private static final int FIELD_TYPE = FIELD_CUSTOM_ID_BASE + 1; private static final String FIELD_TYPE = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 1);
private static final int FIELD_RENDERER_NAME = FIELD_CUSTOM_ID_BASE + 2; private static final String FIELD_RENDERER_NAME =
private static final int FIELD_RENDERER_INDEX = FIELD_CUSTOM_ID_BASE + 3; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 2);
private static final int FIELD_RENDERER_FORMAT = FIELD_CUSTOM_ID_BASE + 4; private static final String FIELD_RENDERER_INDEX =
private static final int FIELD_RENDERER_FORMAT_SUPPORT = FIELD_CUSTOM_ID_BASE + 5; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 3);
private static final int FIELD_IS_RECOVERABLE = FIELD_CUSTOM_ID_BASE + 6; 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} * {@inheritDoc}
@ -420,14 +423,14 @@ public final class ExoPlaybackException extends PlaybackException {
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = super.toBundle(); Bundle bundle = super.toBundle();
bundle.putInt(keyForField(FIELD_TYPE), type); bundle.putInt(FIELD_TYPE, type);
bundle.putString(keyForField(FIELD_RENDERER_NAME), rendererName); bundle.putString(FIELD_RENDERER_NAME, rendererName);
bundle.putInt(keyForField(FIELD_RENDERER_INDEX), rendererIndex); bundle.putInt(FIELD_RENDERER_INDEX, rendererIndex);
if (rendererFormat != null) { 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.putInt(FIELD_RENDERER_FORMAT_SUPPORT, rendererFormatSupport);
bundle.putBoolean(keyForField(FIELD_IS_RECOVERABLE), isRecoverable); bundle.putBoolean(FIELD_IS_RECOVERABLE, isRecoverable);
return bundle; return bundle;
} }
} }

View File

@ -24,6 +24,7 @@ import android.media.AudioDeviceInfo;
import android.media.AudioTrack; import android.media.AudioTrack;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.os.Looper; import android.os.Looper;
import android.os.Process;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
import android.view.SurfaceView; import android.view.SurfaceView;
@ -131,15 +132,15 @@ import java.util.List;
* threading model"> * threading model">
* *
* <ul> * <ul>
* <li>ExoPlayer instances must be accessed from a single application thread. For the vast * <li>ExoPlayer instances must be accessed from a single application thread unless indicated
* majority of cases this should be the application's main thread. Using the application's * otherwise. For the vast majority of cases this should be the application's main thread.
* main thread is also a requirement when using ExoPlayer's UI components or the IMA * Using the application's main thread is also a requirement when using ExoPlayer's UI
* extension. The thread on which an ExoPlayer instance must be accessed can be explicitly * components or the IMA extension. The thread on which an ExoPlayer instance must be accessed
* specified by passing a `Looper` when creating the player. If no `Looper` is specified, then * can be explicitly specified by passing a `Looper` when creating the player. If no `Looper`
* the `Looper` of the thread that the player is created on is used, or if that thread does * is specified, then the `Looper` of the thread that the player is created on is used, or if
* not have a `Looper`, the `Looper` of the application's main thread is used. In all cases * that thread does not have a `Looper`, the `Looper` of the application's main thread is
* the `Looper` of the thread from which the player must be accessed can be queried using * used. In all cases the `Looper` of the thread from which the player must be accessed can be
* {@link #getApplicationLooper()}. * queried using {@link #getApplicationLooper()}.
* <li>Registered listeners are called on the thread associated with {@link * <li>Registered listeners are called on the thread associated with {@link
* #getApplicationLooper()}. Note that this means registered listeners are called on the same * #getApplicationLooper()}. Note that this means registered listeners are called on the same
* thread which must be used to access the player. * thread which must be used to access the player.
@ -485,6 +486,7 @@ public interface ExoPlayer extends Player {
/* package */ long detachSurfaceTimeoutMs; /* package */ long detachSurfaceTimeoutMs;
/* package */ boolean pauseAtEndOfMediaItems; /* package */ boolean pauseAtEndOfMediaItems;
/* package */ boolean usePlatformDiagnostics; /* package */ boolean usePlatformDiagnostics;
@Nullable /* package */ Looper playbackLooper;
/* package */ boolean buildCalled; /* package */ boolean buildCalled;
/** /**
@ -527,6 +529,7 @@ public interface ExoPlayer extends Player {
* <li>{@code pauseAtEndOfMediaItems}: {@code false} * <li>{@code pauseAtEndOfMediaItems}: {@code false}
* <li>{@code usePlatformDiagnostics}: {@code true} * <li>{@code usePlatformDiagnostics}: {@code true}
* <li>{@link Clock}: {@link Clock#DEFAULT} * <li>{@link Clock}: {@link Clock#DEFAULT}
* <li>{@code playbackLooper}: {@code null} (create new thread)
* </ul> * </ul>
* *
* @param context A {@link Context}. * @param context A {@link Context}.
@ -1134,6 +1137,24 @@ public interface ExoPlayer extends Player {
return this; return this;
} }
/**
* 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. * Builds an {@link ExoPlayer} instance.
* *
@ -1208,6 +1229,8 @@ public interface ExoPlayer extends Player {
/** /**
* Adds a listener to receive audio offload events. * Adds a listener to receive audio offload events.
* *
* <p>This method can be called from any thread.
*
* @param listener The listener to register. * @param listener The listener to register.
*/ */
@UnstableApi @UnstableApi
@ -1228,6 +1251,8 @@ public interface ExoPlayer extends Player {
/** /**
* Adds an {@link AnalyticsListener} to receive analytics events. * Adds an {@link AnalyticsListener} to receive analytics events.
* *
* <p>This method can be called from any thread.
*
* @param listener The listener to be added. * @param listener The listener to be added.
*/ */
void addAnalyticsListener(AnalyticsListener listener); void addAnalyticsListener(AnalyticsListener listener);
@ -1293,11 +1318,19 @@ public interface ExoPlayer extends Player {
@Deprecated @Deprecated
TrackSelectionArray getCurrentTrackSelections(); 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 @UnstableApi
Looper getPlaybackLooper(); 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 @UnstableApi
Clock getClock(); Clock getClock();

View File

@ -18,6 +18,7 @@ package androidx.media3.exoplayer;
import static androidx.media3.common.C.TRACK_TYPE_AUDIO; import static androidx.media3.common.C.TRACK_TYPE_AUDIO;
import static androidx.media3.common.C.TRACK_TYPE_CAMERA_MOTION; import static androidx.media3.common.C.TRACK_TYPE_CAMERA_MOTION;
import static androidx.media3.common.C.TRACK_TYPE_VIDEO; import static androidx.media3.common.C.TRACK_TYPE_VIDEO;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull; import static androidx.media3.common.util.Util.castNonNull;
@ -76,7 +77,6 @@ import androidx.media3.common.Tracks;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue; import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup; import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.HandlerWrapper;
@ -88,6 +88,7 @@ import androidx.media3.exoplayer.PlayerMessage.Target;
import androidx.media3.exoplayer.Renderer.MessageType; import androidx.media3.exoplayer.Renderer.MessageType;
import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.analytics.AnalyticsListener; import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector;
import androidx.media3.exoplayer.analytics.MediaMetricsListener; import androidx.media3.exoplayer.analytics.MediaMetricsListener;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.audio.AudioRendererEventListener;
@ -345,7 +346,8 @@ import java.util.concurrent.TimeoutException;
applicationLooper, applicationLooper,
clock, clock,
playbackInfoUpdateListener, playbackInfoUpdateListener,
playerId); playerId,
builder.playbackLooper);
volume = 1; volume = 1;
repeatMode = Player.REPEAT_MODE_OFF; repeatMode = Player.REPEAT_MODE_OFF;
@ -478,7 +480,7 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public void removeAudioOffloadListener(AudioOffloadListener listener) { public void removeAudioOffloadListener(AudioOffloadListener listener) {
// Don't verify application thread. We allow calls to this method from any thread. verifyApplicationThread();
audioOffloadListeners.remove(listener); audioOffloadListeners.remove(listener);
} }
@ -621,7 +623,6 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public void addMediaItems(int index, List<MediaItem> mediaItems) { public void addMediaItems(int index, List<MediaItem> mediaItems) {
verifyApplicationThread(); verifyApplicationThread();
index = min(index, mediaSourceHolderSnapshots.size());
addMediaSources(index, createMediaSources(mediaItems)); addMediaSources(index, createMediaSources(mediaItems));
} }
@ -646,7 +647,8 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public void addMediaSources(int index, List<MediaSource> mediaSources) { public void addMediaSources(int index, List<MediaSource> mediaSources) {
verifyApplicationThread(); verifyApplicationThread();
Assertions.checkArgument(index >= 0); checkArgument(index >= 0);
index = min(index, mediaSourceHolderSnapshots.size());
Timeline oldTimeline = getCurrentTimeline(); Timeline oldTimeline = getCurrentTimeline();
pendingOperationAcks++; pendingOperationAcks++;
List<MediaSourceList.MediaSourceHolder> holders = addMediaSourceHolders(index, mediaSources); List<MediaSourceList.MediaSourceHolder> holders = addMediaSourceHolders(index, mediaSources);
@ -672,7 +674,13 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public void removeMediaItems(int fromIndex, int toIndex) { public void removeMediaItems(int fromIndex, int toIndex) {
verifyApplicationThread(); verifyApplicationThread();
toIndex = min(toIndex, mediaSourceHolderSnapshots.size()); checkArgument(fromIndex >= 0 && toIndex >= fromIndex);
int playlistSize = mediaSourceHolderSnapshots.size();
toIndex = min(toIndex, playlistSize);
if (fromIndex >= playlistSize || fromIndex == toIndex) {
// Do nothing.
return;
}
PlaybackInfo newPlaybackInfo = removeMediaItemsInternal(fromIndex, toIndex); PlaybackInfo newPlaybackInfo = removeMediaItemsInternal(fromIndex, toIndex);
boolean positionDiscontinuity = boolean positionDiscontinuity =
!newPlaybackInfo.periodId.periodUid.equals(playbackInfo.periodId.periodUid); !newPlaybackInfo.periodId.periodUid.equals(playbackInfo.periodId.periodUid);
@ -691,14 +699,16 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) { public void moveMediaItems(int fromIndex, int toIndex, int newFromIndex) {
verifyApplicationThread(); verifyApplicationThread();
Assertions.checkArgument( checkArgument(fromIndex >= 0 && fromIndex <= toIndex && newFromIndex >= 0);
fromIndex >= 0 int playlistSize = mediaSourceHolderSnapshots.size();
&& fromIndex <= toIndex toIndex = min(toIndex, playlistSize);
&& toIndex <= mediaSourceHolderSnapshots.size() newFromIndex = min(newFromIndex, playlistSize - (toIndex - fromIndex));
&& newFromIndex >= 0); if (fromIndex >= playlistSize || fromIndex == toIndex || fromIndex == newFromIndex) {
// Do nothing.
return;
}
Timeline oldTimeline = getCurrentTimeline(); Timeline oldTimeline = getCurrentTimeline();
pendingOperationAcks++; pendingOperationAcks++;
newFromIndex = min(newFromIndex, mediaSourceHolderSnapshots.size() - (toIndex - fromIndex));
Util.moveItems(mediaSourceHolderSnapshots, fromIndex, toIndex, newFromIndex); Util.moveItems(mediaSourceHolderSnapshots, fromIndex, toIndex, newFromIndex);
Timeline newTimeline = createMaskingTimeline(); Timeline newTimeline = createMaskingTimeline();
PlaybackInfo newPlaybackInfo = PlaybackInfo newPlaybackInfo =
@ -821,16 +831,51 @@ import java.util.concurrent.TimeoutException;
} }
@Override @Override
protected void repeatCurrentMediaItem() { public void seekTo(
int mediaItemIndex,
long positionMs,
@Player.Command int seekCommand,
boolean isRepeatingCurrentItem) {
verifyApplicationThread(); verifyApplicationThread();
seekToInternal( checkArgument(mediaItemIndex >= 0);
getCurrentMediaItemIndex(), /* positionMs= */ C.TIME_UNSET, /* repeatMediaItem= */ true); analyticsCollector.notifySeekStarted();
} Timeline timeline = playbackInfo.timeline;
if (!timeline.isEmpty() && mediaItemIndex >= timeline.getWindowCount()) {
@Override return;
public void seekTo(int mediaItemIndex, long positionMs) { }
verifyApplicationThread(); pendingOperationAcks++;
seekToInternal(mediaItemIndex, positionMs, /* repeatMediaItem= */ false); if (isPlayingAd()) {
// TODO: Investigate adding support for seeking during ads. This is complicated to do in
// general because the midroll ad preceding the seek destination must be played before the
// content position can be played, if a different ad is playing at the moment.
Log.w(TAG, "seekTo ignored because an ad is playing");
ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate =
new ExoPlayerImplInternal.PlaybackInfoUpdate(this.playbackInfo);
playbackInfoUpdate.incrementPendingOperationAcks(1);
playbackInfoUpdateListener.onPlaybackInfoUpdate(playbackInfoUpdate);
return;
}
@Player.State
int newPlaybackState =
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : STATE_BUFFERING;
int oldMaskingMediaItemIndex = getCurrentMediaItemIndex();
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState);
newPlaybackInfo =
maskTimelineAndPosition(
newPlaybackInfo,
timeline,
maskWindowPositionMsOrGetPeriodPositionUs(timeline, mediaItemIndex, positionMs));
internalPlayer.seekTo(timeline, mediaItemIndex, Util.msToUs(positionMs));
updatePlaybackInfo(
newPlaybackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ true,
/* positionDiscontinuity= */ true,
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
oldMaskingMediaItemIndex,
isRepeatingCurrentItem);
} }
@Override @Override
@ -1486,7 +1531,7 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public void removeAnalyticsListener(AnalyticsListener listener) { public void removeAnalyticsListener(AnalyticsListener listener) {
// Don't verify application thread. We allow calls to this method from any thread. verifyApplicationThread();
analyticsCollector.removeListener(checkNotNull(listener)); analyticsCollector.removeListener(checkNotNull(listener));
} }
@ -1603,9 +1648,8 @@ import java.util.concurrent.TimeoutException;
@Override @Override
public void removeListener(Listener listener) { public void removeListener(Listener listener) {
// Don't verify application thread. We allow calls to this method from any thread. verifyApplicationThread();
checkNotNull(listener); listeners.remove(checkNotNull(listener));
listeners.remove(listener);
} }
@Override @Override
@ -1688,8 +1732,14 @@ import java.util.concurrent.TimeoutException;
return false; return false;
} }
@SuppressWarnings("deprecation") // Calling deprecated methods.
/* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) { /* package */ void setThrowsWhenUsingWrongThread(boolean throwsWhenUsingWrongThread) {
this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread; this.throwsWhenUsingWrongThread = throwsWhenUsingWrongThread;
listeners.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread);
if (analyticsCollector instanceof DefaultAnalyticsCollector) {
((DefaultAnalyticsCollector) analyticsCollector)
.setThrowsWhenUsingWrongThread(throwsWhenUsingWrongThread);
}
} }
/** /**
@ -2219,8 +2269,6 @@ import java.util.concurrent.TimeoutException;
} }
private PlaybackInfo removeMediaItemsInternal(int fromIndex, int toIndex) { private PlaybackInfo removeMediaItemsInternal(int fromIndex, int toIndex) {
Assertions.checkArgument(
fromIndex >= 0 && toIndex >= fromIndex && toIndex <= mediaSourceHolderSnapshots.size());
int currentIndex = getCurrentMediaItemIndex(); int currentIndex = getCurrentMediaItemIndex();
Timeline oldTimeline = getCurrentTimeline(); Timeline oldTimeline = getCurrentTimeline();
int currentMediaSourceCount = mediaSourceHolderSnapshots.size(); int currentMediaSourceCount = mediaSourceHolderSnapshots.size();
@ -2259,7 +2307,7 @@ import java.util.concurrent.TimeoutException;
private PlaybackInfo maskTimelineAndPosition( private PlaybackInfo maskTimelineAndPosition(
PlaybackInfo playbackInfo, Timeline timeline, @Nullable Pair<Object, Long> periodPositionUs) { PlaybackInfo playbackInfo, Timeline timeline, @Nullable Pair<Object, Long> periodPositionUs) {
Assertions.checkArgument(timeline.isEmpty() || periodPositionUs != null); checkArgument(timeline.isEmpty() || periodPositionUs != null);
Timeline oldTimeline = playbackInfo.timeline; Timeline oldTimeline = playbackInfo.timeline;
// Mask the timeline. // Mask the timeline.
playbackInfo = playbackInfo.copyWithTimeline(timeline); playbackInfo = playbackInfo.copyWithTimeline(timeline);
@ -2689,48 +2737,6 @@ import java.util.concurrent.TimeoutException;
} }
} }
private void seekToInternal(int mediaItemIndex, long positionMs, boolean repeatMediaItem) {
analyticsCollector.notifySeekStarted();
Timeline timeline = playbackInfo.timeline;
if (mediaItemIndex < 0
|| (!timeline.isEmpty() && mediaItemIndex >= timeline.getWindowCount())) {
throw new IllegalSeekPositionException(timeline, mediaItemIndex, positionMs);
}
pendingOperationAcks++;
if (isPlayingAd()) {
// TODO: Investigate adding support for seeking during ads. This is complicated to do in
// general because the midroll ad preceding the seek destination must be played before the
// content position can be played, if a different ad is playing at the moment.
Log.w(TAG, "seekTo ignored because an ad is playing");
ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfoUpdate =
new ExoPlayerImplInternal.PlaybackInfoUpdate(this.playbackInfo);
playbackInfoUpdate.incrementPendingOperationAcks(1);
playbackInfoUpdateListener.onPlaybackInfoUpdate(playbackInfoUpdate);
return;
}
@Player.State
int newPlaybackState =
getPlaybackState() == Player.STATE_IDLE ? Player.STATE_IDLE : STATE_BUFFERING;
int oldMaskingMediaItemIndex = getCurrentMediaItemIndex();
PlaybackInfo newPlaybackInfo = playbackInfo.copyWithPlaybackState(newPlaybackState);
newPlaybackInfo =
maskTimelineAndPosition(
newPlaybackInfo,
timeline,
maskWindowPositionMsOrGetPeriodPositionUs(timeline, mediaItemIndex, positionMs));
internalPlayer.seekTo(timeline, mediaItemIndex, Util.msToUs(positionMs));
updatePlaybackInfo(
newPlaybackInfo,
/* ignored */ TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED,
/* ignored */ PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST,
/* seekProcessed= */ true,
/* positionDiscontinuity= */ true,
/* positionDiscontinuityReason= */ DISCONTINUITY_REASON_SEEK,
/* discontinuityWindowStartPositionUs= */ getCurrentPositionUsInternal(newPlaybackInfo),
oldMaskingMediaItemIndex,
repeatMediaItem);
}
private static DeviceInfo createDeviceInfo(StreamVolumeManager streamVolumeManager) { private static DeviceInfo createDeviceInfo(StreamVolumeManager streamVolumeManager) {
return new DeviceInfo( return new DeviceInfo(
DeviceInfo.PLAYBACK_TYPE_LOCAL, DeviceInfo.PLAYBACK_TYPE_LOCAL,

View File

@ -189,7 +189,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
private final LoadControl loadControl; private final LoadControl loadControl;
private final BandwidthMeter bandwidthMeter; private final BandwidthMeter bandwidthMeter;
private final HandlerWrapper handler; private final HandlerWrapper handler;
private final HandlerThread internalPlaybackThread; @Nullable private final HandlerThread internalPlaybackThread;
private final Looper playbackLooper; private final Looper playbackLooper;
private final Timeline.Window window; private final Timeline.Window window;
private final Timeline.Period period; private final Timeline.Period period;
@ -244,7 +244,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
Looper applicationLooper, Looper applicationLooper,
Clock clock, Clock clock,
PlaybackInfoUpdateListener playbackInfoUpdateListener, PlaybackInfoUpdateListener playbackInfoUpdateListener,
PlayerId playerId) { PlayerId playerId,
Looper playbackLooper) {
this.playbackInfoUpdateListener = playbackInfoUpdateListener; this.playbackInfoUpdateListener = playbackInfoUpdateListener;
this.renderers = renderers; this.renderers = renderers;
this.trackSelector = trackSelector; this.trackSelector = trackSelector;
@ -280,17 +281,23 @@ import java.util.concurrent.atomic.AtomicBoolean;
deliverPendingMessageAtStartPositionRequired = true; deliverPendingMessageAtStartPositionRequired = true;
Handler eventHandler = new Handler(applicationLooper); HandlerWrapper eventHandler = clock.createHandler(applicationLooper, /* callback= */ null);
queue = new MediaPeriodQueue(analyticsCollector, eventHandler); queue = new MediaPeriodQueue(analyticsCollector, eventHandler);
mediaSourceList = mediaSourceList =
new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId); new MediaSourceList(/* listener= */ this, analyticsCollector, eventHandler, playerId);
// Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can if (playbackLooper != null) {
// not normally change to this priority" is incorrect. internalPlaybackThread = null;
internalPlaybackThread = new HandlerThread("ExoPlayer:Playback", Process.THREAD_PRIORITY_AUDIO); this.playbackLooper = playbackLooper;
internalPlaybackThread.start(); } else {
playbackLooper = internalPlaybackThread.getLooper(); // Note: The documentation for Process.THREAD_PRIORITY_AUDIO that states "Applications can
handler = clock.createHandler(playbackLooper, this); // 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) { public void experimentalSetForegroundModeTimeoutMs(long setForegroundModeTimeoutMs) {
@ -393,7 +400,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
@Override @Override
public synchronized void sendMessage(PlayerMessage message) { public synchronized void sendMessage(PlayerMessage message) {
if (released || !internalPlaybackThread.isAlive()) { if (released || !playbackLooper.getThread().isAlive()) {
Log.w(TAG, "Ignoring messages sent after release."); Log.w(TAG, "Ignoring messages sent after release.");
message.markAsProcessed(/* isDelivered= */ false); message.markAsProcessed(/* isDelivered= */ false);
return; return;
@ -408,7 +415,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @return Whether the operations succeeded. If false, the operation timed out. * @return Whether the operations succeeded. If false, the operation timed out.
*/ */
public synchronized boolean setForegroundMode(boolean foregroundMode) { public synchronized boolean setForegroundMode(boolean foregroundMode) {
if (released || !internalPlaybackThread.isAlive()) { if (released || !playbackLooper.getThread().isAlive()) {
return true; return true;
} }
if (foregroundMode) { if (foregroundMode) {
@ -430,7 +437,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @return Whether the release succeeded. If false, the release timed out. * @return Whether the release succeeded. If false, the release timed out.
*/ */
public synchronized boolean release() { public synchronized boolean release() {
if (released || !internalPlaybackThread.isAlive()) { if (released || !playbackLooper.getThread().isAlive()) {
return true; return true;
} }
handler.sendEmptyMessage(MSG_RELEASE); handler.sendEmptyMessage(MSG_RELEASE);
@ -1382,7 +1389,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
/* resetError= */ false); /* resetError= */ false);
loadControl.onReleased(); loadControl.onReleased();
setState(Player.STATE_IDLE); setState(Player.STATE_IDLE);
internalPlaybackThread.quit(); if (internalPlaybackThread != null) {
internalPlaybackThread.quit();
}
synchronized (this) { synchronized (this) {
released = true; released = true;
notifyAll(); notifyAll();

View File

@ -26,6 +26,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Player.RepeatMode; import androidx.media3.common.Player.RepeatMode;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.exoplayer.analytics.AnalyticsCollector; import androidx.media3.exoplayer.analytics.AnalyticsCollector;
import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaPeriod;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; 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.Period period;
private final Timeline.Window window; private final Timeline.Window window;
private final AnalyticsCollector analyticsCollector; private final AnalyticsCollector analyticsCollector;
private final Handler analyticsCollectorHandler; private final HandlerWrapper analyticsCollectorHandler;
private long nextWindowSequenceNumber; private long nextWindowSequenceNumber;
private @RepeatMode int repeatMode; private @RepeatMode int repeatMode;
@ -91,7 +92,7 @@ import com.google.common.collect.ImmutableList;
* on. * on.
*/ */
public MediaPeriodQueue( public MediaPeriodQueue(
AnalyticsCollector analyticsCollector, Handler analyticsCollectorHandler) { AnalyticsCollector analyticsCollector, HandlerWrapper analyticsCollectorHandler) {
this.analyticsCollector = analyticsCollector; this.analyticsCollector = analyticsCollector;
this.analyticsCollectorHandler = analyticsCollectorHandler; this.analyticsCollectorHandler = analyticsCollectorHandler;
period = new Timeline.Period(); period = new Timeline.Period();

View File

@ -15,13 +15,16 @@
*/ */
package androidx.media3.exoplayer; package androidx.media3.exoplayer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
import android.os.Handler; import android.os.Handler;
import android.util.Pair;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener; import androidx.media3.datasource.TransferListener;
@ -48,6 +51,7 @@ import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import org.checkerframework.checker.nullness.compatqual.NullableType;
/** /**
* Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified * Concatenates multiple {@link MediaSource}s. The list of {@link MediaSource}s can be modified
@ -77,11 +81,10 @@ import java.util.Set;
private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod; private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
private final Map<Object, MediaSourceHolder> mediaSourceByUid; private final Map<Object, MediaSourceHolder> mediaSourceByUid;
private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener; private final MediaSourceListInfoRefreshListener mediaSourceListInfoListener;
private final MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
private final HashMap<MediaSourceList.MediaSourceHolder, MediaSourceAndListener> childSources; private final HashMap<MediaSourceList.MediaSourceHolder, MediaSourceAndListener> childSources;
private final Set<MediaSourceHolder> enabledMediaSourceHolders; private final Set<MediaSourceHolder> enabledMediaSourceHolders;
private final AnalyticsCollector eventListener;
private final HandlerWrapper eventHandler;
private ShuffleOrder shuffleOrder; private ShuffleOrder shuffleOrder;
private boolean isPrepared; private boolean isPrepared;
@ -101,7 +104,7 @@ import java.util.Set;
public MediaSourceList( public MediaSourceList(
MediaSourceListInfoRefreshListener listener, MediaSourceListInfoRefreshListener listener,
AnalyticsCollector analyticsCollector, AnalyticsCollector analyticsCollector,
Handler analyticsCollectorHandler, HandlerWrapper analyticsCollectorHandler,
PlayerId playerId) { PlayerId playerId) {
this.playerId = playerId; this.playerId = playerId;
mediaSourceListInfoListener = listener; mediaSourceListInfoListener = listener;
@ -109,12 +112,10 @@ import java.util.Set;
mediaSourceByMediaPeriod = new IdentityHashMap<>(); mediaSourceByMediaPeriod = new IdentityHashMap<>();
mediaSourceByUid = new HashMap<>(); mediaSourceByUid = new HashMap<>();
mediaSourceHolders = new ArrayList<>(); mediaSourceHolders = new ArrayList<>();
mediaSourceEventDispatcher = new MediaSourceEventListener.EventDispatcher(); eventListener = analyticsCollector;
drmEventDispatcher = new DrmSessionEventListener.EventDispatcher(); eventHandler = analyticsCollectorHandler;
childSources = new HashMap<>(); childSources = new HashMap<>();
enabledMediaSourceHolders = new HashSet<>(); enabledMediaSourceHolders = new HashSet<>();
mediaSourceEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
drmEventDispatcher.addEventListener(analyticsCollectorHandler, analyticsCollector);
} }
/** /**
@ -308,7 +309,7 @@ import java.util.Set;
Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid); Object mediaSourceHolderUid = getMediaSourceHolderUid(id.periodUid);
MediaSource.MediaPeriodId childMediaPeriodId = MediaSource.MediaPeriodId childMediaPeriodId =
id.copyWithPeriodUid(getChildPeriodUid(id.periodUid)); id.copyWithPeriodUid(getChildPeriodUid(id.periodUid));
MediaSourceHolder holder = Assertions.checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid)); MediaSourceHolder holder = checkNotNull(mediaSourceByUid.get(mediaSourceHolderUid));
enableMediaSource(holder); enableMediaSource(holder);
holder.activeMediaPeriodIds.add(childMediaPeriodId); holder.activeMediaPeriodIds.add(childMediaPeriodId);
MediaPeriod mediaPeriod = MediaPeriod mediaPeriod =
@ -324,8 +325,7 @@ import java.util.Set;
* @param mediaPeriod The period to release. * @param mediaPeriod The period to release.
*/ */
public void releasePeriod(MediaPeriod mediaPeriod) { public void releasePeriod(MediaPeriod mediaPeriod) {
MediaSourceHolder holder = MediaSourceHolder holder = checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
Assertions.checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
holder.mediaSource.releasePeriod(mediaPeriod); holder.mediaSource.releasePeriod(mediaPeriod);
holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id); holder.activeMediaPeriodIds.remove(((MaskingMediaPeriod) mediaPeriod).id);
if (!mediaSourceByMediaPeriod.isEmpty()) { if (!mediaSourceByMediaPeriod.isEmpty()) {
@ -450,8 +450,7 @@ import java.util.Set;
private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) { private void maybeReleaseChildSource(MediaSourceHolder mediaSourceHolder) {
// Release if the source has been removed from the playlist and no periods are still active. // Release if the source has been removed from the playlist and no periods are still active.
if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) { if (mediaSourceHolder.isRemoved && mediaSourceHolder.activeMediaPeriodIds.isEmpty()) {
MediaSourceAndListener removedChild = MediaSourceAndListener removedChild = checkNotNull(childSources.remove(mediaSourceHolder));
Assertions.checkNotNull(childSources.remove(mediaSourceHolder));
removedChild.mediaSource.releaseSource(removedChild.caller); removedChild.mediaSource.releaseSource(removedChild.caller);
removedChild.mediaSource.removeEventListener(removedChild.eventListener); removedChild.mediaSource.removeEventListener(removedChild.eventListener);
removedChild.mediaSource.removeDrmEventListener(removedChild.eventListener); removedChild.mediaSource.removeDrmEventListener(removedChild.eventListener);
@ -526,12 +525,8 @@ import java.util.Set;
implements MediaSourceEventListener, DrmSessionEventListener { implements MediaSourceEventListener, DrmSessionEventListener {
private final MediaSourceList.MediaSourceHolder id; private final MediaSourceList.MediaSourceHolder id;
private MediaSourceEventListener.EventDispatcher mediaSourceEventDispatcher;
private DrmSessionEventListener.EventDispatcher drmEventDispatcher;
public ForwardingEventListener(MediaSourceList.MediaSourceHolder id) { public ForwardingEventListener(MediaSourceList.MediaSourceHolder id) {
mediaSourceEventDispatcher = MediaSourceList.this.mediaSourceEventDispatcher;
drmEventDispatcher = MediaSourceList.this.drmEventDispatcher;
this.id = id; this.id = id;
} }
@ -543,8 +538,14 @@ import java.util.Set;
@Nullable MediaSource.MediaPeriodId mediaPeriodId, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData, LoadEventInfo loadEventData,
MediaLoadData mediaLoadData) { MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
mediaSourceEventDispatcher.loadStarted(loadEventData, mediaLoadData); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onLoadStarted(
eventParameters.first, eventParameters.second, loadEventData, mediaLoadData));
} }
} }
@ -554,8 +555,14 @@ import java.util.Set;
@Nullable MediaSource.MediaPeriodId mediaPeriodId, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData, LoadEventInfo loadEventData,
MediaLoadData mediaLoadData) { MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
mediaSourceEventDispatcher.loadCompleted(loadEventData, mediaLoadData); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onLoadCompleted(
eventParameters.first, eventParameters.second, loadEventData, mediaLoadData));
} }
} }
@ -565,8 +572,14 @@ import java.util.Set;
@Nullable MediaSource.MediaPeriodId mediaPeriodId, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
LoadEventInfo loadEventData, LoadEventInfo loadEventData,
MediaLoadData mediaLoadData) { MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
mediaSourceEventDispatcher.loadCanceled(loadEventData, mediaLoadData); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onLoadCanceled(
eventParameters.first, eventParameters.second, loadEventData, mediaLoadData));
} }
} }
@ -578,8 +591,19 @@ import java.util.Set;
MediaLoadData mediaLoadData, MediaLoadData mediaLoadData,
IOException error, IOException error,
boolean wasCanceled) { boolean wasCanceled) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
mediaSourceEventDispatcher.loadError(loadEventData, mediaLoadData, error, wasCanceled); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onLoadError(
eventParameters.first,
eventParameters.second,
loadEventData,
mediaLoadData,
error,
wasCanceled));
} }
} }
@ -588,8 +612,14 @@ import java.util.Set;
int windowIndex, int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
MediaLoadData mediaLoadData) { MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
mediaSourceEventDispatcher.upstreamDiscarded(mediaLoadData); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onUpstreamDiscarded(
eventParameters.first, checkNotNull(eventParameters.second), mediaLoadData));
} }
} }
@ -598,8 +628,14 @@ import java.util.Set;
int windowIndex, int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
MediaLoadData mediaLoadData) { MediaLoadData mediaLoadData) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
mediaSourceEventDispatcher.downstreamFormatChanged(mediaLoadData); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onDownstreamFormatChanged(
eventParameters.first, eventParameters.second, mediaLoadData));
} }
} }
@ -610,75 +646,94 @@ import java.util.Set;
int windowIndex, int windowIndex,
@Nullable MediaSource.MediaPeriodId mediaPeriodId, @Nullable MediaSource.MediaPeriodId mediaPeriodId,
@DrmSession.State int state) { @DrmSession.State int state) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
drmEventDispatcher.drmSessionAcquired(state); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onDrmSessionAcquired(
eventParameters.first, eventParameters.second, state));
} }
} }
@Override @Override
public void onDrmKeysLoaded( public void onDrmKeysLoaded(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
drmEventDispatcher.drmKeysLoaded(); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() -> eventListener.onDrmKeysLoaded(eventParameters.first, eventParameters.second));
} }
} }
@Override @Override
public void onDrmSessionManagerError( public void onDrmSessionManagerError(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, Exception error) { int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId, Exception error) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
drmEventDispatcher.drmSessionManagerError(error); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onDrmSessionManagerError(
eventParameters.first, eventParameters.second, error));
} }
} }
@Override @Override
public void onDrmKeysRestored( public void onDrmKeysRestored(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
drmEventDispatcher.drmKeysRestored(); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() -> eventListener.onDrmKeysRestored(eventParameters.first, eventParameters.second));
} }
} }
@Override @Override
public void onDrmKeysRemoved( public void onDrmKeysRemoved(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
drmEventDispatcher.drmKeysRemoved(); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() -> eventListener.onDrmKeysRemoved(eventParameters.first, eventParameters.second));
} }
} }
@Override @Override
public void onDrmSessionReleased( public void onDrmSessionReleased(
int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) { int windowIndex, @Nullable MediaSource.MediaPeriodId mediaPeriodId) {
if (maybeUpdateEventDispatcher(windowIndex, mediaPeriodId)) { @Nullable
drmEventDispatcher.drmSessionReleased(); Pair<Integer, MediaSource.@NullableType MediaPeriodId> eventParameters =
getEventParameters(windowIndex, mediaPeriodId);
if (eventParameters != null) {
eventHandler.post(
() ->
eventListener.onDrmSessionReleased(eventParameters.first, eventParameters.second));
} }
} }
/** Updates the event dispatcher and returns whether the event should be dispatched. */ /** Updates the event parameters and returns whether the event should be dispatched. */
private boolean maybeUpdateEventDispatcher( @Nullable
private Pair<Integer, MediaSource.@NullableType MediaPeriodId> getEventParameters(
int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) { int childWindowIndex, @Nullable MediaSource.MediaPeriodId childMediaPeriodId) {
@Nullable MediaSource.MediaPeriodId mediaPeriodId = null; @Nullable MediaSource.MediaPeriodId mediaPeriodId = null;
if (childMediaPeriodId != null) { if (childMediaPeriodId != null) {
mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId); mediaPeriodId = getMediaPeriodIdForChildMediaPeriodId(id, childMediaPeriodId);
if (mediaPeriodId == null) { if (mediaPeriodId == null) {
// Media period not found. Ignore event. // Media period not found. Ignore event.
return false; return null;
} }
} }
int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex); int windowIndex = getWindowIndexForChildWindowIndex(id, childWindowIndex);
if (mediaSourceEventDispatcher.windowIndex != windowIndex return Pair.create(windowIndex, mediaPeriodId);
|| !Util.areEqual(mediaSourceEventDispatcher.mediaPeriodId, mediaPeriodId)) {
mediaSourceEventDispatcher =
MediaSourceList.this.mediaSourceEventDispatcher.withParameters(
windowIndex, mediaPeriodId, /* mediaTimeOffsetMs= */ 0L);
}
if (drmEventDispatcher.windowIndex != windowIndex
|| !Util.areEqual(drmEventDispatcher.mediaPeriodId, mediaPeriodId)) {
drmEventDispatcher =
MediaSourceList.this.drmEventDispatcher.withParameters(windowIndex, mediaPeriodId);
}
return true;
} }
} }
} }

View File

@ -144,13 +144,13 @@ public interface RendererCapabilities {
/** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */ /** A mask to apply to {@link Capabilities} to obtain {@link DecoderSupport} only. */
int MODE_SUPPORT_MASK = 0b11 << 7; int MODE_SUPPORT_MASK = 0b11 << 7;
/** /**
* The renderer will use a decoder for fallback mimetype if possible as format's MIME type is * The format's MIME type is unsupported and the renderer may use a decoder for a fallback MIME
* unsupported * type.
*/ */
int DECODER_SUPPORT_FALLBACK_MIMETYPE = 0b10 << 7; int DECODER_SUPPORT_FALLBACK_MIMETYPE = 0b10 << 7;
/** The renderer is able to use the primary decoder for the format's MIME type. */ /** The renderer is able to use the primary decoder for the format's MIME type. */
int DECODER_SUPPORT_PRIMARY = 0b1 << 7; 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; int DECODER_SUPPORT_FALLBACK = 0;
/** /**

View File

@ -15,6 +15,8 @@
*/ */
package androidx.media3.exoplayer; package androidx.media3.exoplayer;
import static androidx.annotation.VisibleForTesting.PROTECTED;
import android.content.Context; import android.content.Context;
import android.media.AudioDeviceInfo; import android.media.AudioDeviceInfo;
import android.os.Looper; import android.os.Looper;
@ -1004,10 +1006,16 @@ public class SimpleExoPlayer extends BasePlayer
return player.isLoading(); return player.isLoading();
} }
@SuppressWarnings("ForOverride") // Forwarding to ForOverride method in ExoPlayerImpl.
@Override @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(); blockUntilConstructorFinished();
player.seekTo(mediaItemIndex, positionMs); player.seekTo(mediaItemIndex, positionMs, seekCommand, isRepeatingCurrentItem);
} }
@Override @Override

View File

@ -96,6 +96,20 @@ public class DefaultAnalyticsCollector implements AnalyticsCollector {
eventTimes = new SparseArray<>(); 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 @Override
@CallSuper @CallSuper
public void addListener(AnalyticsListener listener) { public void addListener(AnalyticsListener listener) {

View File

@ -60,6 +60,7 @@ import androidx.media3.extractor.Ac3Util;
import androidx.media3.extractor.Ac4Util; import androidx.media3.extractor.Ac4Util;
import androidx.media3.extractor.DtsUtil; import androidx.media3.extractor.DtsUtil;
import androidx.media3.extractor.MpegAudioUtil; import androidx.media3.extractor.MpegAudioUtil;
import androidx.media3.extractor.OpusUtil;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe; import com.google.errorprone.annotations.InlineMe;
import com.google.errorprone.annotations.InlineMeValidationDisabled; 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, * @param pcmFrameSize The size of the PCM frames if the {@code encoding} is PCM, 1 otherwise,
* in bytes. * in bytes.
* @param sampleRate The sample rate of the format, in Hz. * @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 * @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 * 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 * forward, etc. This will be {@code 1} unless {@link
@ -217,6 +220,7 @@ public final class DefaultAudioSink implements AudioSink {
@OutputMode int outputMode, @OutputMode int outputMode,
int pcmFrameSize, int pcmFrameSize,
int sampleRate, int sampleRate,
int bitrate,
double maxAudioTrackPlaybackSpeed); double maxAudioTrackPlaybackSpeed);
} }
@ -788,8 +792,9 @@ public final class DefaultAudioSink implements AudioSink {
getAudioTrackMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding), getAudioTrackMinBufferSize(outputSampleRate, outputChannelConfig, outputEncoding),
outputEncoding, outputEncoding,
outputMode, outputMode,
outputPcmFrameSize, outputPcmFrameSize != C.LENGTH_UNSET ? outputPcmFrameSize : 1,
outputSampleRate, outputSampleRate,
inputFormat.bitrate,
enableAudioTrackPlaybackParams ? MAX_PLAYBACK_SPEED : DEFAULT_PLAYBACK_SPEED); enableAudioTrackPlaybackParams ? MAX_PLAYBACK_SPEED : DEFAULT_PLAYBACK_SPEED);
offloadDisabledUntilNextConfiguration = false; offloadDisabledUntilNextConfiguration = false;
@ -1000,9 +1005,11 @@ public final class DefaultAudioSink implements AudioSink {
getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount()); getSubmittedFrames() - trimmingAudioProcessor.getTrimmedFrameCount());
if (!startMediaTimeUsNeedsSync if (!startMediaTimeUsNeedsSync
&& Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) { && Math.abs(expectedPresentationTimeUs - presentationTimeUs) > 200000) {
listener.onAudioSinkError( if (listener != null) {
new AudioSink.UnexpectedDiscontinuityException( listener.onAudioSinkError(
presentationTimeUs, expectedPresentationTimeUs)); new AudioSink.UnexpectedDiscontinuityException(
presentationTimeUs, expectedPresentationTimeUs));
}
startMediaTimeUsNeedsSync = true; startMediaTimeUsNeedsSync = true;
} }
if (startMediaTimeUsNeedsSync) { if (startMediaTimeUsNeedsSync) {
@ -1785,6 +1792,8 @@ public final class DefaultAudioSink implements AudioSink {
? 0 ? 0
: (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset) : (Ac3Util.parseTrueHdSyncframeAudioSampleCount(buffer, syncframeOffset)
* Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT); * Ac3Util.TRUEHD_RECHUNK_SAMPLE_COUNT);
case C.ENCODING_OPUS:
return OpusUtil.parsePacketAudioSampleCount(buffer);
case C.ENCODING_PCM_16BIT: case C.ENCODING_PCM_16BIT:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN: case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_24BIT:

View File

@ -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_OFFLOAD;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH; import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PASSTHROUGH;
import static androidx.media3.exoplayer.audio.DefaultAudioSink.OUTPUT_MODE_PCM; 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 com.google.common.primitives.Ints.checkedCast;
import static java.lang.Math.max; import static java.lang.Math.max;
@ -32,7 +33,9 @@ import androidx.media3.extractor.Ac3Util;
import androidx.media3.extractor.Ac4Util; import androidx.media3.extractor.Ac4Util;
import androidx.media3.extractor.DtsUtil; import androidx.media3.extractor.DtsUtil;
import androidx.media3.extractor.MpegAudioUtil; import androidx.media3.extractor.MpegAudioUtil;
import androidx.media3.extractor.OpusUtil;
import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.math.RoundingMode;
/** Provide the buffer size to use when creating an {@link AudioTrack}. */ /** Provide the buffer size to use when creating an {@link AudioTrack}. */
@UnstableApi @UnstableApi
@ -173,10 +176,11 @@ public class DefaultAudioTrackBufferSizeProvider
@OutputMode int outputMode, @OutputMode int outputMode,
int pcmFrameSize, int pcmFrameSize,
int sampleRate, int sampleRate,
int bitrate,
double maxAudioTrackPlaybackSpeed) { double maxAudioTrackPlaybackSpeed) {
int bufferSize = int bufferSize =
get1xBufferSizeInBytes( get1xBufferSizeInBytes(
minBufferSizeInBytes, encoding, outputMode, pcmFrameSize, sampleRate); minBufferSizeInBytes, encoding, outputMode, pcmFrameSize, sampleRate, bitrate);
// Maintain the buffer duration by scaling the size accordingly. // Maintain the buffer duration by scaling the size accordingly.
bufferSize = (int) (bufferSize * maxAudioTrackPlaybackSpeed); bufferSize = (int) (bufferSize * maxAudioTrackPlaybackSpeed);
// Buffer size must not be lower than the AudioTrack min buffer size for this format. // 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. */ /** Returns the buffer size for playback at 1x speed. */
protected int get1xBufferSizeInBytes( 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) { switch (outputMode) {
case OUTPUT_MODE_PCM: case OUTPUT_MODE_PCM:
return getPcmBufferSizeInBytes(minBufferSizeInBytes, sampleRate, pcmFrameSize); return getPcmBufferSizeInBytes(minBufferSizeInBytes, sampleRate, pcmFrameSize);
case OUTPUT_MODE_PASSTHROUGH: case OUTPUT_MODE_PASSTHROUGH:
return getPassthroughBufferSizeInBytes(encoding); return getPassthroughBufferSizeInBytes(encoding, bitrate);
case OUTPUT_MODE_OFFLOAD: case OUTPUT_MODE_OFFLOAD:
return getOffloadBufferSizeInBytes(encoding); return getOffloadBufferSizeInBytes(encoding);
default: default:
@ -209,13 +218,16 @@ public class DefaultAudioTrackBufferSizeProvider
} }
/** Returns the buffer size for passthrough playback. */ /** 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; int bufferSizeUs = passthroughBufferDurationUs;
if (encoding == C.ENCODING_AC3) { if (encoding == C.ENCODING_AC3) {
bufferSizeUs *= ac3BufferMultiplicationFactor; bufferSizeUs *= ac3BufferMultiplicationFactor;
} }
int maxByteRate = getMaximumEncodedRateBytesPerSecond(encoding); int byteRate =
return checkedCast((long) bufferSizeUs * maxByteRate / C.MICROS_PER_SECOND); 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. */ /** Returns the buffer size for offload playback. */
@ -255,6 +267,8 @@ public class DefaultAudioTrackBufferSizeProvider
return DtsUtil.DTS_HD_MAX_RATE_BYTES_PER_SECOND; return DtsUtil.DTS_HD_MAX_RATE_BYTES_PER_SECOND;
case C.ENCODING_DOLBY_TRUEHD: case C.ENCODING_DOLBY_TRUEHD:
return Ac3Util.TRUEHD_MAX_RATE_BYTES_PER_SECOND; 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:
case C.ENCODING_PCM_16BIT_BIG_ENDIAN: case C.ENCODING_PCM_16BIT_BIG_ENDIAN:
case C.ENCODING_PCM_24BIT: case C.ENCODING_PCM_24BIT:

View File

@ -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. * @param format The input media format.
* @return Whether the decoder may support decoding the given {@code format}. * @return Whether the decoder may support decoding the given {@code format}.
@ -256,7 +257,7 @@ public final class MediaCodecInfo {
return false; return false;
} }
if (!isCodecProfileAndLevelSupported(format)) { if (!isCodecProfileAndLevelSupported(format, /* checkPerformanceCapabilities= */ true)) {
return false; 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) { private boolean isSampleMimeTypeSupported(Format format) {
return mimeType.equals(format.sampleMimeType) return mimeType.equals(format.sampleMimeType)
|| mimeType.equals(MediaCodecUtil.getAlternativeCodecMimeType(format)); || mimeType.equals(MediaCodecUtil.getAlternativeCodecMimeType(format));
} }
private boolean isCodecProfileAndLevelSupported(Format format) { private boolean isCodecProfileAndLevelSupported(
if (format.codecs == null) { Format format, boolean checkPerformanceCapabilities) {
return true;
}
Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format); Pair<Integer, Integer> codecProfileAndLevel = MediaCodecUtil.getCodecProfileAndLevel(format);
if (codecProfileAndLevel == null) { if (codecProfileAndLevel == null) {
// If we don't know any better, we assume that the profile and level are supported. // 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) { for (CodecProfileLevel profileLevel : profileLevels) {
if (profileLevel.profile == profile if (profileLevel.profile == profile
&& profileLevel.level >= level && (profileLevel.level >= level || !checkPerformanceCapabilities)
&& !needsProfileExcludedWorkaround(mimeType, profile)) { && !needsProfileExcludedWorkaround(mimeType, profile)) {
return true; return true;
} }

View File

@ -1113,6 +1113,14 @@ public abstract class MediaCodecRenderer extends BaseRenderer {
} }
codecInitializedTimestamp = SystemClock.elapsedRealtime(); 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.codecInfo = codecInfo;
this.codecOperatingRate = codecOperatingRate; this.codecOperatingRate = codecOperatingRate;
codecInputFormat = inputFormat; 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 <= 17 && "OMX.allwinner.video.decoder.avc".equals(name))
|| (Util.SDK_INT <= 29 || (Util.SDK_INT <= 29
&& ("OMX.broadcom.video_decoder.tunnel".equals(name) && ("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); || ("Amazon".equals(Util.MANUFACTURER) && "AFTS".equals(Util.MODEL) && codecInfo.secure);
} }

View File

@ -190,22 +190,15 @@ public final class MediaCodecUtil {
} }
/** /**
* Returns a copy of the provided decoder list sorted such that decoders with format support are * Returns a copy of the provided decoder list sorted such that decoders with functional format
* listed first. The returned list is modifiable for convenience. * support are listed first. The returned list is modifiable for convenience.
*/ */
@CheckResult @CheckResult
public static List<MediaCodecInfo> getDecoderInfosSortedByFormatSupport( public static List<MediaCodecInfo> getDecoderInfosSortedByFormatSupport(
List<MediaCodecInfo> decoderInfos, Format format) { List<MediaCodecInfo> decoderInfos, Format format) {
decoderInfos = new ArrayList<>(decoderInfos); decoderInfos = new ArrayList<>(decoderInfos);
sortByScore( sortByScore(
decoderInfos, decoderInfos, decoderInfo -> decoderInfo.isFormatFunctionallySupported(format) ? 1 : 0);
decoderInfo -> {
try {
return decoderInfo.isFormatSupported(format) ? 1 : 0;
} catch (DecoderQueryException e) {
return -1;
}
});
return decoderInfos; return decoderInfos;
} }

View File

@ -40,7 +40,16 @@ import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; 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 @UnstableApi
public abstract class DownloadService extends Service { public abstract class DownloadService extends Service {
@ -574,6 +583,17 @@ public abstract class DownloadService extends Service {
Util.startForegroundService(context, intent); 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 @Override
public void onCreate() { public void onCreate() {
if (channelId != null) { if (channelId != null) {

View File

@ -61,7 +61,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
private static final int MSG_UPDATE_TIMELINE = 4; private static final int MSG_UPDATE_TIMELINE = 4;
private static final int MSG_ON_COMPLETION = 5; 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(); new MediaItem.Builder().setUri(Uri.EMPTY).build();
// Accessed on any thread. // Accessed on any thread.
@ -451,7 +451,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
public MediaItem getMediaItem() { public MediaItem getMediaItem() {
// This method is actually never called because getInitialTimeline is implemented and hence the // 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. // MaskingMediaSource does not need to create a placeholder timeline for this media source.
return EMPTY_MEDIA_ITEM; return PLACEHOLDER_MEDIA_ITEM;
} }
@Override @Override
@ -1018,7 +1018,7 @@ public final class ConcatenatingMediaSource extends CompositeMediaSource<MediaSo
@Override @Override
public MediaItem getMediaItem() { public MediaItem getMediaItem() {
return EMPTY_MEDIA_ITEM; return PLACEHOLDER_MEDIA_ITEM;
} }
@Override @Override

View File

@ -0,0 +1,610 @@
/*
* Copyright 2021 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.exoplayer.source;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.upstream.Allocator;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.IdentityHashMap;
/**
* Concatenates multiple {@link MediaSource MediaSources}, combining everything in one single {@link
* Timeline.Window}.
*
* <p>This class can only be used under the following conditions:
*
* <ul>
* <li>All sources must be non-empty.
* <li>All {@link Timeline.Window Windows} defined by the sources, except the first, must have an
* {@link Timeline.Window#getPositionInFirstPeriodUs() period offset} of zero. This excludes,
* for example, live streams or {@link ClippingMediaSource} with a non-zero start position.
* </ul>
*/
@UnstableApi
public final class ConcatenatingMediaSource2 extends CompositeMediaSource<Integer> {
/** A builder for {@link ConcatenatingMediaSource2} instances. */
public static final class Builder {
private final ImmutableList.Builder<MediaSourceHolder> mediaSourceHoldersBuilder;
private int index;
@Nullable private MediaItem mediaItem;
@Nullable private MediaSource.Factory mediaSourceFactory;
/** Creates the builder. */
public Builder() {
mediaSourceHoldersBuilder = ImmutableList.builder();
}
/**
* Instructs the builder to use a {@link DefaultMediaSourceFactory} to convert {@link MediaItem
* MediaItems} to {@link MediaSource MediaSources} for all future calls to {@link
* #add(MediaItem)} or {@link #add(MediaItem, long)}.
*
* @param context A {@link Context}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder useDefaultMediaSourceFactory(Context context) {
return setMediaSourceFactory(new DefaultMediaSourceFactory(context));
}
/**
* Sets a {@link MediaSource.Factory} that is used to convert {@link MediaItem MediaItems} to
* {@link MediaSource MediaSources} for all future calls to {@link #add(MediaItem)} or {@link
* #add(MediaItem, long)}.
*
* @param mediaSourceFactory A {@link MediaSource.Factory}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMediaSourceFactory(MediaSource.Factory mediaSourceFactory) {
this.mediaSourceFactory = checkNotNull(mediaSourceFactory);
return this;
}
/**
* Sets the {@link MediaItem} to be used for the concatenated media source.
*
* <p>This {@link MediaItem} will be used as {@link Timeline.Window#mediaItem} for the
* concatenated source and will be returned by {@link Player#getCurrentMediaItem()}.
*
* <p>The default is {@code MediaItem.fromUri(Uri.EMPTY)}.
*
* @param mediaItem The {@link MediaItem}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMediaItem(MediaItem mediaItem) {
this.mediaItem = mediaItem;
return this;
}
/**
* Adds a {@link MediaItem} to the concatenation.
*
* <p>{@link #useDefaultMediaSourceFactory(Context)} or {@link
* #setMediaSourceFactory(MediaSource.Factory)} must be called before this method.
*
* <p>This method must not be used with media items for progressive media that can't provide
* their duration with their first {@link Timeline} update. Use {@link #add(MediaItem, long)}
* instead.
*
* @param mediaItem The {@link MediaItem}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder add(MediaItem mediaItem) {
return add(mediaItem, /* initialPlaceholderDurationMs= */ C.TIME_UNSET);
}
/**
* Adds a {@link MediaItem} to the concatenation and specifies its initial placeholder duration
* used while the actual duration is still unknown.
*
* <p>{@link #useDefaultMediaSourceFactory(Context)} or {@link
* #setMediaSourceFactory(MediaSource.Factory)} must be called before this method.
*
* <p>Setting a placeholder duration is required for media items for progressive media that
* can't provide their duration with their first {@link Timeline} update. It may also be used
* for other items to make the duration known immediately.
*
* @param mediaItem The {@link MediaItem}.
* @param initialPlaceholderDurationMs The initial placeholder duration in milliseconds used
* while the actual duration is still unknown, or {@link C#TIME_UNSET} to not define one.
* The placeholder duration is used for every {@link Timeline.Window} defined by {@link
* Timeline} of the {@link MediaItem}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder add(MediaItem mediaItem, long initialPlaceholderDurationMs) {
checkNotNull(mediaItem);
checkStateNotNull(
mediaSourceFactory,
"Must use useDefaultMediaSourceFactory or setMediaSourceFactory first.");
return add(mediaSourceFactory.createMediaSource(mediaItem), initialPlaceholderDurationMs);
}
/**
* Adds a {@link MediaSource} to the concatenation.
*
* <p>This method must not be used for sources like {@link ProgressiveMediaSource} that can't
* provide their duration with their first {@link Timeline} update. Use {@link #add(MediaSource,
* long)} instead.
*
* @param mediaSource The {@link MediaSource}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder add(MediaSource mediaSource) {
return add(mediaSource, /* initialPlaceholderDurationMs= */ C.TIME_UNSET);
}
/**
* Adds a {@link MediaSource} to the concatenation and specifies its initial placeholder
* duration used while the actual duration is still unknown.
*
* <p>Setting a placeholder duration is required for sources like {@link ProgressiveMediaSource}
* that can't provide their duration with their first {@link Timeline} update. It may also be
* used for other sources to make the duration known immediately.
*
* @param mediaSource The {@link MediaSource}.
* @param initialPlaceholderDurationMs The initial placeholder duration in milliseconds used
* while the actual duration is still unknown, or {@link C#TIME_UNSET} to not define one.
* The placeholder duration is used for every {@link Timeline.Window} defined by {@link
* Timeline} of the {@link MediaSource}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder add(MediaSource mediaSource, long initialPlaceholderDurationMs) {
checkNotNull(mediaSource);
checkState(
!(mediaSource instanceof ProgressiveMediaSource)
|| initialPlaceholderDurationMs != C.TIME_UNSET,
"Progressive media source must define an initial placeholder duration.");
mediaSourceHoldersBuilder.add(
new MediaSourceHolder(mediaSource, index++, Util.msToUs(initialPlaceholderDurationMs)));
return this;
}
/** Builds the concatenating media source. */
public ConcatenatingMediaSource2 build() {
checkArgument(index > 0, "Must add at least one source to the concatenation.");
if (mediaItem == null) {
mediaItem = MediaItem.fromUri(Uri.EMPTY);
}
return new ConcatenatingMediaSource2(mediaItem, mediaSourceHoldersBuilder.build());
}
}
private static final int MSG_UPDATE_TIMELINE = 0;
private final MediaItem mediaItem;
private final ImmutableList<MediaSourceHolder> mediaSourceHolders;
private final IdentityHashMap<MediaPeriod, MediaSourceHolder> mediaSourceByMediaPeriod;
@Nullable private Handler playbackThreadHandler;
private boolean timelineUpdateScheduled;
private ConcatenatingMediaSource2(
MediaItem mediaItem, ImmutableList<MediaSourceHolder> mediaSourceHolders) {
this.mediaItem = mediaItem;
this.mediaSourceHolders = mediaSourceHolders;
mediaSourceByMediaPeriod = new IdentityHashMap<>();
}
@Nullable
@Override
public Timeline getInitialTimeline() {
return maybeCreateConcatenatedTimeline();
}
@Override
public MediaItem getMediaItem() {
return mediaItem;
}
@Override
protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) {
super.prepareSourceInternal(mediaTransferListener);
playbackThreadHandler = new Handler(/* callback= */ this::handleMessage);
for (int i = 0; i < mediaSourceHolders.size(); i++) {
MediaSourceHolder holder = mediaSourceHolders.get(i);
prepareChildSource(/* id= */ i, holder.mediaSource);
}
scheduleTimelineUpdate();
}
@SuppressWarnings("MissingSuperCall")
@Override
protected void enableInternal() {
// Suppress enabling all child sources here as they can be lazily enabled when creating periods.
}
@Override
public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
int holderIndex = getChildIndex(id.periodUid);
MediaSourceHolder holder = mediaSourceHolders.get(holderIndex);
MediaPeriodId childMediaPeriodId =
id.copyWithPeriodUid(getChildPeriodUid(id.periodUid))
.copyWithWindowSequenceNumber(
getChildWindowSequenceNumber(
id.windowSequenceNumber, mediaSourceHolders.size(), holder.index));
enableChildSource(holder.index);
holder.activeMediaPeriods++;
MediaPeriod mediaPeriod =
holder.mediaSource.createPeriod(childMediaPeriodId, allocator, startPositionUs);
mediaSourceByMediaPeriod.put(mediaPeriod, holder);
disableUnusedMediaSources();
return mediaPeriod;
}
@Override
public void releasePeriod(MediaPeriod mediaPeriod) {
MediaSourceHolder holder = checkNotNull(mediaSourceByMediaPeriod.remove(mediaPeriod));
holder.mediaSource.releasePeriod(mediaPeriod);
holder.activeMediaPeriods--;
if (!mediaSourceByMediaPeriod.isEmpty()) {
disableUnusedMediaSources();
}
}
@Override
protected void releaseSourceInternal() {
super.releaseSourceInternal();
if (playbackThreadHandler != null) {
playbackThreadHandler.removeCallbacksAndMessages(null);
playbackThreadHandler = null;
}
timelineUpdateScheduled = false;
}
@Override
protected void onChildSourceInfoRefreshed(
Integer childSourceId, MediaSource mediaSource, Timeline newTimeline) {
scheduleTimelineUpdate();
}
@Override
@Nullable
protected MediaPeriodId getMediaPeriodIdForChildMediaPeriodId(
Integer childSourceId, MediaPeriodId mediaPeriodId) {
int childIndex =
getChildIndexFromChildWindowSequenceNumber(
mediaPeriodId.windowSequenceNumber, mediaSourceHolders.size());
if (childSourceId != childIndex) {
// Ensure the reported media period id has the expected window sequence number. Otherwise it
// does not belong to this child source.
return null;
}
long windowSequenceNumber =
getWindowSequenceNumberFromChildWindowSequenceNumber(
mediaPeriodId.windowSequenceNumber, mediaSourceHolders.size());
Object periodUid = getPeriodUid(childSourceId, mediaPeriodId.periodUid);
return mediaPeriodId
.copyWithPeriodUid(periodUid)
.copyWithWindowSequenceNumber(windowSequenceNumber);
}
@Override
protected int getWindowIndexForChildWindowIndex(Integer childSourceId, int windowIndex) {
return 0;
}
private boolean handleMessage(Message msg) {
if (msg.what == MSG_UPDATE_TIMELINE) {
updateTimeline();
}
return true;
}
private void scheduleTimelineUpdate() {
if (!timelineUpdateScheduled) {
checkNotNull(playbackThreadHandler).obtainMessage(MSG_UPDATE_TIMELINE).sendToTarget();
timelineUpdateScheduled = true;
}
}
private void updateTimeline() {
timelineUpdateScheduled = false;
@Nullable ConcatenatedTimeline timeline = maybeCreateConcatenatedTimeline();
if (timeline != null) {
refreshSourceInfo(timeline);
}
}
private void disableUnusedMediaSources() {
for (int i = 0; i < mediaSourceHolders.size(); i++) {
MediaSourceHolder holder = mediaSourceHolders.get(i);
if (holder.activeMediaPeriods == 0) {
disableChildSource(holder.index);
}
}
}
@Nullable
private ConcatenatedTimeline maybeCreateConcatenatedTimeline() {
Timeline.Window window = new Timeline.Window();
Timeline.Period period = new Timeline.Period();
ImmutableList.Builder<Timeline> timelinesBuilder = ImmutableList.builder();
ImmutableList.Builder<Integer> firstPeriodIndicesBuilder = ImmutableList.builder();
ImmutableList.Builder<Long> periodOffsetsInWindowUsBuilder = ImmutableList.builder();
int periodCount = 0;
boolean isSeekable = true;
boolean isDynamic = false;
long durationUs = 0;
long defaultPositionUs = 0;
long nextPeriodOffsetInWindowUs = 0;
boolean manifestsAreIdentical = true;
boolean hasInitialManifest = false;
@Nullable Object initialManifest = null;
for (int i = 0; i < mediaSourceHolders.size(); i++) {
MediaSourceHolder holder = mediaSourceHolders.get(i);
Timeline timeline = holder.mediaSource.getTimeline();
checkArgument(!timeline.isEmpty(), "Can't concatenate empty child Timeline.");
timelinesBuilder.add(timeline);
firstPeriodIndicesBuilder.add(periodCount);
periodCount += timeline.getPeriodCount();
for (int j = 0; j < timeline.getWindowCount(); j++) {
timeline.getWindow(/* windowIndex= */ j, window);
if (!hasInitialManifest) {
initialManifest = window.manifest;
hasInitialManifest = true;
}
manifestsAreIdentical =
manifestsAreIdentical && Util.areEqual(initialManifest, window.manifest);
long windowDurationUs = window.durationUs;
if (windowDurationUs == C.TIME_UNSET) {
if (holder.initialPlaceholderDurationUs == C.TIME_UNSET) {
// Source duration isn't known yet and we have no placeholder duration.
return null;
}
windowDurationUs = holder.initialPlaceholderDurationUs;
}
durationUs += windowDurationUs;
if (holder.index == 0 && j == 0) {
defaultPositionUs = window.defaultPositionUs;
nextPeriodOffsetInWindowUs = -window.positionInFirstPeriodUs;
} else {
checkArgument(
window.positionInFirstPeriodUs == 0,
"Can't concatenate windows. A window has a non-zero offset in a period.");
}
// Assume placeholder windows are seekable to not prevent seeking in other periods.
isSeekable &= window.isSeekable || window.isPlaceholder;
isDynamic |= window.isDynamic;
}
int childPeriodCount = timeline.getPeriodCount();
for (int j = 0; j < childPeriodCount; j++) {
periodOffsetsInWindowUsBuilder.add(nextPeriodOffsetInWindowUs);
timeline.getPeriod(/* periodIndex= */ j, period);
long periodDurationUs = period.durationUs;
if (periodDurationUs == C.TIME_UNSET) {
checkArgument(
childPeriodCount == 1,
"Can't concatenate multiple periods with unknown duration in one window.");
long windowDurationUs =
window.durationUs != C.TIME_UNSET
? window.durationUs
: holder.initialPlaceholderDurationUs;
periodDurationUs = windowDurationUs + window.positionInFirstPeriodUs;
}
nextPeriodOffsetInWindowUs += periodDurationUs;
}
}
return new ConcatenatedTimeline(
mediaItem,
timelinesBuilder.build(),
firstPeriodIndicesBuilder.build(),
periodOffsetsInWindowUsBuilder.build(),
isSeekable,
isDynamic,
durationUs,
defaultPositionUs,
manifestsAreIdentical ? initialManifest : null);
}
/**
* Returns the period uid for the concatenated source from the child index and child period uid.
*/
private static Object getPeriodUid(int childIndex, Object childPeriodUid) {
return Pair.create(childIndex, childPeriodUid);
}
/** Returns the child index from the period uid of the concatenated source. */
@SuppressWarnings("unchecked")
private static int getChildIndex(Object periodUid) {
return ((Pair<Integer, Object>) periodUid).first;
}
/** Returns the uid of child period from the period uid of the concatenated source. */
@SuppressWarnings("unchecked")
private static Object getChildPeriodUid(Object periodUid) {
return ((Pair<Integer, Object>) periodUid).second;
}
/** Returns the window sequence number used for the child source. */
private static long getChildWindowSequenceNumber(
long windowSequenceNumber, int childCount, int childIndex) {
return windowSequenceNumber * childCount + childIndex;
}
/** Returns the index of the child source from a child window sequence number. */
private static int getChildIndexFromChildWindowSequenceNumber(
long childWindowSequenceNumber, int childCount) {
return (int) (childWindowSequenceNumber % childCount);
}
/** Returns the concatenated window sequence number from a child window sequence number. */
private static long getWindowSequenceNumberFromChildWindowSequenceNumber(
long childWindowSequenceNumber, int childCount) {
return childWindowSequenceNumber / childCount;
}
/* package */ static final class MediaSourceHolder {
public final MaskingMediaSource mediaSource;
public final int index;
public final long initialPlaceholderDurationUs;
public int activeMediaPeriods;
public MediaSourceHolder(
MediaSource mediaSource, int index, long initialPlaceholderDurationUs) {
this.mediaSource = new MaskingMediaSource(mediaSource, /* useLazyPreparation= */ false);
this.index = index;
this.initialPlaceholderDurationUs = initialPlaceholderDurationUs;
}
}
private static final class ConcatenatedTimeline extends Timeline {
private final MediaItem mediaItem;
private final ImmutableList<Timeline> timelines;
private final ImmutableList<Integer> firstPeriodIndices;
private final ImmutableList<Long> periodOffsetsInWindowUs;
private final boolean isSeekable;
private final boolean isDynamic;
private final long durationUs;
private final long defaultPositionUs;
@Nullable private final Object manifest;
public ConcatenatedTimeline(
MediaItem mediaItem,
ImmutableList<Timeline> timelines,
ImmutableList<Integer> firstPeriodIndices,
ImmutableList<Long> periodOffsetsInWindowUs,
boolean isSeekable,
boolean isDynamic,
long durationUs,
long defaultPositionUs,
@Nullable Object manifest) {
this.mediaItem = mediaItem;
this.timelines = timelines;
this.firstPeriodIndices = firstPeriodIndices;
this.periodOffsetsInWindowUs = periodOffsetsInWindowUs;
this.isSeekable = isSeekable;
this.isDynamic = isDynamic;
this.durationUs = durationUs;
this.defaultPositionUs = defaultPositionUs;
this.manifest = manifest;
}
@Override
public int getWindowCount() {
return 1;
}
@Override
public int getPeriodCount() {
return periodOffsetsInWindowUs.size();
}
@Override
public final Window getWindow(
int windowIndex, Window window, long defaultPositionProjectionUs) {
return window.set(
Window.SINGLE_WINDOW_UID,
mediaItem,
manifest,
/* presentationStartTimeMs= */ C.TIME_UNSET,
/* windowStartTimeMs= */ C.TIME_UNSET,
/* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET,
isSeekable,
isDynamic,
/* liveConfiguration= */ null,
defaultPositionUs,
durationUs,
/* firstPeriodIndex= */ 0,
/* lastPeriodIndex= */ getPeriodCount() - 1,
/* positionInFirstPeriodUs= */ -periodOffsetsInWindowUs.get(0));
}
@Override
public final Period getPeriodByUid(Object periodUid, Period period) {
int childIndex = getChildIndex(periodUid);
Object childPeriodUid = getChildPeriodUid(periodUid);
Timeline timeline = timelines.get(childIndex);
int periodIndex =
firstPeriodIndices.get(childIndex) + timeline.getIndexOfPeriod(childPeriodUid);
timeline.getPeriodByUid(childPeriodUid, period);
period.windowIndex = 0;
period.positionInWindowUs = periodOffsetsInWindowUs.get(periodIndex);
period.uid = periodUid;
return period;
}
@Override
public final Period getPeriod(int periodIndex, Period period, boolean setIds) {
int childIndex = getChildIndexByPeriodIndex(periodIndex);
int firstPeriodIndexInChild = firstPeriodIndices.get(childIndex);
timelines.get(childIndex).getPeriod(periodIndex - firstPeriodIndexInChild, period, setIds);
period.windowIndex = 0;
period.positionInWindowUs = periodOffsetsInWindowUs.get(periodIndex);
if (setIds) {
period.uid = getPeriodUid(childIndex, checkNotNull(period.uid));
}
return period;
}
@Override
public final int getIndexOfPeriod(Object uid) {
if (!(uid instanceof Pair) || !(((Pair<?, ?>) uid).first instanceof Integer)) {
return C.INDEX_UNSET;
}
int childIndex = getChildIndex(uid);
Object periodUid = getChildPeriodUid(uid);
int periodIndexInChild = timelines.get(childIndex).getIndexOfPeriod(periodUid);
return periodIndexInChild == C.INDEX_UNSET
? C.INDEX_UNSET
: firstPeriodIndices.get(childIndex) + periodIndexInChild;
}
@Override
public final Object getUidOfPeriod(int periodIndex) {
int childIndex = getChildIndexByPeriodIndex(periodIndex);
int firstPeriodIndexInChild = firstPeriodIndices.get(childIndex);
Object periodUidInChild =
timelines.get(childIndex).getUidOfPeriod(periodIndex - firstPeriodIndexInChild);
return getPeriodUid(childIndex, periodUidInChild);
}
private int getChildIndexByPeriodIndex(int periodIndex) {
return Util.binarySearchFloor(
firstPeriodIndices, periodIndex + 1, /* inclusive= */ false, /* stayInBounds= */ false);
}
}
}

View File

@ -118,17 +118,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
for (int i = 0; i < selections.length; i++) { for (int i = 0; i < selections.length; i++) {
Integer streamChildIndex = streams[i] == null ? null : streamPeriodIndices.get(streams[i]); Integer streamChildIndex = streams[i] == null ? null : streamPeriodIndices.get(streams[i]);
streamChildIndices[i] = streamChildIndex == null ? C.INDEX_UNSET : streamChildIndex; streamChildIndices[i] = streamChildIndex == null ? C.INDEX_UNSET : streamChildIndex;
selectionChildIndices[i] = C.INDEX_UNSET;
if (selections[i] != null) { if (selections[i] != null) {
TrackGroup mergedTrackGroup = selections[i].getTrackGroup(); TrackGroup mergedTrackGroup = selections[i].getTrackGroup();
TrackGroup childTrackGroup = // mergedTrackGroup.id is 'periods array index' + ":" + childTrackGroup.id
checkNotNull(childTrackGroupByMergedTrackGroup.get(mergedTrackGroup)); selectionChildIndices[i] =
for (int j = 0; j < periods.length; j++) { Integer.parseInt(mergedTrackGroup.id.substring(0, mergedTrackGroup.id.indexOf(":")));
if (periods[j].getTrackGroups().indexOf(childTrackGroup) != C.INDEX_UNSET) { } else {
selectionChildIndices[i] = j; selectionChildIndices[i] = C.INDEX_UNSET;
break;
}
}
} }
} }
streamPeriodIndices.clear(); streamPeriodIndices.clear();

View File

@ -72,7 +72,7 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
} }
private static final int PERIOD_COUNT_UNSET = -1; 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(); new MediaItem.Builder().setMediaId("MergingMediaSource").build();
private final boolean adjustPeriodTimeOffsets; private final boolean adjustPeriodTimeOffsets;
@ -163,7 +163,7 @@ public final class MergingMediaSource extends CompositeMediaSource<Integer> {
@Override @Override
public MediaItem getMediaItem() { public MediaItem getMediaItem() {
return mediaSources.length > 0 ? mediaSources[0].getMediaItem() : EMPTY_MEDIA_ITEM; return mediaSources.length > 0 ? mediaSources[0].getMediaItem() : PLACEHOLDER_MEDIA_ITEM;
} }
@Override @Override

View File

@ -15,10 +15,7 @@
*/ */
package androidx.media3.exoplayer.source; package androidx.media3.exoplayer.source;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable; import androidx.media3.common.Bundleable;
import androidx.media3.common.C; 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.BundleableUtil;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList; 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; import java.util.List;
/** /**
@ -118,21 +112,13 @@ public final class TrackGroupArray implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_TRACK_GROUPS = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_TRACK_GROUPS,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUPS = 0;
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putParcelableArrayList( bundle.putParcelableArrayList(
keyForField(FIELD_TRACK_GROUPS), BundleableUtil.toBundleArrayList(trackGroups)); FIELD_TRACK_GROUPS, BundleableUtil.toBundleArrayList(trackGroups));
return bundle; return bundle;
} }
@ -140,8 +126,7 @@ public final class TrackGroupArray implements Bundleable {
public static final Creator<TrackGroupArray> CREATOR = public static final Creator<TrackGroupArray> CREATOR =
bundle -> { bundle -> {
@Nullable @Nullable
List<Bundle> trackGroupBundles = List<Bundle> trackGroupBundles = bundle.getParcelableArrayList(FIELD_TRACK_GROUPS);
bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS));
if (trackGroupBundles == null) { if (trackGroupBundles == null) {
return new TrackGroupArray(); 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);
}
} }

View File

@ -427,7 +427,7 @@ public final class TextRenderer extends BaseRenderer implements Callback {
@SideEffectFree @SideEffectFree
private long getCurrentEventTimeUs(long positionUs) { private long getCurrentEventTimeUs(long positionUs) {
int nextEventTimeIndex = subtitle.getNextEventTimeIndex(positionUs); int nextEventTimeIndex = subtitle.getNextEventTimeIndex(positionUs);
if (nextEventTimeIndex == 0) { if (nextEventTimeIndex == 0 || subtitle.getEventTimeCount() == 0) {
return subtitle.timeUs; return subtitle.timeUs;
} }

View File

@ -827,69 +827,62 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Video // Video
setExceedVideoConstraintsIfNecessary( setExceedVideoConstraintsIfNecessary(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField(Parameters.FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY), Parameters.FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY,
defaultValue.exceedVideoConstraintsIfNecessary)); defaultValue.exceedVideoConstraintsIfNecessary));
setAllowVideoMixedMimeTypeAdaptiveness( setAllowVideoMixedMimeTypeAdaptiveness(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS), Parameters.FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS,
defaultValue.allowVideoMixedMimeTypeAdaptiveness)); defaultValue.allowVideoMixedMimeTypeAdaptiveness));
setAllowVideoNonSeamlessAdaptiveness( setAllowVideoNonSeamlessAdaptiveness(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField(Parameters.FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS), Parameters.FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS,
defaultValue.allowVideoNonSeamlessAdaptiveness)); defaultValue.allowVideoNonSeamlessAdaptiveness));
setAllowVideoMixedDecoderSupportAdaptiveness( setAllowVideoMixedDecoderSupportAdaptiveness(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField( Parameters.FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS,
Parameters.FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS),
defaultValue.allowVideoMixedDecoderSupportAdaptiveness)); defaultValue.allowVideoMixedDecoderSupportAdaptiveness));
// Audio // Audio
setExceedAudioConstraintsIfNecessary( setExceedAudioConstraintsIfNecessary(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField(Parameters.FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY), Parameters.FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY,
defaultValue.exceedAudioConstraintsIfNecessary)); defaultValue.exceedAudioConstraintsIfNecessary));
setAllowAudioMixedMimeTypeAdaptiveness( setAllowAudioMixedMimeTypeAdaptiveness(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS), Parameters.FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS,
defaultValue.allowAudioMixedMimeTypeAdaptiveness)); defaultValue.allowAudioMixedMimeTypeAdaptiveness));
setAllowAudioMixedSampleRateAdaptiveness( setAllowAudioMixedSampleRateAdaptiveness(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField(Parameters.FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS), Parameters.FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS,
defaultValue.allowAudioMixedSampleRateAdaptiveness)); defaultValue.allowAudioMixedSampleRateAdaptiveness));
setAllowAudioMixedChannelCountAdaptiveness( setAllowAudioMixedChannelCountAdaptiveness(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField( Parameters.FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS,
Parameters.FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS),
defaultValue.allowAudioMixedChannelCountAdaptiveness)); defaultValue.allowAudioMixedChannelCountAdaptiveness));
setAllowAudioMixedDecoderSupportAdaptiveness( setAllowAudioMixedDecoderSupportAdaptiveness(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField( Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS,
Parameters.FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS),
defaultValue.allowAudioMixedDecoderSupportAdaptiveness)); defaultValue.allowAudioMixedDecoderSupportAdaptiveness));
setConstrainAudioChannelCountToDeviceCapabilities( setConstrainAudioChannelCountToDeviceCapabilities(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField( Parameters.FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES,
Parameters.FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES),
defaultValue.constrainAudioChannelCountToDeviceCapabilities)); defaultValue.constrainAudioChannelCountToDeviceCapabilities));
// General // General
setExceedRendererCapabilitiesIfNecessary( setExceedRendererCapabilitiesIfNecessary(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField(Parameters.FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY), Parameters.FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY,
defaultValue.exceedRendererCapabilitiesIfNecessary)); defaultValue.exceedRendererCapabilitiesIfNecessary));
setTunnelingEnabled( setTunnelingEnabled(
bundle.getBoolean( bundle.getBoolean(Parameters.FIELD_TUNNELING_ENABLED, defaultValue.tunnelingEnabled));
Parameters.keyForField(Parameters.FIELD_TUNNELING_ENABLED),
defaultValue.tunnelingEnabled));
setAllowMultipleAdaptiveSelections( setAllowMultipleAdaptiveSelections(
bundle.getBoolean( bundle.getBoolean(
Parameters.keyForField(Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS), Parameters.FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS,
defaultValue.allowMultipleAdaptiveSelections)); defaultValue.allowMultipleAdaptiveSelections));
// Overrides // Overrides
selectionOverrides = new SparseArray<>(); selectionOverrides = new SparseArray<>();
setSelectionOverridesFromBundle(bundle); setSelectionOverridesFromBundle(bundle);
rendererDisabledFlags = rendererDisabledFlags =
makeSparseBooleanArrayFromTrueKeys( makeSparseBooleanArrayFromTrueKeys(
bundle.getIntArray( bundle.getIntArray(Parameters.FIELD_RENDERER_DISABLED_INDICES));
Parameters.keyForField(Parameters.FIELD_RENDERER_DISABLED_INDICES)));
} }
@CanIgnoreReturnValue @CanIgnoreReturnValue
@ -1571,20 +1564,17 @@ public class DefaultTrackSelector extends MappingTrackSelector {
private void setSelectionOverridesFromBundle(Bundle bundle) { private void setSelectionOverridesFromBundle(Bundle bundle) {
@Nullable @Nullable
int[] rendererIndices = int[] rendererIndices =
bundle.getIntArray( bundle.getIntArray(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDICES);
Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_RENDERER_INDICES));
@Nullable @Nullable
ArrayList<Bundle> trackGroupArrayBundles = ArrayList<Bundle> trackGroupArrayBundles =
bundle.getParcelableArrayList( bundle.getParcelableArrayList(Parameters.FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS);
Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS));
List<TrackGroupArray> trackGroupArrays = List<TrackGroupArray> trackGroupArrays =
trackGroupArrayBundles == null trackGroupArrayBundles == null
? ImmutableList.of() ? ImmutableList.of()
: BundleableUtil.fromBundleList(TrackGroupArray.CREATOR, trackGroupArrayBundles); : BundleableUtil.fromBundleList(TrackGroupArray.CREATOR, trackGroupArrayBundles);
@Nullable @Nullable
SparseArray<Bundle> selectionOverrideBundles = SparseArray<Bundle> selectionOverrideBundles =
bundle.getSparseParcelableArray( bundle.getSparseParcelableArray(Parameters.FIELD_SELECTION_OVERRIDES);
Parameters.keyForField(Parameters.FIELD_SELECTION_OVERRIDES));
SparseArray<SelectionOverride> selectionOverrides = SparseArray<SelectionOverride> selectionOverrides =
selectionOverrideBundles == null selectionOverrideBundles == null
? new SparseArray<>() ? new SparseArray<>()
@ -1874,32 +1864,40 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Bundleable implementation. // Bundleable implementation.
private static final int FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY = FIELD_CUSTOM_ID_BASE; private static final String FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY =
private static final int FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE);
FIELD_CUSTOM_ID_BASE + 1; private static final String FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS =
private static final int FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS = FIELD_CUSTOM_ID_BASE + 2; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 1);
private static final int FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY = FIELD_CUSTOM_ID_BASE + 3; private static final String FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS =
private static final int FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 2);
FIELD_CUSTOM_ID_BASE + 4; private static final String FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY =
private static final int FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 3);
FIELD_CUSTOM_ID_BASE + 5; private static final String FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS =
private static final int FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 4);
FIELD_CUSTOM_ID_BASE + 6; private static final String FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS =
private static final int FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY = Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 5);
FIELD_CUSTOM_ID_BASE + 7; private static final String FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS =
private static final int FIELD_TUNNELING_ENABLED = FIELD_CUSTOM_ID_BASE + 8; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 6);
private static final int FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS = FIELD_CUSTOM_ID_BASE + 9; private static final String FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY =
private static final int FIELD_SELECTION_OVERRIDES_RENDERER_INDICES = FIELD_CUSTOM_ID_BASE + 10; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 7);
private static final int FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS = private static final String FIELD_TUNNELING_ENABLED =
FIELD_CUSTOM_ID_BASE + 11; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 8);
private static final int FIELD_SELECTION_OVERRIDES = FIELD_CUSTOM_ID_BASE + 12; private static final String FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS =
private static final int FIELD_RENDERER_DISABLED_INDICES = FIELD_CUSTOM_ID_BASE + 13; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 9);
private static final int FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = private static final String FIELD_SELECTION_OVERRIDES_RENDERER_INDICES =
FIELD_CUSTOM_ID_BASE + 14; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 10);
private static final int FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS = private static final String FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS =
FIELD_CUSTOM_ID_BASE + 15; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 11);
private static final int FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES = private static final String FIELD_SELECTION_OVERRIDES =
FIELD_CUSTOM_ID_BASE + 16; Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 12);
private static final String FIELD_RENDERER_DISABLED_INDICES =
Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 13);
private static final String FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS =
Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 14);
private static final String FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS =
Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 15);
private static final String FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES =
Util.intToStringMaxRadix(FIELD_CUSTOM_ID_BASE + 16);
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
@ -1907,49 +1905,40 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Video // Video
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY), FIELD_EXCEED_VIDEO_CONSTRAINTS_IF_NECESSARY, exceedVideoConstraintsIfNecessary);
exceedVideoConstraintsIfNecessary);
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS), FIELD_ALLOW_VIDEO_MIXED_MIME_TYPE_ADAPTIVENESS, allowVideoMixedMimeTypeAdaptiveness);
allowVideoMixedMimeTypeAdaptiveness);
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS), FIELD_ALLOW_VIDEO_NON_SEAMLESS_ADAPTIVENESS, allowVideoNonSeamlessAdaptiveness);
allowVideoNonSeamlessAdaptiveness);
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), FIELD_ALLOW_VIDEO_MIXED_DECODER_SUPPORT_ADAPTIVENESS,
allowVideoMixedDecoderSupportAdaptiveness); allowVideoMixedDecoderSupportAdaptiveness);
// Audio // Audio
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NCESSARY), FIELD_EXCEED_AUDIO_CONSTRAINTS_IF_NECESSARY, exceedAudioConstraintsIfNecessary);
exceedAudioConstraintsIfNecessary);
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS), FIELD_ALLOW_AUDIO_MIXED_MIME_TYPE_ADAPTIVENESS, allowAudioMixedMimeTypeAdaptiveness);
allowAudioMixedMimeTypeAdaptiveness);
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS), FIELD_ALLOW_AUDIO_MIXED_SAMPLE_RATE_ADAPTIVENESS, allowAudioMixedSampleRateAdaptiveness);
allowAudioMixedSampleRateAdaptiveness);
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS), FIELD_ALLOW_AUDIO_MIXED_CHANNEL_COUNT_ADAPTIVENESS,
allowAudioMixedChannelCountAdaptiveness); allowAudioMixedChannelCountAdaptiveness);
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS), FIELD_ALLOW_AUDIO_MIXED_DECODER_SUPPORT_ADAPTIVENESS,
allowAudioMixedDecoderSupportAdaptiveness); allowAudioMixedDecoderSupportAdaptiveness);
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES), FIELD_CONSTRAIN_AUDIO_CHANNEL_COUNT_TO_DEVICE_CAPABILITIES,
constrainAudioChannelCountToDeviceCapabilities); constrainAudioChannelCountToDeviceCapabilities);
// General // General
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY), FIELD_EXCEED_RENDERER_CAPABILITIES_IF_NECESSARY, exceedRendererCapabilitiesIfNecessary);
exceedRendererCapabilitiesIfNecessary); bundle.putBoolean(FIELD_TUNNELING_ENABLED, tunnelingEnabled);
bundle.putBoolean(keyForField(FIELD_TUNNELING_ENABLED), tunnelingEnabled); bundle.putBoolean(FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS, allowMultipleAdaptiveSelections);
bundle.putBoolean(
keyForField(FIELD_ALLOW_MULTIPLE_ADAPTIVE_SELECTIONS), allowMultipleAdaptiveSelections);
putSelectionOverridesToBundle(bundle, selectionOverrides); putSelectionOverridesToBundle(bundle, selectionOverrides);
// Only true values are put into rendererDisabledFlags. // Only true values are put into rendererDisabledFlags.
bundle.putIntArray( bundle.putIntArray(
keyForField(FIELD_RENDERER_DISABLED_INDICES), FIELD_RENDERER_DISABLED_INDICES, getKeysFromSparseBooleanArray(rendererDisabledFlags));
getKeysFromSparseBooleanArray(rendererDisabledFlags));
return bundle; return bundle;
} }
@ -1982,12 +1971,12 @@ public class DefaultTrackSelector extends MappingTrackSelector {
rendererIndices.add(rendererIndex); rendererIndices.add(rendererIndex);
} }
bundle.putIntArray( bundle.putIntArray(
keyForField(FIELD_SELECTION_OVERRIDES_RENDERER_INDICES), Ints.toArray(rendererIndices)); FIELD_SELECTION_OVERRIDES_RENDERER_INDICES, Ints.toArray(rendererIndices));
bundle.putParcelableArrayList( bundle.putParcelableArrayList(
keyForField(FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS), FIELD_SELECTION_OVERRIDES_TRACK_GROUP_ARRAYS,
BundleableUtil.toBundleArrayList(trackGroupArrays)); BundleableUtil.toBundleArrayList(trackGroupArrays));
bundle.putSparseParcelableArray( bundle.putSparseParcelableArray(
keyForField(FIELD_SELECTION_OVERRIDES), BundleableUtil.toBundleSparseArray(selections)); FIELD_SELECTION_OVERRIDES, BundleableUtil.toBundleSparseArray(selections));
} }
} }
@ -2116,27 +2105,17 @@ public class DefaultTrackSelector extends MappingTrackSelector {
// Bundleable implementation. // Bundleable implementation.
@Documented private static final String FIELD_GROUP_INDEX = Util.intToStringMaxRadix(0);
@Retention(RetentionPolicy.SOURCE) private static final String FIELD_TRACKS = Util.intToStringMaxRadix(1);
@Target(TYPE_USE) private static final String FIELD_TRACK_TYPE = Util.intToStringMaxRadix(2);
@IntDef({
FIELD_GROUP_INDEX,
FIELD_TRACKS,
FIELD_TRACK_TYPE,
})
private @interface FieldNumber {}
private static final int FIELD_GROUP_INDEX = 0;
private static final int FIELD_TRACKS = 1;
private static final int FIELD_TRACK_TYPE = 2;
@UnstableApi @UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_GROUP_INDEX), groupIndex); bundle.putInt(FIELD_GROUP_INDEX, groupIndex);
bundle.putIntArray(keyForField(FIELD_TRACKS), tracks); bundle.putIntArray(FIELD_TRACKS, tracks);
bundle.putInt(keyForField(FIELD_TRACK_TYPE), type); bundle.putInt(FIELD_TRACK_TYPE, type);
return bundle; return bundle;
} }
@ -2144,17 +2123,13 @@ public class DefaultTrackSelector extends MappingTrackSelector {
@UnstableApi @UnstableApi
public static final Creator<SelectionOverride> CREATOR = public static final Creator<SelectionOverride> CREATOR =
bundle -> { bundle -> {
int groupIndex = bundle.getInt(keyForField(FIELD_GROUP_INDEX), -1); int groupIndex = bundle.getInt(FIELD_GROUP_INDEX, -1);
@Nullable int[] tracks = bundle.getIntArray(keyForField(FIELD_TRACKS)); @Nullable int[] tracks = bundle.getIntArray(FIELD_TRACKS);
int trackType = bundle.getInt(keyForField(FIELD_TRACK_TYPE), -1); int trackType = bundle.getInt(FIELD_TRACK_TYPE, -1);
Assertions.checkArgument(groupIndex >= 0 && trackType >= 0); Assertions.checkArgument(groupIndex >= 0 && trackType >= 0);
Assertions.checkNotNull(tracks); Assertions.checkNotNull(tracks);
return new SelectionOverride(groupIndex, tracks, trackType); return new SelectionOverride(groupIndex, tracks, trackType);
}; };
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
} }
/** /**

View File

@ -48,27 +48,27 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
/** Default initial Wifi bitrate estimate in bits per second. */ /** Default initial Wifi bitrate estimate in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI = public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_WIFI =
ImmutableList.of(4_800_000L, 3_100_000L, 2_100_000L, 1_500_000L, 800_000L); ImmutableList.of(4_400_000L, 3_200_000L, 2_300_000L, 1_600_000L, 810_000L);
/** Default initial 2G bitrate estimates in bits per second. */ /** Default initial 2G bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_2G = public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_2G =
ImmutableList.of(1_500_000L, 1_000_000L, 730_000L, 440_000L, 170_000L); ImmutableList.of(1_400_000L, 990_000L, 730_000L, 510_000L, 230_000L);
/** Default initial 3G bitrate estimates in bits per second. */ /** Default initial 3G bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_3G = public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_3G =
ImmutableList.of(2_200_000L, 1_400_000L, 1_100_000L, 910_000L, 620_000L); ImmutableList.of(2_100_000L, 1_400_000L, 1_000_000L, 890_000L, 640_000L);
/** Default initial 4G bitrate estimates in bits per second. */ /** Default initial 4G bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_4G = public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_4G =
ImmutableList.of(3_000_000L, 1_900_000L, 1_400_000L, 1_000_000L, 660_000L); ImmutableList.of(2_600_000L, 1_700_000L, 1_300_000L, 1_000_000L, 700_000L);
/** Default initial 5G-NSA bitrate estimates in bits per second. */ /** Default initial 5G-NSA bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_NSA = public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_NSA =
ImmutableList.of(6_000_000L, 4_100_000L, 3_200_000L, 1_800_000L, 1_000_000L); ImmutableList.of(5_700_000L, 3_700_000L, 2_300_000L, 1_700_000L, 990_000L);
/** Default initial 5G-SA bitrate estimates in bits per second. */ /** Default initial 5G-SA bitrate estimates in bits per second. */
public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_SA = public static final ImmutableList<Long> DEFAULT_INITIAL_BITRATE_ESTIMATES_5G_SA =
ImmutableList.of(2_800_000L, 2_400_000L, 1_600_000L, 1_100_000L, 950_000L); ImmutableList.of(2_800_000L, 1_800_000L, 1_400_000L, 1_100_000L, 870_000L);
/** /**
* Default initial bitrate estimate used when the device is offline or the network type cannot be * Default initial bitrate estimate used when the device is offline or the network type cannot be
@ -483,394 +483,410 @@ public final class DefaultBandwidthMeter implements BandwidthMeter, TransferList
*/ */
private static int[] getInitialBitrateCountryGroupAssignment(String country) { private static int[] getInitialBitrateCountryGroupAssignment(String country) {
switch (country) { switch (country) {
case "AD":
case "CW":
return new int[] {2, 2, 0, 0, 2, 2};
case "AE": case "AE":
return new int[] {1, 4, 4, 4, 4, 0}; return new int[] {1, 4, 3, 4, 4, 2};
case "AG": case "AG":
return new int[] {2, 4, 1, 2, 2, 2}; return new int[] {2, 4, 3, 4, 2, 2};
case "AI": case "AL":
return new int[] {0, 2, 0, 3, 2, 2}; return new int[] {1, 1, 1, 3, 2, 2};
case "AM": case "AM":
return new int[] {2, 3, 2, 3, 2, 2}; return new int[] {2, 3, 2, 3, 2, 2};
case "AO": case "AO":
return new int[] {4, 4, 3, 2, 2, 2}; return new int[] {4, 4, 4, 3, 2, 2};
case "AS": case "AS":
return new int[] {2, 2, 3, 3, 2, 2}; return new int[] {2, 2, 3, 3, 2, 2};
case "AT": case "AT":
return new int[] {1, 0, 1, 1, 0, 0}; return new int[] {1, 2, 1, 4, 1, 4};
case "AU": case "AU":
return new int[] {0, 1, 1, 1, 2, 0}; return new int[] {0, 2, 1, 1, 3, 0};
case "AW":
return new int[] {1, 3, 4, 4, 2, 2};
case "BA":
return new int[] {1, 2, 1, 1, 2, 2};
case "BD":
return new int[] {2, 1, 3, 3, 2, 2};
case "BE": case "BE":
return new int[] {0, 1, 4, 4, 3, 2}; return new int[] {0, 1, 4, 4, 3, 2};
case "BF":
return new int[] {4, 3, 4, 3, 2, 2};
case "BH": case "BH":
return new int[] {1, 2, 1, 3, 4, 2}; return new int[] {1, 3, 1, 4, 4, 2};
case "BJ": case "BJ":
return new int[] {4, 4, 3, 3, 2, 2}; return new int[] {4, 4, 2, 3, 2, 2};
case "BN":
return new int[] {3, 2, 0, 1, 2, 2};
case "BO": case "BO":
return new int[] {1, 2, 3, 2, 2, 2}; return new int[] {1, 2, 3, 2, 2, 2};
case "BS": case "BR":
return new int[] {4, 4, 2, 2, 2, 2}; return new int[] {1, 1, 2, 1, 1, 0};
case "BT":
return new int[] {3, 1, 3, 2, 2, 2};
case "BW": case "BW":
return new int[] {3, 2, 1, 0, 2, 2}; return new int[] {3, 2, 1, 0, 2, 2};
case "BY": case "BY":
return new int[] {0, 1, 2, 3, 2, 2}; return new int[] {1, 1, 2, 3, 2, 2};
case "BZ":
return new int[] {2, 4, 2, 1, 2, 2};
case "CA": case "CA":
return new int[] {0, 2, 2, 2, 3, 2}; return new int[] {0, 2, 3, 3, 3, 3};
case "CD":
return new int[] {4, 2, 3, 2, 2, 2};
case "CH": case "CH":
return new int[] {0, 0, 0, 1, 0, 2}; return new int[] {0, 0, 0, 0, 0, 3};
case "BZ":
case "CK":
return new int[] {2, 2, 2, 1, 2, 2};
case "CL":
return new int[] {1, 1, 2, 1, 3, 2};
case "CM": case "CM":
return new int[] {3, 3, 3, 3, 2, 2}; return new int[] {4, 3, 3, 4, 2, 2};
case "CN": case "CN":
return new int[] {2, 0, 1, 1, 3, 2}; return new int[] {2, 0, 4, 3, 3, 1};
case "CO": case "CO":
return new int[] {2, 3, 4, 3, 2, 2}; return new int[] {2, 3, 4, 2, 2, 2};
case "CR": case "CR":
return new int[] {2, 3, 4, 4, 2, 2}; return new int[] {2, 4, 4, 4, 2, 2};
case "CV": case "CV":
return new int[] {2, 1, 0, 0, 2, 2}; return new int[] {2, 3, 0, 1, 2, 2};
case "BN": case "CZ":
case "CW": return new int[] {0, 0, 2, 0, 1, 2};
return new int[] {2, 2, 0, 0, 2, 2};
case "DE": case "DE":
return new int[] {0, 1, 2, 2, 2, 3}; return new int[] {0, 1, 3, 2, 2, 2};
case "DK":
return new int[] {0, 0, 3, 2, 0, 2};
case "DO": case "DO":
return new int[] {3, 4, 4, 4, 4, 2}; return new int[] {3, 4, 4, 4, 4, 2};
case "AZ":
case "BF":
case "DZ":
return new int[] {3, 3, 4, 4, 2, 2};
case "EC": case "EC":
return new int[] {2, 3, 2, 1, 2, 2}; return new int[] {1, 3, 2, 1, 2, 2};
case "ET": case "CI":
return new int[] {4, 3, 3, 1, 2, 2}; case "EG":
return new int[] {3, 4, 3, 3, 2, 2};
case "FI": case "FI":
return new int[] {0, 0, 0, 3, 0, 2}; return new int[] {0, 0, 0, 2, 0, 2};
case "FJ": case "FJ":
return new int[] {3, 1, 2, 2, 2, 2}; return new int[] {3, 1, 2, 3, 2, 2};
case "FM": case "FM":
return new int[] {4, 2, 4, 1, 2, 2}; return new int[] {4, 2, 3, 0, 2, 2};
case "FR": case "AI":
return new int[] {1, 2, 3, 1, 0, 2};
case "GB":
return new int[] {0, 0, 1, 1, 1, 1};
case "GE":
return new int[] {1, 1, 1, 2, 2, 2};
case "BB": case "BB":
case "BM":
case "BQ":
case "DM": case "DM":
case "FO": case "FO":
case "GI":
return new int[] {0, 2, 0, 0, 2, 2}; return new int[] {0, 2, 0, 0, 2, 2};
case "AF": case "FR":
case "GM": return new int[] {1, 1, 2, 1, 1, 2};
return new int[] {4, 3, 3, 4, 2, 2}; case "GB":
case "GN": return new int[] {0, 1, 1, 2, 1, 2};
return new int[] {4, 3, 4, 2, 2, 2}; case "GE":
case "GQ": return new int[] {1, 0, 0, 2, 2, 2};
return new int[] {4, 2, 1, 4, 2, 2}; case "GG":
case "GT": return new int[] {0, 2, 1, 0, 2, 2};
return new int[] {2, 3, 2, 2, 2, 2};
case "CG": case "CG":
case "EG": case "GH":
return new int[] {3, 3, 3, 3, 2, 2};
case "GM":
return new int[] {4, 3, 2, 4, 2, 2};
case "GN":
return new int[] {4, 4, 4, 2, 2, 2};
case "GP":
return new int[] {3, 1, 1, 3, 2, 2};
case "GQ":
return new int[] {4, 4, 3, 3, 2, 2};
case "GT":
return new int[] {2, 2, 2, 1, 1, 2};
case "AW":
case "GU":
return new int[] {1, 2, 4, 4, 2, 2};
case "GW": case "GW":
return new int[] {3, 4, 3, 3, 2, 2}; return new int[] {4, 4, 2, 2, 2, 2};
case "GY": case "GY":
return new int[] {3, 2, 2, 1, 2, 2}; return new int[] {3, 0, 1, 1, 2, 2};
case "HK": case "HK":
return new int[] {0, 1, 2, 3, 2, 0}; return new int[] {0, 1, 1, 3, 2, 0};
case "HU": case "HN":
return new int[] {0, 0, 0, 1, 3, 2}; return new int[] {3, 3, 2, 2, 2, 2};
case "ID": case "ID":
return new int[] {3, 1, 2, 2, 3, 2}; return new int[] {3, 1, 1, 2, 3, 2};
case "ES": case "BA":
case "IE": case "IE":
return new int[] {0, 1, 1, 1, 2, 2}; return new int[] {1, 1, 1, 1, 2, 2};
case "CL":
case "IL": case "IL":
return new int[] {1, 2, 2, 2, 3, 2}; return new int[] {1, 2, 2, 3, 4, 2};
case "IM":
return new int[] {0, 2, 0, 1, 2, 2};
case "IN": case "IN":
return new int[] {1, 1, 3, 2, 3, 3}; return new int[] {1, 1, 2, 1, 2, 1};
case "IQ":
return new int[] {3, 2, 2, 3, 2, 2};
case "IR": case "IR":
return new int[] {3, 0, 1, 1, 4, 1}; return new int[] {4, 2, 3, 3, 4, 2};
case "IS":
return new int[] {0, 0, 1, 0, 0, 2};
case "IT": case "IT":
return new int[] {0, 0, 0, 1, 1, 2}; return new int[] {0, 0, 1, 1, 1, 2};
case "GI":
case "JE":
return new int[] {1, 2, 0, 1, 2, 2};
case "JM": case "JM":
return new int[] {2, 4, 3, 2, 2, 2}; return new int[] {2, 4, 2, 1, 2, 2};
case "JO": case "JO":
return new int[] {2, 1, 1, 2, 2, 2}; return new int[] {2, 0, 1, 1, 2, 2};
case "JP": case "JP":
return new int[] {0, 1, 1, 2, 2, 4}; return new int[] {0, 3, 3, 3, 4, 4};
case "KH":
return new int[] {2, 1, 4, 2, 2, 2};
case "CF":
case "KI":
return new int[] {4, 2, 4, 2, 2, 2};
case "FK":
case "KE": case "KE":
case "KP": return new int[] {3, 2, 2, 1, 2, 2};
return new int[] {3, 2, 2, 2, 2, 2}; case "KH":
return new int[] {1, 0, 4, 2, 2, 2};
case "CU":
case "KI":
return new int[] {4, 2, 4, 3, 2, 2};
case "CD":
case "KM":
return new int[] {4, 3, 3, 2, 2, 2};
case "KR": case "KR":
return new int[] {0, 1, 1, 3, 4, 4}; return new int[] {0, 2, 2, 4, 4, 4};
case "CY":
case "KW": case "KW":
return new int[] {1, 0, 0, 0, 0, 2}; return new int[] {1, 0, 1, 0, 0, 2};
case "BD":
case "KZ": case "KZ":
return new int[] {2, 1, 2, 2, 2, 2}; return new int[] {2, 1, 2, 2, 2, 2};
case "LA": case "LA":
return new int[] {1, 2, 1, 3, 2, 2}; return new int[] {1, 2, 1, 3, 2, 2};
case "BS":
case "LB": case "LB":
return new int[] {3, 3, 2, 4, 2, 2}; return new int[] {3, 2, 1, 2, 2, 2};
case "LK": case "LK":
return new int[] {3, 1, 3, 3, 4, 2}; return new int[] {3, 2, 3, 4, 4, 2};
case "CI":
case "DZ":
case "LR": case "LR":
return new int[] {3, 4, 4, 4, 2, 2}; return new int[] {3, 4, 3, 4, 2, 2};
case "LS":
return new int[] {3, 3, 2, 2, 2, 2};
case "LT":
return new int[] {0, 0, 0, 0, 2, 2};
case "LU": case "LU":
return new int[] {1, 0, 3, 2, 1, 4}; return new int[] {1, 1, 4, 2, 0, 2};
case "CY":
case "HR":
case "LV":
return new int[] {1, 0, 0, 0, 0, 2};
case "MA": case "MA":
return new int[] {3, 3, 1, 1, 2, 2}; return new int[] {3, 3, 2, 1, 2, 2};
case "MC": case "MC":
return new int[] {0, 2, 2, 0, 2, 2}; return new int[] {0, 2, 2, 0, 2, 2};
case "MD":
return new int[] {1, 0, 0, 0, 2, 2};
case "ME": case "ME":
return new int[] {2, 0, 0, 1, 2, 2}; return new int[] {2, 0, 0, 1, 1, 2};
case "MH":
return new int[] {4, 2, 1, 3, 2, 2};
case "MK": case "MK":
return new int[] {1, 0, 0, 1, 3, 2}; return new int[] {2, 0, 0, 1, 3, 2};
case "MM": case "MM":
return new int[] {2, 4, 2, 3, 2, 2}; return new int[] {2, 2, 2, 3, 4, 2};
case "MN": case "MN":
return new int[] {2, 0, 1, 2, 2, 2}; return new int[] {2, 0, 1, 2, 2, 2};
case "MO": case "MO":
case "MP": return new int[] {0, 2, 4, 4, 4, 2};
return new int[] {0, 2, 4, 4, 2, 2}; case "KG":
case "GP":
case "MQ": case "MQ":
return new int[] {2, 1, 2, 3, 2, 2}; return new int[] {2, 1, 1, 2, 2, 2};
case "MU": case "MR":
return new int[] {3, 1, 1, 2, 2, 2}; return new int[] {4, 2, 3, 4, 2, 2};
case "DK":
case "EE":
case "HU":
case "LT":
case "MT":
return new int[] {0, 0, 0, 0, 0, 2};
case "MV": case "MV":
return new int[] {3, 4, 1, 4, 2, 2}; return new int[] {3, 4, 1, 3, 3, 2};
case "MW": case "MW":
return new int[] {4, 2, 3, 3, 2, 2}; return new int[] {4, 2, 3, 3, 2, 2};
case "MX": case "MX":
return new int[] {2, 4, 3, 4, 2, 2}; return new int[] {3, 4, 4, 4, 2, 2};
case "MY": case "MY":
return new int[] {1, 0, 3, 1, 3, 2}; return new int[] {1, 0, 4, 1, 2, 2};
case "MZ": case "NA":
return new int[] {3, 1, 2, 1, 2, 2}; return new int[] {3, 4, 3, 2, 2, 2};
case "NC": case "NC":
return new int[] {3, 3, 4, 4, 2, 2}; return new int[] {3, 2, 3, 4, 2, 2};
case "NG": case "NG":
return new int[] {3, 4, 2, 1, 2, 2}; return new int[] {3, 4, 2, 1, 2, 2};
case "NI":
return new int[] {2, 3, 4, 3, 2, 2};
case "NL": case "NL":
return new int[] {0, 2, 2, 3, 0, 3}; return new int[] {0, 2, 3, 3, 0, 4};
case "CZ":
case "NO": case "NO":
return new int[] {0, 0, 2, 0, 1, 2}; return new int[] {0, 1, 2, 1, 1, 2};
case "NP": case "NP":
return new int[] {2, 2, 4, 3, 2, 2}; return new int[] {2, 1, 4, 3, 2, 2};
case "NR": case "NR":
return new int[] {4, 0, 3, 2, 2, 2};
case "NU": case "NU":
return new int[] {4, 2, 2, 1, 2, 2}; return new int[] {4, 2, 2, 1, 2, 2};
case "NZ":
return new int[] {1, 0, 2, 2, 4, 2};
case "OM": case "OM":
return new int[] {2, 3, 1, 3, 4, 2}; return new int[] {2, 3, 1, 3, 4, 2};
case "GU": case "PA":
return new int[] {2, 3, 3, 3, 2, 2};
case "PE": case "PE":
return new int[] {1, 2, 4, 4, 4, 2}; return new int[] {1, 2, 4, 4, 3, 2};
case "CK": case "AF":
case "PF":
return new int[] {2, 2, 2, 1, 2, 2};
case "ML":
case "PG": case "PG":
return new int[] {4, 3, 3, 2, 2, 2}; return new int[] {4, 3, 3, 3, 2, 2};
case "PH": case "PH":
return new int[] {2, 1, 3, 3, 3, 0}; return new int[] {2, 1, 3, 2, 2, 0};
case "NZ":
case "PL": case "PL":
return new int[] {1, 1, 2, 2, 4, 2}; return new int[] {2, 1, 2, 2, 4, 2};
case "PR": case "PR":
return new int[] {2, 0, 2, 1, 2, 1}; return new int[] {2, 0, 2, 0, 2, 1};
case "PS": case "PS":
return new int[] {3, 4, 1, 2, 2, 2}; return new int[] {3, 4, 1, 4, 2, 2};
case "PT":
return new int[] {1, 0, 0, 0, 1, 2};
case "PW": case "PW":
return new int[] {2, 2, 4, 1, 2, 2}; return new int[] {2, 2, 4, 2, 2, 2};
case "QA": case "BL":
return new int[] {2, 4, 4, 4, 4, 2};
case "MF": case "MF":
case "PY":
return new int[] {1, 2, 2, 2, 2, 2};
case "QA":
return new int[] {1, 4, 4, 4, 4, 2};
case "RE": case "RE":
return new int[] {1, 2, 1, 2, 2, 2}; return new int[] {1, 2, 2, 3, 1, 2};
case "RO": case "RO":
return new int[] {0, 0, 1, 2, 1, 2}; return new int[] {0, 0, 1, 2, 1, 2};
case "MD":
case "RS": case "RS":
return new int[] {1, 0, 0, 0, 2, 2}; return new int[] {2, 0, 0, 0, 2, 2};
case "RU": case "RU":
return new int[] {1, 0, 0, 0, 4, 3}; return new int[] {1, 0, 0, 0, 3, 3};
case "RW": case "RW":
return new int[] {3, 4, 2, 0, 2, 2}; return new int[] {3, 3, 1, 0, 2, 2};
case "MU":
case "SA": case "SA":
return new int[] {3, 1, 1, 1, 2, 2}; return new int[] {3, 1, 1, 2, 2, 2};
case "CF":
case "SB": case "SB":
return new int[] {4, 2, 4, 3, 2, 2}; return new int[] {4, 2, 4, 2, 2, 2};
case "SC":
return new int[] {4, 3, 1, 1, 2, 2};
case "SD":
return new int[] {4, 3, 4, 2, 2, 2};
case "SE":
return new int[] {0, 1, 1, 1, 0, 2};
case "SG": case "SG":
return new int[] {1, 1, 2, 2, 2, 1}; return new int[] {2, 3, 3, 3, 3, 3};
case "AQ": case "AQ":
case "ER": case "ER":
case "SH": case "SH":
return new int[] {4, 2, 2, 2, 2, 2}; return new int[] {4, 2, 2, 2, 2, 2};
case "GR":
case "HR":
case "SI":
return new int[] {1, 0, 0, 0, 1, 2};
case "BG": case "BG":
case "MT": case "ES":
case "SK": case "GR":
case "SI":
return new int[] {0, 0, 0, 0, 1, 2}; return new int[] {0, 0, 0, 0, 1, 2};
case "AX": case "IQ":
case "LI": case "SJ":
case "MS": return new int[] {3, 2, 2, 2, 2, 2};
case "PM": case "SK":
case "SM": return new int[] {1, 1, 1, 1, 3, 2};
return new int[] {0, 2, 2, 2, 2, 2}; case "GF":
case "PK":
case "SL":
return new int[] {3, 2, 3, 3, 2, 2};
case "ET":
case "SN": case "SN":
return new int[] {4, 4, 4, 3, 2, 2}; return new int[] {4, 4, 3, 2, 2, 2};
case "SO":
return new int[] {3, 2, 2, 4, 4, 2};
case "SR": case "SR":
return new int[] {2, 4, 3, 0, 2, 2}; return new int[] {2, 4, 3, 0, 2, 2};
case "SS":
return new int[] {4, 3, 2, 3, 2, 2};
case "ST": case "ST":
return new int[] {2, 2, 1, 2, 2, 2}; return new int[] {2, 2, 1, 2, 2, 2};
case "NI": case "PF":
case "PA":
case "SV": case "SV":
return new int[] {2, 3, 3, 3, 2, 2}; return new int[] {2, 3, 3, 1, 2, 2};
case "SZ": case "SZ":
return new int[] {3, 3, 3, 4, 2, 2}; return new int[] {4, 4, 3, 4, 2, 2};
case "SX":
case "TC": case "TC":
return new int[] {1, 2, 1, 0, 2, 2}; return new int[] {2, 2, 1, 3, 2, 2};
case "GA": case "GA":
case "TG": case "TG":
return new int[] {3, 4, 1, 0, 2, 2}; return new int[] {3, 4, 1, 0, 2, 2};
case "TH": case "TH":
return new int[] {0, 2, 2, 3, 3, 4}; return new int[] {0, 1, 2, 1, 2, 2};
case "TK":
return new int[] {2, 2, 2, 4, 2, 2};
case "CU":
case "DJ": case "DJ":
case "SY": case "SY":
case "TJ": case "TJ":
case "TL":
return new int[] {4, 3, 4, 4, 2, 2}; return new int[] {4, 3, 4, 4, 2, 2};
case "SC": case "GL":
case "TK":
return new int[] {2, 2, 2, 4, 2, 2};
case "TL":
return new int[] {4, 2, 4, 4, 2, 2};
case "SS":
case "TM": case "TM":
return new int[] {4, 2, 1, 1, 2, 2}; return new int[] {4, 2, 2, 3, 2, 2};
case "AZ":
case "GF":
case "LY":
case "PK":
case "SO":
case "TO":
return new int[] {3, 2, 3, 3, 2, 2};
case "TR": case "TR":
return new int[] {1, 1, 0, 0, 2, 2}; return new int[] {1, 0, 0, 1, 3, 2};
case "TT": case "TT":
return new int[] {1, 4, 1, 3, 2, 2}; return new int[] {1, 4, 0, 0, 2, 2};
case "EE":
case "IS":
case "LV":
case "PT":
case "SE":
case "TW": case "TW":
return new int[] {0, 0, 0, 0, 0, 2}; return new int[] {0, 2, 0, 0, 0, 0};
case "ML":
case "TZ": case "TZ":
return new int[] {3, 4, 3, 2, 2, 2}; return new int[] {3, 4, 2, 2, 2, 2};
case "IM":
case "UA": case "UA":
return new int[] {0, 2, 1, 1, 2, 2}; return new int[] {0, 1, 1, 2, 4, 2};
case "SL": case "LS":
case "UG": case "UG":
return new int[] {3, 3, 4, 3, 2, 2}; return new int[] {3, 3, 3, 2, 2, 2};
case "US": case "US":
return new int[] {1, 0, 2, 2, 3, 1}; return new int[] {1, 1, 4, 1, 3, 1};
case "AR":
case "KG":
case "TN": case "TN":
case "UY": case "UY":
return new int[] {2, 1, 1, 1, 2, 2}; return new int[] {2, 1, 1, 1, 2, 2};
case "UZ": case "UZ":
return new int[] {2, 2, 3, 4, 2, 2}; return new int[] {2, 2, 3, 4, 3, 2};
case "BL": case "AX":
case "CX": case "CX":
case "LI":
case "MP":
case "MS":
case "PM":
case "SM":
case "VA": case "VA":
return new int[] {1, 2, 2, 2, 2, 2}; return new int[] {0, 2, 2, 2, 2, 2};
case "AD":
case "BM":
case "BQ":
case "GD": case "GD":
case "GL":
case "KN": case "KN":
case "KY": case "KY":
case "LC": case "LC":
case "SX":
case "VC": case "VC":
return new int[] {1, 2, 0, 0, 2, 2}; return new int[] {1, 2, 0, 0, 2, 2};
case "VG": case "VG":
return new int[] {2, 2, 1, 1, 2, 2}; return new int[] {2, 2, 0, 1, 2, 2};
case "GG":
case "VI": case "VI":
return new int[] {0, 2, 0, 1, 2, 2}; return new int[] {0, 2, 1, 2, 2, 2};
case "VN": case "VN":
return new int[] {0, 3, 3, 4, 2, 2}; return new int[] {0, 0, 1, 2, 2, 1};
case "GH":
case "NA":
case "VU": case "VU":
return new int[] {3, 3, 3, 2, 2, 2}; return new int[] {4, 3, 3, 1, 2, 2};
case "IO": case "IO":
case "MH":
case "TV": case "TV":
case "WF": case "WF":
return new int[] {4, 2, 2, 4, 2, 2}; return new int[] {4, 2, 2, 4, 2, 2};
case "BT":
case "MZ":
case "WS": case "WS":
return new int[] {3, 1, 3, 1, 2, 2}; return new int[] {3, 1, 2, 1, 2, 2};
case "AL":
case "XK": case "XK":
return new int[] {1, 1, 1, 1, 2, 2}; return new int[] {1, 2, 1, 1, 2, 2};
case "BI": case "BI":
case "HT": case "HT":
case "KM":
case "MG": case "MG":
case "NE": case "NE":
case "SD":
case "TD": case "TD":
case "VE": case "VE":
case "YE": case "YE":
return new int[] {4, 4, 4, 4, 2, 2}; return new int[] {4, 4, 4, 4, 2, 2};
case "JE":
case "YT": case "YT":
return new int[] {4, 2, 2, 3, 2, 2}; return new int[] {2, 3, 3, 4, 2, 2};
case "ZA": case "ZA":
return new int[] {3, 2, 2, 1, 1, 2}; return new int[] {2, 3, 2, 1, 2, 2};
case "ZM": case "ZM":
return new int[] {3, 3, 4, 2, 2, 2}; return new int[] {4, 4, 4, 3, 3, 2};
case "MR": case "LY":
case "TO":
case "ZW": case "ZW":
return new int[] {4, 2, 4, 4, 2, 2}; return new int[] {3, 2, 4, 3, 2, 2};
default: default:
return new int[] {2, 2, 2, 2, 2, 2}; return new int[] {2, 2, 2, 2, 2, 2};
} }

View File

@ -2050,7 +2050,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer {
} }
private void handleFrameRendered(long presentationTimeUs) { private void handleFrameRendered(long presentationTimeUs) {
if (this != tunnelingOnFrameRenderedListener) { if (this != tunnelingOnFrameRenderedListener || getCodec() == null) {
// Stale event. // Stale event.
return; return;
} }

View File

@ -95,7 +95,6 @@ import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.AudioAttributes; import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.IllegalSeekPositionException;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Metadata; import androidx.media3.common.Metadata;
@ -112,6 +111,7 @@ import androidx.media3.common.TrackGroup;
import androidx.media3.common.Tracks; import androidx.media3.common.Tracks;
import androidx.media3.common.util.Assertions; import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.SystemClock;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.datasource.TransferListener; import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.analytics.AnalyticsListener; import androidx.media3.exoplayer.analytics.AnalyticsListener;
@ -930,31 +930,100 @@ public final class ExoPlayerTest {
} }
@Test @Test
public void illegalSeekPositionDoesThrow() throws Exception { public void seekTo_indexLargerThanPlaylist_isIgnored() throws Exception {
final IllegalSeekPositionException[] exception = new IllegalSeekPositionException[1]; ExoPlayer player = new TestExoPlayerBuilder(context).build();
ActionSchedule actionSchedule = player.setMediaItem(MediaItem.fromUri("http://test"));
new ActionSchedule.Builder(TAG)
.waitForPlaybackState(Player.STATE_BUFFERING) player.seekTo(/* mediaItemIndex= */ 1, /* positionMs= */ 1000);
.executeRunnable(
new PlayerRunnable() { assertThat(player.getCurrentMediaItemIndex()).isEqualTo(0);
@Override player.release();
public void run(ExoPlayer player) { }
try {
player.seekTo(/* mediaItemIndex= */ 100, /* positionMs= */ 0); @Test
} catch (IllegalSeekPositionException e) { public void addMediaItems_indexLargerThanPlaylist_addsToEndOfPlaylist() throws Exception {
exception[0] = e; ExoPlayer player = new TestExoPlayerBuilder(context).build();
} player.setMediaItem(MediaItem.fromUri("http://test"));
} ImmutableList<MediaItem> addedItems =
}) ImmutableList.of(MediaItem.fromUri("http://new1"), MediaItem.fromUri("http://new2"));
.waitForPlaybackState(Player.STATE_ENDED)
.build(); player.addMediaItems(/* index= */ 5000, addedItems);
new ExoPlayerTestRunner.Builder(context)
.setActionSchedule(actionSchedule) assertThat(player.getMediaItemCount()).isEqualTo(3);
.build() assertThat(player.getMediaItemAt(1)).isEqualTo(addedItems.get(0));
.start() assertThat(player.getMediaItemAt(2)).isEqualTo(addedItems.get(1));
.blockUntilActionScheduleFinished(TIMEOUT_MS) player.release();
.blockUntilEnded(TIMEOUT_MS); }
assertThat(exception[0]).isNotNull();
@Test
public void removeMediaItems_fromIndexLargerThanPlaylist_isIgnored() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setMediaItems(
ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")));
player.removeMediaItems(/* fromIndex= */ 5000, /* toIndex= */ 6000);
assertThat(player.getMediaItemCount()).isEqualTo(2);
player.release();
}
@Test
public void removeMediaItems_toIndexLargerThanPlaylist_removesUpToEndOfPlaylist()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
player.setMediaItems(
ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2")));
player.removeMediaItems(/* fromIndex= */ 1, /* toIndex= */ 6000);
assertThat(player.getMediaItemCount()).isEqualTo(1);
assertThat(player.getMediaItemAt(0).localConfiguration.uri.toString())
.isEqualTo("http://item1");
player.release();
}
@Test
public void moveMediaItems_fromIndexLargerThanPlaylist_isIgnored() throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
ImmutableList<MediaItem> items =
ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2"));
player.setMediaItems(items);
player.moveMediaItems(/* fromIndex= */ 5000, /* toIndex= */ 6000, /* newIndex= */ 0);
assertThat(player.getMediaItemAt(0)).isEqualTo(items.get(0));
assertThat(player.getMediaItemAt(1)).isEqualTo(items.get(1));
player.release();
}
@Test
public void moveMediaItems_toIndexLargerThanPlaylist_movesItemsUpToEndOfPlaylist()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
ImmutableList<MediaItem> items =
ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2"));
player.setMediaItems(items);
player.moveMediaItems(/* fromIndex= */ 1, /* toIndex= */ 6000, /* newIndex= */ 0);
assertThat(player.getMediaItemAt(0)).isEqualTo(items.get(1));
assertThat(player.getMediaItemAt(1)).isEqualTo(items.get(0));
player.release();
}
@Test
public void moveMediaItems_newIndexLargerThanPlaylist_movesItemsUpToEndOfPlaylist()
throws Exception {
ExoPlayer player = new TestExoPlayerBuilder(context).build();
ImmutableList<MediaItem> items =
ImmutableList.of(MediaItem.fromUri("http://item1"), MediaItem.fromUri("http://item2"));
player.setMediaItems(items);
player.moveMediaItems(/* fromIndex= */ 0, /* toIndex= */ 1, /* newIndex= */ 5000);
assertThat(player.getMediaItemAt(0)).isEqualTo(items.get(1));
assertThat(player.getMediaItemAt(1)).isEqualTo(items.get(0));
player.release();
} }
@Test @Test
@ -2523,7 +2592,7 @@ public final class ExoPlayerTest {
.build() .build()
.start() .start()
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET); assertThat(target.positionMs).isEqualTo(C.TIME_UNSET);
} }
@Test @Test
@ -2545,7 +2614,7 @@ public final class ExoPlayerTest {
.build() .build()
.start() .start()
.blockUntilEnded(TIMEOUT_MS); .blockUntilEnded(TIMEOUT_MS);
assertThat(target.positionMs).isEqualTo(C.POSITION_UNSET); assertThat(target.positionMs).isEqualTo(C.TIME_UNSET);
} }
@Test @Test
@ -10407,7 +10476,9 @@ public final class ExoPlayerTest {
new Metadata( new Metadata(
new BinaryFrame(/* id= */ "", /* data= */ new byte[0]), new BinaryFrame(/* id= */ "", /* data= */ new byte[0]),
new TextInformationFrame( new TextInformationFrame(
/* id= */ "TT2", /* description= */ null, /* value= */ "title"))) /* id= */ "TT2",
/* description= */ null,
/* values= */ ImmutableList.of("title"))))
.build(); .build();
// Set multiple values together. // Set multiple values together.
@ -11897,7 +11968,11 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(context) new TestExoPlayerBuilder(context)
.setRenderersFactory( .setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> { (handler, videoListener, audioListener, textOutput, metadataOutput) -> {
videoRenderer.set(new FakeVideoRenderer(handler, videoListener)); videoRenderer.set(
new FakeVideoRenderer(
SystemClock.DEFAULT.createHandler(
handler.getLooper(), /* callback= */ null),
videoListener));
return new Renderer[] {videoRenderer.get()}; return new Renderer[] {videoRenderer.get()};
}) })
.build(); .build();
@ -11999,10 +12074,20 @@ public final class ExoPlayerTest {
@Test @Test
@Config(sdk = Config.ALL_SDKS) @Config(sdk = Config.ALL_SDKS)
public void builder_inBackgroundThread_doesNotThrow() throws Exception { public void builder_inBackgroundThreadWithAllowedAnyThreadMethods_doesNotThrow()
throws Exception {
Thread builderThread = Thread builderThread =
new Thread( new Thread(
() -> new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build()); () -> {
ExoPlayer player =
new ExoPlayer.Builder(ApplicationProvider.getApplicationContext()).build();
player.addListener(new Listener() {});
player.addAnalyticsListener(new AnalyticsListener() {});
player.addAudioOffloadListener(new ExoPlayer.AudioOffloadListener() {});
player.getClock();
player.getApplicationLooper();
player.getPlaybackLooper();
});
AtomicReference<Throwable> builderThrow = new AtomicReference<>(); AtomicReference<Throwable> builderThrow = new AtomicReference<>();
builderThread.setUncaughtExceptionHandler((thread, throwable) -> builderThrow.set(throwable)); builderThread.setUncaughtExceptionHandler((thread, throwable) -> builderThrow.set(throwable));
@ -12034,7 +12119,12 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setRenderersFactory( .setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> (handler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {new FakeVideoRenderer(handler, videoListener)}) new Renderer[] {
new FakeVideoRenderer(
SystemClock.DEFAULT.createHandler(
handler.getLooper(), /* callback= */ null),
videoListener)
})
.build(); .build();
AnalyticsListener listener = mock(AnalyticsListener.class); AnalyticsListener listener = mock(AnalyticsListener.class);
player.addAnalyticsListener(listener); player.addAnalyticsListener(listener);
@ -12059,7 +12149,12 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()) new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
.setRenderersFactory( .setRenderersFactory(
(handler, videoListener, audioListener, textOutput, metadataOutput) -> (handler, videoListener, audioListener, textOutput, metadataOutput) ->
new Renderer[] {new FakeVideoRenderer(handler, videoListener)}) new Renderer[] {
new FakeVideoRenderer(
SystemClock.DEFAULT.createHandler(
handler.getLooper(), /* callback= */ null),
videoListener)
})
.build(); .build();
Player.Listener listener = mock(Player.Listener.class); Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener); player.addListener(listener);
@ -12275,7 +12370,7 @@ public final class ExoPlayerTest {
public PositionGrabbingMessageTarget() { public PositionGrabbingMessageTarget() {
mediaItemIndex = C.INDEX_UNSET; mediaItemIndex = C.INDEX_UNSET;
positionMs = C.POSITION_UNSET; positionMs = C.TIME_UNSET;
} }
@Override @Override

Some files were not shown because too many files have changed in this diff Show More