Merge pull request #88 from androidx/main-r1.0.0-beta01

r1.0.0 beta01
This commit is contained in:
Ian Baker 2022-06-17 11:42:16 +01:00 committed by GitHub
commit 2c7201024e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
806 changed files with 41539 additions and 13020 deletions

View File

@ -1,42 +0,0 @@
---
name: Bug report
about: Issue template for a bug report.
title: ''
labels: bug, needs triage
assignees: ''
---
We can only process bug reports that are actionable. Unclear bug reports or
reports with insufficient information may not get attention.
Before filing a bug:
-------------------------
- Search existing issues, including issues that are closed:
https://github.com/androidx/media/issues?q=is%3Aissue
- For ExoPlayer-related bugs, please also check the ExoPlayer tracker:
https://github.com/google/ExoPlayer/issues?q=is%3Aissue
When reporting a bug:
-------------------------
Describe how the issue can be reproduced, ideally using one of the demo apps
or a small sample app that youre able to share as source code on GitHub. To
increase the chance of your issue getting attention, please also include:
- Clear reproduction steps including observed and expected behavior
- Output of running "adb bugreport" in the console shortly after encountering
the issue
- URI to test content for reproduction
- For protected content:
- DRM scheme and license server URL
- Authentication HTTP headers
- AndroidX Media version number
- Android version
- Android device
If there's something you don't want to post publicly, please submit the issue,
then email the link/bug report to dev.exoplayer@gmail.com using a subject in the
format "Issue #1234", where #1234 is your issue number (we don't reply to
emails).

100
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@ -0,0 +1,100 @@
name: Bug Report
description: Report a bug in the Media3 library
labels: ["bug", "needs triage"]
body:
- type: markdown
attributes:
value: |
We can only process bug reports that are actionable. Unclear bug reports or reports with insufficient information may not get attention.
Before filing a bug:
-------------------------
- Search existing issues, including issues that are closed: https://github.com/androidx/media/issues?q=is%3Aissue
- For ExoPlayer-related bugs, please also check the ExoPlayer tracker: https://github.com/google/ExoPlayer/issues?q=is%3Aissue
- type: dropdown
attributes:
label: Media3 Version
description: What version of Media3 are you using?
options:
- 1.0.0-beta01
- 1.0.0-alpha03
- 1.0.0-alpha02
- 1.0.0-alpha01
validations:
required: true
- type: textarea
attributes:
label: Devices that reproduce the issue
placeholder: |
Example:
* Pixel 4 running Android 12
* Samsung S21 running Android 11
validations:
required: true
- type: textarea
attributes:
label: Devices that do not reproduce the issue
placeholder: |
Example:
* Pixel 3 running Android Pie
- type: dropdown
attributes:
label: Reproducible in the demo app?
description: Please try and reproduce the issue in the [Media3 demo app](https://github.com/androidx/media/tree/release/demos/main).
options:
- "Yes"
- "No"
- Not tested
validations:
required: true
- type: textarea
attributes:
label: Reproduction steps
description: Clear and complete steps we can use to reproduce the problem
placeholder: |
Example:
1. Play the attached media in the demo app
2. Seek forward 10s
validations:
required: true
- type: textarea
attributes:
label: Expected result
placeholder: |
Example:
The media plays successfully
validations:
required: true
- type: textarea
attributes:
label: Actual result
placeholder: |
Example:
Playback crashes with the following stack trace:
...
validations:
required: true
- type: textarea
attributes:
label: Media
description: |
Media we can use to reproduce the problem. Either:
* Attach a file here
* Include a media URL
* Refer to a piece of media from the demo app (e.g. `Misc > Dizzy (MP4)`)
* If you don't want to post media publicly please email the info to dev.exoplayer@gmail.com with subject 'Issue #\<issuenumber\>' after filing this issue, and note that you will do this here.
* If you are certain the issue does not depend on the media being played, enter "Not applicable" here.
For DRM-protected media please also include the scheme and license server URL.
validations:
required: true
- type: checkboxes
attributes:
label: Bug Report
description: |
After filing this issue please run `adb bugreport` shortly after reproducing the problem (ideally in the [demo app](https://github.com/androidx/media/tree/release/demos/main)) to capture a zip file, and email this to dev.exoplayer@gmail.com with subject 'Issue #\<issuenumber\>'.
**Note:** Logcat output is **not** the same as a full bug report, and is often missing information that's useful for diagnosing issues. Please ensure you're sending a full bug report zip file.
options:
- label: You will email the zip file produced by `adb bugreport` to dev.exoplayer@gmail.com after filing this issue.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1 @@
blank_issues_enabled: false

View File

@ -1,4 +1,230 @@
# Release notes Release notes
### Unreleased changes
* Extractors:
* Add support for AVI
([#2092](https://github.com/google/ExoPlayer/issues/2092)).
* RTSP:
* Add RTP reader for H263
([#63](https://github.com/androidx/media/pull/63)).
### 1.0.0-beta01 (2022-06-16)
This release corresponds to the
[ExoPlayer 2.18.0 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.0).
* Core library:
* Enable support for Android platform diagnostics via
`MediaMetricsManager`. ExoPlayer will forward playback events and
performance data to the platform, which helps to provide system
performance and debugging information on the device. This data may also
be collected by Google
[if sharing usage and diagnostics data is enabled](https://support.google.com/accounts/answer/6078260)
by the user of the device. Apps can opt-out of contributing to platform
diagnostics for ExoPlayer with
`ExoPlayer.Builder.setUsePlatformDiagnostics(false)`.
* Fix bug that tracks are reset too often when using `MergingMediaSource`,
for example when side-loading subtitles and changing the selected
subtitle mid-playback
([#10248](https://github.com/google/ExoPlayer/issues/10248)).
* Stop detecting 5G-NSA network type on API 29 and 30. These playbacks
will assume a 4G network.
* Disallow passing `null` to
`MediaSource.Factory.setDrmSessionManagerProvider` and
`MediaSource.Factory.setLoadErrorHandlingPolicy`. Instances of
`DefaultDrmSessionManagerProvider` and `DefaultLoadErrorHandlingPolicy`
can be passed explicitly if required.
* Add `MediaItem.RequestMetadata` to represent metadata needed to play
media when the exact `LocalConfiguration` is not known. Also remove
`MediaMetadata.mediaUrl` as this is now included in `RequestMetadata`.
* Add `Player.Command.COMMAND_SET_MEDIA_ITEM` to enable players to allow
setting a single item.
* Track selection:
* Flatten `TrackSelectionOverrides` class into `TrackSelectionParameters`,
and promote `TrackSelectionOverride` to a top level class.
* Rename `TracksInfo` to `Tracks` and `TracksInfo.TrackGroupInfo` to
`Tracks.Group`. `Player.getCurrentTracksInfo` and
`Player.Listener.onTracksInfoChanged` have also been renamed to
`Player.getCurrentTracks` and `Player.Listener.onTracksChanged`.
* Change `DefaultTrackSelector.buildUponParameters` and
`DefaultTrackSelector.Parameters.buildUpon` to return
`DefaultTrackSelector.Parameters.Builder` instead of the deprecated
`DefaultTrackSelector.ParametersBuilder`.
* Add
`DefaultTrackSelector.Parameters.constrainAudioChannelCountToDeviceCapabilities`
which is enabled by default. When enabled, the `DefaultTrackSelector`
will prefer audio tracks whose channel count does not exceed the device
output capabilities. On handheld devices, the `DefaultTrackSelector`
will prefer stereo/mono over multichannel audio formats, unless the
multichannel format can be
[Spatialized](https://developer.android.com/reference/android/media/Spatializer)
(Android 12L+) or is a Dolby surround sound format. In addition, on
devices that support audio spatialization, the `DefaultTrackSelector`
will monitor for changes in the
[Spatializer properties](https://developer.android.com/reference/android/media/Spatializer.OnSpatializerStateChangedListener)
and trigger a new track selection upon these. Devices with a
`television`
[UI mode](https://developer.android.com/guide/topics/resources/providing-resources#UiModeQualifier)
are excluded from these constraints and the format with the highest
channel count will be preferred. To enable this feature, the
`DefaultTrackSelector` instance must be constructed with a `Context`.
* Video:
* Rename `DummySurface` to `PlaceholderSurface`.
* Add AV1 support to the `MediaCodecVideoRenderer.getCodecMaxInputSize`.
* Audio:
* Use LG AC3 audio decoder advertising non-standard MIME type.
* Change the return type of `AudioAttributes.getAudioAttributesV21()` from
`android.media.AudioAttributes` to a new `AudioAttributesV21` wrapper
class, to prevent slow ART verification on API < 21.
* Query the platform (API 29+) or assume the audio encoding channel count
for audio passthrough when the format audio channel count is unset,
which occurs with HLS chunkless preparation
([10204](https://github.com/google/ExoPlayer/issues/10204)).
* Configure `AudioTrack` with channel mask
`AudioFormat.CHANNEL_OUT_7POINT1POINT4` if the decoder outputs 12
channel PCM audio
([#10322](#https://github.com/google/ExoPlayer/pull/10322).
* DRM
* Ensure the DRM session is always correctly updated when seeking
immediately after a format change
([10274](https://github.com/google/ExoPlayer/issues/10274)).
* Text:
* Change `Player.getCurrentCues()` to return `CueGroup` instead of
`List<Cue>`.
* SSA: Support `OutlineColour` style setting when `BorderStyle == 3` (i.e.
`OutlineColour` sets the background of the cue)
([#8435](https://github.com/google/ExoPlayer/issues/8435)).
* CEA-708: Parse data into multiple service blocks and ignore blocks not
associated with the currently selected service number.
* Remove `RawCcExtractor`, which was only used to handle a Google-internal
subtitle format.
* Extractors:
* Matroska: Parse `DiscardPadding` for Opus tracks.
* MP4: Parse bitrates from `esds` boxes.
* Ogg: Allow duplicate Opus ID and comment headers
([#10038](https://github.com/google/ExoPlayer/issues/10038)).
* UI:
* Fix delivery of events to `OnClickListener`s set on `PlayerView`, in the
case that `useController=false`
([#9605](https://github.com/google/ExoPlayer/issues/9605)). Also fix
delivery of events to `OnLongClickListener` for all view configurations.
* Fix incorrectly treating a sequence of touch events that exit the bounds
of `PlayerView` before `ACTION_UP` as a click
([#9861](https://github.com/google/ExoPlayer/issues/9861)).
* Fix `PlayerView` accessibility issue where tapping might toggle playback
rather than hiding the controls
([#8627](https://github.com/google/ExoPlayer/issues/8627)).
* Rewrite `TrackSelectionView` and `TrackSelectionDialogBuilder` to work
with the `Player` interface rather than `ExoPlayer`. This allows the
views to be used with other `Player` implementations, and removes the
dependency from the UI module to the ExoPlayer module. This is a
breaking change.
* Don't show forced text tracks in the `PlayerView` track selector, and
keep a suitable forced text track selected if "None" is selected
([#9432](https://github.com/google/ExoPlayer/issues/9432)).
* DASH:
* Parse channel count from DTS `AudioChannelConfiguration` elements. This
re-enables audio passthrough for DTS streams
([#10159](https://github.com/google/ExoPlayer/issues/10159)).
* Disallow passing `null` to
`DashMediaSource.Factory.setCompositeSequenceableLoaderFactory`.
Instances of `DefaultCompositeSequenceableLoaderFactory` can be passed
explicitly if required.
* HLS:
* Fallback to chunkful preparation if the playlist CODECS attribute does
not contain the audio codec
([#10065](https://github.com/google/ExoPlayer/issues/10065)).
* Disallow passing `null` to
`HlsMediaSource.Factory.setCompositeSequenceableLoaderFactory`,
`HlsMediaSource.Factory.setPlaylistParserFactory`, and
`HlsMediaSource.Factory.setPlaylistTrackerFactory`. Instances of
`DefaultCompositeSequenceableLoaderFactory`,
`DefaultHlsPlaylistParserFactory`, or a reference to
`DefaultHlsPlaylistTracker.FACTORY` can be passed explicitly if
required.
* Smooth Streaming:
* Disallow passing `null` to
`SsMediaSource.Factory.setCompositeSequenceableLoaderFactory`. Instances
of `DefaultCompositeSequenceableLoaderFactory` can be passed explicitly
if required.
* RTSP:
* Add RTP reader for MPEG4
([#35](https://github.com/androidx/media/pull/35)).
* Add RTP reader for HEVC
([#36](https://github.com/androidx/media/pull/36)).
* Add RTP reader for AMR. Currently only mono-channel, non-interleaved AMR
streams are supported. Compound AMR RTP payload is not supported.
([#46](https://github.com/androidx/media/pull/46))
* Add RTP reader for VP8
([#47](https://github.com/androidx/media/pull/47)).
* Add RTP reader for WAV
([#56](https://github.com/androidx/media/pull/56)).
* Fix RTSP basic authorization header.
([#9544](https://github.com/google/ExoPlayer/issues/9544)).
* Stop checking mandatory SDP fields as ExoPlayer doesn't need them
([#10049](https://github.com/google/ExoPlayer/issues/10049)).
* Throw checked exception when parsing RTSP timing
([#10165](https://github.com/google/ExoPlayer/issues/10165)).
* Add RTP reader for VP9
([#47](https://github.com/androidx/media/pull/64)).
* Add RTP reader for OPUS
([#53](https://github.com/androidx/media/pull/53)).
* Session:
* Replace `MediaSession.MediaItemFiller` with
`MediaSession.Callback.onAddMediaItems` to allow asynchronous resolution
of requests.
* Support `setMediaItems(s)` methods when `MediaController` connects to a
legacy media session.
* Remove `MediaController.setMediaUri` and
`MediaSession.Callback.onSetMediaUri`. The same functionality can be
achieved by using `MediaController.setMediaItem` and
`MediaSession.Callback.onAddMediaItems`.
* Forward legacy `MediaController` calls to play media to
`MediaSession.Callback.onAddMediaItems` instead of `onSetMediaUri`.
* Add `MediaNotification.Provider` and `DefaultMediaNotificationProvider`
to provide customization of the notification.
* Add `BitmapLoader` and `SimpleBitmapLoader` for downloading artwork
images.
* Add `MediaSession.setCustomLayout()` to provide backwards compatibility
with the legacy session.
* Add `MediaSession.setSessionExtras()` to provide feature parity with
legacy session.
* Rename `MediaSession.MediaSessionCallback` to `MediaSession.Callback`,
`MediaLibrarySession.MediaLibrarySessionCallback` to
`MediaLibrarySession.Callback` and
`MediaSession.Builder.setSessionCallback` to `setCallback`.
* Fix NPE in `MediaControllerImplLegacy`
([#59](https://github.com/androidx/media/pull/59)).
* Update session position info on timeline
change([#51](https://github.com/androidx/media/issues/51)).
* Fix NPE in `MediaControllerImplBase` after releasing controller
([#74](https://github.com/androidx/media/issues/74)).
* Fix `IndexOutOfBoundsException` when setting less media items than in
the current playlist
([#86](https://github.com/androidx/media/issues/86)).
* Ad playback / IMA:
* Decrease ad polling rate from every 100ms to every 200ms, to line up
with Media Rating Council (MRC) recommendations.
* FFmpeg extension:
* Update CMake version to `3.21.0+` to avoid a CMake bug causing
AndroidStudio's gradle sync to fail
([#9933](https://github.com/google/ExoPlayer/issues/9933)).
* Remove deprecated symbols:
* Remove `Player.Listener.onTracksChanged`. Use
`Player.Listener.onTracksInfoChanged` instead.
* Remove `Player.getCurrentTrackGroups` and
`Player.getCurrentTrackSelections`. Use `Player.getCurrentTracksInfo`
instead. You can also continue to use `ExoPlayer.getCurrentTrackGroups`
and `ExoPlayer.getCurrentTrackSelections`, although these methods remain
deprecated.
* Remove `DownloadHelper`
`DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT` and
`DEFAULT_TRACK_SELECTOR_PARAMETERS` constants. Use
`getDefaultTrackSelectorParameters(Context)` instead when possible, and
`DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise.
* Remove constructor `DefaultTrackSelector(ExoTrackSelection.Factory)`.
Use `DefaultTrackSelector(Context, ExoTrackSelection.Factory)` instead.
### 1.0.0-alpha03 (2022-03-14) ### 1.0.0-alpha03 (2022-03-14)

1800
api.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@ -17,7 +17,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.0.3' classpath 'com.android.tools.build:gradle:7.2.1'
classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2'
classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21'
} }

View File

@ -29,5 +29,10 @@ android {
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
testOptions.unitTests.includeAndroidResources = true testOptions {
unitTests.all {
jvmArgs "-Xmx2g"
}
unitTests.includeAndroidResources true
}
} }

View File

@ -12,21 +12,21 @@
// 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-alpha03' releaseVersion = '1.0.0-beta01'
releaseVersionCode = 1_000_000_0_03 releaseVersionCode = 1_000_000_1_01
minSdkVersion = 16 minSdkVersion = 16
appTargetSdkVersion = 29 appTargetSdkVersion = 29
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some // Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
// additional robolectric config. // additional robolectric config.
targetSdkVersion = 30 targetSdkVersion = 30
compileSdkVersion = 31 compileSdkVersion = 32
dexmakerVersion = '2.28.1' dexmakerVersion = '2.28.1'
junitVersion = '4.13.2' junitVersion = '4.13.2'
// Use the same Guava version as the Android repo: // Use the same Guava version as the Android repo:
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
guavaVersion = '31.0.1-android' guavaVersion = '31.0.1-android'
mockitoVersion = '3.12.4' mockitoVersion = '3.12.4'
robolectricVersion = '4.6.1' robolectricVersion = '4.8.1'
// Keep this in sync with Google's internal Checker Framework version. // Keep this in sync with Google's internal Checker Framework version.
checkerframeworkVersion = '3.13.0' checkerframeworkVersion = '3.13.0'
checkerframeworkCompatVersion = '2.5.5' checkerframeworkCompatVersion = '2.5.5'

View File

@ -87,3 +87,7 @@ include modulePrefix + 'test-data'
project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data') project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data')
include modulePrefix + 'test-utils' include modulePrefix + 'test-utils'
project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils') project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils')
include modulePrefix + 'test-session-common'
project(modulePrefix + 'test-session-common').projectDir = new File(rootDir, 'libraries/test_session_common')
include modulePrefix + 'test-session-current'
project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'libraries/test_session_current')

View File

@ -230,8 +230,8 @@ public class MainActivity extends AppCompatActivity
@Override @Override
public boolean onMove( public boolean onMove(
RecyclerView list, RecyclerView.ViewHolder origin, RecyclerView.ViewHolder target) { RecyclerView list, RecyclerView.ViewHolder origin, RecyclerView.ViewHolder target) {
int fromPosition = origin.getAdapterPosition(); int fromPosition = origin.getBindingAdapterPosition();
int toPosition = target.getAdapterPosition(); int toPosition = target.getBindingAdapterPosition();
if (draggingFromPosition == C.INDEX_UNSET) { if (draggingFromPosition == C.INDEX_UNSET) {
// A drag has started, but changes to the media queue will be reflected in clearView(). // A drag has started, but changes to the media queue will be reflected in clearView().
draggingFromPosition = fromPosition; draggingFromPosition = fromPosition;
@ -243,7 +243,7 @@ public class MainActivity extends AppCompatActivity
@Override @Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition(); int position = viewHolder.getBindingAdapterPosition();
QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder;
if (playerManager.removeItem(queueItemHolder.item)) { if (playerManager.removeItem(queueItemHolder.item)) {
mediaQueueListAdapter.notifyItemRemoved(position); mediaQueueListAdapter.notifyItemRemoved(position);
@ -282,7 +282,7 @@ public class MainActivity extends AppCompatActivity
@Override @Override
public void onClick(View v) { public void onClick(View v) {
playerManager.selectQueueItem(getAdapterPosition()); playerManager.selectQueueItem(getBindingAdapterPosition());
} }
} }

View File

@ -26,7 +26,7 @@ import androidx.media3.common.Player;
import androidx.media3.common.Player.DiscontinuityReason; import androidx.media3.common.Player.DiscontinuityReason;
import androidx.media3.common.Player.TimelineChangeReason; import androidx.media3.common.Player.TimelineChangeReason;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.TracksInfo; import androidx.media3.common.Tracks;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.ui.PlayerControlView; import androidx.media3.ui.PlayerControlView;
import androidx.media3.ui.PlayerView; import androidx.media3.ui.PlayerView;
@ -57,7 +57,7 @@ import java.util.ArrayList;
private final ArrayList<MediaItem> mediaQueue; private final ArrayList<MediaItem> mediaQueue;
private final Listener listener; private final Listener listener;
private TracksInfo lastSeenTrackGroupInfo; private Tracks lastSeenTracks;
private int currentItemIndex; private int currentItemIndex;
private Player currentPlayer; private Player currentPlayer;
@ -219,19 +219,19 @@ import java.util.ArrayList;
} }
@Override @Override
public void onTracksInfoChanged(TracksInfo tracksInfo) { public void onTracksChanged(Tracks tracks) {
if (currentPlayer != localPlayer || tracksInfo == lastSeenTrackGroupInfo) { if (currentPlayer != localPlayer || tracks == lastSeenTracks) {
return; return;
} }
if (!tracksInfo.isTypeSupportedOrEmpty( if (tracks.containsType(C.TRACK_TYPE_VIDEO)
C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) { && !tracks.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) {
listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO); listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO);
} }
if (!tracksInfo.isTypeSupportedOrEmpty( if (tracks.containsType(C.TRACK_TYPE_AUDIO)
C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) { && !tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) {
listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO); listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO);
} }
lastSeenTrackGroupInfo = tracksInfo; lastSeenTracks = tracks;
} }
// CastPlayer.SessionAvailabilityListener implementation. // CastPlayer.SessionAvailabilityListener implementation.

View File

@ -27,6 +27,7 @@ import android.graphics.drawable.BitmapDrawable;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.opengl.GLUtils; import android.opengl.GLUtils;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import java.io.IOException; import java.io.IOException;
import java.util.Locale; import java.util.Locale;
@ -50,7 +51,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Bitmap logoBitmap; private final Bitmap logoBitmap;
private final Canvas overlayCanvas; private final Canvas overlayCanvas;
private GlUtil.@MonotonicNonNull Program program; private @MonotonicNonNull GlProgram program;
private float bitmapScaleX; private float bitmapScaleX;
private float bitmapScaleY; private float bitmapScaleY;
@ -78,7 +79,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
public void initialize() { public void initialize() {
try { try {
program = program =
new GlUtil.Program( new GlProgram(
context, context,
/* vertexShaderFilePath= */ "bitmap_overlay_video_processor_vertex.glsl", /* vertexShaderFilePath= */ "bitmap_overlay_video_processor_vertex.glsl",
/* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl"); /* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl");
@ -86,9 +87,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
program.setBufferAttribute( program.setBufferAttribute(
"aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); "aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
program.setBufferAttribute( program.setBufferAttribute(
"aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); "aTexCoords",
GlUtil.getTextureCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
GLES20.glGenTextures(1, textures, 0); GLES20.glGenTextures(1, textures, 0);
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST);
@ -117,9 +122,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
GlUtil.checkGlError(); GlUtil.checkGlError();
// Run the shader program. // Run the shader program.
GlUtil.Program program = checkNotNull(this.program); GlProgram program = checkNotNull(this.program);
program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* unit= */ 0); program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* texUnitIndex= */ 0);
program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* unit= */ 1); program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* texUnitIndex= */ 1);
program.setFloatUniform("uScaleX", bitmapScaleX); program.setFloatUniform("uScaleX", bitmapScaleX);
program.setFloatUniform("uScaleY", bitmapScaleY); program.setFloatUniform("uScaleY", bitmapScaleY);
program.setFloatsUniform("uTexTransform", transformMatrix); program.setFloatsUniform("uTexTransform", transformMatrix);

View File

@ -20,6 +20,7 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -32,7 +33,6 @@ import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.dash.DashMediaSource; import androidx.media3.exoplayer.dash.DashMediaSource;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager; import androidx.media3.exoplayer.drm.DefaultDrmSessionManager;
@ -144,7 +144,7 @@ public final class MainActivity extends Activity {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); DataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory();
HttpMediaDrmCallback drmCallback = HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager = drmSessionManager =
@ -157,13 +157,18 @@ public final class MainActivity extends Activity {
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this); DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
MediaSource mediaSource; MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); @Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA);
if (type == C.TYPE_DASH) { @C.ContentType
int type =
TextUtils.isEmpty(fileExtension)
? Util.inferContentType(uri)
: Util.inferContentTypeForExtension(fileExtension);
if (type == C.CONTENT_TYPE_DASH) {
mediaSource = mediaSource =
new DashMediaSource.Factory(dataSourceFactory) new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri)); .createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) { } else if (type == C.CONTENT_TYPE_OTHER) {
mediaSource = mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory) new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
@ -181,7 +186,7 @@ public final class MainActivity extends Activity {
Assertions.checkNotNull(this.videoProcessingGLSurfaceView); Assertions.checkNotNull(this.videoProcessingGLSurfaceView);
videoProcessingGLSurfaceView.setPlayer(player); videoProcessingGLSurfaceView.setPlayer(player);
Assertions.checkNotNull(playerView).setPlayer(player); Assertions.checkNotNull(playerView).setPlayer(player);
player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null)); player.addAnalyticsListener(new EventLogger());
this.player = player; this.player = player;
} }

View File

@ -20,6 +20,7 @@ import static androidx.media3.demo.main.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_I
import android.app.Notification; import android.app.Notification;
import android.content.Context; import android.content.Context;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.util.NotificationUtil; import androidx.media3.common.util.NotificationUtil;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.offline.Download; import androidx.media3.exoplayer.offline.Download;
@ -32,6 +33,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler;
import java.util.List; import java.util.List;
/** A service for downloading media. */ /** A service for downloading media. */
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public class DemoDownloadService extends DownloadService { public class DemoDownloadService extends DownloadService {
private static final int JOB_ID = 1; private static final int JOB_ID = 1;

View File

@ -16,12 +16,12 @@
package androidx.media3.demo.main; package androidx.media3.demo.main;
import android.content.Context; import android.content.Context;
import androidx.annotation.OptIn;
import androidx.media3.database.DatabaseProvider; import androidx.media3.database.DatabaseProvider;
import androidx.media3.database.StandaloneDatabaseProvider; import androidx.media3.database.StandaloneDatabaseProvider;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.Cache;
import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.CacheDataSource;
import androidx.media3.datasource.cache.NoOpCacheEvictor; import androidx.media3.datasource.cache.NoOpCacheEvictor;
@ -59,7 +59,7 @@ public final class DemoUtil {
private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads";
private static DataSource.@MonotonicNonNull Factory dataSourceFactory; private static DataSource.@MonotonicNonNull Factory dataSourceFactory;
private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory; private static DataSource.@MonotonicNonNull Factory httpDataSourceFactory;
private static @MonotonicNonNull DatabaseProvider databaseProvider; private static @MonotonicNonNull DatabaseProvider databaseProvider;
private static @MonotonicNonNull File downloadDirectory; private static @MonotonicNonNull File downloadDirectory;
private static @MonotonicNonNull Cache downloadCache; private static @MonotonicNonNull Cache downloadCache;
@ -72,6 +72,7 @@ public final class DemoUtil {
return BuildConfig.USE_DECODER_EXTENSIONS; return BuildConfig.USE_DECODER_EXTENSIONS;
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public static RenderersFactory buildRenderersFactory( public static RenderersFactory buildRenderersFactory(
Context context, boolean preferExtensionRenderer) { Context context, boolean preferExtensionRenderer) {
@DefaultRenderersFactory.ExtensionRendererMode @DefaultRenderersFactory.ExtensionRendererMode
@ -85,7 +86,7 @@ public final class DemoUtil {
.setExtensionRendererMode(extensionRendererMode); .setExtensionRendererMode(extensionRendererMode);
} }
public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) { public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) {
if (httpDataSourceFactory == null) { if (httpDataSourceFactory == null) {
if (USE_CRONET_FOR_NETWORKING) { if (USE_CRONET_FOR_NETWORKING) {
context = context.getApplicationContext(); context = context.getApplicationContext();
@ -117,6 +118,7 @@ public final class DemoUtil {
return dataSourceFactory; return dataSourceFactory;
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public static synchronized DownloadNotificationHelper getDownloadNotificationHelper( public static synchronized DownloadNotificationHelper getDownloadNotificationHelper(
Context context) { Context context) {
if (downloadNotificationHelper == null) { if (downloadNotificationHelper == null) {
@ -136,6 +138,7 @@ public final class DemoUtil {
return downloadTracker; return downloadTracker;
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static synchronized Cache getDownloadCache(Context context) { private static synchronized Cache getDownloadCache(Context context) {
if (downloadCache == null) { if (downloadCache == null) {
File downloadContentDirectory = File downloadContentDirectory =
@ -147,6 +150,7 @@ public final class DemoUtil {
return downloadCache; return downloadCache;
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static synchronized void ensureDownloadManagerInitialized(Context context) { private static synchronized void ensureDownloadManagerInitialized(Context context) {
if (downloadManager == null) { if (downloadManager == null) {
downloadManager = downloadManager =
@ -161,6 +165,7 @@ public final class DemoUtil {
} }
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static synchronized DatabaseProvider getDatabaseProvider(Context context) { private static synchronized DatabaseProvider getDatabaseProvider(Context context) {
if (databaseProvider == null) { if (databaseProvider == null) {
databaseProvider = new StandaloneDatabaseProvider(context); databaseProvider = new StandaloneDatabaseProvider(context);
@ -178,6 +183,7 @@ public final class DemoUtil {
return downloadDirectory; return downloadDirectory;
} }
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static CacheDataSource.Factory buildReadOnlyCacheDataSource( private static CacheDataSource.Factory buildReadOnlyCacheDataSource(
DataSource.Factory upstreamFactory, Cache cache) { DataSource.Factory upstreamFactory, Cache cache) {
return new CacheDataSource.Factory() return new CacheDataSource.Factory()

View File

@ -15,8 +15,7 @@
*/ */
package androidx.media3.demo.main; package androidx.media3.demo.main;
import static androidx.media3.common.util.Assertions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
@ -24,16 +23,18 @@ import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackGroupArray; import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
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.HttpDataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.drm.DrmSession; import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionEventListener;
@ -46,13 +47,14 @@ import androidx.media3.exoplayer.offline.DownloadIndex;
import androidx.media3.exoplayer.offline.DownloadManager; import androidx.media3.exoplayer.offline.DownloadManager;
import androidx.media3.exoplayer.offline.DownloadRequest; import androidx.media3.exoplayer.offline.DownloadRequest;
import androidx.media3.exoplayer.offline.DownloadService; import androidx.media3.exoplayer.offline.DownloadService;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.source.TrackGroupArray;
import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
/** Tracks media that has been downloaded. */ /** Tracks media that has been downloaded. */
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
public class DownloadTracker { public class DownloadTracker {
/** Listens for changes in the tracked downloads. */ /** Listens for changes in the tracked downloads. */
@ -65,31 +67,26 @@ public class DownloadTracker {
private static final String TAG = "DownloadTracker"; private static final String TAG = "DownloadTracker";
private final Context context; private final Context context;
private final HttpDataSource.Factory httpDataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final CopyOnWriteArraySet<Listener> listeners; private final CopyOnWriteArraySet<Listener> listeners;
private final HashMap<Uri, Download> downloads; private final HashMap<Uri, Download> downloads;
private final DownloadIndex downloadIndex; private final DownloadIndex downloadIndex;
private final DefaultTrackSelector.Parameters trackSelectorParameters;
@Nullable private StartDownloadDialogHelper startDownloadDialogHelper; @Nullable private StartDownloadDialogHelper startDownloadDialogHelper;
public DownloadTracker( public DownloadTracker(
Context context, Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) {
HttpDataSource.Factory httpDataSourceFactory,
DownloadManager downloadManager) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
this.httpDataSourceFactory = httpDataSourceFactory; this.dataSourceFactory = dataSourceFactory;
listeners = new CopyOnWriteArraySet<>(); listeners = new CopyOnWriteArraySet<>();
downloads = new HashMap<>(); downloads = new HashMap<>();
downloadIndex = downloadManager.getDownloadIndex(); downloadIndex = downloadManager.getDownloadIndex();
trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context);
downloadManager.addListener(new DownloadManagerListener()); downloadManager.addListener(new DownloadManagerListener());
loadDownloads(); loadDownloads();
} }
public void addListener(Listener listener) { public void addListener(Listener listener) {
checkNotNull(listener); listeners.add(checkNotNull(listener));
listeners.add(listener);
} }
public void removeListener(Listener listener) { public void removeListener(Listener listener) {
@ -120,8 +117,7 @@ public class DownloadTracker {
startDownloadDialogHelper = startDownloadDialogHelper =
new StartDownloadDialogHelper( new StartDownloadDialogHelper(
fragmentManager, fragmentManager,
DownloadHelper.forMediaItem( DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, dataSourceFactory),
context, mediaItem, renderersFactory, httpDataSourceFactory),
mediaItem); mediaItem);
} }
} }
@ -159,7 +155,7 @@ public class DownloadTracker {
private final class StartDownloadDialogHelper private final class StartDownloadDialogHelper
implements DownloadHelper.Callback, implements DownloadHelper.Callback,
DialogInterface.OnClickListener, TrackSelectionDialog.TrackSelectionListener,
DialogInterface.OnDismissListener { DialogInterface.OnDismissListener {
private final FragmentManager fragmentManager; private final FragmentManager fragmentManager;
@ -167,7 +163,6 @@ public class DownloadTracker {
private final MediaItem mediaItem; private final MediaItem mediaItem;
private TrackSelectionDialog trackSelectionDialog; private TrackSelectionDialog trackSelectionDialog;
private MappedTrackInfo mappedTrackInfo;
private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask; private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask;
@Nullable private byte[] keySetId; @Nullable private byte[] keySetId;
@ -220,7 +215,7 @@ public class DownloadTracker {
new WidevineOfflineLicenseFetchTask( new WidevineOfflineLicenseFetchTask(
format, format,
mediaItem.localConfiguration.drmConfiguration, mediaItem.localConfiguration.drmConfiguration,
httpDataSourceFactory, dataSourceFactory,
/* dialogHelper= */ this, /* dialogHelper= */ this,
helper); helper);
widevineOfflineLicenseFetchTask.execute(); widevineOfflineLicenseFetchTask.execute();
@ -237,21 +232,13 @@ public class DownloadTracker {
Log.e(TAG, logMessage, e); Log.e(TAG, logMessage, e);
} }
// DialogInterface.OnClickListener implementation. // TrackSelectionListener implementation.
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onTracksSelected(TrackSelectionParameters trackSelectionParameters) {
for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) {
downloadHelper.clearTrackSelections(periodIndex); downloadHelper.clearTrackSelections(periodIndex);
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { downloadHelper.addTrackSelection(periodIndex, trackSelectionParameters);
if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) {
downloadHelper.addTrackSelectionForSingleRenderer(
periodIndex,
/* rendererIndex= */ i,
trackSelectorParameters,
trackSelectionDialog.getOverrides(/* rendererIndex= */ i));
}
}
} }
DownloadRequest downloadRequest = buildDownloadRequest(); DownloadRequest downloadRequest = buildDownloadRequest();
if (downloadRequest.streamKeys.isEmpty()) { if (downloadRequest.streamKeys.isEmpty()) {
@ -316,21 +303,21 @@ public class DownloadTracker {
return; return;
} }
mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); Tracks tracks = downloadHelper.getTracks(/* periodIndex= */ 0);
if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { if (!TrackSelectionDialog.willHaveContent(tracks)) {
Log.d(TAG, "No dialog content. Downloading entire stream."); Log.d(TAG, "No dialog content. Downloading entire stream.");
startDownload(); startDownload();
downloadHelper.release(); downloadHelper.release();
return; return;
} }
trackSelectionDialog = trackSelectionDialog =
TrackSelectionDialog.createForMappedTrackInfoAndParameters( TrackSelectionDialog.createForTracksAndParameters(
/* titleId= */ R.string.exo_download_description, /* titleId= */ R.string.exo_download_description,
mappedTrackInfo, tracks,
trackSelectorParameters, DownloadHelper.getDefaultTrackSelectorParameters(context),
/* allowAdaptiveSelections= */ false, /* allowAdaptiveSelections= */ false,
/* allowMultipleOverrides= */ true, /* allowMultipleOverrides= */ true,
/* onClickListener= */ this, /* onTracksSelectedListener= */ this,
/* onDismissListener= */ this); /* onDismissListener= */ this);
trackSelectionDialog.show(fragmentManager, /* tag= */ null); trackSelectionDialog.show(fragmentManager, /* tag= */ null);
} }
@ -371,7 +358,7 @@ public class DownloadTracker {
private final Format format; private final Format format;
private final MediaItem.DrmConfiguration drmConfiguration; private final MediaItem.DrmConfiguration drmConfiguration;
private final HttpDataSource.Factory httpDataSourceFactory; private final DataSource.Factory dataSourceFactory;
private final StartDownloadDialogHelper dialogHelper; private final StartDownloadDialogHelper dialogHelper;
private final DownloadHelper downloadHelper; private final DownloadHelper downloadHelper;
@ -381,12 +368,12 @@ public class DownloadTracker {
public WidevineOfflineLicenseFetchTask( public WidevineOfflineLicenseFetchTask(
Format format, Format format,
MediaItem.DrmConfiguration drmConfiguration, MediaItem.DrmConfiguration drmConfiguration,
HttpDataSource.Factory httpDataSourceFactory, DataSource.Factory dataSourceFactory,
StartDownloadDialogHelper dialogHelper, StartDownloadDialogHelper dialogHelper,
DownloadHelper downloadHelper) { DownloadHelper downloadHelper) {
this.format = format; this.format = format;
this.drmConfiguration = drmConfiguration; this.drmConfiguration = drmConfiguration;
this.httpDataSourceFactory = httpDataSourceFactory; this.dataSourceFactory = dataSourceFactory;
this.dialogHelper = dialogHelper; this.dialogHelper = dialogHelper;
this.downloadHelper = downloadHelper; this.downloadHelper = downloadHelper;
} }
@ -397,7 +384,7 @@ public class DownloadTracker {
OfflineLicenseHelper.newWidevineInstance( OfflineLicenseHelper.newWidevineInstance(
drmConfiguration.licenseUri.toString(), drmConfiguration.licenseUri.toString(),
drmConfiguration.forceDefaultLicenseUri, drmConfiguration.forceDefaultLicenseUri,
httpDataSourceFactory, dataSourceFactory,
drmConfiguration.licenseRequestHeaders, drmConfiguration.licenseRequestHeaders,
new DrmSessionEventListener.EventDispatcher()); new DrmSessionEventListener.EventDispatcher());
try { try {
@ -415,7 +402,7 @@ public class DownloadTracker {
if (drmSessionException != null) { if (drmSessionException != null) {
dialogHelper.onOfflineLicenseFetchedError(drmSessionException); dialogHelper.onOfflineLicenseFetchedError(drmSessionException);
} else { } else {
dialogHelper.onOfflineLicenseFetched(downloadHelper, checkStateNotNull(keySetId)); dialogHelper.onOfflineLicenseFetched(downloadHelper, checkNotNull(keySetId));
} }
} }
} }

View File

@ -15,8 +15,9 @@
*/ */
package androidx.media3.demo.main; package androidx.media3.demo.main;
import static androidx.media3.common.util.Assertions.checkNotNull; import static com.google.common.base.Preconditions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState; import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
@ -26,7 +27,6 @@ import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaItem.ClippingConfiguration; import androidx.media3.common.MediaItem.ClippingConfiguration;
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.util.Assertions;
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 java.util.ArrayList; import java.util.ArrayList;
@ -86,7 +86,7 @@ public class IntentUtil {
/** Populates the intent with the given list of {@link MediaItem media items}. */ /** Populates the intent with the given list of {@link MediaItem media items}. */
public static void addToIntent(List<MediaItem> mediaItems, Intent intent) { public static void addToIntent(List<MediaItem> mediaItems, Intent intent) {
Assertions.checkArgument(!mediaItems.isEmpty()); checkArgument(!mediaItems.isEmpty());
if (mediaItems.size() == 1) { if (mediaItems.size() == 1) {
MediaItem mediaItem = mediaItems.get(0); MediaItem mediaItem = mediaItems.get(0);
MediaItem.LocalConfiguration localConfiguration = checkNotNull(mediaItem.localConfiguration); MediaItem.LocalConfiguration localConfiguration = checkNotNull(mediaItem.localConfiguration);
@ -177,7 +177,7 @@ public class IntentUtil {
headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]);
} }
} }
@Nullable UUID drmUuid = Util.getDrmUuid(Util.castNonNull(drmSchemeExtra)); @Nullable UUID drmUuid = Util.getDrmUuid(drmSchemeExtra);
if (drmUuid != null) { if (drmUuid != null) {
builder.setDrmConfiguration( builder.setDrmConfiguration(
new MediaItem.DrmConfiguration.Builder(drmUuid) new MediaItem.DrmConfiguration.Builder(drmUuid)
@ -188,7 +188,7 @@ public class IntentUtil {
intent.getBooleanExtra( intent.getBooleanExtra(
DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false)) DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false))
.setLicenseRequestHeaders(headers) .setLicenseRequestHeaders(headers)
.forceSessionsForAudioAndVideoTracks( .setForceSessionsForAudioAndVideoTracks(
intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false)) intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false))
.build()); .build());
} }
@ -241,7 +241,7 @@ public class IntentUtil {
drmConfiguration.forcedSessionTrackTypes; drmConfiguration.forcedSessionTrackTypes;
if (!forcedDrmSessionTrackTypes.isEmpty()) { if (!forcedDrmSessionTrackTypes.isEmpty()) {
// Only video and audio together are supported. // Only video and audio together are supported.
Assertions.checkState( checkState(
forcedDrmSessionTrackTypes.size() == 2 forcedDrmSessionTrackTypes.size() == 2
&& forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_VIDEO) && forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_VIDEO)
&& forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_AUDIO)); && forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_AUDIO));

View File

@ -17,6 +17,7 @@ package androidx.media3.demo.main;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Pair; import android.util.Pair;
import android.view.KeyEvent; import android.view.KeyEvent;
@ -27,6 +28,7 @@ import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.AudioAttributes; import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -34,11 +36,14 @@ import androidx.media3.common.ErrorMessageProvider;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackException;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.TracksInfo; import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManagerProvider;
import androidx.media3.exoplayer.drm.FrameworkMediaDrm; import androidx.media3.exoplayer.drm.FrameworkMediaDrm;
import androidx.media3.exoplayer.ima.ImaAdsLoader; import androidx.media3.exoplayer.ima.ImaAdsLoader;
import androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource; import androidx.media3.exoplayer.ima.ImaServerSideAdInsertionMediaSource;
@ -48,10 +53,8 @@ import androidx.media3.exoplayer.offline.DownloadRequest;
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory; import androidx.media3.exoplayer.source.DefaultMediaSourceFactory;
import androidx.media3.exoplayer.source.MediaSource; import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.source.ads.AdsLoader; import androidx.media3.exoplayer.source.ads.AdsLoader;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
import androidx.media3.exoplayer.util.DebugTextViewHelper; import androidx.media3.exoplayer.util.DebugTextViewHelper;
import androidx.media3.exoplayer.util.EventLogger; import androidx.media3.exoplayer.util.EventLogger;
import androidx.media3.ui.PlayerControlView;
import androidx.media3.ui.PlayerView; import androidx.media3.ui.PlayerView;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@ -60,7 +63,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** An activity that plays media using {@link ExoPlayer}. */ /** An activity that plays media using {@link ExoPlayer}. */
public class PlayerActivity extends AppCompatActivity public class PlayerActivity extends AppCompatActivity
implements OnClickListener, PlayerControlView.VisibilityListener { implements OnClickListener, PlayerView.ControllerVisibilityListener {
// Saved instance state keys. // Saved instance state keys.
@ -79,10 +82,9 @@ public class PlayerActivity extends AppCompatActivity
private Button selectTracksButton; private Button selectTracksButton;
private DataSource.Factory dataSourceFactory; private DataSource.Factory dataSourceFactory;
private List<MediaItem> mediaItems; private List<MediaItem> mediaItems;
private DefaultTrackSelector trackSelector; private TrackSelectionParameters trackSelectionParameters;
private DefaultTrackSelector.Parameters trackSelectionParameters;
private DebugTextViewHelper debugViewHelper; private DebugTextViewHelper debugViewHelper;
private TracksInfo lastSeenTracksInfo; private Tracks lastSeenTracks;
private boolean startAutoPlay; private boolean startAutoPlay;
private int startItemIndex; private int startItemIndex;
private long startPosition; private long startPosition;
@ -90,7 +92,12 @@ public class PlayerActivity extends AppCompatActivity
// For ad playback only. // For ad playback only.
@Nullable private AdsLoader clientSideAdsLoader; @Nullable private AdsLoader clientSideAdsLoader;
// TODO: Annotate this and serverSideAdsLoaderState below with @OptIn when it can be applied to
// fields (needs http://r.android.com/2004032 to be released into a version of
// androidx.annotation:annotation-experimental).
@Nullable private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader; @Nullable private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader;
private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State
serverSideAdsLoaderState; serverSideAdsLoaderState;
@ -113,22 +120,15 @@ public class PlayerActivity extends AppCompatActivity
playerView.requestFocus(); playerView.requestFocus();
if (savedInstanceState != null) { if (savedInstanceState != null) {
// Restore as DefaultTrackSelector.Parameters in case ExoPlayer specific parameters were set.
trackSelectionParameters = trackSelectionParameters =
DefaultTrackSelector.Parameters.CREATOR.fromBundle( TrackSelectionParameters.fromBundle(
savedInstanceState.getBundle(KEY_TRACK_SELECTION_PARAMETERS)); savedInstanceState.getBundle(KEY_TRACK_SELECTION_PARAMETERS));
startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY); startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY);
startItemIndex = savedInstanceState.getInt(KEY_ITEM_INDEX); startItemIndex = savedInstanceState.getInt(KEY_ITEM_INDEX);
startPosition = savedInstanceState.getLong(KEY_POSITION); startPosition = savedInstanceState.getLong(KEY_POSITION);
Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE); restoreServerSideAdsLoaderState(savedInstanceState);
if (adsLoaderStateBundle != null) {
serverSideAdsLoaderState =
ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle(
adsLoaderStateBundle);
}
} else { } else {
trackSelectionParameters = trackSelectionParameters = new TrackSelectionParameters.Builder(/* context= */ this).build();
new DefaultTrackSelector.ParametersBuilder(/* context= */ this).build();
clearStartPosition(); clearStartPosition();
} }
} }
@ -145,7 +145,7 @@ public class PlayerActivity extends AppCompatActivity
@Override @Override
public void onStart() { public void onStart() {
super.onStart(); super.onStart();
if (Util.SDK_INT > 23) { if (Build.VERSION.SDK_INT > 23) {
initializePlayer(); initializePlayer();
if (playerView != null) { if (playerView != null) {
playerView.onResume(); playerView.onResume();
@ -156,7 +156,7 @@ public class PlayerActivity extends AppCompatActivity
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
if (Util.SDK_INT <= 23 || player == null) { if (Build.VERSION.SDK_INT <= 23 || player == null) {
initializePlayer(); initializePlayer();
if (playerView != null) { if (playerView != null) {
playerView.onResume(); playerView.onResume();
@ -167,7 +167,7 @@ public class PlayerActivity extends AppCompatActivity
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (Util.SDK_INT <= 23) { if (Build.VERSION.SDK_INT <= 23) {
if (playerView != null) { if (playerView != null) {
playerView.onPause(); playerView.onPause();
} }
@ -178,7 +178,7 @@ public class PlayerActivity extends AppCompatActivity
@Override @Override
public void onStop() { public void onStop() {
super.onStop(); super.onStop();
if (Util.SDK_INT > 23) { if (Build.VERSION.SDK_INT > 23) {
if (playerView != null) { if (playerView != null) {
playerView.onPause(); playerView.onPause();
} }
@ -218,9 +218,7 @@ public class PlayerActivity extends AppCompatActivity
outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay); outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay);
outState.putInt(KEY_ITEM_INDEX, startItemIndex); outState.putInt(KEY_ITEM_INDEX, startItemIndex);
outState.putLong(KEY_POSITION, startPosition); outState.putLong(KEY_POSITION, startPosition);
if (serverSideAdsLoaderState != null) { saveServerSideAdsLoaderState(outState);
outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle());
}
} }
// Activity input // Activity input
@ -237,20 +235,20 @@ public class PlayerActivity extends AppCompatActivity
public void onClick(View view) { public void onClick(View view) {
if (view == selectTracksButton if (view == selectTracksButton
&& !isShowingTrackSelectionDialog && !isShowingTrackSelectionDialog
&& TrackSelectionDialog.willHaveContent(trackSelector)) { && TrackSelectionDialog.willHaveContent(player)) {
isShowingTrackSelectionDialog = true; isShowingTrackSelectionDialog = true;
TrackSelectionDialog trackSelectionDialog = TrackSelectionDialog trackSelectionDialog =
TrackSelectionDialog.createForTrackSelector( TrackSelectionDialog.createForPlayer(
trackSelector, player,
/* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false); /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false);
trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null); trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null);
} }
} }
// PlayerControlView.VisibilityListener implementation // PlayerView.ControllerVisibilityListener implementation
@Override @Override
public void onVisibilityChange(int visibility) { public void onVisibilityChanged(int visibility) {
debugRootView.setVisibility(visibility); debugRootView.setVisibility(visibility);
} }
@ -260,7 +258,9 @@ public class PlayerActivity extends AppCompatActivity
setContentView(R.layout.player_activity); setContentView(R.layout.player_activity);
} }
/** @return Whether initialization was successful. */ /**
* @return Whether initialization was successful.
*/
protected boolean initializePlayer() { protected boolean initializePlayer() {
if (player == null) { if (player == null) {
Intent intent = getIntent(); Intent intent = getIntent();
@ -270,26 +270,20 @@ public class PlayerActivity extends AppCompatActivity
return false; return false;
} }
boolean preferExtensionDecoders = lastSeenTracks = Tracks.EMPTY;
intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false); ExoPlayer.Builder playerBuilder =
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
trackSelector = new DefaultTrackSelector(/* context= */ this);
lastSeenTracksInfo = TracksInfo.EMPTY;
player =
new ExoPlayer.Builder(/* context= */ this) new ExoPlayer.Builder(/* context= */ this)
.setRenderersFactory(renderersFactory) .setMediaSourceFactory(createMediaSourceFactory());
.setMediaSourceFactory(createMediaSourceFactory()) setRenderersFactory(
.setTrackSelector(trackSelector) playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false));
.build(); player = playerBuilder.build();
player.setTrackSelectionParameters(trackSelectionParameters); player.setTrackSelectionParameters(trackSelectionParameters);
player.addListener(new PlayerEventListener()); player.addListener(new PlayerEventListener());
player.addAnalyticsListener(new EventLogger(trackSelector)); player.addAnalyticsListener(new EventLogger());
player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true); player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true);
player.setPlayWhenReady(startAutoPlay); player.setPlayWhenReady(startAutoPlay);
playerView.setPlayer(player); playerView.setPlayer(player);
serverSideAdsLoader.setPlayer(player); configurePlayerWithServerSideAdsLoader();
debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper = new DebugTextViewHelper(player, debugTextView);
debugViewHelper.start(); debugViewHelper.start();
} }
@ -303,7 +297,12 @@ public class PlayerActivity extends AppCompatActivity
return true; return true;
} }
@OptIn(markerClass = UnstableApi.class) // SSAI configuration
private MediaSource.Factory createMediaSourceFactory() { private MediaSource.Factory createMediaSourceFactory() {
DefaultDrmSessionManagerProvider drmSessionManagerProvider =
new DefaultDrmSessionManagerProvider();
drmSessionManagerProvider.setDrmHttpDataSourceFactory(
DemoUtil.getHttpDataSourceFactory(/* context= */ this));
ImaServerSideAdInsertionMediaSource.AdsLoader.Builder serverSideAdLoaderBuilder = ImaServerSideAdInsertionMediaSource.AdsLoader.Builder serverSideAdLoaderBuilder =
new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(/* context= */ this, playerView); new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(/* context= */ this, playerView);
if (serverSideAdsLoaderState != null) { if (serverSideAdsLoaderState != null) {
@ -312,13 +311,30 @@ public class PlayerActivity extends AppCompatActivity
serverSideAdsLoader = serverSideAdLoaderBuilder.build(); serverSideAdsLoader = serverSideAdLoaderBuilder.build();
ImaServerSideAdInsertionMediaSource.Factory imaServerSideAdInsertionMediaSourceFactory = ImaServerSideAdInsertionMediaSource.Factory imaServerSideAdInsertionMediaSourceFactory =
new ImaServerSideAdInsertionMediaSource.Factory( new ImaServerSideAdInsertionMediaSource.Factory(
serverSideAdsLoader, new DefaultMediaSourceFactory(dataSourceFactory)); serverSideAdsLoader,
return new DefaultMediaSourceFactory(dataSourceFactory) new DefaultMediaSourceFactory(/* context= */ this)
.setAdsLoaderProvider(this::getClientSideAdsLoader) .setDataSourceFactory(dataSourceFactory));
.setAdViewProvider(playerView) return new DefaultMediaSourceFactory(/* context= */ this)
.setDataSourceFactory(dataSourceFactory)
.setDrmSessionManagerProvider(drmSessionManagerProvider)
.setLocalAdInsertionComponents(
this::getClientSideAdsLoader, /* adViewProvider= */ playerView)
.setServerSideAdInsertionMediaSourceFactory(imaServerSideAdInsertionMediaSourceFactory); .setServerSideAdInsertionMediaSourceFactory(imaServerSideAdInsertionMediaSourceFactory);
} }
@OptIn(markerClass = UnstableApi.class)
private void setRenderersFactory(
ExoPlayer.Builder playerBuilder, boolean preferExtensionDecoders) {
RenderersFactory renderersFactory =
DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders);
playerBuilder.setRenderersFactory(renderersFactory);
}
@OptIn(markerClass = UnstableApi.class)
private void configurePlayerWithServerSideAdsLoader() {
serverSideAdsLoader.setPlayer(player);
}
private List<MediaItem> createMediaItems(Intent intent) { private List<MediaItem> createMediaItems(Intent intent) {
String action = intent.getAction(); String action = intent.getAction();
boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action); boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action);
@ -345,7 +361,7 @@ public class PlayerActivity extends AppCompatActivity
MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration; MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration;
if (drmConfiguration != null) { if (drmConfiguration != null) {
if (Util.SDK_INT < 18) { if (Build.VERSION.SDK_INT < 18) {
showToast(R.string.error_drm_unsupported_before_api_18); showToast(R.string.error_drm_unsupported_before_api_18);
finish(); finish();
return Collections.emptyList(); return Collections.emptyList();
@ -372,8 +388,7 @@ public class PlayerActivity extends AppCompatActivity
if (player != null) { if (player != null) {
updateTrackSelectorParameters(); updateTrackSelectorParameters();
updateStartPosition(); updateStartPosition();
serverSideAdsLoaderState = serverSideAdsLoader.release(); releaseServerSideAdsLoader();
serverSideAdsLoader = null;
debugViewHelper.stop(); debugViewHelper.stop();
debugViewHelper = null; debugViewHelper = null;
player.release(); player.release();
@ -388,6 +403,12 @@ public class PlayerActivity extends AppCompatActivity
} }
} }
@OptIn(markerClass = UnstableApi.class)
private void releaseServerSideAdsLoader() {
serverSideAdsLoaderState = serverSideAdsLoader.release();
serverSideAdsLoader = null;
}
private void releaseClientSideAdsLoader() { private void releaseClientSideAdsLoader() {
if (clientSideAdsLoader != null) { if (clientSideAdsLoader != null) {
clientSideAdsLoader.release(); clientSideAdsLoader.release();
@ -396,12 +417,26 @@ public class PlayerActivity extends AppCompatActivity
} }
} }
@OptIn(markerClass = UnstableApi.class)
private void saveServerSideAdsLoaderState(Bundle outState) {
if (serverSideAdsLoaderState != null) {
outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle());
}
}
@OptIn(markerClass = UnstableApi.class)
private void restoreServerSideAdsLoaderState(Bundle savedInstanceState) {
Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE);
if (adsLoaderStateBundle != null) {
serverSideAdsLoaderState =
ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle(
adsLoaderStateBundle);
}
}
private void updateTrackSelectorParameters() { private void updateTrackSelectorParameters() {
if (player != null) { if (player != null) {
// Until the demo app is fully migrated to TrackSelectionParameters, rely on ExoPlayer to use trackSelectionParameters = player.getTrackSelectionParameters();
// DefaultTrackSelector by default.
trackSelectionParameters =
(DefaultTrackSelector.Parameters) player.getTrackSelectionParameters();
} }
} }
@ -422,8 +457,7 @@ public class PlayerActivity extends AppCompatActivity
// User controls // User controls
private void updateButtonVisibility() { private void updateButtonVisibility() {
selectTracksButton.setEnabled( selectTracksButton.setEnabled(player != null && TrackSelectionDialog.willHaveContent(player));
player != null && TrackSelectionDialog.willHaveContent(trackSelector));
} }
private void showControls() { private void showControls() {
@ -461,20 +495,20 @@ public class PlayerActivity extends AppCompatActivity
@Override @Override
@SuppressWarnings("ReferenceEquality") @SuppressWarnings("ReferenceEquality")
public void onTracksInfoChanged(TracksInfo tracksInfo) { public void onTracksChanged(Tracks tracks) {
updateButtonVisibility(); updateButtonVisibility();
if (tracksInfo == lastSeenTracksInfo) { if (tracks == lastSeenTracks) {
return; return;
} }
if (!tracksInfo.isTypeSupportedOrEmpty( if (tracks.containsType(C.TRACK_TYPE_VIDEO)
C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) { && !tracks.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) {
showToast(R.string.error_unsupported_video); showToast(R.string.error_unsupported_video);
} }
if (!tracksInfo.isTypeSupportedOrEmpty( if (tracks.containsType(C.TRACK_TYPE_AUDIO)
C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) { && !tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) {
showToast(R.string.error_unsupported_audio); showToast(R.string.error_unsupported_audio);
} }
lastSeenTracksInfo = tracksInfo; lastSeenTracks = tracks;
} }
} }
@ -513,10 +547,19 @@ public class PlayerActivity extends AppCompatActivity
private static List<MediaItem> createMediaItems(Intent intent, DownloadTracker downloadTracker) { private static List<MediaItem> createMediaItems(Intent intent, DownloadTracker downloadTracker) {
List<MediaItem> mediaItems = new ArrayList<>(); List<MediaItem> mediaItems = new ArrayList<>();
for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) { for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) {
@Nullable mediaItems.add(
DownloadRequest downloadRequest = maybeSetDownloadProperties(
downloadTracker.getDownloadRequest(item.localConfiguration.uri); item, downloadTracker.getDownloadRequest(item.localConfiguration.uri)));
if (downloadRequest != null) { }
return mediaItems;
}
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private static MediaItem maybeSetDownloadProperties(
MediaItem item, @Nullable DownloadRequest downloadRequest) {
if (downloadRequest == null) {
return item;
}
MediaItem.Builder builder = item.buildUpon(); MediaItem.Builder builder = item.buildUpon();
builder builder
.setMediaId(downloadRequest.id) .setMediaId(downloadRequest.id)
@ -530,12 +573,6 @@ public class PlayerActivity extends AppCompatActivity
builder.setDrmConfiguration( builder.setDrmConfiguration(
drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build()); drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build());
} }
return builder.build();
mediaItems.add(builder.build());
} else {
mediaItems.add(item);
}
}
return mediaItems;
} }
} }

View File

@ -15,9 +15,9 @@
*/ */
package androidx.media3.demo.main; package androidx.media3.demo.main;
import static androidx.media3.common.util.Assertions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static com.google.common.base.Preconditions.checkState;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@ -27,6 +27,7 @@ import android.content.res.AssetManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.util.JsonReader; import android.util.JsonReader;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -41,6 +42,7 @@ import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
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;
@ -53,6 +55,7 @@ import androidx.media3.datasource.DataSourceUtil;
import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.DataSpec;
import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.offline.DownloadService; import androidx.media3.exoplayer.offline.DownloadService;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import java.io.IOException; import java.io.IOException;
@ -116,8 +119,12 @@ public class SampleChooserActivity extends AppCompatActivity
useExtensionRenderers = DemoUtil.useExtensionRenderers(); useExtensionRenderers = DemoUtil.useExtensionRenderers();
downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this); downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this);
loadSample(); loadSample();
startDownloadService();
}
// Start the download service if it should be running but it's not currently. /** Start the download service if it should be running but it's not currently. */
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
private void startDownloadService() {
// Starting the service in the foreground causes notification flicker if there is no scheduled // Starting the service in the foreground causes notification flicker if there is no scheduled
// action. Starting it in the background throws an exception if the app is in the background too // action. Starting it in the background throws an exception if the app is in the background too
// (e.g. if device screen is locked). // (e.g. if device screen is locked).
@ -271,6 +278,7 @@ public class SampleChooserActivity extends AppCompatActivity
private boolean sawError; private boolean sawError;
@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class)
@Override @Override
protected List<PlaylistGroup> doInBackground(String... uris) { protected List<PlaylistGroup> doInBackground(String... uris) {
List<PlaylistGroup> result = new ArrayList<>(); List<PlaylistGroup> result = new ArrayList<>();
@ -436,7 +444,10 @@ public class SampleChooserActivity extends AppCompatActivity
} else { } else {
@Nullable @Nullable
String adaptiveMimeType = String adaptiveMimeType =
Util.getAdaptiveMimeTypeForContentType(Util.inferContentType(uri, extension)); Util.getAdaptiveMimeTypeForContentType(
TextUtils.isEmpty(extension)
? Util.inferContentType(uri)
: Util.inferContentTypeForExtension(extension));
mediaItem mediaItem
.setUri(uri) .setUri(uri)
.setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build())
@ -447,7 +458,7 @@ public class SampleChooserActivity extends AppCompatActivity
new MediaItem.DrmConfiguration.Builder(drmUuid) new MediaItem.DrmConfiguration.Builder(drmUuid)
.setLicenseUri(drmLicenseUri) .setLicenseUri(drmLicenseUri)
.setLicenseRequestHeaders(drmLicenseRequestHeaders) .setLicenseRequestHeaders(drmLicenseRequestHeaders)
.forceSessionsForAudioAndVideoTracks(drmSessionForClearContent) .setForceSessionsForAudioAndVideoTracks(drmSessionForClearContent)
.setMultiSession(drmMultiSession) .setMultiSession(drmMultiSession)
.setForceDefaultLicenseUri(drmForceDefaultLicenseUri) .setForceDefaultLicenseUri(drmForceDefaultLicenseUri)
.build()); .build());
@ -481,7 +492,7 @@ public class SampleChooserActivity extends AppCompatActivity
private PlaylistGroup getGroup(String groupName, List<PlaylistGroup> groups) { private PlaylistGroup getGroup(String groupName, List<PlaylistGroup> groups) {
for (int i = 0; i < groups.size(); i++) { for (int i = 0; i < groups.size(); i++) {
if (Util.areEqual(groupName, groups.get(i).title)) { if (Objects.equal(groupName, groups.get(i).title)) {
return groups.get(i); return groups.get(i);
} }
} }

View File

@ -25,27 +25,50 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.appcompat.app.AppCompatDialog; import androidx.appcompat.app.AppCompatDialog;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentPagerAdapter;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.TrackGroupArray; import androidx.media3.common.Player;
import androidx.media3.common.util.Assertions; import androidx.media3.common.TrackGroup;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.common.TrackSelectionOverride;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.SelectionOverride; import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; import androidx.media3.common.Tracks;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.ui.TrackSelectionView; import androidx.media3.ui.TrackSelectionView;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayout;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
/** Dialog to select tracks. */ /** Dialog to select tracks. */
@OptIn(markerClass = UnstableApi.class)
public final class TrackSelectionDialog extends DialogFragment { public final class TrackSelectionDialog extends DialogFragment {
/** Called when tracks are selected. */
public interface TrackSelectionListener {
/**
* Called when tracks are selected.
*
* @param trackSelectionParameters A {@link TrackSelectionParameters} representing the selected
* tracks. Any manual selections are defined by {@link
* TrackSelectionParameters#disabledTrackTypes} and {@link
* TrackSelectionParameters#overrides}.
*/
void onTracksSelected(TrackSelectionParameters trackSelectionParameters);
}
public static final ImmutableList<Integer> SUPPORTED_TRACK_TYPES =
ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT);
private final SparseArray<TrackSelectionViewFragment> tabFragments; private final SparseArray<TrackSelectionViewFragment> tabFragments;
private final ArrayList<Integer> tabTrackTypes; private final ArrayList<Integer> tabTrackTypes;
@ -55,20 +78,19 @@ public final class TrackSelectionDialog extends DialogFragment {
/** /**
* Returns whether a track selection dialog will have content to display if initialized with the * Returns whether a track selection dialog will have content to display if initialized with the
* specified {@link DefaultTrackSelector} in its current state. * specified {@link Player}.
*/ */
public static boolean willHaveContent(DefaultTrackSelector trackSelector) { public static boolean willHaveContent(Player player) {
MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); return willHaveContent(player.getCurrentTracks());
return mappedTrackInfo != null && willHaveContent(mappedTrackInfo);
} }
/** /**
* Returns whether a track selection dialog will have content to display if initialized with the * Returns whether a track selection dialog will have content to display if initialized with the
* specified {@link MappedTrackInfo}. * specified {@link Tracks}.
*/ */
public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) { public static boolean willHaveContent(Tracks tracks) {
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { for (Tracks.Group trackGroup : tracks.getGroups()) {
if (showTabForRenderer(mappedTrackInfo, i)) { if (SUPPORTED_TRACK_TYPES.contains(trackGroup.getType())) {
return true; return true;
} }
} }
@ -76,78 +98,67 @@ public final class TrackSelectionDialog extends DialogFragment {
} }
/** /**
* Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be * Creates a dialog for a given {@link Player}, whose parameters will be automatically updated
* automatically updated when tracks are selected. * when tracks are selected.
* *
* @param trackSelector The {@link DefaultTrackSelector}. * @param player The {@link Player}.
* @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is
* dismissed. * dismissed.
*/ */
public static TrackSelectionDialog createForTrackSelector( public static TrackSelectionDialog createForPlayer(
DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) { Player player, DialogInterface.OnDismissListener onDismissListener) {
MappedTrackInfo mappedTrackInfo = return createForTracksAndParameters(
Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); R.string.track_selection_title,
TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); player.getCurrentTracks(),
DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); player.getTrackSelectionParameters(),
trackSelectionDialog.init(
/* titleId= */ R.string.track_selection_title,
mappedTrackInfo,
/* initialParameters = */ parameters,
/* allowAdaptiveSelections= */ true, /* allowAdaptiveSelections= */ true,
/* allowMultipleOverrides= */ false, /* allowMultipleOverrides= */ false,
/* onClickListener= */ (dialog, which) -> { player::setTrackSelectionParameters,
DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon();
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
builder
.clearSelectionOverrides(/* rendererIndex= */ i)
.setRendererDisabled(
/* rendererIndex= */ i,
trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i));
List<SelectionOverride> overrides =
trackSelectionDialog.getOverrides(/* rendererIndex= */ i);
if (!overrides.isEmpty()) {
builder.setSelectionOverride(
/* rendererIndex= */ i,
mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i),
overrides.get(0));
}
}
trackSelector.setParameters(builder);
},
onDismissListener); onDismissListener);
return trackSelectionDialog;
} }
/** /**
* Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}. * Creates a dialog for given {@link Tracks} and {@link TrackSelectionParameters}.
* *
* @param titleId The resource id of the dialog title. * @param titleId The resource id of the dialog title.
* @param mappedTrackInfo The {@link MappedTrackInfo} to display. * @param tracks The {@link Tracks} describing the tracks to display.
* @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial * @param trackSelectionParameters The initial {@link TrackSelectionParameters}.
* track selection.
* @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track)
* can be made. * can be made.
* @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected.
* @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected. * @param trackSelectionListener Called when tracks are selected.
* @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is
* dismissed. * dismissed.
*/ */
public static TrackSelectionDialog createForMappedTrackInfoAndParameters( public static TrackSelectionDialog createForTracksAndParameters(
int titleId, int titleId,
MappedTrackInfo mappedTrackInfo, Tracks tracks,
DefaultTrackSelector.Parameters initialParameters, TrackSelectionParameters trackSelectionParameters,
boolean allowAdaptiveSelections, boolean allowAdaptiveSelections,
boolean allowMultipleOverrides, boolean allowMultipleOverrides,
DialogInterface.OnClickListener onClickListener, TrackSelectionListener trackSelectionListener,
DialogInterface.OnDismissListener onDismissListener) { DialogInterface.OnDismissListener onDismissListener) {
TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog();
trackSelectionDialog.init( trackSelectionDialog.init(
tracks,
trackSelectionParameters,
titleId, titleId,
mappedTrackInfo,
initialParameters,
allowAdaptiveSelections, allowAdaptiveSelections,
allowMultipleOverrides, allowMultipleOverrides,
onClickListener, /* onClickListener= */ (dialog, which) -> {
TrackSelectionParameters.Builder builder = trackSelectionParameters.buildUpon();
for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) {
int trackType = SUPPORTED_TRACK_TYPES.get(i);
builder.setTrackTypeDisabled(trackType, trackSelectionDialog.getIsDisabled(trackType));
builder.clearOverridesOfType(trackType);
Map<TrackGroup, TrackSelectionOverride> overrides =
trackSelectionDialog.getOverrides(trackType);
for (TrackSelectionOverride override : overrides.values()) {
builder.addOverride(override);
}
}
trackSelectionListener.onTracksSelected(builder.build());
},
onDismissListener); onDismissListener);
return trackSelectionDialog; return trackSelectionDialog;
} }
@ -160,9 +171,9 @@ public final class TrackSelectionDialog extends DialogFragment {
} }
private void init( private void init(
Tracks tracks,
TrackSelectionParameters trackSelectionParameters,
int titleId, int titleId,
MappedTrackInfo mappedTrackInfo,
DefaultTrackSelector.Parameters initialParameters,
boolean allowAdaptiveSelections, boolean allowAdaptiveSelections,
boolean allowMultipleOverrides, boolean allowMultipleOverrides,
DialogInterface.OnClickListener onClickListener, DialogInterface.OnClickListener onClickListener,
@ -170,45 +181,49 @@ public final class TrackSelectionDialog extends DialogFragment {
this.titleId = titleId; this.titleId = titleId;
this.onClickListener = onClickListener; this.onClickListener = onClickListener;
this.onDismissListener = onDismissListener; this.onDismissListener = onDismissListener;
for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) {
if (showTabForRenderer(mappedTrackInfo, i)) { for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) {
int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i); @C.TrackType int trackType = SUPPORTED_TRACK_TYPES.get(i);
TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); ArrayList<Tracks.Group> trackGroups = new ArrayList<>();
for (Tracks.Group trackGroup : tracks.getGroups()) {
if (trackGroup.getType() == trackType) {
trackGroups.add(trackGroup);
}
}
if (!trackGroups.isEmpty()) {
TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment();
tabFragment.init( tabFragment.init(
mappedTrackInfo, trackGroups,
/* rendererIndex= */ i, trackSelectionParameters.disabledTrackTypes.contains(trackType),
initialParameters.getRendererDisabled(/* rendererIndex= */ i), trackSelectionParameters.overrides,
initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray),
allowAdaptiveSelections, allowAdaptiveSelections,
allowMultipleOverrides); allowMultipleOverrides);
tabFragments.put(i, tabFragment); tabFragments.put(trackType, tabFragment);
tabTrackTypes.add(trackType); tabTrackTypes.add(trackType);
} }
} }
} }
/** /**
* Returns whether a renderer is disabled. * Returns whether the disabled option is selected for the specified track type.
* *
* @param rendererIndex Renderer index. * @param trackType The track type.
* @return Whether the renderer is disabled. * @return Whether the disabled option is selected for the track type.
*/ */
public boolean getIsDisabled(int rendererIndex) { public boolean getIsDisabled(int trackType) {
TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); TrackSelectionViewFragment trackView = tabFragments.get(trackType);
return rendererView != null && rendererView.isDisabled; return trackView != null && trackView.isDisabled;
} }
/** /**
* Returns the list of selected track selection overrides for the specified renderer. There will * Returns the selected track overrides for the specified track type.
* be at most one override for each track group.
* *
* @param rendererIndex Renderer index. * @param trackType The track type.
* @return The list of track selection overrides for this renderer. * @return The track overrides for the track type.
*/ */
public List<SelectionOverride> getOverrides(int rendererIndex) { public Map<TrackGroup, TrackSelectionOverride> getOverrides(int trackType) {
TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); TrackSelectionViewFragment trackView = tabFragments.get(trackType);
return rendererView == null ? Collections.emptyList() : rendererView.overrides; return trackView == null ? Collections.emptyMap() : trackView.overrides;
} }
@Override @Override
@ -248,27 +263,7 @@ public final class TrackSelectionDialog extends DialogFragment {
return dialogView; return dialogView;
} }
private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) { private static String getTrackTypeString(Resources resources, @C.TrackType int trackType) {
TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex);
if (trackGroupArray.length == 0) {
return false;
}
int trackType = mappedTrackInfo.getRendererType(rendererIndex);
return isSupportedTrackType(trackType);
}
private static boolean isSupportedTrackType(int trackType) {
switch (trackType) {
case C.TRACK_TYPE_VIDEO:
case C.TRACK_TYPE_AUDIO:
case C.TRACK_TYPE_TEXT:
return true;
default:
return false;
}
}
private static String getTrackTypeString(Resources resources, int trackType) {
switch (trackType) { switch (trackType) {
case C.TRACK_TYPE_VIDEO: case C.TRACK_TYPE_VIDEO:
return resources.getString(R.string.exo_track_selection_title_video); return resources.getString(R.string.exo_track_selection_title_video);
@ -289,12 +284,12 @@ public final class TrackSelectionDialog extends DialogFragment {
@Override @Override
public Fragment getItem(int position) { public Fragment getItem(int position) {
return tabFragments.valueAt(position); return tabFragments.get(tabTrackTypes.get(position));
} }
@Override @Override
public int getCount() { public int getCount() {
return tabFragments.size(); return tabTrackTypes.size();
} }
@Override @Override
@ -307,13 +302,12 @@ public final class TrackSelectionDialog extends DialogFragment {
public static final class TrackSelectionViewFragment extends Fragment public static final class TrackSelectionViewFragment extends Fragment
implements TrackSelectionView.TrackSelectionListener { implements TrackSelectionView.TrackSelectionListener {
private MappedTrackInfo mappedTrackInfo; private List<Tracks.Group> trackGroups;
private int rendererIndex;
private boolean allowAdaptiveSelections; private boolean allowAdaptiveSelections;
private boolean allowMultipleOverrides; private boolean allowMultipleOverrides;
/* package */ boolean isDisabled; /* package */ boolean isDisabled;
/* package */ List<SelectionOverride> overrides; /* package */ Map<TrackGroup, TrackSelectionOverride> overrides;
public TrackSelectionViewFragment() { public TrackSelectionViewFragment() {
// Retain instance across activity re-creation to prevent losing access to init data. // Retain instance across activity re-creation to prevent losing access to init data.
@ -321,21 +315,20 @@ public final class TrackSelectionDialog extends DialogFragment {
} }
public void init( public void init(
MappedTrackInfo mappedTrackInfo, List<Tracks.Group> trackGroups,
int rendererIndex, boolean isDisabled,
boolean initialIsDisabled, Map<TrackGroup, TrackSelectionOverride> overrides,
@Nullable SelectionOverride initialOverride,
boolean allowAdaptiveSelections, boolean allowAdaptiveSelections,
boolean allowMultipleOverrides) { boolean allowMultipleOverrides) {
this.mappedTrackInfo = mappedTrackInfo; this.trackGroups = trackGroups;
this.rendererIndex = rendererIndex; this.isDisabled = isDisabled;
this.isDisabled = initialIsDisabled;
this.overrides =
initialOverride == null
? Collections.emptyList()
: Collections.singletonList(initialOverride);
this.allowAdaptiveSelections = allowAdaptiveSelections; this.allowAdaptiveSelections = allowAdaptiveSelections;
this.allowMultipleOverrides = allowMultipleOverrides; this.allowMultipleOverrides = allowMultipleOverrides;
// TrackSelectionView does this filtering internally, but we need to do it here as well to
// handle the case where the TrackSelectionView is never created.
this.overrides =
new HashMap<>(
TrackSelectionView.filterOverrides(overrides, trackGroups, allowMultipleOverrides));
} }
@Override @Override
@ -351,8 +344,7 @@ public final class TrackSelectionDialog extends DialogFragment {
trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides);
trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections);
trackSelectionView.init( trackSelectionView.init(
mappedTrackInfo, trackGroups,
rendererIndex,
isDisabled, isDisabled,
overrides, overrides,
/* trackFormatComparator= */ null, /* trackFormatComparator= */ null,
@ -361,7 +353,8 @@ public final class TrackSelectionDialog extends DialogFragment {
} }
@Override @Override
public void onTrackSelectionChanged(boolean isDisabled, List<SelectionOverride> overrides) { public void onTrackSelectionChanged(
boolean isDisabled, Map<TrackGroup, TrackSelectionOverride> overrides) {
this.isDisabled = isDisabled; this.isDisabled = isDisabled;
this.overrides = overrides; this.overrides = overrides;
} }

View File

@ -45,6 +45,7 @@
<service <service
android:name=".PlaybackService" android:name=".PlaybackService"
android:foregroundServiceType="mediaPlayback"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/> <action android:name="androidx.media3.session.MediaSessionService"/>

View File

@ -19,35 +19,118 @@ import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSession.ControllerInfo
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionResult 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
class PlaybackService : MediaLibraryService() { class PlaybackService : MediaLibraryService() {
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var mediaLibrarySession: MediaLibrarySession
private val librarySessionCallback = CustomMediaLibrarySessionCallback() private lateinit var customCommands: List<CommandButton>
private var customLayout = ImmutableList.of<CommandButton>()
companion object { companion object {
private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch" private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch"
private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri" private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
"android.media3.session.demo.SHUFFLE_ON"
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
"android.media3.session.demo.SHUFFLE_OFF"
}
override fun onCreate() {
super.onCreate()
customCommands =
listOf(
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY)
),
getShuffleCommandButton(
SessionCommand(CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY)
)
)
customLayout = ImmutableList.of(customCommands[0])
initializeSessionAndPlayer()
}
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
}
override fun onDestroy() {
player.release()
mediaLibrarySession.release()
super.onDestroy()
}
private inner class CustomMediaLibrarySessionCallback : MediaLibrarySession.Callback {
override fun onConnect(
session: MediaSession,
controller: ControllerInfo
): MediaSession.ConnectionResult {
val connectionResult = super.onConnect(session, controller)
val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
customCommands.forEach { commandButton ->
// Add custom command to available session commands.
commandButton.sessionCommand?.let { availableSessionCommands.add(it) }
}
return MediaSession.ConnectionResult.accept(
availableSessionCommands.build(),
connectionResult.availablePlayerCommands
)
}
override fun onPostConnect(session: MediaSession, controller: ControllerInfo) {
if (!customLayout.isEmpty() && controller.controllerVersion != 0) {
// Let Media3 controller (for instance the MediaNotificationProvider) know about the custom
// layout right after it connected.
ignoreFuture(mediaLibrarySession.setCustomLayout(controller, customLayout))
}
}
override fun onCustomCommand(
session: MediaSession,
controller: ControllerInfo,
customCommand: SessionCommand,
args: Bundle
): ListenableFuture<SessionResult> {
if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON == customCommand.customAction) {
// Enable shuffling.
player.shuffleModeEnabled = true
// Change the custom layout to contain the `Disable shuffling` command.
customLayout = ImmutableList.of(customCommands[1])
// Send the updated custom layout to controllers.
session.setCustomLayout(customLayout)
} else if (CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF == customCommand.customAction) {
// Disable shuffling.
player.shuffleModeEnabled = false
// Change the custom layout to contain the `Enable shuffling` command.
customLayout = ImmutableList.of(customCommands[0])
// Send the updated custom layout to controllers.
session.setCustomLayout(customLayout)
}
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
} }
private inner class CustomMediaLibrarySessionCallback :
MediaLibrarySession.MediaLibrarySessionCallback {
override fun onGetLibraryRoot( override fun onGetLibraryRoot(
session: MediaLibrarySession, session: MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: ControllerInfo,
params: LibraryParams? params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> { ): ListenableFuture<LibraryResult<MediaItem>> {
return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params)) return Futures.immediateFuture(LibraryResult.ofItem(MediaItemTree.getRootItem(), params))
@ -55,7 +138,7 @@ class PlaybackService : MediaLibraryService() {
override fun onGetItem( override fun onGetItem(
session: MediaLibrarySession, session: MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: ControllerInfo,
mediaId: String mediaId: String
): ListenableFuture<LibraryResult<MediaItem>> { ): ListenableFuture<LibraryResult<MediaItem>> {
val item = val item =
@ -66,9 +149,24 @@ class PlaybackService : MediaLibraryService() {
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null)) return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null))
} }
override fun onSubscribe(
session: MediaLibrarySession,
browser: ControllerInfo,
parentId: String,
params: LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
val children =
MediaItemTree.getChildren(parentId)
?: return Futures.immediateFuture(
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
)
session.notifyChildrenChanged(browser, parentId, children.size, params)
return Futures.immediateFuture(LibraryResult.ofVoid())
}
override fun onGetChildren( override fun onGetChildren(
session: MediaLibrarySession, session: MediaLibrarySession,
browser: MediaSession.ControllerInfo, browser: ControllerInfo,
parentId: String, parentId: String,
page: Int, page: Int,
pageSize: Int, pageSize: Int,
@ -83,7 +181,21 @@ class PlaybackService : MediaLibraryService() {
return Futures.immediateFuture(LibraryResult.ofItemList(children, params)) return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
} }
private fun setMediaItemFromSearchQuery(query: String) { override fun onAddMediaItems(
mediaSession: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItems: List<MediaItem>
): ListenableFuture<List<MediaItem>> {
val updatedMediaItems: List<MediaItem> =
mediaItems.map { mediaItem ->
if (mediaItem.requestMetadata.searchQuery != null)
getMediaItemFromSearchQuery(mediaItem.requestMetadata.searchQuery!!)
else MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem
}
return Futures.immediateFuture(updatedMediaItems)
}
private fun getMediaItemFromSearchQuery(query: String): MediaItem {
// Only accept query with pattern "play [Title]" or "[Title]" // Only accept query with pattern "play [Title]" or "[Title]"
// Where [Title]: must be exactly matched // Where [Title]: must be exactly matched
// If no media with exact name found, play a random media instead // If no media with exact name found, play a random media instead
@ -94,54 +206,8 @@ class PlaybackService : MediaLibraryService() {
query query
} }
val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem() return MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem()
player.setMediaItem(item)
} }
override fun onSetMediaUri(
session: MediaSession,
controller: MediaSession.ControllerInfo,
uri: Uri,
extras: Bundle
): Int {
if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) ||
uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT)
) {
var searchQuery =
uri.getQueryParameter("query") ?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED
setMediaItemFromSearchQuery(searchQuery)
return SessionResult.RESULT_SUCCESS
} else {
return SessionResult.RESULT_ERROR_NOT_SUPPORTED
}
}
}
private class CustomMediaItemFiller : MediaSession.MediaItemFiller {
override fun fillInLocalConfiguration(
session: MediaSession,
controller: MediaSession.ControllerInfo,
mediaItem: MediaItem
): MediaItem {
return MediaItemTree.getItem(mediaItem.mediaId) ?: mediaItem
}
}
override fun onCreate() {
super.onCreate()
initializeSessionAndPlayer()
}
override fun onDestroy() {
player.release()
mediaLibrarySession.release()
super.onDestroy()
}
override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession {
return mediaLibrarySession
} }
private fun initializeSessionAndPlayer() { private fun initializeSessionAndPlayer() {
@ -151,13 +217,10 @@ class PlaybackService : MediaLibraryService() {
.build() .build()
MediaItemTree.initialize(assets) MediaItemTree.initialize(assets)
val parentScreenIntent = Intent(this, MainActivity::class.java) val sessionActivityPendingIntent =
val intent = Intent(this, PlayerActivity::class.java)
val pendingIntent =
TaskStackBuilder.create(this).run { TaskStackBuilder.create(this).run {
addNextIntent(parentScreenIntent) addNextIntent(Intent(this@PlaybackService, MainActivity::class.java))
addNextIntent(intent) addNextIntent(Intent(this@PlaybackService, PlayerActivity::class.java))
val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0 val immutableFlag = if (Build.VERSION.SDK_INT >= 23) FLAG_IMMUTABLE else 0
getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT) getPendingIntent(0, immutableFlag or FLAG_UPDATE_CURRENT)
@ -165,8 +228,29 @@ class PlaybackService : MediaLibraryService() {
mediaLibrarySession = mediaLibrarySession =
MediaLibrarySession.Builder(this, player, librarySessionCallback) MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setMediaItemFiller(CustomMediaItemFiller()) .setSessionActivity(sessionActivityPendingIntent)
.setSessionActivity(pendingIntent) .build()
if (!customLayout.isEmpty()) {
// Send custom layout to legacy session.
mediaLibrarySession.setCustomLayout(customLayout)
}
}
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON
return CommandButton.Builder()
.setDisplayName(
getString(
if (isOn) R.string.exo_controls_shuffle_on_description
else R.string.exo_controls_shuffle_off_description
)
)
.setSessionCommand(sessionCommand)
.setIconResId(if (isOn) R.drawable.exo_icon_shuffle_off else R.drawable.exo_icon_shuffle_on)
.build() .build()
} }
private fun ignoreFuture(customLayout: ListenableFuture<SessionResult>) {
/* Do nothing. */
}
} }

View File

@ -19,6 +19,7 @@ import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils;
import android.view.Surface; import android.view.Surface;
import android.view.SurfaceControl; import android.view.SurfaceControl;
import android.view.SurfaceHolder; import android.view.SurfaceHolder;
@ -35,7 +36,6 @@ import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource; import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.DefaultHttpDataSource; import androidx.media3.datasource.DefaultHttpDataSource;
import androidx.media3.datasource.HttpDataSource;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.dash.DashMediaSource; import androidx.media3.exoplayer.dash.DashMediaSource;
import androidx.media3.exoplayer.drm.DefaultDrmSessionManager; import androidx.media3.exoplayer.drm.DefaultDrmSessionManager;
@ -189,7 +189,7 @@ public final class MainActivity extends Activity {
String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA));
String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA));
UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme));
HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); DataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory();
HttpMediaDrmCallback drmCallback = HttpMediaDrmCallback drmCallback =
new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory);
drmSessionManager = drmSessionManager =
@ -202,13 +202,18 @@ public final class MainActivity extends Activity {
DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this); DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this);
MediaSource mediaSource; MediaSource mediaSource;
@C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); @Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA);
if (type == C.TYPE_DASH) { @C.ContentType
int type =
TextUtils.isEmpty(fileExtension)
? Util.inferContentType(uri)
: Util.inferContentTypeForExtension(fileExtension);
if (type == C.CONTENT_TYPE_DASH) {
mediaSource = mediaSource =
new DashMediaSource.Factory(dataSourceFactory) new DashMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)
.createMediaSource(MediaItem.fromUri(uri)); .createMediaSource(MediaItem.fromUri(uri));
} else if (type == C.TYPE_OTHER) { } else if (type == C.CONTENT_TYPE_OTHER) {
mediaSource = mediaSource =
new ProgressiveMediaSource.Factory(dataSourceFactory) new ProgressiveMediaSource.Factory(dataSourceFactory)
.setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager)

View File

@ -0,0 +1,22 @@
# Build targets for a demo MediaPipe graph.
# See README.md for instructions on using MediaPipe in the demo.
load("//mediapipe/java/com/google/mediapipe:mediapipe_aar.bzl", "mediapipe_aar")
load(
"//mediapipe/framework/tool:mediapipe_graph.bzl",
"mediapipe_binary_graph",
)
mediapipe_aar(
name = "edge_detector_mediapipe_aar",
calculators = [
"//mediapipe/calculators/image:luminance_calculator",
"//mediapipe/calculators/image:sobel_edges_calculator",
],
)
mediapipe_binary_graph(
name = "edge_detector_binary_graph",
graph = "edge_detector_mediapipe_graph.pbtxt",
output_name = "edge_detector_mediapipe_graph.binarypb",
)

View File

@ -6,4 +6,61 @@ example by removing audio or video.
See the [demos README](../README.md) for instructions on how to build and run See the [demos README](../README.md) for instructions on how to build and run
this demo. this demo.
## MediaPipe frame processing demo
Building the demo app with [MediaPipe][] integration enabled requires some extra
manual steps.
1. Follow the
[instructions](https://google.github.io/mediapipe/getting_started/install.html)
to install MediaPipe.
1. Copy the Transformer demo's build configuration and MediaPipe graph text
protocol buffer under the MediaPipe source tree. This makes it easy to
[build an AAR][] with bazel by reusing MediaPipe's workspace.
```sh
cd "<path to MediaPipe checkout>"
MEDIAPIPE_ROOT="$(pwd)"
MEDIAPIPE_TRANSFORMER_ROOT="${MEDIAPIPE_ROOT}/mediapipe/java/com/google/mediapipe/transformer"
cd "<path to the transformer demo (containing this readme)>"
TRANSFORMER_DEMO_ROOT="$(pwd)"
mkdir -p "${MEDIAPIPE_TRANSFORMER_ROOT}"
mkdir -p "${TRANSFORMER_DEMO_ROOT}/libs"
cp ${TRANSFORMER_DEMO_ROOT}/BUILD.bazel ${MEDIAPIPE_TRANSFORMER_ROOT}/BUILD
cp ${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets/edge_detector_mediapipe_graph.pbtxt \
${MEDIAPIPE_TRANSFORMER_ROOT}
```
1. Build the AAR and the binary proto for the demo's MediaPipe graph, then copy
them to Transformer.
```sh
cd ${MEDIAPIPE_ROOT}
bazel build -c opt --strip=ALWAYS \
--host_crosstool_top=@bazel_tools//tools/cpp:toolchain \
--fat_apk_cpu=arm64-v8a,armeabi-v7a \
--legacy_whole_archive=0 \
--features=-legacy_whole_archive \
--copt=-fvisibility=hidden \
--copt=-ffunction-sections \
--copt=-fdata-sections \
--copt=-fstack-protector \
--copt=-Oz \
--copt=-fomit-frame-pointer \
--copt=-DABSL_MIN_LOG_LEVEL=2 \
--linkopt=-Wl,--gc-sections,--strip-all \
mediapipe/java/com/google/mediapipe/transformer:edge_detector_mediapipe_aar.aar
cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_aar.aar \
${TRANSFORMER_DEMO_ROOT}/libs
bazel build mediapipe/java/com/google/mediapipe/transformer:edge_detector_binary_graph
cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_graph.binarypb \
${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets
```
1. In Android Studio, gradle sync and select the `withMediaPipe` build variant
(this will only appear if the AAR is present), then build and run the demo
app and select a MediaPipe-based effect.
[Transformer]: https://exoplayer.dev/transforming-media.html [Transformer]: https://exoplayer.dev/transforming-media.html
[MediaPipe]: https://google.github.io/mediapipe/
[build an AAR]: https://google.github.io/mediapipe/getting_started/android_archive_library.html

View File

@ -45,6 +45,27 @@ android {
// This demo app isn't indexed and doesn't have translations. // This demo app isn't indexed and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation' disable 'GoogleAppIndexingWarning','MissingTranslation'
} }
flavorDimensions "mediaPipe"
productFlavors {
noMediaPipe {
dimension "mediaPipe"
}
withMediaPipe {
dimension "mediaPipe"
}
}
// Ignore the withMediaPipe variant if the MediaPipe AAR is not present.
if (!project.file("libs/edge_detector_mediapipe_aar.aar").exists()) {
variantFilter { variant ->
def names = variant.flavors*.name
if (names.contains("withMediaPipe")) {
setIgnore(true)
}
}
}
} }
dependencies { dependencies {
@ -56,6 +77,14 @@ dependencies {
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-exoplayer') implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash')
implementation project(modulePrefix + 'lib-transformer') implementation project(modulePrefix + 'lib-transformer')
implementation project(modulePrefix + 'lib-ui') implementation project(modulePrefix + 'lib-ui')
// For MediaPipe and its dependencies:
withMediaPipeImplementation fileTree(dir: 'libs', include: ['*.aar'])
withMediaPipeImplementation 'com.google.flogger:flogger:latest.release'
withMediaPipeImplementation 'com.google.flogger:flogger-system-backend:latest.release'
withMediaPipeImplementation 'com.google.code.findbugs:jsr305:latest.release'
withMediaPipeImplementation 'com.google.protobuf:protobuf-javalite:3.19.1'
} }

View File

@ -0,0 +1,37 @@
#version 100
// 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.
// ES 2 fragment shader that overlays the bitmap from uTexSampler1 over a video
// frame from uTexSampler0.
precision mediump float;
// Texture containing an input video frame.
uniform sampler2D uTexSampler0;
// Texture containing the overlap bitmap.
uniform sampler2D uTexSampler1;
// Horizontal scaling factor for the overlap bitmap.
uniform float uScaleX;
// Vertical scaling factory for the overlap bitmap.
uniform float uScaleY;
varying vec2 vTexSamplingCoord;
void main() {
vec4 videoColor = texture2D(uTexSampler0, vTexSamplingCoord);
vec4 overlayColor = texture2D(uTexSampler1,
vec2(vTexSamplingCoord.x * uScaleX,
vTexSamplingCoord.y * uScaleY));
// Blend the video decoder output and the overlay bitmap.
gl_FragColor = videoColor * (1.0 - overlayColor.a)
+ overlayColor * overlayColor.a;
}

View File

@ -0,0 +1,31 @@
#version 100
// 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.
// ES 2 fragment shader that samples from a (non-external) texture with uTexSampler,
// copying from this texture to the current output while applying a vignette effect
// by linearly darkening the pixels between uInnerRadius and uOuterRadius.
precision mediump float;
uniform sampler2D uTexSampler;
uniform vec2 uCenter;
uniform float uInnerRadius;
uniform float uOuterRadius;
varying vec2 vTexSamplingCoord;
void main() {
vec3 src = texture2D(uTexSampler, vTexSamplingCoord).xyz;
float dist = distance(vTexSamplingCoord, uCenter);
float scale = clamp(1.0 - (dist - uInnerRadius) / (uOuterRadius - uInnerRadius), 0.0, 1.0);
gl_FragColor = vec4(src.r * scale, src.g * scale, src.b * scale, 1.0);
}

View File

@ -1,4 +1,4 @@
#version 300 es #version 100
// Copyright 2022 The Android Open Source Project // Copyright 2022 The Android Open Source Project
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
@ -12,12 +12,12 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// 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.
in vec4 aFramePosition;
in vec4 aTexCoords; // ES 2 vertex shader that leaves the coordinates unchanged.
uniform mat4 uTexTransform;
uniform mat4 uTransformationMatrix; attribute vec4 aFramePosition;
out vec2 vTexCoords; varying vec2 vTexSamplingCoord;
void main() { void main() {
gl_Position = uTransformationMatrix * aFramePosition; gl_Position = aFramePosition;
vTexCoords = (uTexTransform * aTexCoords).xy; vTexSamplingCoord = vec2(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5);
} }

View File

@ -0,0 +1,168 @@
/*
* 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.demo.transformer;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.Size;
import androidx.media3.common.C;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.FrameProcessingException;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import java.io.IOException;
import java.util.Locale;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link SingleFrameGlTextureProcessor} that overlays a bitmap with a logo and timer on each
* frame.
*
* <p>The bitmap is drawn using an Android {@link Canvas}.
*/
// TODO(b/227625365): Delete this class and use a texture processor from the Transformer library,
// once overlaying a bitmap and text is supported in Transformer.
/* package */ final class BitmapOverlayProcessor implements SingleFrameGlTextureProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl";
private static final int BITMAP_WIDTH_HEIGHT = 512;
private final Paint paint;
private final Bitmap overlayBitmap;
private final Canvas overlayCanvas;
private float bitmapScaleX;
private float bitmapScaleY;
private int bitmapTexId;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull Bitmap logoBitmap;
private @MonotonicNonNull GlProgram glProgram;
public BitmapOverlayProcessor() {
paint = new Paint();
paint.setTextSize(64);
paint.setAntiAlias(true);
paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF);
paint.setColor(Color.GRAY);
overlayBitmap =
Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888);
overlayCanvas = new Canvas(overlayBitmap);
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
outputSize = new Size(inputWidth, inputHeight);
try {
logoBitmap =
((BitmapDrawable)
context.getPackageManager().getApplicationIcon(context.getPackageName()))
.getBitmap();
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e);
}
bitmapTexId = GlUtil.createTexture(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
try {
checkStateNotNull(glProgram).use();
// Draw to the canvas and store it in a texture.
String text =
String.format(Locale.US, "%.02f", presentationTimeUs / (float) C.MICROS_PER_SECOND);
overlayBitmap.eraseColor(Color.TRANSPARENT);
overlayCanvas.drawBitmap(checkStateNotNull(logoBitmap), /* left= */ 3, /* top= */ 378, paint);
overlayCanvas.drawText(text, /* x= */ 160, /* y= */ 466, paint);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId);
GLUtils.texSubImage2D(
GLES20.GL_TEXTURE_2D,
/* level= */ 0,
/* xoffset= */ 0,
/* yoffset= */ 0,
flipBitmapVertically(overlayBitmap));
GlUtil.checkGlError();
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}
@Override
public void release() {
if (glProgram != null) {
glProgram.delete();
}
}
private static Bitmap flipBitmapVertically(Bitmap bitmap) {
Matrix flip = new Matrix();
flip.postScale(1f, -1f);
return Bitmap.createBitmap(
bitmap,
/* x= */ 0,
/* y= */ 0,
bitmap.getWidth(),
bitmap.getHeight(),
flip,
/* filter= */ true);
}
}

View File

@ -32,7 +32,11 @@ import android.widget.TextView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.C;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
import com.google.android.material.slider.RangeSlider;
import com.google.android.material.slider.Slider;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -49,39 +53,84 @@ public final class ConfigurationActivity extends AppCompatActivity {
public static final String AUDIO_MIME_TYPE = "audio_mime_type"; public static final String AUDIO_MIME_TYPE = "audio_mime_type";
public static final String VIDEO_MIME_TYPE = "video_mime_type"; public static final String VIDEO_MIME_TYPE = "video_mime_type";
public static final String RESOLUTION_HEIGHT = "resolution_height"; public static final String RESOLUTION_HEIGHT = "resolution_height";
public static final String TRANSLATE_X = "translate_x";
public static final String TRANSLATE_Y = "translate_y";
public static final String SCALE_X = "scale_x"; public static final String SCALE_X = "scale_x";
public static final String SCALE_Y = "scale_y"; public static final String SCALE_Y = "scale_y";
public static final String ROTATE_DEGREES = "rotate_degrees"; public static final String ROTATE_DEGREES = "rotate_degrees";
public static final String TRIM_START_MS = "trim_start_ms";
public static final String TRIM_END_MS = "trim_end_ms";
public static final String ENABLE_FALLBACK = "enable_fallback";
public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping";
public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; public static final String ENABLE_HDR_EDITING = "enable_hdr_editing";
public static final String DEMO_EFFECTS_SELECTIONS = "demo_effects_selections";
public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x";
public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y";
public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius";
public static final String PERIODIC_VIGNETTE_OUTER_RADIUS = "periodic_vignette_outer_radius";
private static final String[] INPUT_URIS = { private static final String[] INPUT_URIS = {
"https://html5demos.com/assets/dizzy.mp4", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4", "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4", "https://html5demos.com/assets/dizzy.mp4",
"https://html5demos.com/assets/dizzy.webm", "https://html5demos.com/assets/dizzy.webm",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_4s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4",
"https://storage.googleapis.com/exoplayer-test-media-1/gen/screens/dash-vod-single-segment/manifest-baseline.mpd",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-hdr-hdr10.mp4",
}; };
private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS
"MP4 with H264 video and AAC audio", "720p H264 video and AAC audio",
"MP4 with H265 video and AAC audio", "1080p H265 video and AAC audio",
"Long MP4 with H264 video and AAC audio", "360p H264 video and AAC audio",
"WebM with VP8 video and Vorbis audio", "360p VP8 video and Vorbis audio",
"4K H264 video and AAC audio (portrait, no B-frames)",
"8k H265 video and AAC audio",
"Short 1080p H265 video and AAC audio",
"Long 180p H264 video and AAC audio",
"H264 video and AAC audio (portrait, H > W, 0\u00B0)",
"H264 video and AAC audio (portrait, H < W, 90\u00B0)",
"SEF slow motion with 240 fps",
"480p DASH (non-square pixels)",
"HDR (HDR10) H265 video (encoding may fail)",
}; };
private static final String[] DEMO_EFFECTS = {
"Dizzy crop",
"Edge detector (Media Pipe)",
"Periodic vignette",
"3D spin",
"Overlay logo & timer",
"Zoom in start",
};
private static final int PERIODIC_VIGNETTE_INDEX = 2;
private static final String SAME_AS_INPUT_OPTION = "same as input"; private static final String SAME_AS_INPUT_OPTION = "same as input";
private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2);
private @MonotonicNonNull Button chooseFileButton; private @MonotonicNonNull Button selectFileButton;
private @MonotonicNonNull TextView chosenFileTextView; private @MonotonicNonNull TextView selectedFileTextView;
private @MonotonicNonNull CheckBox removeAudioCheckbox; private @MonotonicNonNull CheckBox removeAudioCheckbox;
private @MonotonicNonNull CheckBox removeVideoCheckbox; private @MonotonicNonNull CheckBox removeVideoCheckbox;
private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox; private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox;
private @MonotonicNonNull Spinner audioMimeSpinner; private @MonotonicNonNull Spinner audioMimeSpinner;
private @MonotonicNonNull Spinner videoMimeSpinner; private @MonotonicNonNull Spinner videoMimeSpinner;
private @MonotonicNonNull Spinner resolutionHeightSpinner; private @MonotonicNonNull Spinner resolutionHeightSpinner;
private @MonotonicNonNull Spinner translateSpinner;
private @MonotonicNonNull Spinner scaleSpinner; private @MonotonicNonNull Spinner scaleSpinner;
private @MonotonicNonNull Spinner rotateSpinner; private @MonotonicNonNull Spinner rotateSpinner;
private @MonotonicNonNull CheckBox trimCheckBox;
private @MonotonicNonNull CheckBox enableFallbackCheckBox;
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox;
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox;
private @MonotonicNonNull Button selectDemoEffectsButton;
private boolean @MonotonicNonNull [] demoEffectsSelections;
private int inputUriPosition; private int inputUriPosition;
private long trimStartMs;
private long trimEndMs;
private float periodicVignetteCenterX;
private float periodicVignetteCenterY;
private float periodicVignetteInnerRadius;
private float periodicVignetteOuterRadius;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
@ -90,11 +139,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
findViewById(R.id.transform_button).setOnClickListener(this::startTransformation); findViewById(R.id.transform_button).setOnClickListener(this::startTransformation);
chooseFileButton = findViewById(R.id.choose_file_button); selectFileButton = findViewById(R.id.select_file_button);
chooseFileButton.setOnClickListener(this::chooseFile); selectFileButton.setOnClickListener(this::selectFile);
chosenFileTextView = findViewById(R.id.chosen_file_text_view); selectedFileTextView = findViewById(R.id.selected_file_text_view);
chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox); removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox);
removeAudioCheckbox.setOnClickListener(this::onRemoveAudio); removeAudioCheckbox.setOnClickListener(this::onRemoveAudio);
@ -118,11 +167,10 @@ public final class ConfigurationActivity extends AppCompatActivity {
videoMimeSpinner = findViewById(R.id.video_mime_spinner); videoMimeSpinner = findViewById(R.id.video_mime_spinner);
videoMimeSpinner.setAdapter(videoMimeAdapter); videoMimeSpinner.setAdapter(videoMimeAdapter);
videoMimeAdapter.addAll( videoMimeAdapter.addAll(
SAME_AS_INPUT_OPTION, SAME_AS_INPUT_OPTION, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_H264, MimeTypes.VIDEO_MP4V);
MimeTypes.VIDEO_H263, if (Util.SDK_INT >= 24) {
MimeTypes.VIDEO_H264, videoMimeAdapter.add(MimeTypes.VIDEO_H265);
MimeTypes.VIDEO_H265, }
MimeTypes.VIDEO_MP4V);
ArrayAdapter<String> resolutionHeightAdapter = ArrayAdapter<String> resolutionHeightAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
@ -132,14 +180,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
resolutionHeightAdapter.addAll( resolutionHeightAdapter.addAll(
SAME_AS_INPUT_OPTION, "144", "240", "360", "480", "720", "1080", "1440", "2160"); SAME_AS_INPUT_OPTION, "144", "240", "360", "480", "720", "1080", "1440", "2160");
ArrayAdapter<String> translateAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
translateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
translateSpinner = findViewById(R.id.translate_spinner);
translateSpinner.setAdapter(translateAdapter);
translateAdapter.addAll(
SAME_AS_INPUT_OPTION, "-.1, -.1", "0, 0", ".5, 0", "0, .5", "1, 1", "1.9, 0", "0, 1.9");
ArrayAdapter<String> scaleAdapter = ArrayAdapter<String> scaleAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
@ -152,9 +192,22 @@ public final class ConfigurationActivity extends AppCompatActivity {
rotateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); rotateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
rotateSpinner = findViewById(R.id.rotate_spinner); rotateSpinner = findViewById(R.id.rotate_spinner);
rotateSpinner.setAdapter(rotateAdapter); rotateSpinner.setAdapter(rotateAdapter);
rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "90", "180"); rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180");
trimCheckBox = findViewById(R.id.trim_checkbox);
trimCheckBox.setOnCheckedChangeListener(this::selectTrimBounds);
trimStartMs = C.TIME_UNSET;
trimEndMs = C.TIME_UNSET;
enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox);
enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox);
enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported());
findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported());
enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
demoEffectsSelections = new boolean[DEMO_EFFECTS.length];
selectDemoEffectsButton = findViewById(R.id.select_demo_effects_button);
selectDemoEffectsButton.setOnClickListener(this::selectDemoEffects);
} }
@Override @Override
@ -162,8 +215,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
super.onResume(); super.onResume();
@Nullable Uri intentUri = getIntent().getData(); @Nullable Uri intentUri = getIntent().getData();
if (intentUri != null) { if (intentUri != null) {
checkNotNull(chooseFileButton).setEnabled(false); checkNotNull(selectFileButton).setEnabled(false);
checkNotNull(chosenFileTextView).setText(intentUri.toString()); checkNotNull(selectedFileTextView).setText(intentUri.toString());
} }
} }
@ -180,13 +233,16 @@ public final class ConfigurationActivity extends AppCompatActivity {
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"translateSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableHdrEditingCheckBox" "trimCheckBox",
"enableFallbackCheckBox",
"enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"demoEffectsSelections"
}) })
private void startTransformation(View view) { private void startTransformation(View view) {
Intent transformerIntent = new Intent(this, TransformerActivity.class); Intent transformerIntent = new Intent(/* packageContext= */ this, TransformerActivity.class);
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putBoolean(SHOULD_REMOVE_AUDIO, removeAudioCheckbox.isChecked()); bundle.putBoolean(SHOULD_REMOVE_AUDIO, removeAudioCheckbox.isChecked());
bundle.putBoolean(SHOULD_REMOVE_VIDEO, removeVideoCheckbox.isChecked()); bundle.putBoolean(SHOULD_REMOVE_VIDEO, removeVideoCheckbox.isChecked());
@ -203,13 +259,6 @@ public final class ConfigurationActivity extends AppCompatActivity {
if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) { if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) {
bundle.putInt(RESOLUTION_HEIGHT, Integer.parseInt(selectedResolutionHeight)); bundle.putInt(RESOLUTION_HEIGHT, Integer.parseInt(selectedResolutionHeight));
} }
String selectedTranslate = String.valueOf(translateSpinner.getSelectedItem());
if (!SAME_AS_INPUT_OPTION.equals(selectedTranslate)) {
List<String> translateXY = Arrays.asList(selectedTranslate.split(", "));
checkState(translateXY.size() == 2);
bundle.putFloat(TRANSLATE_X, Float.parseFloat(translateXY.get(0)));
bundle.putFloat(TRANSLATE_Y, Float.parseFloat(translateXY.get(1)));
}
String selectedScale = String.valueOf(scaleSpinner.getSelectedItem()); String selectedScale = String.valueOf(scaleSpinner.getSelectedItem());
if (!SAME_AS_INPUT_OPTION.equals(selectedScale)) { if (!SAME_AS_INPUT_OPTION.equals(selectedScale)) {
List<String> scaleXY = Arrays.asList(selectedScale.split(", ")); List<String> scaleXY = Arrays.asList(selectedScale.split(", "));
@ -221,7 +270,19 @@ public final class ConfigurationActivity extends AppCompatActivity {
if (!SAME_AS_INPUT_OPTION.equals(selectedRotate)) { if (!SAME_AS_INPUT_OPTION.equals(selectedRotate)) {
bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate)); bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate));
} }
if (trimCheckBox.isChecked()) {
bundle.putLong(TRIM_START_MS, trimStartMs);
bundle.putLong(TRIM_END_MS, trimEndMs);
}
bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked());
bundle.putBoolean(
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked());
bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked());
bundle.putBooleanArray(DEMO_EFFECTS_SELECTIONS, demoEffectsSelections);
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_X, periodicVignetteCenterX);
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY);
bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius);
bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius);
transformerIntent.putExtras(bundle); transformerIntent.putExtras(bundle);
@Nullable Uri intentUri = getIntent().getData(); @Nullable Uri intentUri = getIntent().getData();
@ -231,19 +292,82 @@ public final class ConfigurationActivity extends AppCompatActivity {
startActivity(transformerIntent); startActivity(transformerIntent);
} }
private void chooseFile(View view) { private void selectFile(View view) {
new AlertDialog.Builder(/* context= */ this) new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.choose_file_title) .setTitle(R.string.select_file_title)
.setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog) .setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog)
.setPositiveButton(android.R.string.ok, /* listener= */ null) .setPositiveButton(android.R.string.ok, /* listener= */ null)
.create() .create()
.show(); .show();
} }
@RequiresNonNull("chosenFileTextView") private void selectDemoEffects(View view) {
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_demo_effects)
.setMultiChoiceItems(
DEMO_EFFECTS, checkNotNull(demoEffectsSelections), this::selectDemoEffect)
.setPositiveButton(android.R.string.ok, /* listener= */ null)
.create()
.show();
}
private void selectTrimBounds(View view, boolean isChecked) {
if (!isChecked) {
return;
}
View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null);
RangeSlider radiusRangeSlider =
checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider));
radiusRangeSlider.setValues(0f, 60f); // seconds
new AlertDialog.Builder(/* context= */ this)
.setView(dialogView)
.setPositiveButton(
android.R.string.ok,
(DialogInterface dialogInterface, int i) -> {
List<Float> radiusRange = radiusRangeSlider.getValues();
trimStartMs = 1000 * radiusRange.get(0).longValue();
trimEndMs = 1000 * radiusRange.get(1).longValue();
})
.create()
.show();
}
@RequiresNonNull("selectedFileTextView")
private void selectFileInDialog(DialogInterface dialog, int which) { private void selectFileInDialog(DialogInterface dialog, int which) {
inputUriPosition = which; inputUriPosition = which;
chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
}
@RequiresNonNull("demoEffectsSelections")
private void selectDemoEffect(DialogInterface dialog, int which, boolean isChecked) {
demoEffectsSelections[which] = isChecked;
if (!isChecked || which != PERIODIC_VIGNETTE_INDEX) {
return;
}
View dialogView =
getLayoutInflater().inflate(R.layout.periodic_vignette_options, /* root= */ null);
Slider centerXSlider =
checkNotNull(dialogView.findViewById(R.id.periodic_vignette_center_x_slider));
Slider centerYSlider =
checkNotNull(dialogView.findViewById(R.id.periodic_vignette_center_y_slider));
RangeSlider radiusRangeSlider =
checkNotNull(dialogView.findViewById(R.id.periodic_vignette_radius_range_slider));
radiusRangeSlider.setValues(0f, HALF_DIAGONAL);
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.periodic_vignette_options)
.setView(dialogView)
.setPositiveButton(
android.R.string.ok,
(DialogInterface dialogInterface, int i) -> {
periodicVignetteCenterX = centerXSlider.getValue();
periodicVignetteCenterY = centerYSlider.getValue();
List<Float> radiusRange = radiusRangeSlider.getValues();
periodicVignetteInnerRadius = radiusRange.get(0);
periodicVignetteOuterRadius = radiusRange.get(1);
})
.create()
.show();
} }
@RequiresNonNull({ @RequiresNonNull({
@ -251,10 +375,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"translateSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableHdrEditingCheckBox" "enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"selectDemoEffectsButton"
}) })
private void onRemoveAudio(View view) { private void onRemoveAudio(View view) {
if (((CheckBox) view).isChecked()) { if (((CheckBox) view).isChecked()) {
@ -270,10 +395,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"translateSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableHdrEditingCheckBox" "enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"selectDemoEffectsButton"
}) })
private void onRemoveVideo(View view) { private void onRemoveVideo(View view) {
if (((CheckBox) view).isChecked()) { if (((CheckBox) view).isChecked()) {
@ -288,26 +414,34 @@ public final class ConfigurationActivity extends AppCompatActivity {
"audioMimeSpinner", "audioMimeSpinner",
"videoMimeSpinner", "videoMimeSpinner",
"resolutionHeightSpinner", "resolutionHeightSpinner",
"translateSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableHdrEditingCheckBox" "enableRequestSdrToneMappingCheckBox",
"enableHdrEditingCheckBox",
"selectDemoEffectsButton"
}) })
private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) {
audioMimeSpinner.setEnabled(isAudioEnabled); audioMimeSpinner.setEnabled(isAudioEnabled);
videoMimeSpinner.setEnabled(isVideoEnabled); videoMimeSpinner.setEnabled(isVideoEnabled);
resolutionHeightSpinner.setEnabled(isVideoEnabled); resolutionHeightSpinner.setEnabled(isVideoEnabled);
translateSpinner.setEnabled(isVideoEnabled);
scaleSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled);
rotateSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled);
enableRequestSdrToneMappingCheckBox.setEnabled(
isRequestSdrToneMappingSupported() && isVideoEnabled);
enableHdrEditingCheckBox.setEnabled(isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
selectDemoEffectsButton.setEnabled(isVideoEnabled);
findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled);
findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled); findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled);
findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled); findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled);
findViewById(R.id.translate).setEnabled(isVideoEnabled);
findViewById(R.id.scale).setEnabled(isVideoEnabled); findViewById(R.id.scale).setEnabled(isVideoEnabled);
findViewById(R.id.rotate).setEnabled(isVideoEnabled); findViewById(R.id.rotate).setEnabled(isVideoEnabled);
findViewById(R.id.request_sdr_tone_mapping)
.setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled);
findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled);
} }
private static boolean isRequestSdrToneMappingSupported() {
return Util.SDK_INT >= 31;
}
} }

View File

@ -0,0 +1,93 @@
/*
* 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.demo.transformer;
import android.graphics.Matrix;
import androidx.media3.common.C;
import androidx.media3.common.util.Util;
import androidx.media3.transformer.GlMatrixTransformation;
import androidx.media3.transformer.MatrixTransformation;
/**
* Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link
* MatrixTransformation MatrixTransformations} that create video effects by applying transformation
* matrices to the individual video frames.
*/
/* package */ final class MatrixTransformationFactory {
/**
* Returns a {@link MatrixTransformation} that rescales the frames over the first {@value
* #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases
* linearly in size from a single point to filling the full output frame.
*/
public static MatrixTransformation createZoomInTransition() {
return MatrixTransformationFactory::calculateZoomInTransitionMatrix;
}
/**
* Returns a {@link MatrixTransformation} that crops frames to a rectangle that moves on an
* ellipse.
*/
public static MatrixTransformation createDizzyCropEffect() {
return MatrixTransformationFactory::calculateDizzyCropMatrix;
}
/**
* Returns a {@link GlMatrixTransformation} that rotates a frame in 3D around the y-axis and
* applies perspective projection to 2D.
*/
public static GlMatrixTransformation createSpin3dEffect() {
return MatrixTransformationFactory::calculate3dSpinMatrix;
}
private static final float ZOOM_DURATION_SECONDS = 2f;
private static final float DIZZY_CROP_ROTATION_PERIOD_US = 1_500_000f;
private static Matrix calculateZoomInTransitionMatrix(long presentationTimeUs) {
Matrix transformationMatrix = new Matrix();
float scale = Math.min(1, presentationTimeUs / (C.MICROS_PER_SECOND * ZOOM_DURATION_SECONDS));
transformationMatrix.postScale(/* sx= */ scale, /* sy= */ scale);
return transformationMatrix;
}
private static android.graphics.Matrix calculateDizzyCropMatrix(long presentationTimeUs) {
double theta = presentationTimeUs * 2 * Math.PI / DIZZY_CROP_ROTATION_PERIOD_US;
float centerX = 0.5f * (float) Math.cos(theta);
float centerY = 0.5f * (float) Math.sin(theta);
android.graphics.Matrix transformationMatrix = new android.graphics.Matrix();
transformationMatrix.postTranslate(/* dx= */ centerX, /* dy= */ centerY);
transformationMatrix.postScale(/* sx= */ 2f, /* sy= */ 2f);
return transformationMatrix;
}
private static float[] calculate3dSpinMatrix(long presentationTimeUs) {
float[] transformationMatrix = new float[16];
android.opengl.Matrix.frustumM(
transformationMatrix,
/* offset= */ 0,
/* left= */ -1f,
/* right= */ 1f,
/* bottom= */ -1f,
/* top= */ 1f,
/* near= */ 3f,
/* far= */ 5f);
android.opengl.Matrix.translateM(
transformationMatrix, /* mOffset= */ 0, /* x= */ 0f, /* y= */ 0f, /* z= */ -4f);
float theta = Util.usToMs(presentationTimeUs) / 10f;
android.opengl.Matrix.rotateM(
transformationMatrix, /* mOffset= */ 0, theta, /* x= */ 0f, /* y= */ 1f, /* z= */ 0f);
return transformationMatrix;
}
}

View File

@ -0,0 +1,123 @@
/*
* 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.demo.transformer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.GLES20;
import android.util.Size;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.FrameProcessingException;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* A {@link SingleFrameGlTextureProcessor} that periodically dims the frames such that pixels are
* darker the further they are away from the frame center.
*/
/* package */ final class PeriodicVignetteProcessor implements SingleFrameGlTextureProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl";
private static final float DIMMING_PERIOD_US = 5_600_000f;
private float centerX;
private float centerY;
private float minInnerRadius;
private float deltaInnerRadius;
private float outerRadius;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull GlProgram glProgram;
/**
* Creates a new instance.
*
* <p>The inner radius of the vignette effect oscillates smoothly between {@code minInnerRadius}
* and {@code maxInnerRadius}.
*
* <p>The pixels between the inner radius and the {@code outerRadius} are darkened linearly based
* on their distance from {@code innerRadius}. All pixels outside {@code outerRadius} are black.
*
* <p>The parameters are given in normalized texture coordinates from 0 to 1.
*
* @param centerX The x-coordinate of the center of the effect.
* @param centerY The y-coordinate of the center of the effect.
* @param minInnerRadius The lower bound of the radius that is unaffected by the effect.
* @param maxInnerRadius The upper bound of the radius that is unaffected by the effect.
* @param outerRadius The radius after which all pixels are black.
*/
public PeriodicVignetteProcessor(
float centerX, float centerY, float minInnerRadius, float maxInnerRadius, float outerRadius) {
checkArgument(minInnerRadius <= maxInnerRadius);
checkArgument(maxInnerRadius <= outerRadius);
this.centerX = centerX;
this.centerY = centerY;
this.minInnerRadius = minInnerRadius;
this.deltaInnerRadius = maxInnerRadius - minInnerRadius;
this.outerRadius = outerRadius;
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
outputSize = new Size(inputWidth, inputHeight);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY});
glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius});
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
try {
checkStateNotNull(glProgram).use();
double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US;
float innerRadius =
minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta));
glProgram.setFloatsUniform("uInnerRadius", new float[] {innerRadius});
glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}
@Override
public void release() {
if (glProgram != null) {
glProgram.delete();
}
}
}

View File

@ -21,7 +21,6 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Matrix;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.os.Handler;
@ -32,6 +31,7 @@ import android.view.ViewGroup;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
@ -39,17 +39,24 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.util.DebugTextViewHelper; import androidx.media3.exoplayer.util.DebugTextViewHelper;
import androidx.media3.transformer.DefaultEncoderFactory;
import androidx.media3.transformer.EncoderSelector;
import androidx.media3.transformer.GlEffect;
import androidx.media3.transformer.ProgressHolder; import androidx.media3.transformer.ProgressHolder;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import androidx.media3.transformer.TransformationException; import androidx.media3.transformer.TransformationException;
import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.TransformationRequest;
import androidx.media3.transformer.TransformationResult;
import androidx.media3.transformer.Transformer; import androidx.media3.transformer.Transformer;
import androidx.media3.ui.AspectRatioFrameLayout; import androidx.media3.ui.AspectRatioFrameLayout;
import androidx.media3.ui.PlayerView; import androidx.media3.ui.PlayerView;
import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.common.base.Stopwatch; import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker; import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@ -145,9 +152,10 @@ public final class TransformerActivity extends AppCompatActivity {
externalCacheFile = createExternalCacheFile("transformer-output.mp4"); externalCacheFile = createExternalCacheFile("transformer-output.mp4");
String filePath = externalCacheFile.getAbsolutePath(); String filePath = externalCacheFile.getAbsolutePath();
@Nullable Bundle bundle = intent.getExtras(); @Nullable Bundle bundle = intent.getExtras();
MediaItem mediaItem = createMediaItem(bundle, uri);
Transformer transformer = createTransformer(bundle, filePath); Transformer transformer = createTransformer(bundle, filePath);
transformationStopwatch.start(); transformationStopwatch.start();
transformer.startTransformation(MediaItem.fromUri(uri), filePath); transformer.startTransformation(mediaItem, filePath);
this.transformer = transformer; this.transformer = transformer;
} catch (IOException e) { } catch (IOException e) {
throw new IllegalStateException(e); throw new IllegalStateException(e);
@ -174,6 +182,24 @@ public final class TransformerActivity extends AppCompatActivity {
}); });
} }
private MediaItem createMediaItem(@Nullable Bundle bundle, Uri uri) {
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder().setUri(uri);
if (bundle != null) {
long trimStartMs =
bundle.getLong(ConfigurationActivity.TRIM_START_MS, /* defaultValue= */ C.TIME_UNSET);
long trimEndMs =
bundle.getLong(ConfigurationActivity.TRIM_END_MS, /* defaultValue= */ C.TIME_UNSET);
if (trimStartMs != C.TIME_UNSET && trimEndMs != C.TIME_UNSET) {
mediaItemBuilder.setClippingConfiguration(
new MediaItem.ClippingConfiguration.Builder()
.setStartPositionMs(trimStartMs)
.setEndPositionMs(trimEndMs)
.build());
}
}
return mediaItemBuilder.build();
}
// Create a cache file, resetting it if it already exists. // Create a cache file, resetting it if it already exists.
private File createExternalCacheFile(String fileName) throws IOException { private File createExternalCacheFile(String fileName) throws IOException {
File file = new File(getExternalCacheDir(), fileName); File file = new File(getExternalCacheDir(), fileName);
@ -214,22 +240,88 @@ public final class TransformerActivity extends AppCompatActivity {
if (resolutionHeight != C.LENGTH_UNSET) { if (resolutionHeight != C.LENGTH_UNSET) {
requestBuilder.setResolution(resolutionHeight); requestBuilder.setResolution(resolutionHeight);
} }
Matrix transformationMatrix = getTransformationMatrix(bundle);
if (!transformationMatrix.isIdentity()) { float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1);
requestBuilder.setTransformationMatrix(transformationMatrix); float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1);
} requestBuilder.setScale(scaleX, scaleY);
float rotateDegrees =
bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0);
requestBuilder.setRotationDegrees(rotateDegrees);
requestBuilder.setEnableRequestSdrToneMapping(
bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING));
requestBuilder.experimental_setEnableHdrEditing( requestBuilder.experimental_setEnableHdrEditing(
bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING)); bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING));
transformerBuilder transformerBuilder
.setTransformationRequest(requestBuilder.build()) .setTransformationRequest(requestBuilder.build())
.setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO)) .setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO))
.setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO)); .setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO))
.setEncoderFactory(
new DefaultEncoderFactory(
EncoderSelector.DEFAULT,
/* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK)));
ImmutableList.Builder<GlEffect> effects = new ImmutableList.Builder<>();
@Nullable
boolean[] selectedEffects =
bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS);
if (selectedEffects != null) {
if (selectedEffects[0]) {
effects.add(MatrixTransformationFactory.createDizzyCropEffect());
}
if (selectedEffects[1]) {
try {
Class<?> clazz = Class.forName("androidx.media3.demo.transformer.MediaPipeProcessor");
Constructor<?> constructor =
clazz.getConstructor(String.class, String.class, String.class);
effects.add(
() -> {
try {
return (SingleFrameGlTextureProcessor)
constructor.newInstance(
/* graphName= */ "edge_detector_mediapipe_graph.binarypb",
/* inputStreamName= */ "input_video",
/* outputStreamName= */ "output_video");
} catch (Exception e) {
runOnUiThread(() -> showToast(R.string.no_media_pipe_error));
throw new RuntimeException("Failed to load MediaPipe processor", e);
}
});
} catch (Exception e) {
showToast(R.string.no_media_pipe_error);
}
}
if (selectedEffects[2]) {
effects.add(
() ->
new PeriodicVignetteProcessor(
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y),
/* minInnerRadius= */ bundle.getFloat(
ConfigurationActivity.PERIODIC_VIGNETTE_INNER_RADIUS),
/* maxInnerRadius= */ bundle.getFloat(
ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
}
if (selectedEffects[3]) {
effects.add(MatrixTransformationFactory.createSpin3dEffect());
}
if (selectedEffects[4]) {
effects.add(BitmapOverlayProcessor::new);
}
if (selectedEffects[5]) {
effects.add(MatrixTransformationFactory.createZoomInTransition());
}
transformerBuilder.setVideoFrameEffects(effects.build());
}
} }
return transformerBuilder return transformerBuilder
.addListener( .addListener(
new Transformer.Listener() { new Transformer.Listener() {
@Override @Override
public void onTransformationCompleted(MediaItem mediaItem) { public void onTransformationCompleted(
MediaItem mediaItem, TransformationResult transformationResult) {
TransformerActivity.this.onTransformationCompleted(filePath); TransformerActivity.this.onTransformationCompleted(filePath);
} }
@ -243,26 +335,6 @@ public final class TransformerActivity extends AppCompatActivity {
.build(); .build();
} }
private static Matrix getTransformationMatrix(Bundle bundle) {
Matrix transformationMatrix = new Matrix();
float translateX = bundle.getFloat(ConfigurationActivity.TRANSLATE_X, /* defaultValue= */ 0);
float translateY = bundle.getFloat(ConfigurationActivity.TRANSLATE_Y, /* defaultValue= */ 0);
// TODO(b/213198690): Get resolution for aspect ratio and scale all translations' translateX
// by this aspect ratio.
transformationMatrix.postTranslate(translateX, translateY);
float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1);
float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1);
transformationMatrix.postScale(scaleX, scaleY);
float rotateDegrees =
bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0);
transformationMatrix.postRotate(rotateDegrees);
return transformationMatrix;
}
@RequiresNonNull({ @RequiresNonNull({
"informationTextView", "informationTextView",
"progressViewGroup", "progressViewGroup",
@ -335,6 +407,10 @@ public final class TransformerActivity extends AppCompatActivity {
} }
} }
private void showToast(@StringRes int messageResource) {
Toast.makeText(getApplicationContext(), getString(messageResource), Toast.LENGTH_LONG).show();
}
private final class DemoDebugViewProvider implements Transformer.DebugViewProvider { private final class DemoDebugViewProvider implements Transformer.DebugViewProvider {
@Nullable @Nullable

View File

@ -34,18 +34,18 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<Button <Button
android:id="@+id/choose_file_button" android:id="@+id/select_file_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="32dp" android:layout_marginTop="32dp"
android:layout_marginStart="32dp" android:layout_marginStart="32dp"
android:layout_marginEnd="32dp" android:layout_marginEnd="32dp"
android:text="@string/choose_file_title" android:text="@string/select_file_title"
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view" app:layout_constraintTop_toBottomOf="@+id/configuration_text_view"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" /> app:layout_constraintStart_toStartOf="parent" />
<TextView <TextView
android:id="@+id/chosen_file_text_view" android:id="@+id/selected_file_text_view"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
@ -57,14 +57,14 @@
android:gravity="center" android:gravity="center"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/choose_file_button" /> app:layout_constraintTop_toBottomOf="@+id/select_file_button" />
<androidx.core.widget.NestedScrollView <androidx.core.widget.NestedScrollView
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/chosen_file_text_view" app:layout_constraintTop_toBottomOf="@+id/selected_file_text_view"
app:layout_constraintBottom_toTopOf="@+id/transform_button"> app:layout_constraintBottom_toTopOf="@+id/select_demo_effects_button">
<TableLayout <TableLayout
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -137,17 +137,6 @@
android:layout_gravity="right|center_vertical" android:layout_gravity="right|center_vertical"
android:gravity="right" /> android:gravity="right" />
</TableRow> </TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/translate"
android:text="@string/translate"/>
<Spinner
android:id="@+id/translate_spinner"
android:layout_gravity="right|center_vertical"
android:gravity="right" />
</TableRow>
<TableRow <TableRow
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" > android:gravity="center_vertical" >
@ -170,6 +159,36 @@
android:layout_gravity="right|center_vertical" android:layout_gravity="right|center_vertical"
android:gravity="right" /> android:gravity="right" />
</TableRow> </TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/trim"
android:text="@string/trim" />
<CheckBox
android:id="@+id/trim_checkbox"
android:layout_gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/enable_fallback" />
<CheckBox
android:id="@+id/enable_fallback_checkbox"
android:layout_gravity="right"
android:checked="true"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:id="@+id/request_sdr_tone_mapping"
android:text="@string/request_sdr_tone_mapping" />
<CheckBox
android:id="@+id/request_sdr_tone_mapping_checkbox"
android:layout_gravity="right" />
</TableRow>
<TableRow <TableRow
android:layout_weight="1" android:layout_weight="1"
android:gravity="center_vertical" > android:gravity="center_vertical" >
@ -182,6 +201,17 @@
</TableRow> </TableRow>
</TableLayout> </TableLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<Button
android:id="@+id/select_demo_effects_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:text="@string/select_demo_effects"
app:layout_constraintBottom_toTopOf="@+id/transform_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<Button <Button
android:id="@+id/transform_button" android:id="@+id/transform_button"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".ConfigurationActivity">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/center_x" />
<com.google.android.material.slider.Slider
android:id="@+id/periodic_vignette_center_x_slider"
android:valueFrom="0.0"
android:value="0.5"
android:valueTo="1.0"
android:layout_gravity="right"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/center_y" />
<com.google.android.material.slider.Slider
android:id="@+id/periodic_vignette_center_y_slider"
android:valueFrom="0.0"
android:value="0.5"
android:valueTo="1.0"
android:layout_gravity="right"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/radius_range" />
<com.google.android.material.slider.RangeSlider
android:id="@+id/periodic_vignette_radius_range_slider"
android:valueFrom="0.0"
android:valueTo="1.414"
android:layout_gravity="right"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".ConfigurationActivity">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/trim_range" />
<com.google.android.material.slider.RangeSlider
android:id="@+id/trim_bounds_range_slider"
android:valueFrom="0.0"
android:valueTo="60.0"
android:layout_gravity="right"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -17,22 +17,31 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" translatable="false">Transformer Demo</string> <string name="app_name" translatable="false">Transformer Demo</string>
<string name="configuration" translatable="false">Configuration</string> <string name="configuration" translatable="false">Configuration</string>
<string name="choose_file_title" translatable="false">Choose file</string> <string name="select_file_title" translatable="false">Choose file</string>
<string name="remove_audio" translatable="false">Remove audio</string> <string name="remove_audio" translatable="false">Remove audio</string>
<string name="remove_video" translatable="false">Remove video</string> <string name="remove_video" translatable="false">Remove video</string>
<string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string> <string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string>
<string name="audio_mime" translatable="false">Output audio MIME type</string> <string name="audio_mime" translatable="false">Output audio MIME type</string>
<string name="video_mime" translatable="false">Output video MIME type</string> <string name="video_mime" translatable="false">Output video MIME type</string>
<string name="resolution_height" translatable="false">Output video resolution</string> <string name="resolution_height" translatable="false">Output video resolution</string>
<string name="translate" translatable="false">Translate video</string>
<string name="scale" translatable="false">Scale video</string> <string name="scale" translatable="false">Scale video</string>
<string name="rotate" translatable="false">Rotate video (degrees)</string> <string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="transform" translatable="false">Transform</string> <string name="enable_fallback" translatable="false">Enable fallback</string>
<string name="trim" translatable="false">Trim</string>
<string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string>
<string name="hdr_editing" translatable="false">[Experimental] HDR editing</string> <string name="hdr_editing" translatable="false">[Experimental] HDR editing</string>
<string name="select_demo_effects" translatable="false">Add demo effects</string>
<string name="periodic_vignette_options" translatable="false">Periodic vignette options</string>
<string name="no_media_pipe_error" translatable="false">Failed to load MediaPipe processor. Check the README for instructions.</string>
<string name="transform" translatable="false">Transform</string>
<string name="debug_preview" translatable="false">Debug preview:</string> <string name="debug_preview" translatable="false">Debug preview:</string>
<string name="debug_preview_not_available" translatable="false">No debug preview available.</string> <string name="debug_preview_not_available" translatable="false">No debug preview available.</string>
<string name="transformation_started" translatable="false">Transformation started</string> <string name="transformation_started" translatable="false">Transformation started</string>
<string name="transformation_timer" translatable="false">Transformation started %d seconds ago.</string> <string name="transformation_timer" translatable="false">Transformation started %d seconds ago.</string>
<string name="transformation_completed" translatable="false">Transformation completed in %d seconds.</string> <string name="transformation_completed" translatable="false">Transformation completed in %d seconds.</string>
<string name="transformation_error" translatable="false">Transformation error</string> <string name="transformation_error" translatable="false">Transformation error</string>
<string name="center_x">Center X</string>
<string name="center_y">Center Y</string>
<string name="radius_range">Radius range</string>
<string name="trim_range">Bounds in seconds</string>
</resources> </resources>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest package="androidx.media3.demo.transformer">
<uses-sdk />
</manifest>

View File

@ -0,0 +1,13 @@
# Demo MediaPipe graph that shows edges using a SobelEdgesCalculator.
input_stream: "input_video"
output_stream: "output_video"
node: {
calculator: "LuminanceCalculator"
input_stream: "input_video"
output_stream: "luma_video"
}
node: {
calculator: "SobelEdgesCalculator"
input_stream: "luma_video"
output_stream: "output_video"
}

View File

@ -0,0 +1,175 @@
/*
* 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.demo.transformer;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context;
import android.opengl.EGL14;
import android.opengl.GLES20;
import android.util.Size;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.LibraryLoader;
import androidx.media3.transformer.FrameProcessingException;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import com.google.mediapipe.components.FrameProcessor;
import com.google.mediapipe.framework.AndroidAssetUtil;
import com.google.mediapipe.framework.AppTextureFrame;
import com.google.mediapipe.framework.TextureFrame;
import com.google.mediapipe.glutil.EglManager;
import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
* Runs a MediaPipe graph on input frames. The implementation is currently limited to graphs that
* can immediately produce one output frame per input frame.
*/
/* package */ final class MediaPipeProcessor implements SingleFrameGlTextureProcessor {
private static final LibraryLoader LOADER =
new LibraryLoader("mediapipe_jni") {
@Override
protected void loadLibrary(String name) {
System.loadLibrary(name);
}
};
static {
// Not all build configurations require OpenCV to be loaded separately, so attempt to load the
// library but ignore the error if it's not present.
try {
System.loadLibrary("opencv_java3");
} catch (UnsatisfiedLinkError e) {
// Do nothing.
}
}
private static final String COPY_VERTEX_SHADER_NAME = "vertex_shader_copy_es2.glsl";
private static final String COPY_FRAGMENT_SHADER_NAME = "shaders/fragment_shader_copy_es2.glsl";
private final String graphName;
private final String inputStreamName;
private final String outputStreamName;
private final ConditionVariable frameProcessorConditionVariable;
private @MonotonicNonNull FrameProcessor frameProcessor;
private int inputWidth;
private int inputHeight;
private int inputTexId;
private @MonotonicNonNull GlProgram glProgram;
private @MonotonicNonNull TextureFrame outputFrame;
private @MonotonicNonNull RuntimeException frameProcessorPendingError;
/**
* Creates a new texture processor that wraps a MediaPipe graph.
*
* @param graphName Name of a MediaPipe graph asset to load.
* @param inputStreamName Name of the input video stream in the graph.
* @param outputStreamName Name of the input video stream in the graph.
*/
public MediaPipeProcessor(String graphName, String inputStreamName, String outputStreamName) {
checkState(LOADER.isAvailable());
this.graphName = graphName;
this.inputStreamName = inputStreamName;
this.outputStreamName = outputStreamName;
frameProcessorConditionVariable = new ConditionVariable();
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
this.inputTexId = inputTexId;
this.inputWidth = inputWidth;
this.inputHeight = inputHeight;
glProgram = new GlProgram(context, COPY_VERTEX_SHADER_NAME, COPY_FRAGMENT_SHADER_NAME);
AndroidAssetUtil.initializeNativeAssetManager(context);
EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext());
frameProcessor =
new FrameProcessor(
context, eglManager.getNativeContext(), graphName, inputStreamName, outputStreamName);
// Unblock drawFrame when there is an output frame or an error.
frameProcessor.setConsumer(
frame -> {
outputFrame = frame;
frameProcessorConditionVariable.open();
});
frameProcessor.setAsynchronousErrorListener(
error -> {
frameProcessorPendingError = error;
frameProcessorConditionVariable.open();
});
}
@Override
public Size getOutputSize() {
return new Size(inputWidth, inputHeight);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
frameProcessorConditionVariable.close();
// Pass the input frame to MediaPipe.
AppTextureFrame appTextureFrame = new AppTextureFrame(inputTexId, inputWidth, inputHeight);
appTextureFrame.setTimestamp(presentationTimeUs);
checkStateNotNull(frameProcessor).onNewFrame(appTextureFrame);
// Wait for output to be passed to the consumer.
try {
frameProcessorConditionVariable.block();
} catch (InterruptedException e) {
// Propagate the interrupted flag so the next blocking operation will throw.
// TODO(b/230469581): The next processor that runs will not have valid input due to returning
// early here. This could be fixed by checking for interruption in the outer loop that runs
// through the texture processors.
Thread.currentThread().interrupt();
return;
}
if (frameProcessorPendingError != null) {
throw new FrameProcessingException(frameProcessorPendingError);
}
// Copy from MediaPipe's output texture to the current output.
try {
checkStateNotNull(glProgram).use();
glProgram.setSamplerTexIdUniform(
"uTexSampler", checkStateNotNull(outputFrame).getTextureName(), /* texUnitIndex= */ 0);
glProgram.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.bindAttributesAndUniforms();
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
} finally {
checkStateNotNull(outputFrame).release();
}
}
@Override
public void release() {
checkStateNotNull(frameProcessor).close();
}
}

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists

View File

@ -48,9 +48,21 @@ class CombinedJavadocPlugin implements Plugin<Project> {
libraryModule.android.libraryVariants.all { variant -> libraryModule.android.libraryVariants.all { variant ->
def name = variant.buildType.name def name = variant.buildType.name
if (name == "release") { if (name == "release") {
// Works around b/234569640 that causes different versions of the androidx.media
// jar to be on the classpath.
def allJarFiles = []
allJarFiles.addAll(variant.javaCompileProvider.get().classpath.files)
def filteredJarFiles = allJarFiles.findAll { file ->
if (file ==~ /.*media-.\..\..-api.jar$/
&& !file.path.endsWith(
"media-" + project.ext.androidxMediaVersion + "-api.jar")) {
return false;
}
return true;
}
classpath += classpath +=
libraryModule.project.files( libraryModule.project.files(
variant.javaCompileProvider.get().classpath.files, filteredJarFiles,
libraryModule.project.android.getBootClasspath()) libraryModule.project.android.getBootClasspath())
} }
} }

View File

@ -34,19 +34,15 @@ import androidx.media3.common.DeviceInfo;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaLibraryInfo; import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters; import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackGroupArray;
import androidx.media3.common.TrackSelection;
import androidx.media3.common.TrackSelectionArray;
import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo; 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.CueGroup;
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.ListenerSet; import androidx.media3.common.util.ListenerSet;
@ -68,7 +64,6 @@ import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback; import com.google.android.gms.common.api.ResultCallback;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.util.List; import java.util.List;
import org.checkerframework.checker.nullness.compatqual.NullableType;
import org.checkerframework.checker.nullness.qual.RequiresNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/** /**
@ -107,7 +102,8 @@ public final class CastPlayer extends BasePlayer {
COMMAND_GET_MEDIA_ITEMS_METADATA, COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA, COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_CHANGE_MEDIA_ITEMS, COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACK_INFOS) COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM)
.build(); .build();
public static final float MIN_SPEED_SUPPORTED = 0.5f; public static final float MIN_SPEED_SUPPORTED = 0.5f;
@ -115,13 +111,7 @@ public final class CastPlayer extends BasePlayer {
private static final String TAG = "CastPlayer"; private static final String TAG = "CastPlayer";
private static final int RENDERER_COUNT = 3;
private static final int RENDERER_INDEX_VIDEO = 0;
private static final int RENDERER_INDEX_AUDIO = 1;
private static final int RENDERER_INDEX_TEXT = 2;
private static final long PROGRESS_REPORT_PERIOD_MS = 1000; private static final long PROGRESS_REPORT_PERIOD_MS = 1000;
private static final TrackSelectionArray EMPTY_TRACK_SELECTION_ARRAY =
new TrackSelectionArray(null, null, null);
private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0]; private static final long[] EMPTY_TRACK_ID_ARRAY = new long[0];
private final CastContext castContext; private final CastContext castContext;
@ -146,9 +136,7 @@ public final class CastPlayer extends BasePlayer {
private final StateHolder<PlaybackParameters> playbackParameters; private final StateHolder<PlaybackParameters> playbackParameters;
@Nullable private RemoteMediaClient remoteMediaClient; @Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline; private CastTimeline currentTimeline;
private TrackGroupArray currentTrackGroups; private Tracks currentTracks;
private TrackSelectionArray currentTrackSelection;
private TracksInfo currentTracksInfo;
private Commands availableCommands; private Commands availableCommands;
private @Player.State int playbackState; private @Player.State int playbackState;
private int currentWindowIndex; private int currentWindowIndex;
@ -224,9 +212,7 @@ public final class CastPlayer extends BasePlayer {
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT); playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
playbackState = STATE_IDLE; playbackState = STATE_IDLE;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE; currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
currentTrackGroups = TrackGroupArray.EMPTY; currentTracks = Tracks.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
currentTracksInfo = TracksInfo.EMPTY;
availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build(); availableCommands = new Commands.Builder().addAll(PERMANENT_AVAILABLE_COMMANDS).build();
pendingSeekWindowIndex = C.INDEX_UNSET; pendingSeekWindowIndex = C.INDEX_UNSET;
pendingSeekPositionMs = C.TIME_UNSET; pendingSeekPositionMs = C.TIME_UNSET;
@ -473,6 +459,11 @@ public final class CastPlayer extends BasePlayer {
stop(/* reset= */ false); stop(/* reset= */ false);
} }
/**
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@Deprecated @Deprecated
@Override @Override
public void stop(boolean reset) { public void stop(boolean reset) {
@ -558,18 +549,8 @@ public final class CastPlayer extends BasePlayer {
} }
@Override @Override
public TrackGroupArray getCurrentTrackGroups() { public Tracks getCurrentTracks() {
return currentTrackGroups; return currentTracks;
}
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return currentTrackSelection;
}
@Override
public TracksInfo getCurrentTracksInfo() {
return currentTracksInfo;
} }
@Override @Override
@ -730,10 +711,10 @@ public final class CastPlayer extends BasePlayer {
return VideoSize.UNKNOWN; return VideoSize.UNKNOWN;
} }
/** This method is not supported and returns an empty list. */ /** This method is not supported and returns an empty {@link CueGroup}. */
@Override @Override
public ImmutableList<Cue> getCurrentCues() { public CueGroup getCurrentCues() {
return ImmutableList.of(); return CueGroup.EMPTY;
} }
/** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */ /** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */
@ -842,10 +823,7 @@ public final class CastPlayer extends BasePlayer {
} }
if (updateTracksAndSelectionsAndNotifyIfChanged()) { if (updateTracksAndSelectionsAndNotifyIfChanged()) {
listeners.queueEvent( listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED, Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksChanged(currentTracks));
listener -> listener.onTracksChanged(currentTrackGroups, currentTrackSelection));
listeners.queueEvent(
Player.EVENT_TRACKS_CHANGED, listener -> listener.onTracksInfoChanged(currentTracksInfo));
} }
updateAvailableCommandsAndNotifyIfChanged(); updateAvailableCommandsAndNotifyIfChanged();
listeners.flushEvents(); listeners.flushEvents();
@ -1000,55 +978,33 @@ public final class CastPlayer extends BasePlayer {
return false; return false;
} }
MediaStatus mediaStatus = getMediaStatus(); @Nullable MediaStatus mediaStatus = getMediaStatus();
MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null; @Nullable MediaInfo mediaInfo = mediaStatus != null ? mediaStatus.getMediaInfo() : null;
@Nullable
List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null; List<MediaTrack> castMediaTracks = mediaInfo != null ? mediaInfo.getMediaTracks() : null;
if (castMediaTracks == null || castMediaTracks.isEmpty()) { if (castMediaTracks == null || castMediaTracks.isEmpty()) {
boolean hasChanged = !currentTrackGroups.isEmpty(); boolean hasChanged = !Tracks.EMPTY.equals(currentTracks);
currentTrackGroups = TrackGroupArray.EMPTY; currentTracks = Tracks.EMPTY;
currentTrackSelection = EMPTY_TRACK_SELECTION_ARRAY;
currentTracksInfo = TracksInfo.EMPTY;
return hasChanged; return hasChanged;
} }
long[] activeTrackIds = mediaStatus.getActiveTrackIds(); @Nullable long[] activeTrackIds = mediaStatus.getActiveTrackIds();
if (activeTrackIds == null) { if (activeTrackIds == null) {
activeTrackIds = EMPTY_TRACK_ID_ARRAY; activeTrackIds = EMPTY_TRACK_ID_ARRAY;
} }
TrackGroup[] trackGroups = new TrackGroup[castMediaTracks.size()]; Tracks.Group[] trackGroups = new Tracks.Group[castMediaTracks.size()];
@NullableType TrackSelection[] trackSelections = new TrackSelection[RENDERER_COUNT];
TracksInfo.TrackGroupInfo[] trackGroupInfos =
new TracksInfo.TrackGroupInfo[castMediaTracks.size()];
for (int i = 0; i < castMediaTracks.size(); i++) { for (int i = 0; i < castMediaTracks.size(); i++) {
MediaTrack mediaTrack = castMediaTracks.get(i); MediaTrack mediaTrack = castMediaTracks.get(i);
trackGroups[i] = TrackGroup trackGroup =
new TrackGroup(/* id= */ Integer.toString(i), CastUtils.mediaTrackToFormat(mediaTrack)); new TrackGroup(/* id= */ Integer.toString(i), CastUtils.mediaTrackToFormat(mediaTrack));
@C.FormatSupport int[] trackSupport = new int[] {C.FORMAT_HANDLED};
long id = mediaTrack.getId(); boolean[] trackSelected = new boolean[] {isTrackActive(mediaTrack.getId(), activeTrackIds)};
@C.TrackType int trackType = MimeTypes.getTrackType(mediaTrack.getContentType()); trackGroups[i] =
int rendererIndex = getRendererIndexForTrackType(trackType); new Tracks.Group(trackGroup, /* adaptiveSupported= */ false, trackSupport, trackSelected);
boolean supported = rendererIndex != C.INDEX_UNSET;
boolean selected =
isTrackActive(id, activeTrackIds) && supported && trackSelections[rendererIndex] == null;
if (selected) {
trackSelections[rendererIndex] = new CastTrackSelection(trackGroups[i]);
} }
@C.FormatSupport Tracks newTracks = new Tracks(ImmutableList.copyOf(trackGroups));
int[] trackSupport = new int[] {supported ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_TYPE}; if (!newTracks.equals(currentTracks)) {
final boolean[] trackSelected = new boolean[] {selected}; currentTracks = newTracks;
trackGroupInfos[i] =
new TracksInfo.TrackGroupInfo(trackGroups[i], trackSupport, trackType, trackSelected);
}
TrackGroupArray newTrackGroups = new TrackGroupArray(trackGroups);
TrackSelectionArray newTrackSelections = new TrackSelectionArray(trackSelections);
TracksInfo newTracksInfo = new TracksInfo(ImmutableList.copyOf(trackGroupInfos));
if (!newTrackGroups.equals(currentTrackGroups)
|| !newTrackSelections.equals(currentTrackSelection)
|| !newTracksInfo.equals(currentTracksInfo)) {
currentTrackSelection = newTrackSelections;
currentTrackGroups = newTrackGroups;
currentTracksInfo = newTracksInfo;
return true; return true;
} }
return false; return false;
@ -1306,14 +1262,6 @@ public final class CastPlayer extends BasePlayer {
return false; return false;
} }
private static int getRendererIndexForTrackType(@C.TrackType int trackType) {
return trackType == C.TRACK_TYPE_VIDEO
? RENDERER_INDEX_VIDEO
: trackType == C.TRACK_TYPE_AUDIO
? RENDERER_INDEX_AUDIO
: trackType == C.TRACK_TYPE_TEXT ? RENDERER_INDEX_TEXT : C.INDEX_UNSET;
}
private static int getCastRepeatMode(@RepeatMode int repeatMode) { private static int getCastRepeatMode(@RepeatMode int repeatMode) {
switch (repeatMode) { switch (repeatMode) {
case REPEAT_MODE_ONE: case REPEAT_MODE_ONE:

View File

@ -1,96 +0,0 @@
/*
* Copyright (C) 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.cast;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.TrackGroup;
import androidx.media3.common.TrackSelection;
import androidx.media3.common.util.Assertions;
/**
* {@link TrackSelection} that only selects the first track of the provided {@link TrackGroup}.
*
* <p>This relies on {@link CastPlayer} track groups only having one track.
*/
/* package */ class CastTrackSelection implements TrackSelection {
private final TrackGroup trackGroup;
/** @param trackGroup The {@link TrackGroup} from which the first track will only be selected. */
public CastTrackSelection(TrackGroup trackGroup) {
this.trackGroup = trackGroup;
}
@Override
public int getType() {
return TYPE_UNSET;
}
@Override
public TrackGroup getTrackGroup() {
return trackGroup;
}
@Override
public int length() {
return 1;
}
@Override
public Format getFormat(int index) {
Assertions.checkArgument(index == 0);
return trackGroup.getFormat(0);
}
@Override
public int getIndexInTrackGroup(int index) {
return index == 0 ? 0 : C.INDEX_UNSET;
}
@Override
@SuppressWarnings("ReferenceEquality")
public int indexOf(Format format) {
return format == trackGroup.getFormat(0) ? 0 : C.INDEX_UNSET;
}
@Override
public int indexOf(int indexInTrackGroup) {
return indexInTrackGroup == 0 ? 0 : C.INDEX_UNSET;
}
// Object overrides.
@Override
public int hashCode() {
return System.identityHashCode(trackGroup);
}
// Track groups are compared by identity not value, as distinct groups may have the same value.
@Override
@SuppressWarnings({"ReferenceEquality", "EqualsGetClass"})
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
CastTrackSelection other = (CastTrackSelection) obj;
return trackGroup == other.trackGroup;
}
}

View File

@ -36,6 +36,7 @@ import static androidx.media3.common.Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS;
import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; import static androidx.media3.common.Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME; import static androidx.media3.common.Player.COMMAND_SET_DEVICE_VOLUME;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEM;
import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA; import static androidx.media3.common.Player.COMMAND_SET_MEDIA_ITEMS_METADATA;
import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE; import static androidx.media3.common.Player.COMMAND_SET_REPEAT_MODE;
import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE; import static androidx.media3.common.Player.COMMAND_SET_SHUFFLE_MODE;
@ -1359,6 +1360,7 @@ public class CastPlayerTest {
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_MEDIA_ITEMS_METADATA)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEMS_METADATA)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue(); assertThat(castPlayer.isCommandAvailable(COMMAND_CHANGE_MEDIA_ITEMS)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_VOLUME)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_VOLUME)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isFalse(); assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isFalse();

View File

@ -1,77 +0,0 @@
/*
* Copyright (C) 2018 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.cast;
import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.TrackGroup;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Test for {@link CastTrackSelection}. */
@RunWith(AndroidJUnit4.class)
public class CastTrackSelectionTest {
private static final TrackGroup TRACK_GROUP =
new TrackGroup(new Format.Builder().build(), new Format.Builder().build());
private static final CastTrackSelection SELECTION = new CastTrackSelection(TRACK_GROUP);
@Test
public void length_isOne() {
assertThat(SELECTION.length()).isEqualTo(1);
}
@Test
public void getTrackGroup_returnsSameGroup() {
assertThat(SELECTION.getTrackGroup()).isSameInstanceAs(TRACK_GROUP);
}
@Test
public void getFormatSelectedTrack_isFirstTrack() {
assertThat(SELECTION.getFormat(0)).isSameInstanceAs(TRACK_GROUP.getFormat(0));
}
@Test
public void getIndexInTrackGroup_ofSelectedTrack_returnsFirstTrack() {
assertThat(SELECTION.getIndexInTrackGroup(0)).isEqualTo(0);
}
@Test
public void getIndexInTrackGroup_onePastTheEnd_returnsIndexUnset() {
assertThat(SELECTION.getIndexInTrackGroup(1)).isEqualTo(C.INDEX_UNSET);
}
@Test
public void indexOf_selectedTrack_returnsFirstTrack() {
assertThat(SELECTION.indexOf(0)).isEqualTo(0);
}
@Test
public void indexOf_onePastTheEnd_returnsIndexUnset() {
assertThat(SELECTION.indexOf(1)).isEqualTo(C.INDEX_UNSET);
}
@Test(expected = Exception.class)
public void getFormat_outOfBound_throws() {
CastTrackSelection selection = new CastTrackSelection(TRACK_GROUP);
selection.getFormat(1);
}
}

View File

@ -30,6 +30,9 @@ android {
testCoverageEnabled = true testCoverageEnabled = true
} }
} }
lint {
baseline = file("lint-baseline.xml")
}
} }
dependencies { dependencies {

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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.
-->
<issues format="6" by="lint 7.2.1" type="baseline" client="gradle" dependencies="false" name="AGP (7.2.1)" variant="all" version="7.2.1">
<issue
id="NewApi"
message="Call requires API level 33 (current min is 32): `android.media.AudioAttributes.Builder#setSpatializationBehavior`"
errorLine1=" builder.setSpatializationBehavior(spatializationBehavior);"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/java/androidx/media3/common/AudioAttributes.java"
line="278"
column="15"/>
</issue>
</issues>

View File

@ -97,14 +97,18 @@ public final class AdOverlayInfo {
/** An optional, detailed reason that the overlay view is needed. */ /** An optional, detailed reason that the overlay view is needed. */
@Nullable public final String reasonDetail; @Nullable public final String reasonDetail;
/** @deprecated Use {@link Builder} instead. */ /**
* @deprecated Use {@link Builder} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public AdOverlayInfo(View view, @Purpose int purpose) { public AdOverlayInfo(View view, @Purpose int purpose) {
this(view, purpose, /* detailedReason= */ null); this(view, purpose, /* detailedReason= */ null);
} }
/** @deprecated Use {@link Builder} instead. */ /**
* @deprecated Use {@link Builder} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public AdOverlayInfo(View view, @Purpose int purpose, @Nullable String detailedReason) { public AdOverlayInfo(View view, @Purpose int purpose, @Nullable String detailedReason) {

View File

@ -827,6 +827,36 @@ public final class AdPlaybackState implements Bundleable {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount); adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
} }
/**
* Returns a copy of the ad playback state with the given ads ID.
*
* @param adsId The new ads ID.
* @param adPlaybackState The ad playback state to copy.
* @return The new ad playback state.
*/
public static AdPlaybackState fromAdPlaybackState(Object adsId, AdPlaybackState adPlaybackState) {
AdGroup[] adGroups =
new AdGroup[adPlaybackState.adGroupCount - adPlaybackState.removedAdGroupCount];
for (int i = 0; i < adGroups.length; i++) {
AdGroup adGroup = adPlaybackState.adGroups[i];
adGroups[i] =
new AdGroup(
adGroup.timeUs,
adGroup.count,
Arrays.copyOf(adGroup.states, adGroup.states.length),
Arrays.copyOf(adGroup.uris, adGroup.uris.length),
Arrays.copyOf(adGroup.durationsUs, adGroup.durationsUs.length),
adGroup.contentResumeOffsetUs,
adGroup.isServerSideInserted);
}
return new AdPlaybackState(
adsId,
adGroups,
adPlaybackState.adResumePositionUs,
adPlaybackState.contentDurationUs,
adPlaybackState.removedAdGroupCount);
}
@Override @Override
public boolean equals(@Nullable Object o) { public boolean equals(@Nullable Object o) {
if (this == o) { if (this == o) {

View File

@ -28,7 +28,6 @@ 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;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.lang.reflect.Method;
/** /**
* Attributes for audio playback, which configure the underlying platform {@link * Attributes for audio playback, which configure the underlying platform {@link
@ -43,10 +42,31 @@ import java.lang.reflect.Method;
*/ */
public final class AudioAttributes implements Bundleable { public final class AudioAttributes implements Bundleable {
/** A direct wrapper around {@link android.media.AudioAttributes}. */
@RequiresApi(21)
public static final class AudioAttributesV21 {
public final android.media.AudioAttributes audioAttributes;
private AudioAttributesV21(AudioAttributes audioAttributes) {
android.media.AudioAttributes.Builder builder =
new android.media.AudioAttributes.Builder()
.setContentType(audioAttributes.contentType)
.setFlags(audioAttributes.flags)
.setUsage(audioAttributes.usage);
if (Util.SDK_INT >= 29) {
Api29.setAllowedCapturePolicy(builder, audioAttributes.allowedCapturePolicy);
}
if (Util.SDK_INT >= 32) {
Api32.setSpatializationBehavior(builder, audioAttributes.spatializationBehavior);
}
this.audioAttributes = builder.build();
}
}
/** /**
* The default audio attributes, where the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage * The default audio attributes, where the content type is {@link C#AUDIO_CONTENT_TYPE_UNKNOWN},
* is {@link C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are * usage is {@link C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags
* set. * are set.
*/ */
public static final AudioAttributes DEFAULT = new Builder().build(); public static final AudioAttributes DEFAULT = new Builder().build();
@ -62,11 +82,11 @@ public final class AudioAttributes implements Bundleable {
/** /**
* Creates a new builder for {@link AudioAttributes}. * Creates a new builder for {@link AudioAttributes}.
* *
* <p>By default the content type is {@link C#CONTENT_TYPE_UNKNOWN}, usage is {@link * <p>By default the content type is {@link C#AUDIO_CONTENT_TYPE_UNKNOWN}, usage is {@link
* C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are set. * C#USAGE_MEDIA}, capture policy is {@link C#ALLOW_CAPTURE_BY_ALL} and no flags are set.
*/ */
public Builder() { public Builder() {
contentType = C.CONTENT_TYPE_UNKNOWN; contentType = C.AUDIO_CONTENT_TYPE_UNKNOWN;
flags = 0; flags = 0;
usage = C.USAGE_MEDIA; usage = C.USAGE_MEDIA;
allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL; allowedCapturePolicy = C.ALLOW_CAPTURE_BY_ALL;
@ -97,9 +117,7 @@ public final class AudioAttributes implements Bundleable {
return this; return this;
} }
// TODO[b/190759307] Update javadoc to link to AudioAttributes.Builder#setSpatializationBehavior /** See {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)}. */
// once compile SDK target is set to 32.
/** See {@code android.media.AudioAttributes.Builder.setSpatializationBehavior(int)}. */
public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) { public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) {
this.spatializationBehavior = spatializationBehavior; this.spatializationBehavior = spatializationBehavior;
return this; return this;
@ -123,7 +141,7 @@ public final class AudioAttributes implements Bundleable {
/** The {@link C.SpatializationBehavior}. */ /** The {@link C.SpatializationBehavior}. */
public final @C.SpatializationBehavior int spatializationBehavior; public final @C.SpatializationBehavior int spatializationBehavior;
@Nullable private android.media.AudioAttributes audioAttributesV21; @Nullable private AudioAttributesV21 audioAttributesV21;
private AudioAttributes( private AudioAttributes(
@C.AudioContentType int contentType, @C.AudioContentType int contentType,
@ -139,25 +157,15 @@ public final class AudioAttributes implements Bundleable {
} }
/** /**
* Returns a {@link android.media.AudioAttributes} from this instance. * Returns a {@link AudioAttributesV21} from this instance.
* *
* <p>Field {@link AudioAttributes#allowedCapturePolicy} is ignored for API levels prior to 29. * <p>Some fields are ignored if the corresponding {@link android.media.AudioAttributes.Builder}
* setter is not available on the current API level.
*/ */
@RequiresApi(21) @RequiresApi(21)
public android.media.AudioAttributes getAudioAttributesV21() { public AudioAttributesV21 getAudioAttributesV21() {
if (audioAttributesV21 == null) { if (audioAttributesV21 == null) {
android.media.AudioAttributes.Builder builder = audioAttributesV21 = new AudioAttributesV21(this);
new android.media.AudioAttributes.Builder()
.setContentType(contentType)
.setFlags(flags)
.setUsage(usage);
if (Util.SDK_INT >= 29) {
Api29.setAllowedCapturePolicy(builder, allowedCapturePolicy);
}
if (Util.SDK_INT >= 32) {
Api32.setSpatializationBehavior(builder, spatializationBehavior);
}
audioAttributesV21 = builder.build();
} }
return audioAttributesV21; return audioAttributesV21;
} }
@ -251,8 +259,6 @@ public final class AudioAttributes implements Bundleable {
@RequiresApi(29) @RequiresApi(29)
private static final class Api29 { private static final class Api29 {
private Api29() {}
@DoNotInline @DoNotInline
public static void setAllowedCapturePolicy( public static void setAllowedCapturePolicy(
android.media.AudioAttributes.Builder builder, android.media.AudioAttributes.Builder builder,
@ -263,20 +269,11 @@ public final class AudioAttributes implements Bundleable {
@RequiresApi(32) @RequiresApi(32)
private static final class Api32 { private static final class Api32 {
private Api32() {}
@DoNotInline @DoNotInline
public static void setSpatializationBehavior( public static void setSpatializationBehavior(
android.media.AudioAttributes.Builder builder, android.media.AudioAttributes.Builder builder,
@C.SpatializationBehavior int spatializationBehavior) { @C.SpatializationBehavior int spatializationBehavior) {
try { builder.setSpatializationBehavior(spatializationBehavior);
// TODO[b/190759307]: Remove reflection once compile SDK target is set to 32.
Method setSpatializationBehavior =
builder.getClass().getMethod("setSpatializationBehavior", Integer.TYPE);
setSpatializationBehavior.invoke(builder, spatializationBehavior);
} catch (Exception e) {
// Do nothing if reflection fails.
}
} }
} }
} }

View File

@ -94,7 +94,7 @@ public abstract class BasePlayer implements Player {
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
* <p>BasePlayer and its descendents will return {@code true}. * <p>BasePlayer and its descendants will return {@code true}.
*/ */
@Override @Override
public final boolean canAdvertiseSession() { public final boolean canAdvertiseSession() {
@ -143,12 +143,18 @@ public abstract class BasePlayer implements Player {
seekToOffset(getSeekForwardIncrement()); seekToOffset(getSeekForwardIncrement());
} }
/**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final boolean hasPrevious() { public final boolean hasPrevious() {
return hasPreviousMediaItem(); return hasPreviousMediaItem();
} }
/**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final boolean hasPreviousWindow() { public final boolean hasPreviousWindow() {
@ -160,12 +166,18 @@ public abstract class BasePlayer implements Player {
return getPreviousMediaItemIndex() != C.INDEX_UNSET; return getPreviousMediaItemIndex() != C.INDEX_UNSET;
} }
/**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final void previous() { public final void previous() {
seekToPreviousMediaItem(); seekToPreviousMediaItem();
} }
/**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final void seekToPreviousWindow() { public final void seekToPreviousWindow() {
@ -198,12 +210,18 @@ public abstract class BasePlayer implements Player {
} }
} }
/**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final boolean hasNext() { public final boolean hasNext() {
return hasNextMediaItem(); return hasNextMediaItem();
} }
/**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final boolean hasNextWindow() { public final boolean hasNextWindow() {
@ -215,12 +233,18 @@ public abstract class BasePlayer implements Player {
return getNextMediaItemIndex() != C.INDEX_UNSET; return getNextMediaItemIndex() != C.INDEX_UNSET;
} }
/**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final void next() { public final void next() {
seekToNextMediaItem(); seekToNextMediaItem();
} }
/**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final void seekToNextWindow() { public final void seekToNextWindow() {
@ -253,12 +277,18 @@ public abstract class BasePlayer implements Player {
setPlaybackParameters(getPlaybackParameters().withSpeed(speed)); setPlaybackParameters(getPlaybackParameters().withSpeed(speed));
} }
/**
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final int getCurrentWindowIndex() { public final int getCurrentWindowIndex() {
return getCurrentMediaItemIndex(); return getCurrentMediaItemIndex();
} }
/**
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final int getNextWindowIndex() { public final int getNextWindowIndex() {
@ -274,6 +304,9 @@ public abstract class BasePlayer implements Player {
getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled()); getCurrentMediaItemIndex(), getRepeatModeForNavigation(), getShuffleModeEnabled());
} }
/**
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final int getPreviousWindowIndex() { public final int getPreviousWindowIndex() {
@ -326,6 +359,9 @@ public abstract class BasePlayer implements Player {
: duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100); : duration == 0 ? 100 : Util.constrainValue((int) ((position * 100) / duration), 0, 100);
} }
/**
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final boolean isCurrentWindowDynamic() { public final boolean isCurrentWindowDynamic() {
@ -338,6 +374,9 @@ public abstract class BasePlayer implements Player {
return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic; return !timeline.isEmpty() && timeline.getWindow(getCurrentMediaItemIndex(), window).isDynamic;
} }
/**
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final boolean isCurrentWindowLive() { public final boolean isCurrentWindowLive() {
@ -364,6 +403,9 @@ public abstract class BasePlayer implements Player {
return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition(); return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition();
} }
/**
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
*/
@Deprecated @Deprecated
@Override @Override
public final boolean isCurrentWindowSeekable() { public final boolean isCurrentWindowSeekable() {

View File

@ -69,6 +69,9 @@ public final class C {
/** Represents an unset or unknown rate. */ /** Represents an unset or unknown rate. */
public static final float RATE_UNSET = -Float.MAX_VALUE; public static final float RATE_UNSET = -Float.MAX_VALUE;
/** Represents an unset or unknown integer rate. */
@UnstableApi public static final int RATE_UNSET_INT = Integer.MIN_VALUE + 1;
/** Represents an unset or unknown length. */ /** Represents an unset or unknown length. */
public static final int LENGTH_UNSET = -1; public static final int LENGTH_UNSET = -1;
@ -167,11 +170,17 @@ public final class C {
@IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC}) @IntDef({CRYPTO_MODE_UNENCRYPTED, CRYPTO_MODE_AES_CTR, CRYPTO_MODE_AES_CBC})
@UnstableApi @UnstableApi
public @interface CryptoMode {} public @interface CryptoMode {}
/** @see MediaCodec#CRYPTO_MODE_UNENCRYPTED */ /**
* @see MediaCodec#CRYPTO_MODE_UNENCRYPTED
*/
@UnstableApi public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED; @UnstableApi public static final int CRYPTO_MODE_UNENCRYPTED = MediaCodec.CRYPTO_MODE_UNENCRYPTED;
/** @see MediaCodec#CRYPTO_MODE_AES_CTR */ /**
* @see MediaCodec#CRYPTO_MODE_AES_CTR
*/
@UnstableApi public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR; @UnstableApi public static final int CRYPTO_MODE_AES_CTR = MediaCodec.CRYPTO_MODE_AES_CTR;
/** @see MediaCodec#CRYPTO_MODE_AES_CBC */ /**
* @see MediaCodec#CRYPTO_MODE_AES_CBC
*/
@UnstableApi public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC; @UnstableApi public static final int CRYPTO_MODE_AES_CBC = MediaCodec.CRYPTO_MODE_AES_CBC;
/** /**
@ -240,11 +249,17 @@ public final class C {
ENCODING_PCM_FLOAT ENCODING_PCM_FLOAT
}) })
public @interface PcmEncoding {} public @interface PcmEncoding {}
/** @see AudioFormat#ENCODING_INVALID */ /**
* @see AudioFormat#ENCODING_INVALID
*/
@UnstableApi public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID; @UnstableApi public static final int ENCODING_INVALID = AudioFormat.ENCODING_INVALID;
/** @see AudioFormat#ENCODING_PCM_8BIT */ /**
* @see AudioFormat#ENCODING_PCM_8BIT
*/
@UnstableApi public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT; @UnstableApi public static final int ENCODING_PCM_8BIT = AudioFormat.ENCODING_PCM_8BIT;
/** @see AudioFormat#ENCODING_PCM_16BIT */ /**
* @see AudioFormat#ENCODING_PCM_16BIT
*/
@UnstableApi public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT; @UnstableApi public static final int ENCODING_PCM_16BIT = AudioFormat.ENCODING_PCM_16BIT;
/** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */ /** Like {@link #ENCODING_PCM_16BIT}, but with the bytes in big endian order. */
@UnstableApi public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000; @UnstableApi public static final int ENCODING_PCM_16BIT_BIG_ENDIAN = 0x10000000;
@ -252,35 +267,63 @@ public final class C {
@UnstableApi public static final int ENCODING_PCM_24BIT = 0x20000000; @UnstableApi public static final int ENCODING_PCM_24BIT = 0x20000000;
/** PCM encoding with 32 bits per sample. */ /** PCM encoding with 32 bits per sample. */
@UnstableApi public static final int ENCODING_PCM_32BIT = 0x30000000; @UnstableApi public static final int ENCODING_PCM_32BIT = 0x30000000;
/** @see AudioFormat#ENCODING_PCM_FLOAT */ /**
* @see AudioFormat#ENCODING_PCM_FLOAT
*/
@UnstableApi public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT; @UnstableApi public static final int ENCODING_PCM_FLOAT = AudioFormat.ENCODING_PCM_FLOAT;
/** @see AudioFormat#ENCODING_MP3 */ /**
* @see AudioFormat#ENCODING_MP3
*/
@UnstableApi public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3; @UnstableApi public static final int ENCODING_MP3 = AudioFormat.ENCODING_MP3;
/** @see AudioFormat#ENCODING_AAC_LC */ /**
* @see AudioFormat#ENCODING_AAC_LC
*/
@UnstableApi public static final int ENCODING_AAC_LC = AudioFormat.ENCODING_AAC_LC; @UnstableApi public static final int ENCODING_AAC_LC = AudioFormat.ENCODING_AAC_LC;
/** @see AudioFormat#ENCODING_AAC_HE_V1 */ /**
* @see AudioFormat#ENCODING_AAC_HE_V1
*/
@UnstableApi public static final int ENCODING_AAC_HE_V1 = AudioFormat.ENCODING_AAC_HE_V1; @UnstableApi public static final int ENCODING_AAC_HE_V1 = AudioFormat.ENCODING_AAC_HE_V1;
/** @see AudioFormat#ENCODING_AAC_HE_V2 */ /**
* @see AudioFormat#ENCODING_AAC_HE_V2
*/
@UnstableApi public static final int ENCODING_AAC_HE_V2 = AudioFormat.ENCODING_AAC_HE_V2; @UnstableApi public static final int ENCODING_AAC_HE_V2 = AudioFormat.ENCODING_AAC_HE_V2;
/** @see AudioFormat#ENCODING_AAC_XHE */ /**
* @see AudioFormat#ENCODING_AAC_XHE
*/
@UnstableApi public static final int ENCODING_AAC_XHE = AudioFormat.ENCODING_AAC_XHE; @UnstableApi public static final int ENCODING_AAC_XHE = AudioFormat.ENCODING_AAC_XHE;
/** @see AudioFormat#ENCODING_AAC_ELD */ /**
* @see AudioFormat#ENCODING_AAC_ELD
*/
@UnstableApi public static final int ENCODING_AAC_ELD = AudioFormat.ENCODING_AAC_ELD; @UnstableApi public static final int ENCODING_AAC_ELD = AudioFormat.ENCODING_AAC_ELD;
/** AAC Error Resilient Bit-Sliced Arithmetic Coding. */ /** AAC Error Resilient Bit-Sliced Arithmetic Coding. */
@UnstableApi public static final int ENCODING_AAC_ER_BSAC = 0x40000000; @UnstableApi public static final int ENCODING_AAC_ER_BSAC = 0x40000000;
/** @see AudioFormat#ENCODING_AC3 */ /**
* @see AudioFormat#ENCODING_AC3
*/
@UnstableApi public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3; @UnstableApi public static final int ENCODING_AC3 = AudioFormat.ENCODING_AC3;
/** @see AudioFormat#ENCODING_E_AC3 */ /**
* @see AudioFormat#ENCODING_E_AC3
*/
@UnstableApi public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3; @UnstableApi public static final int ENCODING_E_AC3 = AudioFormat.ENCODING_E_AC3;
/** @see AudioFormat#ENCODING_E_AC3_JOC */ /**
* @see AudioFormat#ENCODING_E_AC3_JOC
*/
@UnstableApi public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC; @UnstableApi public static final int ENCODING_E_AC3_JOC = AudioFormat.ENCODING_E_AC3_JOC;
/** @see AudioFormat#ENCODING_AC4 */ /**
* @see AudioFormat#ENCODING_AC4
*/
@UnstableApi public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4; @UnstableApi public static final int ENCODING_AC4 = AudioFormat.ENCODING_AC4;
/** @see AudioFormat#ENCODING_DTS */ /**
* @see AudioFormat#ENCODING_DTS
*/
@UnstableApi public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS; @UnstableApi public static final int ENCODING_DTS = AudioFormat.ENCODING_DTS;
/** @see AudioFormat#ENCODING_DTS_HD */ /**
* @see AudioFormat#ENCODING_DTS_HD
*/
@UnstableApi public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD; @UnstableApi public static final int ENCODING_DTS_HD = AudioFormat.ENCODING_DTS_HD;
/** @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;
/** Represents the behavior affecting whether spatialization will be used. */ /** Represents the behavior affecting whether spatialization will be used. */
@ -290,12 +333,16 @@ public final class C {
@IntDef({SPATIALIZATION_BEHAVIOR_AUTO, SPATIALIZATION_BEHAVIOR_NEVER}) @IntDef({SPATIALIZATION_BEHAVIOR_AUTO, SPATIALIZATION_BEHAVIOR_NEVER})
public @interface SpatializationBehavior {} public @interface SpatializationBehavior {}
// TODO[b/190759307]: Update constant values and javadoc to use SDK once compile SDK target is set /**
// to 32. * @see AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO
/** See AudioAttributes#SPATIALIZATION_BEHAVIOR_AUTO */ */
public static final int SPATIALIZATION_BEHAVIOR_AUTO = 0; public static final int SPATIALIZATION_BEHAVIOR_AUTO =
/** See AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER */ AudioAttributes.SPATIALIZATION_BEHAVIOR_AUTO;
public static final int SPATIALIZATION_BEHAVIOR_NEVER = 1; /**
* @see AudioAttributes#SPATIALIZATION_BEHAVIOR_NEVER
*/
public static final int SPATIALIZATION_BEHAVIOR_NEVER =
AudioAttributes.SPATIALIZATION_BEHAVIOR_NEVER;
/** /**
* Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link * Stream types for an {@link android.media.AudioTrack}. One of {@link #STREAM_TYPE_ALARM}, {@link
@ -321,27 +368,47 @@ public final class C {
STREAM_TYPE_DEFAULT STREAM_TYPE_DEFAULT
}) })
public @interface StreamType {} public @interface StreamType {}
/** @see AudioManager#STREAM_ALARM */ /**
* @see AudioManager#STREAM_ALARM
*/
@UnstableApi public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM; @UnstableApi public static final int STREAM_TYPE_ALARM = AudioManager.STREAM_ALARM;
/** @see AudioManager#STREAM_DTMF */ /**
* @see AudioManager#STREAM_DTMF
*/
@UnstableApi public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF; @UnstableApi public static final int STREAM_TYPE_DTMF = AudioManager.STREAM_DTMF;
/** @see AudioManager#STREAM_MUSIC */ /**
* @see AudioManager#STREAM_MUSIC
*/
@UnstableApi public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC; @UnstableApi public static final int STREAM_TYPE_MUSIC = AudioManager.STREAM_MUSIC;
/** @see AudioManager#STREAM_NOTIFICATION */ /**
* @see AudioManager#STREAM_NOTIFICATION
*/
@UnstableApi public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION; @UnstableApi public static final int STREAM_TYPE_NOTIFICATION = AudioManager.STREAM_NOTIFICATION;
/** @see AudioManager#STREAM_RING */ /**
* @see AudioManager#STREAM_RING
*/
@UnstableApi public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING; @UnstableApi public static final int STREAM_TYPE_RING = AudioManager.STREAM_RING;
/** @see AudioManager#STREAM_SYSTEM */ /**
* @see AudioManager#STREAM_SYSTEM
*/
@UnstableApi public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM; @UnstableApi public static final int STREAM_TYPE_SYSTEM = AudioManager.STREAM_SYSTEM;
/** @see AudioManager#STREAM_VOICE_CALL */ /**
* @see AudioManager#STREAM_VOICE_CALL
*/
@UnstableApi public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL; @UnstableApi public static final int STREAM_TYPE_VOICE_CALL = AudioManager.STREAM_VOICE_CALL;
/** The default stream type used by audio renderers. Equal to {@link #STREAM_TYPE_MUSIC}. */ /** The default stream type used by audio renderers. Equal to {@link #STREAM_TYPE_MUSIC}. */
@UnstableApi public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC; @UnstableApi public static final int STREAM_TYPE_DEFAULT = STREAM_TYPE_MUSIC;
/** /**
* Content types for audio attributes. One of {@link #CONTENT_TYPE_MOVIE}, {@link * Content types for audio attributes. One of:
* #CONTENT_TYPE_MUSIC}, {@link #CONTENT_TYPE_SONIFICATION}, {@link #CONTENT_TYPE_SPEECH} or *
* {@link #CONTENT_TYPE_UNKNOWN}. * <ul>
* <li>{@link #AUDIO_CONTENT_TYPE_MOVIE}
* <li>{@link #AUDIO_CONTENT_TYPE_MUSIC}
* <li>{@link #AUDIO_CONTENT_TYPE_SONIFICATION}
* <li>{@link #AUDIO_CONTENT_TYPE_SPEECH}
* <li>{@link #AUDIO_CONTENT_TYPE_UNKNOWN}
* </ul>
*/ */
// @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.
@ -349,24 +416,46 @@ public final class C {
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({ @IntDef({
CONTENT_TYPE_MOVIE, AUDIO_CONTENT_TYPE_MOVIE,
CONTENT_TYPE_MUSIC, AUDIO_CONTENT_TYPE_MUSIC,
CONTENT_TYPE_SONIFICATION, AUDIO_CONTENT_TYPE_SONIFICATION,
CONTENT_TYPE_SPEECH, AUDIO_CONTENT_TYPE_SPEECH,
CONTENT_TYPE_UNKNOWN AUDIO_CONTENT_TYPE_UNKNOWN
}) })
public @interface AudioContentType {} public @interface AudioContentType {}
/** @see android.media.AudioAttributes#CONTENT_TYPE_MOVIE */ /** See {@link AudioAttributes#CONTENT_TYPE_MOVIE}. */
public static final int CONTENT_TYPE_MOVIE = android.media.AudioAttributes.CONTENT_TYPE_MOVIE; public static final int AUDIO_CONTENT_TYPE_MOVIE = AudioAttributes.CONTENT_TYPE_MOVIE;
/** @see android.media.AudioAttributes#CONTENT_TYPE_MUSIC */ /**
public static final int CONTENT_TYPE_MUSIC = android.media.AudioAttributes.CONTENT_TYPE_MUSIC; * @deprecated Use {@link #AUDIO_CONTENT_TYPE_MOVIE} instead.
/** @see android.media.AudioAttributes#CONTENT_TYPE_SONIFICATION */ */
public static final int CONTENT_TYPE_SONIFICATION = @UnstableApi @Deprecated public static final int CONTENT_TYPE_MOVIE = AUDIO_CONTENT_TYPE_MOVIE;
android.media.AudioAttributes.CONTENT_TYPE_SONIFICATION; /** See {@link AudioAttributes#CONTENT_TYPE_MUSIC}. */
/** @see android.media.AudioAttributes#CONTENT_TYPE_SPEECH */ public static final int AUDIO_CONTENT_TYPE_MUSIC = AudioAttributes.CONTENT_TYPE_MUSIC;
public static final int CONTENT_TYPE_SPEECH = android.media.AudioAttributes.CONTENT_TYPE_SPEECH; /**
/** @see android.media.AudioAttributes#CONTENT_TYPE_UNKNOWN */ * @deprecated Use {@link #AUDIO_CONTENT_TYPE_MUSIC} instead.
public static final int CONTENT_TYPE_UNKNOWN = android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN; */
@UnstableApi @Deprecated public static final int CONTENT_TYPE_MUSIC = AUDIO_CONTENT_TYPE_MUSIC;
/** See {@link AudioAttributes#CONTENT_TYPE_SONIFICATION}. */
public static final int AUDIO_CONTENT_TYPE_SONIFICATION =
AudioAttributes.CONTENT_TYPE_SONIFICATION;
/**
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_SONIFICATION} instead.
*/
@UnstableApi @Deprecated
public static final int CONTENT_TYPE_SONIFICATION = AUDIO_CONTENT_TYPE_SONIFICATION;
/** See {@link AudioAttributes#CONTENT_TYPE_SPEECH}. */
public static final int AUDIO_CONTENT_TYPE_SPEECH = AudioAttributes.CONTENT_TYPE_SPEECH;
/**
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_SPEECH} instead.
*/
@UnstableApi @Deprecated public static final int CONTENT_TYPE_SPEECH = AUDIO_CONTENT_TYPE_SPEECH;
/** See {@link AudioAttributes#CONTENT_TYPE_UNKNOWN}. */
public static final int AUDIO_CONTENT_TYPE_UNKNOWN = AudioAttributes.CONTENT_TYPE_UNKNOWN;
/**
* @deprecated Use {@link #AUDIO_CONTENT_TYPE_UNKNOWN} instead.
*/
@UnstableApi @Deprecated
public static final int CONTENT_TYPE_UNKNOWN = AUDIO_CONTENT_TYPE_UNKNOWN;
/** /**
* Flags for audio attributes. Possible flag value is {@link #FLAG_AUDIBILITY_ENFORCED}. * Flags for audio attributes. Possible flag value is {@link #FLAG_AUDIBILITY_ENFORCED}.
@ -383,7 +472,9 @@ public final class C {
flag = true, flag = true,
value = {FLAG_AUDIBILITY_ENFORCED}) value = {FLAG_AUDIBILITY_ENFORCED})
public @interface AudioFlags {} public @interface AudioFlags {}
/** @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED */ /**
* @see android.media.AudioAttributes#FLAG_AUDIBILITY_ENFORCED
*/
public static final int FLAG_AUDIBILITY_ENFORCED = public static final int FLAG_AUDIBILITY_ENFORCED =
android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED; android.media.AudioAttributes.FLAG_AUDIBILITY_ENFORCED;
@ -421,46 +512,78 @@ public final class C {
USAGE_VOICE_COMMUNICATION_SIGNALLING USAGE_VOICE_COMMUNICATION_SIGNALLING
}) })
public @interface AudioUsage {} public @interface AudioUsage {}
/** @see android.media.AudioAttributes#USAGE_ALARM */ /**
* @see android.media.AudioAttributes#USAGE_ALARM
*/
public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM; public static final int USAGE_ALARM = android.media.AudioAttributes.USAGE_ALARM;
/** @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY */ /**
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY
*/
public static final int USAGE_ASSISTANCE_ACCESSIBILITY = public static final int USAGE_ASSISTANCE_ACCESSIBILITY =
android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY; android.media.AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY;
/** @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE */ /**
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_NAVIGATION_GUIDANCE
*/
public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = public static final int USAGE_ASSISTANCE_NAVIGATION_GUIDANCE =
android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE; android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE;
/** @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION */ /**
* @see android.media.AudioAttributes#USAGE_ASSISTANCE_SONIFICATION
*/
public static final int USAGE_ASSISTANCE_SONIFICATION = public static final int USAGE_ASSISTANCE_SONIFICATION =
android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION; android.media.AudioAttributes.USAGE_ASSISTANCE_SONIFICATION;
/** @see android.media.AudioAttributes#USAGE_ASSISTANT */ /**
* @see android.media.AudioAttributes#USAGE_ASSISTANT
*/
public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT; public static final int USAGE_ASSISTANT = android.media.AudioAttributes.USAGE_ASSISTANT;
/** @see android.media.AudioAttributes#USAGE_GAME */ /**
* @see android.media.AudioAttributes#USAGE_GAME
*/
public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME; public static final int USAGE_GAME = android.media.AudioAttributes.USAGE_GAME;
/** @see android.media.AudioAttributes#USAGE_MEDIA */ /**
* @see android.media.AudioAttributes#USAGE_MEDIA
*/
public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA; public static final int USAGE_MEDIA = android.media.AudioAttributes.USAGE_MEDIA;
/** @see android.media.AudioAttributes#USAGE_NOTIFICATION */ /**
* @see android.media.AudioAttributes#USAGE_NOTIFICATION
*/
public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION; public static final int USAGE_NOTIFICATION = android.media.AudioAttributes.USAGE_NOTIFICATION;
/** @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED */ /**
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_DELAYED
*/
public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED = public static final int USAGE_NOTIFICATION_COMMUNICATION_DELAYED =
android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED; android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED;
/** @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT */ /**
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_INSTANT
*/
public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT = public static final int USAGE_NOTIFICATION_COMMUNICATION_INSTANT =
android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT; android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT;
/** @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST */ /**
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_COMMUNICATION_REQUEST
*/
public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST = public static final int USAGE_NOTIFICATION_COMMUNICATION_REQUEST =
android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST; android.media.AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST;
/** @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT */ /**
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_EVENT
*/
public static final int USAGE_NOTIFICATION_EVENT = public static final int USAGE_NOTIFICATION_EVENT =
android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT; android.media.AudioAttributes.USAGE_NOTIFICATION_EVENT;
/** @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE */ /**
* @see android.media.AudioAttributes#USAGE_NOTIFICATION_RINGTONE
*/
public static final int USAGE_NOTIFICATION_RINGTONE = public static final int USAGE_NOTIFICATION_RINGTONE =
android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE;
/** @see android.media.AudioAttributes#USAGE_UNKNOWN */ /**
* @see android.media.AudioAttributes#USAGE_UNKNOWN
*/
public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN; public static final int USAGE_UNKNOWN = android.media.AudioAttributes.USAGE_UNKNOWN;
/** @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION */ /**
* @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION
*/
public static final int USAGE_VOICE_COMMUNICATION = public static final int USAGE_VOICE_COMMUNICATION =
android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION; android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION;
/** @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING */ /**
* @see android.media.AudioAttributes#USAGE_VOICE_COMMUNICATION_SIGNALLING
*/
public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING = public static final int USAGE_VOICE_COMMUNICATION_SIGNALLING =
android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING; android.media.AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING;
@ -484,8 +607,9 @@ public final class C {
/** /**
* Flags which can apply to a buffer containing a media sample. Possible flag values are {@link * Flags which can apply to a buffer containing a media sample. Possible flag values are {@link
* #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_LAST_SAMPLE}, * #BUFFER_FLAG_KEY_FRAME}, {@link #BUFFER_FLAG_END_OF_STREAM}, {@link #BUFFER_FLAG_FIRST_SAMPLE},
* {@link #BUFFER_FLAG_ENCRYPTED} and {@link #BUFFER_FLAG_DECODE_ONLY}. * {@link #BUFFER_FLAG_LAST_SAMPLE}, {@link #BUFFER_FLAG_ENCRYPTED} and {@link
* #BUFFER_FLAG_DECODE_ONLY}.
*/ */
@UnstableApi @UnstableApi
@Documented @Documented
@ -496,6 +620,7 @@ public final class C {
value = { value = {
BUFFER_FLAG_KEY_FRAME, BUFFER_FLAG_KEY_FRAME,
BUFFER_FLAG_END_OF_STREAM, BUFFER_FLAG_END_OF_STREAM,
BUFFER_FLAG_FIRST_SAMPLE,
BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA, BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA,
BUFFER_FLAG_LAST_SAMPLE, BUFFER_FLAG_LAST_SAMPLE,
BUFFER_FLAG_ENCRYPTED, BUFFER_FLAG_ENCRYPTED,
@ -507,6 +632,8 @@ public final class C {
/** Flag for empty buffers that signal that the end of the stream was reached. */ /** Flag for empty buffers that signal that the end of the stream was reached. */
@UnstableApi @UnstableApi
public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM; public static final int BUFFER_FLAG_END_OF_STREAM = MediaCodec.BUFFER_FLAG_END_OF_STREAM;
/** Indicates that a buffer is known to contain the first media sample of the stream. */
@UnstableApi public static final int BUFFER_FLAG_FIRST_SAMPLE = 1 << 27; // 0x08000000
/** Indicates that a buffer has supplemental data. */ /** Indicates that a buffer has supplemental data. */
@UnstableApi public static final int BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; // 0x10000000 @UnstableApi public static final int BUFFER_FLAG_HAS_SUPPLEMENTAL_DATA = 1 << 28; // 0x10000000
/** Indicates that a buffer is known to contain the last media sample of the stream. */ /** Indicates that a buffer is known to contain the last media sample of the stream. */
@ -620,30 +747,59 @@ public final class C {
public static final String LANGUAGE_UNDETERMINED = "und"; public static final String LANGUAGE_UNDETERMINED = "und";
/** /**
* Represents a streaming or other media type. One of {@link #TYPE_DASH}, {@link #TYPE_SS}, {@link * Represents a streaming or other media type. One of:
* #TYPE_HLS}, {@link #TYPE_RTSP} or {@link #TYPE_OTHER}. *
* <ul>
* <li>{@link #CONTENT_TYPE_DASH}
* <li>{@link #CONTENT_TYPE_SS}
* <li>{@link #CONTENT_TYPE_HLS}
* <li>{@link #CONTENT_TYPE_RTSP}
* <li>{@link #CONTENT_TYPE_OTHER}
* </ul>
*/ */
// @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.
@UnstableApi
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
@IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_RTSP, TYPE_OTHER}) @IntDef({
CONTENT_TYPE_DASH,
CONTENT_TYPE_SS,
CONTENT_TYPE_HLS,
CONTENT_TYPE_RTSP,
CONTENT_TYPE_OTHER
})
public @interface ContentType {} public @interface ContentType {}
/** Value returned by {@link Util#inferContentType(String)} for DASH manifests. */ /** Value representing a DASH manifest. */
@UnstableApi public static final int TYPE_DASH = 0; public static final int CONTENT_TYPE_DASH = 0;
/** Value returned by {@link Util#inferContentType(String)} for Smooth Streaming manifests. */
@UnstableApi public static final int TYPE_SS = 1;
/** Value returned by {@link Util#inferContentType(String)} for HLS manifests. */
@UnstableApi public static final int TYPE_HLS = 2;
/** Value returned by {@link Util#inferContentType(String)} for RTSP. */
@UnstableApi public static final int TYPE_RTSP = 3;
/** /**
* Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or * @deprecated Use {@link #CONTENT_TYPE_DASH} instead.
* Smooth Streaming manifests, or RTSP URIs.
*/ */
@UnstableApi public static final int TYPE_OTHER = 4; @Deprecated @UnstableApi public static final int TYPE_DASH = CONTENT_TYPE_DASH;
/** Value representing a Smooth Streaming manifest. */
public static final int CONTENT_TYPE_SS = 1;
/**
* @deprecated Use {@link #CONTENT_TYPE_SS} instead.
*/
@Deprecated @UnstableApi public static final int TYPE_SS = CONTENT_TYPE_SS;
/** Value representing an HLS manifest. */
public static final int CONTENT_TYPE_HLS = 2;
/**
* @deprecated Use {@link #CONTENT_TYPE_HLS} instead.
*/
@Deprecated @UnstableApi public static final int TYPE_HLS = CONTENT_TYPE_HLS;
/** Value representing an RTSP stream. */
public static final int CONTENT_TYPE_RTSP = 3;
/**
* @deprecated Use {@link #CONTENT_TYPE_RTSP} instead.
*/
@Deprecated @UnstableApi public static final int TYPE_RTSP = CONTENT_TYPE_RTSP;
/** Value representing files other than DASH, HLS or Smooth Streaming manifests, or RTSP URIs. */
public static final int CONTENT_TYPE_OTHER = 4;
/**
* @deprecated Use {@link #CONTENT_TYPE_OTHER} instead.
*/
@Deprecated @UnstableApi public static final int TYPE_OTHER = CONTENT_TYPE_OTHER;
/** A return value for methods where the end of an input was encountered. */ /** A return value for methods where the end of an input was encountered. */
@UnstableApi public static final int RESULT_END_OF_INPUT = -1; @UnstableApi public static final int RESULT_END_OF_INPUT = -1;
@ -898,11 +1054,17 @@ public final class C {
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020}) @IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020})
public @interface ColorSpace {} public @interface ColorSpace {}
/** @see MediaFormat#COLOR_STANDARD_BT709 */ /**
* @see MediaFormat#COLOR_STANDARD_BT709
*/
@UnstableApi public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709; @UnstableApi public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
/** @see MediaFormat#COLOR_STANDARD_BT601_PAL */ /**
* @see MediaFormat#COLOR_STANDARD_BT601_PAL
*/
@UnstableApi public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL; @UnstableApi public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;
/** @see MediaFormat#COLOR_STANDARD_BT2020 */ /**
* @see MediaFormat#COLOR_STANDARD_BT2020
*/
@UnstableApi public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020; @UnstableApi public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
/** /**
@ -915,11 +1077,17 @@ public final class C {
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG}) @IntDef({Format.NO_VALUE, COLOR_TRANSFER_SDR, COLOR_TRANSFER_ST2084, COLOR_TRANSFER_HLG})
public @interface ColorTransfer {} public @interface ColorTransfer {}
/** @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO */ /**
* @see MediaFormat#COLOR_TRANSFER_SDR_VIDEO
*/
@UnstableApi public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO; @UnstableApi public static final int COLOR_TRANSFER_SDR = MediaFormat.COLOR_TRANSFER_SDR_VIDEO;
/** @see MediaFormat#COLOR_TRANSFER_ST2084 */ /**
* @see MediaFormat#COLOR_TRANSFER_ST2084
*/
@UnstableApi public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084; @UnstableApi public static final int COLOR_TRANSFER_ST2084 = MediaFormat.COLOR_TRANSFER_ST2084;
/** @see MediaFormat#COLOR_TRANSFER_HLG */ /**
* @see MediaFormat#COLOR_TRANSFER_HLG
*/
@UnstableApi public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG; @UnstableApi public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
/** /**
@ -932,9 +1100,13 @@ public final class C {
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL}) @IntDef({Format.NO_VALUE, COLOR_RANGE_LIMITED, COLOR_RANGE_FULL})
public @interface ColorRange {} public @interface ColorRange {}
/** @see MediaFormat#COLOR_RANGE_LIMITED */ /**
* @see MediaFormat#COLOR_RANGE_LIMITED
*/
@UnstableApi public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED; @UnstableApi public static final int COLOR_RANGE_LIMITED = MediaFormat.COLOR_RANGE_LIMITED;
/** @see MediaFormat#COLOR_RANGE_FULL */ /**
* @see MediaFormat#COLOR_RANGE_FULL
*/
@UnstableApi public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL; @UnstableApi public static final int COLOR_RANGE_FULL = MediaFormat.COLOR_RANGE_FULL;
/** Video projection types. */ /** Video projection types. */
@ -1200,7 +1372,9 @@ public final class C {
*/ */
@UnstableApi public static final int FORMAT_UNSUPPORTED_TYPE = 0b000; @UnstableApi public static final int FORMAT_UNSUPPORTED_TYPE = 0b000;
/** @deprecated Use {@link Util#usToMs(long)}. */ /**
* @deprecated Use {@link Util#usToMs(long)}.
*/
@UnstableApi @UnstableApi
@InlineMe( @InlineMe(
replacement = "Util.usToMs(timeUs)", replacement = "Util.usToMs(timeUs)",
@ -1210,7 +1384,9 @@ public final class C {
return Util.usToMs(timeUs); return Util.usToMs(timeUs);
} }
/** @deprecated Use {@link Util#msToUs(long)}. */ /**
* @deprecated Use {@link Util#msToUs(long)}.
*/
@UnstableApi @UnstableApi
@InlineMe( @InlineMe(
replacement = "Util.msToUs(timeMs)", replacement = "Util.msToUs(timeMs)",
@ -1220,7 +1396,9 @@ public final class C {
return Util.msToUs(timeMs); return Util.msToUs(timeMs);
} }
/** @deprecated Use {@link Util#generateAudioSessionIdV21(Context)}. */ /**
* @deprecated Use {@link Util#generateAudioSessionIdV21(Context)}.
*/
@UnstableApi @UnstableApi
@InlineMe( @InlineMe(
replacement = "Util.generateAudioSessionIdV21(context)", replacement = "Util.generateAudioSessionIdV21(context)",
@ -1231,7 +1409,9 @@ public final class C {
return Util.generateAudioSessionIdV21(context); return Util.generateAudioSessionIdV21(context);
} }
/** @deprecated Use {@link Util#getFormatSupportString(int)}. */ /**
* @deprecated Use {@link Util#getFormatSupportString(int)}.
*/
@UnstableApi @UnstableApi
@InlineMe( @InlineMe(
replacement = "Util.getFormatSupportString(formatSupport)", replacement = "Util.getFormatSupportString(formatSupport)",
@ -1241,7 +1421,9 @@ public final class C {
return Util.getFormatSupportString(formatSupport); return Util.getFormatSupportString(formatSupport);
} }
/** @deprecated Use {@link Util#getErrorCodeForMediaDrmErrorCode(int)}. */ /**
* @deprecated Use {@link Util#getErrorCodeForMediaDrmErrorCode(int)}.
*/
@UnstableApi @UnstableApi
@InlineMe( @InlineMe(
replacement = "Util.getErrorCodeForMediaDrmErrorCode(mediaDrmErrorCode)", replacement = "Util.getErrorCodeForMediaDrmErrorCode(mediaDrmErrorCode)",

View File

@ -92,7 +92,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
/** Number of {@link SchemeData}s. */ /** Number of {@link SchemeData}s. */
public final int schemeDataCount; public final int schemeDataCount;
/** @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ /**
* @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
*/
public DrmInitData(List<SchemeData> schemeDatas) { public DrmInitData(List<SchemeData> schemeDatas) {
this(null, false, schemeDatas.toArray(new SchemeData[0])); this(null, false, schemeDatas.toArray(new SchemeData[0]));
} }
@ -105,7 +107,9 @@ public final class DrmInitData implements Comparator<SchemeData>, Parcelable {
this(schemeType, false, schemeDatas.toArray(new SchemeData[0])); this(schemeType, false, schemeDatas.toArray(new SchemeData[0]));
} }
/** @param schemeDatas Scheme initialization data for possibly multiple DRM schemes. */ /**
* @param schemeDatas Scheme initialization data for possibly multiple DRM schemes.
*/
public DrmInitData(SchemeData... schemeDatas) { public DrmInitData(SchemeData... schemeDatas) {
this(null, schemeDatas); this(null, schemeDatas);
} }

View File

@ -16,10 +16,8 @@
package androidx.media3.common; package androidx.media3.common;
import android.util.Pair; import android.util.Pair;
import androidx.media3.common.util.UnstableApi;
/** Converts throwables into error codes and user readable error messages. */ /** Converts throwables into error codes and user readable error messages. */
@UnstableApi
public interface ErrorMessageProvider<T extends Throwable> { public interface ErrorMessageProvider<T extends Throwable> {
/** /**

View File

@ -37,13 +37,14 @@ public final class FileTypes {
/** /**
* File types. One of {@link #UNKNOWN}, {@link #AC3}, {@link #AC4}, {@link #ADTS}, {@link #AMR}, * File types. One of {@link #UNKNOWN}, {@link #AC3}, {@link #AC4}, {@link #ADTS}, {@link #AMR},
* {@link #FLAC}, {@link #FLV}, {@link #MATROSKA}, {@link #MP3}, {@link #MP4}, {@link #OGG}, * {@link #FLAC}, {@link #FLV}, {@link #MATROSKA}, {@link #MP3}, {@link #MP4}, {@link #OGG},
* {@link #PS}, {@link #TS}, {@link #WAV}, {@link #WEBVTT} and {@link #JPEG}. * {@link #PS}, {@link #TS}, {@link #WAV}, {@link #WEBVTT}, {@link #JPEG} and {@link #MIDI}.
*/ */
@Documented @Documented
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({ @IntDef({
UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG UNKNOWN, AC3, AC4, ADTS, AMR, FLAC, FLV, MATROSKA, MP3, MP4, OGG, PS, TS, WAV, WEBVTT, JPEG,
MIDI, AVI
}) })
public @interface Type {} public @interface Type {}
/** Unknown file type. */ /** Unknown file type. */
@ -78,6 +79,10 @@ public final class FileTypes {
public static final int WEBVTT = 13; public static final int WEBVTT = 13;
/** File type for the JPEG format. */ /** File type for the JPEG format. */
public static final int JPEG = 14; public static final int JPEG = 14;
/** File type for the MIDI format. */
public static final int MIDI = 15;
/** File type for the AVI format. */
public static final int AVI = 16;
@VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type"; @VisibleForTesting /* package */ static final String HEADER_CONTENT_TYPE = "Content-Type";
@ -89,6 +94,9 @@ public final class FileTypes {
private static final String EXTENSION_AMR = ".amr"; private static final String EXTENSION_AMR = ".amr";
private static final String EXTENSION_FLAC = ".flac"; private static final String EXTENSION_FLAC = ".flac";
private static final String EXTENSION_FLV = ".flv"; private static final String EXTENSION_FLV = ".flv";
private static final String EXTENSION_MID = ".mid";
private static final String EXTENSION_MIDI = ".midi";
private static final String EXTENSION_SMF = ".smf";
private static final String EXTENSION_PREFIX_MK = ".mk"; private static final String EXTENSION_PREFIX_MK = ".mk";
private static final String EXTENSION_WEBM = ".webm"; private static final String EXTENSION_WEBM = ".webm";
private static final String EXTENSION_PREFIX_OG = ".og"; private static final String EXTENSION_PREFIX_OG = ".og";
@ -110,6 +118,7 @@ public final class FileTypes {
private static final String EXTENSION_WEBVTT = ".webvtt"; private static final String EXTENSION_WEBVTT = ".webvtt";
private static final String EXTENSION_JPG = ".jpg"; private static final String EXTENSION_JPG = ".jpg";
private static final String EXTENSION_JPEG = ".jpeg"; private static final String EXTENSION_JPEG = ".jpeg";
private static final String EXTENSION_AVI = ".avi";
private FileTypes() {} private FileTypes() {}
@ -147,6 +156,8 @@ public final class FileTypes {
return FileTypes.FLAC; return FileTypes.FLAC;
case MimeTypes.VIDEO_FLV: case MimeTypes.VIDEO_FLV:
return FileTypes.FLV; return FileTypes.FLV;
case MimeTypes.AUDIO_MIDI:
return FileTypes.MIDI;
case MimeTypes.VIDEO_MATROSKA: case MimeTypes.VIDEO_MATROSKA:
case MimeTypes.AUDIO_MATROSKA: case MimeTypes.AUDIO_MATROSKA:
case MimeTypes.VIDEO_WEBM: case MimeTypes.VIDEO_WEBM:
@ -171,6 +182,8 @@ public final class FileTypes {
return FileTypes.WEBVTT; return FileTypes.WEBVTT;
case MimeTypes.IMAGE_JPEG: case MimeTypes.IMAGE_JPEG:
return FileTypes.JPEG; return FileTypes.JPEG;
case MimeTypes.VIDEO_AVI:
return FileTypes.AVI;
default: default:
return FileTypes.UNKNOWN; return FileTypes.UNKNOWN;
} }
@ -193,6 +206,10 @@ public final class FileTypes {
return FileTypes.FLAC; return FileTypes.FLAC;
} else if (filename.endsWith(EXTENSION_FLV)) { } else if (filename.endsWith(EXTENSION_FLV)) {
return FileTypes.FLV; return FileTypes.FLV;
} else if (filename.endsWith(EXTENSION_MID)
|| filename.endsWith(EXTENSION_MIDI)
|| filename.endsWith(EXTENSION_SMF)) {
return FileTypes.MIDI;
} else if (filename.startsWith( } else if (filename.startsWith(
EXTENSION_PREFIX_MK, EXTENSION_PREFIX_MK,
/* toffset= */ filename.length() - (EXTENSION_PREFIX_MK.length() + 1)) /* toffset= */ filename.length() - (EXTENSION_PREFIX_MK.length() + 1))
@ -232,6 +249,8 @@ public final class FileTypes {
return FileTypes.WEBVTT; return FileTypes.WEBVTT;
} else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) { } else if (filename.endsWith(EXTENSION_JPG) || filename.endsWith(EXTENSION_JPEG)) {
return FileTypes.JPEG; return FileTypes.JPEG;
} else if (filename.endsWith(EXTENSION_AVI)) {
return FileTypes.AVI;
} else { } else {
return FileTypes.UNKNOWN; return FileTypes.UNKNOWN;
} }

View File

@ -769,7 +769,9 @@ public final class Format implements Bundleable {
// Video. // Video.
/** @deprecated Use {@link Format.Builder}. */ /**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public static Format createVideoSampleFormat( public static Format createVideoSampleFormat(
@ -798,7 +800,9 @@ public final class Format implements Bundleable {
.build(); .build();
} }
/** @deprecated Use {@link Format.Builder}. */ /**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public static Format createVideoSampleFormat( public static Format createVideoSampleFormat(
@ -833,7 +837,9 @@ public final class Format implements Bundleable {
// Audio. // Audio.
/** @deprecated Use {@link Format.Builder}. */ /**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public static Format createAudioSampleFormat( public static Format createAudioSampleFormat(
@ -864,7 +870,9 @@ public final class Format implements Bundleable {
.build(); .build();
} }
/** @deprecated Use {@link Format.Builder}. */ /**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public static Format createAudioSampleFormat( public static Format createAudioSampleFormat(
@ -899,7 +907,9 @@ public final class Format implements Bundleable {
// Generic. // Generic.
/** @deprecated Use {@link Format.Builder}. */ /**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public static Format createContainerFormat( public static Format createContainerFormat(
@ -926,7 +936,9 @@ public final class Format implements Bundleable {
.build(); .build();
} }
/** @deprecated Use {@link Format.Builder}. */ /**
* @deprecated Use {@link Format.Builder}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public static Format createSampleFormat(@Nullable String id, @Nullable String sampleMimeType) { public static Format createSampleFormat(@Nullable String id, @Nullable String sampleMimeType) {
@ -986,28 +998,36 @@ public final class Format implements Bundleable {
return new Builder(this); return new Builder(this);
} }
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setMaxInputSize(int)}. */ /**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setMaxInputSize(int)}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Format copyWithMaxInputSize(int maxInputSize) { public Format copyWithMaxInputSize(int maxInputSize) {
return buildUpon().setMaxInputSize(maxInputSize).build(); return buildUpon().setMaxInputSize(maxInputSize).build();
} }
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setSubsampleOffsetUs(long)}. */ /**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setSubsampleOffsetUs(long)}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) { public Format copyWithSubsampleOffsetUs(long subsampleOffsetUs) {
return buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build(); return buildUpon().setSubsampleOffsetUs(subsampleOffsetUs).build();
} }
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setLabel(String)} . */ /**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setLabel(String)} .
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Format copyWithLabel(@Nullable String label) { public Format copyWithLabel(@Nullable String label) {
return buildUpon().setLabel(label).build(); return buildUpon().setLabel(label).build();
} }
/** @deprecated Use {@link #withManifestFormatInfo(Format)}. */ /**
* @deprecated Use {@link #withManifestFormatInfo(Format)}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Format copyWithManifestFormatInfo(Format manifestFormat) { public Format copyWithManifestFormatInfo(Format manifestFormat) {
@ -1092,21 +1112,27 @@ public final class Format implements Bundleable {
return buildUpon().setEncoderDelay(encoderDelay).setEncoderPadding(encoderPadding).build(); return buildUpon().setEncoderDelay(encoderDelay).setEncoderPadding(encoderPadding).build();
} }
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setFrameRate(float)}. */ /**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setFrameRate(float)}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Format copyWithFrameRate(float frameRate) { public Format copyWithFrameRate(float frameRate) {
return buildUpon().setFrameRate(frameRate).build(); return buildUpon().setFrameRate(frameRate).build();
} }
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setDrmInitData(DrmInitData)}. */ /**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setDrmInitData(DrmInitData)}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) { public Format copyWithDrmInitData(@Nullable DrmInitData drmInitData) {
return buildUpon().setDrmInitData(drmInitData).build(); return buildUpon().setDrmInitData(drmInitData).build();
} }
/** @deprecated Use {@link #buildUpon()} and {@link Builder#setMetadata(Metadata)}. */ /**
* @deprecated Use {@link #buildUpon()} and {@link Builder#setMetadata(Metadata)}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Format copyWithMetadata(@Nullable Metadata metadata) { public Format copyWithMetadata(@Nullable Metadata metadata) {
@ -1522,7 +1548,9 @@ public final class Format implements Bundleable {
bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio); bundle.putFloat(keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), pixelWidthHeightRatio);
bundle.putByteArray(keyForField(FIELD_PROJECTION_DATA), projectionData); bundle.putByteArray(keyForField(FIELD_PROJECTION_DATA), projectionData);
bundle.putInt(keyForField(FIELD_STEREO_MODE), stereoMode); bundle.putInt(keyForField(FIELD_STEREO_MODE), stereoMode);
bundle.putBundle(keyForField(FIELD_COLOR_INFO), BundleableUtil.toNullableBundle(colorInfo)); if (colorInfo != null) {
bundle.putBundle(keyForField(FIELD_COLOR_INFO), colorInfo.toBundle());
}
// Audio specific. // Audio specific.
bundle.putInt(keyForField(FIELD_CHANNEL_COUNT), channelCount); bundle.putInt(keyForField(FIELD_CHANNEL_COUNT), channelCount);
bundle.putInt(keyForField(FIELD_SAMPLE_RATE), sampleRate); bundle.putInt(keyForField(FIELD_SAMPLE_RATE), sampleRate);
@ -1589,11 +1617,13 @@ public final class Format implements Bundleable {
bundle.getFloat( bundle.getFloat(
keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT.pixelWidthHeightRatio)) keyForField(FIELD_PIXEL_WIDTH_HEIGHT_RATIO), DEFAULT.pixelWidthHeightRatio))
.setProjectionData(bundle.getByteArray(keyForField(FIELD_PROJECTION_DATA))) .setProjectionData(bundle.getByteArray(keyForField(FIELD_PROJECTION_DATA)))
.setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode)) .setStereoMode(bundle.getInt(keyForField(FIELD_STEREO_MODE), DEFAULT.stereoMode));
.setColorInfo( Bundle colorInfoBundle = bundle.getBundle(keyForField(FIELD_COLOR_INFO));
BundleableUtil.fromNullableBundle( if (colorInfoBundle != null) {
ColorInfo.CREATOR, bundle.getBundle(keyForField(FIELD_COLOR_INFO)))) builder.setColorInfo(ColorInfo.CREATOR.fromBundle(colorInfoBundle));
}
// Audio specific. // Audio specific.
builder
.setChannelCount(bundle.getInt(keyForField(FIELD_CHANNEL_COUNT), DEFAULT.channelCount)) .setChannelCount(bundle.getInt(keyForField(FIELD_CHANNEL_COUNT), DEFAULT.channelCount))
.setSampleRate(bundle.getInt(keyForField(FIELD_SAMPLE_RATE), DEFAULT.sampleRate)) .setSampleRate(bundle.getInt(keyForField(FIELD_SAMPLE_RATE), DEFAULT.sampleRate))
.setPcmEncoding(bundle.getInt(keyForField(FIELD_PCM_ENCODING), DEFAULT.pcmEncoding)) .setPcmEncoding(bundle.getInt(keyForField(FIELD_PCM_ENCODING), DEFAULT.pcmEncoding))

View File

@ -22,6 +22,7 @@ import android.view.SurfaceView;
import android.view.TextureView; import android.view.TextureView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.text.Cue; import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import java.util.List; import java.util.List;
@ -298,7 +299,11 @@ public class ForwardingPlayer implements Player {
player.seekForward(); player.seekForward();
} }
/** Calls {@link Player#hasPrevious()} on the delegate and returns the result. */ /**
* Calls {@link Player#hasPrevious()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -306,7 +311,11 @@ public class ForwardingPlayer implements Player {
return player.hasPrevious(); return player.hasPrevious();
} }
/** Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result. */ /**
* Calls {@link Player#hasPreviousWindow()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -320,7 +329,11 @@ public class ForwardingPlayer implements Player {
return player.hasPreviousMediaItem(); return player.hasPreviousMediaItem();
} }
/** Calls {@link Player#previous()} on the delegate. */ /**
* Calls {@link Player#previous()} on the delegate.
*
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -328,7 +341,11 @@ public class ForwardingPlayer implements Player {
player.previous(); player.previous();
} }
/** Calls {@link Player#seekToPreviousWindow()} on the delegate. */ /**
* Calls {@link Player#seekToPreviousWindow()} on the delegate.
*
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -354,7 +371,11 @@ public class ForwardingPlayer implements Player {
return player.getMaxSeekToPreviousPosition(); return player.getMaxSeekToPreviousPosition();
} }
/** Calls {@link Player#hasNext()} on the delegate and returns the result. */ /**
* Calls {@link Player#hasNext()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -362,7 +383,11 @@ public class ForwardingPlayer implements Player {
return player.hasNext(); return player.hasNext();
} }
/** Calls {@link Player#hasNextWindow()} on the delegate and returns the result. */ /**
* Calls {@link Player#hasNextWindow()} on the delegate and returns the result.
*
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -376,7 +401,11 @@ public class ForwardingPlayer implements Player {
return player.hasNextMediaItem(); return player.hasNextMediaItem();
} }
/** Calls {@link Player#next()} on the delegate. */ /**
* Calls {@link Player#next()} on the delegate.
*
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -384,7 +413,11 @@ public class ForwardingPlayer implements Player {
player.next(); player.next();
} }
/** Calls {@link Player#seekToNextWindow()} on the delegate. */ /**
* Calls {@link Player#seekToNextWindow()} on the delegate.
*
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -428,7 +461,13 @@ public class ForwardingPlayer implements Player {
player.stop(); player.stop();
} }
/** Calls {@link Player#stop(boolean)} on the delegate. */ /**
* Calls {@link Player#stop(boolean)} on the delegate.
*
* @deprecated Use {@link #stop()} and {@link #clearMediaItems()} (if {@code reset} is true) or
* just {@link #stop()} (if {@code reset} is false). Any player error will be cleared when
* {@link #prepare() re-preparing} the player.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -442,26 +481,10 @@ public class ForwardingPlayer implements Player {
player.release(); player.release();
} }
/** Calls {@link Player#getCurrentTrackGroups()} on the delegate and returns the result. */ /** Calls {@link Player#getCurrentTracks()} on the delegate and returns the result. */
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override @Override
public TrackGroupArray getCurrentTrackGroups() { public Tracks getCurrentTracks() {
return player.getCurrentTrackGroups(); return player.getCurrentTracks();
}
/** Calls {@link Player#getCurrentTrackSelections()} on the delegate and returns the result. */
@SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated
@Override
public TrackSelectionArray getCurrentTrackSelections() {
return player.getCurrentTrackSelections();
}
/** Calls {@link Player#getCurrentTracksInfo()} on the delegate and returns the result. */
@Override
public TracksInfo getCurrentTracksInfo() {
return player.getCurrentTracksInfo();
} }
/** Calls {@link Player#getTrackSelectionParameters()} on the delegate and returns the result. */ /** Calls {@link Player#getTrackSelectionParameters()} on the delegate and returns the result. */
@ -513,7 +536,11 @@ public class ForwardingPlayer implements Player {
return player.getCurrentPeriodIndex(); return player.getCurrentPeriodIndex();
} }
/** Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result. */ /**
* Calls {@link Player#getCurrentWindowIndex()} on the delegate and returns the result.
*
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -527,7 +554,11 @@ public class ForwardingPlayer implements Player {
return player.getCurrentMediaItemIndex(); return player.getCurrentMediaItemIndex();
} }
/** Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result. */ /**
* Calls {@link Player#getNextWindowIndex()} on the delegate and returns the result.
*
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -541,7 +572,11 @@ public class ForwardingPlayer implements Player {
return player.getNextMediaItemIndex(); return player.getNextMediaItemIndex();
} }
/** Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result. */ /**
* Calls {@link Player#getPreviousWindowIndex()} on the delegate and returns the result.
*
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -604,7 +639,11 @@ public class ForwardingPlayer implements Player {
return player.getTotalBufferedDuration(); return player.getTotalBufferedDuration();
} }
/** Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result. */ /**
* Calls {@link Player#isCurrentWindowDynamic()} on the delegate and returns the result.
*
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -618,7 +657,11 @@ public class ForwardingPlayer implements Player {
return player.isCurrentMediaItemDynamic(); return player.isCurrentMediaItemDynamic();
} }
/** Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result. */ /**
* Calls {@link Player#isCurrentWindowLive()} on the delegate and returns the result.
*
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -638,7 +681,11 @@ public class ForwardingPlayer implements Player {
return player.getCurrentLiveOffset(); return player.getCurrentLiveOffset();
} }
/** Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result. */ /**
* Calls {@link Player#isCurrentWindowSeekable()} on the delegate and returns the result.
*
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
*/
@SuppressWarnings("deprecation") // Forwarding to deprecated method @SuppressWarnings("deprecation") // Forwarding to deprecated method
@Deprecated @Deprecated
@Override @Override
@ -768,7 +815,7 @@ public class ForwardingPlayer implements Player {
/** Calls {@link Player#getCurrentCues()} on the delegate and returns the result. */ /** Calls {@link Player#getCurrentCues()} on the delegate and returns the result. */
@Override @Override
public List<Cue> getCurrentCues() { public CueGroup getCurrentCues() {
return player.getCurrentCues(); return player.getCurrentCues();
} }
@ -847,14 +894,8 @@ public class ForwardingPlayer implements Player {
} }
@Override @Override
@SuppressWarnings("deprecation") public void onTracksChanged(Tracks tracks) {
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) { listener.onTracksChanged(tracks);
listener.onTracksChanged(trackGroups, trackSelections);
}
@Override
public void onTracksInfoChanged(TracksInfo tracksInfo) {
listener.onTracksInfoChanged(tracksInfo);
} }
@Override @Override
@ -1014,6 +1055,11 @@ public class ForwardingPlayer implements Player {
listener.onCues(cues); listener.onCues(cues);
} }
@Override
public void onCues(CueGroup cueGroup) {
listener.onCues(cueGroup);
}
@Override @Override
public void onMetadata(Metadata metadata) { public void onMetadata(Metadata metadata) {
listener.onMetadata(metadata); listener.onMetadata(metadata);

View File

@ -29,6 +29,7 @@ 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.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.InlineMe;
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;
@ -84,6 +85,7 @@ public final class MediaItem implements Bundleable {
// TODO: Change this to LiveConfiguration once all the deprecated individual setters // TODO: Change this to LiveConfiguration once all the deprecated individual setters
// are removed. // are removed.
private LiveConfiguration.Builder liveConfiguration; private LiveConfiguration.Builder liveConfiguration;
private RequestMetadata requestMetadata;
/** Creates a builder. */ /** Creates a builder. */
@SuppressWarnings("deprecation") // Temporarily uses DrmConfiguration.Builder() constructor. @SuppressWarnings("deprecation") // Temporarily uses DrmConfiguration.Builder() constructor.
@ -93,6 +95,7 @@ public final class MediaItem implements Bundleable {
streamKeys = Collections.emptyList(); streamKeys = Collections.emptyList();
subtitleConfigurations = ImmutableList.of(); subtitleConfigurations = ImmutableList.of();
liveConfiguration = new LiveConfiguration.Builder(); liveConfiguration = new LiveConfiguration.Builder();
requestMetadata = RequestMetadata.EMPTY;
} }
private Builder(MediaItem mediaItem) { private Builder(MediaItem mediaItem) {
@ -101,6 +104,7 @@ public final class MediaItem implements Bundleable {
mediaId = mediaItem.mediaId; mediaId = mediaItem.mediaId;
mediaMetadata = mediaItem.mediaMetadata; mediaMetadata = mediaItem.mediaMetadata;
liveConfiguration = mediaItem.liveConfiguration.buildUpon(); liveConfiguration = mediaItem.liveConfiguration.buildUpon();
requestMetadata = mediaItem.requestMetadata;
@Nullable LocalConfiguration localConfiguration = mediaItem.localConfiguration; @Nullable LocalConfiguration localConfiguration = mediaItem.localConfiguration;
if (localConfiguration != null) { if (localConfiguration != null) {
customCacheKey = localConfiguration.customCacheKey; customCacheKey = localConfiguration.customCacheKey;
@ -315,12 +319,12 @@ public final class MediaItem implements Bundleable {
/** /**
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link * @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
* DrmConfiguration.Builder#forceSessionsForAudioAndVideoTracks(boolean)} instead. * DrmConfiguration.Builder#setForceSessionsForAudioAndVideoTracks(boolean)} instead.
*/ */
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Builder setDrmSessionForClearPeriods(boolean sessionForClearPeriods) { public Builder setDrmSessionForClearPeriods(boolean sessionForClearPeriods) {
drmConfiguration.forceSessionsForAudioAndVideoTracks(sessionForClearPeriods); drmConfiguration.setForceSessionsForAudioAndVideoTracks(sessionForClearPeriods);
return this; return this;
} }
@ -525,6 +529,12 @@ public final class MediaItem implements Bundleable {
return this; return this;
} }
/** Sets the request metadata. */
public Builder setRequestMetadata(RequestMetadata requestMetadata) {
this.requestMetadata = requestMetadata;
return this;
}
/** Returns a new {@link MediaItem} instance with the current builder values. */ /** Returns a new {@link MediaItem} instance with the current builder values. */
@SuppressWarnings("deprecation") // Using PlaybackProperties while it exists. @SuppressWarnings("deprecation") // Using PlaybackProperties while it exists.
public MediaItem build() { public MediaItem build() {
@ -549,7 +559,8 @@ public final class MediaItem implements Bundleable {
clippingConfiguration.buildClippingProperties(), clippingConfiguration.buildClippingProperties(),
localConfiguration, localConfiguration,
liveConfiguration.build(), liveConfiguration.build(),
mediaMetadata != null ? mediaMetadata : MediaMetadata.EMPTY); mediaMetadata != null ? mediaMetadata : MediaMetadata.EMPTY,
requestMetadata);
} }
} }
@ -659,6 +670,19 @@ public final class MediaItem implements Bundleable {
return this; return this;
} }
/**
* @deprecated Use {@link #setForceSessionsForAudioAndVideoTracks(boolean)} instead.
*/
@UnstableApi
@Deprecated
@InlineMe(
replacement =
"this.setForceSessionsForAudioAndVideoTracks(forceSessionsForAudioAndVideoTracks)")
public Builder forceSessionsForAudioAndVideoTracks(
boolean forceSessionsForAudioAndVideoTracks) {
return setForceSessionsForAudioAndVideoTracks(forceSessionsForAudioAndVideoTracks);
}
/** /**
* Sets whether a DRM session should be used for clear tracks of type {@link * Sets whether a DRM session should be used for clear tracks of type {@link
* C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_AUDIO}. * C#TRACK_TYPE_VIDEO} and {@link C#TRACK_TYPE_AUDIO}.
@ -666,10 +690,10 @@ public final class MediaItem implements Bundleable {
* <p>This method overrides what has been set by previously calling {@link * <p>This method overrides what has been set by previously calling {@link
* #setForcedSessionTrackTypes(List)}. * #setForcedSessionTrackTypes(List)}.
*/ */
public Builder forceSessionsForAudioAndVideoTracks( public Builder setForceSessionsForAudioAndVideoTracks(
boolean useClearSessionsForAudioAndVideoTracks) { boolean forceSessionsForAudioAndVideoTracks) {
this.setForcedSessionTrackTypes( this.setForcedSessionTrackTypes(
useClearSessionsForAudioAndVideoTracks forceSessionsForAudioAndVideoTracks
? ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO) ? ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO)
: ImmutableList.of()); : ImmutableList.of());
return this; return this;
@ -680,10 +704,10 @@ public final class MediaItem implements Bundleable {
* when the tracks are in the clear. * when the tracks are in the clear.
* *
* <p>For the common case of using a DRM session for {@link C#TRACK_TYPE_VIDEO} and {@link * <p>For the common case of using a DRM session for {@link C#TRACK_TYPE_VIDEO} and {@link
* C#TRACK_TYPE_AUDIO}, {@link #forceSessionsForAudioAndVideoTracks(boolean)} can be used. * C#TRACK_TYPE_AUDIO}, {@link #setForceSessionsForAudioAndVideoTracks(boolean)} can be used.
* *
* <p>This method overrides what has been set by previously calling {@link * <p>This method overrides what has been set by previously calling {@link
* #forceSessionsForAudioAndVideoTracks(boolean)}. * #setForceSessionsForAudioAndVideoTracks(boolean)}.
*/ */
public Builder setForcedSessionTrackTypes( public Builder setForcedSessionTrackTypes(
List<@C.TrackType Integer> forcedSessionTrackTypes) { List<@C.TrackType Integer> forcedSessionTrackTypes) {
@ -712,7 +736,9 @@ public final class MediaItem implements Bundleable {
/** The UUID of the protection scheme. */ /** The UUID of the protection scheme. */
public final UUID scheme; public final UUID scheme;
/** @deprecated Use {@link #scheme} instead. */ /**
* @deprecated Use {@link #scheme} instead.
*/
@UnstableApi @Deprecated public final UUID uuid; @UnstableApi @Deprecated public final UUID uuid;
/** /**
@ -721,7 +747,9 @@ public final class MediaItem implements Bundleable {
*/ */
@Nullable public final Uri licenseUri; @Nullable public final Uri licenseUri;
/** @deprecated Use {@link #licenseRequestHeaders} instead. */ /**
* @deprecated Use {@link #licenseRequestHeaders} instead.
*/
@UnstableApi @Deprecated public final ImmutableMap<String, String> requestHeaders; @UnstableApi @Deprecated public final ImmutableMap<String, String> requestHeaders;
/** The headers to attach to requests sent to the DRM license server. */ /** The headers to attach to requests sent to the DRM license server. */
@ -742,7 +770,9 @@ public final class MediaItem implements Bundleable {
*/ */
public final boolean forceDefaultLicenseUri; public final boolean forceDefaultLicenseUri;
/** @deprecated Use {@link #forcedSessionTrackTypes}. */ /**
* @deprecated Use {@link #forcedSessionTrackTypes}.
*/
@UnstableApi @Deprecated public final ImmutableList<@C.TrackType Integer> sessionForClearTypes; @UnstableApi @Deprecated public final ImmutableList<@C.TrackType Integer> sessionForClearTypes;
/** /**
* The types of tracks for which to always use a DRM session even if the content is unencrypted. * The types of tracks for which to always use a DRM session even if the content is unencrypted.
@ -929,7 +959,9 @@ public final class MediaItem implements Bundleable {
/** Optional subtitles to be sideloaded. */ /** Optional subtitles to be sideloaded. */
public final ImmutableList<SubtitleConfiguration> subtitleConfigurations; public final ImmutableList<SubtitleConfiguration> subtitleConfigurations;
/** @deprecated Use {@link #subtitleConfigurations} instead. */ /**
* @deprecated Use {@link #subtitleConfigurations} instead.
*/
@UnstableApi @Deprecated public final List<Subtitle> subtitles; @UnstableApi @Deprecated public final List<Subtitle> subtitles;
/** /**
@ -998,7 +1030,9 @@ public final class MediaItem implements Bundleable {
} }
} }
/** @deprecated Use {@link LocalConfiguration}. */ /**
* @deprecated Use {@link LocalConfiguration}.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public static final class PlaybackProperties extends LocalConfiguration { public static final class PlaybackProperties extends LocalConfiguration {
@ -1160,7 +1194,9 @@ public final class MediaItem implements Bundleable {
builder.maxPlaybackSpeed); builder.maxPlaybackSpeed);
} }
/** @deprecated Use {@link Builder} instead. */ /**
* @deprecated Use {@link Builder} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public LiveConfiguration( public LiveConfiguration(
@ -1299,7 +1335,7 @@ public final class MediaItem implements Bundleable {
} }
/** Sets the MIME type. */ /** Sets the MIME type. */
public Builder setMimeType(String mimeType) { public Builder setMimeType(@Nullable String mimeType) {
this.mimeType = mimeType; this.mimeType = mimeType;
return this; return this;
} }
@ -1427,19 +1463,25 @@ public final class MediaItem implements Bundleable {
} }
} }
/** @deprecated Use {@link MediaItem.SubtitleConfiguration} instead */ /**
* @deprecated Use {@link MediaItem.SubtitleConfiguration} instead
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public static final class Subtitle extends SubtitleConfiguration { public static final class Subtitle extends SubtitleConfiguration {
/** @deprecated Use {@link Builder} instead. */ /**
* @deprecated Use {@link Builder} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Subtitle(Uri uri, String mimeType, @Nullable String language) { public Subtitle(Uri uri, String mimeType, @Nullable String language) {
this(uri, mimeType, language, /* selectionFlags= */ 0); this(uri, mimeType, language, /* selectionFlags= */ 0);
} }
/** @deprecated Use {@link Builder} instead. */ /**
* @deprecated Use {@link Builder} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Subtitle( public Subtitle(
@ -1447,7 +1489,9 @@ public final class MediaItem implements Bundleable {
this(uri, mimeType, language, selectionFlags, /* roleFlags= */ 0, /* label= */ null); this(uri, mimeType, language, selectionFlags, /* roleFlags= */ 0, /* label= */ null);
} }
/** @deprecated Use {@link Builder} instead. */ /**
* @deprecated Use {@link Builder} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Subtitle( public Subtitle(
@ -1550,7 +1594,9 @@ public final class MediaItem implements Bundleable {
return buildClippingProperties(); return buildClippingProperties();
} }
/** @deprecated Use {@link #build()} instead. */ /**
* @deprecated Use {@link #build()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public ClippingProperties buildClippingProperties() { public ClippingProperties buildClippingProperties() {
@ -1680,7 +1726,9 @@ public final class MediaItem implements Bundleable {
} }
} }
/** @deprecated Use {@link ClippingConfiguration} instead. */ /**
* @deprecated Use {@link ClippingConfiguration} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public static final class ClippingProperties extends ClippingConfiguration { public static final class ClippingProperties extends ClippingConfiguration {
@ -1692,6 +1740,146 @@ public final class MediaItem implements Bundleable {
} }
} }
/**
* Metadata that helps the player to understand a playback request represented by a {@link
* MediaItem}.
*
* <p>This metadata is most useful for cases where playback requests are forwarded to other player
* instances (e.g. from a {@link android.media.session.MediaController}) and the player creating
* the request doesn't know the required {@link LocalConfiguration} for playback.
*/
public static final class RequestMetadata implements Bundleable {
/** Empty request metadata. */
public static final RequestMetadata EMPTY = new Builder().build();
/** Builder for {@link RequestMetadata} instances. */
public static final class Builder {
@Nullable private Uri mediaUri;
@Nullable private String searchQuery;
@Nullable private Bundle extras;
/** Constructs an instance. */
public Builder() {}
private Builder(RequestMetadata requestMetadata) {
this.mediaUri = requestMetadata.mediaUri;
this.searchQuery = requestMetadata.searchQuery;
this.extras = requestMetadata.extras;
}
/** Sets the URI of the requested media, or null if not known or applicable. */
public Builder setMediaUri(@Nullable Uri mediaUri) {
this.mediaUri = mediaUri;
return this;
}
/** Sets the search query for the requested media, or null if not applicable. */
public Builder setSearchQuery(@Nullable String searchQuery) {
this.searchQuery = searchQuery;
return this;
}
/** Sets optional extras {@link Bundle}. */
public Builder setExtras(@Nullable Bundle extras) {
this.extras = extras;
return this;
}
/** Builds the request metadata. */
public RequestMetadata build() {
return new RequestMetadata(this);
}
}
/** The URI of the requested media, or null if not known or applicable. */
@Nullable public final Uri mediaUri;
/** The search query for the requested media, or null if not applicable. */
@Nullable public final String searchQuery;
/**
* Optional extras {@link Bundle}.
*
* <p>Given the complexities of checking the equality of two {@link Bundle}s, this is not
* considered in the {@link #equals(Object)} or {@link #hashCode()}.
*/
@Nullable public final Bundle extras;
private RequestMetadata(Builder builder) {
this.mediaUri = builder.mediaUri;
this.searchQuery = builder.searchQuery;
this.extras = builder.extras;
}
/** Returns a {@link Builder} initialized with the values of this instance. */
public Builder buildUpon() {
return new Builder(this);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof RequestMetadata)) {
return false;
}
RequestMetadata that = (RequestMetadata) o;
return Util.areEqual(mediaUri, that.mediaUri) && Util.areEqual(searchQuery, that.searchQuery);
}
@Override
public int hashCode() {
int result = mediaUri == null ? 0 : mediaUri.hashCode();
result = 31 * result + (searchQuery == null ? 0 : searchQuery.hashCode());
return result;
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@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
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
if (mediaUri != null) {
bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri);
}
if (searchQuery != null) {
bundle.putString(keyForField(FIELD_SEARCH_QUERY), searchQuery);
}
if (extras != null) {
bundle.putBundle(keyForField(FIELD_EXTRAS), extras);
}
return bundle;
}
/** Object that can restore {@link RequestMetadata} from a {@link Bundle}. */
@UnstableApi
public static final Creator<RequestMetadata> CREATOR =
bundle ->
new RequestMetadata.Builder()
.setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI)))
.setSearchQuery(bundle.getString(keyForField(FIELD_SEARCH_QUERY)))
.setExtras(bundle.getBundle(keyForField(FIELD_EXTRAS)))
.build();
private static String keyForField(@RequestMetadata.FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
/** /**
* The default media ID that is used if the media ID is not explicitly set by {@link * The default media ID that is used if the media ID is not explicitly set by {@link
* Builder#setMediaId(String)}. * Builder#setMediaId(String)}.
@ -1709,7 +1897,9 @@ public final class MediaItem implements Bundleable {
* boundaries. * boundaries.
*/ */
@Nullable public final LocalConfiguration localConfiguration; @Nullable public final LocalConfiguration localConfiguration;
/** @deprecated Use {@link #localConfiguration} instead. */ /**
* @deprecated Use {@link #localConfiguration} instead.
*/
@UnstableApi @Deprecated @Nullable public final PlaybackProperties playbackProperties; @UnstableApi @Deprecated @Nullable public final PlaybackProperties playbackProperties;
/** The live playback configuration. */ /** The live playback configuration. */
@ -1720,9 +1910,14 @@ public final class MediaItem implements Bundleable {
/** The clipping properties. */ /** The clipping properties. */
public final ClippingConfiguration clippingConfiguration; public final ClippingConfiguration clippingConfiguration;
/** @deprecated Use {@link #clippingConfiguration} instead. */ /**
* @deprecated Use {@link #clippingConfiguration} instead.
*/
@UnstableApi @Deprecated public final ClippingProperties clippingProperties; @UnstableApi @Deprecated public final ClippingProperties clippingProperties;
/** The media {@link RequestMetadata}. */
public final RequestMetadata requestMetadata;
// Using PlaybackProperties and ClippingProperties until they're deleted. // Using PlaybackProperties and ClippingProperties until they're deleted.
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private MediaItem( private MediaItem(
@ -1730,7 +1925,8 @@ public final class MediaItem implements Bundleable {
ClippingProperties clippingConfiguration, ClippingProperties clippingConfiguration,
@Nullable PlaybackProperties localConfiguration, @Nullable PlaybackProperties localConfiguration,
LiveConfiguration liveConfiguration, LiveConfiguration liveConfiguration,
MediaMetadata mediaMetadata) { MediaMetadata mediaMetadata,
RequestMetadata requestMetadata) {
this.mediaId = mediaId; this.mediaId = mediaId;
this.localConfiguration = localConfiguration; this.localConfiguration = localConfiguration;
this.playbackProperties = localConfiguration; this.playbackProperties = localConfiguration;
@ -1738,6 +1934,7 @@ public final class MediaItem implements Bundleable {
this.mediaMetadata = mediaMetadata; this.mediaMetadata = mediaMetadata;
this.clippingConfiguration = clippingConfiguration; this.clippingConfiguration = clippingConfiguration;
this.clippingProperties = clippingConfiguration; this.clippingProperties = clippingConfiguration;
this.requestMetadata = requestMetadata;
} }
/** Returns a {@link Builder} initialized with the values of this instance. */ /** Returns a {@link Builder} initialized with the values of this instance. */
@ -1760,7 +1957,8 @@ public final class MediaItem implements Bundleable {
&& clippingConfiguration.equals(other.clippingConfiguration) && clippingConfiguration.equals(other.clippingConfiguration)
&& Util.areEqual(localConfiguration, other.localConfiguration) && Util.areEqual(localConfiguration, other.localConfiguration)
&& Util.areEqual(liveConfiguration, other.liveConfiguration) && Util.areEqual(liveConfiguration, other.liveConfiguration)
&& Util.areEqual(mediaMetadata, other.mediaMetadata); && Util.areEqual(mediaMetadata, other.mediaMetadata)
&& Util.areEqual(requestMetadata, other.requestMetadata);
} }
@Override @Override
@ -1770,6 +1968,7 @@ public final class MediaItem implements Bundleable {
result = 31 * result + liveConfiguration.hashCode(); result = 31 * result + liveConfiguration.hashCode();
result = 31 * result + clippingConfiguration.hashCode(); result = 31 * result + clippingConfiguration.hashCode();
result = 31 * result + mediaMetadata.hashCode(); result = 31 * result + mediaMetadata.hashCode();
result = 31 * result + requestMetadata.hashCode();
return result; return result;
} }
@ -1782,7 +1981,8 @@ public final class MediaItem implements Bundleable {
FIELD_MEDIA_ID, FIELD_MEDIA_ID,
FIELD_LIVE_CONFIGURATION, FIELD_LIVE_CONFIGURATION,
FIELD_MEDIA_METADATA, FIELD_MEDIA_METADATA,
FIELD_CLIPPING_PROPERTIES FIELD_CLIPPING_PROPERTIES,
FIELD_REQUEST_METADATA
}) })
private @interface FieldNumber {} private @interface FieldNumber {}
@ -1790,6 +1990,7 @@ public final class MediaItem implements Bundleable {
private static final int FIELD_LIVE_CONFIGURATION = 1; private static final int FIELD_LIVE_CONFIGURATION = 1;
private static final int FIELD_MEDIA_METADATA = 2; private static final int FIELD_MEDIA_METADATA = 2;
private static final int FIELD_CLIPPING_PROPERTIES = 3; private static final int FIELD_CLIPPING_PROPERTIES = 3;
private static final int FIELD_REQUEST_METADATA = 4;
/** /**
* {@inheritDoc} * {@inheritDoc}
@ -1805,6 +2006,7 @@ public final class MediaItem implements Bundleable {
bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle()); bundle.putBundle(keyForField(FIELD_LIVE_CONFIGURATION), liveConfiguration.toBundle());
bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle()); bundle.putBundle(keyForField(FIELD_MEDIA_METADATA), mediaMetadata.toBundle());
bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingConfiguration.toBundle()); bundle.putBundle(keyForField(FIELD_CLIPPING_PROPERTIES), clippingConfiguration.toBundle());
bundle.putBundle(keyForField(FIELD_REQUEST_METADATA), requestMetadata.toBundle());
return bundle; return bundle;
} }
@ -1841,12 +2043,20 @@ public final class MediaItem implements Bundleable {
} else { } else {
clippingConfiguration = ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle); clippingConfiguration = ClippingConfiguration.CREATOR.fromBundle(clippingConfigurationBundle);
} }
@Nullable Bundle requestMetadataBundle = bundle.getBundle(keyForField(FIELD_REQUEST_METADATA));
RequestMetadata requestMetadata;
if (requestMetadataBundle == null) {
requestMetadata = RequestMetadata.EMPTY;
} else {
requestMetadata = RequestMetadata.CREATOR.fromBundle(requestMetadataBundle);
}
return new MediaItem( return new MediaItem(
mediaId, mediaId,
clippingConfiguration, clippingConfiguration,
/* localConfiguration= */ null, /* localConfiguration= */ null,
liveConfiguration, liveConfiguration,
mediaMetadata); mediaMetadata,
requestMetadata);
} }
private static String keyForField(@FieldNumber int field) { private static String keyForField(@FieldNumber int field) {

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-alpha03"; public static final String VERSION = "1.0.0-beta01";
/** 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-alpha03"; public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta01";
/** /**
* 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_0_03; public static final int VERSION_INT = 1_000_000_1_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

@ -52,7 +52,6 @@ public final class MediaMetadata implements Bundleable {
@Nullable private CharSequence displayTitle; @Nullable private CharSequence displayTitle;
@Nullable private CharSequence subtitle; @Nullable private CharSequence subtitle;
@Nullable private CharSequence description; @Nullable private CharSequence description;
@Nullable private Uri mediaUri;
@Nullable private Rating userRating; @Nullable private Rating userRating;
@Nullable private Rating overallRating; @Nullable private Rating overallRating;
@Nullable private byte[] artworkData; @Nullable private byte[] artworkData;
@ -88,7 +87,6 @@ public final class MediaMetadata implements Bundleable {
this.displayTitle = mediaMetadata.displayTitle; this.displayTitle = mediaMetadata.displayTitle;
this.subtitle = mediaMetadata.subtitle; this.subtitle = mediaMetadata.subtitle;
this.description = mediaMetadata.description; this.description = mediaMetadata.description;
this.mediaUri = mediaMetadata.mediaUri;
this.userRating = mediaMetadata.userRating; this.userRating = mediaMetadata.userRating;
this.overallRating = mediaMetadata.overallRating; this.overallRating = mediaMetadata.overallRating;
this.artworkData = mediaMetadata.artworkData; this.artworkData = mediaMetadata.artworkData;
@ -161,12 +159,6 @@ public final class MediaMetadata implements Bundleable {
return this; return this;
} }
/** Sets the media {@link Uri}. */
public Builder setMediaUri(@Nullable Uri mediaUri) {
this.mediaUri = mediaUri;
return this;
}
/** Sets the user {@link Rating}. */ /** Sets the user {@link Rating}. */
public Builder setUserRating(@Nullable Rating userRating) { public Builder setUserRating(@Nullable Rating userRating) {
this.userRating = userRating; this.userRating = userRating;
@ -248,7 +240,9 @@ public final class MediaMetadata implements Bundleable {
return this; return this;
} }
/** @deprecated Use {@link #setRecordingYear(Integer)} instead. */ /**
* @deprecated Use {@link #setRecordingYear(Integer)} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
public Builder setYear(@Nullable Integer year) { public Builder setYear(@Nullable Integer year) {
@ -429,9 +423,6 @@ public final class MediaMetadata implements Bundleable {
if (mediaMetadata.description != null) { if (mediaMetadata.description != null) {
setDescription(mediaMetadata.description); setDescription(mediaMetadata.description);
} }
if (mediaMetadata.mediaUri != null) {
setMediaUri(mediaMetadata.mediaUri);
}
if (mediaMetadata.userRating != null) { if (mediaMetadata.userRating != null) {
setUserRating(mediaMetadata.userRating); setUserRating(mediaMetadata.userRating);
} }
@ -634,8 +625,6 @@ public final class MediaMetadata implements Bundleable {
@Nullable public final CharSequence subtitle; @Nullable public final CharSequence subtitle;
/** Optional description. */ /** Optional description. */
@Nullable public final CharSequence description; @Nullable public final CharSequence description;
/** Optional media {@link Uri}. */
@Nullable public final Uri mediaUri;
/** Optional user {@link Rating}. */ /** Optional user {@link Rating}. */
@Nullable public final Rating userRating; @Nullable public final Rating userRating;
/** Optional overall {@link Rating}. */ /** Optional overall {@link Rating}. */
@ -654,7 +643,9 @@ public final class MediaMetadata implements Bundleable {
@Nullable public final @FolderType Integer folderType; @Nullable public final @FolderType Integer folderType;
/** Optional boolean for media playability. */ /** Optional boolean for media playability. */
@Nullable public final Boolean isPlayable; @Nullable public final Boolean isPlayable;
/** @deprecated Use {@link #recordingYear} instead. */ /**
* @deprecated Use {@link #recordingYear} instead.
*/
@UnstableApi @Deprecated @Nullable public final Integer year; @UnstableApi @Deprecated @Nullable public final Integer year;
/** Optional year of the recording date. */ /** Optional year of the recording date. */
@Nullable public final Integer recordingYear; @Nullable public final Integer recordingYear;
@ -718,7 +709,6 @@ public final class MediaMetadata implements Bundleable {
this.displayTitle = builder.displayTitle; this.displayTitle = builder.displayTitle;
this.subtitle = builder.subtitle; this.subtitle = builder.subtitle;
this.description = builder.description; this.description = builder.description;
this.mediaUri = builder.mediaUri;
this.userRating = builder.userRating; this.userRating = builder.userRating;
this.overallRating = builder.overallRating; this.overallRating = builder.overallRating;
this.artworkData = builder.artworkData; this.artworkData = builder.artworkData;
@ -767,7 +757,6 @@ public final class MediaMetadata implements Bundleable {
&& Util.areEqual(displayTitle, that.displayTitle) && Util.areEqual(displayTitle, that.displayTitle)
&& Util.areEqual(subtitle, that.subtitle) && Util.areEqual(subtitle, that.subtitle)
&& Util.areEqual(description, that.description) && Util.areEqual(description, that.description)
&& Util.areEqual(mediaUri, that.mediaUri)
&& Util.areEqual(userRating, that.userRating) && Util.areEqual(userRating, that.userRating)
&& Util.areEqual(overallRating, that.overallRating) && Util.areEqual(overallRating, that.overallRating)
&& Arrays.equals(artworkData, that.artworkData) && Arrays.equals(artworkData, that.artworkData)
@ -803,7 +792,6 @@ public final class MediaMetadata implements Bundleable {
displayTitle, displayTitle,
subtitle, subtitle,
description, description,
mediaUri,
userRating, userRating,
overallRating, overallRating,
Arrays.hashCode(artworkData), Arrays.hashCode(artworkData),
@ -914,7 +902,6 @@ public final class MediaMetadata implements Bundleable {
bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle); bundle.putCharSequence(keyForField(FIELD_DISPLAY_TITLE), displayTitle);
bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle); bundle.putCharSequence(keyForField(FIELD_SUBTITLE), subtitle);
bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description); bundle.putCharSequence(keyForField(FIELD_DESCRIPTION), description);
bundle.putParcelable(keyForField(FIELD_MEDIA_URI), mediaUri);
bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData); bundle.putByteArray(keyForField(FIELD_ARTWORK_DATA), artworkData);
bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri); bundle.putParcelable(keyForField(FIELD_ARTWORK_URI), artworkUri);
bundle.putCharSequence(keyForField(FIELD_WRITER), writer); bundle.putCharSequence(keyForField(FIELD_WRITER), writer);
@ -988,7 +975,6 @@ public final class MediaMetadata implements Bundleable {
.setDisplayTitle(bundle.getCharSequence(keyForField(FIELD_DISPLAY_TITLE))) .setDisplayTitle(bundle.getCharSequence(keyForField(FIELD_DISPLAY_TITLE)))
.setSubtitle(bundle.getCharSequence(keyForField(FIELD_SUBTITLE))) .setSubtitle(bundle.getCharSequence(keyForField(FIELD_SUBTITLE)))
.setDescription(bundle.getCharSequence(keyForField(FIELD_DESCRIPTION))) .setDescription(bundle.getCharSequence(keyForField(FIELD_DESCRIPTION)))
.setMediaUri(bundle.getParcelable(keyForField(FIELD_MEDIA_URI)))
.setArtworkData( .setArtworkData(
bundle.getByteArray(keyForField(FIELD_ARTWORK_DATA)), bundle.getByteArray(keyForField(FIELD_ARTWORK_DATA)),
bundle.containsKey(keyForField(FIELD_ARTWORK_DATA_TYPE)) bundle.containsKey(keyForField(FIELD_ARTWORK_DATA_TYPE))

View File

@ -62,12 +62,16 @@ public final class Metadata implements Parcelable {
private final Entry[] entries; private final Entry[] entries;
/** @param entries The metadata entries. */ /**
* @param entries The metadata entries.
*/
public Metadata(Entry... entries) { public Metadata(Entry... entries) {
this.entries = entries; this.entries = entries;
} }
/** @param entries The metadata entries. */ /**
* @param entries The metadata entries.
*/
public Metadata(List<? extends Entry> entries) { public Metadata(List<? extends Entry> entries) {
this.entries = entries.toArray(new Entry[0]); this.entries = entries.toArray(new Entry[0]);
} }

View File

@ -56,6 +56,10 @@ public final class MimeTypes {
@UnstableApi public static final String VIDEO_FLV = BASE_TYPE_VIDEO + "/x-flv"; @UnstableApi public static final String VIDEO_FLV = BASE_TYPE_VIDEO + "/x-flv";
public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision"; public static final String VIDEO_DOLBY_VISION = BASE_TYPE_VIDEO + "/dolby-vision";
public static final String VIDEO_OGG = BASE_TYPE_VIDEO + "/ogg"; public static final String VIDEO_OGG = BASE_TYPE_VIDEO + "/ogg";
public static final String VIDEO_AVI = BASE_TYPE_VIDEO + "/x-msvideo";
public static final String VIDEO_MJPEG = BASE_TYPE_VIDEO + "/mjpeg";
public static final String VIDEO_MP42 = BASE_TYPE_VIDEO + "/mp42";
public static final String VIDEO_MP43 = BASE_TYPE_VIDEO + "/mp43";
@UnstableApi public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown"; @UnstableApi public static final String VIDEO_UNKNOWN = BASE_TYPE_VIDEO + "/x-unknown";
// audio/ MIME types // audio/ MIME types
@ -87,6 +91,7 @@ public final class MimeTypes {
public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp";
public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb";
public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac"; public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac";
public static final String AUDIO_MIDI = BASE_TYPE_AUDIO + "/midi";
public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm"; public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm";
public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg"; public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg";
@ -108,11 +113,10 @@ public final class MimeTypes {
public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4";
public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm";
@UnstableApi
public static final String APPLICATION_MATROSKA = BASE_TYPE_APPLICATION + "/x-matroska"; public static final String APPLICATION_MATROSKA = BASE_TYPE_APPLICATION + "/x-matroska";
public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml"; public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml";
@UnstableApi public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL";
public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml"; public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml";
public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3";
public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608";
@ -135,7 +139,7 @@ public final class MimeTypes {
@UnstableApi public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; @UnstableApi public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif";
@UnstableApi public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy"; @UnstableApi public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy";
public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait"; public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait";
@UnstableApi public static final String APPLICATION_RTSP = BASE_TYPE_APPLICATION + "/x-rtsp"; public static final String APPLICATION_RTSP = BASE_TYPE_APPLICATION + "/x-rtsp";
// image/ MIME types // image/ MIME types

View File

@ -401,28 +401,6 @@ public class PlaybackException extends Exception implements Bundleable {
// Bundleable implementation. // Bundleable implementation.
/**
* Identifiers for fields in a {@link Bundle} which represents a playback exception. Subclasses
* may use {@link #FIELD_CUSTOM_ID_BASE} to generate more keys using {@link #keyForField(int)}.
*
* <p>Note: Changes to the Bundleable implementation must be backwards compatible, so as to avoid
* breaking communication across different Bundleable implementation versions.
*/
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef(
open = true,
value = {
FIELD_INT_ERROR_CODE,
FIELD_LONG_TIMESTAMP_MS,
FIELD_STRING_MESSAGE,
FIELD_STRING_CAUSE_CLASS_NAME,
FIELD_STRING_CAUSE_MESSAGE,
})
@UnstableApi
protected @interface FieldNumber {}
private static final int FIELD_INT_ERROR_CODE = 0; private static final int FIELD_INT_ERROR_CODE = 0;
private static final int FIELD_LONG_TIMESTAMP_MS = 1; private static final int FIELD_LONG_TIMESTAMP_MS = 1;
private static final int FIELD_STRING_MESSAGE = 2; private static final int FIELD_STRING_MESSAGE = 2;
@ -430,7 +408,7 @@ public class PlaybackException extends Exception implements Bundleable {
private static final int FIELD_STRING_CAUSE_MESSAGE = 4; private static final int FIELD_STRING_CAUSE_MESSAGE = 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
@ -458,11 +436,14 @@ public class PlaybackException extends Exception implements Bundleable {
} }
/** /**
* Converts the given {@link FieldNumber} to a string which can be used as a field key when * Converts the given field number to a string which can be used as a field key when implementing
* implementing {@link #toBundle()} and {@link Bundleable.Creator}. * {@link #toBundle()} and {@link Bundleable.Creator}.
*
* <p>Subclasses should use {@code field} values greater than or equal to {@link
* #FIELD_CUSTOM_ID_BASE}.
*/ */
@UnstableApi @UnstableApi
protected static String keyForField(@FieldNumber int field) { protected static String keyForField(int field) {
return Integer.toString(field, Character.MAX_RADIX); return Integer.toString(field, Character.MAX_RADIX);
} }

View File

@ -32,7 +32,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.text.Cue; import androidx.media3.common.text.Cue;
import androidx.media3.common.util.BundleableUtil; import androidx.media3.common.text.CueGroup;
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.Objects; import com.google.common.base.Objects;
@ -57,8 +57,8 @@ import java.util.List;
* <ul> * <ul>
* <li>They can provide a {@link Timeline} representing the structure of the media being played, * <li>They can provide a {@link Timeline} representing the structure of the media being played,
* which can be obtained by calling {@link #getCurrentTimeline()}. * which can be obtained by calling {@link #getCurrentTimeline()}.
* <li>They can provide a {@link TracksInfo} defining the currently available tracks and which are * <li>They can provide a {@link Tracks} defining the currently available tracks and which are
* selected to be rendered, which can be obtained by calling {@link #getCurrentTracksInfo()}. * selected to be rendered, which can be obtained by calling {@link #getCurrentTracks()}.
* </ul> * </ul>
*/ */
public interface Player { public interface Player {
@ -142,7 +142,9 @@ public interface Player {
* The UID of the window, or {@code null} if the timeline is {@link Timeline#isEmpty() empty}. * The UID of the window, or {@code null} if the timeline is {@link Timeline#isEmpty() empty}.
*/ */
@Nullable public final Object windowUid; @Nullable public final Object windowUid;
/** @deprecated Use {@link #mediaItemIndex} instead. */ /**
* @deprecated Use {@link #mediaItemIndex} instead.
*/
@UnstableApi @Deprecated public final int windowIndex; @UnstableApi @Deprecated public final int windowIndex;
/** The media item index. */ /** The media item index. */
public final int mediaItemIndex; public final int mediaItemIndex;
@ -292,7 +294,9 @@ public interface Player {
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putInt(keyForField(FIELD_MEDIA_ITEM_INDEX), mediaItemIndex); bundle.putInt(keyForField(FIELD_MEDIA_ITEM_INDEX), mediaItemIndex);
bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), BundleableUtil.toNullableBundle(mediaItem)); if (mediaItem != null) {
bundle.putBundle(keyForField(FIELD_MEDIA_ITEM), mediaItem.toBundle());
}
bundle.putInt(keyForField(FIELD_PERIOD_INDEX), periodIndex); bundle.putInt(keyForField(FIELD_PERIOD_INDEX), periodIndex);
bundle.putLong(keyForField(FIELD_POSITION_MS), positionMs); bundle.putLong(keyForField(FIELD_POSITION_MS), positionMs);
bundle.putLong(keyForField(FIELD_CONTENT_POSITION_MS), contentPositionMs); bundle.putLong(keyForField(FIELD_CONTENT_POSITION_MS), contentPositionMs);
@ -307,10 +311,10 @@ public interface Player {
private static PositionInfo fromBundle(Bundle bundle) { private static PositionInfo fromBundle(Bundle bundle) {
int mediaItemIndex = int mediaItemIndex =
bundle.getInt(keyForField(FIELD_MEDIA_ITEM_INDEX), /* defaultValue= */ C.INDEX_UNSET); bundle.getInt(keyForField(FIELD_MEDIA_ITEM_INDEX), /* defaultValue= */ C.INDEX_UNSET);
@Nullable Bundle mediaItemBundle = bundle.getBundle(keyForField(FIELD_MEDIA_ITEM));
@Nullable @Nullable
MediaItem mediaItem = MediaItem mediaItem =
BundleableUtil.fromNullableBundle( mediaItemBundle == null ? null : MediaItem.CREATOR.fromBundle(mediaItemBundle);
MediaItem.CREATOR, bundle.getBundle(keyForField(FIELD_MEDIA_ITEM)));
int periodIndex = int periodIndex =
bundle.getInt(keyForField(FIELD_PERIOD_INDEX), /* defaultValue= */ C.INDEX_UNSET); bundle.getInt(keyForField(FIELD_PERIOD_INDEX), /* defaultValue= */ C.INDEX_UNSET);
long positionMs = long positionMs =
@ -379,7 +383,8 @@ public interface Player {
COMMAND_SET_VIDEO_SURFACE, COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT, COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS, COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACK_INFOS, COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM,
}; };
private final FlagSet.Builder flagsBuilder; private final FlagSet.Builder flagsBuilder;
@ -521,6 +526,11 @@ public interface Player {
return flags.contains(command); return flags.contains(command);
} }
/** Returns whether the set of commands contains at least one of the given {@code commands}. */
public boolean containsAny(@Command int... commands) {
return flags.containsAny(commands);
}
/** Returns the number of commands in this set. */ /** Returns the number of commands in this set. */
public int size() { public int size() {
return flags.size(); return flags.size();
@ -669,41 +679,24 @@ public interface Player {
@Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {} @Nullable MediaItem mediaItem, @MediaItemTransitionReason int reason) {}
/** /**
* Called when the available or selected tracks change. * Called when the tracks change.
* *
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with * <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration. * other events that happen in the same {@link Looper} message queue iteration.
* *
* @param trackGroups The available tracks. Never null, but may be of length zero. * @param tracks The available tracks information. Never null, but may be of length zero.
* @param trackSelections The selected tracks. Never null, but may contain null elements. A
* concrete implementation may include null elements if it has a fixed number of renderer
* components, wishes to report a TrackSelection for each of them, and has one or more
* renderer components that is not assigned any selected tracks.
* @deprecated Use {@link #onTracksInfoChanged(TracksInfo)} instead.
*/ */
@UnstableApi default void onTracksChanged(Tracks tracks) {}
@Deprecated
default void onTracksChanged(
TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {}
/**
* Called when the available or selected tracks change.
*
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
*
* @param tracksInfo The available tracks information. Never null, but may be of length zero.
*/
default void onTracksInfoChanged(TracksInfo tracksInfo) {}
/** /**
* Called when the combined {@link MediaMetadata} changes. * Called when the combined {@link MediaMetadata} changes.
* *
* <p>The provided {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata} * <p>The provided {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata
* and the static and dynamic metadata from the {@link TrackSelection#getFormat(int) track * MediaItem metadata}, the static metadata in the media's {@link Format#metadata Format}, and
* selections' formats} and {@link Listener#onMetadata(Metadata)}. If a field is populated in * any timed metadata that has been parsed from the media and output via {@link
* the {@link MediaItem#mediaMetadata}, it will be prioritised above the same field coming from * Listener#onMetadata(Metadata)}. If a field is populated in the {@link
* static or dynamic metadata. * MediaItem#mediaMetadata}, it will be prioritised above the same field coming from static or
* timed metadata.
* *
* <p>This method may be called multiple times in quick succession. * <p>This method may be called multiple times in quick succession.
* *
@ -732,7 +725,9 @@ public interface Player {
*/ */
default void onIsLoadingChanged(boolean isLoading) {} default void onIsLoadingChanged(boolean isLoading) {}
/** @deprecated Use {@link #onIsLoadingChanged(boolean)} instead. */ /**
* @deprecated Use {@link #onIsLoadingChanged(boolean)} instead.
*/
@Deprecated @Deprecated
@UnstableApi @UnstableApi
default void onLoadingChanged(boolean isLoading) {} default void onLoadingChanged(boolean isLoading) {}
@ -1032,16 +1027,29 @@ public interface Player {
/** /**
* Called when there is a change in the {@link Cue Cues}. * Called when there is a change in the {@link Cue Cues}.
* *
* <p>{@code cues} is in ascending order of priority. If any of the cue boxes overlap when * <p>Both {@link #onCues(List)} and {@link #onCues(CueGroup)} are called when there is a change
* displayed, the {@link Cue} nearer the end of the list should be shown on top. * in the cues. You should only implement one or the other.
* *
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with * <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration. * other events that happen in the same {@link Looper} message queue iteration.
* *
* @param cues The {@link Cue Cues}. May be empty. * @deprecated Use {@link #onCues(CueGroup)} instead.
*/ */
@Deprecated
@UnstableApi
default void onCues(List<Cue> cues) {} default void onCues(List<Cue> cues) {}
/**
* Called when there is a change in the {@link CueGroup}.
*
* <p>Both {@link #onCues(List)} and {@link #onCues(CueGroup)} are called when there is a change
* in the cues. You should only implement one or the other.
*
* <p>{@link #onEvents(Player, Events)} will also be called to report this event along with
* other events that happen in the same {@link Looper} message queue iteration.
*/
default void onCues(CueGroup cueGroup) {}
/** /**
* Called when there is metadata associated with the current playback time. * Called when there is metadata associated with the current playback time.
* *
@ -1316,7 +1324,7 @@ public interface Player {
int EVENT_TIMELINE_CHANGED = 0; int EVENT_TIMELINE_CHANGED = 0;
/** {@link #getCurrentMediaItem()} changed or the player started repeating the current item. */ /** {@link #getCurrentMediaItem()} changed or the player started repeating the current item. */
int EVENT_MEDIA_ITEM_TRANSITION = 1; int EVENT_MEDIA_ITEM_TRANSITION = 1;
/** {@link #getCurrentTracksInfo()} changed. */ /** {@link #getCurrentTracks()} changed. */
int EVENT_TRACKS_CHANGED = 2; int EVENT_TRACKS_CHANGED = 2;
/** {@link #isLoading()} ()} changed. */ /** {@link #isLoading()} ()} changed. */
int EVENT_IS_LOADING_CHANGED = 3; int EVENT_IS_LOADING_CHANGED = 3;
@ -1395,7 +1403,8 @@ public interface Player {
* #COMMAND_GET_VOLUME}, {@link #COMMAND_GET_DEVICE_VOLUME}, {@link #COMMAND_SET_VOLUME}, {@link * #COMMAND_GET_VOLUME}, {@link #COMMAND_GET_DEVICE_VOLUME}, {@link #COMMAND_SET_VOLUME}, {@link
* #COMMAND_SET_DEVICE_VOLUME}, {@link #COMMAND_ADJUST_DEVICE_VOLUME}, {@link * #COMMAND_SET_DEVICE_VOLUME}, {@link #COMMAND_ADJUST_DEVICE_VOLUME}, {@link
* #COMMAND_SET_VIDEO_SURFACE}, {@link #COMMAND_GET_TEXT}, {@link * #COMMAND_SET_VIDEO_SURFACE}, {@link #COMMAND_GET_TEXT}, {@link
* #COMMAND_SET_TRACK_SELECTION_PARAMETERS} or {@link #COMMAND_GET_TRACK_INFOS}. * #COMMAND_SET_TRACK_SELECTION_PARAMETERS}, {@link #COMMAND_GET_TRACKS} or {@link
* #COMMAND_SET_MEDIA_ITEM}.
*/ */
// @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.
@ -1433,7 +1442,8 @@ public interface Player {
COMMAND_SET_VIDEO_SURFACE, COMMAND_SET_VIDEO_SURFACE,
COMMAND_GET_TEXT, COMMAND_GET_TEXT,
COMMAND_SET_TRACK_SELECTION_PARAMETERS, COMMAND_SET_TRACK_SELECTION_PARAMETERS,
COMMAND_GET_TRACK_INFOS, COMMAND_GET_TRACKS,
COMMAND_SET_MEDIA_ITEM,
}) })
@interface Command {} @interface Command {}
/** Command to start, pause or resume playback. */ /** Command to start, pause or resume playback. */
@ -1446,24 +1456,32 @@ public interface Player {
int COMMAND_SEEK_TO_DEFAULT_POSITION = 4; int COMMAND_SEEK_TO_DEFAULT_POSITION = 4;
/** Command to seek anywhere into the current {@link MediaItem}. */ /** Command to seek anywhere into the current {@link MediaItem}. */
int COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM = 5; int COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM = 5;
/** @deprecated Use {@link #COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM} instead. */ /**
* @deprecated Use {@link #COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM} instead.
*/
@UnstableApi @Deprecated int COMMAND_SEEK_IN_CURRENT_WINDOW = COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM; @UnstableApi @Deprecated int COMMAND_SEEK_IN_CURRENT_WINDOW = COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM;
/** Command to seek to the default position of the previous {@link MediaItem}. */ /** Command to seek to the default position of the previous {@link MediaItem}. */
int COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM = 6; int COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM = 6;
/** @deprecated Use {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM} instead. */ /**
* @deprecated Use {@link #COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM} instead.
*/
@UnstableApi @Deprecated @UnstableApi @Deprecated
int COMMAND_SEEK_TO_PREVIOUS_WINDOW = COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM; int COMMAND_SEEK_TO_PREVIOUS_WINDOW = COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM;
/** Command to seek to an earlier position in the current or previous {@link MediaItem}. */ /** Command to seek to an earlier position in the current or previous {@link MediaItem}. */
int COMMAND_SEEK_TO_PREVIOUS = 7; int COMMAND_SEEK_TO_PREVIOUS = 7;
/** Command to seek to the default position of the next {@link MediaItem}. */ /** Command to seek to the default position of the next {@link MediaItem}. */
int COMMAND_SEEK_TO_NEXT_MEDIA_ITEM = 8; int COMMAND_SEEK_TO_NEXT_MEDIA_ITEM = 8;
/** @deprecated Use {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} instead. */ /**
* @deprecated Use {@link #COMMAND_SEEK_TO_NEXT_MEDIA_ITEM} instead.
*/
@UnstableApi @Deprecated int COMMAND_SEEK_TO_NEXT_WINDOW = COMMAND_SEEK_TO_NEXT_MEDIA_ITEM; @UnstableApi @Deprecated int COMMAND_SEEK_TO_NEXT_WINDOW = COMMAND_SEEK_TO_NEXT_MEDIA_ITEM;
/** Command to seek to a later position in the current or next {@link MediaItem}. */ /** Command to seek to a later position in the current or next {@link MediaItem}. */
int COMMAND_SEEK_TO_NEXT = 9; int COMMAND_SEEK_TO_NEXT = 9;
/** Command to seek anywhere in any {@link MediaItem}. */ /** Command to seek anywhere in any {@link MediaItem}. */
int COMMAND_SEEK_TO_MEDIA_ITEM = 10; int COMMAND_SEEK_TO_MEDIA_ITEM = 10;
/** @deprecated Use {@link #COMMAND_SEEK_TO_MEDIA_ITEM} instead. */ /**
* @deprecated Use {@link #COMMAND_SEEK_TO_MEDIA_ITEM} instead.
*/
@UnstableApi @Deprecated int COMMAND_SEEK_TO_WINDOW = COMMAND_SEEK_TO_MEDIA_ITEM; @UnstableApi @Deprecated int COMMAND_SEEK_TO_WINDOW = COMMAND_SEEK_TO_MEDIA_ITEM;
/** Command to seek back by a fixed increment into the current {@link MediaItem}. */ /** Command to seek back by a fixed increment into the current {@link MediaItem}. */
int COMMAND_SEEK_BACK = 11; int COMMAND_SEEK_BACK = 11;
@ -1503,8 +1521,10 @@ public interface Player {
int COMMAND_GET_TEXT = 28; int COMMAND_GET_TEXT = 28;
/** Command to set the player's track selection parameters. */ /** Command to set the player's track selection parameters. */
int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29; int COMMAND_SET_TRACK_SELECTION_PARAMETERS = 29;
/** Command to get track infos. */ /** Command to get details of the current track selection. */
int COMMAND_GET_TRACK_INFOS = 30; int COMMAND_GET_TRACKS = 30;
/** Command to set a {@link MediaItem MediaItem}. */
int COMMAND_SET_MEDIA_ITEM = 31;
/** Represents an invalid {@link Command}. */ /** Represents an invalid {@link Command}. */
int COMMAND_INVALID = -1; int COMMAND_INVALID = -1;
@ -1889,12 +1909,16 @@ public interface Player {
*/ */
void seekForward(); void seekForward();
/** @deprecated Use {@link #hasPreviousMediaItem()} instead. */ /**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
boolean hasPrevious(); boolean hasPrevious();
/** @deprecated Use {@link #hasPreviousMediaItem()} instead. */ /**
* @deprecated Use {@link #hasPreviousMediaItem()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
boolean hasPreviousWindow(); boolean hasPreviousWindow();
@ -1909,12 +1933,16 @@ public interface Player {
*/ */
boolean hasPreviousMediaItem(); boolean hasPreviousMediaItem();
/** @deprecated Use {@link #seekToPreviousMediaItem()} instead. */ /**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
void previous(); void previous();
/** @deprecated Use {@link #seekToPreviousMediaItem()} instead. */ /**
* @deprecated Use {@link #seekToPreviousMediaItem()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
void seekToPreviousWindow(); void seekToPreviousWindow();
@ -1961,12 +1989,16 @@ public interface Player {
*/ */
void seekToPrevious(); void seekToPrevious();
/** @deprecated Use {@link #hasNextMediaItem()} instead. */ /**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
boolean hasNext(); boolean hasNext();
/** @deprecated Use {@link #hasNextMediaItem()} instead. */ /**
* @deprecated Use {@link #hasNextMediaItem()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
boolean hasNextWindow(); boolean hasNextWindow();
@ -1981,12 +2013,16 @@ public interface Player {
*/ */
boolean hasNextMediaItem(); boolean hasNextMediaItem();
/** @deprecated Use {@link #seekToNextMediaItem()} instead. */ /**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
void next(); void next();
/** @deprecated Use {@link #seekToNextMediaItem()} instead. */ /**
* @deprecated Use {@link #seekToNextMediaItem()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
void seekToNextWindow(); void seekToNextWindow();
@ -2077,35 +2113,11 @@ public interface Player {
void release(); void release();
/** /**
* Returns the available track groups. * Returns the current tracks.
* *
* @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray) * @see Listener#onTracksChanged(Tracks)
* @deprecated Use {@link #getCurrentTracksInfo()}.
*/ */
@UnstableApi Tracks getCurrentTracks();
@Deprecated
TrackGroupArray getCurrentTrackGroups();
/**
* Returns the current track selections.
*
* <p>A concrete implementation may include null elements if it has a fixed number of renderer
* components, wishes to report a TrackSelection for each of them, and has one or more renderer
* components that is not assigned any selected tracks.
*
* @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
* @deprecated Use {@link #getCurrentTracksInfo()}.
*/
@UnstableApi
@Deprecated
TrackSelectionArray getCurrentTrackSelections();
/**
* Returns the available tracks, as well as the tracks' support, type, and selection status.
*
* @see Listener#onTracksChanged(TrackGroupArray, TrackSelectionArray)
*/
TracksInfo getCurrentTracksInfo();
/** /**
* Returns the parameters constraining the track selection. * Returns the parameters constraining the track selection.
@ -2137,11 +2149,11 @@ public interface Player {
* Returns the current combined {@link MediaMetadata}, or {@link MediaMetadata#EMPTY} if not * Returns the current combined {@link MediaMetadata}, or {@link MediaMetadata#EMPTY} if not
* supported. * supported.
* *
* <p>This {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata} and the * <p>This {@link MediaMetadata} is a combination of the {@link MediaItem#mediaMetadata MediaItem
* static and dynamic metadata from the {@link TrackSelection#getFormat(int) track selections' * metadata}, the static metadata in the media's {@link Format#metadata Format}, and any timed
* formats} and {@link Listener#onMetadata(Metadata)}. If a field is populated in the {@link * metadata that has been parsed from the media and output via {@link
* MediaItem#mediaMetadata}, it will be prioritised above the same field coming from static or * Listener#onMetadata(Metadata)}. If a field is populated in the {@link MediaItem#mediaMetadata},
* dynamic metadata. * it will be prioritised above the same field coming from static or timed metadata.
*/ */
MediaMetadata getMediaMetadata(); MediaMetadata getMediaMetadata();
@ -2171,7 +2183,9 @@ public interface Player {
/** Returns the index of the period currently being played. */ /** Returns the index of the period currently being played. */
int getCurrentPeriodIndex(); int getCurrentPeriodIndex();
/** @deprecated Use {@link #getCurrentMediaItemIndex()} instead. */ /**
* @deprecated Use {@link #getCurrentMediaItemIndex()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
int getCurrentWindowIndex(); int getCurrentWindowIndex();
@ -2183,7 +2197,9 @@ public interface Player {
*/ */
int getCurrentMediaItemIndex(); int getCurrentMediaItemIndex();
/** @deprecated Use {@link #getNextMediaItemIndex()} instead. */ /**
* @deprecated Use {@link #getNextMediaItemIndex()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
int getNextWindowIndex(); int getNextWindowIndex();
@ -2200,7 +2216,9 @@ public interface Player {
*/ */
int getNextMediaItemIndex(); int getNextMediaItemIndex();
/** @deprecated Use {@link #getPreviousMediaItemIndex()} instead. */ /**
* @deprecated Use {@link #getPreviousMediaItemIndex()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
int getPreviousWindowIndex(); int getPreviousWindowIndex();
@ -2262,7 +2280,9 @@ public interface Player {
*/ */
long getTotalBufferedDuration(); long getTotalBufferedDuration();
/** @deprecated Use {@link #isCurrentMediaItemDynamic()} instead. */ /**
* @deprecated Use {@link #isCurrentMediaItemDynamic()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
boolean isCurrentWindowDynamic(); boolean isCurrentWindowDynamic();
@ -2275,7 +2295,9 @@ public interface Player {
*/ */
boolean isCurrentMediaItemDynamic(); boolean isCurrentMediaItemDynamic();
/** @deprecated Use {@link #isCurrentMediaItemLive()} instead. */ /**
* @deprecated Use {@link #isCurrentMediaItemLive()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
boolean isCurrentWindowLive(); boolean isCurrentWindowLive();
@ -2301,7 +2323,9 @@ public interface Player {
*/ */
long getCurrentLiveOffset(); long getCurrentLiveOffset();
/** @deprecated Use {@link #isCurrentMediaItemSeekable()} instead. */ /**
* @deprecated Use {@link #isCurrentMediaItemSeekable()} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
boolean isCurrentWindowSeekable(); boolean isCurrentWindowSeekable();
@ -2465,8 +2489,8 @@ public interface Player {
*/ */
VideoSize getVideoSize(); VideoSize getVideoSize();
/** Returns the current {@link Cue Cues}. This list may be empty. */ /** Returns the current {@link CueGroup}. */
List<Cue> getCurrentCues(); CueGroup getCurrentCues();
/** Gets the device information. */ /** Gets the device information. */
DeviceInfo getDeviceInfo(); DeviceInfo getDeviceInfo();

View File

@ -32,7 +32,7 @@ import java.lang.annotation.Target;
public abstract class Rating implements Bundleable { public abstract class Rating implements Bundleable {
/** A float value that denotes the rating is unset. */ /** A float value that denotes the rating is unset. */
public static final float RATING_UNSET = -1.0f; /* package */ static final float RATING_UNSET = -1.0f;
// Default package-private constructor to prevent extending Rating class outside this package. // Default package-private constructor to prevent extending Rating class outside this package.
/* package */ Rating() {} /* package */ Rating() {}

View File

@ -44,7 +44,9 @@ public final class StreamKey implements Comparable<StreamKey>, Parcelable {
/** The stream index. */ /** The stream index. */
public final int streamIndex; public final int streamIndex;
/** @deprecated Use {@link #streamIndex}. */ /**
* @deprecated Use {@link #streamIndex}.
*/
@Deprecated public final int trackIndex; @Deprecated public final int trackIndex;
/** /**

View File

@ -169,7 +169,9 @@ public abstract class Timeline implements Bundleable {
*/ */
public Object uid; public Object uid;
/** @deprecated Use {@link #mediaItem} instead. */ /**
* @deprecated Use {@link #mediaItem} instead.
*/
@UnstableApi @Deprecated @Nullable public Object tag; @UnstableApi @Deprecated @Nullable public Object tag;
/** The {@link MediaItem} associated to the window. Not necessarily unique. */ /** The {@link MediaItem} associated to the window. Not necessarily unique. */
@ -212,7 +214,9 @@ public abstract class Timeline implements Bundleable {
/** Whether this window may change when the timeline is updated. */ /** Whether this window may change when the timeline is updated. */
public boolean isDynamic; public boolean isDynamic;
/** @deprecated Use {@link #isLive()} instead. */ /**
* @deprecated Use {@link #isLive()} instead.
*/
@UnstableApi @Deprecated public boolean isLive; @UnstableApi @Deprecated public boolean isLive;
/** /**
@ -1178,7 +1182,9 @@ public abstract class Timeline implements Bundleable {
== C.INDEX_UNSET; == C.INDEX_UNSET;
} }
/** @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long)} instead. */ /**
* @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long)} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
@InlineMe(replacement = "this.getPeriodPositionUs(window, period, windowIndex, windowPositionUs)") @InlineMe(replacement = "this.getPeriodPositionUs(window, period, windowIndex, windowPositionUs)")
@ -1186,7 +1192,9 @@ public abstract class Timeline implements Bundleable {
Window window, Period period, int windowIndex, long windowPositionUs) { Window window, Period period, int windowIndex, long windowPositionUs) {
return getPeriodPositionUs(window, period, windowIndex, windowPositionUs); return getPeriodPositionUs(window, period, windowIndex, windowPositionUs);
} }
/** @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long, long)} instead. */ /**
* @deprecated Use {@link #getPeriodPositionUs(Window, Period, int, long, long)} instead.
*/
@UnstableApi @UnstableApi
@Deprecated @Deprecated
@Nullable @Nullable

View File

@ -34,15 +34,35 @@ import java.lang.annotation.Target;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
/** Defines an immutable group of tracks identified by their format identity. */ /**
* An immutable group of tracks available within a media stream. All tracks in a group present the
* same content, but their formats may differ.
*
* <p>As an example of how tracks can be grouped, consider an adaptive playback where a main video
* feed is provided in five resolutions, and an alternative video feed (e.g., a different camera
* angle in a sports match) is provided in two resolutions. In this case there will be two video
* track groups, one corresponding to the main video feed containing five tracks, and a second for
* the alternative video feed containing two tracks.
*
* <p>Note that audio tracks whose languages differ are not grouped, because content in different
* languages is not considered to be the same. Conversely, audio tracks in the same language that
* only differ in properties such as bitrate, sampling rate, channel count and so on can be grouped.
* This also applies to text tracks.
*
* <p>Note also that this class only contains information derived from the media itself. Unlike
* {@link Tracks.Group}, it does not include runtime information such as the extent to which
* playback of each track is supported by the device, or which tracks are currently selected.
*/
public final class TrackGroup implements Bundleable { public final class TrackGroup implements Bundleable {
private static final String TAG = "TrackGroup"; private static final String TAG = "TrackGroup";
/** The number of tracks in the group. */ /** The number of tracks in the group. */
public final int length; @UnstableApi public final int length;
/** An identifier for the track group. */ /** An identifier for the track group. */
public final String id; @UnstableApi public final String id;
/** The type of tracks in the group. */
@UnstableApi public final @C.TrackType int type;
private final Format[] formats; private final Format[] formats;
@ -71,6 +91,11 @@ public final class TrackGroup implements Bundleable {
this.id = id; this.id = id;
this.formats = formats; this.formats = formats;
this.length = formats.length; this.length = formats.length;
@C.TrackType int type = MimeTypes.getTrackType(formats[0].sampleMimeType);
if (type == C.TRACK_TYPE_UNKNOWN) {
type = MimeTypes.getTrackType(formats[0].containerMimeType);
}
this.type = type;
verifyCorrectness(); verifyCorrectness();
} }
@ -92,6 +117,7 @@ public final class TrackGroup implements Bundleable {
* @param index The index of the track. * @param index The index of the track.
* @return The track's format. * @return The track's format.
*/ */
@UnstableApi
public Format getFormat(int index) { public Format getFormat(int index) {
return formats[index]; return formats[index];
} }
@ -105,6 +131,7 @@ public final class TrackGroup implements Bundleable {
* @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists. * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists.
*/ */
@SuppressWarnings("ReferenceEquality") @SuppressWarnings("ReferenceEquality")
@UnstableApi
public int indexOf(Format format) { public int indexOf(Format format) {
for (int i = 0; i < formats.length; i++) { for (int i = 0; i < formats.length; i++) {
if (format == formats[i]) { if (format == formats[i]) {
@ -134,7 +161,7 @@ public final class TrackGroup implements Bundleable {
return false; return false;
} }
TrackGroup other = (TrackGroup) obj; TrackGroup other = (TrackGroup) obj;
return length == other.length && id.equals(other.id) && Arrays.equals(formats, other.formats); return id.equals(other.id) && Arrays.equals(formats, other.formats);
} }
// Bundleable implementation. // Bundleable implementation.
@ -162,11 +189,12 @@ public final class TrackGroup implements Bundleable {
@UnstableApi @UnstableApi
public static final Creator<TrackGroup> CREATOR = public static final Creator<TrackGroup> CREATOR =
bundle -> { bundle -> {
@Nullable
List<Bundle> formatBundles = bundle.getParcelableArrayList(keyForField(FIELD_FORMATS));
List<Format> formats = List<Format> formats =
BundleableUtil.fromBundleNullableList( formatBundles == null
Format.CREATOR, ? ImmutableList.of()
bundle.getParcelableArrayList(keyForField(FIELD_FORMATS)), : BundleableUtil.fromBundleList(Format.CREATOR, formatBundles);
ImmutableList.of());
String id = bundle.getString(keyForField(FIELD_ID), /* defaultValue= */ ""); String id = bundle.getString(keyForField(FIELD_ID), /* defaultValue= */ "");
return new TrackGroup(id, formats.toArray(new Format[0])); return new TrackGroup(id, formats.toArray(new Format[0]));
}; };

View File

@ -0,0 +1,140 @@
/*
* Copyright (C) 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.common;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static java.util.Collections.max;
import static java.util.Collections.min;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import com.google.common.primitives.Ints;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
/**
* A track selection override, consisting of a {@link TrackGroup} and the indices of the tracks
* within the group that should be selected.
*
* <p>A track selection override is applied during playback if the media being played contains a
* {@link TrackGroup} equal to the one in the override. If a {@link TrackSelectionParameters}
* contains only one override of a given track type that applies to the media, this override will be
* used to control the track selection for that type. If multiple overrides of a given track type
* apply then the player will apply only one of them.
*
* <p>If {@link #trackIndices} is empty then the override specifies that no tracks should be
* selected. Adding an empty override to a {@link TrackSelectionParameters} is similar to {@link
* TrackSelectionParameters.Builder#setTrackTypeDisabled disabling a track type}, except that an
* empty override will only be applied if the media being played contains a {@link TrackGroup} equal
* to the one in the override. Conversely, disabling a track type will prevent selection of tracks
* of that type for all media.
*/
public final class TrackSelectionOverride implements Bundleable {
/** The media {@link TrackGroup} whose {@link #trackIndices} are forced to be selected. */
public final TrackGroup mediaTrackGroup;
/** The indices of tracks in a {@link TrackGroup} to be selected. */
public final ImmutableList<Integer> trackIndices;
@Documented
@Retention(RetentionPolicy.SOURCE)
@IntDef({
FIELD_TRACK_GROUP,
FIELD_TRACKS,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUP = 0;
private static final int FIELD_TRACKS = 1;
/**
* Constructs an instance to force {@code trackIndex} in {@code trackGroup} to be selected.
*
* @param mediaTrackGroup The media {@link TrackGroup} for which to override the track selection.
* @param trackIndex The index of the track in the {@link TrackGroup} to select.
*/
public TrackSelectionOverride(TrackGroup mediaTrackGroup, int trackIndex) {
this(mediaTrackGroup, ImmutableList.of(trackIndex));
}
/**
* Constructs an instance to force {@code trackIndices} in {@code trackGroup} to be selected.
*
* @param mediaTrackGroup The media {@link TrackGroup} for which to override the track selection.
* @param trackIndices The indices of the tracks in the {@link TrackGroup} to select.
*/
public TrackSelectionOverride(TrackGroup mediaTrackGroup, List<Integer> trackIndices) {
if (!trackIndices.isEmpty()) {
if (min(trackIndices) < 0 || max(trackIndices) >= mediaTrackGroup.length) {
throw new IndexOutOfBoundsException();
}
}
this.mediaTrackGroup = mediaTrackGroup;
this.trackIndices = ImmutableList.copyOf(trackIndices);
}
/** Returns the {@link C.TrackType} of the overridden track group. */
public @C.TrackType int getType() {
return mediaTrackGroup.type;
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
TrackSelectionOverride that = (TrackSelectionOverride) obj;
return mediaTrackGroup.equals(that.mediaTrackGroup) && trackIndices.equals(that.trackIndices);
}
@Override
public int hashCode() {
return mediaTrackGroup.hashCode() + 31 * trackIndices.hashCode();
}
// Bundleable implementation
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle());
bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndices));
return bundle;
}
/** Object that can restore {@code TrackSelectionOverride} from a {@link Bundle}. */
@UnstableApi
public static final Creator<TrackSelectionOverride> CREATOR =
bundle -> {
Bundle trackGroupBundle = checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP)));
TrackGroup mediaTrackGroup = TrackGroup.CREATOR.fromBundle(trackGroupBundle);
int[] tracks = checkNotNull(bundle.getIntArray(keyForField(FIELD_TRACKS)));
return new TrackSelectionOverride(mediaTrackGroup, Ints.asList(tracks));
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}

View File

@ -1,313 +0,0 @@
/*
* Copyright (C) 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.common;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.BundleableUtil.fromBundleNullableList;
import static androidx.media3.common.util.BundleableUtil.toBundleArrayList;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.util.Collections.max;
import static java.util.Collections.min;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.primitives.Ints;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Forces the selection of the specified tracks in {@link TrackGroup TrackGroups}.
*
* <p>Each {@link TrackSelectionOverride override} only affects the selection of tracks of that
* {@link C.TrackType type}. For example overriding the selection of an {@link C#TRACK_TYPE_AUDIO
* audio} {@link TrackGroup} will not affect the selection of {@link C#TRACK_TYPE_VIDEO video} or
* {@link C#TRACK_TYPE_TEXT text} tracks.
*
* <p>If multiple {@link TrackGroup TrackGroups} of the same {@link C.TrackType} are overridden,
* which tracks will be selected depend on the player capabilities. For example, by default {@code
* ExoPlayer} doesn't support selecting more than one {@link TrackGroup} per {@link C.TrackType}.
*
* <p>Overrides of {@link TrackGroup} that are not currently available are ignored. For example,
* when the player transitions to the next {@link MediaItem} in a playlist, any overrides of the
* previous {@link MediaItem} are ignored.
*
* @see TrackSelectionParameters#trackSelectionOverrides
*/
public final class TrackSelectionOverrides implements Bundleable {
/** Builder for {@link TrackSelectionOverrides}. */
public static final class Builder {
// Cannot use ImmutableMap.Builder as it doesn't support removing entries.
private final HashMap<TrackGroup, TrackSelectionOverride> overrides;
/** Creates an builder with no {@link TrackSelectionOverride}. */
public Builder() {
overrides = new HashMap<>();
}
private Builder(Map<TrackGroup, TrackSelectionOverride> overrides) {
this.overrides = new HashMap<>(overrides);
}
/** Adds an override for the provided {@link TrackGroup}. */
@UnstableApi
public Builder addOverride(TrackSelectionOverride override) {
overrides.put(override.trackGroup, override);
return this;
}
/** Removes the override associated with the provided {@link TrackGroup} if present. */
@UnstableApi
public Builder clearOverride(TrackGroup trackGroup) {
overrides.remove(trackGroup);
return this;
}
/** Set the override for the type of the provided {@link TrackGroup}. */
public Builder setOverrideForType(TrackSelectionOverride override) {
clearOverridesOfType(override.getTrackType());
overrides.put(override.trackGroup, override);
return this;
}
/**
* Remove any override associated with {@link TrackGroup TrackGroups} of type {@code trackType}.
*/
public Builder clearOverridesOfType(@C.TrackType int trackType) {
for (Iterator<TrackSelectionOverride> it = overrides.values().iterator(); it.hasNext(); ) {
TrackSelectionOverride trackSelectionOverride = it.next();
if (trackSelectionOverride.getTrackType() == trackType) {
it.remove();
}
}
return this;
}
/** Returns a new {@link TrackSelectionOverrides} instance with the current builder values. */
public TrackSelectionOverrides build() {
return new TrackSelectionOverrides(overrides);
}
}
/**
* Forces the selection of {@link #trackIndices} for a {@link TrackGroup}.
*
* <p>If multiple tracks in {@link #trackGroup} are overridden, as many as possible will be
* selected depending on the player capabilities.
*
* <p>If {@link #trackIndices} is empty, no tracks from {@link #trackGroup} will be played. This
* is similar to {@link TrackSelectionParameters#disabledTrackTypes}, except it will only affect
* the playback of the associated {@link TrackGroup}. For example, if the only {@link
* C#TRACK_TYPE_VIDEO} {@link TrackGroup} is associated with no tracks, no video will play until
* the next video starts.
*/
public static final class TrackSelectionOverride implements Bundleable {
/** The {@link TrackGroup} whose {@link #trackIndices} are forced to be selected. */
public final TrackGroup trackGroup;
/** The indices of tracks in a {@link TrackGroup} to be selected. */
public final ImmutableList<Integer> trackIndices;
/** Constructs an instance to force all tracks in {@code trackGroup} to be selected. */
public TrackSelectionOverride(TrackGroup trackGroup) {
this.trackGroup = trackGroup;
ImmutableList.Builder<Integer> builder = new ImmutableList.Builder<>();
for (int i = 0; i < trackGroup.length; i++) {
builder.add(i);
}
this.trackIndices = builder.build();
}
/**
* Constructs an instance to force {@code trackIndices} in {@code trackGroup} to be selected.
*
* @param trackGroup The {@link TrackGroup} for which to override the track selection.
* @param trackIndices The indices of the tracks in the {@link TrackGroup} to select.
*/
public TrackSelectionOverride(TrackGroup trackGroup, List<Integer> trackIndices) {
if (!trackIndices.isEmpty()) {
if (min(trackIndices) < 0 || max(trackIndices) >= trackGroup.length) {
throw new IndexOutOfBoundsException();
}
}
this.trackGroup = trackGroup;
this.trackIndices = ImmutableList.copyOf(trackIndices);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
TrackSelectionOverride that = (TrackSelectionOverride) obj;
return trackGroup.equals(that.trackGroup) && trackIndices.equals(that.trackIndices);
}
@Override
public int hashCode() {
return trackGroup.hashCode() + 31 * trackIndices.hashCode();
}
/** Returns the {@link C.TrackType} of the overriden track group. */
public @C.TrackType int getTrackType() {
return MimeTypes.getTrackType(trackGroup.getFormat(0).sampleMimeType);
}
// Bundleable implementation
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_TRACK_GROUP,
FIELD_TRACKS,
})
private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUP = 0;
private static final int FIELD_TRACKS = 1;
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), trackGroup.toBundle());
bundle.putIntArray(keyForField(FIELD_TRACKS), Ints.toArray(trackIndices));
return bundle;
}
/** Object that can restore {@code TrackSelectionOverride} from a {@link Bundle}. */
@UnstableApi
public static final Creator<TrackSelectionOverride> CREATOR =
bundle -> {
@Nullable Bundle trackGroupBundle = bundle.getBundle(keyForField(FIELD_TRACK_GROUP));
checkNotNull(trackGroupBundle); // Mandatory as there are no reasonable defaults.
TrackGroup trackGroup = TrackGroup.CREATOR.fromBundle(trackGroupBundle);
@Nullable int[] tracks = bundle.getIntArray(keyForField(FIELD_TRACKS));
if (tracks == null) {
return new TrackSelectionOverride(trackGroup);
}
return new TrackSelectionOverride(trackGroup, Ints.asList(tracks));
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}
/** Empty {@code TrackSelectionOverrides}, where no track selection is overridden. */
public static final TrackSelectionOverrides EMPTY =
new TrackSelectionOverrides(ImmutableMap.of());
private final ImmutableMap<TrackGroup, TrackSelectionOverride> overrides;
private TrackSelectionOverrides(Map<TrackGroup, TrackSelectionOverride> overrides) {
this.overrides = ImmutableMap.copyOf(overrides);
}
/** Returns a {@link Builder} initialized with the values of this instance. */
public Builder buildUpon() {
return new Builder(overrides);
}
/** Returns a list of the {@link TrackSelectionOverride overrides}. */
@UnstableApi
public ImmutableList<TrackSelectionOverride> asList() {
return ImmutableList.copyOf(overrides.values());
}
/**
* Returns the {@link TrackSelectionOverride} of the provided {@link TrackGroup} or {@code null}
* if there is none.
*/
@Nullable
public TrackSelectionOverride getOverride(TrackGroup trackGroup) {
return overrides.get(trackGroup);
}
@Override
public boolean equals(@Nullable Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
TrackSelectionOverrides that = (TrackSelectionOverrides) obj;
return overrides.equals(that.overrides);
}
@Override
public int hashCode() {
return overrides.hashCode();
}
// Bundleable implementation
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_OVERRIDES,
})
private @interface FieldNumber {}
private static final int FIELD_OVERRIDES = 0;
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_OVERRIDES), toBundleArrayList(overrides.values()));
return bundle;
}
/** Object that can restore {@code TrackSelectionOverrides} from a {@link Bundle}. */
@UnstableApi
public static final Creator<TrackSelectionOverrides> CREATOR =
bundle -> {
List<TrackSelectionOverride> trackSelectionOverrides =
fromBundleNullableList(
TrackSelectionOverride.CREATOR,
bundle.getParcelableArrayList(keyForField(FIELD_OVERRIDES)),
ImmutableList.of());
ImmutableMap.Builder<TrackGroup, TrackSelectionOverride> builder =
new ImmutableMap.Builder<>();
for (int i = 0; i < trackSelectionOverrides.size(); i++) {
TrackSelectionOverride trackSelectionOverride = trackSelectionOverrides.get(i);
builder.put(trackSelectionOverride.trackGroup, trackSelectionOverride);
}
return new TrackSelectionOverrides(builder.buildOrThrow());
};
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
}

View File

@ -16,37 +16,38 @@
package androidx.media3.common; 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.BundleableUtil.fromNullableBundle; import static androidx.media3.common.util.BundleableUtil.toBundleArrayList;
import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.MoreObjects.firstNonNull;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.content.Context; import android.content.Context;
import android.graphics.Point; import android.graphics.Point;
import android.os.Bundle; import android.os.Bundle;
import android.os.Looper; import android.os.Looper;
import android.view.accessibility.CaptioningManager; import android.view.accessibility.CaptioningManager;
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.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.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import java.lang.annotation.Documented; import java.util.HashMap;
import java.lang.annotation.Retention; import java.util.HashSet;
import java.lang.annotation.RetentionPolicy; import java.util.Iterator;
import java.lang.annotation.Target; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import org.checkerframework.checker.initialization.qual.UnknownInitialization; import org.checkerframework.checker.initialization.qual.UnknownInitialization;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
/** /**
* Constraint parameters for track selection. * Parameters for controlling track selection.
* *
* <p>For example the following code modifies the parameters to restrict video track selections to * <p>Parameters can be queried and set on a {@link Player}. For example the following code modifies
* SD, and to select a German audio track if there is one: * the parameters to restrict video track selections to SD, and to select a German audio track if
* there is one:
* *
* <pre>{@code * <pre>{@code
* // Build on the current parameters. * // Build on the current parameters.
@ -91,12 +92,13 @@ public class TrackSelectionParameters implements Bundleable {
// Text // Text
private ImmutableList<String> preferredTextLanguages; private ImmutableList<String> preferredTextLanguages;
private @C.RoleFlags int preferredTextRoleFlags; private @C.RoleFlags int preferredTextRoleFlags;
private @C.SelectionFlags int ignoredTextSelectionFlags;
private boolean selectUndeterminedTextLanguage; private boolean selectUndeterminedTextLanguage;
// General // General
private boolean forceLowestBitrate; private boolean forceLowestBitrate;
private boolean forceHighestSupportedBitrate; private boolean forceHighestSupportedBitrate;
private TrackSelectionOverrides trackSelectionOverrides; private HashMap<TrackGroup, TrackSelectionOverride> overrides;
private ImmutableSet<@C.TrackType Integer> disabledTrackTypes; private HashSet<@C.TrackType Integer> disabledTrackTypes;
/** /**
* @deprecated {@link Context} constraints will not be set using this constructor. Use {@link * @deprecated {@link Context} constraints will not be set using this constructor. Use {@link
@ -124,12 +126,13 @@ public class TrackSelectionParameters implements Bundleable {
// Text // Text
preferredTextLanguages = ImmutableList.of(); preferredTextLanguages = ImmutableList.of();
preferredTextRoleFlags = 0; preferredTextRoleFlags = 0;
ignoredTextSelectionFlags = 0;
selectUndeterminedTextLanguage = false; selectUndeterminedTextLanguage = false;
// General // General
forceLowestBitrate = false; forceLowestBitrate = false;
forceHighestSupportedBitrate = false; forceHighestSupportedBitrate = false;
trackSelectionOverrides = TrackSelectionOverrides.EMPTY; overrides = new HashMap<>();
disabledTrackTypes = ImmutableSet.of(); disabledTrackTypes = new HashSet<>();
} }
/** /**
@ -224,6 +227,10 @@ public class TrackSelectionParameters implements Bundleable {
bundle.getInt( bundle.getInt(
keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS), keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS),
DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags); DEFAULT_WITHOUT_CONTEXT.preferredTextRoleFlags);
ignoredTextSelectionFlags =
bundle.getInt(
keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS),
DEFAULT_WITHOUT_CONTEXT.ignoredTextSelectionFlags);
selectUndeterminedTextLanguage = selectUndeterminedTextLanguage =
bundle.getBoolean( bundle.getBoolean(
keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE), keyForField(FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE),
@ -236,16 +243,24 @@ public class TrackSelectionParameters implements Bundleable {
bundle.getBoolean( bundle.getBoolean(
keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE), keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE),
DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate); DEFAULT_WITHOUT_CONTEXT.forceHighestSupportedBitrate);
trackSelectionOverrides = @Nullable
fromNullableBundle( List<Bundle> overrideBundleList =
TrackSelectionOverrides.CREATOR, bundle.getParcelableArrayList(keyForField(FIELD_SELECTION_OVERRIDES));
bundle.getBundle(keyForField(FIELD_SELECTION_OVERRIDE_KEYS)), List<TrackSelectionOverride> overrideList =
TrackSelectionOverrides.EMPTY); overrideBundleList == null
disabledTrackTypes = ? ImmutableList.of()
ImmutableSet.copyOf( : BundleableUtil.fromBundleList(TrackSelectionOverride.CREATOR, overrideBundleList);
Ints.asList( overrides = new HashMap<>();
firstNonNull( for (int i = 0; i < overrideList.size(); i++) {
bundle.getIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE)), new int[0]))); TrackSelectionOverride override = overrideList.get(i);
overrides.put(override.mediaTrackGroup, override);
}
int[] disabledTrackTypeArray =
firstNonNull(bundle.getIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE)), new int[0]);
disabledTrackTypes = new HashSet<>();
for (@C.TrackType int disabledTrackType : disabledTrackTypeArray) {
disabledTrackTypes.add(disabledTrackType);
}
} }
/** Overrides the value of the builder with the value of {@link TrackSelectionParameters}. */ /** Overrides the value of the builder with the value of {@link TrackSelectionParameters}. */
@ -254,7 +269,7 @@ public class TrackSelectionParameters implements Bundleable {
"preferredAudioLanguages", "preferredAudioLanguages",
"preferredAudioMimeTypes", "preferredAudioMimeTypes",
"preferredTextLanguages", "preferredTextLanguages",
"trackSelectionOverrides", "overrides",
"disabledTrackTypes", "disabledTrackTypes",
}) })
private void init(@UnknownInitialization Builder this, TrackSelectionParameters parameters) { private void init(@UnknownInitialization Builder this, TrackSelectionParameters parameters) {
@ -281,12 +296,13 @@ public class TrackSelectionParameters implements Bundleable {
// Text // Text
preferredTextLanguages = parameters.preferredTextLanguages; preferredTextLanguages = parameters.preferredTextLanguages;
preferredTextRoleFlags = parameters.preferredTextRoleFlags; preferredTextRoleFlags = parameters.preferredTextRoleFlags;
ignoredTextSelectionFlags = parameters.ignoredTextSelectionFlags;
selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage; selectUndeterminedTextLanguage = parameters.selectUndeterminedTextLanguage;
// General // General
forceLowestBitrate = parameters.forceLowestBitrate; forceLowestBitrate = parameters.forceLowestBitrate;
forceHighestSupportedBitrate = parameters.forceHighestSupportedBitrate; forceHighestSupportedBitrate = parameters.forceHighestSupportedBitrate;
trackSelectionOverrides = parameters.trackSelectionOverrides; disabledTrackTypes = new HashSet<>(parameters.disabledTrackTypes);
disabledTrackTypes = parameters.disabledTrackTypes; overrides = new HashMap<>(parameters.overrides);
} }
/** Overrides the value of the builder with the value of {@link TrackSelectionParameters}. */ /** Overrides the value of the builder with the value of {@link TrackSelectionParameters}. */
@ -604,6 +620,18 @@ public class TrackSelectionParameters implements Bundleable {
return this; return this;
} }
/**
* Sets a bitmask of selection flags that are ignored for text track selections.
*
* @param ignoredTextSelectionFlags A bitmask of {@link C.SelectionFlags} that are ignored for
* text track selections.
* @return This builder.
*/
public Builder setIgnoredTextSelectionFlags(@C.SelectionFlags int ignoredTextSelectionFlags) {
this.ignoredTextSelectionFlags = ignoredTextSelectionFlags;
return this;
}
/** /**
* Sets whether a text track with undetermined language should be selected if no track with * Sets whether a text track with undetermined language should be selected if no track with
* {@link #setPreferredTextLanguages(String...) a preferred language} is available, or if the * {@link #setPreferredTextLanguages(String...) a preferred language} is available, or if the
@ -646,26 +674,73 @@ public class TrackSelectionParameters implements Bundleable {
return this; return this;
} }
/** /** Adds an override, replacing any override for the same {@link TrackGroup}. */
* Sets the selection overrides. public Builder addOverride(TrackSelectionOverride override) {
* overrides.put(override.mediaTrackGroup, override);
* @param trackSelectionOverrides The track selection overrides. return this;
* @return This builder. }
*/
public Builder setTrackSelectionOverrides(TrackSelectionOverrides trackSelectionOverrides) { /** Sets an override, replacing all existing overrides with the same track type. */
this.trackSelectionOverrides = trackSelectionOverrides; public Builder setOverrideForType(TrackSelectionOverride override) {
clearOverridesOfType(override.getType());
overrides.put(override.mediaTrackGroup, override);
return this;
}
/** Removes the override for the provided media {@link TrackGroup}, if there is one. */
public Builder clearOverride(TrackGroup mediaTrackGroup) {
overrides.remove(mediaTrackGroup);
return this;
}
/** Removes all overrides of the provided track type. */
public Builder clearOverridesOfType(@C.TrackType int trackType) {
Iterator<TrackSelectionOverride> it = overrides.values().iterator();
while (it.hasNext()) {
TrackSelectionOverride override = it.next();
if (override.getType() == trackType) {
it.remove();
}
}
return this;
}
/** Removes all overrides. */
public Builder clearOverrides() {
overrides.clear();
return this; return this;
} }
/** /**
* Sets the disabled track types, preventing all tracks of those types from being selected for * Sets the disabled track types, preventing all tracks of those types from being selected for
* playback. * playback. Any previously disabled track types are cleared.
* *
* @param disabledTrackTypes The track types to disable. * @param disabledTrackTypes The track types to disable.
* @return This builder. * @return This builder.
* @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}.
*/ */
@Deprecated
@UnstableApi
public Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) { public Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) {
this.disabledTrackTypes = ImmutableSet.copyOf(disabledTrackTypes); this.disabledTrackTypes.clear();
this.disabledTrackTypes.addAll(disabledTrackTypes);
return this;
}
/**
* Sets whether a track type is disabled. If disabled, no tracks of the specified type will be
* selected for playback.
*
* @param trackType The track type.
* @param disabled Whether the track type should be disabled.
* @return This builder.
*/
public Builder setTrackTypeDisabled(@C.TrackType int trackType, boolean disabled) {
if (disabled) {
disabledTrackTypes.add(trackType);
} else {
disabledTrackTypes.remove(trackType);
}
return this; return this;
} }
@ -842,6 +917,11 @@ public class TrackSelectionParameters implements Bundleable {
* is enabled. * is enabled.
*/ */
public final @C.RoleFlags int preferredTextRoleFlags; public final @C.RoleFlags int preferredTextRoleFlags;
/**
* Bitmask of selection flags that are ignored for text track selections. See {@link
* C.SelectionFlags}. The default value is {@code 0} (i.e., no flags are ignored).
*/
public final @C.SelectionFlags int ignoredTextSelectionFlags;
/** /**
* Whether a text track with undetermined language should be selected if no track with {@link * Whether a text track with undetermined language should be selected if no track with {@link
* #preferredTextLanguages} is available, or if {@link #preferredTextLanguages} is unset. The * #preferredTextLanguages} is available, or if {@link #preferredTextLanguages} is unset. The
@ -860,8 +940,9 @@ public class TrackSelectionParameters implements Bundleable {
*/ */
public final boolean forceHighestSupportedBitrate; public final boolean forceHighestSupportedBitrate;
/** Overrides to force tracks to be selected. */ /** Overrides to force selection of specific tracks. */
public final TrackSelectionOverrides trackSelectionOverrides; public final ImmutableMap<TrackGroup, TrackSelectionOverride> overrides;
/** /**
* The track types that are disabled. No track of a disabled type will be selected, thus no track * The track types that are disabled. No track of a disabled type will be selected, thus no track
* type contained in the set will be played. The default value is that no track type is disabled * type contained in the set will be played. The default value is that no track type is disabled
@ -894,12 +975,13 @@ public class TrackSelectionParameters implements Bundleable {
// Text // Text
this.preferredTextLanguages = builder.preferredTextLanguages; this.preferredTextLanguages = builder.preferredTextLanguages;
this.preferredTextRoleFlags = builder.preferredTextRoleFlags; this.preferredTextRoleFlags = builder.preferredTextRoleFlags;
this.ignoredTextSelectionFlags = builder.ignoredTextSelectionFlags;
this.selectUndeterminedTextLanguage = builder.selectUndeterminedTextLanguage; this.selectUndeterminedTextLanguage = builder.selectUndeterminedTextLanguage;
// General // General
this.forceLowestBitrate = builder.forceLowestBitrate; this.forceLowestBitrate = builder.forceLowestBitrate;
this.forceHighestSupportedBitrate = builder.forceHighestSupportedBitrate; this.forceHighestSupportedBitrate = builder.forceHighestSupportedBitrate;
this.trackSelectionOverrides = builder.trackSelectionOverrides; this.overrides = ImmutableMap.copyOf(builder.overrides);
this.disabledTrackTypes = builder.disabledTrackTypes; this.disabledTrackTypes = ImmutableSet.copyOf(builder.disabledTrackTypes);
} }
/** Creates a new {@link Builder}, copying the initial values from this instance. */ /** Creates a new {@link Builder}, copying the initial values from this instance. */
@ -937,13 +1019,15 @@ public class TrackSelectionParameters implements Bundleable {
&& maxAudioChannelCount == other.maxAudioChannelCount && maxAudioChannelCount == other.maxAudioChannelCount
&& maxAudioBitrate == other.maxAudioBitrate && maxAudioBitrate == other.maxAudioBitrate
&& preferredAudioMimeTypes.equals(other.preferredAudioMimeTypes) && preferredAudioMimeTypes.equals(other.preferredAudioMimeTypes)
// Text
&& preferredTextLanguages.equals(other.preferredTextLanguages) && preferredTextLanguages.equals(other.preferredTextLanguages)
&& preferredTextRoleFlags == other.preferredTextRoleFlags && preferredTextRoleFlags == other.preferredTextRoleFlags
&& ignoredTextSelectionFlags == other.ignoredTextSelectionFlags
&& selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage && selectUndeterminedTextLanguage == other.selectUndeterminedTextLanguage
// General // General
&& forceLowestBitrate == other.forceLowestBitrate && forceLowestBitrate == other.forceLowestBitrate
&& forceHighestSupportedBitrate == other.forceHighestSupportedBitrate && forceHighestSupportedBitrate == other.forceHighestSupportedBitrate
&& trackSelectionOverrides.equals(other.trackSelectionOverrides) && overrides.equals(other.overrides)
&& disabledTrackTypes.equals(other.disabledTrackTypes); && disabledTrackTypes.equals(other.disabledTrackTypes);
} }
@ -973,50 +1057,18 @@ public class TrackSelectionParameters implements Bundleable {
// Text // Text
result = 31 * result + preferredTextLanguages.hashCode(); result = 31 * result + preferredTextLanguages.hashCode();
result = 31 * result + preferredTextRoleFlags; result = 31 * result + preferredTextRoleFlags;
result = 31 * result + ignoredTextSelectionFlags;
result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0); result = 31 * result + (selectUndeterminedTextLanguage ? 1 : 0);
// General // General
result = 31 * result + (forceLowestBitrate ? 1 : 0); result = 31 * result + (forceLowestBitrate ? 1 : 0);
result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0); result = 31 * result + (forceHighestSupportedBitrate ? 1 : 0);
result = 31 * result + trackSelectionOverrides.hashCode(); result = 31 * result + overrides.hashCode();
result = 31 * result + disabledTrackTypes.hashCode(); result = 31 * result + disabledTrackTypes.hashCode();
return result; return result;
} }
// Bundleable implementation // Bundleable implementation
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
FIELD_PREFERRED_AUDIO_LANGUAGES,
FIELD_PREFERRED_AUDIO_ROLE_FLAGS,
FIELD_PREFERRED_TEXT_LANGUAGES,
FIELD_PREFERRED_TEXT_ROLE_FLAGS,
FIELD_SELECT_UNDETERMINED_TEXT_LANGUAGE,
FIELD_MAX_VIDEO_WIDTH,
FIELD_MAX_VIDEO_HEIGHT,
FIELD_MAX_VIDEO_FRAMERATE,
FIELD_MAX_VIDEO_BITRATE,
FIELD_MIN_VIDEO_WIDTH,
FIELD_MIN_VIDEO_HEIGHT,
FIELD_MIN_VIDEO_FRAMERATE,
FIELD_MIN_VIDEO_BITRATE,
FIELD_VIEWPORT_WIDTH,
FIELD_VIEWPORT_HEIGHT,
FIELD_VIEWPORT_ORIENTATION_MAY_CHANGE,
FIELD_PREFERRED_VIDEO_MIMETYPES,
FIELD_MAX_AUDIO_CHANNEL_COUNT,
FIELD_MAX_AUDIO_BITRATE,
FIELD_PREFERRED_AUDIO_MIME_TYPES,
FIELD_FORCE_LOWEST_BITRATE,
FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE,
FIELD_SELECTION_OVERRIDE_KEYS,
FIELD_SELECTION_OVERRIDE_VALUES,
FIELD_DISABLED_TRACK_TYPE,
FIELD_PREFERRED_VIDEO_ROLE_FLAGS
})
private @interface FieldNumber {}
private static final int FIELD_PREFERRED_AUDIO_LANGUAGES = 1; private static final int FIELD_PREFERRED_AUDIO_LANGUAGES = 1;
private static final int FIELD_PREFERRED_AUDIO_ROLE_FLAGS = 2; private static final int FIELD_PREFERRED_AUDIO_ROLE_FLAGS = 2;
private static final int FIELD_PREFERRED_TEXT_LANGUAGES = 3; private static final int FIELD_PREFERRED_TEXT_LANGUAGES = 3;
@ -1039,12 +1091,20 @@ public class TrackSelectionParameters implements Bundleable {
private static final int FIELD_PREFERRED_AUDIO_MIME_TYPES = 20; private static final int FIELD_PREFERRED_AUDIO_MIME_TYPES = 20;
private static final int FIELD_FORCE_LOWEST_BITRATE = 21; private static final int FIELD_FORCE_LOWEST_BITRATE = 21;
private static final int FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE = 22; private static final int FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE = 22;
private static final int FIELD_SELECTION_OVERRIDE_KEYS = 23; private static final int FIELD_SELECTION_OVERRIDES = 23;
private static final int FIELD_SELECTION_OVERRIDE_VALUES = 24; private static final int FIELD_DISABLED_TRACK_TYPE = 24;
private static final int FIELD_DISABLED_TRACK_TYPE = 25; private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 25;
private static final int FIELD_PREFERRED_VIDEO_ROLE_FLAGS = 26; private static final int FIELD_IGNORED_TEXT_SELECTION_FLAGS = 26;
/**
* Defines a minimum field ID value for subclasses to use when implementing {@link #toBundle()}
* and {@link Bundleable.Creator}.
*
* <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)}.
*/
@UnstableApi protected static final int FIELD_CUSTOM_ID_BASE = 1000;
@UnstableApi
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
@ -1080,25 +1140,41 @@ public class TrackSelectionParameters implements Bundleable {
bundle.putStringArray( bundle.putStringArray(
keyForField(FIELD_PREFERRED_TEXT_LANGUAGES), preferredTextLanguages.toArray(new String[0])); keyForField(FIELD_PREFERRED_TEXT_LANGUAGES), preferredTextLanguages.toArray(new String[0]));
bundle.putInt(keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS), preferredTextRoleFlags); bundle.putInt(keyForField(FIELD_PREFERRED_TEXT_ROLE_FLAGS), preferredTextRoleFlags);
bundle.putInt(keyForField(FIELD_IGNORED_TEXT_SELECTION_FLAGS), ignoredTextSelectionFlags);
bundle.putBoolean( bundle.putBoolean(
keyForField(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(keyForField(FIELD_FORCE_LOWEST_BITRATE), forceLowestBitrate);
bundle.putBoolean( bundle.putBoolean(
keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE), forceHighestSupportedBitrate); keyForField(FIELD_FORCE_HIGHEST_SUPPORTED_BITRATE), forceHighestSupportedBitrate);
bundle.putBundle( bundle.putParcelableArrayList(
keyForField(FIELD_SELECTION_OVERRIDE_KEYS), trackSelectionOverrides.toBundle()); keyForField(FIELD_SELECTION_OVERRIDES), toBundleArrayList(overrides.values()));
bundle.putIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE), Ints.toArray(disabledTrackTypes)); bundle.putIntArray(keyForField(FIELD_DISABLED_TRACK_TYPE), Ints.toArray(disabledTrackTypes));
return bundle; return bundle;
} }
/** Object that can restore {@code TrackSelectionParameters} from a {@link Bundle}. */ /** Construct an instance from a {@link Bundle} produced by {@link #toBundle()}. */
@UnstableApi public static TrackSelectionParameters fromBundle(Bundle bundle) {
public static final Creator<TrackSelectionParameters> CREATOR = return new Builder(bundle).build();
bundle -> new Builder(bundle).build(); }
private static String keyForField(@FieldNumber int field) { /**
* @deprecated Use {@link #fromBundle(Bundle)} instead.
*/
@UnstableApi @Deprecated
public static final Creator<TrackSelectionParameters> CREATOR =
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); return Integer.toString(field, Character.MAX_RADIX);
} }
} }

View File

@ -17,14 +17,13 @@ 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.fromBundleNullableList;
import static androidx.media3.common.util.BundleableUtil.fromNullableBundle;
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 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.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -37,49 +36,72 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
/** Information about groups of tracks. */ /** Information about groups of tracks. */
public final class TracksInfo implements Bundleable { public final class Tracks implements Bundleable {
/** /**
* Information about a single group of tracks, including the underlying {@link TrackGroup}, the * Information about a single group of tracks, including the underlying {@link TrackGroup}, the
* {@link C.TrackType type} of tracks it contains, and the level to which each track is supported * level to which each track is supported by the player, and whether any of the tracks are
* by the player. * selected.
*/ */
public static final class TrackGroupInfo implements Bundleable { public static final class Group implements Bundleable {
private final TrackGroup trackGroup;
/** The number of tracks in the group. */
public final int length;
private final TrackGroup mediaTrackGroup;
private final boolean adaptiveSupported;
private final @C.FormatSupport int[] trackSupport; private final @C.FormatSupport int[] trackSupport;
private final @C.TrackType int trackType;
private final boolean[] trackSelected; private final boolean[] trackSelected;
/** /**
* Constructs a TrackGroupInfo. * Constructs an instance.
* *
* @param trackGroup The {@link TrackGroup} described. * @param mediaTrackGroup The underlying {@link TrackGroup} defined by the media.
* @param trackSupport The {@link C.FormatSupport} of each track in the {@code trackGroup}. * @param adaptiveSupported Whether the player supports adaptive selections containing more than
* @param trackType The {@link C.TrackType} of the tracks in the {@code trackGroup}. * one track in the group.
* @param tracksSelected Whether a track is selected for each track in {@code trackGroup}. * @param trackSupport The {@link C.FormatSupport} of each track in the group.
* @param trackSelected Whether each track in the {@code trackGroup} is selected.
*/ */
@UnstableApi @UnstableApi
public TrackGroupInfo( public Group(
TrackGroup trackGroup, TrackGroup mediaTrackGroup,
boolean adaptiveSupported,
@C.FormatSupport int[] trackSupport, @C.FormatSupport int[] trackSupport,
@C.TrackType int trackType, boolean[] trackSelected) {
boolean[] tracksSelected) { length = mediaTrackGroup.length;
int length = trackGroup.length; checkArgument(length == trackSupport.length && length == trackSelected.length);
checkArgument(length == trackSupport.length && length == tracksSelected.length); this.mediaTrackGroup = mediaTrackGroup;
this.trackGroup = trackGroup; this.adaptiveSupported = adaptiveSupported && length > 1;
this.trackSupport = trackSupport.clone(); this.trackSupport = trackSupport.clone();
this.trackType = trackType; this.trackSelected = trackSelected.clone();
this.trackSelected = tracksSelected.clone();
} }
/** Returns the {@link TrackGroup} described by this {@code TrackGroupInfo}. */ /**
public TrackGroup getTrackGroup() { * Returns the underlying {@link TrackGroup} defined by the media.
return trackGroup; *
* <p>Unlike this class, {@link TrackGroup} only contains information defined by the media
* itself, and does not contain runtime information such as which tracks are supported and
* currently selected. This makes it suitable for use as a {@code key} in certain {@code (key,
* value)} data structures.
*/
public TrackGroup getMediaTrackGroup() {
return mediaTrackGroup;
}
/**
* Returns the {@link Format} for a specified track.
*
* @param trackIndex The index of the track in the group.
* @return The {@link Format} of the track.
*/
public Format getTrackFormat(int trackIndex) {
return mediaTrackGroup.getFormat(trackIndex);
} }
/** /**
* Returns the level of support for a specified track. * Returns the level of support for a specified track.
* *
* @param trackIndex The index of the track in the {@link TrackGroup}. * @param trackIndex The index of the track in the group.
* @return The {@link C.FormatSupport} of the track. * @return The {@link C.FormatSupport} of the track.
*/ */
@UnstableApi @UnstableApi
@ -91,7 +113,7 @@ public final class TracksInfo implements Bundleable {
* Returns whether a specified track is supported for playback, without exceeding the advertised * Returns whether a specified track is supported for playback, without exceeding the advertised
* capabilities of the device. Equivalent to {@code isTrackSupported(trackIndex, false)}. * capabilities of the device. Equivalent to {@code isTrackSupported(trackIndex, false)}.
* *
* @param trackIndex The index of the track in the {@link TrackGroup}. * @param trackIndex The index of the track in the group.
* @return True if the track's format can be played, false otherwise. * @return True if the track's format can be played, false otherwise.
*/ */
public boolean isTrackSupported(int trackIndex) { public boolean isTrackSupported(int trackIndex) {
@ -101,7 +123,7 @@ public final class TracksInfo implements Bundleable {
/** /**
* Returns whether a specified track is supported for playback. * Returns whether a specified track is supported for playback.
* *
* @param trackIndex The index of the track in the {@link TrackGroup}. * @param trackIndex The index of the track in the group.
* @param allowExceedsCapabilities Whether to consider the track as supported if it has a * @param allowExceedsCapabilities Whether to consider the track as supported if it has a
* supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised * supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
* capabilities of the device. For example, a video track for which there's a corresponding * capabilities of the device. For example, a video track for which there's a corresponding
@ -120,6 +142,11 @@ public final class TracksInfo implements Bundleable {
return Booleans.contains(trackSelected, true); return Booleans.contains(trackSelected, true);
} }
/** Returns whether adaptive selections containing more than one track are supported. */
public boolean isAdaptiveSupported() {
return adaptiveSupported;
}
/** /**
* Returns whether at least one track in the group is supported for playback, without exceeding * Returns whether at least one track in the group is supported for playback, without exceeding
* the advertised capabilities of the device. Equivalent to {@code isSupported(false)}. * the advertised capabilities of the device. Equivalent to {@code isSupported(false)}.
@ -157,7 +184,7 @@ public final class TracksInfo implements Bundleable {
* playing, however some player implementations have ways of getting such information. For * playing, however some player implementations have ways of getting such information. For
* example, ExoPlayer provides this information via {@code ExoTrackSelection.getSelectedFormat}. * example, ExoPlayer provides this information via {@code ExoTrackSelection.getSelectedFormat}.
* *
* @param trackIndex The index of the track in the {@link TrackGroup}. * @param trackIndex The index of the track in the group.
* @return True if the track is selected, false otherwise. * @return True if the track is selected, false otherwise.
*/ */
public boolean isTrackSelected(int trackIndex) { public boolean isTrackSelected(int trackIndex) {
@ -165,8 +192,8 @@ public final class TracksInfo implements Bundleable {
} }
/** Returns the {@link C.TrackType} of the group. */ /** Returns the {@link C.TrackType} of the group. */
public @C.TrackType int getTrackType() { public @C.TrackType int getType() {
return trackType; return mediaTrackGroup.type;
} }
@Override @Override
@ -177,18 +204,18 @@ public final class TracksInfo implements Bundleable {
if (other == null || getClass() != other.getClass()) { if (other == null || getClass() != other.getClass()) {
return false; return false;
} }
TrackGroupInfo that = (TrackGroupInfo) other; Group that = (Group) other;
return trackType == that.trackType return adaptiveSupported == that.adaptiveSupported
&& trackGroup.equals(that.trackGroup) && mediaTrackGroup.equals(that.mediaTrackGroup)
&& Arrays.equals(trackSupport, that.trackSupport) && Arrays.equals(trackSupport, that.trackSupport)
&& Arrays.equals(trackSelected, that.trackSelected); && Arrays.equals(trackSelected, that.trackSelected);
} }
@Override @Override
public int hashCode() { public int hashCode() {
int result = trackGroup.hashCode(); int result = mediaTrackGroup.hashCode();
result = 31 * result + (adaptiveSupported ? 1 : 0);
result = 31 * result + Arrays.hashCode(trackSupport); result = 31 * result + Arrays.hashCode(trackSupport);
result = 31 * result + trackType;
result = 31 * result + Arrays.hashCode(trackSelected); result = 31 * result + Arrays.hashCode(trackSelected);
return result; return result;
} }
@ -200,44 +227,44 @@ public final class TracksInfo implements Bundleable {
@IntDef({ @IntDef({
FIELD_TRACK_GROUP, FIELD_TRACK_GROUP,
FIELD_TRACK_SUPPORT, FIELD_TRACK_SUPPORT,
FIELD_TRACK_TYPE,
FIELD_TRACK_SELECTED, FIELD_TRACK_SELECTED,
FIELD_ADAPTIVE_SUPPORTED,
}) })
private @interface FieldNumber {} private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUP = 0; private static final int FIELD_TRACK_GROUP = 0;
private static final int FIELD_TRACK_SUPPORT = 1; private static final int FIELD_TRACK_SUPPORT = 1;
private static final int FIELD_TRACK_TYPE = 2;
private static final int FIELD_TRACK_SELECTED = 3; private static final int FIELD_TRACK_SELECTED = 3;
private static final int FIELD_ADAPTIVE_SUPPORTED = 4;
@Override @Override
public Bundle toBundle() { public Bundle toBundle() {
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.putBundle(keyForField(FIELD_TRACK_GROUP), trackGroup.toBundle()); bundle.putBundle(keyForField(FIELD_TRACK_GROUP), mediaTrackGroup.toBundle());
bundle.putIntArray(keyForField(FIELD_TRACK_SUPPORT), trackSupport); bundle.putIntArray(keyForField(FIELD_TRACK_SUPPORT), trackSupport);
bundle.putInt(keyForField(FIELD_TRACK_TYPE), trackType);
bundle.putBooleanArray(keyForField(FIELD_TRACK_SELECTED), trackSelected); bundle.putBooleanArray(keyForField(FIELD_TRACK_SELECTED), trackSelected);
bundle.putBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), adaptiveSupported);
return bundle; return bundle;
} }
/** Object that can restores a {@code TracksInfo} from a {@link Bundle}. */ /** Object that can restore a group of tracks from a {@link Bundle}. */
@UnstableApi @UnstableApi
public static final Creator<TrackGroupInfo> CREATOR = public static final Creator<Group> CREATOR =
bundle -> { bundle -> {
// Can't create a Tracks.Group without a TrackGroup
TrackGroup trackGroup = TrackGroup trackGroup =
fromNullableBundle( TrackGroup.CREATOR.fromBundle(
TrackGroup.CREATOR, bundle.getBundle(keyForField(FIELD_TRACK_GROUP))); checkNotNull(bundle.getBundle(keyForField(FIELD_TRACK_GROUP))));
checkNotNull(trackGroup); // Can't create a trackGroup info without a trackGroup
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(keyForField(FIELD_TRACK_SUPPORT)), new int[trackGroup.length]);
@C.TrackType
int trackType = bundle.getInt(keyForField(FIELD_TRACK_TYPE), C.TRACK_TYPE_UNKNOWN);
boolean[] selected = boolean[] selected =
MoreObjects.firstNonNull( MoreObjects.firstNonNull(
bundle.getBooleanArray(keyForField(FIELD_TRACK_SELECTED)), bundle.getBooleanArray(keyForField(FIELD_TRACK_SELECTED)),
new boolean[trackGroup.length]); new boolean[trackGroup.length]);
return new TrackGroupInfo(trackGroup, trackSupport, trackType, selected); boolean adaptiveSupported =
bundle.getBoolean(keyForField(FIELD_ADAPTIVE_SUPPORTED), false);
return new Group(trackGroup, adaptiveSupported, trackSupport, selected);
}; };
private static String keyForField(@FieldNumber int field) { private static String keyForField(@FieldNumber int field) {
@ -245,39 +272,52 @@ public final class TracksInfo implements Bundleable {
} }
} }
private final ImmutableList<TrackGroupInfo> trackGroupInfos; /** Empty tracks. */
public static final Tracks EMPTY = new Tracks(ImmutableList.of());
/** An {@code TrackInfo} that contains no tracks. */ private final ImmutableList<Group> groups;
@UnstableApi public static final TracksInfo EMPTY = new TracksInfo(ImmutableList.of());
/** /**
* Constructs an instance. * Constructs an instance.
* *
* @param trackGroupInfos The {@link TrackGroupInfo TrackGroupInfos} describing the groups of * @param groups The {@link Group groups} of tracks.
* tracks.
*/ */
@UnstableApi @UnstableApi
public TracksInfo(List<TrackGroupInfo> trackGroupInfos) { public Tracks(List<Group> groups) {
this.trackGroupInfos = ImmutableList.copyOf(trackGroupInfos); this.groups = ImmutableList.copyOf(groups);
} }
/** Returns the {@link TrackGroupInfo TrackGroupInfos} describing the groups of tracks. */ /** Returns the {@link Group groups} of tracks. */
public ImmutableList<TrackGroupInfo> getTrackGroupInfos() { public ImmutableList<Group> getGroups() {
return trackGroupInfos; return groups;
}
/** Returns {@code true} if there are no tracks, and {@code false} otherwise. */
public boolean isEmpty() {
return groups.isEmpty();
}
/** Returns true if there are tracks of type {@code trackType}, and false otherwise. */
public boolean containsType(@C.TrackType int trackType) {
for (int i = 0; i < groups.size(); i++) {
if (groups.get(i).getType() == trackType) {
return true;
}
}
return false;
} }
/** /**
* Returns true if at least one track of type {@code trackType} is {@link * Returns true if at least one track of type {@code trackType} is {@link
* TrackGroupInfo#isTrackSupported(int) supported} or if there are no tracks of this type. * Group#isTrackSupported(int) supported}.
*/ */
public boolean isTypeSupportedOrEmpty(@C.TrackType int trackType) { public boolean isTypeSupported(@C.TrackType int trackType) {
return isTypeSupportedOrEmpty(trackType, /* allowExceedsCapabilities= */ false); return isTypeSupported(trackType, /* allowExceedsCapabilities= */ false);
} }
/** /**
* Returns true if at least one track of type {@code trackType} is {@link * Returns true if at least one track of type {@code trackType} is {@link
* TrackGroupInfo#isTrackSupported(int, boolean) supported} or if there are no tracks of this * Group#isTrackSupported(int, boolean) supported}.
* type.
* *
* @param allowExceedsCapabilities Whether to consider the track as supported if it has a * @param allowExceedsCapabilities Whether to consider the track as supported if it has a
* supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised * supported {@link Format#sampleMimeType MIME type}, but otherwise exceeds the advertised
@ -285,26 +325,42 @@ public final class TracksInfo implements Bundleable {
* decoder whose maximum advertised resolution is exceeded by the resolution of the track. * decoder whose maximum advertised resolution is exceeded by the resolution of the track.
* Such tracks may be playable in some cases. * Such tracks may be playable in some cases.
*/ */
public boolean isTypeSupported(@C.TrackType int trackType, boolean allowExceedsCapabilities) {
for (int i = 0; i < groups.size(); i++) {
if (groups.get(i).getType() == trackType) {
if (groups.get(i).isSupported(allowExceedsCapabilities)) {
return true;
}
}
}
return false;
}
/**
* @deprecated Use {@link #containsType(int)} and {@link #isTypeSupported(int)}.
*/
@Deprecated
@UnstableApi
@SuppressWarnings("deprecation")
public boolean isTypeSupportedOrEmpty(@C.TrackType int trackType) {
return isTypeSupportedOrEmpty(trackType, /* allowExceedsCapabilities= */ false);
}
/**
* @deprecated Use {@link #containsType(int)} and {@link #isTypeSupported(int, boolean)}.
*/
@Deprecated
@UnstableApi
public boolean isTypeSupportedOrEmpty( public boolean isTypeSupportedOrEmpty(
@C.TrackType int trackType, boolean allowExceedsCapabilities) { @C.TrackType int trackType, boolean allowExceedsCapabilities) {
boolean supported = true; return !containsType(trackType) || isTypeSupported(trackType, allowExceedsCapabilities);
for (int i = 0; i < trackGroupInfos.size(); i++) {
if (trackGroupInfos.get(i).trackType == trackType) {
if (trackGroupInfos.get(i).isSupported(allowExceedsCapabilities)) {
return true;
} else {
supported = false;
}
}
}
return supported;
} }
/** Returns true if at least one track of the type {@code trackType} is selected for playback. */ /** Returns true if at least one track of the type {@code trackType} is selected for playback. */
public boolean isTypeSelected(@C.TrackType int trackType) { public boolean isTypeSelected(@C.TrackType int trackType) {
for (int i = 0; i < trackGroupInfos.size(); i++) { for (int i = 0; i < groups.size(); i++) {
TrackGroupInfo trackGroupInfo = trackGroupInfos.get(i); Group group = groups.get(i);
if (trackGroupInfo.isSelected() && trackGroupInfo.getTrackType() == trackType) { if (group.isSelected() && group.getType() == trackType) {
return true; return true;
} }
} }
@ -319,13 +375,13 @@ public final class TracksInfo implements Bundleable {
if (other == null || getClass() != other.getClass()) { if (other == null || getClass() != other.getClass()) {
return false; return false;
} }
TracksInfo that = (TracksInfo) other; Tracks that = (Tracks) other;
return trackGroupInfos.equals(that.trackGroupInfos); return groups.equals(that.groups);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return trackGroupInfos.hashCode(); return groups.hashCode();
} }
// Bundleable implementation. // Bundleable implementation.
@ -333,31 +389,31 @@ public final class TracksInfo implements Bundleable {
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE) @Target(TYPE_USE)
@IntDef({ @IntDef({
FIELD_TRACK_GROUP_INFOS, FIELD_TRACK_GROUPS,
}) })
private @interface FieldNumber {} private @interface FieldNumber {}
private static final int FIELD_TRACK_GROUP_INFOS = 0; 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( bundle.putParcelableArrayList(keyForField(FIELD_TRACK_GROUPS), toBundleArrayList(groups));
keyForField(FIELD_TRACK_GROUP_INFOS), toBundleArrayList(trackGroupInfos));
return bundle; return bundle;
} }
/** Object that can restore a {@code TracksInfo} from a {@link Bundle}. */ /** Object that can restore tracks from a {@link Bundle}. */
@UnstableApi @UnstableApi
public static final Creator<TracksInfo> CREATOR = public static final Creator<Tracks> CREATOR =
bundle -> { bundle -> {
List<TrackGroupInfo> trackGroupInfos = @Nullable
fromBundleNullableList( List<Bundle> groupBundles = bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUPS));
TrackGroupInfo.CREATOR, List<Group> groups =
bundle.getParcelableArrayList(keyForField(FIELD_TRACK_GROUP_INFOS)), groupBundles == null
/* defaultValue= */ ImmutableList.of()); ? ImmutableList.of()
return new TracksInfo(trackGroupInfos); : BundleableUtil.fromBundleList(Group.CREATOR, groupBundles);
return new Tracks(groups);
}; };
private static String keyForField(@FieldNumber int field) { private static String keyForField(@FieldNumber int field) {

View File

@ -0,0 +1,105 @@
/*
* 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.
*/
package androidx.media3.common.text;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.graphics.Bitmap;
import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
/** Class to represent the state of active {@link Cue Cues} at a particular time. */
public final class CueGroup implements Bundleable {
/** Empty {@link CueGroup}. */
@UnstableApi public static final CueGroup EMPTY = new CueGroup(ImmutableList.of());
/**
* The cues in this group.
*
* <p>This list is in ascending order of priority. If any of the cue boxes overlap when displayed,
* the {@link Cue} nearer the end of the list should be shown on top.
*
* <p>This list may be empty if the group represents a state with no cues.
*/
public final ImmutableList<Cue> cues;
/** Creates a CueGroup. */
@UnstableApi
public CueGroup(List<Cue> cues) {
this.cues = ImmutableList.copyOf(cues);
}
// Bundleable implementation.
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_CUES})
private @interface FieldNumber {}
private static final int FIELD_CUES = 0;
@UnstableApi
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_CUES), BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues)));
return bundle;
}
@UnstableApi public static final Creator<CueGroup> CREATOR = CueGroup::fromBundle;
private static final CueGroup fromBundle(Bundle bundle) {
@Nullable ArrayList<Bundle> cueBundles = bundle.getParcelableArrayList(keyForField(FIELD_CUES));
List<Cue> cues =
cueBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(Cue.CREATOR, cueBundles);
return new CueGroup(cues);
}
private static String keyForField(@FieldNumber int field) {
return Integer.toString(field, Character.MAX_RADIX);
}
/**
* Filters out {@link Cue} objects containing {@link Bitmap}. It is used when transferring cues
* between processes to prevent transferring too much data.
*/
private static ImmutableList<Cue> filterOutBitmapCues(List<Cue> cues) {
ImmutableList.Builder<Cue> builder = ImmutableList.builder();
for (int i = 0; i < cues.size(); i++) {
if (cues.get(i).bitmap != null) {
continue;
}
builder.add(cues.get(i));
}
return builder.build();
}
}

View File

@ -31,34 +31,6 @@ import java.util.List;
@UnstableApi @UnstableApi
public final class BundleableUtil { public final class BundleableUtil {
/**
* Converts a {@link Bundleable} to a {@link Bundle}. It's a convenience wrapper of {@link
* Bundleable#toBundle} that can take nullable values.
*/
@Nullable
public static Bundle toNullableBundle(@Nullable Bundleable bundleable) {
return bundleable == null ? null : bundleable.toBundle();
}
/**
* Converts a {@link Bundle} to a {@link Bundleable}. It's a convenience wrapper of {@link
* Bundleable.Creator#fromBundle} that can take nullable values.
*/
@Nullable
public static <T extends Bundleable> T fromNullableBundle(
Bundleable.Creator<T> creator, @Nullable Bundle bundle) {
return bundle == null ? null : creator.fromBundle(bundle);
}
/**
* Converts a {@link Bundle} to a {@link Bundleable}. It's a convenience wrapper of {@link
* Bundleable.Creator#fromBundle} that provides default value to ensure non-null.
*/
public static <T extends Bundleable> T fromNullableBundle(
Bundleable.Creator<T> creator, @Nullable Bundle bundle, T defaultValue) {
return bundle == null ? defaultValue : creator.fromBundle(bundle);
}
/** Converts a list of {@link Bundleable} to a list {@link Bundle}. */ /** Converts a list of {@link Bundleable} to a list {@link Bundle}. */
public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(List<T> bundleableList) { public static <T extends Bundleable> ImmutableList<Bundle> toBundleList(List<T> bundleableList) {
ImmutableList.Builder<Bundle> builder = ImmutableList.builder(); ImmutableList.Builder<Bundle> builder = ImmutableList.builder();
@ -81,34 +53,6 @@ public final class BundleableUtil {
return builder.build(); return builder.build();
} }
/**
* Converts a list of {@link Bundle} to a list of {@link Bundleable}. Returns {@code defaultValue}
* if {@code bundleList} is null.
*/
public static <T extends Bundleable> List<T> fromBundleNullableList(
Bundleable.Creator<T> creator, @Nullable List<Bundle> bundleList, List<T> defaultValue) {
return (bundleList == null) ? defaultValue : fromBundleList(creator, bundleList);
}
/**
* Converts a {@link SparseArray} of {@link Bundle} to a {@link SparseArray} of {@link
* Bundleable}. Returns {@code defaultValue} if {@code bundleSparseArray} is null.
*/
public static <T extends Bundleable> SparseArray<T> fromBundleNullableSparseArray(
Bundleable.Creator<T> creator,
@Nullable SparseArray<Bundle> bundleSparseArray,
SparseArray<T> defaultValue) {
if (bundleSparseArray == null) {
return defaultValue;
}
// Can't use ImmutableList as it doesn't support null elements.
SparseArray<T> result = new SparseArray<>(bundleSparseArray.size());
for (int i = 0; i < bundleSparseArray.size(); i++) {
result.put(bundleSparseArray.keyAt(i), creator.fromBundle(bundleSparseArray.valueAt(i)));
}
return result;
}
/** /**
* Converts a collection of {@link Bundleable} to an {@link ArrayList} of {@link Bundle} so that * Converts a collection of {@link Bundleable} to an {@link ArrayList} of {@link Bundle} so that
* the returned list can be put to {@link Bundle} using {@link Bundle#putParcelableArrayList} * the returned list can be put to {@link Bundle} using {@link Bundle#putParcelableArrayList}
@ -123,6 +67,19 @@ public final class BundleableUtil {
return arrayList; return arrayList;
} }
/**
* Converts a {@link SparseArray} of {@link Bundle} to a {@link SparseArray} of {@link
* Bundleable}.
*/
public static <T extends Bundleable> SparseArray<T> fromBundleSparseArray(
Bundleable.Creator<T> creator, SparseArray<Bundle> bundleSparseArray) {
SparseArray<T> result = new SparseArray<>(bundleSparseArray.size());
for (int i = 0; i < bundleSparseArray.size(); i++) {
result.put(bundleSparseArray.keyAt(i), creator.fromBundle(bundleSparseArray.valueAt(i)));
}
return result;
}
/** /**
* Converts a {@link SparseArray} of {@link Bundleable} to an {@link SparseArray} of {@link * Converts a {@link SparseArray} of {@link Bundleable} to an {@link SparseArray} of {@link
* Bundle} so that the returned {@link SparseArray} can be put to {@link Bundle} using {@link * Bundle} so that the returned {@link SparseArray} can be put to {@link Bundle} using {@link

View File

@ -36,10 +36,14 @@ public interface Clock {
*/ */
long currentTimeMillis(); long currentTimeMillis();
/** @see android.os.SystemClock#elapsedRealtime() */ /**
* @see android.os.SystemClock#elapsedRealtime()
*/
long elapsedRealtime(); long elapsedRealtime();
/** @see android.os.SystemClock#uptimeMillis() */ /**
* @see android.os.SystemClock#uptimeMillis()
*/
long uptimeMillis(); long uptimeMillis();
/** /**

View File

@ -15,6 +15,8 @@
*/ */
package androidx.media3.common.util; package androidx.media3.common.util;
import static androidx.media3.common.util.Assertions.checkArgument;
import android.util.Pair; import android.util.Pair;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.C; import androidx.media3.common.C;
@ -31,6 +33,12 @@ public final class CodecSpecificDataUtil {
private static final String[] HEVC_GENERAL_PROFILE_SPACE_STRINGS = private static final String[] HEVC_GENERAL_PROFILE_SPACE_STRINGS =
new String[] {"", "A", "B", "C"}; new String[] {"", "A", "B", "C"};
// MP4V-ES
private static final int VISUAL_OBJECT_LAYER = 1;
private static final int VISUAL_OBJECT_LAYER_START = 0x20;
private static final int EXTENDED_PAR = 0x0F;
private static final int RECTANGULAR = 0x00;
/** /**
* Parses an ALAC AudioSpecificConfig (i.e. an <a * Parses an ALAC AudioSpecificConfig (i.e. an <a
* href="https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt">ALACSpecificConfig</a>). * href="https://github.com/macosforge/alac/blob/master/ALACMagicCookieDescription.txt">ALACSpecificConfig</a>).
@ -72,6 +80,87 @@ public final class CodecSpecificDataUtil {
&& initializationData.get(0)[0] == 1; && initializationData.get(0)[0] == 1;
} }
/**
* Parses an MPEG-4 Visual configuration information, as defined in ISO/IEC14496-2.
*
* @param videoSpecificConfig A byte array containing the MPEG-4 Visual configuration information
* to parse.
* @return A pair of the video's width and height.
*/
public static Pair<Integer, Integer> getVideoResolutionFromMpeg4VideoConfig(
byte[] videoSpecificConfig) {
int offset = 0;
boolean foundVOL = false;
ParsableByteArray scratchBytes = new ParsableByteArray(videoSpecificConfig);
while (offset + 3 < videoSpecificConfig.length) {
if (scratchBytes.readUnsignedInt24() != VISUAL_OBJECT_LAYER
|| (videoSpecificConfig[offset + 3] & 0xF0) != VISUAL_OBJECT_LAYER_START) {
scratchBytes.setPosition(scratchBytes.getPosition() - 2);
offset++;
continue;
}
foundVOL = true;
break;
}
checkArgument(foundVOL, "Invalid input: VOL not found.");
ParsableBitArray scratchBits = new ParsableBitArray(videoSpecificConfig);
// Skip the start codecs from the bitstream
scratchBits.skipBits((offset + 4) * 8);
scratchBits.skipBits(1); // random_accessible_vol
scratchBits.skipBits(8); // video_object_type_indication
if (scratchBits.readBit()) { // object_layer_identifier
scratchBits.skipBits(4); // video_object_layer_verid
scratchBits.skipBits(3); // video_object_layer_priority
}
int aspectRatioInfo = scratchBits.readBits(4);
if (aspectRatioInfo == EXTENDED_PAR) {
scratchBits.skipBits(8); // par_width
scratchBits.skipBits(8); // par_height
}
if (scratchBits.readBit()) { // vol_control_parameters
scratchBits.skipBits(2); // chroma_format
scratchBits.skipBits(1); // low_delay
if (scratchBits.readBit()) { // vbv_parameters
scratchBits.skipBits(79);
}
}
int videoObjectLayerShape = scratchBits.readBits(2);
checkArgument(
videoObjectLayerShape == RECTANGULAR,
"Only supports rectangular video object layer shape.");
checkArgument(scratchBits.readBit()); // marker_bit
int vopTimeIncrementResolution = scratchBits.readBits(16);
checkArgument(scratchBits.readBit()); // marker_bit
if (scratchBits.readBit()) { // fixed_vop_rate
checkArgument(vopTimeIncrementResolution > 0);
vopTimeIncrementResolution--;
int numBitsToSkip = 0;
while (vopTimeIncrementResolution > 0) {
numBitsToSkip++;
vopTimeIncrementResolution >>= 1;
}
scratchBits.skipBits(numBitsToSkip); // fixed_vop_time_increment
}
checkArgument(scratchBits.readBit()); // marker_bit
int videoObjectLayerWidth = scratchBits.readBits(13);
checkArgument(scratchBits.readBit()); // marker_bit
int videoObjectLayerHeight = scratchBits.readBits(13);
checkArgument(scratchBits.readBit()); // marker_bit
scratchBits.skipBits(1); // interlaced
return Pair.create(videoObjectLayerWidth, videoObjectLayerHeight);
}
/** /**
* Builds an RFC 6381 AVC codec string using the provided parameters. * Builds an RFC 6381 AVC codec string using the provided parameters.
* *

View File

@ -0,0 +1,411 @@
/*
* 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.
*/
package androidx.media3.common.util;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.content.Context;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import androidx.annotation.Nullable;
import java.io.IOException;
import java.nio.Buffer;
import java.util.HashMap;
import java.util.Map;
/**
* Represents a GLSL shader program.
*
* <p>After constructing a program, keep a reference for its lifetime and call {@link #delete()} (or
* release the current GL context) when it's no longer needed.
*/
@UnstableApi
public final class GlProgram {
// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_YUV_target.txt
private static final int GL_SAMPLER_EXTERNAL_2D_Y2Y_EXT = 0x8BE7;
/** The identifier of a compiled and linked GLSL shader program. */
private final int programId;
private final Attribute[] attributes;
private final Uniform[] uniforms;
private final Map<String, Attribute> attributeByName;
private final Map<String, Uniform> uniformByName;
/**
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
*
* @param context The {@link Context}.
* @param vertexShaderFilePath The path to a vertex shader program.
* @param fragmentShaderFilePath The path to a fragment shader program.
* @throws IOException When failing to read shader files.
*/
public GlProgram(Context context, String vertexShaderFilePath, String fragmentShaderFilePath)
throws IOException {
this(
GlUtil.loadAsset(context, vertexShaderFilePath),
GlUtil.loadAsset(context, fragmentShaderFilePath));
}
/**
* Creates a GL shader program from vertex and fragment shader GLSL GLES20 code.
*
* <p>This involves slow steps, like compiling, linking, and switching the GL program, so do not
* call this in fast rendering loops.
*
* @param vertexShaderGlsl The vertex shader program.
* @param fragmentShaderGlsl The fragment shader program.
*/
public GlProgram(String vertexShaderGlsl, String fragmentShaderGlsl) {
programId = GLES20.glCreateProgram();
GlUtil.checkGlError();
// Add the vertex and fragment shaders.
addShader(programId, GLES20.GL_VERTEX_SHADER, vertexShaderGlsl);
addShader(programId, GLES20.GL_FRAGMENT_SHADER, fragmentShaderGlsl);
// Link and use the program, and enumerate attributes/uniforms.
GLES20.glLinkProgram(programId);
int[] linkStatus = new int[] {GLES20.GL_FALSE};
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, /* offset= */ 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
GlUtil.throwGlException(
"Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(programId));
}
GLES20.glUseProgram(programId);
attributeByName = new HashMap<>();
int[] attributeCount = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, /* offset= */ 0);
attributes = new Attribute[attributeCount[0]];
for (int i = 0; i < attributeCount[0]; i++) {
Attribute attribute = Attribute.create(programId, i);
attributes[i] = attribute;
attributeByName.put(attribute.name, attribute);
}
uniformByName = new HashMap<>();
int[] uniformCount = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, /* offset= */ 0);
uniforms = new Uniform[uniformCount[0]];
for (int i = 0; i < uniformCount[0]; i++) {
Uniform uniform = Uniform.create(programId, i);
uniforms[i] = uniform;
uniformByName.put(uniform.name, uniform);
}
GlUtil.checkGlError();
}
private static void addShader(int programId, int type, String glsl) {
int shader = GLES20.glCreateShader(type);
GLES20.glShaderSource(shader, glsl);
GLES20.glCompileShader(shader);
int[] result = new int[] {GLES20.GL_FALSE};
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, /* offset= */ 0);
if (result[0] != GLES20.GL_TRUE) {
GlUtil.throwGlException(GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
}
GLES20.glAttachShader(programId, shader);
GLES20.glDeleteShader(shader);
GlUtil.checkGlError();
}
private static int getAttributeLocation(int programId, String attributeName) {
return GLES20.glGetAttribLocation(programId, attributeName);
}
/** Returns the location of an {@link Attribute}. */
private int getAttributeLocation(String attributeName) {
return getAttributeLocation(programId, attributeName);
}
private static int getUniformLocation(int programId, String uniformName) {
return GLES20.glGetUniformLocation(programId, uniformName);
}
/** Returns the location of a {@link Uniform}. */
public int getUniformLocation(String uniformName) {
return getUniformLocation(programId, uniformName);
}
/**
* Uses the program.
*
* <p>Call this in the rendering loop to switch between different programs.
*/
public void use() {
GLES20.glUseProgram(programId);
GlUtil.checkGlError();
}
/** Deletes the program. Deleted programs cannot be used again. */
public void delete() {
GLES20.glDeleteProgram(programId);
GlUtil.checkGlError();
}
/**
* Returns the location of an {@link Attribute}, which has been enabled as a vertex attribute
* array.
*/
public int getAttributeArrayLocationAndEnable(String attributeName) {
int location = getAttributeLocation(attributeName);
GLES20.glEnableVertexAttribArray(location);
GlUtil.checkGlError();
return location;
}
/** Sets a float buffer type attribute. */
public void setBufferAttribute(String name, float[] values, int size) {
checkNotNull(attributeByName.get(name)).setBuffer(values, size);
}
/**
* Sets a texture sampler type uniform.
*
* @param name The uniform's name.
* @param texId The texture identifier.
* @param texUnitIndex The texture unit index. Use a different index (0, 1, 2, ...) for each
* texture sampler in the program.
*/
public void setSamplerTexIdUniform(String name, int texId, int texUnitIndex) {
checkNotNull(uniformByName.get(name)).setSamplerTexId(texId, texUnitIndex);
}
/** Sets a float type uniform. */
public void setFloatUniform(String name, float value) {
checkNotNull(uniformByName.get(name)).setFloat(value);
}
/** Sets a float array type uniform. */
public void setFloatsUniform(String name, float[] value) {
checkNotNull(uniformByName.get(name)).setFloats(value);
}
/** Binds all attributes and uniforms in the program. */
public void bindAttributesAndUniforms() {
for (Attribute attribute : attributes) {
attribute.bind();
}
for (Uniform uniform : uniforms) {
uniform.bind();
}
}
/** Returns the length of the null-terminated C string in {@code cString}. */
private static int getCStringLength(byte[] cString) {
for (int i = 0; i < cString.length; ++i) {
if (cString[i] == '\0') {
return i;
}
}
return cString.length;
}
/**
* GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}.
*/
private static final class Attribute {
/* Returns the attribute at the given index in the program. */
public static Attribute create(int programId, int index) {
int[] length = new int[1];
GLES20.glGetProgramiv(
programId, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, length, /* offset= */ 0);
byte[] nameBytes = new byte[length[0]];
GLES20.glGetActiveAttrib(
programId,
index,
length[0],
/* unusedLength */ new int[1],
/* lengthOffset= */ 0,
/* unusedSize */ new int[1],
/* sizeOffset= */ 0,
/* unusedType */ new int[1],
/* typeOffset= */ 0,
nameBytes,
/* nameOffset= */ 0);
String name = new String(nameBytes, /* offset= */ 0, getCStringLength(nameBytes));
int location = getAttributeLocation(programId, name);
return new Attribute(name, index, location);
}
/** The name of the attribute in the GLSL sources. */
public final String name;
private final int index;
private final int location;
@Nullable private Buffer buffer;
private int size;
private Attribute(String name, int index, int location) {
this.name = name;
this.index = index;
this.location = location;
}
/**
* Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size}
* elements) to this {@link Attribute}.
*
* @param buffer Buffer to bind to this attribute.
* @param size Number of elements per vertex.
*/
public void setBuffer(float[] buffer, int size) {
this.buffer = GlUtil.createBuffer(buffer);
this.size = size;
}
/**
* Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}.
*
* <p>Should be called before each drawing call.
*/
public void bind() {
Buffer buffer = checkNotNull(this.buffer, "call setBuffer before bind");
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, /* buffer= */ 0);
GLES20.glVertexAttribPointer(
location, size, GLES20.GL_FLOAT, /* normalized= */ false, /* stride= */ 0, buffer);
GLES20.glEnableVertexAttribArray(index);
GlUtil.checkGlError();
}
}
/**
* GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}.
*/
private static final class Uniform {
/** Returns the uniform at the given index in the program. */
public static Uniform create(int programId, int index) {
int[] length = new int[1];
GLES20.glGetProgramiv(
programId, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, length, /* offset= */ 0);
int[] type = new int[1];
byte[] nameBytes = new byte[length[0]];
GLES20.glGetActiveUniform(
programId,
index,
length[0],
/* unusedLength */ new int[1],
/* lengthOffset= */ 0,
/* unusedSize */ new int[1],
/*sizeOffset= */ 0,
type,
/* typeOffset= */ 0,
nameBytes,
/* nameOffset= */ 0);
String name = new String(nameBytes, /* offset= */ 0, getCStringLength(nameBytes));
int location = getUniformLocation(programId, name);
return new Uniform(name, location, type[0]);
}
/** The name of the uniform in the GLSL sources. */
public final String name;
private final int location;
private final int type;
private final float[] value;
private int texId;
private int texUnitIndex;
private Uniform(String name, int location, int type) {
this.name = name;
this.location = location;
this.type = type;
this.value = new float[16];
}
/**
* Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform.
*
* @param texId The GL texture identifier from which to sample.
* @param texUnitIndex The GL texture unit index.
*/
public void setSamplerTexId(int texId, int texUnitIndex) {
this.texId = texId;
this.texUnitIndex = texUnitIndex;
}
/** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */
public void setFloat(float value) {
this.value[0] = value;
}
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
public void setFloats(float[] value) {
System.arraycopy(value, /* srcPos= */ 0, this.value, /* destPos= */ 0, value.length);
}
/**
* Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)}, {@link
* #setFloat(float)} or {@link #setFloats(float[])}.
*
* <p>Should be called before each drawing call.
*/
public void bind() {
switch (type) {
case GLES20.GL_FLOAT:
GLES20.glUniform1fv(location, /* count= */ 1, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_VEC2:
GLES20.glUniform2fv(location, /* count= */ 1, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_VEC3:
GLES20.glUniform3fv(location, /* count= */ 1, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_MAT3:
GLES20.glUniformMatrix3fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_MAT4:
GLES20.glUniformMatrix4fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_SAMPLER_2D:
case GLES11Ext.GL_SAMPLER_EXTERNAL_OES:
case GL_SAMPLER_EXTERNAL_2D_Y2Y_EXT:
if (texId == 0) {
throw new IllegalStateException("No call to setSamplerTexId() before bind.");
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + texUnitIndex);
GlUtil.checkGlError();
GlUtil.bindTexture(
type == GLES20.GL_SAMPLER_2D
? GLES20.GL_TEXTURE_2D
: GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
texId);
GLES20.glUniform1i(location, texUnitIndex);
GlUtil.checkGlError();
break;
default:
throw new IllegalStateException("Unexpected uniform type: " + type);
}
}
}
}

View File

@ -16,7 +16,6 @@
package androidx.media3.common.util; package androidx.media3.common.util;
import static android.opengl.GLU.gluErrorString; import static android.opengl.GLU.gluErrorString;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
@ -33,13 +32,10 @@ import androidx.annotation.RequiresApi;
import androidx.media3.common.C; import androidx.media3.common.C;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.Buffer;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.FloatBuffer; import java.nio.FloatBuffer;
import java.nio.IntBuffer; import java.util.List;
import java.util.HashMap;
import java.util.Map;
import javax.microedition.khronos.egl.EGL10; import javax.microedition.khronos.egl.EGL10;
/** OpenGL ES utilities. */ /** OpenGL ES utilities. */
@ -55,157 +51,16 @@ public final class GlUtil {
} }
} }
/** // TODO(b/231937416): Consider removing this flag, enabling assertions by default, and making
* Represents a GLSL shader program. // GlException checked.
*
* <p>After constructing a program, keep a reference for its lifetime and call {@link #delete()}
* (or release the current GL context) when it's no longer needed.
*/
public static final class Program {
/** The identifier of a compiled and linked GLSL shader program. */
private final int programId;
private final Attribute[] attributes;
private final Uniform[] uniforms;
private final Map<String, Attribute> attributeByName;
private final Map<String, Uniform> uniformByName;
/**
* Compiles a GL shader program from vertex and fragment shader GLSL GLES20 code.
*
* @param context The {@link Context}.
* @param vertexShaderFilePath The path to a vertex shader program.
* @param fragmentShaderFilePath The path to a fragment shader program.
* @throws IOException When failing to read shader files.
*/
public Program(Context context, String vertexShaderFilePath, String fragmentShaderFilePath)
throws IOException {
this(loadAsset(context, vertexShaderFilePath), loadAsset(context, fragmentShaderFilePath));
}
/**
* Creates a GL shader program from vertex and fragment shader GLSL GLES20 code.
*
* <p>This involves slow steps, like compiling, linking, and switching the GL program, so do not
* call this in fast rendering loops.
*
* @param vertexShaderGlsl The vertex shader program.
* @param fragmentShaderGlsl The fragment shader program.
*/
public Program(String vertexShaderGlsl, String fragmentShaderGlsl) {
programId = GLES20.glCreateProgram();
checkGlError();
// Add the vertex and fragment shaders.
addShader(programId, GLES20.GL_VERTEX_SHADER, vertexShaderGlsl);
addShader(programId, GLES20.GL_FRAGMENT_SHADER, fragmentShaderGlsl);
// Link and use the program, and enumerate attributes/uniforms.
GLES20.glLinkProgram(programId);
int[] linkStatus = new int[] {GLES20.GL_FALSE};
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, /* offset= */ 0);
if (linkStatus[0] != GLES20.GL_TRUE) {
throwGlException(
"Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(programId));
}
GLES20.glUseProgram(programId);
attributeByName = new HashMap<>();
int[] attributeCount = new int[1];
GLES20.glGetProgramiv(
programId, GLES20.GL_ACTIVE_ATTRIBUTES, attributeCount, /* offset= */ 0);
attributes = new Attribute[attributeCount[0]];
for (int i = 0; i < attributeCount[0]; i++) {
Attribute attribute = Attribute.create(programId, i);
attributes[i] = attribute;
attributeByName.put(attribute.name, attribute);
}
uniformByName = new HashMap<>();
int[] uniformCount = new int[1];
GLES20.glGetProgramiv(programId, GLES20.GL_ACTIVE_UNIFORMS, uniformCount, /* offset= */ 0);
uniforms = new Uniform[uniformCount[0]];
for (int i = 0; i < uniformCount[0]; i++) {
Uniform uniform = Uniform.create(programId, i);
uniforms[i] = uniform;
uniformByName.put(uniform.name, uniform);
}
checkGlError();
}
/**
* Uses the program.
*
* <p>Call this in the rendering loop to switch between different programs.
*/
public void use() {
// TODO(http://b/205002913): When multiple GL programs are supported by Transformer, make sure
// to call use() to switch between programs.
GLES20.glUseProgram(programId);
checkGlError();
}
/** Deletes the program. Deleted programs cannot be used again. */
public void delete() {
GLES20.glDeleteProgram(programId);
checkGlError();
}
/**
* Returns the location of an {@link Attribute}, which has been enabled as a vertex attribute
* array.
*/
public int getAttributeArrayLocationAndEnable(String attributeName) {
int location = getAttributeLocation(attributeName);
GLES20.glEnableVertexAttribArray(location);
checkGlError();
return location;
}
/** Returns the location of an {@link Attribute}. */
private int getAttributeLocation(String attributeName) {
return GlUtil.getAttributeLocation(programId, attributeName);
}
/** Returns the location of a {@link Uniform}. */
public int getUniformLocation(String uniformName) {
return GlUtil.getUniformLocation(programId, uniformName);
}
/** Sets a float buffer type attribute. */
public void setBufferAttribute(String name, float[] values, int size) {
checkNotNull(attributeByName.get(name)).setBuffer(values, size);
}
/** Sets a texture sampler type uniform. */
public void setSamplerTexIdUniform(String name, int texId, int unit) {
checkNotNull(uniformByName.get(name)).setSamplerTexId(texId, unit);
}
/** Sets a float type uniform. */
public void setFloatUniform(String name, float value) {
checkNotNull(uniformByName.get(name)).setFloat(value);
}
/** Sets a float array type uniform. */
public void setFloatsUniform(String name, float[] value) {
checkNotNull(uniformByName.get(name)).setFloats(value);
}
/** Binds all attributes and uniforms in the program. */
public void bindAttributesAndUniforms() {
for (Attribute attribute : attributes) {
attribute.bind();
}
for (Uniform uniform : uniforms) {
uniform.bind();
}
}
}
/** Whether to throw a {@link GlException} in case of an OpenGL error. */ /** Whether to throw a {@link GlException} in case of an OpenGL error. */
public static boolean glAssertionsEnabled = false; public static boolean glAssertionsEnabled = false;
/** Number of vertices in a rectangle. */ /** Number of elements in a 3d homogeneous coordinate vector describing a vertex. */
public static final int RECTANGLE_VERTICES_COUNT = 4; public static final int HOMOGENEOUS_COORDINATE_VECTOR_SIZE = 4;
/** Length of the normalized device coordinate (NDC) space, which spans from -1 to 1. */
public static final float LENGTH_NDC = 2f;
private static final String TAG = "GlUtil"; private static final String TAG = "GlUtil";
@ -214,8 +69,6 @@ public final class GlUtil {
// https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_surfaceless_context.txt // https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_surfaceless_context.txt
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context"; private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_YUV_target.txt
private static final int GL_SAMPLER_EXTERNAL_2D_Y2Y_EXT = 0x8BE7;
// https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt // https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt
private static final int EGL_GL_COLORSPACE_KHR = 0x309D; private static final int EGL_GL_COLORSPACE_KHR = 0x309D;
// https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt // https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt
@ -270,9 +123,24 @@ public final class GlUtil {
}; };
} }
/** Flattens the list of 4 element NDC coordinate vectors into a buffer. */
public static float[] createVertexBuffer(List<float[]> vertexList) {
float[] vertexBuffer = new float[HOMOGENEOUS_COORDINATE_VECTOR_SIZE * vertexList.size()];
for (int i = 0; i < vertexList.size(); i++) {
System.arraycopy(
/* src= */ vertexList.get(i),
/* srcPos= */ 0,
/* dest= */ vertexBuffer,
/* destPos= */ HOMOGENEOUS_COORDINATE_VECTOR_SIZE * i,
/* length= */ HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
}
return vertexBuffer;
}
/** /**
* Returns whether creating a GL context with {@value #EXTENSION_PROTECTED_CONTENT} is possible. * Returns whether creating a GL context with {@value #EXTENSION_PROTECTED_CONTENT} is possible.
* If {@code true}, the device supports a protected output path for DRM content when using GL. *
* <p>If {@code true}, the device supports a protected output path for DRM content when using GL.
*/ */
public static boolean isProtectedContentExtensionSupported(Context context) { public static boolean isProtectedContentExtensionSupported(Context context) {
if (Util.SDK_INT < 24) { if (Util.SDK_INT < 24) {
@ -299,7 +167,11 @@ public final class GlUtil {
} }
/** /**
* Returns whether creating a GL context with {@value #EXTENSION_SURFACELESS_CONTEXT} is possible. * Returns whether the {@value #EXTENSION_SURFACELESS_CONTEXT} extension is supported.
*
* <p>This extension allows passing {@link EGL14#EGL_NO_SURFACE} for both the write and read
* surfaces in a call to {@link EGL14#eglMakeCurrent(EGLDisplay, EGLSurface, EGLSurface,
* EGLContext)}.
*/ */
public static boolean isSurfacelessContextExtensionSupported() { public static boolean isSurfacelessContextExtensionSupported() {
if (Util.SDK_INT < 17) { if (Util.SDK_INT < 17) {
@ -359,6 +231,77 @@ public final class GlUtil {
EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ); EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ);
} }
/**
* Creates a new {@link EGLSurface} wrapping a pixel buffer.
*
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @param width The width of the pixel buffer.
* @param height The height of the pixel buffer.
*/
@RequiresApi(17)
private static EGLSurface createPbufferSurface(EGLDisplay eglDisplay, int width, int height) {
int[] pbufferAttributes =
new int[] {
EGL14.EGL_WIDTH, width,
EGL14.EGL_HEIGHT, height,
EGL14.EGL_NONE
};
return Api17.createEglPbufferSurface(
eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888, pbufferAttributes);
}
/**
* Returns a placeholder {@link EGLSurface} to use when reading and writing to the surface is not
* required.
*
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @return {@link EGL14#EGL_NO_SURFACE} if supported and a 1x1 pixel buffer surface otherwise.
*/
@RequiresApi(17)
public static EGLSurface createPlaceholderEglSurface(EGLDisplay eglDisplay) {
return isSurfacelessContextExtensionSupported()
? EGL14.EGL_NO_SURFACE
: createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1);
}
/**
* Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer.
*
* @param eglContext The {@link EGLContext} to make current.
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
*/
@RequiresApi(17)
public static void focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay) {
EGLSurface eglSurface = createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1);
focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1);
}
/**
* Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer, for HDR rendering
* with Rec. 2020 color primaries and using the PQ transfer function.
*
* @param eglContext The {@link EGLContext} to make current.
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
*/
@RequiresApi(17)
public static void focusPlaceholderEglSurfaceBt2020Pq(
EGLContext eglContext, EGLDisplay eglDisplay) {
int[] pbufferAttributes =
new int[] {
EGL14.EGL_WIDTH,
/* width= */ 1,
EGL14.EGL_HEIGHT,
/* height= */ 1,
EGL_GL_COLORSPACE_KHR,
EGL_GL_COLORSPACE_BT2020_PQ_EXT,
EGL14.EGL_NONE
};
EGLSurface eglSurface =
Api17.createEglPbufferSurface(
eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_1010102, pbufferAttributes);
focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1);
}
/** /**
* If there is an OpenGl error, logs the error and if {@link #glAssertionsEnabled} is true throws * If there is an OpenGl error, logs the error and if {@link #glAssertionsEnabled} is true throws
* a {@link GlException}. * a {@link GlException}.
@ -376,13 +319,54 @@ public final class GlUtil {
} }
/** /**
* Makes the specified {@code surface} the render target, using a viewport of {@code width} by * Asserts the texture size is valid.
*
* @param width The width for a texture.
* @param height The height for a texture.
* @throws GlException If the texture width or height is invalid.
*/
public static void assertValidTextureSize(int width, int height) {
// TODO(b/201293185): Consider handling adjustments for sizes > GL_MAX_TEXTURE_SIZE
// (ex. downscaling appropriately) in a texture processor instead of asserting incorrect
// values.
// For valid GL sizes, see:
// https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml
int[] maxTextureSizeBuffer = new int[1];
GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeBuffer, 0);
int maxTextureSize = maxTextureSizeBuffer[0];
if (width < 0 || height < 0) {
throwGlException("width or height is less than 0");
}
if (width > maxTextureSize || height > maxTextureSize) {
throwGlException("width or height is greater than GL_MAX_TEXTURE_SIZE " + maxTextureSize);
}
}
/**
* Makes the specified {@code eglSurface} the render target, using a viewport of {@code width} by
* {@code height} pixels. * {@code height} pixels.
*/ */
@RequiresApi(17) @RequiresApi(17)
public static void focusSurface( public static void focusEglSurface(
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface surface, int width, int height) { EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface eglSurface, int width, int height) {
Api17.focusSurface(eglDisplay, eglContext, surface, width, height); Api17.focusRenderTarget(
eglDisplay, eglContext, eglSurface, /* framebuffer= */ 0, width, height);
}
/**
* Makes the specified {@code framebuffer} the render target, using a viewport of {@code width} by
* {@code height} pixels.
*/
@RequiresApi(17)
public static void focusFramebuffer(
EGLDisplay eglDisplay,
EGLContext eglContext,
EGLSurface eglSurface,
int framebuffer,
int width,
int height) {
Api17.focusRenderTarget(eglDisplay, eglContext, eglSurface, framebuffer, width, height);
} }
/** /**
@ -447,49 +431,94 @@ public final class GlUtil {
* GL_CLAMP_TO_EDGE wrapping. * GL_CLAMP_TO_EDGE wrapping.
*/ */
public static int createExternalTexture() { public static int createExternalTexture() {
int texId = generateTexture();
bindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
return texId;
}
/**
* Returns the texture identifier for a newly-allocated texture with the specified dimensions.
*
* @param width of the new texture in pixels
* @param height of the new texture in pixels
*/
public static int createTexture(int width, int height) {
assertValidTextureSize(width, height);
int texId = generateTexture();
bindTexture(GLES20.GL_TEXTURE_2D, texId);
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(width * height * 4);
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D,
/* level= */ 0,
GLES20.GL_RGBA,
width,
height,
/* border= */ 0,
GLES20.GL_RGBA,
GLES20.GL_UNSIGNED_BYTE,
byteBuffer);
checkGlError();
return texId;
}
/** Returns a new GL texture identifier. */
private static int generateTexture() {
checkEglException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] texId = new int[1]; int[] texId = new int[1];
GLES20.glGenTextures(/* n= */ 1, IntBuffer.wrap(texId)); GLES20.glGenTextures(/* n= */ 1, texId, /* offset= */ 0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[0]);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(
GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
checkGlError(); checkGlError();
return texId[0]; return texId[0];
} }
private static void addShader(int programId, int type, String glsl) { /**
int shader = GLES20.glCreateShader(type); * Binds the texture of the given type with default configuration of GL_LINEAR filtering and
GLES20.glShaderSource(shader, glsl); * GL_CLAMP_TO_EDGE wrapping.
GLES20.glCompileShader(shader); *
* @param texId The texture identifier.
int[] result = new int[] {GLES20.GL_FALSE}; * @param textureTarget The target to which the texture is bound, e.g. {@link
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, /* offset= */ 0); * GLES20#GL_TEXTURE_2D} for a two-dimensional texture or {@link
if (result[0] != GLES20.GL_TRUE) { * GLES11Ext#GL_TEXTURE_EXTERNAL_OES} for an external texture.
throwGlException(GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl); */
} public static void bindTexture(int textureTarget, int texId) {
GLES20.glBindTexture(textureTarget, texId);
GLES20.glAttachShader(programId, shader); checkGlError();
GLES20.glDeleteShader(shader); GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
checkGlError();
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
checkGlError();
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
checkGlError();
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
checkGlError(); checkGlError();
} }
private static int getAttributeLocation(int programId, String attributeName) { /**
return GLES20.glGetAttribLocation(programId, attributeName); * Returns a new framebuffer for the texture.
*
* @param texId The identifier of the texture to attach to the framebuffer.
*/
public static int createFboForTexture(int texId) {
checkEglException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] fboId = new int[1];
GLES20.glGenFramebuffers(/* n= */ 1, fboId, /* offset= */ 0);
checkGlError();
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fboId[0]);
checkGlError();
GLES20.glFramebufferTexture2D(
GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, texId, 0);
checkGlError();
return fboId[0];
} }
private static int getUniformLocation(int programId, String uniformName) { /* package */ static void throwGlException(String errorMsg) {
return GLES20.glGetUniformLocation(programId, uniformName);
}
private static void throwGlException(String errorMsg) {
Log.e(TAG, errorMsg);
if (glAssertionsEnabled) { if (glAssertionsEnabled) {
throw new GlException(errorMsg); throw new GlException(errorMsg);
} else {
Log.e(TAG, errorMsg);
} }
} }
@ -499,205 +528,9 @@ public final class GlUtil {
} }
} }
/** Returns the length of the null-terminated string in {@code strVal}. */ private static void checkEglException(String errorMessage) {
private static int strlen(byte[] strVal) { int error = EGL14.eglGetError();
for (int i = 0; i < strVal.length; ++i) { checkEglException(error == EGL14.EGL_SUCCESS, errorMessage + ", error code: " + error);
if (strVal[i] == '\0') {
return i;
}
}
return strVal.length;
}
/**
* GL attribute, which can be attached to a buffer with {@link Attribute#setBuffer(float[], int)}.
*/
private static final class Attribute {
/* Returns the attribute at the given index in the program. */
public static Attribute create(int programId, int index) {
int[] length = new int[1];
GLES20.glGetProgramiv(
programId, GLES20.GL_ACTIVE_ATTRIBUTE_MAX_LENGTH, length, /* offset= */ 0);
byte[] nameBytes = new byte[length[0]];
GLES20.glGetActiveAttrib(
programId,
index,
length[0],
/* unusedLength */ new int[1],
/* lengthOffset= */ 0,
/* unusedSize */ new int[1],
/* sizeOffset= */ 0,
/* unusedType */ new int[1],
/* typeOffset= */ 0,
nameBytes,
/* nameOffset= */ 0);
String name = new String(nameBytes, /* offset= */ 0, strlen(nameBytes));
int location = getAttributeLocation(programId, name);
return new Attribute(name, index, location);
}
/** The name of the attribute in the GLSL sources. */
public final String name;
private final int index;
private final int location;
@Nullable private Buffer buffer;
private int size;
private Attribute(String name, int index, int location) {
this.name = name;
this.index = index;
this.location = location;
}
/**
* Configures {@link #bind()} to attach vertices in {@code buffer} (each of size {@code size}
* elements) to this {@link Attribute}.
*
* @param buffer Buffer to bind to this attribute.
* @param size Number of elements per vertex.
*/
public void setBuffer(float[] buffer, int size) {
this.buffer = createBuffer(buffer);
this.size = size;
}
/**
* Sets the vertex attribute to whatever was attached via {@link #setBuffer(float[], int)}.
*
* <p>Should be called before each drawing call.
*/
public void bind() {
Buffer buffer = checkNotNull(this.buffer, "call setBuffer before bind");
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, /* buffer= */ 0);
GLES20.glVertexAttribPointer(
location, size, GLES20.GL_FLOAT, /* normalized= */ false, /* stride= */ 0, buffer);
GLES20.glEnableVertexAttribArray(index);
checkGlError();
}
}
/**
* GL uniform, which can be attached to a sampler using {@link Uniform#setSamplerTexId(int, int)}.
*/
private static final class Uniform {
/** Returns the uniform at the given index in the program. */
public static Uniform create(int programId, int index) {
int[] length = new int[1];
GLES20.glGetProgramiv(
programId, GLES20.GL_ACTIVE_UNIFORM_MAX_LENGTH, length, /* offset= */ 0);
int[] type = new int[1];
byte[] nameBytes = new byte[length[0]];
GLES20.glGetActiveUniform(
programId,
index,
length[0],
/* unusedLength */ new int[1],
/* lengthOffset= */ 0,
/* unusedSize */ new int[1],
/*sizeOffset= */ 0,
type,
/* typeOffset= */ 0,
nameBytes,
/* nameOffset= */ 0);
String name = new String(nameBytes, /* offset= */ 0, strlen(nameBytes));
int location = getUniformLocation(programId, name);
return new Uniform(name, location, type[0]);
}
/** The name of the uniform in the GLSL sources. */
public final String name;
private final int location;
private final int type;
private final float[] value;
private int texId;
private int unit;
private Uniform(String name, int location, int type) {
this.name = name;
this.location = location;
this.type = type;
this.value = new float[16];
}
/**
* Configures {@link #bind()} to use the specified {@code texId} for this sampler uniform.
*
* @param texId The GL texture identifier from which to sample.
* @param unit The GL texture unit index.
*/
public void setSamplerTexId(int texId, int unit) {
this.texId = texId;
this.unit = unit;
}
/** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */
public void setFloat(float value) {
this.value[0] = value;
}
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
public void setFloats(float[] value) {
System.arraycopy(value, /* srcPos= */ 0, this.value, /* destPos= */ 0, value.length);
}
/**
* Sets the uniform to whatever value was passed via {@link #setSamplerTexId(int, int)}, {@link
* #setFloat(float)} or {@link #setFloats(float[])}.
*
* <p>Should be called before each drawing call.
*/
public void bind() {
if (type == GLES20.GL_FLOAT) {
GLES20.glUniform1fv(location, /* count= */ 1, value, /* offset= */ 0);
checkGlError();
return;
}
if (type == GLES20.GL_FLOAT_MAT3) {
GLES20.glUniformMatrix3fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
checkGlError();
return;
}
if (type == GLES20.GL_FLOAT_MAT4) {
GLES20.glUniformMatrix4fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
checkGlError();
return;
}
if (texId == 0) {
throw new IllegalStateException("No call to setSamplerTexId() before bind.");
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + unit);
if (type == GLES11Ext.GL_SAMPLER_EXTERNAL_OES || type == GL_SAMPLER_EXTERNAL_2D_Y2Y_EXT) {
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
} else if (type == GLES20.GL_SAMPLER_2D) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texId);
} else {
throw new IllegalStateException("Unexpected uniform type: " + type);
}
GLES20.glUniform1i(location, unit);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(
GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
checkGlError();
}
} }
@RequiresApi(17) @RequiresApi(17)
@ -748,25 +581,48 @@ public final class GlUtil {
Object surface, Object surface,
int[] configAttributes, int[] configAttributes,
int[] windowSurfaceAttributes) { int[] windowSurfaceAttributes) {
return EGL14.eglCreateWindowSurface( EGLSurface eglSurface =
EGL14.eglCreateWindowSurface(
eglDisplay, eglDisplay,
getEglConfig(eglDisplay, configAttributes), getEglConfig(eglDisplay, configAttributes),
surface, surface,
windowSurfaceAttributes, windowSurfaceAttributes,
/* offset= */ 0); /* offset= */ 0);
checkEglException("Error creating surface");
return eglSurface;
} }
@DoNotInline @DoNotInline
public static void focusSurface( public static EGLSurface createEglPbufferSurface(
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface surface, int width, int height) { EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) {
int[] boundFrameBuffer = new int[1]; EGLSurface eglSurface =
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, boundFrameBuffer, /* offset= */ 0); EGL14.eglCreatePbufferSurface(
int defaultFrameBuffer = 0; eglDisplay,
if (boundFrameBuffer[0] != defaultFrameBuffer) { getEglConfig(eglDisplay, configAttributes),
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, defaultFrameBuffer); pbufferAttributes,
/* offset= */ 0);
checkEglException("Error creating surface");
return eglSurface;
} }
EGL14.eglMakeCurrent(eglDisplay, surface, surface, eglContext);
@DoNotInline
public static void focusRenderTarget(
EGLDisplay eglDisplay,
EGLContext eglContext,
EGLSurface eglSurface,
int framebuffer,
int width,
int height) {
int[] boundFramebuffer = new int[1];
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, boundFramebuffer, /* offset= */ 0);
if (boundFramebuffer[0] != framebuffer) {
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer);
}
checkGlError();
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
checkEglException("Error making context current");
GLES20.glViewport(/* x= */ 0, /* y= */ 0, width, height); GLES20.glViewport(/* x= */ 0, /* y= */ 0, width, height);
checkGlError();
} }
@DoNotInline @DoNotInline
@ -777,19 +633,15 @@ public final class GlUtil {
} }
EGL14.eglMakeCurrent( EGL14.eglMakeCurrent(
eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); eglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
int error = EGL14.eglGetError(); checkEglException("Error releasing context");
checkEglException(error == EGL14.EGL_SUCCESS, "Error releasing context: " + error);
if (eglContext != null) { if (eglContext != null) {
EGL14.eglDestroyContext(eglDisplay, eglContext); EGL14.eglDestroyContext(eglDisplay, eglContext);
error = EGL14.eglGetError(); checkEglException("Error destroying context");
checkEglException(error == EGL14.EGL_SUCCESS, "Error destroying context: " + error);
} }
EGL14.eglReleaseThread(); EGL14.eglReleaseThread();
error = EGL14.eglGetError(); checkEglException("Error releasing thread");
checkEglException(error == EGL14.EGL_SUCCESS, "Error releasing thread: " + error);
EGL14.eglTerminate(eglDisplay); EGL14.eglTerminate(eglDisplay);
error = EGL14.eglGetError(); checkEglException("Error terminating display");
checkEglException(error == EGL14.EGL_SUCCESS, "Error terminating display: " + error);
} }
@DoNotInline @DoNotInline

View File

@ -27,7 +27,9 @@ public abstract class LibraryLoader {
private boolean loadAttempted; private boolean loadAttempted;
private boolean isAvailable; private boolean isAvailable;
/** @param libraries The names of the libraries to load. */ /**
* @param libraries The names of the libraries to load.
*/
public LibraryLoader(String... libraries) { public LibraryLoader(String... libraries) {
nativeLibraries = libraries; nativeLibraries = libraries;
} }

View File

@ -82,7 +82,9 @@ public final class Log {
Log.logStackTraces = logStackTraces; Log.logStackTraces = logStackTraces;
} }
/** @see android.util.Log#d(String, String) */ /**
* @see android.util.Log#d(String, String)
*/
@Pure @Pure
public static void d(@Size(max = 23) String tag, String message) { public static void d(@Size(max = 23) String tag, String message) {
if (logLevel == LOG_LEVEL_ALL) { if (logLevel == LOG_LEVEL_ALL) {
@ -90,13 +92,17 @@ public final class Log {
} }
} }
/** @see android.util.Log#d(String, String, Throwable) */ /**
* @see android.util.Log#d(String, String, Throwable)
*/
@Pure @Pure
public static void d(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) { public static void d(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
d(tag, appendThrowableString(message, throwable)); d(tag, appendThrowableString(message, throwable));
} }
/** @see android.util.Log#i(String, String) */ /**
* @see android.util.Log#i(String, String)
*/
@Pure @Pure
public static void i(@Size(max = 23) String tag, String message) { public static void i(@Size(max = 23) String tag, String message) {
if (logLevel <= LOG_LEVEL_INFO) { if (logLevel <= LOG_LEVEL_INFO) {
@ -104,13 +110,17 @@ public final class Log {
} }
} }
/** @see android.util.Log#i(String, String, Throwable) */ /**
* @see android.util.Log#i(String, String, Throwable)
*/
@Pure @Pure
public static void i(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) { public static void i(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
i(tag, appendThrowableString(message, throwable)); i(tag, appendThrowableString(message, throwable));
} }
/** @see android.util.Log#w(String, String) */ /**
* @see android.util.Log#w(String, String)
*/
@Pure @Pure
public static void w(@Size(max = 23) String tag, String message) { public static void w(@Size(max = 23) String tag, String message) {
if (logLevel <= LOG_LEVEL_WARNING) { if (logLevel <= LOG_LEVEL_WARNING) {
@ -118,13 +128,17 @@ public final class Log {
} }
} }
/** @see android.util.Log#w(String, String, Throwable) */ /**
* @see android.util.Log#w(String, String, Throwable)
*/
@Pure @Pure
public static void w(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) { public static void w(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
w(tag, appendThrowableString(message, throwable)); w(tag, appendThrowableString(message, throwable));
} }
/** @see android.util.Log#e(String, String) */ /**
* @see android.util.Log#e(String, String)
*/
@Pure @Pure
public static void e(@Size(max = 23) String tag, String message) { public static void e(@Size(max = 23) String tag, String message) {
if (logLevel <= LOG_LEVEL_ERROR) { if (logLevel <= LOG_LEVEL_ERROR) {
@ -132,7 +146,9 @@ public final class Log {
} }
} }
/** @see android.util.Log#e(String, String, Throwable) */ /**
* @see android.util.Log#e(String, String, Throwable)
*/
@Pure @Pure
public static void e(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) { public static void e(@Size(max = 23) String tag, String message, @Nullable Throwable throwable) {
e(tag, appendThrowableString(message, throwable)); e(tag, appendThrowableString(message, throwable));

View File

@ -30,7 +30,9 @@ public final class LongArray {
this(DEFAULT_INITIAL_CAPACITY); this(DEFAULT_INITIAL_CAPACITY);
} }
/** @param initialCapacity The initial capacity of the array. */ /**
* @param initialCapacity The initial capacity of the array.
*/
public LongArray(int initialCapacity) { public LongArray(int initialCapacity) {
values = new long[initialCapacity]; values = new long[initialCapacity];
} }

View File

@ -47,6 +47,19 @@ public final class MediaFormatUtil {
// The constant value must not be changed, because it's also set by the framework MediaParser API. // The constant value must not be changed, because it's also set by the framework MediaParser API.
public static final String KEY_PCM_ENCODING_EXTENDED = "exo-pcm-encoding-int"; public static final String KEY_PCM_ENCODING_EXTENDED = "exo-pcm-encoding-int";
/**
* The {@link MediaFormat} key for the maximum bitrate in bits per second.
*
* <p>The associated value is an integer.
*
* <p>The key string constant is the same as {@code MediaFormat#KEY_MAX_BITRATE}. Values for it
* are already returned by the framework MediaExtractor; the key is a hidden field in {@code
* MediaFormat} though, which is why it's being replicated here.
*/
// The constant value must not be changed, because it's also set by the framework MediaParser and
// MediaExtractor APIs.
public static final String KEY_MAX_BIT_RATE = "max-bitrate";
private static final int MAX_POWER_OF_TWO_INT = 1 << 30; private static final int MAX_POWER_OF_TWO_INT = 1 << 30;
/** /**
@ -63,6 +76,7 @@ public final class MediaFormatUtil {
public static MediaFormat createMediaFormatFromFormat(Format format) { public static MediaFormat createMediaFormatFromFormat(Format format) {
MediaFormat result = new MediaFormat(); MediaFormat result = new MediaFormat();
maybeSetInteger(result, MediaFormat.KEY_BIT_RATE, format.bitrate); maybeSetInteger(result, MediaFormat.KEY_BIT_RATE, format.bitrate);
maybeSetInteger(result, KEY_MAX_BIT_RATE, format.peakBitrate);
maybeSetInteger(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount); maybeSetInteger(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount);
maybeSetColorInfo(result, format.colorInfo); maybeSetColorInfo(result, format.colorInfo);

View File

@ -25,8 +25,8 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.telephony.PhoneStateListener; import android.telephony.TelephonyCallback;
import android.telephony.ServiceState; import android.telephony.TelephonyCallback.DisplayInfoListener;
import android.telephony.TelephonyDisplayInfo; import android.telephony.TelephonyDisplayInfo;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
@ -59,24 +59,6 @@ public final class NetworkTypeObserver {
void onNetworkTypeChanged(@C.NetworkType int networkType); void onNetworkTypeChanged(@C.NetworkType int networkType);
} }
/*
* Static configuration that may need to be set at app startup time is located in a separate
* static Config class. This allows apps to set their desired config without incurring unnecessary
* class loading costs during startup.
*/
/** Configuration for {@link NetworkTypeObserver}. */
public static final class Config {
private static volatile boolean disable5GNsaDisambiguation;
/** Disables logic to disambiguate 5G-NSA networks from 4G networks. */
public static void disable5GNsaDisambiguation() {
disable5GNsaDisambiguation = true;
}
private Config() {}
}
@Nullable private static NetworkTypeObserver staticInstance; @Nullable private static NetworkTypeObserver staticInstance;
private final Handler mainHandler; private final Handler mainHandler;
@ -232,55 +214,51 @@ public final class NetworkTypeObserver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
@C.NetworkType int networkType = getNetworkTypeFromConnectivityManager(context); @C.NetworkType int networkType = getNetworkTypeFromConnectivityManager(context);
if (Util.SDK_INT >= 29 if (Util.SDK_INT >= 31 && networkType == C.NETWORK_TYPE_4G) {
&& !Config.disable5GNsaDisambiguation
&& networkType == C.NETWORK_TYPE_4G) {
// Delay update of the network type to check whether this is actually 5G-NSA. // Delay update of the network type to check whether this is actually 5G-NSA.
try { Api31.disambiguate4gAnd5gNsa(context, /* instance= */ NetworkTypeObserver.this);
// We can't access TelephonyManager getters like getServiceState() directly as they
// require special permissions. Attaching a listener is permission-free because the
// callback data is censored to not include sensitive information.
TelephonyManager telephonyManager =
checkNotNull((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
TelephonyManagerListener listener = new TelephonyManagerListener();
if (Util.SDK_INT < 31) {
telephonyManager.listen(listener, PhoneStateListener.LISTEN_SERVICE_STATE);
} else { } else {
// Display info information can only be requested without permission from API 31.
telephonyManager.listen(listener, PhoneStateListener.LISTEN_DISPLAY_INFO_CHANGED);
}
// We are only interested in the initial response with the current state, so unregister
// the listener immediately.
telephonyManager.listen(listener, PhoneStateListener.LISTEN_NONE);
return;
} catch (RuntimeException e) {
// Ignore problems with listener registration and keep reporting as 4G.
}
}
updateNetworkType(networkType); updateNetworkType(networkType);
} }
} }
private class TelephonyManagerListener extends PhoneStateListener {
@Override
public void onServiceStateChanged(@Nullable ServiceState serviceState) {
// This workaround to check the toString output of ServiceState only works on API 29 and 30.
String serviceStateString = serviceState == null ? "" : serviceState.toString();
boolean is5gNsa =
serviceStateString.contains("nrState=CONNECTED")
|| serviceStateString.contains("nrState=NOT_RESTRICTED");
updateNetworkType(is5gNsa ? C.NETWORK_TYPE_5G_NSA : C.NETWORK_TYPE_4G);
} }
@RequiresApi(31) @RequiresApi(31)
private static final class Api31 {
public static void disambiguate4gAnd5gNsa(Context context, NetworkTypeObserver instance) {
try {
TelephonyManager telephonyManager =
checkNotNull((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
DisplayInfoCallback callback = new DisplayInfoCallback(instance);
telephonyManager.registerTelephonyCallback(context.getMainExecutor(), callback);
// We are only interested in the initial response with the current state, so unregister
// the listener immediately.
telephonyManager.unregisterTelephonyCallback(callback);
} catch (RuntimeException e) {
// Ignore problems with listener registration and keep reporting as 4G.
instance.updateNetworkType(C.NETWORK_TYPE_4G);
}
}
private static final class DisplayInfoCallback extends TelephonyCallback
implements DisplayInfoListener {
private final NetworkTypeObserver instance;
public DisplayInfoCallback(NetworkTypeObserver instance) {
this.instance = instance;
}
@Override @Override
public void onDisplayInfoChanged(TelephonyDisplayInfo telephonyDisplayInfo) { public void onDisplayInfoChanged(TelephonyDisplayInfo telephonyDisplayInfo) {
int overrideNetworkType = telephonyDisplayInfo.getOverrideNetworkType(); int overrideNetworkType = telephonyDisplayInfo.getOverrideNetworkType();
boolean is5gNsa = boolean is5gNsa =
overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA
|| overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE; || overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_NSA_MMWAVE
updateNetworkType(is5gNsa ? C.NETWORK_TYPE_5G_NSA : C.NETWORK_TYPE_4G); || overrideNetworkType == TelephonyDisplayInfo.OVERRIDE_NETWORK_TYPE_NR_ADVANCED;
instance.updateNetworkType(is5gNsa ? C.NETWORK_TYPE_5G_NSA : C.NETWORK_TYPE_4G);
}
} }
} }
} }

View File

@ -54,17 +54,29 @@ public final class NotificationUtil {
IMPORTANCE_HIGH IMPORTANCE_HIGH
}) })
public @interface Importance {} public @interface Importance {}
/** @see NotificationManager#IMPORTANCE_UNSPECIFIED */ /**
* @see NotificationManager#IMPORTANCE_UNSPECIFIED
*/
public static final int IMPORTANCE_UNSPECIFIED = NotificationManager.IMPORTANCE_UNSPECIFIED; public static final int IMPORTANCE_UNSPECIFIED = NotificationManager.IMPORTANCE_UNSPECIFIED;
/** @see NotificationManager#IMPORTANCE_NONE */ /**
* @see NotificationManager#IMPORTANCE_NONE
*/
public static final int IMPORTANCE_NONE = NotificationManager.IMPORTANCE_NONE; public static final int IMPORTANCE_NONE = NotificationManager.IMPORTANCE_NONE;
/** @see NotificationManager#IMPORTANCE_MIN */ /**
* @see NotificationManager#IMPORTANCE_MIN
*/
public static final int IMPORTANCE_MIN = NotificationManager.IMPORTANCE_MIN; public static final int IMPORTANCE_MIN = NotificationManager.IMPORTANCE_MIN;
/** @see NotificationManager#IMPORTANCE_LOW */ /**
* @see NotificationManager#IMPORTANCE_LOW
*/
public static final int IMPORTANCE_LOW = NotificationManager.IMPORTANCE_LOW; public static final int IMPORTANCE_LOW = NotificationManager.IMPORTANCE_LOW;
/** @see NotificationManager#IMPORTANCE_DEFAULT */ /**
* @see NotificationManager#IMPORTANCE_DEFAULT
*/
public static final int IMPORTANCE_DEFAULT = NotificationManager.IMPORTANCE_DEFAULT; public static final int IMPORTANCE_DEFAULT = NotificationManager.IMPORTANCE_DEFAULT;
/** @see NotificationManager#IMPORTANCE_HIGH */ /**
* @see NotificationManager#IMPORTANCE_HIGH
*/
public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH; public static final int IMPORTANCE_HIGH = NotificationManager.IMPORTANCE_HIGH;
/** /**

View File

@ -47,8 +47,27 @@ import java.lang.annotation.Target;
* Android Studio, in order to alert developers to the risk of breaking changes. * Android Studio, in order to alert developers to the risk of breaking changes.
* *
* <p>Individual usage sites can be opted-in to suppress the lint error by using the {@link * <p>Individual usage sites can be opted-in to suppress the lint error by using the {@link
* androidx.annotation.OptIn} annotation: {@code @androidx.annotation.OptIn(markerClass = * androidx.annotation.OptIn} annotation.
* androidx.media3.common.util.UnstableApi.class)}. *
* <p>In Java:
*
* <pre>{@code
* import androidx.annotation.OptIn;
* import androidx.media3.common.util.UnstableApi;
* ...
* @OptIn(markerClass = UnstableApi.class)
* private void methodUsingUnstableApis() { ... }
* }</pre>
*
* <p>In Kotlin:
*
* <pre>{@code
* import androidx.annotation.OptIn
* import androidx.media3.common.util.UnstableApi
* ...
* @OptIn(UnstableApi::class)
* private fun methodUsingUnstableApis() { ... }
* }</pre>
* *
* <p>Whole projects can be opted-in by suppressing the specific lint error in their <a * <p>Whole projects can be opted-in by suppressing the specific lint error in their <a
* href="https://developer.android.com/studio/write/lint#pref">{@code lint.xml} file</a>: * href="https://developer.android.com/studio/write/lint#pref">{@code lint.xml} file</a>:

View File

@ -29,7 +29,7 @@ public class AudioAttributesTest {
public void roundTripViaBundle_yieldsEqualInstance() { public void roundTripViaBundle_yieldsEqualInstance() {
AudioAttributes audioAttributes = AudioAttributes audioAttributes =
new AudioAttributes.Builder() new AudioAttributes.Builder()
.setContentType(C.CONTENT_TYPE_SONIFICATION) .setContentType(C.AUDIO_CONTENT_TYPE_SONIFICATION)
.setFlags(C.FLAG_AUDIBILITY_ENFORCED) .setFlags(C.FLAG_AUDIBILITY_ENFORCED)
.setUsage(C.USAGE_ALARM) .setUsage(C.USAGE_ALARM)
.setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_SYSTEM) .setAllowedCapturePolicy(C.ALLOW_CAPTURE_BY_SYSTEM)

View File

@ -18,22 +18,17 @@ package androidx.media3.common;
import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED; import static androidx.media3.common.Player.EVENT_IS_PLAYING_CHANGED;
import static androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION; import static androidx.media3.common.Player.EVENT_MEDIA_ITEM_TRANSITION;
import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED; import static androidx.media3.common.Player.EVENT_TIMELINE_CHANGED;
import static androidx.media3.common.util.Assertions.checkArgument;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.same; import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import androidx.media3.test.utils.StubPlayer; import androidx.media3.test.utils.StubPlayer;
import androidx.media3.test.utils.TestUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Queue;
import java.util.Set; import java.util.Set;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -105,7 +100,7 @@ public class ForwardingPlayerTest {
@Test @Test
public void forwardingPlayer_overridesAllPlayerMethods() throws Exception { public void forwardingPlayer_overridesAllPlayerMethods() throws Exception {
// Check with reflection that ForwardingPlayer overrides all Player methods. // Check with reflection that ForwardingPlayer overrides all Player methods.
List<Method> methods = getPublicMethods(Player.class); List<Method> methods = TestUtil.getPublicMethods(Player.class);
for (Method method : methods) { for (Method method : methods) {
assertThat( assertThat(
ForwardingPlayer.class ForwardingPlayer.class
@ -119,7 +114,7 @@ public class ForwardingPlayerTest {
public void forwardingListener_overridesAllListenerMethods() throws Exception { public void forwardingListener_overridesAllListenerMethods() throws Exception {
// Check with reflection that ForwardingListener overrides all Listener methods. // Check with reflection that ForwardingListener overrides all Listener methods.
Class<?> forwardingListenerClass = getInnerClass("ForwardingListener"); Class<?> forwardingListenerClass = getInnerClass("ForwardingListener");
List<Method> methods = getPublicMethods(Player.Listener.class); List<Method> methods = TestUtil.getPublicMethods(Player.Listener.class);
for (Method method : methods) { for (Method method : methods) {
assertThat( assertThat(
forwardingListenerClass forwardingListenerClass
@ -129,32 +124,6 @@ public class ForwardingPlayerTest {
} }
} }
/** Returns all the public methods of a Java interface. */
private static List<Method> getPublicMethods(Class<?> anInterface) {
checkArgument(anInterface.isInterface());
// Run a BFS over all extended interfaces to inspect them all.
Queue<Class<?>> interfacesQueue = new ArrayDeque<>();
interfacesQueue.add(anInterface);
Set<Class<?>> interfaces = new HashSet<>();
while (!interfacesQueue.isEmpty()) {
Class<?> currentInterface = interfacesQueue.remove();
if (interfaces.add(currentInterface)) {
Collections.addAll(interfacesQueue, currentInterface.getInterfaces());
}
}
List<Method> list = new ArrayList<>();
for (Class<?> currentInterface : interfaces) {
for (Method method : currentInterface.getDeclaredMethods()) {
if (Modifier.isPublic(method.getModifiers())) {
list.add(method);
}
}
}
return list;
}
private static Class<?> getInnerClass(String className) { private static Class<?> getInnerClass(String className) {
for (Class<?> innerClass : ForwardingPlayer.class.getDeclaredClasses()) { for (Class<?> innerClass : ForwardingPlayer.class.getDeclaredClasses()) {
if (innerClass.getSimpleName().equals(className)) { if (innerClass.getSimpleName().equals(className)) {

View File

@ -19,6 +19,8 @@ import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertThrows;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle;
import androidx.media3.common.MediaItem.RequestMetadata;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@ -223,7 +225,7 @@ public class MediaItemTest {
new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID) new MediaItem.DrmConfiguration.Builder(C.WIDEVINE_UUID)
.setLicenseUri(licenseUri) .setLicenseUri(licenseUri)
.setForcedSessionTrackTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO)) .setForcedSessionTrackTypes(ImmutableList.of(C.TRACK_TYPE_AUDIO))
.forceSessionsForAudioAndVideoTracks(true) .setForceSessionsForAudioAndVideoTracks(true)
.build(); .build();
assertThat(drmConfiguration.sessionForClearTypes) assertThat(drmConfiguration.sessionForClearTypes)
@ -579,6 +581,24 @@ public class MediaItemTest {
assertThat(mediaItem.liveConfiguration.maxOffsetMs).isEqualTo(1234); assertThat(mediaItem.liveConfiguration.maxOffsetMs).isEqualTo(1234);
} }
@Test
public void builder_setRequestMetadata_setsRequestMetadata() {
Bundle extras = new Bundle();
extras.putString("key", "value");
RequestMetadata requestMetadata =
new RequestMetadata.Builder()
.setMediaUri(Uri.parse("http://test.test"))
.setSearchQuery("Play media!")
.setExtras(extras)
.build();
MediaItem mediaItem =
new MediaItem.Builder().setMediaId("mediaID").setRequestMetadata(requestMetadata).build();
assertThat(mediaItem.requestMetadata).isEqualTo(requestMetadata);
assertThat(mediaItem.requestMetadata.extras.getString("key")).isEqualTo("value");
}
@Test @Test
@SuppressWarnings("deprecation") // Testing deprecated setter methods @SuppressWarnings("deprecation") // Testing deprecated setter methods
public void buildUpon_individualSetters_equalsToOriginal() { public void buildUpon_individualSetters_equalsToOriginal() {
@ -677,6 +697,11 @@ public class MediaItemTest {
.setLabel("label") .setLabel("label")
.setId("id") .setId("id")
.build())) .build()))
.setRequestMetadata(
new RequestMetadata.Builder()
.setMediaUri(Uri.parse("http://test.test"))
.setSearchQuery("search")
.build())
.setTag(new Object()) .setTag(new Object())
.build(); .build();
@ -704,6 +729,11 @@ public class MediaItemTest {
.setClipRelativeToDefaultPosition(true) .setClipRelativeToDefaultPosition(true)
.setClipRelativeToLiveWindow(true) .setClipRelativeToLiveWindow(true)
.setClipStartsAtKeyFrame(true) .setClipStartsAtKeyFrame(true)
.setRequestMetadata(
new RequestMetadata.Builder()
.setMediaUri(Uri.parse("http://test.test"))
.setSearchQuery("search")
.build())
.build(); .build();
assertThat(mediaItem.localConfiguration).isNull(); assertThat(mediaItem.localConfiguration).isNull();

View File

@ -41,7 +41,6 @@ public class MediaMetadataTest {
assertThat(mediaMetadata.displayTitle).isNull(); assertThat(mediaMetadata.displayTitle).isNull();
assertThat(mediaMetadata.subtitle).isNull(); assertThat(mediaMetadata.subtitle).isNull();
assertThat(mediaMetadata.description).isNull(); assertThat(mediaMetadata.description).isNull();
assertThat(mediaMetadata.mediaUri).isNull();
assertThat(mediaMetadata.userRating).isNull(); assertThat(mediaMetadata.userRating).isNull();
assertThat(mediaMetadata.overallRating).isNull(); assertThat(mediaMetadata.overallRating).isNull();
assertThat(mediaMetadata.artworkData).isNull(); assertThat(mediaMetadata.artworkData).isNull();
@ -127,7 +126,6 @@ public class MediaMetadataTest {
.setDisplayTitle("display title") .setDisplayTitle("display title")
.setSubtitle("subtitle") .setSubtitle("subtitle")
.setDescription("description") .setDescription("description")
.setMediaUri(Uri.parse("https://www.google.com"))
.setUserRating(new HeartRating(false)) .setUserRating(new HeartRating(false))
.setOverallRating(new PercentageRating(87.4f)) .setOverallRating(new PercentageRating(87.4f))
.setArtworkData( .setArtworkData(

View File

@ -0,0 +1,71 @@
/*
* Copyright (C) 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.common;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link TrackSelectionOverride}. */
@RunWith(AndroidJUnit4.class)
public final class TrackSelectionOverrideTest {
@Test
public void newTrackSelectionOverride_withOneTrack_selectsOneTrack() {
TrackSelectionOverride trackSelectionOverride =
new TrackSelectionOverride(newTrackGroupWithIds(1, 2), /* trackIndex= */ 1);
assertThat(trackSelectionOverride.mediaTrackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackIndices).containsExactly(1).inOrder();
}
@Test
public void newTrackSelectionOverride_withTracks_selectsOnlySpecifiedTracks() {
TrackSelectionOverride trackSelectionOverride =
new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of(1));
assertThat(trackSelectionOverride.mediaTrackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackIndices).containsExactly(1);
}
@Test
public void newTrackSelectionOverride_with0Tracks_selectsAllSpecifiedTracks() {
TrackSelectionOverride trackSelectionOverride =
new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of());
assertThat(trackSelectionOverride.mediaTrackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackIndices).isEmpty();
}
@Test
public void newTrackSelectionOverride_withInvalidIndex_throws() {
assertThrows(
IndexOutOfBoundsException.class,
() -> new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of(2)));
}
private static TrackGroup newTrackGroupWithIds(int... ids) {
return new TrackGroup(
Arrays.stream(ids)
.mapToObj(id -> new Format.Builder().setId(id).build())
.toArray(Format[]::new));
}
}

View File

@ -1,172 +0,0 @@
/*
* Copyright (C) 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.common;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import androidx.media3.common.TrackSelectionOverrides.TrackSelectionOverride;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit tests for {@link TrackSelectionOverrides}. */
@RunWith(AndroidJUnit4.class)
public final class TrackSelectionOverridesTest {
public static final TrackGroup AAC_TRACK_GROUP =
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build());
private static TrackGroup newTrackGroupWithIds(int... ids) {
return new TrackGroup(
Arrays.stream(ids)
.mapToObj(id -> new Format.Builder().setId(id).build())
.toArray(Format[]::new));
}
@Test
public void newTrackSelectionOverride_withJustTrackGroup_selectsAllTracks() {
TrackSelectionOverride trackSelectionOverride =
new TrackSelectionOverride(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackIndices).containsExactly(0, 1).inOrder();
}
@Test
public void newTrackSelectionOverride_withTracks_selectsOnlySpecifiedTracks() {
TrackSelectionOverride trackSelectionOverride =
new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of(1));
assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackIndices).containsExactly(1);
}
@Test
public void newTrackSelectionOverride_with0Tracks_selectsAllSpecifiedTracks() {
TrackSelectionOverride trackSelectionOverride =
new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of());
assertThat(trackSelectionOverride.trackGroup).isEqualTo(newTrackGroupWithIds(1, 2));
assertThat(trackSelectionOverride.trackIndices).isEmpty();
}
@Test
public void newTrackSelectionOverride_withInvalidIndex_throws() {
assertThrows(
IndexOutOfBoundsException.class,
() -> new TrackSelectionOverride(newTrackGroupWithIds(1, 2), ImmutableList.of(2)));
}
@Test
public void roundTripViaBundle_withOverrides_yieldsEqualInstance() {
TrackSelectionOverrides trackSelectionOverrides =
new TrackSelectionOverrides.Builder()
.setOverrideForType(
new TrackSelectionOverride(newTrackGroupWithIds(3, 4), ImmutableList.of(1)))
.addOverride(new TrackSelectionOverride(newTrackGroupWithIds(5, 6)))
.build();
TrackSelectionOverrides fromBundle =
TrackSelectionOverrides.CREATOR.fromBundle(trackSelectionOverrides.toBundle());
assertThat(fromBundle).isEqualTo(trackSelectionOverrides);
assertThat(fromBundle.asList()).isEqualTo(trackSelectionOverrides.asList());
}
@Test
public void builder_byDefault_isEmpty() {
TrackSelectionOverrides trackSelectionOverrides = new TrackSelectionOverrides.Builder().build();
assertThat(trackSelectionOverrides.asList()).isEmpty();
assertThat(trackSelectionOverrides).isEqualTo(TrackSelectionOverrides.EMPTY);
}
@Test
public void addOverride_onDifferentGroups_addsOverride() {
TrackSelectionOverride override1 = new TrackSelectionOverride(newTrackGroupWithIds(1));
TrackSelectionOverride override2 = new TrackSelectionOverride(newTrackGroupWithIds(2));
TrackSelectionOverrides trackSelectionOverrides =
new TrackSelectionOverrides.Builder().addOverride(override1).addOverride(override2).build();
assertThat(trackSelectionOverrides.asList()).containsExactly(override1, override2);
assertThat(trackSelectionOverrides.getOverride(override1.trackGroup)).isEqualTo(override1);
assertThat(trackSelectionOverrides.getOverride(override2.trackGroup)).isEqualTo(override2);
}
@Test
public void addOverride_onSameGroup_replacesOverride() {
TrackGroup trackGroup = newTrackGroupWithIds(1, 2, 3);
TrackSelectionOverride override1 =
new TrackSelectionOverride(trackGroup, /* trackIndices= */ ImmutableList.of(0));
TrackSelectionOverride override2 =
new TrackSelectionOverride(trackGroup, /* trackIndices= */ ImmutableList.of(1));
TrackSelectionOverrides trackSelectionOverrides =
new TrackSelectionOverrides.Builder().addOverride(override1).addOverride(override2).build();
assertThat(trackSelectionOverrides.asList()).containsExactly(override2);
assertThat(trackSelectionOverrides.getOverride(override2.trackGroup)).isEqualTo(override2);
}
@Test
public void setOverrideForType_onSameType_replacesOverride() {
TrackSelectionOverride override1 = new TrackSelectionOverride(newTrackGroupWithIds(1));
TrackSelectionOverride override2 = new TrackSelectionOverride(newTrackGroupWithIds(2));
TrackSelectionOverrides trackSelectionOverrides =
new TrackSelectionOverrides.Builder()
.setOverrideForType(override1)
.setOverrideForType(override2)
.build();
assertThat(trackSelectionOverrides.asList()).containsExactly(override2);
assertThat(trackSelectionOverrides.getOverride(override2.trackGroup)).isEqualTo(override2);
}
@Test
public void clearOverridesOfType_ofTypeAudio_removesAudioOverride() {
TrackSelectionOverride override1 = new TrackSelectionOverride(AAC_TRACK_GROUP);
TrackSelectionOverride override2 = new TrackSelectionOverride(newTrackGroupWithIds(1));
TrackSelectionOverrides trackSelectionOverrides =
new TrackSelectionOverrides.Builder()
.addOverride(override1)
.addOverride(override2)
.clearOverridesOfType(C.TRACK_TYPE_AUDIO)
.build();
assertThat(trackSelectionOverrides.asList()).containsExactly(override2);
assertThat(trackSelectionOverrides.getOverride(override2.trackGroup)).isEqualTo(override2);
}
@Test
public void clearOverride_ofTypeGroup_removesOverride() {
TrackSelectionOverride override1 = new TrackSelectionOverride(AAC_TRACK_GROUP);
TrackSelectionOverride override2 = new TrackSelectionOverride(newTrackGroupWithIds(1));
TrackSelectionOverrides trackSelectionOverrides =
new TrackSelectionOverrides.Builder()
.addOverride(override1)
.addOverride(override2)
.clearOverride(override2.trackGroup)
.build();
assertThat(trackSelectionOverrides.asList()).containsExactly(override1);
assertThat(trackSelectionOverrides.getOverride(override1.trackGroup)).isEqualTo(override1);
}
}

View File

@ -18,10 +18,8 @@ package androidx.media3.common;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import androidx.media3.common.TrackSelectionOverrides.TrackSelectionOverride;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@ -29,6 +27,9 @@ import org.junit.runner.RunWith;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
public final class TrackSelectionParametersTest { public final class TrackSelectionParametersTest {
private static final TrackGroup AAC_TRACK_GROUP =
new TrackGroup(new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build());
@Test @Test
public void defaultValue_withoutChange_isAsExpected() { public void defaultValue_withoutChange_isAsExpected() {
TrackSelectionParameters parameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT; TrackSelectionParameters parameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT;
@ -55,26 +56,25 @@ public final class TrackSelectionParametersTest {
assertThat(parameters.preferredAudioMimeTypes).isEmpty(); assertThat(parameters.preferredAudioMimeTypes).isEmpty();
assertThat(parameters.preferredTextLanguages).isEmpty(); assertThat(parameters.preferredTextLanguages).isEmpty();
assertThat(parameters.preferredTextRoleFlags).isEqualTo(0); assertThat(parameters.preferredTextRoleFlags).isEqualTo(0);
assertThat(parameters.ignoredTextSelectionFlags).isEqualTo(0);
assertThat(parameters.selectUndeterminedTextLanguage).isFalse(); assertThat(parameters.selectUndeterminedTextLanguage).isFalse();
// General // General
assertThat(parameters.forceLowestBitrate).isFalse(); assertThat(parameters.forceLowestBitrate).isFalse();
assertThat(parameters.forceHighestSupportedBitrate).isFalse(); assertThat(parameters.forceHighestSupportedBitrate).isFalse();
assertThat(parameters.trackSelectionOverrides.asList()).isEmpty(); assertThat(parameters.overrides).isEmpty();
assertThat(parameters.disabledTrackTypes).isEmpty(); assertThat(parameters.disabledTrackTypes).isEmpty();
} }
@Test @Test
public void parametersSet_fromDefault_isAsExpected() { public void parametersSet_fromDefault_isAsExpected() {
TrackSelectionOverrides trackSelectionOverrides = TrackSelectionOverride override1 =
new TrackSelectionOverrides.Builder() new TrackSelectionOverride(
.addOverride(new TrackSelectionOverride(new TrackGroup(new Format.Builder().build()))) new TrackGroup(new Format.Builder().build()), /* trackIndex= */ 0);
.addOverride( TrackSelectionOverride override2 =
new TrackSelectionOverride( new TrackSelectionOverride(
new TrackGroup( new TrackGroup(
new Format.Builder().setId(4).build(), new Format.Builder().setId(4).build(), new Format.Builder().setId(5).build()),
new Format.Builder().setId(5).build()), /* trackIndices= */ ImmutableList.of(1));
/* trackIndices= */ ImmutableList.of(1)))
.build();
TrackSelectionParameters parameters = TrackSelectionParameters parameters =
TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
.buildUpon() .buildUpon()
@ -99,12 +99,22 @@ public final class TrackSelectionParametersTest {
// Text // Text
.setPreferredTextLanguages("de", "en") .setPreferredTextLanguages("de", "en")
.setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION) .setPreferredTextRoleFlags(C.ROLE_FLAG_CAPTION)
.setIgnoredTextSelectionFlags(C.SELECTION_FLAG_AUTOSELECT)
.setSelectUndeterminedTextLanguage(true) .setSelectUndeterminedTextLanguage(true)
// General // General
.setForceLowestBitrate(false) .setForceLowestBitrate(false)
.setForceHighestSupportedBitrate(true) .setForceHighestSupportedBitrate(true)
.setTrackSelectionOverrides(trackSelectionOverrides) .addOverride(
.setDisabledTrackTypes(ImmutableSet.of(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT)) new TrackSelectionOverride(
new TrackGroup(new Format.Builder().build()), /* trackIndex= */ 0))
.addOverride(
new TrackSelectionOverride(
new TrackGroup(
new Format.Builder().setId(4).build(),
new Format.Builder().setId(5).build()),
/* trackIndices= */ ImmutableList.of(1)))
.setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, /* disabled= */ true)
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, /* disabled= */ true)
.build(); .build();
// Video // Video
@ -133,11 +143,14 @@ public final class TrackSelectionParametersTest {
// Text // Text
assertThat(parameters.preferredTextLanguages).containsExactly("de", "en").inOrder(); assertThat(parameters.preferredTextLanguages).containsExactly("de", "en").inOrder();
assertThat(parameters.preferredTextRoleFlags).isEqualTo(C.ROLE_FLAG_CAPTION); assertThat(parameters.preferredTextRoleFlags).isEqualTo(C.ROLE_FLAG_CAPTION);
assertThat(parameters.ignoredTextSelectionFlags).isEqualTo(C.SELECTION_FLAG_AUTOSELECT);
assertThat(parameters.selectUndeterminedTextLanguage).isTrue(); assertThat(parameters.selectUndeterminedTextLanguage).isTrue();
// General // General
assertThat(parameters.forceLowestBitrate).isFalse(); assertThat(parameters.forceLowestBitrate).isFalse();
assertThat(parameters.forceHighestSupportedBitrate).isTrue(); assertThat(parameters.forceHighestSupportedBitrate).isTrue();
assertThat(parameters.trackSelectionOverrides).isEqualTo(trackSelectionOverrides); assertThat(parameters.overrides)
.containsExactly(
override1.mediaTrackGroup, override1, override2.mediaTrackGroup, override2);
assertThat(parameters.disabledTrackTypes) assertThat(parameters.disabledTrackTypes)
.containsExactly(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT); .containsExactly(C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT);
} }
@ -178,4 +191,115 @@ public final class TrackSelectionParametersTest {
assertThat(parameters.viewportHeight).isEqualTo(Integer.MAX_VALUE); assertThat(parameters.viewportHeight).isEqualTo(Integer.MAX_VALUE);
assertThat(parameters.viewportOrientationMayChange).isTrue(); assertThat(parameters.viewportOrientationMayChange).isTrue();
} }
@Test
public void roundTripViaBundle_withOverride_yieldsEqualInstance() {
TrackSelectionOverride override =
new TrackSelectionOverride(
newTrackGroupWithIds(3, 4), /* trackIndices= */ ImmutableList.of(1));
TrackSelectionParameters trackSelectionParameters =
new TrackSelectionParameters.Builder(getApplicationContext()).addOverride(override).build();
TrackSelectionParameters fromBundle =
TrackSelectionParameters.fromBundle(trackSelectionParameters.toBundle());
assertThat(fromBundle).isEqualTo(trackSelectionParameters);
assertThat(trackSelectionParameters.overrides)
.containsExactly(override.mediaTrackGroup, override);
}
@Test
public void addOverride_onDifferentGroups_addsOverride() {
TrackSelectionOverride override1 =
new TrackSelectionOverride(newTrackGroupWithIds(1), /* trackIndex= */ 0);
TrackSelectionOverride override2 =
new TrackSelectionOverride(newTrackGroupWithIds(2), /* trackIndex= */ 0);
TrackSelectionParameters trackSelectionParameters =
new TrackSelectionParameters.Builder(getApplicationContext())
.addOverride(override1)
.addOverride(override2)
.build();
assertThat(trackSelectionParameters.overrides)
.containsExactly(
override1.mediaTrackGroup, override1, override2.mediaTrackGroup, override2);
}
@Test
public void addOverride_onSameGroup_replacesOverride() {
TrackGroup trackGroup = newTrackGroupWithIds(1, 2, 3);
TrackSelectionOverride override1 =
new TrackSelectionOverride(trackGroup, /* trackIndices= */ ImmutableList.of(0));
TrackSelectionOverride override2 =
new TrackSelectionOverride(trackGroup, /* trackIndices= */ ImmutableList.of(1));
TrackSelectionParameters trackSelectionParameters =
new TrackSelectionParameters.Builder(getApplicationContext())
.addOverride(override1)
.addOverride(override2)
.build();
assertThat(trackSelectionParameters.overrides)
.containsExactly(override2.mediaTrackGroup, override2);
}
@Test
public void setOverrideForType_onSameType_replacesOverride() {
TrackSelectionOverride override1 =
new TrackSelectionOverride(newTrackGroupWithIds(1), /* trackIndex= */ 0);
TrackSelectionOverride override2 =
new TrackSelectionOverride(newTrackGroupWithIds(2), /* trackIndex= */ 0);
TrackSelectionParameters trackSelectionParameters =
new TrackSelectionParameters.Builder(getApplicationContext())
.setOverrideForType(override1)
.setOverrideForType(override2)
.build();
assertThat(trackSelectionParameters.overrides)
.containsExactly(override2.mediaTrackGroup, override2);
}
@Test
public void clearOverridesOfType_ofTypeAudio_removesAudioOverride() {
TrackSelectionOverride override1 =
new TrackSelectionOverride(AAC_TRACK_GROUP, /* trackIndex= */ 0);
TrackSelectionOverride override2 =
new TrackSelectionOverride(newTrackGroupWithIds(1), /* trackIndex= */ 0);
TrackSelectionParameters trackSelectionParameters =
new TrackSelectionParameters.Builder(getApplicationContext())
.addOverride(override1)
.addOverride(override2)
.clearOverridesOfType(C.TRACK_TYPE_AUDIO)
.build();
assertThat(trackSelectionParameters.overrides)
.containsExactly(override2.mediaTrackGroup, override2);
}
@Test
public void clearOverride_ofTypeGroup_removesOverride() {
TrackSelectionOverride override1 =
new TrackSelectionOverride(AAC_TRACK_GROUP, /* trackIndex= */ 0);
TrackSelectionOverride override2 =
new TrackSelectionOverride(newTrackGroupWithIds(1), /* trackIndex= */ 0);
TrackSelectionParameters trackSelectionParameters =
new TrackSelectionParameters.Builder(getApplicationContext())
.addOverride(override1)
.addOverride(override2)
.clearOverride(override2.mediaTrackGroup)
.build();
assertThat(trackSelectionParameters.overrides)
.containsExactly(override1.mediaTrackGroup, override1);
}
private static TrackGroup newTrackGroupWithIds(int... ids) {
Format[] formats = new Format[ids.length];
for (int i = 0; i < ids.length; i++) {
formats[i] = new Format.Builder().setId(ids[i]).build();
}
return new TrackGroup(formats);
}
} }

View File

@ -1,110 +0,0 @@
/*
* Copyright (C) 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.common;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link TracksInfo}. */
@RunWith(AndroidJUnit4.class)
public class TracksInfoTest {
@Test
public void roundTripViaBundle_ofEmptyTracksInfo_yieldsEqualInstance() {
TracksInfo before = TracksInfo.EMPTY;
TracksInfo after = TracksInfo.CREATOR.fromBundle(before.toBundle());
assertThat(after).isEqualTo(before);
}
@Test
public void roundTripViaBundle_ofTracksInfo_yieldsEqualInstance() {
TracksInfo before =
new TracksInfo(
ImmutableList.of(
new TracksInfo.TrackGroupInfo(
new TrackGroup(new Format.Builder().build()),
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
C.TRACK_TYPE_AUDIO,
new boolean[] {true}),
new TracksInfo.TrackGroupInfo(
new TrackGroup(new Format.Builder().build(), new Format.Builder().build()),
new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_UNSUPPORTED_TYPE},
C.TRACK_TYPE_VIDEO,
new boolean[] {false, true})));
TracksInfo after = TracksInfo.CREATOR.fromBundle(before.toBundle());
assertThat(after).isEqualTo(before);
}
@Test
public void tracksInfoGetters_withoutTrack_returnExpectedValues() {
TracksInfo tracksInfo = new TracksInfo(ImmutableList.of());
assertThat(tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_AUDIO)).isTrue();
assertThat(tracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse();
ImmutableList<TracksInfo.TrackGroupInfo> trackGroupInfos = tracksInfo.getTrackGroupInfos();
assertThat(trackGroupInfos).isEmpty();
}
@Test
public void tracksInfo_emptyStaticInstance_isEmpty() {
TracksInfo tracksInfo = TracksInfo.EMPTY;
assertThat(tracksInfo.getTrackGroupInfos()).isEmpty();
assertThat(tracksInfo).isEqualTo(new TracksInfo(ImmutableList.of()));
}
@Test
public void tracksInfoGetters_ofComplexTracksInfo_returnExpectedValues() {
TracksInfo.TrackGroupInfo trackGroupInfo0 =
new TracksInfo.TrackGroupInfo(
new TrackGroup(new Format.Builder().build()),
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
C.TRACK_TYPE_AUDIO,
/* tracksSelected= */ new boolean[] {false});
TracksInfo.TrackGroupInfo trackGroupInfo1 =
new TracksInfo.TrackGroupInfo(
new TrackGroup(new Format.Builder().build(), new Format.Builder().build()),
new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_HANDLED},
C.TRACK_TYPE_VIDEO,
/* tracksSelected= */ new boolean[] {false, true});
TracksInfo tracksInfo = new TracksInfo(ImmutableList.of(trackGroupInfo0, trackGroupInfo1));
assertThat(tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_VIDEO)).isTrue();
assertThat(tracksInfo.isTypeSupportedOrEmpty(C.TRACK_TYPE_TEXT)).isTrue();
assertThat(tracksInfo.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracksInfo.isTypeSelected(C.TRACK_TYPE_VIDEO)).isTrue();
ImmutableList<TracksInfo.TrackGroupInfo> trackGroupInfos = tracksInfo.getTrackGroupInfos();
assertThat(trackGroupInfos).hasSize(2);
assertThat(trackGroupInfos.get(0)).isSameInstanceAs(trackGroupInfo0);
assertThat(trackGroupInfos.get(1)).isSameInstanceAs(trackGroupInfo1);
assertThat(trackGroupInfos.get(0).isTrackSupported(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSupported(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSupported(1)).isTrue();
assertThat(trackGroupInfos.get(0).getTrackSupport(0)).isEqualTo(C.FORMAT_EXCEEDS_CAPABILITIES);
assertThat(trackGroupInfos.get(1).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_DRM);
assertThat(trackGroupInfos.get(1).getTrackSupport(1)).isEqualTo(C.FORMAT_HANDLED);
assertThat(trackGroupInfos.get(0).isTrackSelected(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSelected(0)).isFalse();
assertThat(trackGroupInfos.get(1).isTrackSelected(1)).isTrue();
assertThat(trackGroupInfos.get(0).getTrackType()).isEqualTo(C.TRACK_TYPE_AUDIO);
assertThat(trackGroupInfos.get(1).getTrackType()).isEqualTo(C.TRACK_TYPE_VIDEO);
}
}

View File

@ -0,0 +1,143 @@
/*
* Copyright (C) 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.common;
import static androidx.media3.common.MimeTypes.AUDIO_AAC;
import static androidx.media3.common.MimeTypes.VIDEO_H264;
import static com.google.common.truth.Truth.assertThat;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link Tracks}. */
@RunWith(AndroidJUnit4.class)
public class TracksTest {
@Test
public void roundTripViaBundle_ofEmptyTracks_yieldsEqualInstance() {
Tracks before = Tracks.EMPTY;
Tracks after = Tracks.CREATOR.fromBundle(before.toBundle());
assertThat(after).isEqualTo(before);
}
@Test
public void roundTripViaBundle_ofTracks_yieldsEqualInstance() {
Tracks before =
new Tracks(
ImmutableList.of(
new Tracks.Group(
new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
/* adaptiveSupported= */ false,
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
/* trackSelected= */ new boolean[] {true}),
new Tracks.Group(
new TrackGroup(
new Format.Builder().setSampleMimeType(VIDEO_H264).build(),
new Format.Builder().setSampleMimeType(VIDEO_H264).build()),
/* adaptiveSupported= */ true,
new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_UNSUPPORTED_TYPE},
/* trackSelected= */ new boolean[] {false, true})));
Tracks after = Tracks.CREATOR.fromBundle(before.toBundle());
assertThat(after).isEqualTo(before);
}
@Test
public void getters_withoutTrack_returnExpectedValues() {
Tracks tracks = new Tracks(ImmutableList.of());
assertThat(tracks.containsType(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true))
.isFalse();
assertThat(tracks.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse();
ImmutableList<Tracks.Group> trackGroups = tracks.getGroups();
assertThat(trackGroups).isEmpty();
}
@Test
public void emptyStaticInstance_isEmpty() {
Tracks tracks = Tracks.EMPTY;
assertThat(tracks.getGroups()).isEmpty();
assertThat(tracks).isEqualTo(new Tracks(ImmutableList.of()));
}
@Test
public void getters_ofComplexTracks_returnExpectedValues() {
Tracks.Group trackGroup0 =
new Tracks.Group(
new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
/* adaptiveSupported= */ false,
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
/* trackSelected= */ new boolean[] {false});
Tracks.Group trackGroup1 =
new Tracks.Group(
new TrackGroup(
new Format.Builder().setSampleMimeType(VIDEO_H264).build(),
new Format.Builder().setSampleMimeType(VIDEO_H264).build()),
/* adaptiveSupported= */ true,
new int[] {C.FORMAT_UNSUPPORTED_DRM, C.FORMAT_HANDLED},
/* trackSelected= */ new boolean[] {false, true});
Tracks tracks = new Tracks(ImmutableList.of(trackGroup0, trackGroup1));
assertThat(tracks.containsType(C.TRACK_TYPE_AUDIO)).isTrue();
assertThat(tracks.containsType(C.TRACK_TYPE_VIDEO)).isTrue();
assertThat(tracks.containsType(C.TRACK_TYPE_TEXT)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_VIDEO)).isTrue();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_TEXT)).isFalse();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true))
.isTrue();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true))
.isTrue();
assertThat(tracks.isTypeSupported(C.TRACK_TYPE_TEXT, /* allowExceedsCapabilities= */ true))
.isFalse();
assertThat(tracks.isTypeSelected(C.TRACK_TYPE_AUDIO)).isFalse();
assertThat(tracks.isTypeSelected(C.TRACK_TYPE_VIDEO)).isTrue();
ImmutableList<Tracks.Group> trackGroups = tracks.getGroups();
assertThat(trackGroups).hasSize(2);
assertThat(trackGroups.get(0)).isSameInstanceAs(trackGroup0);
assertThat(trackGroups.get(1)).isSameInstanceAs(trackGroup1);
assertThat(trackGroups.get(0).isTrackSupported(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSupported(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSupported(1)).isTrue();
assertThat(trackGroups.get(0).getTrackSupport(0)).isEqualTo(C.FORMAT_EXCEEDS_CAPABILITIES);
assertThat(trackGroups.get(1).getTrackSupport(0)).isEqualTo(C.FORMAT_UNSUPPORTED_DRM);
assertThat(trackGroups.get(1).getTrackSupport(1)).isEqualTo(C.FORMAT_HANDLED);
assertThat(trackGroups.get(0).isTrackSelected(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSelected(0)).isFalse();
assertThat(trackGroups.get(1).isTrackSelected(1)).isTrue();
assertThat(trackGroups.get(0).getType()).isEqualTo(C.TRACK_TYPE_AUDIO);
assertThat(trackGroups.get(1).getType()).isEqualTo(C.TRACK_TYPE_VIDEO);
}
/**
* Tests that {@link Tracks.Group#isAdaptiveSupported} returns false if the group only contains a
* single track, even if true is passed to the constructor.
*/
@Test
public void groupWithSingleTrack_isNotAdaptive() {
Tracks.Group trackGroup =
new Tracks.Group(
new TrackGroup(new Format.Builder().setSampleMimeType(AUDIO_AAC).build()),
/* adaptiveSupported= */ true,
new int[] {C.FORMAT_EXCEEDS_CAPABILITIES},
/* trackSelected= */ new boolean[] {false});
assertThat(trackGroup.isAdaptiveSupported()).isFalse();
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.
*
*/
package androidx.media3.common.text;
import static com.google.common.truth.Truth.assertThat;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Parcel;
import android.text.SpannedString;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Tests for {@link CueGroup}. */
@RunWith(AndroidJUnit4.class)
public class CueGroupTest {
@Test
public void bundleAndUnBundleCueGroup() {
Cue textCue = new Cue.Builder().setText(SpannedString.valueOf("text")).build();
Cue bitmapCue =
new Cue.Builder().setBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)).build();
ImmutableList<Cue> cues = ImmutableList.of(textCue, bitmapCue);
CueGroup cueGroup = new CueGroup(cues);
Parcel parcel = Parcel.obtain();
try {
parcel.writeBundle(cueGroup.toBundle());
parcel.setDataPosition(0);
Bundle bundle = parcel.readBundle();
CueGroup filteredCueGroup = CueGroup.CREATOR.fromBundle(bundle);
assertThat(filteredCueGroup.cues).containsExactly(textCue);
} finally {
parcel.recycle();
}
}
}

View File

@ -22,6 +22,7 @@ import static android.graphics.Color.argb;
import static android.graphics.Color.parseColor; import static android.graphics.Color.parseColor;
import static androidx.media3.common.util.ColorParser.parseTtmlColor; import static androidx.media3.common.util.ColorParser.parseTtmlColor;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.graphics.Color; import android.graphics.Color;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
@ -34,24 +35,26 @@ public final class ColorParserTest {
// Negative tests. // Negative tests.
@Test(expected = IllegalArgumentException.class) @Test
public void parseUnknownColor() { public void parseUnknownColor() {
ColorParser.parseTtmlColor("colorOfAnElectron"); assertThrows(
IllegalArgumentException.class, () -> ColorParser.parseTtmlColor("colorOfAnElectron"));
} }
@Test(expected = IllegalArgumentException.class) @Test
public void parseNull() { public void parseNull() {
ColorParser.parseTtmlColor(null); assertThrows(IllegalArgumentException.class, () -> ColorParser.parseTtmlColor(null));
} }
@Test(expected = IllegalArgumentException.class) @Test
public void parseEmpty() { public void parseEmpty() {
ColorParser.parseTtmlColor(""); assertThrows(IllegalArgumentException.class, () -> ColorParser.parseTtmlColor(""));
} }
@Test(expected = IllegalArgumentException.class) @Test
public void rgbColorParsingRgbValuesNegative() { public void rgbColorParsingRgbValuesNegative() {
ColorParser.parseTtmlColor("rgb(-4, 55, 209)"); assertThrows(
IllegalArgumentException.class, () -> ColorParser.parseTtmlColor("rgb(-4, 55, 209)"));
} }
// Positive tests. // Positive tests.

View File

@ -21,6 +21,7 @@ import static androidx.media3.common.util.Util.escapeFileName;
import static androidx.media3.common.util.Util.getCodecsOfType; import static androidx.media3.common.util.Util.getCodecsOfType;
import static androidx.media3.common.util.Util.getStringForTime; import static androidx.media3.common.util.Util.getStringForTime;
import static androidx.media3.common.util.Util.gzip; import static androidx.media3.common.util.Util.gzip;
import static androidx.media3.common.util.Util.maxValue;
import static androidx.media3.common.util.Util.minValue; import static androidx.media3.common.util.Util.minValue;
import static androidx.media3.common.util.Util.parseXsDateTime; import static androidx.media3.common.util.Util.parseXsDateTime;
import static androidx.media3.common.util.Util.parseXsDuration; import static androidx.media3.common.util.Util.parseXsDuration;
@ -102,50 +103,109 @@ public class UtilTest {
@Test @Test
public void inferContentType_handlesHlsIsmUris() { public void inferContentType_handlesHlsIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl)")) assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl)")))
.isEqualTo(C.TYPE_HLS); .isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl,quality=hd)")) assertThat(
.isEqualTo(C.TYPE_HLS); Util.inferContentType(
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl)")) Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl,quality=hd)")))
.isEqualTo(C.TYPE_HLS); .isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(
Util.inferContentType(
Uri.parse("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl)")))
.isEqualTo(C.CONTENT_TYPE_HLS);
} }
@Test @Test
public void inferContentType_handlesHlsIsmV3Uris() { public void inferContentType_handlesHlsIsmV3Uris() {
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl-v3)")) assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl-v3)")))
.isEqualTo(C.TYPE_HLS); .isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(format=m3u8-aapl-v3,quality=hd)")) assertThat(
.isEqualTo(C.TYPE_HLS); Util.inferContentType(
assertThat(Util.inferContentType("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl-v3)")) Uri.parse("http://a.b/c.ism/manifest(format=m3u8-aapl-v3,quality=hd)")))
.isEqualTo(C.TYPE_HLS); .isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(
Util.inferContentType(
Uri.parse("http://a.b/c.ism/manifest(quality=hd,format=m3u8-aapl-v3)")))
.isEqualTo(C.CONTENT_TYPE_HLS);
} }
@Test @Test
public void inferContentType_handlesDashIsmUris() { public void inferContentType_handlesDashIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(format=mpd-time-csf)")) assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest(format=mpd-time-csf)")))
.isEqualTo(C.TYPE_DASH); .isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(format=mpd-time-csf,quality=hd)")) assertThat(
.isEqualTo(C.TYPE_DASH); Util.inferContentType(
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(quality=hd,format=mpd-time-csf)")) Uri.parse("http://a.b/c.isml/manifest(format=mpd-time-csf,quality=hd)")))
.isEqualTo(C.TYPE_DASH); .isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(
Util.inferContentType(
Uri.parse("http://a.b/c.isml/manifest(quality=hd,format=mpd-time-csf)")))
.isEqualTo(C.CONTENT_TYPE_DASH);
} }
@Test @Test
public void inferContentType_handlesSmoothStreamingIsmUris() { public void inferContentType_handlesSmoothStreamingIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.ism")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism"))).isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml"))).isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.ism/")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/"))).isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/"))).isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.ism/Manifest")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/Manifest")))
assertThat(Util.inferContentType("http://a.b/c.isml/manifest")).isEqualTo(C.TYPE_SS); .isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType("http://a.b/c.isml/manifest(filter=x)")).isEqualTo(C.TYPE_SS); assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest")))
assertThat(Util.inferContentType("http://a.b/c.isml/manifest_hd")).isEqualTo(C.TYPE_SS); .isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest(filter=x)")))
.isEqualTo(C.CONTENT_TYPE_SS);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.isml/manifest_hd")))
.isEqualTo(C.CONTENT_TYPE_SS);
} }
@Test @Test
public void inferContentType_handlesOtherIsmUris() { public void inferContentType_handlesOtherIsmUris() {
assertThat(Util.inferContentType("http://a.b/c.ism/video.mp4")).isEqualTo(C.TYPE_OTHER); assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/video.mp4")))
assertThat(Util.inferContentType("http://a.b/c.ism/prefix-manifest")).isEqualTo(C.TYPE_OTHER); .isEqualTo(C.CONTENT_TYPE_OTHER);
assertThat(Util.inferContentType(Uri.parse("http://a.b/c.ism/prefix-manifest")))
.isEqualTo(C.CONTENT_TYPE_OTHER);
}
/**
* Test that the deprecated {@link Util#inferContentType(String)} works when passed only a file
* extension and the leading dot.
*/
@SuppressWarnings("deprecation")
@Test
public void inferContentType_extensionAsPath() {
assertThat(Util.inferContentType(".m3u8")).isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(Util.inferContentType(".mpd")).isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(Util.inferContentType(".ism")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType(".isml")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentType(".mp4")).isEqualTo(C.CONTENT_TYPE_OTHER);
}
// Testing deprecated method.
@SuppressWarnings("deprecation")
@Test
public void inferContentType_extensionOverride() {
assertThat(
Util.inferContentType(
Uri.parse("file:///path/to/something.mpd"), /* overrideExtension= */ null))
.isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(
Util.inferContentType(
Uri.parse("file:///path/to/something.mpd"), /* overrideExtension= */ ""))
.isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(
Util.inferContentType(
Uri.parse("file:///path/to/something.mpd"), /* overrideExtension= */ "m3u8"))
.isEqualTo(C.CONTENT_TYPE_HLS);
}
@Test
public void inferContentTypeForExtension() {
assertThat(Util.inferContentTypeForExtension("m3u8")).isEqualTo(C.CONTENT_TYPE_HLS);
assertThat(Util.inferContentTypeForExtension("mpd")).isEqualTo(C.CONTENT_TYPE_DASH);
assertThat(Util.inferContentTypeForExtension("ism")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentTypeForExtension("isml")).isEqualTo(C.TYPE_SS);
assertThat(Util.inferContentTypeForExtension("mp4")).isEqualTo(C.CONTENT_TYPE_OTHER);
} }
@Test @Test
@ -747,6 +807,21 @@ public class UtilTest {
assertThrows(NoSuchElementException.class, () -> minValue(new SparseLongArray())); assertThrows(NoSuchElementException.class, () -> minValue(new SparseLongArray()));
} }
@Test
public void sparseLongArrayMaxValue_returnsMaxValue() {
SparseLongArray sparseLongArray = new SparseLongArray();
sparseLongArray.put(0, 2);
sparseLongArray.put(25, 10);
sparseLongArray.put(42, 1);
assertThat(maxValue(sparseLongArray)).isEqualTo(10);
}
@Test
public void sparseLongArrayMaxValue_emptyArray_throws() {
assertThrows(NoSuchElementException.class, () -> maxValue(new SparseLongArray()));
}
@Test @Test
public void parseXsDuration_returnsParsedDurationInMillis() { public void parseXsDuration_returnsParsedDurationInMillis() {
assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L);
@ -1087,6 +1162,7 @@ public class UtilTest {
assertThat(Util.normalizeLanguageCode("ara-ayl")).isEqualTo("ar-ayl"); assertThat(Util.normalizeLanguageCode("ara-ayl")).isEqualTo("ar-ayl");
// Special case of short codes that are actually part of a macrolanguage. // Special case of short codes that are actually part of a macrolanguage.
assertThat(Util.normalizeLanguageCode("arb")).isEqualTo("ar-arb");
assertThat(Util.normalizeLanguageCode("nb")).isEqualTo("no-nob"); assertThat(Util.normalizeLanguageCode("nb")).isEqualTo("no-nob");
assertThat(Util.normalizeLanguageCode("nn")).isEqualTo("no-nno"); assertThat(Util.normalizeLanguageCode("nn")).isEqualTo("no-nno");
assertThat(Util.normalizeLanguageCode("nob")).isEqualTo("no-nob"); assertThat(Util.normalizeLanguageCode("nob")).isEqualTo("no-nob");

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