Merge pull request #210 from androidx/release-1.0.0-beta03

1.0.0-beta03
This commit is contained in:
Michael Katz 2022-11-23 11:38:16 +00:00 committed by GitHub
commit c2cbb6370a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
620 changed files with 33483 additions and 9123 deletions

View File

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

3
.gitignore vendored
View File

@ -76,3 +76,6 @@ extensions/cronet/jniLibs/*
!extensions/cronet/jniLibs/README.md
extensions/cronet/libs/*
!extensions/cronet/libs/README.md
# MIDI extension
extensions/midi/lib

View File

@ -21,7 +21,7 @@ all of the information requested in the issue template.
## Pull requests
We will also consider high quality pull requests. These should normally merge
We will also consider high quality pull requests. These should merge
into the `main` branch. Before a pull request can be accepted you must submit
a Contributor License Agreement, as described below.

View File

@ -1,4 +1,145 @@
Release notes
Release notes
### 1.0.0-beta03 (2022-11-22)
This release corresponds to the
[ExoPlayer 2.18.2 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.2).
* Core library:
* Add `ExoPlayer.isTunnelingEnabled` to check if tunneling is enabled for
the currently selected tracks
([#2518](https://github.com/google/ExoPlayer/issues/2518)).
* Add `WrappingMediaSource` to simplify wrapping a single `MediaSource`
([#7279](https://github.com/google/ExoPlayer/issues/7279)).
* Discard back buffer before playback gets stuck due to insufficient
available memory.
* Close the Tracing "doSomeWork" block when offload is enabled.
* Fix session tracking problem with fast seeks in `PlaybackStatsListener`
([#180](https://github.com/androidx/media/issues/180)).
* Send missing `onMediaItemTransition` callback when calling `seekToNext`
or `seekToPrevious` in a single-item playlist
([#10667](https://github.com/google/ExoPlayer/issues/10667)).
* Add `Player.getSurfaceSize` that returns the size of the surface on
which the video is rendered.
* Fix bug where removing listeners during the player release can cause an
`IllegalStateException`
([#10758](https://github.com/google/ExoPlayer/issues/10758)).
* Build:
* Enforce minimum `compileSdkVersion` to avoid compilation errors
([#10684](https://github.com/google/ExoPlayer/issues/10684)).
* Avoid publishing block when included in another gradle build.
* Track selection:
* Prefer other tracks to Dolby Vision if display does not support it.
([#8944](https://github.com/google/ExoPlayer/issues/8944)).
* Downloads:
* Fix potential infinite loop in `ProgressiveDownloader` caused by
simultaneous download and playback with the same `PriorityTaskManager`
([#10570](https://github.com/google/ExoPlayer/pull/10570)).
* Make download notification appear immediately
([#183](https://github.com/androidx/media/pull/183)).
* Limit parallel download removals to 1 to avoid excessive thread creation
([#10458](https://github.com/google/ExoPlayer/issues/10458)).
* Video:
* Try alternative decoder for Dolby Vision if display does not support it.
([#9794](https://github.com/google/ExoPlayer/issues/9794)).
* Audio:
* Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid
OutOfMemory errors when releasing multiple players at the same time
([#10057](https://github.com/google/ExoPlayer/issues/10057)).
* Adds `AudioOffloadListener.onExperimentalOffloadedPlayback` for the
AudioTrack offload state.
([#134](https://github.com/androidx/media/issues/134)).
* Make `AudioTrackBufferSizeProvider` a public interface.
* Add `ExoPlayer.setPreferredAudioDevice` to set the preferred audio
output device ([#135](https://github.com/androidx/media/issues/135)).
* Rename `androidx.media3.exoplayer.audio.AudioProcessor` to
`androidx.media3.common.audio.AudioProcessor`.
* Map 8-channel and 12-channel audio to the 7.1 and 7.1.4 channel masks
respectively on all Android versions
([#10701](https://github.com/google/ExoPlayer/issues/10701)).
* Metadata:
* `MetadataRenderer` can now be configured to render metadata as soon as
they are available. Create an instance with
`MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory,
boolean)` to specify whether the renderer will output metadata early or
in sync with the player position.
* DRM:
* Work around a bug in the Android 13 ClearKey implementation that returns
a non-empty but invalid license URL.
* Fix `setMediaDrmSession failed: session not opened` error when switching
between DRM schemes in a playlist (e.g. Widevine to ClearKey).
* Text:
* CEA-608: Ensure service switch commands on field 2 are handled correctly
([#10666](https://github.com/google/ExoPlayer/issues/10666)).
* DASH:
* Parse `EventStream.presentationTimeOffset` from manifests
([#10460](https://github.com/google/ExoPlayer/issues/10460)).
* UI:
* Use current overrides of the player as preset in
`TrackSelectionDialogBuilder`
([#10429](https://github.com/google/ExoPlayer/issues/10429)).
* Session:
* Ensure commands are always executed in the correct order even if some
require asynchronous resolution
([#85](https://github.com/androidx/media/issues/85)).
* Add `DefaultMediaNotificationProvider.Builder` to build
`DefaultMediaNotificationProvider` instances. The builder can configure
the notification ID, the notification channel ID and the notification
channel name used by the provider. Also, add method
`DefaultMediaNotificationProvider.setSmallIcon(int)` to set the
notifications small icon.
([#104](https://github.com/androidx/media/issues/104)).
* Ensure commands sent before `MediaController.release()` are not dropped
([#99](https://github.com/androidx/media/issues/99)).
* `SimpleBitmapLoader` can load bitmap from `file://` URIs
([#108](https://github.com/androidx/media/issues/108)).
* Fix assertion that prevents `MediaController` to seek over an ad in a
period ([#122](https://github.com/androidx/media/issues/122)).
* When playback ends, the `MediaSessionService` is stopped from the
foreground and a notification is shown to restart playback of the last
played media item
([#112](https://github.com/androidx/media/issues/112)).
* Don't start a foreground service with a pending intent for pause
([#167](https://github.com/androidx/media/issues/167)).
* Manually hide the 'badge' associated with the notification created by
`DefaultNotificationProvider` on API 26 and API 27 (the badge is
automatically hidden on API 28+)
([#131](https://github.com/androidx/media/issues/131)).
* Fix bug where a second binder connection from a legacy MediaSession to a
Media3 MediaController causes IllegalStateExceptions
([#49](https://github.com/androidx/media/issues/49)).
* RTSP:
* Add H263 fragmented packet handling
([#119](https://github.com/androidx/media/pull/119)).
* Add support for MP4A-LATM
([#162](https://github.com/androidx/media/pull/162)).
* IMA:
* Add timeout for loading ad information to handle cases where the IMA SDK
gets stuck loading an ad
([#10510](https://github.com/google/ExoPlayer/issues/10510)).
* Prevent skipping mid-roll ads when seeking to the end of the content
([#10685](https://github.com/google/ExoPlayer/issues/10685)).
* Correctly calculate window duration for live streams with server-side
inserted ads, for example IMA DAI
([#10764](https://github.com/google/ExoPlayer/issues/10764)).
* FFmpeg extension:
* Add newly required flags to link FFmpeg libraries with NDK 23.1.7779620
and above ([#9933](https://github.com/google/ExoPlayer/issues/9933)).
* AV1 extension:
* Update CMake version to avoid incompatibilities with the latest Android
Studio releases
([#9933](https://github.com/google/ExoPlayer/issues/9933)).
* Cast extension:
* Implement `getDeviceInfo()` to be able to identify `CastPlayer` when
controlling playback with a `MediaController`
([#142](https://github.com/androidx/media/issues/142)).
* Transformer:
* Add muxer watchdog timer to detect when generating an output sample is
too slow.
* Remove deprecated symbols:
* Remove `Transformer.Builder.setOutputMimeType(String)`. This feature has
been removed. The MIME type will always be MP4 when the default muxer is
used.
### 1.0.0-beta02 (2022-07-21)
@ -32,6 +173,8 @@ This release corresponds to the
* RTSP:
* Add VP8 fragmented packet handling
([#110](https://github.com/androidx/media/pull/110)).
* Support frames/fragments in VP9
([#115](https://github.com/androidx/media/pull/115)).
* Leanback extension:
* Listen to `playWhenReady` changes in `LeanbackAdapter`
([10420](https://github.com/google/ExoPlayer/issues/10420)).
@ -266,6 +409,8 @@ This release corresponds to the
`DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise.
* Remove constructor `DefaultTrackSelector(ExoTrackSelection.Factory)`.
Use `DefaultTrackSelector(Context, ExoTrackSelection.Factory)` instead.
* Remove `Transformer.Builder.setContext`. The `Context` should be passed
to the `Transformer.Builder` constructor instead.
### 1.0.0-alpha03 (2022-03-14)

View File

@ -22,6 +22,9 @@ android {
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
aarMetadata {
minCompileSdk = project.ext.compileSdkVersion
}
}
compileOptions {

View File

@ -12,15 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
project.ext {
releaseVersion = '1.0.0-beta02'
releaseVersionCode = 1_000_000_1_02
releaseVersion = '1.0.0-beta03'
releaseVersionCode = 1_000_000_1_03
minSdkVersion = 16
appTargetSdkVersion = 29
appTargetSdkVersion = 33
// API version before restricting local file access.
// https://developer.android.com/training/data-storage/app-specific
mainDemoAppTargetSdkVersion = 29
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
// additional robolectric config.
targetSdkVersion = 30
compileSdkVersion = 32
dexmakerVersion = '2.28.1'
compileSdkVersion = 33
dexmakerVersion = '2.28.3'
junitVersion = '4.13.2'
// Use the same Guava version as the Android repo:
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
@ -40,7 +43,7 @@ project.ext {
androidxConstraintLayoutVersion = '2.0.4'
androidxCoreVersion = '1.7.0'
androidxFuturesVersion = '1.1.0'
androidxMediaVersion = '1.4.3'
androidxMediaVersion = '1.6.0'
androidxMedia2Version = '1.2.0'
androidxMultidexVersion = '2.0.1'
androidxRecyclerViewVersion = '1.2.1'

View File

@ -78,6 +78,9 @@ project(modulePrefix + 'lib-extractor').projectDir = new File(rootDir, 'librarie
include modulePrefix + 'lib-cast'
project(modulePrefix + 'lib-cast').projectDir = new File(rootDir, 'libraries/cast')
include modulePrefix + 'lib-effect'
project(modulePrefix + 'lib-effect').projectDir = new File(rootDir, 'libraries/effect')
include modulePrefix + 'lib-transformer'
project(modulePrefix + 'lib-transformer').projectDir = new File(rootDir, 'libraries/transformer')

View File

@ -22,8 +22,13 @@
<uses-sdk/>
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
android:largeHeap="true" android:allowBackup="false">
<application
android:name="androidx.multidex.MultiDexApplication"
android:label="@string/application_name"
android:icon="@mipmap/ic_launcher"
android:largeHeap="true"
android:allowBackup="false"
android:taskAffinity="">
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>

View File

@ -52,6 +52,7 @@ dependencies {
implementation project(modulePrefix + 'lib-exoplayer-smoothstreaming')
implementation project(modulePrefix + 'lib-ui')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
}

View File

@ -22,6 +22,7 @@
<uses-sdk/>
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name">

View File

@ -29,6 +29,7 @@ import android.opengl.GLUtils;
import androidx.media3.common.C;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log;
import java.io.IOException;
import java.util.Locale;
import javax.microedition.khronos.opengles.GL10;
@ -41,6 +42,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* package */ final class BitmapOverlayVideoProcessor
implements VideoProcessingGLSurfaceView.VideoProcessor {
private static final String TAG = "BitmapOverlayVP";
private static final int OVERLAY_WIDTH = 512;
private static final int OVERLAY_HEIGHT = 256;
@ -85,6 +87,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl");
} catch (IOException e) {
throw new IllegalStateException(e);
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to initialize the shader program", e);
return;
}
program.setBufferAttribute(
"aFramePosition",
@ -119,7 +124,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
GLUtils.texSubImage2D(
GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap);
GlUtil.checkGlError();
try {
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to populate the texture", e);
}
// Run the shader program.
GlProgram program = checkNotNull(this.program);
@ -128,16 +137,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
program.setFloatUniform("uScaleX", bitmapScaleX);
program.setFloatUniform("uScaleY", bitmapScaleY);
program.setFloatsUniform("uTexTransform", transformMatrix);
program.bindAttributesAndUniforms();
try {
program.bindAttributesAndUniforms();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to update the shader program", e);
}
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError();
try {
GlUtil.checkGlError();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to draw a frame", e);
}
}
@Override
public void release() {
if (program != null) {
program.delete();
try {
program.delete();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to delete the shader program", e);
}
}
}
}

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.demo.gl;
import static androidx.media3.common.util.Assertions.checkNotNull;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
@ -83,7 +85,8 @@ public final class MainActivity extends Activity {
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
new VideoProcessingGLSurfaceView(
context, requestSecureSurface, new BitmapOverlayVideoProcessor(context));
FrameLayout contentFrame = findViewById(R.id.exo_content_frame);
checkNotNull(playerView);
FrameLayout contentFrame = playerView.findViewById(R.id.exo_content_frame);
contentFrame.addView(videoProcessingGLSurfaceView);
this.videoProcessingGLSurfaceView = videoProcessingGLSurfaceView;
}

View File

@ -28,6 +28,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.TimedValueQueue;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
@ -70,6 +71,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
}
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
private static final String TAG = "VPGlSurfaceView";
private final VideoRenderer renderer;
private final Handler mainHandler;
@ -239,7 +241,11 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
@Override
public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {
texture = GlUtil.createExternalTexture();
try {
texture = GlUtil.createExternalTexture();
} catch (GlUtil.GlException e) {
Log.e(TAG, "Failed to create an external texture", e);
}
surfaceTexture = new SurfaceTexture(texture);
surfaceTexture.setOnFrameAvailableListener(
surfaceTexture -> {

View File

@ -11,6 +11,7 @@
// 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.
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
@ -26,7 +27,9 @@ android {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
// Not using appTargetSDKVersion to allow local file access on API 29
// and higher [Internal ref: b/191644662]
targetSdkVersion project.ext.mainDemoAppTargetSdkVersion
multiDexEnabled true
}

View File

@ -399,7 +399,7 @@
"uri": "ssai://dai.google.com/?contentSourceId=2528370&videoId=tears-of-steel&format=2&adsId=1"
},
{
"name": "HLS Live: Big Buck Bunny (mid), 3 ads each [10 s]",
"name": "HLS Live: Big Buck Bunny (mid), 3 ads [10/10/10s]",
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
},
{

View File

@ -14,6 +14,7 @@
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.media3.demo.session">
<uses-sdk/>
@ -21,10 +22,12 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Media3Demo">
android:theme="@style/Theme.Media3Demo"
tools:replace="android:name">
<activity
android:name=".MainActivity"

View File

@ -13,10 +13,25 @@
"id": "video_02",
"title": "TTML Netflix Japanese examples (IMSC1.1)",
"source": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_japanese_ttml.xml",
"album": "Video with subtitle",
"artist": "Netflix",
"artist": "Subtitles",
"genre": "Video",
"image": "https://cdn.pixabay.com/photo/2014/10/09/13/14/video-481821_960_720.png",
"subtitles": [
{
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_japanese_ttml.xml",
"subtitle_mime_type": "application/ttml+xml",
"subtitle_lang": "ja"
}
]
},
{
"id": "video_03",
"title": "MPEG-4 Timed Text",
"album": "Video with subtitle",
"artist": "Subtitles",
"genre": "Video",
"source": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4",
"image": "https://cdn.pixabay.com/photo/2014/10/09/13/14/video-481821_960_720.png"
},
{

View File

@ -34,7 +34,6 @@ import androidx.media3.session.MediaBrowser
import androidx.media3.session.SessionToken
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
class MainActivity : AppCompatActivity() {
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
@ -105,7 +104,7 @@ class MainActivity : AppCompatActivity() {
SessionToken(this, ComponentName(this, PlaybackService::class.java))
)
.buildAsync()
browserFuture.addListener({ pushRoot() }, MoreExecutors.directExecutor())
browserFuture.addListener({ pushRoot() }, ContextCompat.getMainExecutor(this))
}
private fun releaseBrowser() {
@ -132,7 +131,7 @@ class MainActivity : AppCompatActivity() {
subItemMediaList.addAll(children)
mediaListAdapter.notifyDataSetChanged()
},
MoreExecutors.directExecutor()
ContextCompat.getMainExecutor(this)
)
}

View File

@ -18,10 +18,13 @@ package androidx.media3.demo.session
import android.content.res.AssetManager
import android.net.Uri
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaItem.SubtitleConfiguration
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_GENRES
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
import androidx.media3.common.util.Util
import com.google.common.collect.ImmutableList
import org.json.JSONObject
@ -65,13 +68,13 @@ object MediaItemTree {
mediaId: String,
isPlayable: Boolean,
@MediaMetadata.FolderType folderType: Int,
subtitleConfigurations: List<SubtitleConfiguration> = mutableListOf(),
album: String? = null,
artist: String? = null,
genre: String? = null,
sourceUri: Uri? = null,
imageUri: Uri? = null,
imageUri: Uri? = null
): MediaItem {
// TODO(b/194280027): add artwork
val metadata =
MediaMetadata.Builder()
.setAlbumTitle(album)
@ -82,8 +85,10 @@ object MediaItemTree {
.setIsPlayable(isPlayable)
.setArtworkUri(imageUri)
.build()
return MediaItem.Builder()
.setMediaId(mediaId)
.setSubtitleConfigurations(subtitleConfigurations)
.setMediaMetadata(metadata)
.setUri(sourceUri)
.build()
@ -156,6 +161,19 @@ object MediaItemTree {
val title = mediaObject.getString("title")
val artist = mediaObject.getString("artist")
val genre = mediaObject.getString("genre")
val subtitleConfigurations: MutableList<SubtitleConfiguration> = mutableListOf()
if (mediaObject.has("subtitles")) {
val subtitlesJson = mediaObject.getJSONArray("subtitles")
for (i in 0 until subtitlesJson.length()) {
val subtitleObject = subtitlesJson.getJSONObject(i)
subtitleConfigurations.add(
SubtitleConfiguration.Builder(Uri.parse(subtitleObject.getString("subtitle_uri")))
.setMimeType(subtitleObject.getString("subtitle_mime_type"))
.setLanguage(subtitleObject.getString("subtitle_lang"))
.build()
)
}
}
val sourceUri = Uri.parse(mediaObject.getString("source"))
val imageUri = Uri.parse(mediaObject.getString("image"))
// key of such items in tree
@ -170,12 +188,13 @@ object MediaItemTree {
title = title,
mediaId = idInTree,
isPlayable = true,
folderType = FOLDER_TYPE_NONE,
subtitleConfigurations,
album = album,
artist = artist,
genre = genre,
sourceUri = sourceUri,
imageUri = imageUri,
folderType = FOLDER_TYPE_NONE
imageUri = imageUri
)
)
@ -188,7 +207,8 @@ object MediaItemTree {
title = album,
mediaId = albumFolderIdInTree,
isPlayable = true,
folderType = FOLDER_TYPE_PLAYLISTS
folderType = FOLDER_TYPE_ALBUMS,
subtitleConfigurations
)
)
treeNodes[ALBUM_ID]!!.addChild(albumFolderIdInTree)
@ -203,7 +223,8 @@ object MediaItemTree {
title = artist,
mediaId = artistFolderIdInTree,
isPlayable = true,
folderType = FOLDER_TYPE_PLAYLISTS
folderType = FOLDER_TYPE_ARTISTS,
subtitleConfigurations
)
)
treeNodes[ARTIST_ID]!!.addChild(artistFolderIdInTree)
@ -218,7 +239,8 @@ object MediaItemTree {
title = genre,
mediaId = genreFolderIdInTree,
isPlayable = true,
folderType = FOLDER_TYPE_PLAYLISTS
folderType = FOLDER_TYPE_GENRES,
subtitleConfigurations
)
)
treeNodes[GENRE_ID]!!.addChild(genreFolderIdInTree)

View File

@ -30,6 +30,7 @@ import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.session.MediaBrowser
@ -38,7 +39,6 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
class PlayableFolderActivity : AppCompatActivity() {
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
@ -69,10 +69,13 @@ class PlayableFolderActivity : AppCompatActivity() {
mediaList.setOnItemClickListener { _, _, position, _ ->
run {
val browser = this.browser ?: return@run
browser.setMediaItems(subItemMediaList)
browser.setMediaItems(
subItemMediaList,
/* startIndex= */ position,
/* startPositionMs= */ C.TIME_UNSET
)
browser.shuffleModeEnabled = false
browser.prepare()
browser.seekToDefaultPosition(/* windowIndex= */ position)
browser.play()
val intent = Intent(this, PlayerActivity::class.java)
startActivity(intent)
@ -132,7 +135,7 @@ class PlayableFolderActivity : AppCompatActivity() {
SessionToken(this, ComponentName(this, PlaybackService::class.java))
)
.buildAsync()
browserFuture.addListener({ displayFolder() }, MoreExecutors.directExecutor())
browserFuture.addListener({ displayFolder() }, ContextCompat.getMainExecutor(this))
}
private fun releaseBrowser() {

View File

@ -29,9 +29,11 @@ import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.media3.common.C.TRACK_TYPE_TEXT
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.Tracks
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import androidx.media3.ui.PlayerView
@ -147,6 +149,10 @@ class PlayerActivity : AppCompatActivity() {
override fun onRepeatModeChanged(repeatMode: Int) {
updateRepeatSwitchUI(repeatMode)
}
override fun onTracksChanged(tracks: Tracks) {
playerView.setShowSubtitleButton(tracks.isTypeSupported(TRACK_TYPE_TEXT))
}
}
)
}

View File

@ -20,6 +20,7 @@ android {
compileSdkVersion project.ext.compileSdkVersion
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
@ -76,11 +77,14 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:' + androidxConstraintLayoutVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-effect')
implementation project(modulePrefix + 'lib-exoplayer')
implementation project(modulePrefix + 'lib-exoplayer-dash')
implementation project(modulePrefix + 'lib-transformer')
implementation project(modulePrefix + 'lib-ui')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// For MediaPipe and its dependencies:
withMediaPipeImplementation fileTree(dir: 'libs', include: ['*.aar'])
withMediaPipeImplementation 'com.google.flogger:flogger:latest.release'

View File

@ -29,6 +29,7 @@
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat"
android:taskAffinity=""
android:requestLegacyExternalStorage="true"
tools:targetApi="29">
<activity android:name=".ConfigurationActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"

View File

@ -15,6 +15,7 @@
*/
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;
@ -27,15 +28,14 @@ import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.util.Size;
import android.util.Pair;
import androidx.media3.common.C;
import androidx.media3.common.FrameProcessingException;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.FrameProcessingException;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import androidx.media3.effect.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
@ -45,10 +45,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/
// 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;
}
/* package */ final class BitmapOverlayProcessor extends SingleFrameGlTextureProcessor {
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";
@ -57,16 +54,25 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Paint paint;
private final Bitmap overlayBitmap;
private final Bitmap logoBitmap;
private final Canvas overlayCanvas;
private final GlProgram glProgram;
private float bitmapScaleX;
private float bitmapScaleY;
private int bitmapTexId;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull Bitmap logoBitmap;
private @MonotonicNonNull GlProgram glProgram;
public BitmapOverlayProcessor() {
/**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
* @throws FrameProcessingException If a problem occurs while reading shader files.
*/
public BitmapOverlayProcessor(Context context, boolean useHdr) throws FrameProcessingException {
super(useHdr);
checkArgument(!useHdr, "BitmapOverlayProcessor does not support HDR colors.");
paint = new Paint();
paint.setTextSize(64);
paint.setAntiAlias(true);
@ -75,19 +81,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
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 =
@ -97,30 +90,46 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} 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);
try {
bitmapTexId =
GlUtil.createTexture(
BITMAP_WIDTH_HEIGHT,
BITMAP_WIDTH_HEIGHT,
/* useHighPrecisionColorComponents= */ false);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
} catch (GlUtil.GlException | IOException e) {
throw new FrameProcessingException(e);
}
// 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);
}
@Override
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
return Pair.create(inputWidth, inputHeight);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
checkStateNotNull(glProgram).use();
glProgram.use();
// Draw to the canvas and store it in a texture.
String text =
@ -137,19 +146,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
flipBitmapVertically(overlayBitmap));
GlUtil.checkGlError();
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
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);
throw new FrameProcessingException(e, presentationTimeUs);
}
}
@Override
public void release() {
if (glProgram != null) {
public void release() throws FrameProcessingException {
super.release();
try {
glProgram.delete();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}

View File

@ -18,9 +18,11 @@ package androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import android.Manifest;
import android.app.Activity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
@ -29,9 +31,14 @@ import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.media3.common.C;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
@ -59,14 +66,28 @@ public final class ConfigurationActivity extends AppCompatActivity {
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_DEBUG_PREVIEW = "enable_debug_preview";
public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping";
public static final String FORCE_INTERPRET_HDR_VIDEO_AS_SDR = "force_interpret_hdr_video_as_sdr";
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 = {
public static final String COLOR_FILTER_SELECTION = "color_filter_selection";
public static final String CONTRAST_VALUE = "contrast_value";
public static final String RGB_ADJUSTMENT_RED_SCALE = "rgb_adjustment_red_scale";
public static final String RGB_ADJUSTMENT_GREEN_SCALE = "rgb_adjustment_green_scale";
public static final String RGB_ADJUSTMENT_BLUE_SCALE = "rgb_adjustment_blue_scale";
public static final String HSL_ADJUSTMENTS_HUE = "hsl_adjustments_hue";
public static final String HSL_ADJUSTMENTS_SATURATION = "hsl_adjustments_saturation";
public static final String HSL_ADJUSTMENTS_LIGHTNESS = "hsl_adjustments_lightness";
public static final int COLOR_FILTER_GRAYSCALE = 0;
public static final int COLOR_FILTER_INVERTED = 1;
public static final int COLOR_FILTER_SEPIA = 2;
public static final int FILE_PERMISSION_REQUEST_CODE = 1;
private static final String[] PRESET_FILE_URIS = {
"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://html5demos.com/assets/dizzy.mp4",
@ -79,9 +100,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"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",
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4",
};
private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS
private static final String[] PRESET_FILE_URI_DESCRIPTIONS = { // same order as PRESET_FILE_URIS
"720p H264 video and AAC audio",
"1080p H265 video and AAC audio",
"360p H264 video and AAC audio",
@ -94,21 +115,32 @@ public final class ConfigurationActivity extends AppCompatActivity {
"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)",
"HDR (HDR10) H265 limited range video (encoding may fail)",
};
private static final String[] DEMO_EFFECTS = {
"Dizzy crop",
"Edge detector (Media Pipe)",
"Color filters",
"Map White to Green Color Lookup Table",
"RGB Adjustments",
"HSL Adjustments",
"Contrast",
"Periodic vignette",
"3D spin",
"Overlay logo & timer",
"Zoom in start",
};
private static final int PERIODIC_VIGNETTE_INDEX = 2;
private static final int COLOR_FILTERS_INDEX = 2;
private static final int RGB_ADJUSTMENTS_INDEX = 4;
private static final int HSL_ADJUSTMENT_INDEX = 5;
private static final int CONTRAST_INDEX = 6;
private static final int PERIODIC_VIGNETTE_INDEX = 7;
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 selectFileButton;
private @MonotonicNonNull ActivityResultLauncher<Intent> localFilePickerLauncher;
private @MonotonicNonNull Button selectPresetFileButton;
private @MonotonicNonNull Button selectLocalFileButton;
private @MonotonicNonNull TextView selectedFileTextView;
private @MonotonicNonNull CheckBox removeAudioCheckbox;
private @MonotonicNonNull CheckBox removeVideoCheckbox;
@ -120,13 +152,24 @@ public final class ConfigurationActivity extends AppCompatActivity {
private @MonotonicNonNull Spinner rotateSpinner;
private @MonotonicNonNull CheckBox trimCheckBox;
private @MonotonicNonNull CheckBox enableFallbackCheckBox;
private @MonotonicNonNull CheckBox enableDebugPreviewCheckBox;
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox;
private @MonotonicNonNull CheckBox forceInterpretHdrVideoAsSdrCheckBox;
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox;
private @MonotonicNonNull Button selectDemoEffectsButton;
private boolean @MonotonicNonNull [] demoEffectsSelections;
private @Nullable Uri localFileUri;
private int inputUriPosition;
private long trimStartMs;
private long trimEndMs;
private int colorFilterSelection;
private float rgbAdjustmentRedScale;
private float rgbAdjustmentGreenScale;
private float rgbAdjustmentBlueScale;
private float contrastValue;
private float hueAdjustment;
private float saturationAdjustment;
private float lightnessAdjustment;
private float periodicVignetteCenterX;
private float periodicVignetteCenterY;
private float periodicVignetteInnerRadius;
@ -139,11 +182,10 @@ public final class ConfigurationActivity extends AppCompatActivity {
findViewById(R.id.transform_button).setOnClickListener(this::startTransformation);
selectFileButton = findViewById(R.id.select_file_button);
selectFileButton.setOnClickListener(this::selectFile);
flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox);
selectedFileTextView = findViewById(R.id.selected_file_text_view);
selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]);
removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox);
removeAudioCheckbox.setOnClickListener(this::onRemoveAudio);
@ -151,7 +193,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
removeVideoCheckbox = findViewById(R.id.remove_video_checkbox);
removeVideoCheckbox.setOnClickListener(this::onRemoveVideo);
flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox);
selectPresetFileButton = findViewById(R.id.select_preset_file_button);
selectPresetFileButton.setOnClickListener(this::selectPresetFile);
selectLocalFileButton = findViewById(R.id.select_local_file_button);
selectLocalFileButton.setOnClickListener(this::selectLocalFile);
ArrayAdapter<String> audioMimeAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
@ -200,14 +246,38 @@ public final class ConfigurationActivity extends AppCompatActivity {
trimEndMs = C.TIME_UNSET;
enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox);
enableDebugPreviewCheckBox = findViewById(R.id.enable_debug_preview_checkbox);
enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox);
enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported());
findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported());
forceInterpretHdrVideoAsSdrCheckBox =
findViewById(R.id.force_interpret_hdr_video_as_sdr_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);
localFilePickerLauncher =
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
this::localFilePickerLauncherResult);
}
@Override
public void onRequestPermissionsResult(
int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == FILE_PERMISSION_REQUEST_CODE
&& grantResults.length == 1
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
launchLocalFilePicker();
} else {
Toast.makeText(
getApplicationContext(), getString(R.string.permission_denied), Toast.LENGTH_LONG)
.show();
}
}
@Override
@ -215,7 +285,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
super.onResume();
@Nullable Uri intentUri = getIntent().getData();
if (intentUri != null) {
checkNotNull(selectFileButton).setEnabled(false);
checkNotNull(selectPresetFileButton).setEnabled(false);
checkNotNull(selectLocalFileButton).setEnabled(false);
checkNotNull(selectedFileTextView).setText(intentUri.toString());
}
}
@ -237,7 +308,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"rotateSpinner",
"trimCheckBox",
"enableFallbackCheckBox",
"enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox",
"demoEffectsSelections"
})
@ -275,32 +348,85 @@ public final class ConfigurationActivity extends AppCompatActivity {
bundle.putLong(TRIM_END_MS, trimEndMs);
}
bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked());
bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked());
bundle.putBoolean(
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked());
bundle.putBoolean(
FORCE_INTERPRET_HDR_VIDEO_AS_SDR, forceInterpretHdrVideoAsSdrCheckBox.isChecked());
bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked());
bundle.putBooleanArray(DEMO_EFFECTS_SELECTIONS, demoEffectsSelections);
bundle.putInt(COLOR_FILTER_SELECTION, colorFilterSelection);
bundle.putFloat(CONTRAST_VALUE, contrastValue);
bundle.putFloat(RGB_ADJUSTMENT_RED_SCALE, rgbAdjustmentRedScale);
bundle.putFloat(RGB_ADJUSTMENT_GREEN_SCALE, rgbAdjustmentGreenScale);
bundle.putFloat(RGB_ADJUSTMENT_BLUE_SCALE, rgbAdjustmentBlueScale);
bundle.putFloat(HSL_ADJUSTMENTS_HUE, hueAdjustment);
bundle.putFloat(HSL_ADJUSTMENTS_SATURATION, saturationAdjustment);
bundle.putFloat(HSL_ADJUSTMENTS_LIGHTNESS, lightnessAdjustment);
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);
@Nullable Uri intentUri = getIntent().getData();
transformerIntent.setData(
intentUri != null ? intentUri : Uri.parse(INPUT_URIS[inputUriPosition]));
@Nullable Uri intentUri;
if (getIntent().getData() != null) {
intentUri = getIntent().getData();
} else if (localFileUri != null) {
intentUri = localFileUri;
} else {
intentUri = Uri.parse(PRESET_FILE_URIS[inputUriPosition]);
}
transformerIntent.setData(intentUri);
startActivity(transformerIntent);
}
private void selectFile(View view) {
private void selectPresetFile(View view) {
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_file_title)
.setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog)
.setTitle(R.string.select_preset_file_title)
.setSingleChoiceItems(
PRESET_FILE_URI_DESCRIPTIONS, inputUriPosition, this::selectPresetFileInDialog)
.setPositiveButton(android.R.string.ok, /* listener= */ null)
.create()
.show();
}
@RequiresNonNull("selectedFileTextView")
private void selectPresetFileInDialog(DialogInterface dialog, int which) {
inputUriPosition = which;
localFileUri = null;
selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]);
}
private void selectLocalFile(View view) {
int permissionStatus =
ActivityCompat.checkSelfPermission(
ConfigurationActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE);
if (permissionStatus != PackageManager.PERMISSION_GRANTED) {
String[] neededPermissions = {Manifest.permission.READ_EXTERNAL_STORAGE};
ActivityCompat.requestPermissions(
ConfigurationActivity.this, neededPermissions, FILE_PERMISSION_REQUEST_CODE);
} else {
launchLocalFilePicker();
}
}
private void launchLocalFilePicker() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("video/*");
checkNotNull(localFilePickerLauncher).launch(intent);
}
@RequiresNonNull("selectedFileTextView")
private void localFilePickerLauncherResult(ActivityResult result) {
Intent data = result.getData();
if (data != null) {
localFileUri = checkNotNull(data.getData());
selectedFileTextView.setText(localFileUri.toString());
}
}
private void selectDemoEffects(View view) {
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_demo_effects)
@ -316,35 +442,122 @@ public final class ConfigurationActivity extends AppCompatActivity {
return;
}
View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null);
RangeSlider radiusRangeSlider =
RangeSlider trimRangeSlider =
checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider));
radiusRangeSlider.setValues(0f, 60f); // seconds
trimRangeSlider.setValues(0f, 10f); // 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();
List<Float> trimRange = trimRangeSlider.getValues();
trimStartMs = Math.round(1000 * trimRange.get(0));
trimEndMs = Math.round(1000 * trimRange.get(1));
})
.create()
.show();
}
@RequiresNonNull("selectedFileTextView")
private void selectFileInDialog(DialogInterface dialog, int which) {
inputUriPosition = which;
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) {
if (!isChecked) {
return;
}
switch (which) {
case COLOR_FILTERS_INDEX:
controlColorFiltersSettings();
break;
case RGB_ADJUSTMENTS_INDEX:
controlRgbAdjustmentsScale();
break;
case CONTRAST_INDEX:
controlContrastSettings();
break;
case HSL_ADJUSTMENT_INDEX:
controlHslAdjustmentSettings();
break;
case PERIODIC_VIGNETTE_INDEX:
controlPeriodicVignetteSettings();
break;
}
}
private void controlColorFiltersSettings() {
new AlertDialog.Builder(/* context= */ this)
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> dialogInterface.dismiss())
.setSingleChoiceItems(
this.getResources().getStringArray(R.array.color_filter_options),
colorFilterSelection,
(DialogInterface dialogInterface, int i) -> {
checkState(
i == COLOR_FILTER_GRAYSCALE
|| i == COLOR_FILTER_INVERTED
|| i == COLOR_FILTER_SEPIA);
colorFilterSelection = i;
dialogInterface.dismiss();
})
.create()
.show();
}
private void controlRgbAdjustmentsScale() {
View dialogView =
getLayoutInflater().inflate(R.layout.rgb_adjustment_options, /* root= */ null);
Slider redScaleSlider = checkNotNull(dialogView.findViewById(R.id.rgb_adjustment_red_scale));
Slider greenScaleSlider =
checkNotNull(dialogView.findViewById(R.id.rgb_adjustment_green_scale));
Slider blueScaleSlider = checkNotNull(dialogView.findViewById(R.id.rgb_adjustment_blue_scale));
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.rgb_adjustment_options)
.setView(dialogView)
.setPositiveButton(
android.R.string.ok,
(DialogInterface dialogInterface, int i) -> {
rgbAdjustmentRedScale = redScaleSlider.getValue();
rgbAdjustmentGreenScale = greenScaleSlider.getValue();
rgbAdjustmentBlueScale = blueScaleSlider.getValue();
})
.create()
.show();
}
private void controlContrastSettings() {
View dialogView = getLayoutInflater().inflate(R.layout.contrast_options, /* root= */ null);
Slider contrastSlider = checkNotNull(dialogView.findViewById(R.id.contrast_slider));
new AlertDialog.Builder(/* context= */ this)
.setView(dialogView)
.setPositiveButton(
android.R.string.ok,
(DialogInterface dialogInterface, int i) -> contrastValue = contrastSlider.getValue())
.create()
.show();
}
private void controlHslAdjustmentSettings() {
View dialogView =
getLayoutInflater().inflate(R.layout.hsl_adjustment_options, /* root= */ null);
Slider hueAdjustmentSlider = checkNotNull(dialogView.findViewById(R.id.hsl_adjustments_hue));
Slider saturationAdjustmentSlider =
checkNotNull(dialogView.findViewById(R.id.hsl_adjustments_saturation));
Slider lightnessAdjustmentSlider =
checkNotNull(dialogView.findViewById(R.id.hsl_adjustment_lightness));
new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.hsl_adjustment_options)
.setView(dialogView)
.setPositiveButton(
android.R.string.ok,
(DialogInterface dialogInterface, int i) -> {
hueAdjustment = hueAdjustmentSlider.getValue();
saturationAdjustment = saturationAdjustmentSlider.getValue();
lightnessAdjustment = lightnessAdjustmentSlider.getValue();
})
.create()
.show();
}
private void controlPeriodicVignetteSettings() {
View dialogView =
getLayoutInflater().inflate(R.layout.periodic_vignette_options, /* root= */ null);
Slider centerXSlider =
@ -377,7 +590,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
"enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox",
"selectDemoEffectsButton"
})
@ -397,7 +612,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
"enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox",
"selectDemoEffectsButton"
})
@ -416,7 +633,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
"enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox",
"selectDemoEffectsButton"
})
@ -426,8 +645,10 @@ public final class ConfigurationActivity extends AppCompatActivity {
resolutionHeightSpinner.setEnabled(isVideoEnabled);
scaleSpinner.setEnabled(isVideoEnabled);
rotateSpinner.setEnabled(isVideoEnabled);
enableDebugPreviewCheckBox.setEnabled(isVideoEnabled);
enableRequestSdrToneMappingCheckBox.setEnabled(
isRequestSdrToneMappingSupported() && isVideoEnabled);
forceInterpretHdrVideoAsSdrCheckBox.setEnabled(isVideoEnabled);
enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
selectDemoEffectsButton.setEnabled(isVideoEnabled);
@ -438,6 +659,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
findViewById(R.id.rotate).setEnabled(isVideoEnabled);
findViewById(R.id.request_sdr_tone_mapping)
.setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled);
findViewById(R.id.force_interpret_hdr_video_as_sdr).setEnabled(isVideoEnabled);
findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled);
}

View File

@ -18,8 +18,8 @@ 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;
import androidx.media3.effect.GlMatrixTransformation;
import androidx.media3.effect.MatrixTransformation;
/**
* Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link

View File

@ -16,39 +16,29 @@
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 android.util.Pair;
import androidx.media3.common.FrameProcessingException;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.FrameProcessingException;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import androidx.media3.effect.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;
}
/* package */ final class PeriodicVignetteProcessor extends SingleFrameGlTextureProcessor {
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;
private final GlProgram glProgram;
private final float minInnerRadius;
private final float deltaInnerRadius;
/**
* Creates a new instance.
@ -61,29 +51,35 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*
* <p>The parameters are given in normalized texture coordinates from 0 to 1.
*
* @param context The {@link Context}.
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
* @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.
* @throws FrameProcessingException If a problem occurs while reading shader files.
*/
public PeriodicVignetteProcessor(
float centerX, float centerY, float minInnerRadius, float maxInnerRadius, float outerRadius) {
Context context,
boolean useHdr,
float centerX,
float centerY,
float minInnerRadius,
float maxInnerRadius,
float outerRadius)
throws FrameProcessingException {
super(useHdr);
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);
try {
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
} catch (IOException | GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
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.
@ -94,14 +90,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
return Pair.create(inputWidth, inputHeight);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try {
checkStateNotNull(glProgram).use();
glProgram.use();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US;
float innerRadius =
minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta));
@ -110,14 +107,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// 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);
throw new FrameProcessingException(e, presentationTimeUs);
}
}
@Override
public void release() {
if (glProgram != null) {
public void release() throws FrameProcessingException {
super.release();
try {
glProgram.delete();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
}
}

View File

@ -17,10 +17,13 @@ package androidx.media3.demo.transformer;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@ -28,28 +31,37 @@ import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AppCompatActivity;
import androidx.media3.common.C;
import androidx.media3.common.DebugViewProvider;
import androidx.media3.common.Effect;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Util;
import androidx.media3.effect.Contrast;
import androidx.media3.effect.GlEffect;
import androidx.media3.effect.GlTextureProcessor;
import androidx.media3.effect.HslAdjustment;
import androidx.media3.effect.RgbAdjustment;
import androidx.media3.effect.RgbFilter;
import androidx.media3.effect.RgbMatrix;
import androidx.media3.effect.SingleColorLut;
import androidx.media3.exoplayer.ExoPlayer;
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.SingleFrameGlTextureProcessor;
import androidx.media3.transformer.TransformationException;
import androidx.media3.transformer.TransformationRequest;
import androidx.media3.transformer.TransformationResult;
import androidx.media3.transformer.Transformer;
import androidx.media3.ui.AspectRatioFrameLayout;
import androidx.media3.ui.PlayerView;
import com.google.android.material.card.MaterialCardView;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.common.base.Stopwatch;
import com.google.common.base.Ticker;
@ -66,7 +78,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
public final class TransformerActivity extends AppCompatActivity {
private static final String TAG = "TransformerActivity";
private @MonotonicNonNull PlayerView playerView;
private @MonotonicNonNull Button displayInputButton;
private @MonotonicNonNull MaterialCardView inputCardView;
private @MonotonicNonNull PlayerView inputPlayerView;
private @MonotonicNonNull PlayerView outputPlayerView;
private @MonotonicNonNull TextView debugTextView;
private @MonotonicNonNull TextView informationTextView;
private @MonotonicNonNull ViewGroup progressViewGroup;
@ -75,7 +90,8 @@ public final class TransformerActivity extends AppCompatActivity {
private @MonotonicNonNull AspectRatioFrameLayout debugFrame;
@Nullable private DebugTextViewHelper debugTextViewHelper;
@Nullable private ExoPlayer player;
@Nullable private ExoPlayer inputPlayer;
@Nullable private ExoPlayer outputPlayer;
@Nullable private Transformer transformer;
@Nullable private File externalCacheFile;
@ -84,16 +100,21 @@ public final class TransformerActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.transformer_activity);
playerView = findViewById(R.id.player_view);
inputCardView = findViewById(R.id.input_card_view);
inputPlayerView = findViewById(R.id.input_player_view);
outputPlayerView = findViewById(R.id.output_player_view);
debugTextView = findViewById(R.id.debug_text_view);
informationTextView = findViewById(R.id.information_text_view);
progressViewGroup = findViewById(R.id.progress_view_group);
progressIndicator = findViewById(R.id.progress_indicator);
debugFrame = findViewById(R.id.debug_aspect_ratio_frame_layout);
displayInputButton = findViewById(R.id.display_input_button);
displayInputButton.setOnClickListener(this::toggleInputVideoDisplay);
transformationStopwatch =
Stopwatch.createUnstarted(
new Ticker() {
@Override
public long read() {
return android.os.SystemClock.elapsedRealtimeNanos();
}
@ -107,13 +128,17 @@ public final class TransformerActivity extends AppCompatActivity {
checkNotNull(progressIndicator);
checkNotNull(informationTextView);
checkNotNull(transformationStopwatch);
checkNotNull(playerView);
checkNotNull(inputCardView);
checkNotNull(inputPlayerView);
checkNotNull(outputPlayerView);
checkNotNull(debugTextView);
checkNotNull(progressViewGroup);
checkNotNull(debugFrame);
checkNotNull(displayInputButton);
startTransformation();
playerView.onResume();
inputPlayerView.onResume();
outputPlayerView.onResume();
}
@Override
@ -127,7 +152,8 @@ public final class TransformerActivity extends AppCompatActivity {
// stop watch to be stopped in a transformer callback.
checkNotNull(transformationStopwatch).reset();
checkNotNull(playerView).onPause();
checkNotNull(inputPlayerView).onPause();
checkNotNull(outputPlayerView).onPause();
releasePlayer();
checkNotNull(externalCacheFile).delete();
@ -135,7 +161,10 @@ public final class TransformerActivity extends AppCompatActivity {
}
@RequiresNonNull({
"playerView",
"inputCardView",
"inputPlayerView",
"outputPlayerView",
"displayInputButton",
"debugTextView",
"informationTextView",
"progressIndicator",
@ -161,7 +190,8 @@ public final class TransformerActivity extends AppCompatActivity {
throw new IllegalStateException(e);
}
informationTextView.setText(R.string.transformation_started);
playerView.setVisibility(View.GONE);
inputCardView.setVisibility(View.GONE);
outputPlayerView.setVisibility(View.GONE);
Handler mainHandler = new Handler(getMainLooper());
ProgressHolder progressHolder = new ProgressHolder();
mainHandler.post(
@ -200,20 +230,11 @@ public final class TransformerActivity extends AppCompatActivity {
return mediaItemBuilder.build();
}
// Create a cache file, resetting it if it already exists.
private File createExternalCacheFile(String fileName) throws IOException {
File file = new File(getExternalCacheDir(), fileName);
if (file.exists() && !file.delete()) {
throw new IllegalStateException("Could not delete the previous transformer output file");
}
if (!file.createNewFile()) {
throw new IllegalStateException("Could not create the transformer output file");
}
return file;
}
@RequiresNonNull({
"playerView",
"inputCardView",
"inputPlayerView",
"outputPlayerView",
"displayInputButton",
"debugTextView",
"informationTextView",
"transformationStopwatch",
@ -251,6 +272,8 @@ public final class TransformerActivity extends AppCompatActivity {
requestBuilder.setEnableRequestSdrToneMapping(
bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING));
requestBuilder.experimental_setForceInterpretHdrVideoAsSdr(
bundle.getBoolean(ConfigurationActivity.FORCE_INTERPRET_HDR_VIDEO_AS_SDR));
requestBuilder.experimental_setEnableHdrEditing(
bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING));
transformerBuilder
@ -258,62 +281,14 @@ public final class TransformerActivity extends AppCompatActivity {
.setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO))
.setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO))
.setEncoderFactory(
new DefaultEncoderFactory(
EncoderSelector.DEFAULT,
/* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK)));
new DefaultEncoderFactory.Builder(this.getApplicationContext())
.setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))
.build());
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());
transformerBuilder.setVideoEffects(createVideoEffectsListFromBundle(bundle));
if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) {
transformerBuilder.setDebugViewProvider(new DemoDebugViewProvider());
}
}
return transformerBuilder
@ -322,7 +297,7 @@ public final class TransformerActivity extends AppCompatActivity {
@Override
public void onTransformationCompleted(
MediaItem mediaItem, TransformationResult transformationResult) {
TransformerActivity.this.onTransformationCompleted(filePath);
TransformerActivity.this.onTransformationCompleted(filePath, mediaItem);
}
@Override
@ -331,10 +306,151 @@ public final class TransformerActivity extends AppCompatActivity {
TransformerActivity.this.onTransformationError(exception);
}
})
.setDebugViewProvider(new DemoDebugViewProvider())
.build();
}
/** Creates a cache file, resetting it if it already exists. */
private File createExternalCacheFile(String fileName) throws IOException {
File file = new File(getExternalCacheDir(), fileName);
if (file.exists() && !file.delete()) {
throw new IllegalStateException("Could not delete the previous transformer output file");
}
if (!file.createNewFile()) {
throw new IllegalStateException("Could not create the transformer output file");
}
return file;
}
private ImmutableList<Effect> createVideoEffectsListFromBundle(Bundle bundle) {
@Nullable
boolean[] selectedEffects =
bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS);
if (selectedEffects == null) {
return ImmutableList.of();
}
ImmutableList.Builder<Effect> effects = new ImmutableList.Builder<>();
if (selectedEffects[0]) {
effects.add(MatrixTransformationFactory.createDizzyCropEffect());
}
if (selectedEffects[1]) {
try {
Class<?> clazz = Class.forName("androidx.media3.demo.transformer.MediaPipeProcessor");
Constructor<?> constructor =
clazz.getConstructor(
Context.class,
boolean.class,
String.class,
boolean.class,
String.class,
String.class);
effects.add(
(GlEffect)
(Context context, boolean useHdr) -> {
try {
return (GlTextureProcessor)
constructor.newInstance(
context,
useHdr,
/* graphName= */ "edge_detector_mediapipe_graph.binarypb",
/* isSingleFrameGraph= */ true,
/* 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]) {
switch (bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION)) {
case ConfigurationActivity.COLOR_FILTER_GRAYSCALE:
effects.add(RgbFilter.createGrayscaleFilter());
break;
case ConfigurationActivity.COLOR_FILTER_INVERTED:
effects.add(RgbFilter.createInvertedFilter());
break;
case ConfigurationActivity.COLOR_FILTER_SEPIA:
// W3C Sepia RGBA matrix with sRGB as a target color space:
// https://www.w3.org/TR/filter-effects-1/#sepiaEquivalent
// The matrix is defined for the sRGB color space and the Transformer library
// uses a linear RGB color space internally. Meaning this is only for demonstration
// purposes and it does not display a correct sepia frame.
float[] sepiaMatrix = {
0.393f, 0.349f, 0.272f, 0, 0.769f, 0.686f, 0.534f, 0, 0.189f, 0.168f, 0.131f, 0, 0, 0,
0, 1
};
effects.add((RgbMatrix) (presentationTimeUs, useHdr) -> sepiaMatrix);
break;
default:
throw new IllegalStateException(
"Unexpected color filter "
+ bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION));
}
}
if (selectedEffects[3]) {
int length = 3;
int[][][] mapWhiteToGreenLut = new int[length][length][length];
int scale = 255 / (length - 1);
for (int r = 0; r < length; r++) {
for (int g = 0; g < length; g++) {
for (int b = 0; b < length; b++) {
mapWhiteToGreenLut[r][g][b] =
Color.rgb(/* red= */ r * scale, /* green= */ g * scale, /* blue= */ b * scale);
}
}
}
mapWhiteToGreenLut[length - 1][length - 1][length - 1] = Color.GREEN;
effects.add(SingleColorLut.createFromCube(mapWhiteToGreenLut));
}
if (selectedEffects[4]) {
effects.add(
new RgbAdjustment.Builder()
.setRedScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_RED_SCALE))
.setGreenScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_GREEN_SCALE))
.setBlueScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_BLUE_SCALE))
.build());
}
if (selectedEffects[5]) {
effects.add(
new HslAdjustment.Builder()
.adjustHue(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_HUE))
.adjustSaturation(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_SATURATION))
.adjustLightness(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_LIGHTNESS))
.build());
}
if (selectedEffects[6]) {
effects.add(new Contrast(bundle.getFloat(ConfigurationActivity.CONTRAST_VALUE)));
}
if (selectedEffects[7]) {
effects.add(
(GlEffect)
(Context context, boolean useHdr) ->
new PeriodicVignetteProcessor(
context,
useHdr,
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[8]) {
effects.add(MatrixTransformationFactory.createSpin3dEffect());
}
if (selectedEffects[9]) {
effects.add((GlEffect) BitmapOverlayProcessor::new);
}
if (selectedEffects[10]) {
effects.add(MatrixTransformationFactory.createZoomInTransition());
}
return effects.build();
}
@RequiresNonNull({
"informationTextView",
"progressViewGroup",
@ -346,44 +462,66 @@ public final class TransformerActivity extends AppCompatActivity {
informationTextView.setText(R.string.transformation_error);
progressViewGroup.setVisibility(View.GONE);
debugFrame.removeAllViews();
Toast.makeText(
TransformerActivity.this, "Transformation error: " + exception, Toast.LENGTH_LONG)
Toast.makeText(getApplicationContext(), "Transformation error: " + exception, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Transformation error", exception);
}
@RequiresNonNull({
"playerView",
"inputCardView",
"inputPlayerView",
"outputPlayerView",
"displayInputButton",
"debugTextView",
"informationTextView",
"progressViewGroup",
"debugFrame",
"transformationStopwatch",
})
private void onTransformationCompleted(String filePath) {
private void onTransformationCompleted(String filePath, MediaItem inputMediaItem) {
transformationStopwatch.stop();
informationTextView.setText(
getString(
R.string.transformation_completed, transformationStopwatch.elapsed(TimeUnit.SECONDS)));
progressViewGroup.setVisibility(View.GONE);
debugFrame.removeAllViews();
playerView.setVisibility(View.VISIBLE);
playMediaItem(MediaItem.fromUri("file://" + filePath));
inputCardView.setVisibility(View.VISIBLE);
outputPlayerView.setVisibility(View.VISIBLE);
displayInputButton.setVisibility(View.VISIBLE);
playMediaItems(inputMediaItem, MediaItem.fromUri("file://" + filePath));
Log.d(TAG, "Output file path: file://" + filePath);
}
@RequiresNonNull({"playerView", "debugTextView"})
private void playMediaItem(MediaItem mediaItem) {
playerView.setPlayer(null);
@RequiresNonNull({
"inputCardView",
"inputPlayerView",
"outputPlayerView",
"debugTextView",
})
private void playMediaItems(MediaItem inputMediaItem, MediaItem outputMediaItem) {
inputPlayerView.setPlayer(null);
outputPlayerView.setPlayer(null);
releasePlayer();
ExoPlayer player = new ExoPlayer.Builder(/* context= */ this).build();
playerView.setPlayer(player);
player.setMediaItem(mediaItem);
player.play();
player.prepare();
this.player = player;
debugTextViewHelper = new DebugTextViewHelper(player, debugTextView);
ExoPlayer inputPlayer = new ExoPlayer.Builder(/* context= */ this).build();
inputPlayerView.setPlayer(inputPlayer);
inputPlayerView.setControllerAutoShow(false);
inputPlayer.setMediaItem(inputMediaItem);
inputPlayer.prepare();
this.inputPlayer = inputPlayer;
inputPlayer.setVolume(0f);
ExoPlayer outputPlayer = new ExoPlayer.Builder(/* context= */ this).build();
outputPlayerView.setPlayer(outputPlayer);
outputPlayerView.setControllerAutoShow(false);
outputPlayer.setMediaItem(outputMediaItem);
outputPlayer.prepare();
this.outputPlayer = outputPlayer;
inputPlayer.play();
outputPlayer.play();
debugTextViewHelper = new DebugTextViewHelper(outputPlayer, debugTextView);
debugTextViewHelper.start();
}
@ -392,9 +530,13 @@ public final class TransformerActivity extends AppCompatActivity {
debugTextViewHelper.stop();
debugTextViewHelper = null;
}
if (player != null) {
player.release();
player = null;
if (inputPlayer != null) {
inputPlayer.release();
inputPlayer = null;
}
if (outputPlayer != null) {
outputPlayer.release();
outputPlayer = null;
}
}
@ -411,11 +553,45 @@ public final class TransformerActivity extends AppCompatActivity {
Toast.makeText(getApplicationContext(), getString(messageResource), Toast.LENGTH_LONG).show();
}
private final class DemoDebugViewProvider implements Transformer.DebugViewProvider {
@RequiresNonNull({
"inputCardView",
"displayInputButton",
})
private void toggleInputVideoDisplay(View view) {
if (inputCardView.getVisibility() == View.GONE) {
inputCardView.setVisibility(View.VISIBLE);
displayInputButton.setText(getString(R.string.hide_input_video));
} else if (inputCardView.getVisibility() == View.VISIBLE) {
checkNotNull(inputPlayer).pause();
inputCardView.setVisibility(View.GONE);
displayInputButton.setText(getString(R.string.show_input_video));
}
}
private final class DemoDebugViewProvider implements DebugViewProvider {
private @MonotonicNonNull SurfaceView surfaceView;
private int width;
private int height;
public DemoDebugViewProvider() {
width = C.LENGTH_UNSET;
height = C.LENGTH_UNSET;
}
@Nullable
@Override
public SurfaceView getDebugPreviewSurfaceView(int width, int height) {
checkState(
surfaceView == null || (this.width == width && this.height == height),
"Transformer should not change the output size mid-transformation.");
if (surfaceView != null) {
return surfaceView;
}
this.width = width;
this.height = height;
// Update the UI on the main thread and wait for the output surface to be available.
CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1);
SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
@ -452,6 +628,7 @@ public final class TransformerActivity extends AppCompatActivity {
Thread.currentThread().interrupt();
return null;
}
this.surfaceView = surfaceView;
return surfaceView;
}
}

View File

@ -34,16 +34,26 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/select_file_button"
android:id="@+id/select_preset_file_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_file_title"
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view"
android:layout_marginStart="8dp"
android:text="@string/select_preset_file_title"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view" />
<Button
android:id="@+id/select_local_file_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginEnd="8dp"
android:text="@string/select_local_file_title"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view" />
<TextView
android:id="@+id/selected_file_text_view"
android:layout_width="0dp"
@ -57,52 +67,50 @@
android:gravity="center"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/select_file_button" />
app:layout_constraintTop_toBottomOf="@+id/select_preset_file_button" />
<androidx.core.widget.NestedScrollView
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/selected_file_text_view"
app:layout_constraintBottom_toTopOf="@+id/select_demo_effects_button">
<TableLayout
android:layout_width="fill_parent"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:stretchColumns="0"
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" >
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/remove_audio" />
<CheckBox
android:id="@+id/remove_audio_checkbox"
android:layout_gravity="right"/>
android:layout_gravity="end"
android:id="@+id/remove_audio_checkbox"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TableRow android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/remove_video"/>
<CheckBox
android:id="@+id/remove_video_checkbox"
android:layout_gravity="right" />
android:layout_gravity="end" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/flatten_for_slow_motion"/>
<CheckBox
android:id="@+id/flatten_for_slow_motion_checkbox"
android:layout_gravity="right" />
android:layout_gravity="end" />
</TableRow>
<TableRow
android:layout_weight="1"
@ -160,44 +168,64 @@
android:gravity="right" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:id="@+id/trim"
android:text="@string/trim" />
<CheckBox
android:id="@+id/trim_checkbox"
android:layout_gravity="right" />
android:layout_gravity="end" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/enable_fallback" />
<CheckBox
android:id="@+id/enable_fallback_checkbox"
android:layout_gravity="right"
android:layout_gravity="end"
android:checked="true"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:text="@string/enable_debug_preview" />
<CheckBox
android:id="@+id/enable_debug_preview_checkbox"
android:layout_gravity="end"
android:checked="true"/>
</TableRow>
<TableRow
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
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" />
android:layout_gravity="end" />
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:id="@+id/hdr_editing"
android:text="@string/hdr_editing" />
<CheckBox
android:id="@+id/hdr_editing_checkbox"
android:layout_gravity="right" />
android:layout_gravity="end" />
</TableRow>
<TableRow
android:layout_weight="1">
<TextView
android:layout_gravity="center_vertical"
android:id="@+id/force_interpret_hdr_video_as_sdr"
android:text="@string/force_interpret_hdr_video_as_sdr" />
<CheckBox
android:id="@+id/force_interpret_hdr_video_as_sdr_checkbox"
android:layout_gravity="end" />
</TableRow>
</TableLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -0,0 +1,49 @@
<?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/contrast_value" />
<com.google.android.material.slider.Slider
android:id="@+id/contrast_slider"
android:valueFrom="-1.0"
android:value="0.0"
android:valueTo="1.0"
android:layout_gravity="right"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,73 @@
<?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/hue_adjustment" />
<com.google.android.material.slider.Slider
android:id="@+id/hsl_adjustments_hue"
android:valueFrom="-360"
android:value="0"
android:valueTo="360"
android:layout_gravity="right"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/saturation_adjustment" />
<com.google.android.material.slider.Slider
android:id="@+id/hsl_adjustments_saturation"
android:valueFrom="-100"
android:value="0"
android:valueTo="100"
android:layout_gravity="right"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/lightness_adjustment" />
<com.google.android.material.slider.Slider
android:id="@+id/hsl_adjustment_lightness"
android:valueFrom="-100"
android:value="0"
android:valueTo="100"
android:layout_gravity="right"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,76 @@
<?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/rgb_adjustment_scale_red" />
<com.google.android.material.slider.Slider
android:id="@+id/rgb_adjustment_red_scale"
android:valueFrom="0"
android:value="1"
android:valueTo="2"
android:layout_gravity="right"
app:labelBehavior="gone"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/rgb_adjustment_scale_green" />
<com.google.android.material.slider.Slider
android:id="@+id/rgb_adjustment_green_scale"
android:valueFrom="0"
android:value="1"
android:valueTo="2"
android:layout_gravity="right"
app:labelBehavior="gone"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/rgb_adjustment_scale_blue" />
<com.google.android.material.slider.Slider
android:id="@+id/rgb_adjustment_blue_scale"
android:valueFrom="0"
android:value="1"
android:valueTo="2"
android:layout_gravity="right"
app:labelBehavior="gone"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -29,42 +29,113 @@
app:cardElevation="2dp"
android:gravity="center_vertical" >
<TextView
android:id="@+id/information_text_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
<LinearLayout
android:layout_width="match_parent"
android:orientation="vertical"
android:padding="8dp" />
android:layout_height="wrap_content">
<TextView
android:id="@+id/information_text_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="8dp" />
<Button
android:id="@+id/display_input_button"
android:visibility="gone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/hide_input_video"
android:layout_margin="8dp" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/input_card_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="16dp"
app:cardCornerRadius="4dp"
app:cardElevation="2dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:padding="8dp"
android:text="@string/input_video" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<androidx.media3.ui.PlayerView
android:id="@+id/input_player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<androidx.media3.ui.AspectRatioFrameLayout
android:id="@+id/input_debug_aspect_ratio_frame_layout"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
</LinearLayout>
<TextView
android:id="@+id/debug_text_view"
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:id="@+id/output_card_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_margin="16dp"
app:cardCornerRadius="4dp"
app:cardElevation="2dp">
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/output_video" />
<TextView
android:id="@+id/debug_text_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:textSize="10sp"
tools:ignore="SmallSp"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_gravity="center"
android:layout_height="wrap_content">
<androidx.media3.ui.PlayerView
android:id="@+id/output_player_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="10sp"
tools:ignore="SmallSp"/>
android:layout_height="wrap_content" />
<LinearLayout
android:id="@+id/progress_view_group"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_gravity="bottom"
android:padding="8dp"
android:orientation="vertical">
@ -96,5 +167,9 @@
</LinearLayout>
</FrameLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

View File

@ -17,7 +17,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" translatable="false">Transformer Demo</string>
<string name="configuration" translatable="false">Configuration</string>
<string name="select_file_title" translatable="false">Choose file</string>
<string name="select_preset_file_title" translatable="false">Choose preset file</string>
<string name="select_local_file_title">Choose local file</string>
<string name="remove_audio" translatable="false">Remove audio</string>
<string name="remove_video" translatable="false">Remove video</string>
<string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string>
@ -27,9 +28,11 @@
<string name="scale" translatable="false">Scale video</string>
<string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="enable_fallback" translatable="false">Enable fallback</string>
<string name="enable_debug_preview" translatable="false">Enable debug preview</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="force_interpret_hdr_video_as_sdr" translatable="false">[Experimental] Force interpret HDR video as SDR (API 29+)</string>
<string name="hdr_editing" translatable="false">[Experimental] HDR editing (API 31+)</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>
@ -40,8 +43,27 @@
<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_error" translatable="false">Transformation error</string>
<string name="trim_range">Bounds in seconds</string>
<string-array name="color_filter_options">
<item>Grayscale</item>
<item>Inverted</item>
<item>Sepia</item>
</string-array>
<string name="contrast_value" translatable="false">Contrast value</string>
<string name="rgb_adjustment_options" translatable="false">Scale RGB Channels individually</string>
<string name="rgb_adjustment_scale_red" translatable="false">Scale red</string>
<string name="rgb_adjustment_scale_green" translatable="false">Scale green</string>
<string name="rgb_adjustment_scale_blue" translatable="false">Scale blue</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>
<string name="hsl_adjustment_options" translatable="false">HSL adjustment options</string>
<string name="hue_adjustment">Hue adjustment</string>
<string name="saturation_adjustment">Saturation adjustment</string>
<string name="lightness_adjustment">Lightness adjustment</string>
<string name="input_video">Input video:</string>
<string name="output_video">Output video:</string>
<string name="permission_denied">Permission Denied</string>
<string name="hide_input_video">Hide input video</string>
<string name="show_input_video">Show input video</string>
</resources>

View File

@ -15,32 +15,37 @@
*/
package androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
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.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.FrameProcessingException;
import androidx.media3.common.util.LibraryLoader;
import androidx.media3.transformer.FrameProcessingException;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import androidx.media3.common.util.Util;
import androidx.media3.effect.GlTextureProcessor;
import androidx.media3.effect.TextureInfo;
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;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
/**
* 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 {
/** Runs a MediaPipe graph on input frames. */
/* package */ final class MediaPipeProcessor implements GlTextureProcessor {
private static final String THREAD_NAME = "Demo:MediaPipeProcessor";
private static final long RELEASE_WAIT_TIME_MS = 100;
private static final long RETRY_WAIT_TIME_MS = 1;
private static final LibraryLoader LOADER =
new LibraryLoader("mediapipe_jni") {
@ -60,116 +65,218 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
}
}
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 FrameProcessor frameProcessor;
private final ConcurrentHashMap<TextureInfo, TextureFrame> outputFrames;
private final boolean isSingleFrameGraph;
@Nullable private final ExecutorService singleThreadExecutorService;
private final Queue<Future<?>> futures;
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;
private InputListener inputListener;
private OutputListener outputListener;
private ErrorListener errorListener;
private boolean acceptedFrame;
/**
* Creates a new texture processor that wraps a MediaPipe graph.
*
* <p>If {@code isSingleFrameGraph} is {@code false}, the {@code MediaPipeProcessor} may waste CPU
* time by continuously attempting to queue input frames to MediaPipe until they are accepted or
* waste memory if MediaPipe accepts and stores many frames internally.
*
* @param context The {@link Context}.
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
* @param graphName Name of a MediaPipe graph asset to load.
* @param isSingleFrameGraph Whether the MediaPipe graph will eventually produce one output frame
* each time an input frame (and no other input) has been queued.
* @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) {
public MediaPipeProcessor(
Context context,
boolean useHdr,
String graphName,
boolean isSingleFrameGraph,
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);
// TODO(b/227624622): Confirm whether MediaPipeProcessor could support HDR colors.
checkArgument(!useHdr, "MediaPipeProcessor does not support HDR colors.");
this.isSingleFrameGraph = isSingleFrameGraph;
singleThreadExecutorService =
isSingleFrameGraph ? null : Util.newSingleThreadExecutor(THREAD_NAME);
futures = new ArrayDeque<>();
inputListener = new InputListener() {};
outputListener = new OutputListener() {};
errorListener = (frameProcessingException) -> {};
EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext());
frameProcessor =
new FrameProcessor(
context, eglManager.getNativeContext(), graphName, inputStreamName, outputStreamName);
outputFrames = new ConcurrentHashMap<>();
// OnWillAddFrameListener is called on the same thread as frameProcessor.onNewFrame(...), so no
// synchronization is needed for acceptedFrame.
frameProcessor.setOnWillAddFrameListener((long timestamp) -> acceptedFrame = true);
}
// Unblock drawFrame when there is an output frame or an error.
@Override
public void setInputListener(InputListener inputListener) {
this.inputListener = inputListener;
if (!isSingleFrameGraph || outputFrames.isEmpty()) {
inputListener.onReadyToAcceptInputFrame();
}
}
@Override
public void setOutputListener(OutputListener outputListener) {
this.outputListener = outputListener;
frameProcessor.setConsumer(
frame -> {
outputFrame = frame;
frameProcessorConditionVariable.open();
TextureInfo texture =
new TextureInfo(
frame.getTextureName(),
/* fboId= */ C.INDEX_UNSET,
frame.getWidth(),
frame.getHeight());
outputFrames.put(texture, frame);
outputListener.onOutputFrameAvailable(texture, frame.getTimestamp());
});
}
@Override
public void setErrorListener(ErrorListener errorListener) {
this.errorListener = errorListener;
frameProcessor.setAsynchronousErrorListener(
error -> {
frameProcessorPendingError = error;
frameProcessorConditionVariable.open();
});
error -> errorListener.onFrameProcessingError(new FrameProcessingException(error)));
}
@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);
public void queueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
AppTextureFrame appTextureFrame =
new AppTextureFrame(inputTexture.texId, inputTexture.width, inputTexture.height);
// TODO(b/238302213): Handle timestamps restarting from 0 when applying effects to a playlist.
// MediaPipe will fail if the timestamps are not monotonically increasing.
// Also make sure that a MediaPipe graph producing additional frames only starts producing
// frames for the next MediaItem after receiving the first frame of that MediaItem as input
// to avoid MediaPipe producing extra frames after the last MediaItem has ended.
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();
if (isSingleFrameGraph) {
boolean acceptedFrame = maybeQueueInputFrameSynchronous(appTextureFrame, inputTexture);
checkState(
acceptedFrame,
"queueInputFrame must only be called when a new input frame can be accepted");
return;
}
if (frameProcessorPendingError != null) {
throw new FrameProcessingException(frameProcessorPendingError);
}
// TODO(b/241782273): Avoid retrying continuously until the frame is accepted by using a
// currently non-existent MediaPipe API to be notified when MediaPipe has capacity to accept a
// new frame.
queueInputFrameAsynchronous(appTextureFrame, inputTexture);
}
// Copy from MediaPipe's output texture to the current output.
private boolean maybeQueueInputFrameSynchronous(
AppTextureFrame appTextureFrame, TextureInfo inputTexture) {
acceptedFrame = false;
frameProcessor.onNewFrame(appTextureFrame);
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();
appTextureFrame.waitUntilReleasedWithGpuSync();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
errorListener.onFrameProcessingError(new FrameProcessingException(e));
}
if (acceptedFrame) {
inputListener.onInputFrameProcessed(inputTexture);
}
return acceptedFrame;
}
private void queueInputFrameAsynchronous(
AppTextureFrame appTextureFrame, TextureInfo inputTexture) {
removeFinishedFutures();
futures.add(
checkStateNotNull(singleThreadExecutorService)
.submit(
() -> {
while (!maybeQueueInputFrameSynchronous(appTextureFrame, inputTexture)) {
try {
Thread.sleep(RETRY_WAIT_TIME_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (errorListener != null) {
errorListener.onFrameProcessingError(new FrameProcessingException(e));
}
}
}
inputListener.onReadyToAcceptInputFrame();
}));
}
@Override
public void releaseOutputFrame(TextureInfo outputTexture) {
checkStateNotNull(outputFrames.get(outputTexture)).release();
if (isSingleFrameGraph) {
inputListener.onReadyToAcceptInputFrame();
}
}
@Override
public void release() {
checkStateNotNull(frameProcessor).close();
if (isSingleFrameGraph) {
frameProcessor.close();
return;
}
Queue<Future<?>> futures = checkStateNotNull(this.futures);
while (!futures.isEmpty()) {
futures.remove().cancel(/* mayInterruptIfRunning= */ false);
}
ExecutorService singleThreadExecutorService =
checkStateNotNull(this.singleThreadExecutorService);
singleThreadExecutorService.shutdown();
try {
if (!singleThreadExecutorService.awaitTermination(RELEASE_WAIT_TIME_MS, MILLISECONDS)) {
errorListener.onFrameProcessingError(new FrameProcessingException("Release timed out"));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
errorListener.onFrameProcessingError(new FrameProcessingException(e));
}
frameProcessor.close();
}
@Override
public final void signalEndOfCurrentInputStream() {
if (isSingleFrameGraph) {
frameProcessor.waitUntilIdle();
outputListener.onCurrentOutputStreamEnded();
return;
}
removeFinishedFutures();
futures.add(
checkStateNotNull(singleThreadExecutorService)
.submit(
() -> {
frameProcessor.waitUntilIdle();
outputListener.onCurrentOutputStreamEnded();
}));
}
private void removeFinishedFutures() {
while (!futures.isEmpty()) {
if (!futures.element().isDone()) {
return;
}
try {
futures.remove().get();
} catch (ExecutionException e) {
errorListener.onFrameProcessingError(new FrameProcessingException(e));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
errorListener.onFrameProcessingError(new FrameProcessingException(e));
}
}
}
}

View File

@ -21,7 +21,7 @@ class CombinedJavadocPlugin implements Plugin<Project> {
// Dackka snapshots are listed at https://androidx.dev/dackka/builds.
static final String DACKKA_JAR_URL =
"https://androidx.dev/dackka/builds/8003564/artifacts/dackka-0.0.14.jar"
"https://androidx.dev/dackka/builds/9221390/artifacts/dackka-1.0.4-all.jar"
@Override
void apply(Project project) {
@ -58,6 +58,11 @@ class CombinedJavadocPlugin implements Plugin<Project> {
"media-" + project.ext.androidxMediaVersion + "-api.jar")) {
return false;
}
if (file ==~ /.*\/core-.\..\..-api.jar$/
&& !file.path.endsWith(
"core-" + project.ext.androidxCoreVersion + "-api.jar")) {
return false;
}
return true;
}
classpath +=
@ -115,11 +120,16 @@ class CombinedJavadocPlugin implements Plugin<Project> {
def sourcesString = project.files(sources.flatten())
.filter({ f -> project.file(f).exists() }).join(";")
def dependenciesString = project.files(dependencies).asPath.replace(':', ';')
def sourceSet = [
"-src", sourcesString,
"-classpath", dependenciesString,
"-documentedVisibilities", "PUBLIC;PROTECTED"
].join(" ")
args("-moduleName", "",
"-outputDir", "$dackkaOutputDir",
"-globalLinks", "$globalLinksString",
"-loggingLevel", "WARN",
"-sourceSet", "-src $sourcesString -classpath $dependenciesString",
"-sourceSet", "$sourceSet",
"-offlineMode")
environment("DEVSITE_TENANT", "androidx/media3")
}

View File

@ -47,6 +47,7 @@ import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.android.gms.cast.CastStatusCodes;
@ -82,6 +83,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@UnstableApi
public final class CastPlayer extends BasePlayer {
/** The {@link DeviceInfo} returned by {@link #getDeviceInfo() this player}. */
public static final DeviceInfo DEVICE_INFO =
new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 0);
static {
MediaLibraryInfo.registerModule("media3.cast");
}
@ -723,16 +728,22 @@ public final class CastPlayer extends BasePlayer {
return VideoSize.UNKNOWN;
}
/** This method is not supported and returns {@link Size#UNKNOWN}. */
@Override
public Size getSurfaceSize() {
return Size.UNKNOWN;
}
/** This method is not supported and returns an empty {@link CueGroup}. */
@Override
public CueGroup getCurrentCues() {
return CueGroup.EMPTY;
return CueGroup.EMPTY_TIME_ZERO;
}
/** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */
/** This method always returns {@link CastPlayer#DEVICE_INFO}. */
@Override
public DeviceInfo getDeviceInfo() {
return DeviceInfo.UNKNOWN;
return DEVICE_INFO;
}
/** This method is not supported and always returns {@code 0}. */

View File

@ -62,6 +62,7 @@ import static org.mockito.MockitoAnnotations.initMocks;
import android.net.Uri;
import androidx.media3.common.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.MimeTypes;
@ -1864,6 +1865,14 @@ public class CastPlayerTest {
verify(mockListener, never()).onMediaMetadataChanged(any());
}
@Test
public void getDeviceInfo_returnsCorrectDeviceInfoWithPlaybackTypeRemote() {
DeviceInfo deviceInfo = castPlayer.getDeviceInfo();
assertThat(deviceInfo).isEqualTo(CastPlayer.DEVICE_INFO);
assertThat(deviceInfo.playbackType).isEqualTo(DeviceInfo.PLAYBACK_TYPE_REMOTE);
}
private int[] createMediaQueueItemIds(int numberOfIds) {
int[] mediaQueueItemIds = new int[numberOfIds];
for (int i = 0; i < numberOfIds; i++) {

View File

@ -25,6 +25,7 @@ import android.view.View;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -77,6 +78,7 @@ public final class AdOverlayInfo {
*
* @return This builder, for convenience.
*/
@CanIgnoreReturnValue
public Builder setDetailedReason(@Nullable String detailedReason) {
this.detailedReason = detailedReason;
return this;

View File

@ -64,6 +64,13 @@ public final class AdPlaybackState implements Bundleable {
public final long timeUs;
/** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */
public final int count;
/**
* The original number of ads in the ad group in case the ad group is only partially available,
* or {@link C#LENGTH_UNSET} if unknown. An ad can be partially available when a server side
* inserted ad live stream is joined while an ad is already playing and some ad information is
* missing.
*/
public final int originalCount;
/** The URI of each ad in the ad group. */
public final @NullableType Uri[] uris;
/** The state of each ad in the ad group. */
@ -88,6 +95,7 @@ public final class AdPlaybackState implements Bundleable {
this(
timeUs,
/* count= */ C.LENGTH_UNSET,
/* originalCount= */ C.LENGTH_UNSET,
/* states= */ new int[0],
/* uris= */ new Uri[0],
/* durationsUs= */ new long[0],
@ -98,6 +106,7 @@ public final class AdPlaybackState implements Bundleable {
private AdGroup(
long timeUs,
int count,
int originalCount,
@AdState int[] states,
@NullableType Uri[] uris,
long[] durationsUs,
@ -106,6 +115,7 @@ public final class AdPlaybackState implements Bundleable {
checkArgument(states.length == uris.length);
this.timeUs = timeUs;
this.count = count;
this.originalCount = originalCount;
this.states = states;
this.uris = uris;
this.durationsUs = durationsUs;
@ -125,6 +135,9 @@ public final class AdPlaybackState implements Bundleable {
* Returns the index of the next ad in the ad group that should be played after playing {@code
* lastPlayedAdIndex}, or {@link #count} if no later ads should be played. If no ads have been
* played, pass -1 to get the index of the first ad to play.
*
* <p>Note: {@linkplain #isServerSideInserted Server side inserted ads} are always considered
* playable.
*/
public int getNextAdIndexToPlay(@IntRange(from = -1) int lastPlayedAdIndex) {
int nextAdIndexToPlay = lastPlayedAdIndex + 1;
@ -170,6 +183,7 @@ public final class AdPlaybackState implements Bundleable {
AdGroup adGroup = (AdGroup) o;
return timeUs == adGroup.timeUs
&& count == adGroup.count
&& originalCount == adGroup.originalCount
&& Arrays.equals(uris, adGroup.uris)
&& Arrays.equals(states, adGroup.states)
&& Arrays.equals(durationsUs, adGroup.durationsUs)
@ -180,6 +194,7 @@ public final class AdPlaybackState implements Bundleable {
@Override
public int hashCode() {
int result = count;
result = 31 * result + originalCount;
result = 31 * result + (int) (timeUs ^ (timeUs >>> 32));
result = 31 * result + Arrays.hashCode(uris);
result = 31 * result + Arrays.hashCode(states);
@ -193,7 +208,14 @@ public final class AdPlaybackState implements Bundleable {
@CheckResult
public AdGroup withTimeUs(long timeUs) {
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
/** Returns a new instance with the ad count set to {@code count}. */
@ -203,7 +225,14 @@ public final class AdPlaybackState implements Bundleable {
long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
@NullableType Uri[] uris = Arrays.copyOf(this.uris, count);
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
/**
@ -221,7 +250,14 @@ public final class AdPlaybackState implements Bundleable {
uris[index] = uri;
states[index] = AD_STATE_AVAILABLE;
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
/**
@ -235,7 +271,7 @@ public final class AdPlaybackState implements Bundleable {
@CheckResult
public AdGroup withAdState(@AdState int state, @IntRange(from = 0) int index) {
checkArgument(count == C.LENGTH_UNSET || index < count);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, /* count= */ index + 1);
checkArgument(
states[index] == AD_STATE_UNAVAILABLE
|| states[index] == AD_STATE_AVAILABLE
@ -249,7 +285,14 @@ public final class AdPlaybackState implements Bundleable {
this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length);
states[index] = state;
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
/** Returns a new instance with the specified ad durations, in microseconds. */
@ -261,21 +304,75 @@ public final class AdPlaybackState implements Bundleable {
durationsUs = Arrays.copyOf(durationsUs, uris.length);
}
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
/** Returns an instance with the specified {@link #contentResumeOffsetUs}. */
@CheckResult
public AdGroup withContentResumeOffsetUs(long contentResumeOffsetUs) {
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
/** Returns an instance with the specified value for {@link #isServerSideInserted}. */
@CheckResult
public AdGroup withIsServerSideInserted(boolean isServerSideInserted) {
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
/** Returns an instance with the specified value for {@link #originalCount}. */
public AdGroup withOriginalAdCount(int originalCount) {
return new AdGroup(
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
/** Removes the last ad from the ad group. */
public AdGroup withLastAdRemoved() {
int newCount = states.length - 1;
@AdState int[] newStates = Arrays.copyOf(states, newCount);
@NullableType Uri[] newUris = Arrays.copyOf(uris, newCount);
long[] newDurationsUs = durationsUs;
if (durationsUs.length > newCount) {
newDurationsUs = Arrays.copyOf(durationsUs, newCount);
}
return new AdGroup(
timeUs,
newCount,
originalCount,
newStates,
newUris,
newDurationsUs,
/* contentResumeOffsetUs= */ Util.sum(newDurationsUs),
isServerSideInserted);
}
/**
@ -288,6 +385,7 @@ public final class AdPlaybackState implements Bundleable {
return new AdGroup(
timeUs,
/* count= */ 0,
originalCount,
/* states= */ new int[0],
/* uris= */ new Uri[0],
/* durationsUs= */ new long[0],
@ -302,7 +400,14 @@ public final class AdPlaybackState implements Bundleable {
}
}
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
/**
@ -324,7 +429,14 @@ public final class AdPlaybackState implements Bundleable {
}
}
return new AdGroup(
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
timeUs,
count,
originalCount,
states,
uris,
durationsUs,
contentResumeOffsetUs,
isServerSideInserted);
}
@CheckResult
@ -358,6 +470,7 @@ public final class AdPlaybackState implements Bundleable {
FIELD_DURATIONS_US,
FIELD_CONTENT_RESUME_OFFSET_US,
FIELD_IS_SERVER_SIDE_INSERTED,
FIELD_ORIGINAL_COUNT
})
private @interface FieldNumber {}
@ -368,6 +481,7 @@ public final class AdPlaybackState implements Bundleable {
private static final int FIELD_DURATIONS_US = 4;
private static final int FIELD_CONTENT_RESUME_OFFSET_US = 5;
private static final int FIELD_IS_SERVER_SIDE_INSERTED = 6;
private static final int FIELD_ORIGINAL_COUNT = 7;
// putParcelableArrayList actually supports null elements.
@SuppressWarnings("nullness:argument")
@ -376,6 +490,7 @@ public final class AdPlaybackState implements Bundleable {
Bundle bundle = new Bundle();
bundle.putLong(keyForField(FIELD_TIME_US), timeUs);
bundle.putInt(keyForField(FIELD_COUNT), count);
bundle.putInt(keyForField(FIELD_ORIGINAL_COUNT), originalCount);
bundle.putParcelableArrayList(
keyForField(FIELD_URIS), new ArrayList<@NullableType Uri>(Arrays.asList(uris)));
bundle.putIntArray(keyForField(FIELD_STATES), states);
@ -393,6 +508,8 @@ public final class AdPlaybackState implements Bundleable {
private static AdGroup fromBundle(Bundle bundle) {
long timeUs = bundle.getLong(keyForField(FIELD_TIME_US));
int count = bundle.getInt(keyForField(FIELD_COUNT), /* defaultValue= */ C.LENGTH_UNSET);
int originalCount =
bundle.getInt(keyForField(FIELD_ORIGINAL_COUNT), /* defaultValue= */ C.LENGTH_UNSET);
@Nullable
ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(keyForField(FIELD_URIS));
@Nullable
@ -404,6 +521,7 @@ public final class AdPlaybackState implements Bundleable {
return new AdGroup(
timeUs,
count,
originalCount,
states == null ? new int[0] : states,
uriList == null ? new Uri[0] : uriList.toArray(new Uri[0]),
durationsUs == null ? new long[0] : durationsUs,
@ -470,7 +588,7 @@ public final class AdPlaybackState implements Bundleable {
*/
public final long contentDurationUs;
/**
* The number of ad groups the have been removed. Ad groups with indices between {@code 0}
* The number of ad groups that have been removed. Ad groups with indices between {@code 0}
* (inclusive) and {@code removedAdGroupCount} (exclusive) will be empty and must not be modified
* by any of the {@code with*} methods.
*/
@ -639,18 +757,40 @@ public final class AdPlaybackState implements Bundleable {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/** Returns an instance with the specified ad URI. */
/**
* Returns an instance with the specified ad URI and the ad marked as {@linkplain
* #AD_STATE_AVAILABLE available}.
*
* @throws IllegalStateException If {@link Uri#EMPTY} is passed as argument for a client-side
* inserted ad group.
*/
@CheckResult
public AdPlaybackState withAdUri(
public AdPlaybackState withAvailableAdUri(
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup, Uri uri) {
int adjustedIndex = adGroupIndex - removedAdGroupCount;
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
checkState(!Uri.EMPTY.equals(uri) || adGroups[adjustedIndex].isServerSideInserted);
adGroups[adjustedIndex] = adGroups[adjustedIndex].withAdUri(uri, adIndexInAdGroup);
return new AdPlaybackState(
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/** Returns an instance with the specified ad marked as played. */
/**
* Returns an instance with the specified ad marked as {@linkplain #AD_STATE_AVAILABLE available}.
*
* <p>Must not be called with client side inserted ad groups. Client side inserted ads should use
* {@link #withAvailableAdUri}.
*
* @throws IllegalStateException in case this methods is called on an ad group that {@linkplain
* AdGroup#isServerSideInserted is not server side inserted}.
*/
@CheckResult
public AdPlaybackState withAvailableAd(
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup) {
return withAvailableAdUri(adGroupIndex, adIndexInAdGroup, Uri.EMPTY);
}
/** Returns an instance with the specified ad marked as {@linkplain #AD_STATE_PLAYED played}. */
@CheckResult
public AdPlaybackState withPlayedAd(
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup) {
@ -662,7 +802,7 @@ public final class AdPlaybackState implements Bundleable {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/** Returns an instance with the specified ad marked as skipped. */
/** Returns an instance with the specified ad marked as {@linkplain #AD_STATE_SKIPPED skipped}. */
@CheckResult
public AdPlaybackState withSkippedAd(
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup) {
@ -674,7 +814,20 @@ public final class AdPlaybackState implements Bundleable {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/** Returns an instance with the specified ad marked as having a load error. */
/** Returns an instance with the last ad of the given ad group removed. */
@CheckResult
public AdPlaybackState withLastAdRemoved(@IntRange(from = 0) int adGroupIndex) {
int adjustedIndex = adGroupIndex - removedAdGroupCount;
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
adGroups[adjustedIndex] = adGroups[adjustedIndex].withLastAdRemoved();
return new AdPlaybackState(
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/**
* Returns an instance with the specified ad marked {@linkplain #AD_STATE_ERROR as having a load
* error}.
*/
@CheckResult
public AdPlaybackState withAdLoadError(
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup) {
@ -796,6 +949,23 @@ public final class AdPlaybackState implements Bundleable {
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/**
* Returns an instance with the specified value for {@link AdGroup#originalCount} in the specified
* ad group.
*/
@CheckResult
public AdPlaybackState withOriginalAdCount(
@IntRange(from = 0) int adGroupIndex, int originalAdCount) {
int adjustedIndex = adGroupIndex - removedAdGroupCount;
if (adGroups[adjustedIndex].originalCount == originalAdCount) {
return this;
}
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
adGroups[adjustedIndex] = adGroups[adjustedIndex].withOriginalAdCount(originalAdCount);
return new AdPlaybackState(
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
}
/**
* Returns an instance with the specified value for {@link AdGroup#isServerSideInserted} in the
* specified ad group.
@ -843,6 +1013,7 @@ public final class AdPlaybackState implements Bundleable {
new AdGroup(
adGroup.timeUs,
adGroup.count,
adGroup.originalCount,
Arrays.copyOf(adGroup.states, adGroup.states.length),
Arrays.copyOf(adGroup.uris, adGroup.uris.length),
Arrays.copyOf(adGroup.durationsUs, adGroup.durationsUs.length),

View File

@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -94,30 +95,35 @@ public final class AudioAttributes implements Bundleable {
}
/** See {@link android.media.AudioAttributes.Builder#setContentType(int)} */
@CanIgnoreReturnValue
public Builder setContentType(@C.AudioContentType int contentType) {
this.contentType = contentType;
return this;
}
/** See {@link android.media.AudioAttributes.Builder#setFlags(int)} */
@CanIgnoreReturnValue
public Builder setFlags(@C.AudioFlags int flags) {
this.flags = flags;
return this;
}
/** See {@link android.media.AudioAttributes.Builder#setUsage(int)} */
@CanIgnoreReturnValue
public Builder setUsage(@C.AudioUsage int usage) {
this.usage = usage;
return this;
}
/** See {@link android.media.AudioAttributes.Builder#setAllowedCapturePolicy(int)}. */
@CanIgnoreReturnValue
public Builder setAllowedCapturePolicy(@C.AudioAllowedCapturePolicy int allowedCapturePolicy) {
this.allowedCapturePolicy = allowedCapturePolicy;
return this;
}
/** See {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)}. */
@CanIgnoreReturnValue
public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) {
this.spatializationBehavior = spatializationBehavior;
return this;

View File

@ -21,7 +21,8 @@ import static java.lang.Math.min;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.util.Collections;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.ForOverride;
import java.util.List;
/** Abstract base {@link Player} which implements common implementation independent methods. */
@ -36,17 +37,17 @@ public abstract class BasePlayer implements Player {
@Override
public final void setMediaItem(MediaItem mediaItem) {
setMediaItems(Collections.singletonList(mediaItem));
setMediaItems(ImmutableList.of(mediaItem));
}
@Override
public final void setMediaItem(MediaItem mediaItem, long startPositionMs) {
setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs);
setMediaItems(ImmutableList.of(mediaItem), /* startIndex= */ 0, startPositionMs);
}
@Override
public final void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
setMediaItems(Collections.singletonList(mediaItem), resetPosition);
setMediaItems(ImmutableList.of(mediaItem), resetPosition);
}
@Override
@ -56,12 +57,12 @@ public abstract class BasePlayer implements Player {
@Override
public final void addMediaItem(int index, MediaItem mediaItem) {
addMediaItems(index, Collections.singletonList(mediaItem));
addMediaItems(index, ImmutableList.of(mediaItem));
}
@Override
public final void addMediaItem(MediaItem mediaItem) {
addMediaItems(Collections.singletonList(mediaItem));
addMediaItems(ImmutableList.of(mediaItem));
}
@Override
@ -187,7 +188,12 @@ public abstract class BasePlayer implements Player {
@Override
public final void seekToPreviousMediaItem() {
int previousMediaItemIndex = getPreviousMediaItemIndex();
if (previousMediaItemIndex != C.INDEX_UNSET) {
if (previousMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (previousMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem();
} else {
seekToDefaultPosition(previousMediaItemIndex);
}
}
@ -254,7 +260,12 @@ public abstract class BasePlayer implements Player {
@Override
public final void seekToNextMediaItem() {
int nextMediaItemIndex = getNextMediaItemIndex();
if (nextMediaItemIndex != C.INDEX_UNSET) {
if (nextMediaItemIndex == C.INDEX_UNSET) {
return;
}
if (nextMediaItemIndex == getCurrentMediaItemIndex()) {
repeatCurrentMediaItem();
} else {
seekToDefaultPosition(nextMediaItemIndex);
}
}
@ -426,6 +437,17 @@ public abstract class BasePlayer implements Player {
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
}
/**
* Repeat the current media item.
*
* <p>The default implementation seeks to the default position in the current item, which can be
* overridden for additional handling.
*/
@ForOverride
protected void repeatCurrentMediaItem() {
seekToDefaultPosition();
}
private @RepeatMode int getRepeatModeForNavigation() {
@RepeatMode int repeatMode = getRepeatMode();
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;

View File

@ -1044,29 +1044,31 @@ public final class C {
*/
@UnstableApi public static final int STEREO_MODE_STEREO_MESH = 3;
// LINT.IfChange(color_space)
/**
* Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT709}, {@link
* #COLOR_SPACE_BT601} or {@link #COLOR_SPACE_BT2020}.
* Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT601}, {@link
* #COLOR_SPACE_BT709} or {@link #COLOR_SPACE_BT2020}.
*/
@UnstableApi
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020})
@IntDef({Format.NO_VALUE, COLOR_SPACE_BT601, COLOR_SPACE_BT709, COLOR_SPACE_BT2020})
public @interface ColorSpace {}
/**
* @see MediaFormat#COLOR_STANDARD_BT709
*/
@UnstableApi public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
/**
* @see MediaFormat#COLOR_STANDARD_BT601_PAL
*/
@UnstableApi public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;
/**
* @see MediaFormat#COLOR_STANDARD_BT709
*/
@UnstableApi public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
/**
* @see MediaFormat#COLOR_STANDARD_BT2020
*/
@UnstableApi public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
// LINT.IfChange(color_transfer)
/**
* Video color transfer characteristics. One of {@link Format#NO_VALUE}, {@link
* #COLOR_TRANSFER_SDR}, {@link #COLOR_TRANSFER_ST2084} or {@link #COLOR_TRANSFER_HLG}.
@ -1090,6 +1092,7 @@ public final class C {
*/
@UnstableApi public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
// LINT.IfChange(color_range)
/**
* Video color range. One of {@link Format#NO_VALUE}, {@link #COLOR_RANGE_LIMITED} or {@link
* #COLOR_RANGE_FULL}.

View File

@ -28,10 +28,23 @@ import java.lang.annotation.Target;
import java.util.Arrays;
import org.checkerframework.dataflow.qual.Pure;
/** Stores color info. */
/**
* Stores color info.
*
* <p>When a {@code null} {@code ColorInfo} instance is used, this often represents a generic {@link
* #SDR_BT709_LIMITED} instance.
*/
@UnstableApi
public final class ColorInfo implements Bundleable {
/** Color info representing SDR BT.709 limited range, which is a common SDR video color format. */
public static final ColorInfo SDR_BT709_LIMITED =
new ColorInfo(
C.COLOR_SPACE_BT709,
C.COLOR_RANGE_LIMITED,
C.COLOR_TRANSFER_SDR,
/* hdrStaticInfo= */ null);
/**
* Returns the {@link C.ColorSpace} corresponding to the given ISO color primary code, as per
* table A.7.21.1 in Rec. ITU-T T.832 (03/2009), or {@link Format#NO_VALUE} if no mapping can be
@ -76,6 +89,13 @@ public final class ColorInfo implements Bundleable {
}
}
/** Returns whether the {@code ColorInfo} uses an HDR {@link C.ColorTransfer}. */
public static boolean isTransferHdr(@Nullable ColorInfo colorInfo) {
return colorInfo != null
&& colorInfo.colorTransfer != Format.NO_VALUE
&& colorInfo.colorTransfer != C.COLOR_TRANSFER_SDR;
}
/**
* The color space of the video. Valid values are {@link C#COLOR_SPACE_BT601}, {@link
* C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown.

View File

@ -0,0 +1,37 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import android.view.SurfaceView;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
/** Provider for views to show diagnostic information during a transformation, for debugging. */
@UnstableApi
public interface DebugViewProvider {
/** Debug view provider that doesn't show any debug info. */
DebugViewProvider NONE = (int width, int height) -> null;
/**
* Returns a new surface view to show a preview of transformer output with the given width/height
* in pixels, or {@code null} if no debug information should be shown.
*
* <p>This method may be called on an arbitrary thread.
*/
@Nullable
SurfaceView getDebugPreviewSurfaceView(int width, int height);
}

View File

@ -13,21 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.transformer;
package androidx.media3.common;
import androidx.media3.common.util.UnstableApi;
/**
* Interface for a video frame effect with a {@link SingleFrameGlTextureProcessor} implementation.
*
* <p>Implementations contain information specifying the effect and can be {@linkplain
* #toGlTextureProcessor() converted} to a {@link SingleFrameGlTextureProcessor} which applies the
* effect.
*/
/** Marker interface for a video frame effect. */
@UnstableApi
public interface GlEffect {
/** Returns a {@link SingleFrameGlTextureProcessor} that applies the effect. */
// TODO(b/227625423): use GlTextureProcessor here once this interface exists.
SingleFrameGlTextureProcessor toGlTextureProcessor();
}
public interface Effect {}

View File

@ -22,6 +22,7 @@ import android.util.SparseBooleanArray;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/**
* A set of integer flags.
@ -53,6 +54,7 @@ public final class FlagSet {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder add(int flag) {
checkState(!buildCalled);
flags.append(flag, /* value= */ true);
@ -67,6 +69,7 @@ public final class FlagSet {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder addIf(int flag, boolean condition) {
if (condition) {
return add(flag);
@ -81,6 +84,7 @@ public final class FlagSet {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder addAll(int... flags) {
for (int flag : flags) {
add(flag);
@ -95,6 +99,7 @@ public final class FlagSet {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder addAll(FlagSet flags) {
for (int i = 0; i < flags.size(); i++) {
add(flags.get(i));
@ -109,6 +114,7 @@ public final class FlagSet {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder remove(int flag) {
checkState(!buildCalled);
flags.delete(flag);
@ -123,6 +129,7 @@ public final class FlagSet {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder removeIf(int flag, boolean condition) {
if (condition) {
return remove(flag);
@ -137,6 +144,7 @@ public final class FlagSet {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder removeAll(int... flags) {
for (int flag : flags) {
remove(flag);

View File

@ -24,6 +24,7 @@ import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Joiner;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -248,6 +249,7 @@ public final class Format implements Bundleable {
* @param id The {@link Format#id}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setId(@Nullable String id) {
this.id = id;
return this;
@ -260,6 +262,7 @@ public final class Format implements Bundleable {
* @param id The {@link Format#id}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setId(int id) {
this.id = Integer.toString(id);
return this;
@ -271,6 +274,7 @@ public final class Format implements Bundleable {
* @param label The {@link Format#label}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setLabel(@Nullable String label) {
this.label = label;
return this;
@ -282,6 +286,7 @@ public final class Format implements Bundleable {
* @param language The {@link Format#language}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setLanguage(@Nullable String language) {
this.language = language;
return this;
@ -293,6 +298,7 @@ public final class Format implements Bundleable {
* @param selectionFlags The {@link Format#selectionFlags}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setSelectionFlags(@C.SelectionFlags int selectionFlags) {
this.selectionFlags = selectionFlags;
return this;
@ -304,6 +310,7 @@ public final class Format implements Bundleable {
* @param roleFlags The {@link Format#roleFlags}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setRoleFlags(@C.RoleFlags int roleFlags) {
this.roleFlags = roleFlags;
return this;
@ -315,6 +322,7 @@ public final class Format implements Bundleable {
* @param averageBitrate The {@link Format#averageBitrate}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setAverageBitrate(int averageBitrate) {
this.averageBitrate = averageBitrate;
return this;
@ -326,6 +334,7 @@ public final class Format implements Bundleable {
* @param peakBitrate The {@link Format#peakBitrate}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setPeakBitrate(int peakBitrate) {
this.peakBitrate = peakBitrate;
return this;
@ -337,6 +346,7 @@ public final class Format implements Bundleable {
* @param codecs The {@link Format#codecs}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setCodecs(@Nullable String codecs) {
this.codecs = codecs;
return this;
@ -348,6 +358,7 @@ public final class Format implements Bundleable {
* @param metadata The {@link Format#metadata}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setMetadata(@Nullable Metadata metadata) {
this.metadata = metadata;
return this;
@ -361,6 +372,7 @@ public final class Format implements Bundleable {
* @param containerMimeType The {@link Format#containerMimeType}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setContainerMimeType(@Nullable String containerMimeType) {
this.containerMimeType = containerMimeType;
return this;
@ -374,6 +386,7 @@ public final class Format implements Bundleable {
* @param sampleMimeType {@link Format#sampleMimeType}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setSampleMimeType(@Nullable String sampleMimeType) {
this.sampleMimeType = sampleMimeType;
return this;
@ -385,6 +398,7 @@ public final class Format implements Bundleable {
* @param maxInputSize The {@link Format#maxInputSize}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setMaxInputSize(int maxInputSize) {
this.maxInputSize = maxInputSize;
return this;
@ -396,6 +410,7 @@ public final class Format implements Bundleable {
* @param initializationData The {@link Format#initializationData}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setInitializationData(@Nullable List<byte[]> initializationData) {
this.initializationData = initializationData;
return this;
@ -407,6 +422,7 @@ public final class Format implements Bundleable {
* @param drmInitData The {@link Format#drmInitData}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setDrmInitData(@Nullable DrmInitData drmInitData) {
this.drmInitData = drmInitData;
return this;
@ -418,6 +434,7 @@ public final class Format implements Bundleable {
* @param subsampleOffsetUs The {@link Format#subsampleOffsetUs}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setSubsampleOffsetUs(long subsampleOffsetUs) {
this.subsampleOffsetUs = subsampleOffsetUs;
return this;
@ -431,6 +448,7 @@ public final class Format implements Bundleable {
* @param width The {@link Format#width}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setWidth(int width) {
this.width = width;
return this;
@ -442,6 +460,7 @@ public final class Format implements Bundleable {
* @param height The {@link Format#height}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setHeight(int height) {
this.height = height;
return this;
@ -453,6 +472,7 @@ public final class Format implements Bundleable {
* @param frameRate The {@link Format#frameRate}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setFrameRate(float frameRate) {
this.frameRate = frameRate;
return this;
@ -464,6 +484,7 @@ public final class Format implements Bundleable {
* @param rotationDegrees The {@link Format#rotationDegrees}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setRotationDegrees(int rotationDegrees) {
this.rotationDegrees = rotationDegrees;
return this;
@ -475,6 +496,7 @@ public final class Format implements Bundleable {
* @param pixelWidthHeightRatio The {@link Format#pixelWidthHeightRatio}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setPixelWidthHeightRatio(float pixelWidthHeightRatio) {
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
return this;
@ -486,6 +508,7 @@ public final class Format implements Bundleable {
* @param projectionData The {@link Format#projectionData}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setProjectionData(@Nullable byte[] projectionData) {
this.projectionData = projectionData;
return this;
@ -497,6 +520,7 @@ public final class Format implements Bundleable {
* @param stereoMode The {@link Format#stereoMode}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setStereoMode(@C.StereoMode int stereoMode) {
this.stereoMode = stereoMode;
return this;
@ -508,6 +532,7 @@ public final class Format implements Bundleable {
* @param colorInfo The {@link Format#colorInfo}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setColorInfo(@Nullable ColorInfo colorInfo) {
this.colorInfo = colorInfo;
return this;
@ -521,6 +546,7 @@ public final class Format implements Bundleable {
* @param channelCount The {@link Format#channelCount}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setChannelCount(int channelCount) {
this.channelCount = channelCount;
return this;
@ -532,6 +558,7 @@ public final class Format implements Bundleable {
* @param sampleRate The {@link Format#sampleRate}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setSampleRate(int sampleRate) {
this.sampleRate = sampleRate;
return this;
@ -543,6 +570,7 @@ public final class Format implements Bundleable {
* @param pcmEncoding The {@link Format#pcmEncoding}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setPcmEncoding(@C.PcmEncoding int pcmEncoding) {
this.pcmEncoding = pcmEncoding;
return this;
@ -554,6 +582,7 @@ public final class Format implements Bundleable {
* @param encoderDelay The {@link Format#encoderDelay}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setEncoderDelay(int encoderDelay) {
this.encoderDelay = encoderDelay;
return this;
@ -565,6 +594,7 @@ public final class Format implements Bundleable {
* @param encoderPadding The {@link Format#encoderPadding}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setEncoderPadding(int encoderPadding) {
this.encoderPadding = encoderPadding;
return this;
@ -578,6 +608,7 @@ public final class Format implements Bundleable {
* @param accessibilityChannel The {@link Format#accessibilityChannel}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setAccessibilityChannel(int accessibilityChannel) {
this.accessibilityChannel = accessibilityChannel;
return this;
@ -591,6 +622,7 @@ public final class Format implements Bundleable {
* @param cryptoType The {@link C.CryptoType}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setCryptoType(@C.CryptoType int cryptoType) {
this.cryptoType = cryptoType;
return this;
@ -1515,6 +1547,15 @@ public final class Format implements Bundleable {
@UnstableApi
@Override
public Bundle toBundle() {
return toBundle(/* excludeMetadata= */ false);
}
/**
* Returns a {@link Bundle} representing the information stored in this object. If {@code
* excludeMetadata} is true, {@linkplain Format#metadata metadata} is excluded.
*/
@UnstableApi
public Bundle toBundle(boolean excludeMetadata) {
Bundle bundle = new Bundle();
bundle.putString(keyForField(FIELD_ID), id);
bundle.putString(keyForField(FIELD_LABEL), label);
@ -1524,10 +1565,10 @@ public final class Format implements Bundleable {
bundle.putInt(keyForField(FIELD_AVERAGE_BITRATE), averageBitrate);
bundle.putInt(keyForField(FIELD_PEAK_BITRATE), peakBitrate);
bundle.putString(keyForField(FIELD_CODECS), codecs);
// Metadata is currently not Bundleable because Metadata.Entry is an Interface,
// which would be difficult to unbundle in a backward compatible way.
// The entries are additionally of limited usefulness to remote processes.
bundle.putParcelable(keyForField(FIELD_METADATA), metadata);
if (!excludeMetadata) {
// TODO (internal ref: b/239701618)
bundle.putParcelable(keyForField(FIELD_METADATA), metadata);
}
// Container specific.
bundle.putString(keyForField(FIELD_CONTAINER_MIME_TYPE), containerMimeType);
// Sample specific.

View File

@ -23,6 +23,7 @@ import android.view.TextureView;
import androidx.annotation.Nullable;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import java.util.List;
@ -759,6 +760,12 @@ public class ForwardingPlayer implements Player {
return player.getVideoSize();
}
/** Calls {@link Player#getSurfaceSize()} on the delegate and returns the result. */
@Override
public Size getSurfaceSize() {
return player.getSurfaceSize();
}
/** Calls {@link Player#clearVideoSurface()} on the delegate. */
@Override
public void clearVideoSurface() {

View File

@ -0,0 +1,60 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import androidx.media3.common.util.UnstableApi;
/** Value class specifying information about a decoded video frame. */
@UnstableApi
public class FrameInfo {
/** The width of the frame, in pixels. */
public final int width;
/** The height of the frame, in pixels. */
public final int height;
/** The ratio of width over height for each pixel. */
public final float pixelWidthHeightRatio;
/**
* An offset in microseconds that is part of the input timestamps and should be ignored for
* processing but added back to the output timestamps.
*
* <p>The offset stays constant within a stream but changes in between streams to ensure that
* frame timestamps are always monotonically increasing.
*/
public final long streamOffsetUs;
// TODO(b/227624622): Add color space information for HDR.
/**
* Creates a new instance.
*
* @param width The width of the frame, in pixels.
* @param height The height of the frame, in pixels.
* @param pixelWidthHeightRatio The ratio of width over height for each pixel.
* @param streamOffsetUs An offset in microseconds that is part of the input timestamps and should
* be ignored for processing but added back to the output timestamps.
*/
public FrameInfo(int width, int height, float pixelWidthHeightRatio, long streamOffsetUs) {
checkArgument(width > 0, "width must be positive, but is: " + width);
checkArgument(height > 0, "height must be positive, but is: " + height);
this.width = width;
this.height = height;
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
this.streamOffsetUs = streamOffsetUs;
}
}

View File

@ -13,15 +13,34 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.transformer;
package androidx.media3.common;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi;
/** Thrown when an exception occurs while applying effects to video frames. */
@UnstableApi
public final class FrameProcessingException extends Exception {
/**
* Wraps the given exception in a {@code FrameProcessingException} if it is not already a {@code
* FrameProcessingException} and returns the exception otherwise.
*/
public static FrameProcessingException from(Exception exception) {
return from(exception, /* presentationTimeUs= */ C.TIME_UNSET);
}
/**
* Wraps the given exception in a {@code FrameProcessingException} with the given timestamp if it
* is not already a {@code FrameProcessingException} and returns the exception otherwise.
*/
public static FrameProcessingException from(Exception exception, long presentationTimeUs) {
if (exception instanceof FrameProcessingException) {
return (FrameProcessingException) exception;
} else {
return new FrameProcessingException(exception, presentationTimeUs);
}
}
/**
* The microsecond timestamp of the frame being processed while the exception occurred or {@link
* C#TIME_UNSET} if unknown.

View File

@ -0,0 +1,205 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import android.content.Context;
import android.opengl.EGLExt;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import java.util.List;
/**
* Interface for a frame processor that applies changes to individual video frames.
*
* <p>The changes are specified by {@link Effect} instances passed to {@link Factory#create}.
*
* <p>Manages its input {@link Surface}, which can be accessed via {@link #getInputSurface()}. The
* output {@link Surface} must be set by the caller using {@link
* #setOutputSurfaceInfo(SurfaceInfo)}.
*
* <p>The caller must {@linkplain #registerInputFrame() register} input frames before rendering them
* to the input {@link Surface}.
*/
@UnstableApi
public interface FrameProcessor {
// TODO(b/243036513): Allow effects to be replaced.
/** A factory for {@link FrameProcessor} instances. */
interface Factory {
/**
* Creates a new {@link FrameProcessor} instance.
*
* @param context A {@link Context}.
* @param listener A {@link Listener}.
* @param effects The {@link Effect} instances to apply to each frame.
* @param debugViewProvider A {@link DebugViewProvider}.
* @param colorInfo The {@link ColorInfo} for input and output frames.
* @param releaseFramesAutomatically If {@code true}, the {@link FrameProcessor} will render
* output frames to the {@linkplain #setOutputSurfaceInfo(SurfaceInfo) output surface}
* automatically as {@link FrameProcessor} is done processing them. If {@code false}, the
* {@link FrameProcessor} will block until {@link #releaseOutputFrame(long)} is called, to
* render or drop the frame.
* @return A new instance.
* @throws FrameProcessingException If a problem occurs while creating the {@link
* FrameProcessor}.
*/
FrameProcessor create(
Context context,
Listener listener,
List<Effect> effects,
DebugViewProvider debugViewProvider,
ColorInfo colorInfo,
boolean releaseFramesAutomatically)
throws FrameProcessingException;
}
/**
* Listener for asynchronous frame processing events.
*
* <p>All listener methods must be called from the same thread.
*/
interface Listener {
/**
* Called when the output size changes.
*
* <p>The output size is the frame size in pixels after applying all {@linkplain Effect
* effects}.
*
* <p>The output size may differ from the size specified using {@link
* #setOutputSurfaceInfo(SurfaceInfo)}.
*/
void onOutputSizeChanged(int width, int height);
/**
* Called when an output frame with the given {@code presentationTimeUs} becomes available.
*
* @param presentationTimeUs The presentation time of the frame, in microseconds.
*/
void onOutputFrameAvailable(long presentationTimeUs);
/**
* Called when an exception occurs during asynchronous frame processing.
*
* <p>If an error occurred, consuming and producing further frames will not work as expected and
* the {@link FrameProcessor} should be released.
*/
void onFrameProcessingError(FrameProcessingException exception);
/** Called after the {@link FrameProcessor} has produced its final output frame. */
void onFrameProcessingEnded();
}
/**
* Indicates the frame should be released immediately after {@link #releaseOutputFrame(long)} is
* invoked.
*/
long RELEASE_OUTPUT_FRAME_IMMEDIATELY = -1;
/** Indicates the frame should be dropped after {@link #releaseOutputFrame(long)} is invoked. */
long DROP_OUTPUT_FRAME = -2;
/** Returns the input {@link Surface}, where {@link FrameProcessor} consumes input frames from. */
Surface getInputSurface();
/**
* Sets information about the input frames.
*
* <p>The new input information is applied from the next frame {@linkplain #registerInputFrame()
* registered} onwards.
*
* <p>Pixels are expanded using the {@link FrameInfo#pixelWidthHeightRatio} so that the output
* frames' pixels have a ratio of 1.
*
* <p>The caller should update {@link FrameInfo#streamOffsetUs} when switching input streams to
* ensure that frame timestamps are always monotonically increasing.
*/
void setInputFrameInfo(FrameInfo inputFrameInfo);
/**
* Informs the {@code FrameProcessor} that a frame will be queued to its input surface.
*
* <p>Must be called before rendering a frame to the frame processor's input surface.
*
* @throws IllegalStateException If called after {@link #signalEndOfInput()} or before {@link
* #setInputFrameInfo(FrameInfo)}.
*/
void registerInputFrame();
/**
* Returns the number of input frames that have been {@linkplain #registerInputFrame() registered}
* but not processed off the {@linkplain #getInputSurface() input surface} yet.
*/
int getPendingInputFrameCount();
/**
* Sets the output surface and supporting information. When output frames are released and not
* dropped, they will be rendered to this output {@link SurfaceInfo}.
*
* <p>The new output {@link SurfaceInfo} is applied from the next output frame rendered onwards.
* If the output {@link SurfaceInfo} is {@code null}, the {@code FrameProcessor} will stop
* rendering pending frames and resume rendering once a non-null {@link SurfaceInfo} is set.
*
* <p>If the dimensions given in {@link SurfaceInfo} do not match the {@linkplain
* Listener#onOutputSizeChanged(int,int) output size after applying the final effect} the frames
* are resized before rendering to the surface and letter/pillar-boxing is applied.
*
* <p>The caller is responsible for tracking the lifecycle of the {@link SurfaceInfo#surface}
* including calling this method with a new surface if it is destroyed. When this method returns,
* the previous output surface is no longer being used and can safely be released by the caller.
*/
void setOutputSurfaceInfo(@Nullable SurfaceInfo outputSurfaceInfo);
/**
* Releases the oldest unreleased output frame that has become {@linkplain
* Listener#onOutputFrameAvailable(long) available} at the given {@code releaseTimeNs}.
*
* <p>This will either render the output frame to the {@linkplain #setOutputSurfaceInfo output
* surface}, or drop the frame, per {@code releaseTimeNs}.
*
* <p>This method must only be called if {@code releaseFramesAutomatically} was set to {@code
* false} using the {@link Factory} and should be called exactly once for each frame that becomes
* {@linkplain Listener#onOutputFrameAvailable(long) available}.
*
* <p>The {@code releaseTimeNs} may be passed to {@link EGLExt#eglPresentationTimeANDROID}
* depending on the implementation.
*
* @param releaseTimeNs The release time to use for the frame, in nanoseconds. The release time
* can be before of after the current system time. Use {@link #DROP_OUTPUT_FRAME} to drop the
* frame, or {@link #RELEASE_OUTPUT_FRAME_IMMEDIATELY} to release the frame immediately.
*/
void releaseOutputFrame(long releaseTimeNs);
/**
* Informs the {@code FrameProcessor} that no further input frames should be accepted.
*
* @throws IllegalStateException If called more than once.
*/
void signalEndOfInput();
/**
* Releases all resources.
*
* <p>If the frame processor is released before it has {@linkplain
* Listener#onFrameProcessingEnded() ended}, it will attempt to cancel processing any input frames
* that have already become available. Input frames that become available after release are
* ignored.
*
* <p>This method blocks until all resources are released or releasing times out.
*/
void release();
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import android.media.MediaPlayer;
import android.os.Looper;
import androidx.media3.common.util.UnstableApi;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
/** A {@link Player} wrapper for the legacy Android platform {@link MediaPlayer}. */
@UnstableApi
public final class LegacyMediaPlayerWrapper extends SimpleBasePlayer {
private final MediaPlayer player;
private boolean playWhenReady;
/**
* Creates the {@link MediaPlayer} wrapper.
*
* @param looper The {@link Looper} used to call all methods on.
*/
public LegacyMediaPlayerWrapper(Looper looper) {
super(looper);
this.player = new MediaPlayer();
}
@Override
protected State getState() {
return new State.Builder()
.setAvailableCommands(new Commands.Builder().addAll(Player.COMMAND_PLAY_PAUSE).build())
.setPlayWhenReady(playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build();
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
this.playWhenReady = playWhenReady;
// TODO: Only call these methods if the player is in Started or Paused state.
if (playWhenReady) {
player.start();
} else {
player.pause();
}
return Futures.immediateVoidFuture();
}
}

View File

@ -29,6 +29,7 @@ import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@ -126,6 +127,7 @@ public final class MediaItem implements Bundleable {
*
* <p>By default {@link #DEFAULT_MEDIA_ID} is used.
*/
@CanIgnoreReturnValue
public Builder setMediaId(String mediaId) {
this.mediaId = checkNotNull(mediaId);
return this;
@ -138,6 +140,7 @@ public final class MediaItem implements Bundleable {
* during {@link #build()} and no other {@code Builder} methods that would populate {@link
* MediaItem#localConfiguration} should be called.
*/
@CanIgnoreReturnValue
public Builder setUri(@Nullable String uri) {
return setUri(uri == null ? null : Uri.parse(uri));
}
@ -149,6 +152,7 @@ public final class MediaItem implements Bundleable {
* during {@link #build()} and no other {@code Builder} methods that would populate {@link
* MediaItem#localConfiguration} should be called.
*/
@CanIgnoreReturnValue
public Builder setUri(@Nullable Uri uri) {
this.uri = uri;
return this;
@ -163,12 +167,14 @@ public final class MediaItem implements Bundleable {
*
* @param mimeType The MIME type.
*/
@CanIgnoreReturnValue
public Builder setMimeType(@Nullable String mimeType) {
this.mimeType = mimeType;
return this;
}
/** Sets the {@link ClippingConfiguration}, defaults to {@link ClippingConfiguration#UNSET}. */
@CanIgnoreReturnValue
public Builder setClippingConfiguration(ClippingConfiguration clippingConfiguration) {
this.clippingConfiguration = clippingConfiguration.buildUpon();
return this;
@ -178,6 +184,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
* ClippingConfiguration.Builder#setStartPositionMs(long)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setClipStartPositionMs(@IntRange(from = 0) long startPositionMs) {
@ -189,6 +196,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
* ClippingConfiguration.Builder#setEndPositionMs(long)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setClipEndPositionMs(long endPositionMs) {
@ -200,6 +208,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
* ClippingConfiguration.Builder#setRelativeToLiveWindow(boolean)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setClipRelativeToLiveWindow(boolean relativeToLiveWindow) {
@ -211,6 +220,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
* ClippingConfiguration.Builder#setRelativeToDefaultPosition(boolean)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setClipRelativeToDefaultPosition(boolean relativeToDefaultPosition) {
@ -222,6 +232,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
* ClippingConfiguration.Builder#setStartsAtKeyFrame(boolean)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setClipStartsAtKeyFrame(boolean startsAtKeyFrame) {
@ -230,6 +241,7 @@ public final class MediaItem implements Bundleable {
}
/** Sets the optional DRM configuration. */
@CanIgnoreReturnValue
public Builder setDrmConfiguration(@Nullable DrmConfiguration drmConfiguration) {
this.drmConfiguration =
drmConfiguration != null ? drmConfiguration.buildUpon() : new DrmConfiguration.Builder();
@ -240,6 +252,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
* DrmConfiguration.Builder#setLicenseUri(Uri)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmLicenseUri(@Nullable Uri licenseUri) {
@ -251,6 +264,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
* DrmConfiguration.Builder#setLicenseUri(String)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmLicenseUri(@Nullable String licenseUri) {
@ -264,6 +278,7 @@ public final class MediaItem implements Bundleable {
* DrmConfiguration.Builder#setLicenseRequestHeaders(Map)} doesn't accept null, use an empty
* map to clear the headers.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmLicenseRequestHeaders(
@ -277,6 +292,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and pass the {@code uuid} to
* {@link DrmConfiguration.Builder#Builder(UUID)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmUuid(@Nullable UUID uuid) {
@ -288,6 +304,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
* DrmConfiguration.Builder#setMultiSession(boolean)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmMultiSession(boolean multiSession) {
@ -299,6 +316,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
* DrmConfiguration.Builder#setForceDefaultLicenseUri(boolean)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmForceDefaultLicenseUri(boolean forceDefaultLicenseUri) {
@ -310,6 +328,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
* DrmConfiguration.Builder#setPlayClearContentWithoutKey(boolean)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmPlayClearContentWithoutKey(boolean playClearContentWithoutKey) {
@ -321,6 +340,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
* DrmConfiguration.Builder#setForceSessionsForAudioAndVideoTracks(boolean)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmSessionForClearPeriods(boolean sessionForClearPeriods) {
@ -334,6 +354,7 @@ public final class MediaItem implements Bundleable {
* DrmConfiguration.Builder#setForcedSessionTrackTypes(List)} doesn't accept null, use an
* empty list to clear the contents.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmSessionForClearTypes(
@ -347,6 +368,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
* DrmConfiguration.Builder#setKeySetId(byte[])} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setDrmKeySetId(@Nullable byte[] keySetId) {
@ -363,6 +385,7 @@ public final class MediaItem implements Bundleable {
* <p>If {@link #setUri} is passed a non-null {@code uri}, the stream keys are used to create a
* {@link LocalConfiguration} object. Otherwise they will be ignored.
*/
@CanIgnoreReturnValue
@UnstableApi
public Builder setStreamKeys(@Nullable List<StreamKey> streamKeys) {
this.streamKeys =
@ -377,6 +400,7 @@ public final class MediaItem implements Bundleable {
*
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*/
@CanIgnoreReturnValue
@UnstableApi
public Builder setCustomCacheKey(@Nullable String customCacheKey) {
this.customCacheKey = customCacheKey;
@ -388,6 +412,7 @@ public final class MediaItem implements Bundleable {
* #setSubtitleConfigurations(List)} doesn't accept null, use an empty list to clear the
* contents.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setSubtitles(@Nullable List<Subtitle> subtitles) {
@ -401,6 +426,7 @@ public final class MediaItem implements Bundleable {
*
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*/
@CanIgnoreReturnValue
public Builder setSubtitleConfigurations(List<SubtitleConfiguration> subtitleConfigurations) {
this.subtitleConfigurations = ImmutableList.copyOf(subtitleConfigurations);
return this;
@ -411,6 +437,7 @@ public final class MediaItem implements Bundleable {
*
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*/
@CanIgnoreReturnValue
public Builder setAdsConfiguration(@Nullable AdsConfiguration adsConfiguration) {
this.adsConfiguration = adsConfiguration;
return this;
@ -421,6 +448,7 @@ public final class MediaItem implements Bundleable {
* with {@link Uri#parse(String)} and pass the result to {@link
* AdsConfiguration.Builder#Builder(Uri)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setAdTagUri(@Nullable String adTagUri) {
@ -431,6 +459,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setAdsConfiguration(AdsConfiguration)} and pass the {@code adTagUri}
* to {@link AdsConfiguration.Builder#Builder(Uri)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setAdTagUri(@Nullable Uri adTagUri) {
@ -442,6 +471,7 @@ public final class MediaItem implements Bundleable {
* {@link AdsConfiguration.Builder#Builder(Uri)} and the {@code adsId} to {@link
* AdsConfiguration.Builder#setAdsId(Object)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setAdTagUri(@Nullable Uri adTagUri, @Nullable Object adsId) {
@ -451,6 +481,7 @@ public final class MediaItem implements Bundleable {
}
/** Sets the {@link LiveConfiguration}. Defaults to {@link LiveConfiguration#UNSET}. */
@CanIgnoreReturnValue
public Builder setLiveConfiguration(LiveConfiguration liveConfiguration) {
this.liveConfiguration = liveConfiguration.buildUpon();
return this;
@ -460,6 +491,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setTargetOffsetMs(long)}.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveTargetOffsetMs(long liveTargetOffsetMs) {
@ -471,6 +503,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setMinOffsetMs(long)}.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveMinOffsetMs(long liveMinOffsetMs) {
@ -482,6 +515,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setMaxOffsetMs(long)}.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveMaxOffsetMs(long liveMaxOffsetMs) {
@ -493,6 +527,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setMinPlaybackSpeed(float)}.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveMinPlaybackSpeed(float minPlaybackSpeed) {
@ -504,6 +539,7 @@ public final class MediaItem implements Bundleable {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setMaxPlaybackSpeed(float)}.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveMaxPlaybackSpeed(float maxPlaybackSpeed) {
@ -518,18 +554,21 @@ public final class MediaItem implements Bundleable {
*
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
*/
@CanIgnoreReturnValue
public Builder setTag(@Nullable Object tag) {
this.tag = tag;
return this;
}
/** Sets the media metadata. */
@CanIgnoreReturnValue
public Builder setMediaMetadata(MediaMetadata mediaMetadata) {
this.mediaMetadata = mediaMetadata;
return this;
}
/** Sets the request metadata. */
@CanIgnoreReturnValue
public Builder setRequestMetadata(RequestMetadata requestMetadata) {
this.requestMetadata = requestMetadata;
return this;
@ -613,6 +652,7 @@ public final class MediaItem implements Bundleable {
}
/** Sets the {@link UUID} of the protection scheme. */
@CanIgnoreReturnValue
public Builder setScheme(UUID scheme) {
this.scheme = scheme;
return this;
@ -622,6 +662,7 @@ public final class MediaItem implements Bundleable {
* @deprecated This only exists to support the deprecated {@link
* MediaItem.Builder#setDrmUuid(UUID)}.
*/
@CanIgnoreReturnValue
@Deprecated
private Builder setNullableScheme(@Nullable UUID scheme) {
this.scheme = scheme;
@ -629,24 +670,28 @@ public final class MediaItem implements Bundleable {
}
/** Sets the optional default DRM license server URI. */
@CanIgnoreReturnValue
public Builder setLicenseUri(@Nullable Uri licenseUri) {
this.licenseUri = licenseUri;
return this;
}
/** Sets the optional default DRM license server URI. */
@CanIgnoreReturnValue
public Builder setLicenseUri(@Nullable String licenseUri) {
this.licenseUri = licenseUri == null ? null : Uri.parse(licenseUri);
return this;
}
/** Sets the optional request headers attached to DRM license requests. */
@CanIgnoreReturnValue
public Builder setLicenseRequestHeaders(Map<String, String> licenseRequestHeaders) {
this.licenseRequestHeaders = ImmutableMap.copyOf(licenseRequestHeaders);
return this;
}
/** Sets whether multi session is enabled. */
@CanIgnoreReturnValue
public Builder setMultiSession(boolean multiSession) {
this.multiSession = multiSession;
return this;
@ -656,6 +701,7 @@ public final class MediaItem implements Bundleable {
* Sets whether to always use the default DRM license server URI even if the media specifies
* its own DRM license server URI.
*/
@CanIgnoreReturnValue
public Builder setForceDefaultLicenseUri(boolean forceDefaultLicenseUri) {
this.forceDefaultLicenseUri = forceDefaultLicenseUri;
return this;
@ -665,6 +711,7 @@ public final class MediaItem implements Bundleable {
* Sets whether clear samples within protected content should be played when keys for the
* encrypted part of the content have yet to be loaded.
*/
@CanIgnoreReturnValue
public Builder setPlayClearContentWithoutKey(boolean playClearContentWithoutKey) {
this.playClearContentWithoutKey = playClearContentWithoutKey;
return this;
@ -673,6 +720,7 @@ public final class MediaItem implements Bundleable {
/**
* @deprecated Use {@link #setForceSessionsForAudioAndVideoTracks(boolean)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
@InlineMe(
@ -690,6 +738,7 @@ public final class MediaItem implements Bundleable {
* <p>This method overrides what has been set by previously calling {@link
* #setForcedSessionTrackTypes(List)}.
*/
@CanIgnoreReturnValue
public Builder setForceSessionsForAudioAndVideoTracks(
boolean forceSessionsForAudioAndVideoTracks) {
this.setForcedSessionTrackTypes(
@ -709,6 +758,7 @@ public final class MediaItem implements Bundleable {
* <p>This method overrides what has been set by previously calling {@link
* #setForceSessionsForAudioAndVideoTracks(boolean)}.
*/
@CanIgnoreReturnValue
public Builder setForcedSessionTrackTypes(
List<@C.TrackType Integer> forcedSessionTrackTypes) {
this.forcedSessionTrackTypes = ImmutableList.copyOf(forcedSessionTrackTypes);
@ -722,6 +772,7 @@ public final class MediaItem implements Bundleable {
* release an existing offline license (see {@code DefaultDrmSessionManager#setMode(int
* mode,byte[] offlineLicenseKeySetId)}).
*/
@CanIgnoreReturnValue
public Builder setKeySetId(@Nullable byte[] keySetId) {
this.keySetId = keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null;
return this;
@ -864,6 +915,7 @@ public final class MediaItem implements Bundleable {
}
/** Sets the ad tag URI to load. */
@CanIgnoreReturnValue
public Builder setAdTagUri(Uri adTagUri) {
this.adTagUri = adTagUri;
return this;
@ -875,6 +927,7 @@ public final class MediaItem implements Bundleable {
* <p>See details on {@link AdsConfiguration#adsId} for how the ads identifier is used and how
* it's calculated if not explicitly set.
*/
@CanIgnoreReturnValue
public Builder setAdsId(@Nullable Object adsId) {
this.adsId = adsId;
return this;
@ -1093,6 +1146,7 @@ public final class MediaItem implements Bundleable {
*
* <p>Defaults to {@link C#TIME_UNSET}, indicating the media-defined default will be used.
*/
@CanIgnoreReturnValue
public Builder setTargetOffsetMs(long targetOffsetMs) {
this.targetOffsetMs = targetOffsetMs;
return this;
@ -1105,6 +1159,7 @@ public final class MediaItem implements Bundleable {
*
* <p>Defaults to {@link C#TIME_UNSET}, indicating the media-defined default will be used.
*/
@CanIgnoreReturnValue
public Builder setMinOffsetMs(long minOffsetMs) {
this.minOffsetMs = minOffsetMs;
return this;
@ -1117,6 +1172,7 @@ public final class MediaItem implements Bundleable {
*
* <p>Defaults to {@link C#TIME_UNSET}, indicating the media-defined default will be used.
*/
@CanIgnoreReturnValue
public Builder setMaxOffsetMs(long maxOffsetMs) {
this.maxOffsetMs = maxOffsetMs;
return this;
@ -1127,6 +1183,7 @@ public final class MediaItem implements Bundleable {
*
* <p>Defaults to {@link C#RATE_UNSET}, indicating the media-defined default will be used.
*/
@CanIgnoreReturnValue
public Builder setMinPlaybackSpeed(float minPlaybackSpeed) {
this.minPlaybackSpeed = minPlaybackSpeed;
return this;
@ -1137,6 +1194,7 @@ public final class MediaItem implements Bundleable {
*
* <p>Defaults to {@link C#RATE_UNSET}, indicating the media-defined default will be used.
*/
@CanIgnoreReturnValue
public Builder setMaxPlaybackSpeed(float maxPlaybackSpeed) {
this.maxPlaybackSpeed = maxPlaybackSpeed;
return this;
@ -1329,42 +1387,49 @@ public final class MediaItem implements Bundleable {
}
/** Sets the {@link Uri} to the subtitle file. */
@CanIgnoreReturnValue
public Builder setUri(Uri uri) {
this.uri = uri;
return this;
}
/** Sets the MIME type. */
@CanIgnoreReturnValue
public Builder setMimeType(@Nullable String mimeType) {
this.mimeType = mimeType;
return this;
}
/** Sets the optional language of the subtitle file. */
@CanIgnoreReturnValue
public Builder setLanguage(@Nullable String language) {
this.language = language;
return this;
}
/** Sets the flags used for track selection. */
@CanIgnoreReturnValue
public Builder setSelectionFlags(@C.SelectionFlags int selectionFlags) {
this.selectionFlags = selectionFlags;
return this;
}
/** Sets the role flags. These are used for track selection. */
@CanIgnoreReturnValue
public Builder setRoleFlags(@C.RoleFlags int roleFlags) {
this.roleFlags = roleFlags;
return this;
}
/** Sets the optional label for this subtitle track. */
@CanIgnoreReturnValue
public Builder setLabel(@Nullable String label) {
this.label = label;
return this;
}
/** Sets the optional ID for this subtitle track. */
@CanIgnoreReturnValue
public Builder setId(@Nullable String id) {
this.id = id;
return this;
@ -1541,6 +1606,7 @@ public final class MediaItem implements Bundleable {
* Sets the optional start position in milliseconds which must be a value larger than or equal
* to zero (Default: 0).
*/
@CanIgnoreReturnValue
public Builder setStartPositionMs(@IntRange(from = 0) long startPositionMs) {
Assertions.checkArgument(startPositionMs >= 0);
this.startPositionMs = startPositionMs;
@ -1552,6 +1618,7 @@ public final class MediaItem implements Bundleable {
* to zero, or {@link C#TIME_END_OF_SOURCE} to end when playback reaches the end of media
* (Default: {@link C#TIME_END_OF_SOURCE}).
*/
@CanIgnoreReturnValue
public Builder setEndPositionMs(long endPositionMs) {
Assertions.checkArgument(endPositionMs == C.TIME_END_OF_SOURCE || endPositionMs >= 0);
this.endPositionMs = endPositionMs;
@ -1563,6 +1630,7 @@ public final class MediaItem implements Bundleable {
* {@code false}, live streams end when playback reaches the end position in live window seen
* when the media is first loaded (Default: {@code false}).
*/
@CanIgnoreReturnValue
public Builder setRelativeToLiveWindow(boolean relativeToLiveWindow) {
this.relativeToLiveWindow = relativeToLiveWindow;
return this;
@ -1572,6 +1640,7 @@ public final class MediaItem implements Bundleable {
* Sets whether the start position and the end position are relative to the default position
* in the window (Default: {@code false}).
*/
@CanIgnoreReturnValue
public Builder setRelativeToDefaultPosition(boolean relativeToDefaultPosition) {
this.relativeToDefaultPosition = relativeToDefaultPosition;
return this;
@ -1581,6 +1650,7 @@ public final class MediaItem implements Bundleable {
* Sets whether the start point is guaranteed to be a key frame. If {@code false}, the
* playback transition into the clip may not be seamless (Default: {@code false}).
*/
@CanIgnoreReturnValue
public Builder setStartsAtKeyFrame(boolean startsAtKeyFrame) {
this.startsAtKeyFrame = startsAtKeyFrame;
return this;
@ -1745,7 +1815,7 @@ public final class MediaItem implements Bundleable {
* 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
* instances (e.g. from a {@code androidx.media3.session.MediaController}) and the player creating
* the request doesn't know the required {@link LocalConfiguration} for playback.
*/
public static final class RequestMetadata implements Bundleable {
@ -1770,18 +1840,21 @@ public final class MediaItem implements Bundleable {
}
/** Sets the URI of the requested media, or null if not known or applicable. */
@CanIgnoreReturnValue
public Builder setMediaUri(@Nullable Uri mediaUri) {
this.mediaUri = mediaUri;
return this;
}
/** Sets the search query for the requested media, or null if not applicable. */
@CanIgnoreReturnValue
public Builder setSearchQuery(@Nullable String searchQuery) {
this.searchQuery = searchQuery;
return this;
}
/** Sets optional extras {@link Bundle}. */
@CanIgnoreReturnValue
public Builder setExtras(@Nullable Bundle extras) {
this.extras = extras;
return this;

View File

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

View File

@ -29,6 +29,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -114,30 +115,35 @@ public final class MediaMetadata implements Bundleable {
}
/** Sets the title. */
@CanIgnoreReturnValue
public Builder setTitle(@Nullable CharSequence title) {
this.title = title;
return this;
}
/** Sets the artist. */
@CanIgnoreReturnValue
public Builder setArtist(@Nullable CharSequence artist) {
this.artist = artist;
return this;
}
/** Sets the album title. */
@CanIgnoreReturnValue
public Builder setAlbumTitle(@Nullable CharSequence albumTitle) {
this.albumTitle = albumTitle;
return this;
}
/** Sets the album artist. */
@CanIgnoreReturnValue
public Builder setAlbumArtist(@Nullable CharSequence albumArtist) {
this.albumArtist = albumArtist;
return this;
}
/** Sets the display title. */
@CanIgnoreReturnValue
public Builder setDisplayTitle(@Nullable CharSequence displayTitle) {
this.displayTitle = displayTitle;
return this;
@ -148,24 +154,28 @@ public final class MediaMetadata implements Bundleable {
*
* <p>This is the secondary title of the media, unrelated to closed captions.
*/
@CanIgnoreReturnValue
public Builder setSubtitle(@Nullable CharSequence subtitle) {
this.subtitle = subtitle;
return this;
}
/** Sets the description. */
@CanIgnoreReturnValue
public Builder setDescription(@Nullable CharSequence description) {
this.description = description;
return this;
}
/** Sets the user {@link Rating}. */
@CanIgnoreReturnValue
public Builder setUserRating(@Nullable Rating userRating) {
this.userRating = userRating;
return this;
}
/** Sets the overall {@link Rating}. */
@CanIgnoreReturnValue
public Builder setOverallRating(@Nullable Rating overallRating) {
this.overallRating = overallRating;
return this;
@ -175,6 +185,7 @@ public final class MediaMetadata implements Bundleable {
* @deprecated Use {@link #setArtworkData(byte[] data, Integer pictureType)} or {@link
* #maybeSetArtworkData(byte[] data, int pictureType)}, providing a {@link PictureType}.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setArtworkData(@Nullable byte[] artworkData) {
@ -185,6 +196,7 @@ public final class MediaMetadata implements Bundleable {
* Sets the artwork data as a compressed byte array with an associated {@link PictureType
* artworkDataType}.
*/
@CanIgnoreReturnValue
public Builder setArtworkData(
@Nullable byte[] artworkData, @Nullable @PictureType Integer artworkDataType) {
this.artworkData = artworkData == null ? null : artworkData.clone();
@ -200,6 +212,7 @@ public final class MediaMetadata implements Bundleable {
* <p>Use {@link #setArtworkData(byte[], Integer)} to set the artwork data without checking the
* {@link PictureType}.
*/
@CanIgnoreReturnValue
public Builder maybeSetArtworkData(byte[] artworkData, @PictureType int artworkDataType) {
if (this.artworkData == null
|| Util.areEqual(artworkDataType, PICTURE_TYPE_FRONT_COVER)
@ -211,30 +224,35 @@ public final class MediaMetadata implements Bundleable {
}
/** Sets the artwork {@link Uri}. */
@CanIgnoreReturnValue
public Builder setArtworkUri(@Nullable Uri artworkUri) {
this.artworkUri = artworkUri;
return this;
}
/** Sets the track number. */
@CanIgnoreReturnValue
public Builder setTrackNumber(@Nullable Integer trackNumber) {
this.trackNumber = trackNumber;
return this;
}
/** Sets the total number of tracks. */
@CanIgnoreReturnValue
public Builder setTotalTrackCount(@Nullable Integer totalTrackCount) {
this.totalTrackCount = totalTrackCount;
return this;
}
/** Sets the {@link FolderType}. */
@CanIgnoreReturnValue
public Builder setFolderType(@Nullable @FolderType Integer folderType) {
this.folderType = folderType;
return this;
}
/** Sets whether the media is playable. */
@CanIgnoreReturnValue
public Builder setIsPlayable(@Nullable Boolean isPlayable) {
this.isPlayable = isPlayable;
return this;
@ -243,6 +261,7 @@ public final class MediaMetadata implements Bundleable {
/**
* @deprecated Use {@link #setRecordingYear(Integer)} instead.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setYear(@Nullable Integer year) {
@ -250,6 +269,7 @@ public final class MediaMetadata implements Bundleable {
}
/** Sets the year of the recording date. */
@CanIgnoreReturnValue
public Builder setRecordingYear(@Nullable Integer recordingYear) {
this.recordingYear = recordingYear;
return this;
@ -260,6 +280,7 @@ public final class MediaMetadata implements Bundleable {
*
* <p>Value should be between 1 and 12.
*/
@CanIgnoreReturnValue
public Builder setRecordingMonth(
@Nullable @IntRange(from = 1, to = 12) Integer recordingMonth) {
this.recordingMonth = recordingMonth;
@ -271,12 +292,14 @@ public final class MediaMetadata implements Bundleable {
*
* <p>Value should be between 1 and 31.
*/
@CanIgnoreReturnValue
public Builder setRecordingDay(@Nullable @IntRange(from = 1, to = 31) Integer recordingDay) {
this.recordingDay = recordingDay;
return this;
}
/** Sets the year of the release date. */
@CanIgnoreReturnValue
public Builder setReleaseYear(@Nullable Integer releaseYear) {
this.releaseYear = releaseYear;
return this;
@ -287,6 +310,7 @@ public final class MediaMetadata implements Bundleable {
*
* <p>Value should be between 1 and 12.
*/
@CanIgnoreReturnValue
public Builder setReleaseMonth(@Nullable @IntRange(from = 1, to = 12) Integer releaseMonth) {
this.releaseMonth = releaseMonth;
return this;
@ -297,60 +321,70 @@ public final class MediaMetadata implements Bundleable {
*
* <p>Value should be between 1 and 31.
*/
@CanIgnoreReturnValue
public Builder setReleaseDay(@Nullable @IntRange(from = 1, to = 31) Integer releaseDay) {
this.releaseDay = releaseDay;
return this;
}
/** Sets the writer. */
@CanIgnoreReturnValue
public Builder setWriter(@Nullable CharSequence writer) {
this.writer = writer;
return this;
}
/** Sets the composer. */
@CanIgnoreReturnValue
public Builder setComposer(@Nullable CharSequence composer) {
this.composer = composer;
return this;
}
/** Sets the conductor. */
@CanIgnoreReturnValue
public Builder setConductor(@Nullable CharSequence conductor) {
this.conductor = conductor;
return this;
}
/** Sets the disc number. */
@CanIgnoreReturnValue
public Builder setDiscNumber(@Nullable Integer discNumber) {
this.discNumber = discNumber;
return this;
}
/** Sets the total number of discs. */
@CanIgnoreReturnValue
public Builder setTotalDiscCount(@Nullable Integer totalDiscCount) {
this.totalDiscCount = totalDiscCount;
return this;
}
/** Sets the genre. */
@CanIgnoreReturnValue
public Builder setGenre(@Nullable CharSequence genre) {
this.genre = genre;
return this;
}
/** Sets the compilation. */
@CanIgnoreReturnValue
public Builder setCompilation(@Nullable CharSequence compilation) {
this.compilation = compilation;
return this;
}
/** Sets the name of the station streaming the media. */
@CanIgnoreReturnValue
public Builder setStation(@Nullable CharSequence station) {
this.station = station;
return this;
}
/** Sets the extras {@link Bundle}. */
@CanIgnoreReturnValue
public Builder setExtras(@Nullable Bundle extras) {
this.extras = extras;
return this;
@ -365,6 +399,7 @@ public final class MediaMetadata implements Bundleable {
* <p>In the event that multiple {@link Metadata.Entry} objects within the {@link Metadata}
* relate to the same {@link MediaMetadata} field, then the last one will be used.
*/
@CanIgnoreReturnValue
@UnstableApi
public Builder populateFromMetadata(Metadata metadata) {
for (int i = 0; i < metadata.length(); i++) {
@ -384,6 +419,7 @@ public final class MediaMetadata implements Bundleable {
* <p>In the event that multiple {@link Metadata.Entry} objects within any of the {@link
* Metadata} relate to the same {@link MediaMetadata} field, then the last one will be used.
*/
@CanIgnoreReturnValue
@UnstableApi
public Builder populateFromMetadata(List<Metadata> metadataList) {
for (int i = 0; i < metadataList.size(); i++) {
@ -397,6 +433,7 @@ public final class MediaMetadata implements Bundleable {
}
/** Populates all the fields from {@code mediaMetadata}, provided they are non-null. */
@CanIgnoreReturnValue
@UnstableApi
public Builder populate(@Nullable MediaMetadata mediaMetadata) {
if (mediaMetadata == null) {

View File

@ -20,6 +20,7 @@ import android.os.Parcelable;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.primitives.Longs;
import java.util.Arrays;
import java.util.List;
@ -61,11 +62,28 @@ public final class Metadata implements Parcelable {
}
private final Entry[] entries;
/**
* The presentation time of the metadata, in microseconds.
*
* <p>This time is an offset from the start of the current {@link Timeline.Period}.
*
* <p>This time is {@link C#TIME_UNSET} when not known or undefined.
*/
public final long presentationTimeUs;
/**
* @param entries The metadata entries.
*/
public Metadata(Entry... entries) {
this(/* presentationTimeUs= */ C.TIME_UNSET, entries);
}
/**
* @param presentationTimeUs The presentation time for the metadata entries.
* @param entries The metadata entries.
*/
public Metadata(long presentationTimeUs, Entry... entries) {
this.presentationTimeUs = presentationTimeUs;
this.entries = entries;
}
@ -73,7 +91,15 @@ public final class Metadata implements Parcelable {
* @param entries The metadata entries.
*/
public Metadata(List<? extends Entry> entries) {
this.entries = entries.toArray(new Entry[0]);
this(entries.toArray(new Entry[0]));
}
/**
* @param presentationTimeUs The presentation time for the metadata entries.
* @param entries The metadata entries.
*/
public Metadata(long presentationTimeUs, List<? extends Entry> entries) {
this(presentationTimeUs, entries.toArray(new Entry[0]));
}
/* package */ Metadata(Parcel in) {
@ -81,6 +107,7 @@ public final class Metadata implements Parcelable {
for (int i = 0; i < entries.length; i++) {
entries[i] = in.readParcelable(Entry.class.getClassLoader());
}
presentationTimeUs = in.readLong();
}
/** Returns the number of metadata entries. */
@ -123,7 +150,21 @@ public final class Metadata implements Parcelable {
if (entriesToAppend.length == 0) {
return this;
}
return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend));
return new Metadata(
presentationTimeUs, Util.nullSafeArrayConcatenation(entries, entriesToAppend));
}
/**
* Returns a copy of this metadata with the specified presentation time.
*
* @param presentationTimeUs The new presentation time, in microseconds.
* @return The metadata instance with the new presentation time.
*/
public Metadata copyWithPresentationTimeUs(long presentationTimeUs) {
if (this.presentationTimeUs == presentationTimeUs) {
return this;
}
return new Metadata(presentationTimeUs, entries);
}
@Override
@ -135,17 +176,21 @@ public final class Metadata implements Parcelable {
return false;
}
Metadata other = (Metadata) obj;
return Arrays.equals(entries, other.entries);
return Arrays.equals(entries, other.entries) && presentationTimeUs == other.presentationTimeUs;
}
@Override
public int hashCode() {
return Arrays.hashCode(entries);
int result = Arrays.hashCode(entries);
result = 31 * result + Longs.hashCode(presentationTimeUs);
return result;
}
@Override
public String toString() {
return "entries=" + Arrays.toString(entries);
return "entries="
+ Arrays.toString(entries)
+ (presentationTimeUs == C.TIME_UNSET ? "" : ", presentationTimeUs=" + presentationTimeUs);
}
// Parcelable implementation.
@ -161,6 +206,7 @@ public final class Metadata implements Parcelable {
for (Entry entry : entries) {
dest.writeParcelable(entry, 0);
}
dest.writeLong(presentationTimeUs);
}
public static final Parcelable.Creator<Metadata> CREATOR =

View File

@ -91,11 +91,15 @@ public final class MimeTypes {
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_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_MSGSM = BASE_TYPE_AUDIO + "/gsm";
public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg";
public static final String AUDIO_WAV = BASE_TYPE_AUDIO + "/wav";
public static final String AUDIO_MIDI = BASE_TYPE_AUDIO + "/midi";
@UnstableApi
public static final String AUDIO_EXOPLAYER_MIDI = BASE_TYPE_AUDIO + "/x-exoplayer-midi";
@UnstableApi public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown";
// text/ MIME types

View File

@ -33,9 +33,11 @@ import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.media3.common.text.Cue;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -406,6 +408,7 @@ public interface Player {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder add(@Command int command) {
flagsBuilder.add(command);
return this;
@ -419,6 +422,7 @@ public interface Player {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder addIf(@Command int command, boolean condition) {
flagsBuilder.addIf(command, condition);
return this;
@ -431,6 +435,7 @@ public interface Player {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder addAll(@Command int... commands) {
flagsBuilder.addAll(commands);
return this;
@ -443,6 +448,7 @@ public interface Player {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder addAll(Commands commands) {
flagsBuilder.addAll(commands.flags);
return this;
@ -454,6 +460,7 @@ public interface Player {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder addAllCommands() {
flagsBuilder.addAll(SUPPORTED_COMMANDS);
return this;
@ -466,6 +473,7 @@ public interface Player {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder remove(@Command int command) {
flagsBuilder.remove(command);
return this;
@ -479,6 +487,7 @@ public interface Player {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder removeIf(@Command int command, boolean condition) {
flagsBuilder.removeIf(command, condition);
return this;
@ -491,6 +500,7 @@ public interface Player {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
public Builder removeAll(@Command int... commands) {
flagsBuilder.removeAll(commands);
return this;
@ -2489,6 +2499,14 @@ public interface Player {
*/
VideoSize getVideoSize();
/**
* Gets the size of the surface on which the video is rendered.
*
* @see Listener#onSurfaceSizeChanged(int, int)
*/
@UnstableApi
Size getSurfaceSize();
/** Returns the current {@link CueGroup}. */
CueGroup getCurrentCues();

View File

@ -0,0 +1,802 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Util.castNonNull;
import android.os.Looper;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.TextureView;
import androidx.annotation.Nullable;
import androidx.media3.common.text.CueGroup;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Supplier;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.ForOverride;
import java.util.HashSet;
import java.util.List;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
/**
* A base implementation for {@link Player} that reduces the number of methods to implement to a
* minimum.
*
* <p>Implementation notes:
*
* <ul>
* <li>Subclasses must override {@link #getState()} to populate the current player state on
* request.
* <li>The {@link State} should set the {@linkplain State.Builder#setAvailableCommands available
* commands} to indicate which {@link Player} methods are supported.
* <li>All setter-like player methods (for example, {@link #setPlayWhenReady}) forward to
* overridable methods (for example, {@link #handleSetPlayWhenReady}) that can be used to
* handle these requests. These methods return a {@link ListenableFuture} to indicate when the
* request has been handled and is fully reflected in the values returned from {@link
* #getState}. This class will automatically request a state update once the request is done.
* If the state changes can be handled synchronously, these methods can return Guava's {@link
* Futures#immediateVoidFuture()}.
* <li>Subclasses can manually trigger state updates with {@link #invalidateState}, for example if
* something changes independent of {@link Player} method calls.
* </ul>
*
* This base class handles various aspects of the player implementation to simplify the subclass:
*
* <ul>
* <li>The {@link State} can only be created with allowed combinations of state values, avoiding
* any invalid player states.
* <li>Only functionality that is declared as {@linkplain Player.Command available} needs to be
* implemented. Other methods are automatically ignored.
* <li>Listener handling and informing listeners of state changes is handled automatically.
* <li>The base class provides a framework for asynchronous handling of method calls. It changes
* the visible playback state immediately to the most likely outcome to ensure the
* user-visible state changes look like synchronous operations. The state is then updated
* again once the asynchronous method calls have been fully handled.
* </ul>
*/
@UnstableApi
public abstract class SimpleBasePlayer extends BasePlayer {
/** An immutable state description of the player. */
protected static final class State {
/** A builder for {@link State} objects. */
public static final class Builder {
private Commands availableCommands;
private boolean playWhenReady;
private @PlayWhenReadyChangeReason int playWhenReadyChangeReason;
/** Creates the builder. */
public Builder() {
availableCommands = Commands.EMPTY;
playWhenReady = false;
playWhenReadyChangeReason = Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST;
}
private Builder(State state) {
this.availableCommands = state.availableCommands;
this.playWhenReady = state.playWhenReady;
this.playWhenReadyChangeReason = state.playWhenReadyChangeReason;
}
/**
* Sets the available {@link Commands}.
*
* @param availableCommands The available {@link Commands}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setAvailableCommands(Commands availableCommands) {
this.availableCommands = availableCommands;
return this;
}
/**
* Sets whether playback should proceed when ready and not suppressed.
*
* @param playWhenReady Whether playback should proceed when ready and not suppressed.
* @param playWhenReadyChangeReason The {@linkplain PlayWhenReadyChangeReason reason} for
* changing the value.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPlayWhenReady(
boolean playWhenReady, @PlayWhenReadyChangeReason int playWhenReadyChangeReason) {
this.playWhenReady = playWhenReady;
this.playWhenReadyChangeReason = playWhenReadyChangeReason;
return this;
}
/** Builds the {@link State}. */
public State build() {
return new State(this);
}
}
/** The available {@link Commands}. */
public final Commands availableCommands;
/** Whether playback should proceed when ready and not suppressed. */
public final boolean playWhenReady;
/** The last reason for changing {@link #playWhenReady}. */
public final @PlayWhenReadyChangeReason int playWhenReadyChangeReason;
private State(Builder builder) {
this.availableCommands = builder.availableCommands;
this.playWhenReady = builder.playWhenReady;
this.playWhenReadyChangeReason = builder.playWhenReadyChangeReason;
}
/** Returns a {@link Builder} pre-populated with the current state values. */
public Builder buildUpon() {
return new Builder(this);
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof State)) {
return false;
}
State state = (State) o;
return playWhenReady == state.playWhenReady
&& playWhenReadyChangeReason == state.playWhenReadyChangeReason
&& availableCommands.equals(state.availableCommands);
}
@Override
public int hashCode() {
int result = 7;
result = 31 * result + availableCommands.hashCode();
result = 31 * result + (playWhenReady ? 1 : 0);
result = 31 * result + playWhenReadyChangeReason;
return result;
}
}
private final ListenerSet<Listener> listeners;
private final Looper applicationLooper;
private final HandlerWrapper applicationHandler;
private final HashSet<ListenableFuture<?>> pendingOperations;
private @MonotonicNonNull State state;
/**
* Creates the base class.
*
* @param applicationLooper The {@link Looper} that must be used for all calls to the player and
* that is used to call listeners on.
*/
protected SimpleBasePlayer(Looper applicationLooper) {
this(applicationLooper, Clock.DEFAULT);
}
/**
* Creates the base class.
*
* @param applicationLooper The {@link Looper} that must be used for all calls to the player and
* that is used to call listeners on.
* @param clock The {@link Clock} that will be used by the player.
*/
protected SimpleBasePlayer(Looper applicationLooper, Clock clock) {
this.applicationLooper = applicationLooper;
applicationHandler = clock.createHandler(applicationLooper, /* callback= */ null);
pendingOperations = new HashSet<>();
@SuppressWarnings("nullness:argument.type.incompatible") // Using this in constructor.
ListenerSet<Player.Listener> listenerSet =
new ListenerSet<>(
applicationLooper,
clock,
(listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags)));
listeners = listenerSet;
}
@Override
public final void addListener(Listener listener) {
// Don't verify application thread. We allow calls to this method from any thread.
listeners.add(checkNotNull(listener));
}
@Override
public final void removeListener(Listener listener) {
// Don't verify application thread. We allow calls to this method from any thread.
checkNotNull(listener);
listeners.remove(listener);
}
@Override
public final Looper getApplicationLooper() {
// Don't verify application thread. We allow calls to this method from any thread.
return applicationLooper;
}
@Override
public final Commands getAvailableCommands() {
verifyApplicationThreadAndInitState();
return state.availableCommands;
}
@Override
public final void setPlayWhenReady(boolean playWhenReady) {
verifyApplicationThreadAndInitState();
State state = this.state;
if (!state.availableCommands.contains(Player.COMMAND_PLAY_PAUSE)) {
return;
}
updateStateForPendingOperation(
/* pendingOperation= */ handleSetPlayWhenReady(playWhenReady),
/* placeholderStateSupplier= */ () ->
state
.buildUpon()
.setPlayWhenReady(playWhenReady, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build());
}
@Override
public final boolean getPlayWhenReady() {
verifyApplicationThreadAndInitState();
return state.playWhenReady;
}
@Override
public final void setMediaItems(List<MediaItem> mediaItems, boolean resetPosition) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setMediaItems(
List<MediaItem> mediaItems, int startIndex, long startPositionMs) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void addMediaItems(int index, List<MediaItem> mediaItems) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void moveMediaItems(int fromIndex, int toIndex, int newIndex) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void removeMediaItems(int fromIndex, int toIndex) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void prepare() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final int getPlaybackState() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final int getPlaybackSuppressionReason() {
// TODO: implement.
throw new IllegalStateException();
}
@Nullable
@Override
public final PlaybackException getPlayerError() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setRepeatMode(int repeatMode) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final int getRepeatMode() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setShuffleModeEnabled(boolean shuffleModeEnabled) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final boolean getShuffleModeEnabled() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final boolean isLoading() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void seekTo(int mediaItemIndex, long positionMs) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final long getSeekBackIncrement() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final long getSeekForwardIncrement() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final long getMaxSeekToPreviousPosition() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setPlaybackParameters(PlaybackParameters playbackParameters) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final PlaybackParameters getPlaybackParameters() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void stop() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void stop(boolean reset) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void release() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final Tracks getCurrentTracks() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final TrackSelectionParameters getTrackSelectionParameters() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setTrackSelectionParameters(TrackSelectionParameters parameters) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final MediaMetadata getMediaMetadata() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final MediaMetadata getPlaylistMetadata() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setPlaylistMetadata(MediaMetadata mediaMetadata) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final Timeline getCurrentTimeline() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final int getCurrentPeriodIndex() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final int getCurrentMediaItemIndex() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final long getDuration() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final long getCurrentPosition() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final long getBufferedPosition() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final long getTotalBufferedDuration() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final boolean isPlayingAd() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final int getCurrentAdGroupIndex() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final int getCurrentAdIndexInAdGroup() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final long getContentPosition() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final long getContentBufferedPosition() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final AudioAttributes getAudioAttributes() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setVolume(float volume) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final float getVolume() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void clearVideoSurface() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void clearVideoSurface(@Nullable Surface surface) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setVideoSurface(@Nullable Surface surface) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void clearVideoSurfaceHolder(@Nullable SurfaceHolder surfaceHolder) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setVideoSurfaceView(@Nullable SurfaceView surfaceView) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void clearVideoSurfaceView(@Nullable SurfaceView surfaceView) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setVideoTextureView(@Nullable TextureView textureView) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void clearVideoTextureView(@Nullable TextureView textureView) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final VideoSize getVideoSize() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final Size getSurfaceSize() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final CueGroup getCurrentCues() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final DeviceInfo getDeviceInfo() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final int getDeviceVolume() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final boolean isDeviceMuted() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setDeviceVolume(int volume) {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void increaseDeviceVolume() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void decreaseDeviceVolume() {
// TODO: implement.
throw new IllegalStateException();
}
@Override
public final void setDeviceMuted(boolean muted) {
// TODO: implement.
throw new IllegalStateException();
}
/**
* Invalidates the current state.
*
* <p>Triggers a call to {@link #getState()} and informs listeners if the state changed.
*
* <p>Note that this may not have an immediate effect while there are still player methods being
* handled asynchronously. The state will be invalidated automatically once these pending
* synchronous operations are finished and there is no need to call this method again.
*/
protected final void invalidateState() {
verifyApplicationThreadAndInitState();
if (!pendingOperations.isEmpty()) {
return;
}
updateStateAndInformListeners(getState());
}
/**
* Returns the current {@link State} of the player.
*
* <p>The {@link State} should include all {@linkplain
* State.Builder#setAvailableCommands(Commands) available commands} indicating which player
* methods are allowed to be called.
*
* <p>Note that this method won't be called while asynchronous handling of player methods is in
* progress. This means that the implementation doesn't need to handle state changes caused by
* these asynchronous operations until they are done and can return the currently known state
* directly. The placeholder state used while these asynchronous operations are in progress can be
* customized by overriding {@link #getPlaceholderState(State)} if required.
*/
@ForOverride
protected abstract State getState();
/**
* Returns the placeholder state used while a player method is handled asynchronously.
*
* <p>The {@code suggestedPlaceholderState} already contains the most likely state update, for
* example setting {@link State#playWhenReady} to true if {@code player.setPlayWhenReady(true)} is
* called, and an implementations only needs to override this method if it can determine a more
* accurate placeholder state.
*
* @param suggestedPlaceholderState The suggested placeholder {@link State}, including the most
* likely outcome of handling all pending asynchronous operations.
* @return The placeholder {@link State} to use while asynchronous operations are pending.
*/
@ForOverride
protected State getPlaceholderState(State suggestedPlaceholderState) {
return suggestedPlaceholderState;
}
/**
* Handles calls to set {@link State#playWhenReady}.
*
* <p>Will only be called if {@link Player.Command#COMMAND_PLAY_PAUSE} is available.
*
* @param playWhenReady The requested {@link State#playWhenReady}
* @return A {@link ListenableFuture} indicating the completion of all immediate {@link State}
* changes caused by this call.
* @see Player#setPlayWhenReady(boolean)
* @see Player#play()
* @see Player#pause()
*/
@ForOverride
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
throw new IllegalStateException();
}
@SuppressWarnings("deprecation") // Calling deprecated listener methods.
@RequiresNonNull("state")
private void updateStateAndInformListeners(State newState) {
State previousState = state;
// Assign new state immediately such that all getters return the right values, but use a
// snapshot of the previous and new state so that listener invocations are triggered correctly.
this.state = newState;
boolean playWhenReadyChanged = previousState.playWhenReady != newState.playWhenReady;
if (playWhenReadyChanged /* TODO: || playbackStateChanged */) {
listeners.queueEvent(
/* eventFlag= */ C.INDEX_UNSET,
listener ->
listener.onPlayerStateChanged(newState.playWhenReady, /* TODO */ Player.STATE_IDLE));
}
if (playWhenReadyChanged
|| previousState.playWhenReadyChangeReason != newState.playWhenReadyChangeReason) {
listeners.queueEvent(
Player.EVENT_PLAY_WHEN_READY_CHANGED,
listener ->
listener.onPlayWhenReadyChanged(
newState.playWhenReady, newState.playWhenReadyChangeReason));
}
if (isPlaying(previousState) != isPlaying(newState)) {
listeners.queueEvent(
Player.EVENT_IS_PLAYING_CHANGED,
listener -> listener.onIsPlayingChanged(isPlaying(newState)));
}
if (!previousState.availableCommands.equals(newState.availableCommands)) {
listeners.queueEvent(
Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
listener -> listener.onAvailableCommandsChanged(newState.availableCommands));
}
listeners.flushEvents();
}
@EnsuresNonNull("state")
private void verifyApplicationThreadAndInitState() {
if (Thread.currentThread() != applicationLooper.getThread()) {
String message =
Util.formatInvariant(
"Player is accessed on the wrong thread.\n"
+ "Current thread: '%s'\n"
+ "Expected thread: '%s'\n"
+ "See https://exoplayer.dev/issues/player-accessed-on-wrong-thread",
Thread.currentThread().getName(), applicationLooper.getThread().getName());
throw new IllegalStateException(message);
}
if (state == null) {
// First time accessing state.
state = getState();
}
}
@RequiresNonNull("state")
private void updateStateForPendingOperation(
ListenableFuture<?> pendingOperation, Supplier<State> placeholderStateSupplier) {
if (pendingOperation.isDone() && pendingOperations.isEmpty()) {
updateStateAndInformListeners(getState());
} else {
pendingOperations.add(pendingOperation);
State suggestedPlaceholderState = placeholderStateSupplier.get();
updateStateAndInformListeners(getPlaceholderState(suggestedPlaceholderState));
pendingOperation.addListener(
() -> {
castNonNull(state); // Already check by method @RequiresNonNull pre-condition.
pendingOperations.remove(pendingOperation);
if (pendingOperations.isEmpty()) {
updateStateAndInformListeners(getState());
}
},
this::postOrRunOnApplicationHandler);
}
}
private void postOrRunOnApplicationHandler(Runnable runnable) {
if (applicationHandler.getLooper() == Looper.myLooper()) {
runnable.run();
} else {
applicationHandler.post(runnable);
}
}
private static boolean isPlaying(State state) {
return state.playWhenReady && false;
// TODO: && state.playbackState == Player.STATE_READY
// && state.playbackSuppressionReason == PLAYBACK_SUPPRESSION_REASON_NONE
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import static androidx.media3.common.util.Assertions.checkArgument;
import android.view.Surface;
import androidx.annotation.Nullable;
import androidx.media3.common.util.UnstableApi;
/** Immutable value class for a {@link Surface} and supporting information. */
@UnstableApi
public final class SurfaceInfo {
/** The {@link Surface}. */
public final Surface surface;
/** The width of frames rendered to the {@link #surface}, in pixels. */
public final int width;
/** The height of frames rendered to the {@link #surface}, in pixels. */
public final int height;
/**
* A counter-clockwise rotation to apply to frames before rendering them to the {@link #surface}.
*
* <p>Must be 0, 90, 180, or 270 degrees. Default is 0.
*/
public final int orientationDegrees;
/** Creates a new instance. */
public SurfaceInfo(Surface surface, int width, int height) {
this(surface, width, height, /* orientationDegrees= */ 0);
}
/** Creates a new instance. */
public SurfaceInfo(Surface surface, int width, int height, int orientationDegrees) {
checkArgument(
orientationDegrees == 0
|| orientationDegrees == 90
|| orientationDegrees == 180
|| orientationDegrees == 270,
"orientationDegrees must be 0, 90, 180, or 270");
this.surface = surface;
this.width = width;
this.height = height;
this.orientationDegrees = orientationDegrees;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof SurfaceInfo)) {
return false;
}
SurfaceInfo that = (SurfaceInfo) o;
return width == that.width
&& height == that.height
&& orientationDegrees == that.orientationDegrees
&& surface.equals(that.surface);
}
@Override
public int hashCode() {
int result = surface.hashCode();
result = 31 * result + width;
result = 31 * result + height;
result = 31 * result + orientationDegrees;
return result;
}
}

View File

@ -34,6 +34,7 @@ import androidx.media3.common.util.BundleUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.InlineMe;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
@ -261,6 +262,7 @@ public abstract class Timeline implements Bundleable {
}
/** Sets the data held by this window. */
@CanIgnoreReturnValue
@UnstableApi
@SuppressWarnings("deprecation")
public Window set(
@ -626,6 +628,7 @@ public abstract class Timeline implements Bundleable {
* period is not within the window.
* @return This period, for convenience.
*/
@CanIgnoreReturnValue
@UnstableApi
public Period set(
@Nullable Object id,
@ -662,6 +665,7 @@ public abstract class Timeline implements Bundleable {
* information has yet to be loaded.
* @return This period, for convenience.
*/
@CanIgnoreReturnValue
@UnstableApi
public Period set(
@Nullable Object id,

View File

@ -26,11 +26,11 @@ import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@ -179,8 +179,11 @@ public final class TrackGroup implements Bundleable {
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_FORMATS), BundleableUtil.toBundleArrayList(Lists.newArrayList(formats)));
ArrayList<Bundle> arrayList = new ArrayList<>(formats.length);
for (Format format : formats) {
arrayList.add(format.toBundle(/* excludeMetadata= */ true));
}
bundle.putParcelableArrayList(keyForField(FIELD_FORMATS), arrayList);
bundle.putString(keyForField(FIELD_ID), id);
return bundle;
}

View File

@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Ints;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
@ -306,6 +307,7 @@ public class TrackSelectionParameters implements Bundleable {
}
/** Overrides the value of the builder with the value of {@link TrackSelectionParameters}. */
@CanIgnoreReturnValue
@UnstableApi
protected Builder set(TrackSelectionParameters parameters) {
init(parameters);
@ -319,6 +321,7 @@ public class TrackSelectionParameters implements Bundleable {
*
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMaxVideoSizeSd() {
return setMaxVideoSize(1279, 719);
}
@ -328,6 +331,7 @@ public class TrackSelectionParameters implements Bundleable {
*
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder clearVideoSizeConstraints() {
return setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
@ -339,6 +343,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param maxVideoHeight Maximum allowed video height in pixels.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {
this.maxVideoWidth = maxVideoWidth;
this.maxVideoHeight = maxVideoHeight;
@ -351,6 +356,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param maxVideoFrameRate Maximum allowed video frame rate in hertz.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMaxVideoFrameRate(int maxVideoFrameRate) {
this.maxVideoFrameRate = maxVideoFrameRate;
return this;
@ -362,6 +368,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param maxVideoBitrate Maximum allowed video bitrate in bits per second.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMaxVideoBitrate(int maxVideoBitrate) {
this.maxVideoBitrate = maxVideoBitrate;
return this;
@ -374,6 +381,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param minVideoHeight Minimum allowed video height in pixels.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMinVideoSize(int minVideoWidth, int minVideoHeight) {
this.minVideoWidth = minVideoWidth;
this.minVideoHeight = minVideoHeight;
@ -386,6 +394,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param minVideoFrameRate Minimum allowed video frame rate in hertz.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMinVideoFrameRate(int minVideoFrameRate) {
this.minVideoFrameRate = minVideoFrameRate;
return this;
@ -397,6 +406,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param minVideoBitrate Minimum allowed video bitrate in bits per second.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMinVideoBitrate(int minVideoBitrate) {
this.minVideoBitrate = minVideoBitrate;
return this;
@ -411,6 +421,7 @@ public class TrackSelectionParameters implements Bundleable {
* playback.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setViewportSizeToPhysicalDisplaySize(
Context context, boolean viewportOrientationMayChange) {
// Assume the viewport is fullscreen.
@ -424,6 +435,7 @@ public class TrackSelectionParameters implements Bundleable {
*
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder clearViewportSizeConstraints() {
return setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true);
}
@ -438,6 +450,7 @@ public class TrackSelectionParameters implements Bundleable {
* playback.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setViewportSize(
int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
this.viewportWidth = viewportWidth;
@ -464,6 +477,7 @@ public class TrackSelectionParameters implements Bundleable {
* empty list for no preference.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPreferredVideoMimeTypes(String... mimeTypes) {
preferredVideoMimeTypes = ImmutableList.copyOf(mimeTypes);
return this;
@ -475,6 +489,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param preferredVideoRoleFlags Preferred video role flags.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPreferredVideoRoleFlags(@C.RoleFlags int preferredVideoRoleFlags) {
this.preferredVideoRoleFlags = preferredVideoRoleFlags;
return this;
@ -503,6 +518,7 @@ public class TrackSelectionParameters implements Bundleable {
* there's no default.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPreferredAudioLanguages(String... preferredAudioLanguages) {
this.preferredAudioLanguages = normalizeLanguageCodes(preferredAudioLanguages);
return this;
@ -514,6 +530,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param preferredAudioRoleFlags Preferred audio role flags.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPreferredAudioRoleFlags(@C.RoleFlags int preferredAudioRoleFlags) {
this.preferredAudioRoleFlags = preferredAudioRoleFlags;
return this;
@ -525,6 +542,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param maxAudioChannelCount Maximum allowed audio channel count.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMaxAudioChannelCount(int maxAudioChannelCount) {
this.maxAudioChannelCount = maxAudioChannelCount;
return this;
@ -536,6 +554,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param maxAudioBitrate Maximum allowed audio bitrate in bits per second.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setMaxAudioBitrate(int maxAudioBitrate) {
this.maxAudioBitrate = maxAudioBitrate;
return this;
@ -559,6 +578,7 @@ public class TrackSelectionParameters implements Bundleable {
* empty list for no preference.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPreferredAudioMimeTypes(String... mimeTypes) {
preferredAudioMimeTypes = ImmutableList.copyOf(mimeTypes);
return this;
@ -575,6 +595,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param context A {@link Context}.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(
Context context) {
if (Util.SDK_INT >= 19) {
@ -604,6 +625,7 @@ public class TrackSelectionParameters implements Bundleable {
* track otherwise.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPreferredTextLanguages(String... preferredTextLanguages) {
this.preferredTextLanguages = normalizeLanguageCodes(preferredTextLanguages);
return this;
@ -615,6 +637,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param preferredTextRoleFlags Preferred text role flags.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {
this.preferredTextRoleFlags = preferredTextRoleFlags;
return this;
@ -627,6 +650,7 @@ public class TrackSelectionParameters implements Bundleable {
* text track selections.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setIgnoredTextSelectionFlags(@C.SelectionFlags int ignoredTextSelectionFlags) {
this.ignoredTextSelectionFlags = ignoredTextSelectionFlags;
return this;
@ -641,6 +665,7 @@ public class TrackSelectionParameters implements Bundleable {
* be selected if no preferred language track is available.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) {
this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage;
return this;
@ -656,6 +681,7 @@ public class TrackSelectionParameters implements Bundleable {
* video tracks.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setForceLowestBitrate(boolean forceLowestBitrate) {
this.forceLowestBitrate = forceLowestBitrate;
return this;
@ -669,18 +695,21 @@ public class TrackSelectionParameters implements Bundleable {
* and video tracks.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) {
this.forceHighestSupportedBitrate = forceHighestSupportedBitrate;
return this;
}
/** Adds an override, replacing any override for the same {@link TrackGroup}. */
@CanIgnoreReturnValue
public Builder addOverride(TrackSelectionOverride override) {
overrides.put(override.mediaTrackGroup, override);
return this;
}
/** Sets an override, replacing all existing overrides with the same track type. */
@CanIgnoreReturnValue
public Builder setOverrideForType(TrackSelectionOverride override) {
clearOverridesOfType(override.getType());
overrides.put(override.mediaTrackGroup, override);
@ -688,12 +717,14 @@ public class TrackSelectionParameters implements Bundleable {
}
/** Removes the override for the provided media {@link TrackGroup}, if there is one. */
@CanIgnoreReturnValue
public Builder clearOverride(TrackGroup mediaTrackGroup) {
overrides.remove(mediaTrackGroup);
return this;
}
/** Removes all overrides of the provided track type. */
@CanIgnoreReturnValue
public Builder clearOverridesOfType(@C.TrackType int trackType) {
Iterator<TrackSelectionOverride> it = overrides.values().iterator();
while (it.hasNext()) {
@ -706,6 +737,7 @@ public class TrackSelectionParameters implements Bundleable {
}
/** Removes all overrides. */
@CanIgnoreReturnValue
public Builder clearOverrides() {
overrides.clear();
return this;
@ -719,6 +751,7 @@ public class TrackSelectionParameters implements Bundleable {
* @return This builder.
* @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}.
*/
@CanIgnoreReturnValue
@Deprecated
@UnstableApi
public Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) {
@ -735,6 +768,7 @@ public class TrackSelectionParameters implements Bundleable {
* @param disabled Whether the track type should be disabled.
* @return This builder.
*/
@CanIgnoreReturnValue
public Builder setTrackTypeDisabled(@C.TrackType int trackType, boolean disabled) {
if (disabled) {
disabledTrackTypes.add(trackType);

View File

@ -13,12 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.audio;
package androidx.media3.common.audio;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@ -70,6 +73,25 @@ public interface AudioProcessor {
+ encoding
+ ']';
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof AudioFormat)) {
return false;
}
AudioFormat that = (AudioFormat) o;
return sampleRate == that.sampleRate
&& channelCount == that.channelCount
&& encoding == that.encoding;
}
@Override
public int hashCode() {
return Objects.hashCode(sampleRate, channelCount, encoding);
}
}
/** Exception thrown when a processor can't be configured for a given input audio format. */
@ -98,6 +120,7 @@ public interface AudioProcessor {
* @return The configured output audio format if this instance is {@link #isActive() active}.
* @throws UnhandledAudioFormatException Thrown if the specified format can't be handled as input.
*/
@CanIgnoreReturnValue
AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException;
/** Returns whether the processor is configured and will process input buffers. */
@ -134,8 +157,8 @@ public interface AudioProcessor {
ByteBuffer getOutput();
/**
* Returns whether this processor will return no more output from {@link #getOutput()} until it
* has been {@link #flush()}ed and more input has been queued.
* Returns whether this processor will return no more output from {@link #getOutput()} until
* {@link #flush()} has been called and more input has been queued.
*/
boolean isEnded();

View File

@ -0,0 +1,74 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common.audio;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.util.UnstableApi;
/**
* Provides a chain of audio processors, which are used for any user-defined processing and applying
* playback parameters (if supported). Because applying playback parameters can skip and
* stretch/compress audio, the sink will query the chain for information on how to transform its
* output position to map it onto a media position, via {@link #getMediaDuration(long)} and {@link
* #getSkippedOutputFrameCount()}.
*/
@UnstableApi
public interface AudioProcessorChain {
/**
* Returns the fixed chain of audio processors that will process audio. This method is called once
* during initialization, but audio processors may change state to become active/inactive during
* playback.
*/
AudioProcessor[] getAudioProcessors();
/**
* Configures audio processors to apply the specified playback parameters immediately, returning
* the new playback parameters, which may differ from those passed in. Only called when processors
* have no input pending.
*
* @param playbackParameters The playback parameters to try to apply.
* @return The playback parameters that were actually applied.
*/
PlaybackParameters applyPlaybackParameters(PlaybackParameters playbackParameters);
/**
* Configures audio processors to apply whether to skip silences immediately, returning the new
* value. Only called when processors have no input pending.
*
* @param skipSilenceEnabled Whether silences should be skipped in the audio stream.
* @return The new value.
*/
boolean applySkipSilenceEnabled(boolean skipSilenceEnabled);
/**
* Returns the media duration corresponding to the specified playout duration, taking speed
* adjustment due to audio processing into account.
*
* <p>The scaling performed by this method will use the actual playback speed achieved by the
* audio processor chain, on average, since it was last flushed. This may differ very slightly
* from the target playback speed.
*
* @param playoutDuration The playout duration to scale.
* @return The corresponding media duration, in the same units as {@code duration}.
*/
long getMediaDuration(long playoutDuration);
/**
* Returns the number of output audio frames skipped since the audio processors were last flushed.
*/
long getSkippedOutputFrameCount();
}

View File

@ -0,0 +1,19 @@
/*
* 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.
*/
@NonNullApi
package androidx.media3.common.audio;
import androidx.media3.common.util.NonNullApi;

View File

@ -36,6 +36,7 @@ import androidx.media3.common.Bundleable;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import com.google.common.base.Objects;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -628,6 +629,7 @@ public final class Cue implements Bundleable {
*
* @see Cue#text
*/
@CanIgnoreReturnValue
public Builder setText(CharSequence text) {
this.text = text;
return this;
@ -649,6 +651,7 @@ public final class Cue implements Bundleable {
*
* @see Cue#bitmap
*/
@CanIgnoreReturnValue
public Builder setBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
return this;
@ -672,6 +675,7 @@ public final class Cue implements Bundleable {
*
* @see Cue#textAlignment
*/
@CanIgnoreReturnValue
public Builder setTextAlignment(@Nullable Layout.Alignment textAlignment) {
this.textAlignment = textAlignment;
return this;
@ -695,6 +699,7 @@ public final class Cue implements Bundleable {
*
* @see Cue#multiRowAlignment
*/
@CanIgnoreReturnValue
public Builder setMultiRowAlignment(@Nullable Layout.Alignment multiRowAlignment) {
this.multiRowAlignment = multiRowAlignment;
return this;
@ -707,6 +712,7 @@ public final class Cue implements Bundleable {
* @see Cue#line
* @see Cue#lineType
*/
@CanIgnoreReturnValue
public Builder setLine(float line, @LineType int lineType) {
this.line = line;
this.lineType = lineType;
@ -739,6 +745,7 @@ public final class Cue implements Bundleable {
*
* @see Cue#lineAnchor
*/
@CanIgnoreReturnValue
public Builder setLineAnchor(@AnchorType int lineAnchor) {
this.lineAnchor = lineAnchor;
return this;
@ -760,6 +767,7 @@ public final class Cue implements Bundleable {
*
* @see Cue#position
*/
@CanIgnoreReturnValue
public Builder setPosition(float position) {
this.position = position;
return this;
@ -781,6 +789,7 @@ public final class Cue implements Bundleable {
*
* @see Cue#positionAnchor
*/
@CanIgnoreReturnValue
public Builder setPositionAnchor(@AnchorType int positionAnchor) {
this.positionAnchor = positionAnchor;
return this;
@ -802,6 +811,7 @@ public final class Cue implements Bundleable {
* @see Cue#textSize
* @see Cue#textSizeType
*/
@CanIgnoreReturnValue
public Builder setTextSize(float textSize, @TextSizeType int textSizeType) {
this.textSize = textSize;
this.textSizeType = textSizeType;
@ -834,6 +844,7 @@ public final class Cue implements Bundleable {
*
* @see Cue#size
*/
@CanIgnoreReturnValue
public Builder setSize(float size) {
this.size = size;
return this;
@ -855,6 +866,7 @@ public final class Cue implements Bundleable {
*
* @see Cue#bitmapHeight
*/
@CanIgnoreReturnValue
public Builder setBitmapHeight(float bitmapHeight) {
this.bitmapHeight = bitmapHeight;
return this;
@ -878,6 +890,7 @@ public final class Cue implements Bundleable {
* @see Cue#windowColor
* @see Cue#windowColorSet
*/
@CanIgnoreReturnValue
public Builder setWindowColor(@ColorInt int windowColor) {
this.windowColor = windowColor;
this.windowColorSet = true;
@ -885,6 +898,7 @@ public final class Cue implements Bundleable {
}
/** Sets {@link Cue#windowColorSet} to false. */
@CanIgnoreReturnValue
public Builder clearWindowColor() {
this.windowColorSet = false;
return this;
@ -915,12 +929,14 @@ public final class Cue implements Bundleable {
*
* @see Cue#verticalType
*/
@CanIgnoreReturnValue
public Builder setVerticalType(@VerticalType int verticalType) {
this.verticalType = verticalType;
return this;
}
/** Sets the shear angle for this Cue. */
@CanIgnoreReturnValue
public Builder setShearDegrees(float shearDegrees) {
this.shearDegrees = shearDegrees;
return this;

View File

@ -22,6 +22,7 @@ import android.os.Bundle;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.media3.common.Bundleable;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.BundleableUtil;
import androidx.media3.common.util.UnstableApi;
import com.google.common.collect.ImmutableList;
@ -35,8 +36,10 @@ 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());
/** An empty group with no {@link Cue Cues} and presentation time of zero. */
@UnstableApi
public static final CueGroup EMPTY_TIME_ZERO =
new CueGroup(ImmutableList.of(), /* presentationTimeUs= */ 0);
/**
* The cues in this group.
@ -47,11 +50,18 @@ public final class CueGroup implements Bundleable {
* <p>This list may be empty if the group represents a state with no cues.
*/
public final ImmutableList<Cue> cues;
/**
* The presentation time of the {@link #cues}, in microseconds.
*
* <p>This time is an offset from the start of the current {@link Timeline.Period}.
*/
@UnstableApi public final long presentationTimeUs;
/** Creates a CueGroup. */
@UnstableApi
public CueGroup(List<Cue> cues) {
public CueGroup(List<Cue> cues, long presentationTimeUs) {
this.cues = ImmutableList.copyOf(cues);
this.presentationTimeUs = presentationTimeUs;
}
// Bundleable implementation.
@ -59,10 +69,11 @@ public final class CueGroup implements Bundleable {
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({FIELD_CUES})
@IntDef({FIELD_CUES, FIELD_PRESENTATION_TIME_US})
private @interface FieldNumber {}
private static final int FIELD_CUES = 0;
private static final int FIELD_PRESENTATION_TIME_US = 1;
@UnstableApi
@Override
@ -70,6 +81,7 @@ public final class CueGroup implements Bundleable {
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(
keyForField(FIELD_CUES), BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues)));
bundle.putLong(keyForField(FIELD_PRESENTATION_TIME_US), presentationTimeUs);
return bundle;
}
@ -81,7 +93,8 @@ public final class CueGroup implements Bundleable {
cueBundles == null
? ImmutableList.of()
: BundleableUtil.fromBundleList(Cue.CREATOR, cueBundles);
return new CueGroup(cues);
long presentationTimeUs = bundle.getLong(keyForField(FIELD_PRESENTATION_TIME_US));
return new CueGroup(cues, presentationTimeUs);
}
private static String keyForField(@FieldNumber int field) {

View File

@ -79,13 +79,6 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
/** A runtime exception to be thrown if some EGL operations failed. */
public static final class GlException extends RuntimeException {
private GlException(String msg) {
super(msg);
}
}
private final Handler handler;
private final int[] textureIdHolder;
@Nullable private final TextureImageListener callback;
@ -125,7 +118,7 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
*
* @param secureMode The {@link SecureMode} to be used for EGL surface.
*/
public void init(@SecureMode int secureMode) {
public void init(@SecureMode int secureMode) throws GlUtil.GlException {
display = getDefaultDisplay();
EGLConfig config = chooseEGLConfig(display);
context = createEGLContext(display, config, secureMode);
@ -206,22 +199,18 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
}
}
private static EGLDisplay getDefaultDisplay() {
private static EGLDisplay getDefaultDisplay() throws GlUtil.GlException {
EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
if (display == null) {
throw new GlException("eglGetDisplay failed");
}
GlUtil.checkGlException(display != null, "eglGetDisplay failed");
int[] version = new int[2];
boolean eglInitialized =
EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1);
if (!eglInitialized) {
throw new GlException("eglInitialize failed");
}
GlUtil.checkGlException(eglInitialized, "eglInitialize failed");
return display;
}
private static EGLConfig chooseEGLConfig(EGLDisplay display) {
private static EGLConfig chooseEGLConfig(EGLDisplay display) throws GlUtil.GlException {
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
boolean success =
@ -234,18 +223,17 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
/* config_size= */ 1,
numConfigs,
/* num_configOffset= */ 0);
if (!success || numConfigs[0] <= 0 || configs[0] == null) {
throw new GlException(
Util.formatInvariant(
/* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
success, numConfigs[0], configs[0]));
}
GlUtil.checkGlException(
success && numConfigs[0] > 0 && configs[0] != null,
Util.formatInvariant(
/* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
success, numConfigs[0], configs[0]));
return configs[0];
}
private static EGLContext createEGLContext(
EGLDisplay display, EGLConfig config, @SecureMode int secureMode) {
EGLDisplay display, EGLConfig config, @SecureMode int secureMode) throws GlUtil.GlException {
int[] glAttributes;
if (secureMode == SECURE_MODE_NONE) {
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
@ -262,14 +250,13 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
EGLContext context =
EGL14.eglCreateContext(
display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0);
if (context == null) {
throw new GlException("eglCreateContext failed");
}
GlUtil.checkGlException(context != null, "eglCreateContext failed");
return context;
}
private static EGLSurface createEGLSurface(
EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) {
EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode)
throws GlUtil.GlException {
EGLSurface surface;
if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {
surface = EGL14.EGL_NO_SURFACE;
@ -297,20 +284,16 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
};
}
surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0);
if (surface == null) {
throw new GlException("eglCreatePbufferSurface failed");
}
GlUtil.checkGlException(surface != null, "eglCreatePbufferSurface failed");
}
boolean eglMadeCurrent =
EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context);
if (!eglMadeCurrent) {
throw new GlException("eglMakeCurrent failed");
}
GlUtil.checkGlException(eglMadeCurrent, "eglMakeCurrent failed");
return surface;
}
private static void generateTextureIds(int[] textureIdHolder) {
private static void generateTextureIds(int[] textureIdHolder) throws GlUtil.GlException {
GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0);
GlUtil.checkGlError();
}

View File

@ -22,6 +22,7 @@ import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import androidx.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.Buffer;
import java.util.HashMap;
import java.util.Map;
@ -54,10 +55,26 @@ public final class GlProgram {
* @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));
throws IOException, GlUtil.GlException {
this(loadAsset(context, vertexShaderFilePath), loadAsset(context, fragmentShaderFilePath));
}
/**
* Loads a file from the assets folder.
*
* @param context The {@link Context}.
* @param assetPath The path to the file to load, from the assets folder.
* @return The content of the file to load.
* @throws IOException If the file couldn't be read.
*/
public static String loadAsset(Context context, String assetPath) throws IOException {
@Nullable InputStream inputStream = null;
try {
inputStream = context.getAssets().open(assetPath);
return Util.fromUtf8Bytes(Util.toByteArray(inputStream));
} finally {
Util.closeQuietly(inputStream);
}
}
/**
@ -69,7 +86,7 @@ public final class GlProgram {
* @param vertexShaderGlsl The vertex shader program.
* @param fragmentShaderGlsl The fragment shader program.
*/
public GlProgram(String vertexShaderGlsl, String fragmentShaderGlsl) {
public GlProgram(String vertexShaderGlsl, String fragmentShaderGlsl) throws GlUtil.GlException {
programId = GLES20.glCreateProgram();
GlUtil.checkGlError();
@ -81,10 +98,9 @@ public final class GlProgram {
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));
}
GlUtil.checkGlException(
linkStatus[0] == GLES20.GL_TRUE,
"Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(programId));
GLES20.glUseProgram(programId);
attributeByName = new HashMap<>();
int[] attributeCount = new int[1];
@ -107,16 +123,15 @@ public final class GlProgram {
GlUtil.checkGlError();
}
private static void addShader(int programId, int type, String glsl) {
private static void addShader(int programId, int type, String glsl) throws GlUtil.GlException {
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);
}
GlUtil.checkGlException(
result[0] == GLES20.GL_TRUE, GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
GLES20.glAttachShader(programId, shader);
GLES20.glDeleteShader(shader);
@ -146,13 +161,13 @@ public final class GlProgram {
*
* <p>Call this in the rendering loop to switch between different programs.
*/
public void use() {
public void use() throws GlUtil.GlException {
GLES20.glUseProgram(programId);
GlUtil.checkGlError();
}
/** Deletes the program. Deleted programs cannot be used again. */
public void delete() {
public void delete() throws GlUtil.GlException {
GLES20.glDeleteProgram(programId);
GlUtil.checkGlError();
}
@ -161,7 +176,7 @@ public final class GlProgram {
* Returns the location of an {@link Attribute}, which has been enabled as a vertex attribute
* array.
*/
public int getAttributeArrayLocationAndEnable(String attributeName) {
public int getAttributeArrayLocationAndEnable(String attributeName) throws GlUtil.GlException {
int location = getAttributeLocation(attributeName);
GLES20.glEnableVertexAttribArray(location);
GlUtil.checkGlError();
@ -185,18 +200,23 @@ public final class GlProgram {
checkNotNull(uniformByName.get(name)).setSamplerTexId(texId, texUnitIndex);
}
/** Sets a float type uniform. */
/** Sets an {@code int} type uniform. */
public void setIntUniform(String name, int value) {
checkNotNull(uniformByName.get(name)).setInt(value);
}
/** Sets a {@code float} type uniform. */
public void setFloatUniform(String name, float value) {
checkNotNull(uniformByName.get(name)).setFloat(value);
}
/** Sets a float array type uniform. */
/** Sets a {@code float[]} 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() {
public void bindAttributesAndUniforms() throws GlUtil.GlException {
for (Attribute attribute : attributes) {
attribute.bind();
}
@ -277,7 +297,7 @@ public final class GlProgram {
*
* <p>Should be called before each drawing call.
*/
public void bind() {
public void bind() throws GlUtil.GlException {
Buffer buffer = checkNotNull(this.buffer, "call setBuffer before bind");
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, /* buffer= */ 0);
GLES20.glVertexAttribPointer(
@ -324,16 +344,17 @@ public final class GlProgram {
private final int location;
private final int type;
private final float[] value;
private final float[] floatValue;
private int texId;
private int intValue;
private int texIdValue;
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];
this.floatValue = new float[16];
}
/**
@ -343,18 +364,22 @@ public final class GlProgram {
* @param texUnitIndex The GL texture unit index.
*/
public void setSamplerTexId(int texId, int texUnitIndex) {
this.texId = texId;
this.texIdValue = 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 {@code int} {@code value}. */
public void setInt(int value) {
this.intValue = value;
}
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
/** Configures {@link #bind()} to use the specified {@code float} {@code value}. */
public void setFloat(float value) {
this.floatValue[0] = value;
}
/** Configures {@link #bind()} to use the specified {@code float[]} {@code value}. */
public void setFloats(float[] value) {
System.arraycopy(value, /* srcPos= */ 0, this.value, /* destPos= */ 0, value.length);
System.arraycopy(value, /* srcPos= */ 0, this.floatValue, /* destPos= */ 0, value.length);
}
/**
@ -363,34 +388,37 @@ public final class GlProgram {
*
* <p>Should be called before each drawing call.
*/
public void bind() {
public void bind() throws GlUtil.GlException {
switch (type) {
case GLES20.GL_INT:
GLES20.glUniform1i(location, intValue);
break;
case GLES20.GL_FLOAT:
GLES20.glUniform1fv(location, /* count= */ 1, value, /* offset= */ 0);
GLES20.glUniform1fv(location, /* count= */ 1, floatValue, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_VEC2:
GLES20.glUniform2fv(location, /* count= */ 1, value, /* offset= */ 0);
GLES20.glUniform2fv(location, /* count= */ 1, floatValue, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_VEC3:
GLES20.glUniform3fv(location, /* count= */ 1, value, /* offset= */ 0);
GLES20.glUniform3fv(location, /* count= */ 1, floatValue, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_MAT3:
GLES20.glUniformMatrix3fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
location, /* count= */ 1, /* transpose= */ false, floatValue, /* offset= */ 0);
GlUtil.checkGlError();
break;
case GLES20.GL_FLOAT_MAT4:
GLES20.glUniformMatrix4fv(
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
location, /* count= */ 1, /* transpose= */ false, floatValue, /* 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) {
if (texIdValue == 0) {
throw new IllegalStateException("No call to setSamplerTexId() before bind.");
}
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + texUnitIndex);
@ -399,7 +427,7 @@ public final class GlProgram {
type == GLES20.GL_SAMPLER_2D
? GLES20.GL_TEXTURE_2D
: GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
texId);
texIdValue);
GLES20.glUniform1i(location, texUnitIndex);
GlUtil.checkGlError();
break;

View File

@ -16,6 +16,8 @@
package androidx.media3.common.util;
import static android.opengl.GLU.gluErrorString;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkState;
import android.content.Context;
import android.content.pm.PackageManager;
@ -26,15 +28,16 @@ import android.opengl.EGLDisplay;
import android.opengl.EGLSurface;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.GLES30;
import android.opengl.Matrix;
import androidx.annotation.DoNotInline;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.Arrays;
import java.util.List;
import javax.microedition.khronos.egl.EGL10;
@ -43,41 +46,21 @@ import javax.microedition.khronos.egl.EGL10;
@UnstableApi
public final class GlUtil {
/** Thrown when an OpenGL error occurs and {@link #glAssertionsEnabled} is {@code true}. */
public static final class GlException extends RuntimeException {
/** Thrown when an OpenGL error occurs. */
public static final class GlException extends Exception {
/** Creates an instance with the specified error message. */
public GlException(String message) {
super(message);
}
}
// TODO(b/231937416): Consider removing this flag, enabling assertions by default, and making
// GlException checked.
/** Whether to throw a {@link GlException} in case of an OpenGL error. */
public static boolean glAssertionsEnabled = false;
/** Number of elements in a 3d homogeneous coordinate vector describing a vertex. */
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";
// https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_protected_content.txt
private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
// https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_surfaceless_context.txt
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
// https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt
private static final int EGL_GL_COLORSPACE_KHR = 0x309D;
// https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt
private static final int EGL_GL_COLORSPACE_BT2020_PQ_EXT = 0x3340;
private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_NONE = new int[] {EGL14.EGL_NONE};
private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ =
new int[] {EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_BT2020_PQ_EXT, EGL14.EGL_NONE};
private static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_8888 =
public static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_8888 =
new int[] {
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_RED_SIZE, /* redSize= */ 8,
@ -88,7 +71,7 @@ public final class GlUtil {
EGL14.EGL_STENCIL_SIZE, /* stencilSize= */ 0,
EGL14.EGL_NONE
};
private static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_1010102 =
public static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_1010102 =
new int[] {
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
EGL14.EGL_RED_SIZE, /* redSize= */ 10,
@ -100,6 +83,15 @@ public final class GlUtil {
EGL14.EGL_NONE
};
// https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_protected_content.txt
private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
// https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_surfaceless_context.txt
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 String EXTENSION_YUV_TARGET = "GL_EXT_YUV_target";
private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_NONE = new int[] {EGL14.EGL_NONE};
/** Class only contains static methods. */
private GlUtil() {}
@ -123,6 +115,18 @@ public final class GlUtil {
};
}
/** Creates a 4x4 identity matrix. */
public static float[] create4x4IdentityMatrix() {
float[] matrix = new float[16];
setToIdentity(matrix);
return matrix;
}
/** Sets the input {@code matrix} to an identity matrix. */
public static void setToIdentity(float[] matrix) {
Matrix.setIdentityM(matrix, /* smOffset= */ 0);
}
/** 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()];
@ -182,25 +186,88 @@ public final class GlUtil {
return eglExtensions != null && eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT);
}
/**
* Returns whether the {@value #EXTENSION_YUV_TARGET} extension is supported.
*
* <p>This extension allows sampling raw YUV values from an external texture, which is required
* for HDR.
*/
public static boolean isYuvTargetExtensionSupported() {
if (Util.SDK_INT < 17) {
return false;
}
@Nullable String glExtensions;
if (Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT)) {
// Create a placeholder context and make it current to allow calling GLES20.glGetString().
try {
EGLDisplay eglDisplay = createEglDisplay();
EGLContext eglContext = createEglContext(eglDisplay);
focusPlaceholderEglSurface(eglContext, eglDisplay);
glExtensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);
destroyEglContext(eglDisplay, eglContext);
} catch (GlException e) {
return false;
}
} else {
glExtensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);
}
return glExtensions != null && glExtensions.contains(EXTENSION_YUV_TARGET);
}
/** Returns an initialized default {@link EGLDisplay}. */
@RequiresApi(17)
public static EGLDisplay createEglDisplay() {
public static EGLDisplay createEglDisplay() throws GlException {
return Api17.createEglDisplay();
}
/** Returns a new {@link EGLContext} for the specified {@link EGLDisplay}. */
/**
* Creates a new {@link EGLContext} for the specified {@link EGLDisplay}.
*
* <p>Configures the {@link EGLContext} with {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888} and OpenGL
* ES 2.0.
*
* @param eglDisplay The {@link EGLDisplay} to create an {@link EGLContext} for.
*/
@RequiresApi(17)
public static EGLContext createEglContext(EGLDisplay eglDisplay) {
return Api17.createEglContext(eglDisplay, /* version= */ 2, EGL_CONFIG_ATTRIBUTES_RGBA_8888);
public static EGLContext createEglContext(EGLDisplay eglDisplay) throws GlException {
return createEglContext(eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888);
}
/**
* Returns a new {@link EGLContext} for the specified {@link EGLDisplay}, requesting ES 3 and an
* RGBA 1010102 config.
* Creates a new {@link EGLContext} for the specified {@link EGLDisplay}.
*
* @param eglDisplay The {@link EGLDisplay} to create an {@link EGLContext} for.
* @param configAttributes The attributes to configure EGL with. Accepts either {@link
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102}, which will request OpenGL ES 3.0, or {@link
* #EGL_CONFIG_ATTRIBUTES_RGBA_8888}, which will request OpenGL ES 2.0.
*/
@RequiresApi(17)
public static EGLContext createEglContextEs3Rgba1010102(EGLDisplay eglDisplay) {
return Api17.createEglContext(eglDisplay, /* version= */ 3, EGL_CONFIG_ATTRIBUTES_RGBA_1010102);
public static EGLContext createEglContext(EGLDisplay eglDisplay, int[] configAttributes)
throws GlException {
checkArgument(
Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_8888)
|| Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_1010102));
return Api17.createEglContext(
eglDisplay,
/* version= */ Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_1010102) ? 3 : 2,
configAttributes);
}
/**
* Returns a new {@link EGLSurface} wrapping the specified {@code surface}.
*
* <p>The {@link EGLSurface} will configure with {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888} and
* OpenGL ES 2.0.
*
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @param surface The surface to wrap; must be a surface, surface texture or surface holder.
*/
@RequiresApi(17)
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) throws GlException {
return Api17.getEglSurface(
eglDisplay, surface, EGL_CONFIG_ATTRIBUTES_RGBA_8888, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE);
}
/**
@ -208,27 +275,14 @@ public final class GlUtil {
*
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @param surface The surface to wrap; must be a surface, surface texture or surface holder.
* @param configAttributes The attributes to configure EGL with. Accepts {@link
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}.
*/
@RequiresApi(17)
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) {
public static EGLSurface getEglSurface(
EGLDisplay eglDisplay, Object surface, int[] configAttributes) throws GlException {
return Api17.getEglSurface(
eglDisplay, surface, EGL_CONFIG_ATTRIBUTES_RGBA_8888, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE);
}
/**
* Returns a new {@link EGLSurface} wrapping the specified {@code surface}, for HDR rendering with
* Rec. 2020 color primaries and using the PQ transfer function.
*
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @param surface The surface to wrap; must be a surface, surface texture or surface holder.
*/
@RequiresApi(17)
public static EGLSurface getEglSurfaceBt2020Pq(EGLDisplay eglDisplay, Object surface) {
return Api17.getEglSurface(
eglDisplay,
surface,
EGL_CONFIG_ATTRIBUTES_RGBA_1010102,
EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ);
eglDisplay, surface, configAttributes, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE);
}
/**
@ -237,84 +291,81 @@ public final class GlUtil {
* @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.
* @param configAttributes EGL configuration attributes. Valid arguments include {@link
* #EGL_CONFIG_ATTRIBUTES_RGBA_8888} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_1010102}.
*/
@RequiresApi(17)
private static EGLSurface createPbufferSurface(EGLDisplay eglDisplay, int width, int height) {
private static EGLSurface createPbufferSurface(
EGLDisplay eglDisplay, int width, int height, int[] configAttributes) throws GlException {
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);
return Api17.createEglPbufferSurface(eglDisplay, configAttributes, pbufferAttributes);
}
/**
* Returns a placeholder {@link EGLSurface} to use when reading and writing to the surface is not
* required.
* Creates and focuses a placeholder {@link EGLSurface}.
*
* <p>This makes a {@link EGLContext} current when reading and writing to a surface is not
* required, configured with {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}.
*
* @param eglContext The {@link EGLContext} to make current.
* @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);
public static EGLSurface focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay)
throws GlException {
return createFocusedPlaceholderEglSurface(
eglContext, eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888);
}
/**
* Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer.
* Creates and focuses a placeholder {@link EGLSurface}.
*
* <p>This makes a {@link EGLContext} current when reading and writing to a surface is not
* required.
*
* @param eglContext The {@link EGLContext} to make current.
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
* @param configAttributes The attributes to configure EGL with. Accepts {@link
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}.
* @return A placeholder {@link EGLSurface} that has been focused to allow rendering to take
* place, or {@link EGL14#EGL_NO_SURFACE} if the current context supports rendering without a
* surface.
*/
@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
};
public static EGLSurface createFocusedPlaceholderEglSurface(
EGLContext eglContext, EGLDisplay eglDisplay, int[] configAttributes) throws GlException {
EGLSurface eglSurface =
Api17.createEglPbufferSurface(
eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_1010102, pbufferAttributes);
isSurfacelessContextExtensionSupported()
? EGL14.EGL_NO_SURFACE
: createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1, configAttributes);
focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1);
return eglSurface;
}
/**
* If there is an OpenGl error, logs the error and if {@link #glAssertionsEnabled} is true throws
* a {@link GlException}.
* Collects all OpenGL errors that occurred since this method was last called and throws a {@link
* GlException} with the combined error message.
*/
public static void checkGlError() {
int lastError = GLES20.GL_NO_ERROR;
public static void checkGlError() throws GlException {
StringBuilder errorMessageBuilder = new StringBuilder();
boolean foundError = false;
int error;
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
Log.e(TAG, "glError: " + gluErrorString(error));
lastError = error;
if (foundError) {
errorMessageBuilder.append('\n');
}
errorMessageBuilder.append("glError: ").append(gluErrorString(error));
foundError = true;
}
if (lastError != GLES20.GL_NO_ERROR) {
throwGlException("glError: " + gluErrorString(lastError));
if (foundError) {
throw new GlException(errorMessageBuilder.toString());
}
}
@ -325,31 +376,43 @@ public final class GlUtil {
* @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) {
private static void assertValidTextureSize(int width, int height) throws GlException {
// 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];
checkState(
maxTextureSize > 0,
"Create a OpenGL context first or run the GL methods on an OpenGL thread.");
if (width < 0 || height < 0) {
throwGlException("width or height is less than 0");
throw new GlException("width or height is less than 0");
}
if (width > maxTextureSize || height > maxTextureSize) {
throwGlException("width or height is greater than GL_MAX_TEXTURE_SIZE " + maxTextureSize);
throw new GlException(
"width or height is greater than GL_MAX_TEXTURE_SIZE " + maxTextureSize);
}
}
/** Fills the pixels in the current output render target with (r=0, g=0, b=0, a=0). */
public static void clearOutputFrame() throws GlException {
GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
GlUtil.checkGlError();
}
/**
* Makes the specified {@code eglSurface} the render target, using a viewport of {@code width} by
* {@code height} pixels.
*/
@RequiresApi(17)
public static void focusEglSurface(
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface eglSurface, int width, int height) {
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface eglSurface, int width, int height)
throws GlException {
Api17.focusRenderTarget(
eglDisplay, eglContext, eglSurface, /* framebuffer= */ 0, width, height);
}
@ -365,16 +428,34 @@ public final class GlUtil {
EGLSurface eglSurface,
int framebuffer,
int width,
int height) {
int height)
throws GlException {
Api17.focusRenderTarget(eglDisplay, eglContext, eglSurface, framebuffer, width, height);
}
/**
* Makes the specified {@code framebuffer} the render target, using a viewport of {@code width} by
* {@code height} pixels.
*
* <p>The caller must ensure that there is a current OpenGL context before calling this method.
*
* @param framebuffer The identifier of the framebuffer object to bind as the output render
* target.
* @param width The viewport width, in pixels.
* @param height The viewport height, in pixels.
*/
@RequiresApi(17)
public static void focusFramebufferUsingCurrentContext(int framebuffer, int width, int height)
throws GlException {
Api17.focusFramebufferUsingCurrentContext(framebuffer, width, height);
}
/**
* Deletes a GL texture.
*
* @param textureId The ID of the texture to delete.
*/
public static void deleteTexture(int textureId) {
public static void deleteTexture(int textureId) throws GlException {
GLES20.glDeleteTextures(/* n= */ 1, new int[] {textureId}, /* offset= */ 0);
checkGlError();
}
@ -385,7 +466,7 @@ public final class GlUtil {
*/
@RequiresApi(17)
public static void destroyEglContext(
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) {
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) throws GlException {
Api17.destroyEglContext(eglDisplay, eglContext);
}
@ -403,46 +484,53 @@ public final class GlUtil {
*
* @param capacity The new buffer's capacity, in floats.
*/
public static FloatBuffer createBuffer(int capacity) {
private static FloatBuffer createBuffer(int capacity) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(capacity * C.BYTES_PER_FLOAT);
return byteBuffer.order(ByteOrder.nativeOrder()).asFloatBuffer();
}
/**
* Loads a file from the assets folder.
*
* @param context The {@link Context}.
* @param assetPath The path to the file to load, from the assets folder.
* @return The content of the file to load.
* @throws IOException If the file couldn't be read.
*/
public static String loadAsset(Context context, String assetPath) throws IOException {
@Nullable InputStream inputStream = null;
try {
inputStream = context.getAssets().open(assetPath);
return Util.fromUtf8Bytes(Util.toByteArray(inputStream));
} finally {
Util.closeQuietly(inputStream);
}
}
/**
* Creates a GL_TEXTURE_EXTERNAL_OES with default configuration of GL_LINEAR filtering and
* GL_CLAMP_TO_EDGE wrapping.
*/
public static int createExternalTexture() {
public static int createExternalTexture() throws GlException {
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.
* Allocates a new RGBA texture with the specified dimensions and color component precision.
*
* @param width of the new texture in pixels
* @param height of the new texture in pixels
* @param width The width of the new texture in pixels.
* @param height The height of the new texture in pixels.
* @param useHighPrecisionColorComponents If {@code false}, uses 8-bit unsigned bytes. If {@code
* true}, use 16-bit (half-precision) floating-point.
* @throws GlException If the texture allocation fails.
* @return The texture identifier for the newly-allocated texture.
*/
public static int createTexture(int width, int height) {
public static int createTexture(int width, int height, boolean useHighPrecisionColorComponents)
throws GlException {
// TODO(227624622): Implement a pixel test that confirms 16f has less posterization.
if (useHighPrecisionColorComponents) {
checkState(Util.SDK_INT >= 18, "GLES30 extensions are not supported below API 18.");
return createTexture(width, height, GLES30.GL_RGBA16F, GLES30.GL_HALF_FLOAT);
}
return createTexture(width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE);
}
/**
* Allocates a new RGBA texture with the specified dimensions and color component precision.
*
* @param width The width of the new texture in pixels.
* @param height The height of the new texture in pixels.
* @param internalFormat The number of color components in the texture, as well as their format.
* @param type The data type of the pixel data.
* @throws GlException If the texture allocation fails.
* @return The texture identifier for the newly-allocated texture.
*/
private static int createTexture(int width, int height, int internalFormat, int type)
throws GlException {
assertValidTextureSize(width, height);
int texId = generateTexture();
bindTexture(GLES20.GL_TEXTURE_2D, texId);
@ -450,20 +538,20 @@ public final class GlUtil {
GLES20.glTexImage2D(
GLES20.GL_TEXTURE_2D,
/* level= */ 0,
GLES20.GL_RGBA,
internalFormat,
width,
height,
/* border= */ 0,
GLES20.GL_RGBA,
GLES20.GL_UNSIGNED_BYTE,
type,
byteBuffer);
checkGlError();
return texId;
}
/** Returns a new GL texture identifier. */
private static int generateTexture() {
checkEglException(
private static int generateTexture() throws GlException {
checkGlException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] texId = new int[1];
@ -476,12 +564,12 @@ public final class GlUtil {
* Binds the texture of the given type with default configuration of GL_LINEAR filtering and
* GL_CLAMP_TO_EDGE wrapping.
*
* @param texId The texture identifier.
* @param textureTarget The target to which the texture is bound, e.g. {@link
* GLES20#GL_TEXTURE_2D} for a two-dimensional texture or {@link
* GLES11Ext#GL_TEXTURE_EXTERNAL_OES} for an external texture.
* @param texId The texture identifier.
*/
public static void bindTexture(int textureTarget, int texId) {
public static void bindTexture(int textureTarget, int texId) throws GlException {
GLES20.glBindTexture(textureTarget, texId);
checkGlError();
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
@ -499,8 +587,8 @@ public final class GlUtil {
*
* @param texId The identifier of the texture to attach to the framebuffer.
*/
public static int createFboForTexture(int texId) {
checkEglException(
public static int createFboForTexture(int texId) throws GlException {
checkGlException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
int[] fboId = new int[1];
@ -514,23 +602,19 @@ public final class GlUtil {
return fboId[0];
}
/* package */ static void throwGlException(String errorMsg) {
if (glAssertionsEnabled) {
throw new GlException(errorMsg);
} else {
Log.e(TAG, errorMsg);
}
}
private static void checkEglException(boolean expression, String errorMessage) {
/**
* Throws a {@link GlException} with the given message if {@code expression} evaluates to {@code
* false}.
*/
public static void checkGlException(boolean expression, String errorMessage) throws GlException {
if (!expression) {
throwGlException(errorMessage);
throw new GlException(errorMessage);
}
}
private static void checkEglException(String errorMessage) {
private static void checkEglException(String errorMessage) throws GlException {
int error = EGL14.eglGetError();
checkEglException(error == EGL14.EGL_SUCCESS, errorMessage + ", error code: " + error);
checkGlException(error == EGL14.EGL_SUCCESS, errorMessage + ", error code: " + error);
}
@RequiresApi(17)
@ -538,24 +622,24 @@ public final class GlUtil {
private Api17() {}
@DoNotInline
public static EGLDisplay createEglDisplay() {
public static EGLDisplay createEglDisplay() throws GlException {
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
checkEglException(!eglDisplay.equals(EGL14.EGL_NO_DISPLAY), "No EGL display.");
if (!EGL14.eglInitialize(
eglDisplay,
/* unusedMajor */ new int[1],
/* majorOffset= */ 0,
/* unusedMinor */ new int[1],
/* minorOffset= */ 0)) {
throwGlException("Error in eglInitialize.");
}
checkGlException(!eglDisplay.equals(EGL14.EGL_NO_DISPLAY), "No EGL display.");
checkGlException(
EGL14.eglInitialize(
eglDisplay,
/* unusedMajor */ new int[1],
/* majorOffset= */ 0,
/* unusedMinor */ new int[1],
/* minorOffset= */ 0),
"Error in eglInitialize.");
checkGlError();
return eglDisplay;
}
@DoNotInline
public static EGLContext createEglContext(
EGLDisplay eglDisplay, int version, int[] configAttributes) {
EGLDisplay eglDisplay, int version, int[] configAttributes) throws GlException {
int[] contextAttributes = {EGL14.EGL_CONTEXT_CLIENT_VERSION, version, EGL14.EGL_NONE};
EGLContext eglContext =
EGL14.eglCreateContext(
@ -566,7 +650,7 @@ public final class GlUtil {
/* offset= */ 0);
if (eglContext == null) {
EGL14.eglTerminate(eglDisplay);
throwGlException(
throw new GlException(
"eglCreateContext() failed to create a valid context. The device may not support EGL"
+ " version "
+ version);
@ -580,7 +664,8 @@ public final class GlUtil {
EGLDisplay eglDisplay,
Object surface,
int[] configAttributes,
int[] windowSurfaceAttributes) {
int[] windowSurfaceAttributes)
throws GlException {
EGLSurface eglSurface =
EGL14.eglCreateWindowSurface(
eglDisplay,
@ -594,7 +679,7 @@ public final class GlUtil {
@DoNotInline
public static EGLSurface createEglPbufferSurface(
EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) {
EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) throws GlException {
EGLSurface eglSurface =
EGL14.eglCreatePbufferSurface(
eglDisplay,
@ -612,22 +697,32 @@ public final class GlUtil {
EGLSurface eglSurface,
int framebuffer,
int width,
int height) {
int height)
throws GlException {
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
checkEglException("Error making context current");
focusFramebufferUsingCurrentContext(framebuffer, width, height);
}
@DoNotInline
public static void focusFramebufferUsingCurrentContext(int framebuffer, int width, int height)
throws GlException {
checkGlException(
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
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);
checkGlError();
}
@DoNotInline
public static void destroyEglContext(
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) {
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) throws GlException {
if (eglDisplay == null) {
return;
}
@ -645,7 +740,8 @@ public final class GlUtil {
}
@DoNotInline
private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes) {
private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes)
throws GlException {
EGLConfig[] eglConfigs = new EGLConfig[1];
if (!EGL14.eglChooseConfig(
eglDisplay,
@ -656,7 +752,7 @@ public final class GlUtil {
/* config_size= */ 1,
/* unusedNumConfig */ new int[1],
/* num_configOffset= */ 0)) {
throwGlException("eglChooseConfig failed.");
throw new GlException("eglChooseConfig failed.");
}
return eglConfigs[0];
}

View File

@ -270,6 +270,7 @@ public final class ListenerSet<T extends @NonNull Object> {
public void release(IterationFinishedEvent<T> event) {
released = true;
if (needsIterationFinishedEvent) {
needsIterationFinishedEvent = false;
event.invoke(listener, flagsBuilder.build());
}
}

View File

@ -15,6 +15,8 @@
*/
package androidx.media3.common.util;
import static androidx.media3.common.util.Util.SDK_INT;
import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.MediaFormat;
@ -191,6 +193,53 @@ public final class MediaFormatUtil {
}
}
/**
* Creates and returns a {@code ColorInfo}, if a valid instance is described in the {@link
* MediaFormat}.
*/
@Nullable
public static ColorInfo getColorInfo(MediaFormat mediaFormat) {
if (SDK_INT < 29) {
return null;
}
int colorSpace =
mediaFormat.getInteger(MediaFormat.KEY_COLOR_STANDARD, /* defaultValue= */ Format.NO_VALUE);
int colorRange =
mediaFormat.getInteger(MediaFormat.KEY_COLOR_RANGE, /* defaultValue= */ Format.NO_VALUE);
int colorTransfer =
mediaFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER, /* defaultValue= */ Format.NO_VALUE);
@Nullable
ByteBuffer hdrStaticInfoByteBuffer = mediaFormat.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO);
@Nullable
byte[] hdrStaticInfo =
hdrStaticInfoByteBuffer != null ? getArray(hdrStaticInfoByteBuffer) : null;
// Some devices may produce invalid values from MediaFormat#getInteger.
// See b/239435670 for more information.
if (!isValidColorSpace(colorSpace)) {
colorSpace = Format.NO_VALUE;
}
if (!isValidColorRange(colorRange)) {
colorRange = Format.NO_VALUE;
}
if (!isValidColorTransfer(colorTransfer)) {
colorTransfer = Format.NO_VALUE;
}
if (colorSpace != Format.NO_VALUE
|| colorRange != Format.NO_VALUE
|| colorTransfer != Format.NO_VALUE
|| hdrStaticInfo != null) {
return new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo);
}
return null;
}
public static byte[] getArray(ByteBuffer byteBuffer) {
byte[] array = new byte[byteBuffer.remaining()];
byteBuffer.get(array);
return array;
}
// Internal methods.
private static void setBooleanAsInt(MediaFormat format, String key, int value) {
@ -253,5 +302,31 @@ public final class MediaFormatUtil {
mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, mediaFormatPcmEncoding);
}
/** Whether this is a valid {@link C.ColorSpace} instance. */
private static boolean isValidColorSpace(int colorSpace) {
// LINT.IfChange(color_space)
return colorSpace == C.COLOR_SPACE_BT601
|| colorSpace == C.COLOR_SPACE_BT709
|| colorSpace == C.COLOR_SPACE_BT2020
|| colorSpace == Format.NO_VALUE;
}
/** Whether this is a valid {@link C.ColorRange} instance. */
private static boolean isValidColorRange(int colorRange) {
// LINT.IfChange(color_range)
return colorRange == C.COLOR_RANGE_LIMITED
|| colorRange == C.COLOR_RANGE_FULL
|| colorRange == Format.NO_VALUE;
}
/** Whether this is a valid {@link C.ColorTransfer} instance. */
private static boolean isValidColorTransfer(int colorTransfer) {
// LINT.IfChange(color_transfer)
return colorTransfer == C.COLOR_TRANSFER_SDR
|| colorTransfer == C.COLOR_TRANSFER_ST2084
|| colorTransfer == C.COLOR_TRANSFER_HLG
|| colorTransfer == Format.NO_VALUE;
}
private MediaFormatUtil() {}
}

View File

@ -94,7 +94,7 @@ public final class NetworkTypeObserver {
networkType = C.NETWORK_TYPE_UNKNOWN;
IntentFilter filter = new IntentFilter();
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
context.registerReceiver(/* receiver= */ new Receiver(), filter);
Util.registerReceiverNotExported(context, new Receiver(), filter);
}
/**

View File

@ -0,0 +1,85 @@
/*
* 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.checkArgument;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
/** Immutable class for describing width and height dimensions in pixels. */
@UnstableApi
public final class Size {
/** A static instance to represent an unknown size value. */
public static final Size UNKNOWN =
new Size(/* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET);
private final int width;
private final int height;
/**
* Creates a new immutable Size instance.
*
* @param width The width of the size, in pixels, or {@link C#LENGTH_UNSET} if unknown.
* @param height The height of the size, in pixels, or {@link C#LENGTH_UNSET} if unknown.
* @throws IllegalArgumentException if an invalid {@code width} or {@code height} is specified.
*/
public Size(int width, int height) {
checkArgument(
(width == C.LENGTH_UNSET || width >= 0) && (height == C.LENGTH_UNSET || height >= 0));
this.width = width;
this.height = height;
}
/** Returns the width of the size (in pixels), or {@link C#LENGTH_UNSET} if unknown. */
public int getWidth() {
return width;
}
/** Returns the height of the size (in pixels), or {@link C#LENGTH_UNSET} if unknown. */
public int getHeight() {
return height;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj instanceof Size) {
Size other = (Size) obj;
return width == other.width && height == other.height;
}
return false;
}
@Override
public String toString() {
return width + "x" + height;
}
@Override
public int hashCode() {
// assuming most sizes are <2^16, doing a rotate will give us perfect hashing
return height ^ ((width << (Integer.SIZE / 2)) | (width >>> (Integer.SIZE / 2)));
}
}

View File

@ -21,6 +21,7 @@ import android.os.Handler;
import android.os.Looper;
import androidx.annotation.GuardedBy;
import androidx.annotation.Nullable;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
import java.util.List;
@ -136,6 +137,7 @@ import java.util.List;
@Nullable private android.os.Message message;
@Nullable private SystemHandlerWrapper handler;
@CanIgnoreReturnValue
public SystemMessage setMessage(android.os.Message message, SystemHandlerWrapper handler) {
this.message = message;
this.handler = handler;

View File

@ -66,6 +66,8 @@ public final class TimestampAdjuster {
* Next sample timestamps for calling threads in shared mode when {@link #timestampOffsetUs} has
* not yet been set.
*/
// incompatible type argument for type parameter T of ThreadLocal.
@SuppressWarnings("nullness:type.argument.type.incompatible")
private final ThreadLocal<Long> nextSampleTimestampUs;
/**
@ -73,6 +75,8 @@ public final class TimestampAdjuster {
* microseconds, or {@link #MODE_NO_OFFSET} if timestamps should not be offset, or {@link
* #MODE_SHARED} if the adjuster will be used in shared mode.
*/
// incompatible types in assignment.
@SuppressWarnings("nullness:assignment.type.incompatible")
public TimestampAdjuster(long firstSampleTimestampUs) {
nextSampleTimestampUs = new ThreadLocal<>();
reset(firstSampleTimestampUs);

View File

@ -34,9 +34,11 @@ import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.UiModeManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
@ -78,6 +80,11 @@ import androidx.media3.common.Player;
import androidx.media3.common.Player.Commands;
import com.google.common.base.Ascii;
import com.google.common.base.Charsets;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
@ -100,6 +107,8 @@ import java.util.MissingResourceException;
import java.util.NoSuchElementException;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
@ -116,8 +125,8 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
public final class Util {
/**
* Like {@link android.os.Build.VERSION#SDK_INT}, but in a place where it can be conveniently
* overridden for local testing.
* Like {@link Build.VERSION#SDK_INT}, but in a place where it can be conveniently overridden for
* local testing.
*/
@UnstableApi public static final int SDK_INT = Build.VERSION.SDK_INT;
@ -189,6 +198,54 @@ public final class Util {
return outputStream.toByteArray();
}
/**
* Registers a {@link BroadcastReceiver} that's not intended to receive broadcasts from other
* apps. This will be enforced by specifying {@link Context#RECEIVER_NOT_EXPORTED} if {@link
* #SDK_INT} is 33 or above.
*
* @param context The context on which {@link Context#registerReceiver} will be called.
* @param receiver The {@link BroadcastReceiver} to register. This value may be null.
* @param filter Selects the Intent broadcasts to be received.
* @return The first sticky intent found that matches {@code filter}, or null if there are none.
*/
@UnstableApi
@Nullable
public static Intent registerReceiverNotExported(
Context context, @Nullable BroadcastReceiver receiver, IntentFilter filter) {
if (SDK_INT < 33) {
return context.registerReceiver(receiver, filter);
} else {
return context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
}
}
/**
* Registers a {@link BroadcastReceiver} that's not intended to receive broadcasts from other
* apps. This will be enforced by specifying {@link Context#RECEIVER_NOT_EXPORTED} if {@link
* #SDK_INT} is 33 or above.
*
* @param context The context on which {@link Context#registerReceiver} will be called.
* @param receiver The {@link BroadcastReceiver} to register. This value may be null.
* @param filter Selects the Intent broadcasts to be received.
* @param handler Handler identifying the thread that will receive the Intent.
* @return The first sticky intent found that matches {@code filter}, or null if there are none.
*/
@UnstableApi
@Nullable
public static Intent registerReceiverNotExported(
Context context, BroadcastReceiver receiver, IntentFilter filter, Handler handler) {
if (SDK_INT < 33) {
return context.registerReceiver(receiver, filter, /* broadcastPermission= */ null, handler);
} else {
return context.registerReceiver(
receiver,
filter,
/* broadcastPermission= */ null,
handler,
Context.RECEIVER_NOT_EXPORTED);
}
}
/**
* Calls {@link Context#startForegroundService(Intent)} if {@link #SDK_INT} is 26 or higher, or
* {@link Context#startService(Intent)} otherwise.
@ -574,6 +631,94 @@ public final class Util {
}
}
/**
* Posts the {@link Runnable} if the calling thread differs with the {@link Looper} of the {@link
* Handler}. Otherwise, runs the {@link Runnable} directly. Also returns a {@link
* ListenableFuture} for when the {@link Runnable} has run.
*
* @param handler The handler to which the {@link Runnable} will be posted.
* @param runnable The runnable to either post or run.
* @param successValue The value to set in the {@link ListenableFuture} once the runnable
* completes.
* @param <T> The type of {@code successValue}.
* @return A {@link ListenableFuture} for when the {@link Runnable} has run.
*/
@UnstableApi
public static <T> ListenableFuture<T> postOrRunWithCompletion(
Handler handler, Runnable runnable, T successValue) {
SettableFuture<T> outputFuture = SettableFuture.create();
postOrRun(
handler,
() -> {
try {
if (outputFuture.isCancelled()) {
return;
}
runnable.run();
outputFuture.set(successValue);
} catch (Throwable e) {
outputFuture.setException(e);
}
});
return outputFuture;
}
/**
* Asynchronously transforms the result of a {@link ListenableFuture}.
*
* <p>The transformation function is called using a {@linkplain MoreExecutors#directExecutor()
* direct executor}.
*
* <p>The returned Future attempts to keep its cancellation state in sync with that of the input
* future and that of the future returned by the transform function. That is, if the returned
* Future is cancelled, it will attempt to cancel the other two, and if either of the other two is
* cancelled, the returned Future will also be cancelled. All forwarded cancellations will not
* attempt to interrupt.
*
* @param future The input {@link ListenableFuture}.
* @param transformFunction The function transforming the result of the input future.
* @param <T> The result type of the input future.
* @param <U> The result type of the transformation function.
* @return A {@link ListenableFuture} for the transformed result.
*/
@UnstableApi
public static <T, U> ListenableFuture<T> transformFutureAsync(
ListenableFuture<U> future, AsyncFunction<U, T> transformFunction) {
// This is a simplified copy of Guava's Futures.transformAsync.
SettableFuture<T> outputFuture = SettableFuture.create();
outputFuture.addListener(
() -> {
if (outputFuture.isCancelled()) {
future.cancel(/* mayInterruptIfRunning= */ false);
}
},
MoreExecutors.directExecutor());
future.addListener(
() -> {
U inputFutureResult;
try {
inputFutureResult = Futures.getDone(future);
} catch (CancellationException cancellationException) {
outputFuture.cancel(/* mayInterruptIfRunning= */ false);
return;
} catch (ExecutionException exception) {
@Nullable Throwable cause = exception.getCause();
outputFuture.setException(cause == null ? exception : cause);
return;
} catch (RuntimeException | Error error) {
outputFuture.setException(error);
return;
}
try {
outputFuture.setFuture(transformFunction.apply(inputFutureResult));
} catch (Throwable exception) {
outputFuture.setException(exception);
}
},
MoreExecutors.directExecutor());
return outputFuture;
}
/**
* Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the
* application's main thread if the current thread doesn't have a {@link Looper}.
@ -1716,6 +1861,7 @@ public final class Util {
* @return The channel configuration or {@link AudioFormat#CHANNEL_INVALID} if output is not
* possible.
*/
@SuppressLint("InlinedApi") // Inlined AudioFormat constants.
@UnstableApi
public static int getAudioTrackChannelConfig(int channelCount) {
switch (channelCount) {
@ -1734,21 +1880,9 @@ public final class Util {
case 7:
return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
case 8:
if (SDK_INT >= 23) {
return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
} else if (SDK_INT >= 21) {
// Equal to AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, which is hidden before Android M.
return AudioFormat.CHANNEL_OUT_5POINT1
| AudioFormat.CHANNEL_OUT_SIDE_LEFT
| AudioFormat.CHANNEL_OUT_SIDE_RIGHT;
} else {
// 8 ch output is not supported before Android L.
return AudioFormat.CHANNEL_INVALID;
}
return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
case 12:
return Util.SDK_INT >= 32
? AudioFormat.CHANNEL_OUT_7POINT1POINT4
: AudioFormat.CHANNEL_INVALID;
return AudioFormat.CHANNEL_OUT_7POINT1POINT4;
default:
return AudioFormat.CHANNEL_INVALID;
}
@ -2604,6 +2738,7 @@ public final class Util {
* @param newFromIndex The new from index.
*/
@UnstableApi
@SuppressWarnings("ExtendsObject") // See go/lsc-extends-object
public static <T extends Object> void moveItems(
List<T> items, int fromIndex, int toIndex, int newFromIndex) {
ArrayDeque<T> removedItems = new ArrayDeque<>();

View File

@ -25,6 +25,7 @@ import static org.junit.Assert.fail;
import android.net.Uri;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -33,7 +34,7 @@ import org.junit.runner.RunWith;
public class AdPlaybackStateTest {
private static final long[] TEST_AD_GROUP_TIMES_US = new long[] {0, 5_000_000, 10_000_000};
private static final Uri TEST_URI = Uri.EMPTY;
private static final Uri TEST_URI = Uri.parse("http://www.google.com");
private static final Object TEST_ADS_ID = new Object();
@Test
@ -52,7 +53,7 @@ public class AdPlaybackStateTest {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 2);
assertThat(state.getAdGroup(1).uris[0]).isNull();
@ -99,7 +100,7 @@ public class AdPlaybackStateTest {
.withRemovedAdGroupCount(1)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 2)
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 1)
.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI)
.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI)
.withSkippedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0);
state =
@ -139,8 +140,8 @@ public class AdPlaybackStateTest {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
assertThat(state.getAdGroup(1).getFirstAdIndexToPlay()).isEqualTo(0);
}
@ -150,8 +151,8 @@ public class AdPlaybackStateTest {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
@ -165,8 +166,8 @@ public class AdPlaybackStateTest {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withSkippedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
@ -180,8 +181,8 @@ public class AdPlaybackStateTest {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1);
@ -194,7 +195,7 @@ public class AdPlaybackStateTest {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1);
@ -207,9 +208,9 @@ public class AdPlaybackStateTest {
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
state = state.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true);
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
@ -222,9 +223,9 @@ public class AdPlaybackStateTest {
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
state = state.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true);
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1);
@ -247,6 +248,51 @@ public class AdPlaybackStateTest {
}
}
@Test
public void withAvailableAd() {
int adGroupIndex = 2;
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US)
.withRemovedAdGroupCount(2)
.withAdCount(adGroupIndex, 3)
.withAdDurationsUs(adGroupIndex, /* adDurationsUs...*/ 10, 20, 30)
.withIsServerSideInserted(adGroupIndex, true);
state = state.withAvailableAd(adGroupIndex, /* adIndexInAdGroup= */ 2);
assertThat(state.getAdGroup(adGroupIndex).states)
.asList()
.containsExactly(AD_STATE_UNAVAILABLE, AD_STATE_UNAVAILABLE, AD_STATE_AVAILABLE)
.inOrder();
assertThat(state.getAdGroup(adGroupIndex).uris)
.asList()
.containsExactly(null, null, Uri.EMPTY)
.inOrder();
state =
state
.withAvailableAd(adGroupIndex, /* adIndexInAdGroup= */ 0)
.withAvailableAd(adGroupIndex, /* adIndexInAdGroup= */ 1)
.withAvailableAd(adGroupIndex, /* adIndexInAdGroup= */ 2);
assertThat(state.getAdGroup(adGroupIndex).states)
.asList()
.containsExactly(AD_STATE_AVAILABLE, AD_STATE_AVAILABLE, AD_STATE_AVAILABLE)
.inOrder();
}
@Test
public void withAvailableAd_forClientSideAdGroup_throwsRuntimeException() {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US)
.withRemovedAdGroupCount(2)
.withAdCount(/* adGroupIndex= */ 2, 3)
.withAdDurationsUs(/* adGroupIndex= */ 2, /* adDurationsUs...*/ 10, 20, 30);
Assert.assertThrows(
IllegalStateException.class, () -> state.withAvailableAd(/* adGroupIndex= */ 2, 1));
}
@Test
public void skipAllWithoutAdCount() {
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US);
@ -265,6 +311,51 @@ public class AdPlaybackStateTest {
assertThat(state.getAdGroup(1).count).isEqualTo(C.LENGTH_UNSET);
}
@Test
public void withOriginalAdCount() {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 5_000_000)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2);
state = state.withOriginalAdCount(/* adGroupIndex= */ 0, /* originalAdCount= */ 3);
assertThat(state.getAdGroup(0).count).isEqualTo(2);
assertThat(state.getAdGroup(0).originalCount).isEqualTo(3);
}
@Test
public void withOriginalAdCount_unsetValue_defaultsToIndexUnset() {
AdPlaybackState state =
new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 5_000_000)
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2);
assertThat(state.getAdGroup(0).count).isEqualTo(2);
assertThat(state.getAdGroup(0).originalCount).isEqualTo(C.INDEX_UNSET);
}
@Test
public void withLastAdGroupRemoved() {
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 5_000_000);
state =
state
.withAdCount(/* adGroupIndex= */ 0, 3)
.withAdDurationsUs(/* adGroupIndex= */ 0, 10_000L, 20_000L, 30_000L)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1)
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
state = state.withLastAdRemoved(0);
assertThat(state.getAdGroup(/* adGroupIndex= */ 0).states).asList().hasSize(2);
assertThat(state.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
.asList()
.containsExactly(10_000L, 20_000L)
.inOrder();
assertThat(state.getAdGroup(/* adGroupIndex= */ 0).states)
.asList()
.containsExactly(AD_STATE_PLAYED, AD_STATE_PLAYED);
}
@Test
public void withResetAdGroup_resetsAdsInFinalStates() {
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US);
@ -272,10 +363,10 @@ public class AdPlaybackStateTest {
state =
state.withAdDurationsUs(
/* adGroupIndex= */ 1, /* adDurationsUs...= */ 1_000L, 2_000L, 3_000L, 4_000L, 5_000L);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, Uri.EMPTY);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, Uri.EMPTY);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3, Uri.EMPTY);
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4, Uri.EMPTY);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3, TEST_URI);
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4, TEST_URI);
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2);
state = state.withSkippedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3);
state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4);
@ -303,7 +394,7 @@ public class AdPlaybackStateTest {
.inOrder();
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).uris)
.asList()
.containsExactly(null, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY)
.containsExactly(null, TEST_URI, TEST_URI, TEST_URI, TEST_URI)
.inOrder();
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).durationsUs)
.asList()
@ -317,12 +408,12 @@ public class AdPlaybackStateTest {
.withRemovedAdGroupCount(1)
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0)
.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI)
.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI)
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 2)
.withSkippedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0)
.withPlayedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 1)
.withAdUri(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, TEST_URI)
.withAdUri(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 1, TEST_URI)
.withAvailableAdUri(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, TEST_URI)
.withAvailableAdUri(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 1, TEST_URI)
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 4444)
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 3333)
.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true)

View File

@ -20,6 +20,7 @@ import static androidx.media3.common.MimeTypes.VIDEO_MP4;
import static androidx.media3.common.MimeTypes.VIDEO_WEBM;
import static com.google.common.truth.Truth.assertThat;
import android.os.Bundle;
import androidx.media3.test.utils.FakeMetadataEntry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.ArrayList;
@ -46,6 +47,16 @@ public final class FormatTest {
assertThat(formatFromBundle).isEqualTo(formatToBundle);
}
@Test
public void roundTripViaBundle_excludeMetadata_hasMetadataExcluded() {
Format format = createTestFormat();
Bundle bundleWithMetadataExcluded = format.toBundle(/* excludeMetadata= */ true);
Format formatWithMetadataExcluded = Format.CREATOR.fromBundle(bundleWithMetadataExcluded);
assertThat(formatWithMetadataExcluded).isEqualTo(format.buildUpon().setMetadata(null).build());
}
private static Format createTestFormat() {
byte[] initData1 = new byte[] {1, 2, 3};
byte[] initData2 = new byte[] {4, 5, 6};
@ -60,7 +71,6 @@ public final class FormatTest {
DrmInitData drmInitData = new DrmInitData(drmData1, drmData2);
byte[] projectionData = new byte[] {1, 2, 3};
Metadata metadata = new Metadata(new FakeMetadataEntry("id1"), new FakeMetadataEntry("id2"));
ColorInfo colorInfo =

View File

@ -30,7 +30,10 @@ public class MetadataTest {
@Test
public void parcelable() {
Metadata metadataToParcel =
new Metadata(new FakeMetadataEntry("id1"), new FakeMetadataEntry("id2"));
new Metadata(
/* presentationTimeUs= */ 1_230_000,
new FakeMetadataEntry("id1"),
new FakeMetadataEntry("id2"));
Parcel parcel = Parcel.obtain();
metadataToParcel.writeToParcel(parcel, 0);

View File

@ -0,0 +1,406 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.os.Looper;
import androidx.media3.common.Player.Commands;
import androidx.media3.common.Player.Listener;
import androidx.media3.common.SimpleBasePlayer.State;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Unit test for {@link SimpleBasePlayer}. */
@RunWith(AndroidJUnit4.class)
public class SimpleBasePlayerTest {
@Test
public void allPlayerInterfaceMethods_declaredFinal() throws Exception {
for (Method method : Player.class.getDeclaredMethods()) {
assertThat(
SimpleBasePlayer.class
.getMethod(method.getName(), method.getParameterTypes())
.getModifiers()
& Modifier.FINAL)
.isNotEqualTo(0);
}
}
@Test
public void stateBuildUpon_build_isEqual() {
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlayWhenReady(
/* playWhenReady= */ true,
/* playWhenReadyChangeReason= */ Player
.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
.build();
State newState = state.buildUpon().build();
assertThat(newState).isEqualTo(state);
assertThat(newState.hashCode()).isEqualTo(state.hashCode());
}
@Test
public void stateBuilderSetAvailableCommands_setsAvailableCommands() {
Commands commands =
new Commands.Builder()
.addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE)
.build();
State state = new State.Builder().setAvailableCommands(commands).build();
assertThat(state.availableCommands).isEqualTo(commands);
}
@Test
public void stateBuilderSetPlayWhenReady_setsStatePlayWhenReadyAndReason() {
State state =
new State.Builder()
.setPlayWhenReady(
/* playWhenReady= */ true,
/* playWhenReadyChangeReason= */ Player
.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
.build();
assertThat(state.playWhenReady).isTrue();
assertThat(state.playWhenReadyChangeReason)
.isEqualTo(Player.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS);
}
@Test
public void getterMethods_noOtherMethodCalls_returnCurrentState() {
Commands commands =
new Commands.Builder()
.addAll(Player.COMMAND_GET_DEVICE_VOLUME, Player.COMMAND_GET_TIMELINE)
.build();
State state =
new State.Builder()
.setAvailableCommands(commands)
.setPlayWhenReady(
/* playWhenReady= */ true,
/* playWhenReadyChangeReason= */ Player
.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
.build();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return state;
}
};
assertThat(player.getApplicationLooper()).isEqualTo(Looper.myLooper());
assertThat(player.getAvailableCommands()).isEqualTo(commands);
assertThat(player.getPlayWhenReady()).isTrue();
}
@SuppressWarnings("deprecation") // Verifying deprecated listener call.
@Test
public void invalidateState_updatesStateAndInformsListeners() {
State state1 =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlayWhenReady(
/* playWhenReady= */ true,
/* playWhenReadyChangeReason= */ Player
.PLAY_WHEN_READY_CHANGE_REASON_AUDIO_FOCUS_LOSS)
.build();
Commands commands = new Commands.Builder().add(Player.COMMAND_GET_TEXT).build();
State state2 =
new State.Builder()
.setAvailableCommands(commands)
.setPlayWhenReady(
/* playWhenReady= */ false,
/* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
.build();
AtomicBoolean returnState2 = new AtomicBoolean();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return returnState2.get() ? state2 : state1;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
// Verify state1 is used.
assertThat(player.getPlayWhenReady()).isTrue();
returnState2.set(true);
player.invalidateState();
// Verify updated state.
assertThat(player.getAvailableCommands()).isEqualTo(commands);
assertThat(player.getPlayWhenReady()).isFalse();
// Verify listener calls.
verify(listener).onAvailableCommandsChanged(commands);
verify(listener)
.onPlayWhenReadyChanged(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
verify(listener)
.onPlayerStateChanged(/* playWhenReady= */ false, /* playbackState= */ Player.STATE_IDLE);
verifyNoMoreInteractions(listener);
}
@Test
public void invalidateState_duringAsyncMethodHandling_isIgnored() {
State state1 =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlayWhenReady(
/* playWhenReady= */ true,
/* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build();
State state2 =
state1
.buildUpon()
.setPlayWhenReady(
/* playWhenReady= */ false,
/* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
.build();
AtomicReference<State> currentState = new AtomicReference<>(state1);
SettableFuture<?> asyncFuture = SettableFuture.create();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return currentState.get();
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
return asyncFuture;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
// Verify state1 is used trigger async method.
assertThat(player.getPlayWhenReady()).isTrue();
player.setPlayWhenReady(true);
currentState.set(state2);
player.invalidateState();
// Verify placeholder state is used (and not state2).
assertThat(player.getPlayWhenReady()).isTrue();
// Finish async operation and verify no listeners are informed.
currentState.set(state1);
asyncFuture.set(null);
assertThat(player.getPlayWhenReady()).isTrue();
verifyNoMoreInteractions(listener);
}
@Test
public void overlappingAsyncMethodHandling_onlyUpdatesStateAfterAllDone() {
State state1 =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlayWhenReady(
/* playWhenReady= */ true,
/* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build();
State state2 =
state1
.buildUpon()
.setPlayWhenReady(
/* playWhenReady= */ false,
/* playWhenReadyChangeReason= */ Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
.build();
AtomicReference<State> currentState = new AtomicReference<>(state1);
ArrayList<SettableFuture<?>> asyncFutures = new ArrayList<>();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return currentState.get();
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
SettableFuture<?> future = SettableFuture.create();
asyncFutures.add(future);
return future;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
// Verify state1 is used.
assertThat(player.getPlayWhenReady()).isTrue();
// Trigger multiple parallel async calls and set state2 (which should never be used).
player.setPlayWhenReady(true);
currentState.set(state2);
assertThat(player.getPlayWhenReady()).isTrue();
player.setPlayWhenReady(true);
assertThat(player.getPlayWhenReady()).isTrue();
player.setPlayWhenReady(true);
assertThat(player.getPlayWhenReady()).isTrue();
// Finish async operation and verify state2 is not used while operations are pending.
asyncFutures.get(1).set(null);
assertThat(player.getPlayWhenReady()).isTrue();
asyncFutures.get(2).set(null);
assertThat(player.getPlayWhenReady()).isTrue();
verifyNoMoreInteractions(listener);
// Finish last async operation and verify updated state and listener calls.
asyncFutures.get(0).set(null);
assertThat(player.getPlayWhenReady()).isFalse();
verify(listener)
.onPlayWhenReadyChanged(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
}
@SuppressWarnings("deprecation") // Verifying deprecated listener call.
@Test
public void setPlayWhenReady_immediateHandling_updatesStateAndInformsListeners() {
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlayWhenReady(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build();
State updatedState =
state
.buildUpon()
.setPlayWhenReady(
/* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
.build();
AtomicBoolean stateUpdated = new AtomicBoolean();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return stateUpdated.get() ? updatedState : state;
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
stateUpdated.set(true);
return Futures.immediateVoidFuture();
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
// Intentionally use parameter that doesn't match final result.
player.setPlayWhenReady(false);
assertThat(player.getPlayWhenReady()).isTrue();
verify(listener)
.onPlayWhenReadyChanged(
/* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
verify(listener)
.onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE);
verifyNoMoreInteractions(listener);
}
@SuppressWarnings("deprecation") // Verifying deprecated listener call.
@Test
public void setPlayWhenReady_asyncHandling_usesPlaceholderStateAndInformsListeners() {
State state =
new State.Builder()
.setAvailableCommands(new Commands.Builder().addAllCommands().build())
.setPlayWhenReady(
/* playWhenReady= */ false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST)
.build();
State updatedState =
state
.buildUpon()
.setPlayWhenReady(
/* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE)
.build();
SettableFuture<?> future = SettableFuture.create();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return future.isDone() ? updatedState : state;
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
return future;
}
};
Listener listener = mock(Listener.class);
player.addListener(listener);
player.setPlayWhenReady(true);
// Verify placeholder state and listener calls.
assertThat(player.getPlayWhenReady()).isTrue();
verify(listener)
.onPlayWhenReadyChanged(
/* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST);
verify(listener)
.onPlayerStateChanged(/* playWhenReady= */ true, /* playbackState= */ Player.STATE_IDLE);
verifyNoMoreInteractions(listener);
future.set(null);
// Verify actual state update.
assertThat(player.getPlayWhenReady()).isTrue();
verify(listener)
.onPlayWhenReadyChanged(
/* playWhenReady= */ true, Player.PLAY_WHEN_READY_CHANGE_REASON_REMOTE);
verifyNoMoreInteractions(listener);
}
@Test
public void setPlayWhenReady_withoutAvailableCommand_isNotForwarded() {
State state =
new State.Builder()
.setAvailableCommands(
new Commands.Builder().addAllCommands().remove(Player.COMMAND_PLAY_PAUSE).build())
.build();
AtomicBoolean callForwarded = new AtomicBoolean();
SimpleBasePlayer player =
new SimpleBasePlayer(Looper.myLooper()) {
@Override
protected State getState() {
return state;
}
@Override
protected ListenableFuture<?> handleSetPlayWhenReady(boolean playWhenReady) {
callForwarded.set(true);
return Futures.immediateVoidFuture();
}
};
player.setPlayWhenReady(true);
assertThat(callForwarded.get()).isFalse();
}
}

View File

@ -37,7 +37,7 @@ public class CueGroupTest {
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);
CueGroup cueGroup = new CueGroup(cues, /* presentationTimeUs= */ 1_230_000);
Parcel parcel = Parcel.obtain();
try {

View File

@ -49,7 +49,7 @@ public class ListenerSetTest {
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
verifyNoMoreInteractions(listener);
}
@ -67,6 +67,7 @@ public class ListenerSetTest {
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
listenerSet.flushEvents();
ShadowLooper.idleMainLooper();
InOrder inOrder = Mockito.inOrder(listener1, listener2);
inOrder.verify(listener1).callback1();
@ -75,6 +76,8 @@ public class ListenerSetTest {
inOrder.verify(listener2).callback2();
inOrder.verify(listener1).callback1();
inOrder.verify(listener2).callback1();
inOrder.verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1, EVENT_ID_2));
inOrder.verify(listener2).iterationFinished(createFlagSet(EVENT_ID_1, EVENT_ID_2));
inOrder.verifyNoMoreInteractions();
}
@ -99,6 +102,7 @@ public class ListenerSetTest {
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
listenerSet.flushEvents();
ShadowLooper.idleMainLooper();
InOrder inOrder = Mockito.inOrder(listener1, listener2);
inOrder.verify(listener1).callback1();
@ -107,6 +111,8 @@ public class ListenerSetTest {
inOrder.verify(listener2).callback2();
inOrder.verify(listener1).callback3();
inOrder.verify(listener2).callback3();
inOrder.verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1, EVENT_ID_2, EVENT_ID_3));
inOrder.verify(listener2).iterationFinished(createFlagSet(EVENT_ID_1, EVENT_ID_2, EVENT_ID_3));
inOrder.verifyNoMoreInteractions();
}
@ -131,7 +137,7 @@ public class ListenerSetTest {
// Iteration with single flush.
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
listenerSet.flushEvents();
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
// Iteration with multiple flushes.
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
@ -139,11 +145,11 @@ public class ListenerSetTest {
listenerSet.flushEvents();
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
listenerSet.flushEvents();
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
// Iteration with recursive call.
listenerSet.sendEvent(EVENT_ID_3, TestListener::callback3);
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
InOrder inOrder = Mockito.inOrder(listener1, listener2);
inOrder.verify(listener1).callback2();
@ -192,7 +198,7 @@ public class ListenerSetTest {
listenerSet.add(listener3);
listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2);
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
inOrder.verify(listener1).callback2();
@ -216,7 +222,7 @@ public class ListenerSetTest {
listenerSet.queueEvent(/* eventFlag= */ C.INDEX_UNSET, TestListener::callback1);
listenerSet.flushEvents();
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
// Asserts that negative event flag (INDEX_UNSET) can be used without throwing.
}
@ -242,7 +248,7 @@ public class ListenerSetTest {
// listener2 was added.
listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1);
listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2);
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
InOrder inOrder = Mockito.inOrder(listener1, listener2);
inOrder.verify(listener1).callback1();
@ -267,7 +273,7 @@ public class ListenerSetTest {
listenerSet.add(listener2);
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
listenerSet.flushEvents();
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
InOrder inOrder = Mockito.inOrder(listener1, listener2);
inOrder.verify(listener1).callback1();
@ -299,7 +305,7 @@ public class ListenerSetTest {
listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1);
listenerSet.remove(listener1);
listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2);
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
verify(listener1).callback1();
verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1));
@ -320,7 +326,7 @@ public class ListenerSetTest {
listenerSet.remove(listener1);
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
listenerSet.flushEvents();
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
verify(listener2, times(2)).callback1();
verify(listener2).iterationFinished(createFlagSet(EVENT_ID_1));
@ -347,13 +353,43 @@ public class ListenerSetTest {
// Listener2 shouldn't even get this event as it's released before the event can be invoked.
listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1);
listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2);
ShadowLooper.runMainLooperToNextTask();
ShadowLooper.idleMainLooper();
verify(listener1).callback1();
verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1));
verifyNoMoreInteractions(listener1, listener2);
}
@Test
public void remove_withRecursionDuringRelease_callsAllPendingEventsAndIterationFinished() {
ListenerSet<TestListener> listenerSet =
new ListenerSet<>(Looper.myLooper(), Clock.DEFAULT, TestListener::iterationFinished);
TestListener listener2 = mock(TestListener.class);
// Listener1 removes Listener2 from within the callback triggered by release().
TestListener listener1 =
spy(
new TestListener() {
@Override
public void iterationFinished(FlagSet flags) {
listenerSet.remove(listener2);
}
});
listenerSet.add(listener1);
listenerSet.add(listener2);
// Listener2 should still get the event and iterationFinished callback because it was triggered
// before the release and the listener removal.
listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1);
listenerSet.release();
ShadowLooper.idleMainLooper();
verify(listener1).callback1();
verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1));
verify(listener2).callback1();
verify(listener2).iterationFinished(createFlagSet(EVENT_ID_1));
verifyNoMoreInteractions(listener1, listener2);
}
@Test
public void release_preventsRegisteringNewListeners() {
ListenerSet<TestListener> listenerSet =

View File

@ -28,10 +28,16 @@ import static androidx.media3.common.util.Util.parseXsDuration;
import static androidx.media3.common.util.Util.unescapeFileName;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.StrikethroughSpan;
@ -41,6 +47,9 @@ import androidx.media3.common.C;
import androidx.media3.test.utils.TestUtil;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.io.ByteStreams;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import java.io.ByteArrayInputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@ -49,16 +58,21 @@ import java.util.Arrays;
import java.util.Formatter;
import java.util.NoSuchElementException;
import java.util.Random;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.zip.Deflater;
import java.util.zip.GZIPInputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
/** Unit tests for {@link Util}. */
@RunWith(AndroidJUnit4.class)
public class UtilTest {
private static final int TIMEOUT_MS = 10000;
@Test
public void addWithOverflowDefault_withoutOverFlow_returnsSum() {
long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0);
@ -1238,6 +1252,246 @@ public class UtilTest {
.isEqualTo(0);
}
@Test
public void postOrRun_withMatchingThread_runsInline() {
Runnable mockRunnable = mock(Runnable.class);
Util.postOrRun(new Handler(Looper.myLooper()), mockRunnable);
verify(mockRunnable).run();
}
@Test
public void postOrRun_fromDifferentThread_posts() throws Exception {
Runnable mockRunnable = mock(Runnable.class);
HandlerThread handlerThread = new HandlerThread("TestThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
ConditionVariable postedCondition = new ConditionVariable();
handler.post(
() -> {
Util.postOrRun(new Handler(Looper.getMainLooper()), mockRunnable);
postedCondition.open();
});
postedCondition.block(TIMEOUT_MS);
handlerThread.quit();
verify(mockRunnable, never()).run();
ShadowLooper.idleMainLooper();
verify(mockRunnable).run();
}
@Test
public void postOrRunWithCompletion_withMatchingThread_runsInline() throws Exception {
Runnable mockRunnable = mock(Runnable.class);
Object expectedResult = new Object();
ListenableFuture<Object> future =
Util.postOrRunWithCompletion(new Handler(Looper.myLooper()), mockRunnable, expectedResult);
assertThat(future.isDone()).isTrue();
assertThat(future.get()).isEqualTo(expectedResult);
verify(mockRunnable).run();
}
@Test
public void postOrRunWithCompletion_withException_hasException() throws Exception {
Object expectedResult = new Object();
ListenableFuture<Object> future =
Util.postOrRunWithCompletion(
new Handler(Looper.myLooper()),
() -> {
throw new IllegalStateException();
},
expectedResult);
assertThat(future.isDone()).isTrue();
ExecutionException executionException = assertThrows(ExecutionException.class, future::get);
assertThat(executionException).hasCauseThat().isInstanceOf(IllegalStateException.class);
}
@Test
public void postOrRunWithCompletion_fromDifferentThread_posts() throws Exception {
Runnable mockRunnable = mock(Runnable.class);
Object expectedResult = new Object();
HandlerThread handlerThread = new HandlerThread("TestThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
ConditionVariable postedCondition = new ConditionVariable();
AtomicReference<ListenableFuture<Object>> futureReference = new AtomicReference<>();
handler.post(
() -> {
futureReference.set(
Util.postOrRunWithCompletion(
new Handler(Looper.getMainLooper()), mockRunnable, expectedResult));
postedCondition.open();
});
postedCondition.block(TIMEOUT_MS);
handlerThread.quit();
ListenableFuture<Object> future = futureReference.get();
verify(mockRunnable, never()).run();
assertThat(future.isDone()).isFalse();
ShadowLooper.idleMainLooper();
assertThat(future.isDone()).isTrue();
assertThat(future.get()).isEqualTo(expectedResult);
verify(mockRunnable).run();
}
@Test
public void postOrRunWithCompletion_withCancel_isNeverRun() throws Exception {
Runnable mockRunnable = mock(Runnable.class);
Object expectedResult = new Object();
HandlerThread handlerThread = new HandlerThread("TestThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
ConditionVariable postedCondition = new ConditionVariable();
AtomicReference<ListenableFuture<Object>> futureReference = new AtomicReference<>();
handler.post(
() -> {
futureReference.set(
Util.postOrRunWithCompletion(
new Handler(Looper.getMainLooper()), mockRunnable, expectedResult));
postedCondition.open();
});
postedCondition.block(TIMEOUT_MS);
handlerThread.quit();
ListenableFuture<Object> future = futureReference.get();
future.cancel(/* mayInterruptIfRunning= */ false);
ShadowLooper.idleMainLooper();
verify(mockRunnable, never()).run();
}
@Test
public void transformFutureAsync_withCancelledInput_isCancelled() {
SettableFuture<Object> inputFuture = SettableFuture.create();
inputFuture.cancel(/* mayInterruptIfRunning= */ false);
ListenableFuture<Object> outputFuture =
Util.transformFutureAsync(inputFuture, input -> Futures.immediateFuture(new Object()));
assertThat(outputFuture.isCancelled()).isTrue();
}
@Test
public void transformFutureAsync_withExceptionInput_hasException() {
SettableFuture<Object> inputFuture = SettableFuture.create();
Exception expectedException = new Exception();
inputFuture.setException(expectedException);
ListenableFuture<Object> outputFuture =
Util.transformFutureAsync(inputFuture, input -> Futures.immediateFuture(new Object()));
assertThat(outputFuture.isDone()).isTrue();
ExecutionException executionException =
assertThrows(ExecutionException.class, outputFuture::get);
assertThat(executionException).hasCauseThat().isEqualTo(expectedException);
}
@Test
public void transformFutureAsync_withCancelledTransform_isCancelled() {
SettableFuture<Object> inputFuture = SettableFuture.create();
SettableFuture<Object> transformFuture = SettableFuture.create();
ListenableFuture<Object> outputFuture =
Util.transformFutureAsync(inputFuture, input -> transformFuture);
assertThat(outputFuture.isDone()).isFalse();
inputFuture.set(new Object());
assertThat(outputFuture.isDone()).isFalse();
transformFuture.cancel(/* mayInterruptIfRunning= */ false);
assertThat(outputFuture.isCancelled()).isTrue();
}
@Test
public void transformFutureAsync_withExceptionInTransformFunction_hasException() {
SettableFuture<Object> inputFuture = SettableFuture.create();
Exception expectedException = new Exception();
ListenableFuture<Object> outputFuture =
Util.transformFutureAsync(
inputFuture,
input -> {
throw expectedException;
});
assertThat(outputFuture.isDone()).isFalse();
inputFuture.set(new Object());
assertThat(outputFuture.isDone()).isTrue();
ExecutionException executionException =
assertThrows(ExecutionException.class, outputFuture::get);
assertThat(executionException).hasCauseThat().isEqualTo(expectedException);
}
@Test
public void transformFutureAsync_withExceptionDuringTransform_hasException() {
SettableFuture<Object> inputFuture = SettableFuture.create();
Exception expectedException = new Exception();
ListenableFuture<Object> outputFuture =
Util.transformFutureAsync(
inputFuture, input -> Futures.immediateFailedFuture(expectedException));
assertThat(outputFuture.isDone()).isFalse();
inputFuture.set(new Object());
assertThat(outputFuture.isDone()).isTrue();
ExecutionException executionException =
assertThrows(ExecutionException.class, outputFuture::get);
assertThat(executionException).hasCauseThat().isEqualTo(expectedException);
}
@Test
public void transformFutureAsync_cancelDuringInput_inputIsCancelled() {
SettableFuture<Object> inputFuture = SettableFuture.create();
ListenableFuture<Object> outputFuture =
Util.transformFutureAsync(inputFuture, input -> Futures.immediateFuture(new Object()));
assertThat(outputFuture.isDone()).isFalse();
outputFuture.cancel(/* mayInterruptIfRunning= */ true);
assertThat(inputFuture.isCancelled()).isTrue();
}
@Test
public void transformFutureAsync_cancelDuringTransform_transformIsCancelled() {
SettableFuture<Object> inputFuture = SettableFuture.create();
SettableFuture<Object> transformFuture = SettableFuture.create();
ListenableFuture<Object> outputFuture =
Util.transformFutureAsync(inputFuture, input -> transformFuture);
assertThat(outputFuture.isDone()).isFalse();
inputFuture.set(new Object());
assertThat(outputFuture.isDone()).isFalse();
outputFuture.cancel(/* mayInterruptIfRunning= */ true);
assertThat(transformFuture.isCancelled()).isTrue();
}
@Test
public void transformFutureAsync_withSuccessfulTransform_returnsTransformedResult()
throws Exception {
SettableFuture<Object> inputFuture = SettableFuture.create();
SettableFuture<Object> transformFuture = SettableFuture.create();
Object expectedOutput = new Object();
ListenableFuture<Object> outputFuture =
Util.transformFutureAsync(inputFuture, input -> transformFuture);
assertThat(outputFuture.isDone()).isFalse();
inputFuture.set(new Object());
assertThat(outputFuture.isDone()).isFalse();
transformFuture.set(expectedOutput);
assertThat(outputFuture.isDone()).isTrue();
assertThat(outputFuture.get()).isEqualTo(expectedOutput);
}
private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);

View File

@ -39,6 +39,7 @@ dependencies {
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion

View File

@ -22,6 +22,7 @@
<uses-sdk/>
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
android:usesCleartextTraffic="true"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">

View File

@ -24,6 +24,7 @@ import androidx.media3.common.C;
import androidx.media3.common.MediaLibraryInfo;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -89,6 +90,7 @@ public final class DataSpec {
* @param uriString The {@link DataSpec#uri}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setUri(String uriString) {
this.uri = Uri.parse(uriString);
return this;
@ -100,6 +102,7 @@ public final class DataSpec {
* @param uri The {@link DataSpec#uri}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setUri(Uri uri) {
this.uri = uri;
return this;
@ -111,6 +114,7 @@ public final class DataSpec {
* @param uriPositionOffset The {@link DataSpec#uriPositionOffset}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setUriPositionOffset(long uriPositionOffset) {
this.uriPositionOffset = uriPositionOffset;
return this;
@ -122,6 +126,7 @@ public final class DataSpec {
* @param httpMethod The {@link DataSpec#httpMethod}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setHttpMethod(@HttpMethod int httpMethod) {
this.httpMethod = httpMethod;
return this;
@ -133,6 +138,7 @@ public final class DataSpec {
* @param httpBody The {@link DataSpec#httpBody}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setHttpBody(@Nullable byte[] httpBody) {
this.httpBody = httpBody;
return this;
@ -148,6 +154,7 @@ public final class DataSpec {
* @param httpRequestHeaders The {@link DataSpec#httpRequestHeaders}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setHttpRequestHeaders(Map<String, String> httpRequestHeaders) {
this.httpRequestHeaders = httpRequestHeaders;
return this;
@ -159,6 +166,7 @@ public final class DataSpec {
* @param position The {@link DataSpec#position}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setPosition(long position) {
this.position = position;
return this;
@ -170,6 +178,7 @@ public final class DataSpec {
* @param length The {@link DataSpec#length}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setLength(long length) {
this.length = length;
return this;
@ -181,6 +190,7 @@ public final class DataSpec {
* @param key The {@link DataSpec#key}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setKey(@Nullable String key) {
this.key = key;
return this;
@ -192,6 +202,7 @@ public final class DataSpec {
* @param flags The {@link DataSpec#flags}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setFlags(@Flags int flags) {
this.flags = flags;
return this;
@ -203,6 +214,7 @@ public final class DataSpec {
* @param customData The {@link DataSpec#customData}.
* @return The builder.
*/
@CanIgnoreReturnValue
public Builder setCustomData(@Nullable Object customData) {
this.customData = customData;
return this;

View File

@ -23,6 +23,7 @@ import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@ -33,25 +34,27 @@ import java.util.Map;
* A {@link DataSource} that supports multiple URI schemes. The supported schemes are:
*
* <ul>
* <li>file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just
* /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is
* a local file URI).
* <li>asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4).
* <li>rawresource: For fetching data from a raw resource in the application's apk (e.g.
* rawresource:///resourceId, where rawResourceId is the integer identifier of the raw
* resource).
* <li>android.resource: For fetching data in the application's apk (e.g.
* android.resource:///resourceId or android.resource://resourceType/resourceName). See {@link
* RawResourceDataSource} for more information about the URI form.
* <li>content: For fetching data from a content URI (e.g. content://authority/path/123).
* <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
* explicit dependency on ExoPlayer's RTMP extension.
* <li>data: For parsing data inlined in the URI as defined in RFC 2397.
* <li>udp: For fetching data over UDP (e.g. udp://something.com/media).
* <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4),
* if constructed using {@link #DefaultDataSource(Context, String, boolean)}, or any other
* schemes supported by a base data source if constructed using {@link
* #DefaultDataSource(Context, DataSource)}.
* <li>{@code file}: For fetching data from a local file (e.g. {@code
* file:///path/to/media/media.mp4}, or just {@code /path/to/media/media.mp4} because the
* implementation assumes that a URI without a scheme is a local file URI).
* <li>{@code asset}: For fetching data from an asset in the application's APK (e.g. {@code
* asset:///media.mp4}).
* <li>{@code rawresource}: For fetching data from a raw resource in the application's APK (e.g.
* {@code rawresource:///resourceId}, where {@code rawResourceId} is the integer identifier of
* the raw resource).
* <li>{@code android.resource}: For fetching data in the application's APK (e.g. {@code
* android.resource:///resourceId} or {@code android.resource://resourceType/resourceName}).
* See {@link RawResourceDataSource} for more information about the URI form.
* <li>{@code content}: For fetching data from a content URI (e.g. {@code
* content://authority/path/123}).
* <li>{@code rtmp}: For fetching data over RTMP. Only supported if the project using ExoPlayer
* has an explicit dependency on ExoPlayer's RTMP extension.
* <li>{@code data}: For parsing data inlined in the URI as defined in RFC 2397.
* <li>{@code udp}: For fetching data over UDP (e.g. {@code udp://something.com/media}).
* <li>{@code http(s)}: For fetching data over HTTP and HTTPS (e.g. {@code
* https://www.something.com/media.mp4}), if constructed using {@link
* #DefaultDataSource(Context, String, boolean)}, or any other schemes supported by a base
* data source if constructed using {@link #DefaultDataSource(Context, DataSource)}.
* </ul>
*/
public final class DefaultDataSource implements DataSource {
@ -97,6 +100,7 @@ public final class DefaultDataSource implements DataSource {
* @param transferListener The listener that will be used.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setTransferListener(@Nullable TransferListener transferListener) {
this.transferListener = transferListener;

View File

@ -34,6 +34,7 @@ import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.net.HttpHeaders;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
@ -82,6 +83,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
}
@CanIgnoreReturnValue
@UnstableApi
@Override
public final Factory setDefaultRequestProperties(Map<String, String> defaultRequestProperties) {
@ -99,6 +101,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* agent of the underlying platform.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setUserAgent(@Nullable String userAgent) {
this.userAgent = userAgent;
@ -113,6 +116,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* @param connectTimeoutMs The connect timeout, in milliseconds, that will be used.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setConnectTimeoutMs(int connectTimeoutMs) {
this.connectTimeoutMs = connectTimeoutMs;
@ -127,6 +131,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* @param readTimeoutMs The connect timeout, in milliseconds, that will be used.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setReadTimeoutMs(int readTimeoutMs) {
this.readTimeoutMs = readTimeoutMs;
@ -141,6 +146,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* @param allowCrossProtocolRedirects Whether to allow cross protocol redirects.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setAllowCrossProtocolRedirects(boolean allowCrossProtocolRedirects) {
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
@ -158,6 +164,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* predicate that was previously set.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
this.contentTypePredicate = contentTypePredicate;
@ -174,6 +181,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* @param transferListener The listener that will be used.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setTransferListener(@Nullable TransferListener transferListener) {
this.transferListener = transferListener;
@ -184,6 +192,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
* Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a
* POST request.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) {
this.keepPostFor302Redirects = keepPostFor302Redirects;

View File

@ -30,6 +30,7 @@ import androidx.media3.common.PlaybackException;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
@ -82,6 +83,7 @@ public final class FileDataSource extends BaseDataSource {
* @param listener The {@link TransferListener}.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setListener(@Nullable TransferListener listener) {
this.listener = listener;
return this;

View File

@ -25,6 +25,7 @@ import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.common.base.Ascii;
import com.google.common.base.Predicate;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.annotation.Documented;
@ -157,6 +158,7 @@ public interface HttpDataSource extends DataSource {
return createDataSourceInternal(defaultRequestProperties);
}
@CanIgnoreReturnValue
@Override
public final Factory setDefaultRequestProperties(Map<String, String> defaultRequestProperties) {
this.defaultRequestProperties.clearAndSet(defaultRequestProperties);

View File

@ -28,6 +28,7 @@ import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSink;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.cache.Cache.CacheException;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
@ -65,6 +66,7 @@ public final class CacheDataSink implements DataSink {
* @param cache The cache to which data will be written.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setCache(Cache cache) {
this.cache = cache;
return this;
@ -83,6 +85,7 @@ public final class CacheDataSink implements DataSink {
* fragmentation.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setFragmentSize(long fragmentSize) {
this.fragmentSize = fragmentSize;
return this;
@ -97,6 +100,7 @@ public final class CacheDataSink implements DataSink {
* @param bufferSize The buffer size in bytes.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setBufferSize(int bufferSize) {
this.bufferSize = bufferSize;
return this;

View File

@ -42,6 +42,7 @@ import androidx.media3.datasource.PriorityDataSource;
import androidx.media3.datasource.TeeDataSource;
import androidx.media3.datasource.TransferListener;
import androidx.media3.datasource.cache.Cache.CacheException;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.annotation.Documented;
@ -88,6 +89,7 @@ public final class CacheDataSource implements DataSource {
* @param cache The cache that will be used.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setCache(Cache cache) {
this.cache = cache;
return this;
@ -111,6 +113,7 @@ public final class CacheDataSource implements DataSource {
* @param cacheReadDataSourceFactory The {@link DataSource.Factory} for reading from the cache.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setCacheReadDataSourceFactory(DataSource.Factory cacheReadDataSourceFactory) {
this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;
return this;
@ -126,6 +129,7 @@ public final class CacheDataSource implements DataSource {
* DataSinks} for writing data to the cache, or {@code null} to disable writing.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setCacheWriteDataSinkFactory(
@Nullable DataSink.Factory cacheWriteDataSinkFactory) {
this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory;
@ -141,6 +145,7 @@ public final class CacheDataSource implements DataSource {
* @param cacheKeyFactory The {@link CacheKeyFactory}.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setCacheKeyFactory(CacheKeyFactory cacheKeyFactory) {
this.cacheKeyFactory = cacheKeyFactory;
return this;
@ -162,6 +167,7 @@ public final class CacheDataSource implements DataSource {
* cache, or {@code null} to cause failure in the case of a cache miss.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setUpstreamDataSourceFactory(
@Nullable DataSource.Factory upstreamDataSourceFactory) {
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
@ -186,6 +192,7 @@ public final class CacheDataSource implements DataSource {
* @param upstreamPriorityTaskManager The upstream {@link PriorityTaskManager}.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setUpstreamPriorityTaskManager(
@Nullable PriorityTaskManager upstreamPriorityTaskManager) {
this.upstreamPriorityTaskManager = upstreamPriorityTaskManager;
@ -210,6 +217,7 @@ public final class CacheDataSource implements DataSource {
* @param upstreamPriority The priority to use when requesting data from upstream.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setUpstreamPriority(int upstreamPriority) {
this.upstreamPriority = upstreamPriority;
return this;
@ -223,6 +231,7 @@ public final class CacheDataSource implements DataSource {
* @param flags The {@link CacheDataSource.Flags}.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setFlags(@CacheDataSource.Flags int flags) {
this.flags = flags;
return this;
@ -236,6 +245,7 @@ public final class CacheDataSource implements DataSource {
* @param eventListener The {@link EventListener}.
* @return This factory.
*/
@CanIgnoreReturnValue
public Factory setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener;
return this;

View File

@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -81,6 +82,7 @@ public class ContentMetadataMutations {
* @param value The value to be set.
* @return This instance, for convenience.
*/
@CanIgnoreReturnValue
public ContentMetadataMutations set(String name, String value) {
return checkAndSet(name, value);
}
@ -92,6 +94,7 @@ public class ContentMetadataMutations {
* @param value The value to be set.
* @return This instance, for convenience.
*/
@CanIgnoreReturnValue
public ContentMetadataMutations set(String name, long value) {
return checkAndSet(name, value);
}
@ -103,6 +106,7 @@ public class ContentMetadataMutations {
* @param value The value to be set.
* @return This instance, for convenience.
*/
@CanIgnoreReturnValue
public ContentMetadataMutations set(String name, byte[] value) {
return checkAndSet(name, Arrays.copyOf(value, value.length));
}
@ -113,6 +117,7 @@ public class ContentMetadataMutations {
* @param name The name of the metadata value.
* @return This instance, for convenience.
*/
@CanIgnoreReturnValue
public ContentMetadataMutations remove(String name) {
removedValues.add(name);
editedValues.remove(name);
@ -137,6 +142,7 @@ public class ContentMetadataMutations {
return Collections.unmodifiableMap(hashMap);
}
@CanIgnoreReturnValue
private ContentMetadataMutations checkAndSet(String name, Object value) {
editedValues.put(Assertions.checkNotNull(name), Assertions.checkNotNull(value));
removedValues.remove(name);

View File

@ -24,11 +24,12 @@ dependencies {
implementation project(modulePrefix + 'lib-common')
implementation project(modulePrefix + 'lib-datasource')
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
// Instrumentation tests assume that an app-packaged version of cronet is
// available.

View File

@ -23,6 +23,7 @@
<uses-sdk/>
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="false"
android:usesCleartextTraffic="true"
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"/>

View File

@ -42,6 +42,7 @@ import com.google.common.base.Ascii;
import com.google.common.base.Predicate;
import com.google.common.net.HttpHeaders;
import com.google.common.primitives.Longs;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.SocketTimeoutException;
@ -142,6 +143,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
}
@CanIgnoreReturnValue
@UnstableApi
@Override
public final Factory setDefaultRequestProperties(Map<String, String> defaultRequestProperties) {
@ -162,6 +164,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* agent of the underlying {@link CronetEngine}.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setUserAgent(@Nullable String userAgent) {
this.userAgent = userAgent;
@ -181,6 +184,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* UrlRequest.Builder#REQUEST_PRIORITY_*} constants.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setRequestPriority(int requestPriority) {
this.requestPriority = requestPriority;
@ -195,6 +199,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* @param connectTimeoutMs The connect timeout, in milliseconds, that will be used.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setConnectionTimeoutMs(int connectTimeoutMs) {
this.connectTimeoutMs = connectTimeoutMs;
@ -212,6 +217,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setResetTimeoutOnRedirects(boolean resetTimeoutOnRedirects) {
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
@ -228,6 +234,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* to the redirect url in the "Cookie" header.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setHandleSetCookieRequests(boolean handleSetCookieRequests) {
this.handleSetCookieRequests = handleSetCookieRequests;
@ -242,6 +249,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* @param readTimeoutMs The connect timeout, in milliseconds, that will be used.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setReadTimeoutMs(int readTimeoutMs) {
this.readTimeoutMs = readTimeoutMs;
@ -261,6 +269,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* predicate that was previously set.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
this.contentTypePredicate = contentTypePredicate;
@ -274,6 +283,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a
* POST request.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) {
this.keepPostFor302Redirects = keepPostFor302Redirects;
@ -293,6 +303,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* @param transferListener The listener that will be used.
* @return This factory.
*/
@CanIgnoreReturnValue
@UnstableApi
public Factory setTransferListener(@Nullable TransferListener transferListener) {
this.transferListener = transferListener;
@ -313,6 +324,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
* @deprecated Do not use {@link CronetDataSource} or its factory in cases where a suitable
* {@link CronetEngine} is not available. Use the fallback factory directly in such cases.
*/
@CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Factory setFallbackFactory(@Nullable HttpDataSource.Factory fallbackFactory) {

View File

@ -145,7 +145,7 @@ public final class CronetDataSourceTest {
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
// This value can be anything since the DataSpec is unset.
testResponseHeader.put("Content-Length", Long.toString(TEST_CONTENT_LENGTH));
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
testUrlResponseInfo = createUrlResponseInfo(/* statusCode= */ 200);
}
@After

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