diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index f970c1ad124..38ed1ba728c 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -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
diff --git a/.gitignore b/.gitignore
index 3ab16a94fdf..ce7cddb44b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 34953c71f8a..18b494425ad 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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.
diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 70e9bcdf25e..1eb87be7155 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -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)
diff --git a/common_library_config.gradle b/common_library_config.gradle
index 9d14a1f601e..a831ddea0b8 100644
--- a/common_library_config.gradle
+++ b/common_library_config.gradle
@@ -22,6 +22,9 @@ android {
targetSdkVersion project.ext.targetSdkVersion
consumerProguardFiles 'proguard-rules.txt'
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
+ aarMetadata {
+ minCompileSdk = project.ext.compileSdkVersion
+ }
}
compileOptions {
diff --git a/constants.gradle b/constants.gradle
index b72c48b65c3..ac9b80f1d6c 100644
--- a/constants.gradle
+++ b/constants.gradle
@@ -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'
diff --git a/core_settings.gradle b/core_settings.gradle
index baca4217532..b331d11b4da 100644
--- a/core_settings.gradle
+++ b/core_settings.gradle
@@ -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')
diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml
index 2e07deae943..b6a9b56eac3 100644
--- a/demos/cast/src/main/AndroidManifest.xml
+++ b/demos/cast/src/main/AndroidManifest.xml
@@ -22,8 +22,13 @@
-
+
diff --git a/demos/gl/build.gradle b/demos/gl/build.gradle
index 9aad43098b4..ff8333a2543 100644
--- a/demos/gl/build.gradle
+++ b/demos/gl/build.gradle
@@ -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
}
diff --git a/demos/gl/src/main/AndroidManifest.xml b/demos/gl/src/main/AndroidManifest.xml
index d4d79a70536..d44f8ea343c 100644
--- a/demos/gl/src/main/AndroidManifest.xml
+++ b/demos/gl/src/main/AndroidManifest.xml
@@ -22,6 +22,7 @@
diff --git a/demos/gl/src/main/java/androidx/media3/demo/gl/BitmapOverlayVideoProcessor.java b/demos/gl/src/main/java/androidx/media3/demo/gl/BitmapOverlayVideoProcessor.java
index 735abf4cfa0..793e1fd4939 100644
--- a/demos/gl/src/main/java/androidx/media3/demo/gl/BitmapOverlayVideoProcessor.java
+++ b/demos/gl/src/main/java/androidx/media3/demo/gl/BitmapOverlayVideoProcessor.java
@@ -29,6 +29,7 @@
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 @@
/* 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 @@ public void initialize() {
/* 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 @@ public void draw(int frameTexture, long frameTimestampUs, float[] transformMatri
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 @@ public void draw(int frameTexture, long frameTimestampUs, float[] transformMatri
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);
+ }
}
}
}
diff --git a/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java b/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java
index 4923b4a3901..c9e7ccd11cd 100644
--- a/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java
+++ b/demos/gl/src/main/java/androidx/media3/demo/gl/MainActivity.java
@@ -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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
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;
}
diff --git a/demos/gl/src/main/java/androidx/media3/demo/gl/VideoProcessingGLSurfaceView.java b/demos/gl/src/main/java/androidx/media3/demo/gl/VideoProcessingGLSurfaceView.java
index 9c95122d4b6..c7c0aba58a9 100644
--- a/demos/gl/src/main/java/androidx/media3/demo/gl/VideoProcessingGLSurfaceView.java
+++ b/demos/gl/src/main/java/androidx/media3/demo/gl/VideoProcessingGLSurfaceView.java
@@ -28,6 +28,7 @@
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 interface VideoProcessor {
}
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 VideoRenderer(VideoProcessor videoProcessor) {
@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 -> {
diff --git a/demos/main/build.gradle b/demos/main/build.gradle
index 402afb13603..2d7d92a0fb2 100644
--- a/demos/main/build.gradle
+++ b/demos/main/build.gradle
@@ -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
}
diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json
index 09688fa73ac..ac7b5ce7492 100644
--- a/demos/main/src/main/assets/media.exolist.json
+++ b/demos/main/src/main/assets/media.exolist.json
@@ -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"
},
{
diff --git a/demos/session/src/main/AndroidManifest.xml b/demos/session/src/main/AndroidManifest.xml
index e94b527e83d..90557fc6c7f 100644
--- a/demos/session/src/main/AndroidManifest.xml
+++ b/demos/session/src/main/AndroidManifest.xml
@@ -14,6 +14,7 @@
limitations under the License.
-->
@@ -21,10 +22,12 @@
+ android:theme="@style/Theme.Media3Demo"
+ tools:replace="android:name">
@@ -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)
)
}
diff --git a/demos/session/src/main/java/androidx/media3/demo/session/MediaItemTree.kt b/demos/session/src/main/java/androidx/media3/demo/session/MediaItemTree.kt
index 01eebe92bac..d1ece8ba12d 100644
--- a/demos/session/src/main/java/androidx/media3/demo/session/MediaItemTree.kt
+++ b/demos/session/src/main/java/androidx/media3/demo/session/MediaItemTree.kt
@@ -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 = 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 = 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)
diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlayableFolderActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlayableFolderActivity.kt
index 125b0e0b8a1..9c6f3e5b4a6 100644
--- a/demos/session/src/main/java/androidx/media3/demo/session/PlayableFolderActivity.kt
+++ b/demos/session/src/main/java/androidx/media3/demo/session/PlayableFolderActivity.kt
@@ -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
@@ -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() {
diff --git a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt
index 18eb9c05e2f..99305948578 100644
--- a/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt
+++ b/demos/session/src/main/java/androidx/media3/demo/session/PlayerActivity.kt
@@ -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))
+ }
}
)
}
diff --git a/demos/transformer/build.gradle b/demos/transformer/build.gradle
index a745fcea1f5..16eb6c61013 100644
--- a/demos/transformer/build.gradle
+++ b/demos/transformer/build.gradle
@@ -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'
diff --git a/demos/transformer/src/main/AndroidManifest.xml b/demos/transformer/src/main/AndroidManifest.xml
index ff7e08db743..d12622c1a88 100644
--- a/demos/transformer/src/main/AndroidManifest.xml
+++ b/demos/transformer/src/main/AndroidManifest.xml
@@ -29,6 +29,7 @@
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat"
android:taskAffinity=""
+ android:requestLegacyExternalStorage="true"
tools:targetApi="29">
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 @@ public void initialize(Context context, int inputTexId, int inputWidth, int inpu
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e);
}
- bitmapTexId = GlUtil.createTexture(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT);
- GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
-
- glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
+ 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);
+ } 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);
- glProgram.setFloatUniform("uScaleX", bitmapScaleX);
- glProgram.setFloatUniform("uScaleY", bitmapScaleY);
}
@Override
- public Size getOutputSize() {
- return checkStateNotNull(outputSize);
+ public Pair 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 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 @@ public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
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);
}
}
diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java
index 026f3960917..eac602c80e6 100644
--- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java
+++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java
@@ -18,9 +18,11 @@
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.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 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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
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 audioMimeAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
@@ -200,14 +246,38 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
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 @@ protected void onResume() {
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 @@ protected void onNewIntent(Intent intent) {
"rotateSpinner",
"trimCheckBox",
"enableFallbackCheckBox",
+ "enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox",
+ "forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox",
"demoEffectsSelections"
})
@@ -275,32 +348,85 @@ private void startTransformation(View view) {
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 @@ private void selectTrimBounds(View view, boolean isChecked) {
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 radiusRange = radiusRangeSlider.getValues();
- trimStartMs = 1000 * radiusRange.get(0).longValue();
- trimEndMs = 1000 * radiusRange.get(1).longValue();
+ List 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 @@ private void selectDemoEffect(DialogInterface dialog, int which, boolean isCheck
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
+ "enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox",
+ "forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox",
"selectDemoEffectsButton"
})
@@ -397,7 +612,9 @@ private void onRemoveAudio(View view) {
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
+ "enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox",
+ "forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox",
"selectDemoEffectsButton"
})
@@ -416,7 +633,9 @@ private void onRemoveVideo(View view) {
"resolutionHeightSpinner",
"scaleSpinner",
"rotateSpinner",
+ "enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox",
+ "forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox",
"selectDemoEffectsButton"
})
@@ -426,8 +645,10 @@ private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoE
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 @@ private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoE
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);
}
diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/MatrixTransformationFactory.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/MatrixTransformationFactory.java
index 2aded6a470e..0f31a589c97 100644
--- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/MatrixTransformationFactory.java
+++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/MatrixTransformationFactory.java
@@ -18,8 +18,8 @@
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
diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/PeriodicVignetteProcessor.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/PeriodicVignetteProcessor.java
index 74c1a312946..079843f22cd 100644
--- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/PeriodicVignetteProcessor.java
+++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/PeriodicVignetteProcessor.java
@@ -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 @@
*
*
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 @@ public void initialize(Context context, int inputTexId, int inputWidth, int inpu
}
@Override
- public Size getOutputSize() {
- return checkStateNotNull(outputSize);
+ public Pair 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 @@ public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
// 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);
}
}
}
diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java
index 594459e3150..1ace6bfb654 100644
--- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java
+++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java
@@ -17,10 +17,13 @@
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.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 @@
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 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
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 @@ protected void onStart() {
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 @@ protected void onStop() {
// 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 @@ protected void onStop() {
}
@RequiresNonNull({
- "playerView",
+ "inputCardView",
+ "inputPlayerView",
+ "outputPlayerView",
+ "displayInputButton",
"debugTextView",
"informationTextView",
"progressIndicator",
@@ -161,7 +190,8 @@ private void startTransformation() {
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 @@ private MediaItem createMediaItem(@Nullable Bundle bundle, Uri uri) {
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 @@ private Transformer createTransformer(@Nullable Bundle bundle, String filePath)
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,29 +281,78 @@ private Transformer createTransformer(@Nullable Bundle bundle, String filePath)
.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)));
-
- ImmutableList.Builder 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(
- () -> {
+ new DefaultEncoderFactory.Builder(this.getApplicationContext())
+ .setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))
+ .build());
+
+ transformerBuilder.setVideoEffects(createVideoEffectsListFromBundle(bundle));
+
+ if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) {
+ transformerBuilder.setDebugViewProvider(new DemoDebugViewProvider());
+ }
+ }
+ return transformerBuilder
+ .addListener(
+ new Transformer.Listener() {
+ @Override
+ public void onTransformationCompleted(
+ MediaItem mediaItem, TransformationResult transformationResult) {
+ TransformerActivity.this.onTransformationCompleted(filePath, mediaItem);
+ }
+
+ @Override
+ public void onTransformationError(
+ MediaItem mediaItem, TransformationException exception) {
+ TransformerActivity.this.onTransformationError(exception);
+ }
+ })
+ .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 createVideoEffectsListFromBundle(Bundle bundle) {
+ @Nullable
+ boolean[] selectedEffects =
+ bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS);
+ if (selectedEffects == null) {
+ return ImmutableList.of();
+ }
+ ImmutableList.Builder 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 (SingleFrameGlTextureProcessor)
+ return (GlTextureProcessor)
constructor.newInstance(
+ context,
+ useHdr,
/* graphName= */ "edge_detector_mediapipe_graph.binarypb",
+ /* isSingleFrameGraph= */ true,
/* inputStreamName= */ "input_video",
/* outputStreamName= */ "output_video");
} catch (Exception e) {
@@ -288,14 +360,77 @@ private Transformer createTransformer(@Nullable Bundle bundle, String filePath)
throw new RuntimeException("Failed to load MediaPipe processor", e);
}
});
- } catch (Exception e) {
- showToast(R.string.no_media_pipe_error);
+ } 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);
}
}
- if (selectedEffects[2]) {
- effects.add(
- () ->
+ }
+ 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(
@@ -303,36 +438,17 @@ private Transformer createTransformer(@Nullable Bundle bundle, String filePath)
/* maxInnerRadius= */ bundle.getFloat(
ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS),
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
- }
- if (selectedEffects[3]) {
- effects.add(MatrixTransformationFactory.createSpin3dEffect());
- }
- if (selectedEffects[4]) {
- effects.add(BitmapOverlayProcessor::new);
- }
- if (selectedEffects[5]) {
- effects.add(MatrixTransformationFactory.createZoomInTransition());
- }
- transformerBuilder.setVideoFrameEffects(effects.build());
- }
}
- return transformerBuilder
- .addListener(
- new Transformer.Listener() {
- @Override
- public void onTransformationCompleted(
- MediaItem mediaItem, TransformationResult transformationResult) {
- TransformerActivity.this.onTransformationCompleted(filePath);
- }
-
- @Override
- public void onTransformationError(
- MediaItem mediaItem, TransformationException exception) {
- TransformerActivity.this.onTransformationError(exception);
- }
- })
- .setDebugViewProvider(new DemoDebugViewProvider())
- .build();
+ 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({
@@ -346,44 +462,66 @@ private void onTransformationError(TransformationException exception) {
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 @@ private void releasePlayer() {
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 @@ private void showToast(@StringRes int messageResource) {
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 void surfaceDestroyed(SurfaceHolder surfaceHolder) {
Thread.currentThread().interrupt();
return null;
}
+ this.surfaceView = surfaceView;
return surfaceView;
}
}
diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml
index 2879d6a637a..2a481bea698 100644
--- a/demos/transformer/src/main/res/layout/configuration_activity.xml
+++ b/demos/transformer/src/main/res/layout/configuration_activity.xml
@@ -34,16 +34,26 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
+
+
+ app:layout_constraintTop_toBottomOf="@+id/configuration_text_view" />
+ app:layout_constraintTop_toBottomOf="@+id/select_preset_file_button" />
+ android:layout_weight="1">
+ android:layout_gravity="end"
+ android:id="@+id/remove_audio_checkbox"/>
-
+
+ android:layout_gravity="end" />
+ android:layout_weight="1">
+ android:layout_gravity="end" />
+ android:layout_weight="1">
+ android:layout_gravity="end" />
+ android:layout_weight="1">
+ android:layout_weight="1">
+
+
+
+
+ android:layout_gravity="end" />
+ android:layout_weight="1">
+ android:layout_gravity="end" />
+
+
+
+
diff --git a/demos/transformer/src/main/res/layout/contrast_options.xml b/demos/transformer/src/main/res/layout/contrast_options.xml
new file mode 100644
index 00000000000..4ccfdc0db5f
--- /dev/null
+++ b/demos/transformer/src/main/res/layout/contrast_options.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/transformer/src/main/res/layout/hsl_adjustment_options.xml b/demos/transformer/src/main/res/layout/hsl_adjustment_options.xml
new file mode 100644
index 00000000000..7b847ad8711
--- /dev/null
+++ b/demos/transformer/src/main/res/layout/hsl_adjustment_options.xml
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/transformer/src/main/res/layout/rgb_adjustment_options.xml b/demos/transformer/src/main/res/layout/rgb_adjustment_options.xml
new file mode 100644
index 00000000000..c87e8fad179
--- /dev/null
+++ b/demos/transformer/src/main/res/layout/rgb_adjustment_options.xml
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/transformer/src/main/res/layout/transformer_activity.xml b/demos/transformer/src/main/res/layout/transformer_activity.xml
index 324ed010a22..05e2e4892f9 100644
--- a/demos/transformer/src/main/res/layout/transformer_activity.xml
+++ b/demos/transformer/src/main/res/layout/transformer_activity.xml
@@ -29,42 +29,113 @@
app:cardElevation="2dp"
android:gravity="center_vertical" >
-
+ android:layout_height="wrap_content">
+
+
+
+
+
+
+
-
+
+
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dp"
+ android:padding="8dp"
+ android:text="@string/input_video" />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+ android:layout_height="wrap_content" />
@@ -96,5 +167,9 @@
+
+
+
+
diff --git a/demos/transformer/src/main/res/values/strings.xml b/demos/transformer/src/main/res/values/strings.xml
index 50ac310080e..6f26949a591 100644
--- a/demos/transformer/src/main/res/values/strings.xml
+++ b/demos/transformer/src/main/res/values/strings.xml
@@ -17,7 +17,8 @@
Transformer DemoConfiguration
- Choose file
+ Choose preset file
+ Choose local fileRemove audioRemove videoFlatten for slow motion
@@ -27,9 +28,11 @@
Scale videoRotate video (degrees)Enable fallback
+ Enable debug previewTrimRequest SDR tone-mapping (API 31+)
- [Experimental] HDR editing
+ [Experimental] Force interpret HDR video as SDR (API 29+)
+ [Experimental] HDR editing (API 31+)Add demo effectsPeriodic vignette optionsFailed to load MediaPipe processor. Check the README for instructions.
@@ -40,8 +43,27 @@
Transformation started %d seconds ago.Transformation completed in %d seconds.Transformation error
+ Bounds in seconds
+
+ Grayscale
+ Inverted
+ Sepia
+
+ Contrast value
+ Scale RGB Channels individually
+ Scale red
+ Scale green
+ Scale blueCenter XCenter YRadius range
- Bounds in seconds
+ HSL adjustment options
+ Hue adjustment
+ Saturation adjustment
+ Lightness adjustment
+ Input video:
+ Output video:
+ Permission Denied
+ Hide input video
+ Show input video
diff --git a/demos/transformer/src/withMediaPipe/java/androidx/media3/demo/transformer/MediaPipeProcessor.java b/demos/transformer/src/withMediaPipe/java/androidx/media3/demo/transformer/MediaPipeProcessor.java
index 8860a2ccc91..84c06d3e494 100644
--- a/demos/transformer/src/withMediaPipe/java/androidx/media3/demo/transformer/MediaPipeProcessor.java
+++ b/demos/transformer/src/withMediaPipe/java/androidx/media3/demo/transformer/MediaPipeProcessor.java
@@ -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 @@ protected void loadLibrary(String name) {
}
}
- private static final String COPY_VERTEX_SHADER_NAME = "vertex_shader_copy_es2.glsl";
- private static final String COPY_FRAGMENT_SHADER_NAME = "shaders/fragment_shader_copy_es2.glsl";
-
- private final String graphName;
- private final String inputStreamName;
- private final String outputStreamName;
- private final ConditionVariable frameProcessorConditionVariable;
+ private final FrameProcessor frameProcessor;
+ private final ConcurrentHashMap outputFrames;
+ private final boolean isSingleFrameGraph;
+ @Nullable private final ExecutorService singleThreadExecutorService;
+ private final Queue> futures;
- 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.
*
+ *
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();
- });
- frameProcessor.setAsynchronousErrorListener(
- error -> {
- frameProcessorPendingError = error;
- 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 Size getOutputSize() {
- return new Size(inputWidth, inputHeight);
+ public void setErrorListener(ErrorListener errorListener) {
+ this.errorListener = errorListener;
+ frameProcessor.setAsynchronousErrorListener(
+ error -> errorListener.onFrameProcessingError(new FrameProcessingException(error)));
}
@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);
+ if (isSingleFrameGraph) {
+ boolean acceptedFrame = maybeQueueInputFrameSynchronous(appTextureFrame, inputTexture);
+ checkState(
+ acceptedFrame,
+ "queueInputFrame must only be called when a new input frame can be accepted");
+ return;
+ }
- // Wait for output to be passed to the consumer.
+ // 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);
+ }
+
+ private boolean maybeQueueInputFrameSynchronous(
+ AppTextureFrame appTextureFrame, TextureInfo inputTexture) {
+ acceptedFrame = false;
+ frameProcessor.onNewFrame(appTextureFrame);
try {
- frameProcessorConditionVariable.block();
+ appTextureFrame.waitUntilReleasedWithGpuSync();
} catch (InterruptedException e) {
- // Propagate the interrupted flag so the next blocking operation will throw.
- // TODO(b/230469581): The next processor that runs will not have valid input due to returning
- // early here. This could be fixed by checking for interruption in the outer loop that runs
- // through the texture processors.
Thread.currentThread().interrupt();
- return;
+ 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();
+ }));
+ }
- if (frameProcessorPendingError != null) {
- throw new FrameProcessingException(frameProcessorPendingError);
+ @Override
+ public void releaseOutputFrame(TextureInfo outputTexture) {
+ checkStateNotNull(outputFrames.get(outputTexture)).release();
+ if (isSingleFrameGraph) {
+ inputListener.onReadyToAcceptInputFrame();
+ }
+ }
+
+ @Override
+ public void release() {
+ if (isSingleFrameGraph) {
+ frameProcessor.close();
+ return;
}
- // Copy from MediaPipe's output texture to the current output.
+ Queue> futures = checkStateNotNull(this.futures);
+ while (!futures.isEmpty()) {
+ futures.remove().cancel(/* mayInterruptIfRunning= */ false);
+ }
+ ExecutorService singleThreadExecutorService =
+ checkStateNotNull(this.singleThreadExecutorService);
+ singleThreadExecutorService.shutdown();
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();
+ 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 void release() {
- checkStateNotNull(frameProcessor).close();
+ 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));
+ }
+ }
}
}
diff --git a/javadoc_combined.gradle b/javadoc_combined.gradle
index 0b37687b410..5d6848fc470 100644
--- a/javadoc_combined.gradle
+++ b/javadoc_combined.gradle
@@ -21,7 +21,7 @@ class CombinedJavadocPlugin implements Plugin {
// 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 {
"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 {
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")
}
diff --git a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java
index acdd0fe8c72..57b81b3fa5c 100644
--- a/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java
+++ b/libraries/cast/src/main/java/androidx/media3/cast/CastPlayer.java
@@ -47,6 +47,7 @@
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 @@
@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 VideoSize getVideoSize() {
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}. */
diff --git a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java
index 0462878afa5..5837d8492d9 100644
--- a/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java
+++ b/libraries/cast/src/test/java/androidx/media3/cast/CastPlayerTest.java
@@ -62,6 +62,7 @@
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 void setMediaItems_equalMetadata_doesNotNotifyOnMediaMetadataChanged() {
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++) {
diff --git a/libraries/common/src/main/java/androidx/media3/common/AdOverlayInfo.java b/libraries/common/src/main/java/androidx/media3/common/AdOverlayInfo.java
index 40d367aee97..e411912a9b0 100644
--- a/libraries/common/src/main/java/androidx/media3/common/AdOverlayInfo.java
+++ b/libraries/common/src/main/java/androidx/media3/common/AdOverlayInfo.java
@@ -25,6 +25,7 @@
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 Builder(View view, @Purpose int purpose) {
*
* @return This builder, for convenience.
*/
+ @CanIgnoreReturnValue
public Builder setDetailedReason(@Nullable String detailedReason) {
this.detailedReason = detailedReason;
return this;
diff --git a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java
index ba3caf1d563..8efa8f218d8 100644
--- a/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java
+++ b/libraries/common/src/main/java/androidx/media3/common/AdPlaybackState.java
@@ -64,6 +64,13 @@ public static final class AdGroup 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 AdGroup(long timeUs) {
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 AdGroup(long timeUs) {
private AdGroup(
long timeUs,
int count,
+ int originalCount,
@AdState int[] states,
@NullableType Uri[] uris,
long[] durationsUs,
@@ -106,6 +115,7 @@ private AdGroup(
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 int getFirstAdIndexToPlay() {
* 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.
+ *
+ *
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 boolean equals(@Nullable Object o) {
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 boolean equals(@Nullable Object o) {
@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 int hashCode() {
@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 AdGroup withAdCount(int count) {
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 AdGroup withAdUri(Uri uri, @IntRange(from = 0) int index) {
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 AdGroup withAdUri(Uri uri, @IntRange(from = 0) int index) {
@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 AdGroup withAdState(@AdState int state, @IntRange(from = 0) int index) {
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 AdGroup withAdDurationsUs(long[] durationsUs) {
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 AdGroup withAllAdsSkipped() {
return new AdGroup(
timeUs,
/* count= */ 0,
+ originalCount,
/* states= */ new int[0],
/* uris= */ new Uri[0],
/* durationsUs= */ new long[0],
@@ -302,7 +400,14 @@ public AdGroup withAllAdsSkipped() {
}
}
return new AdGroup(
- timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
+ timeUs,
+ count,
+ originalCount,
+ states,
+ uris,
+ durationsUs,
+ contentResumeOffsetUs,
+ isServerSideInserted);
}
/**
@@ -324,7 +429,14 @@ public AdGroup withAllAdsReset() {
}
}
return new AdGroup(
- timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
+ timeUs,
+ count,
+ originalCount,
+ states,
+ uris,
+ durationsUs,
+ contentResumeOffsetUs,
+ isServerSideInserted);
}
@CheckResult
@@ -358,6 +470,7 @@ private static long[] copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int
FIELD_DURATIONS_US,
FIELD_CONTENT_RESUME_OFFSET_US,
FIELD_IS_SERVER_SIDE_INSERTED,
+ FIELD_ORIGINAL_COUNT
})
private @interface FieldNumber {}
@@ -368,6 +481,7 @@ private static long[] copyDurationsUsWithSpaceForAdCount(long[] durationsUs, int
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 Bundle toBundle() {
Bundle bundle = new Bundle();
bundle.putLong(keyForField(FIELD_TIME_US), timeUs);
bundle.putInt(keyForField(FIELD_COUNT), count);
+ bundle.putInt(keyForField(FIELD_ORIGINAL_COUNT), originalCount);
bundle.putParcelableArrayList(
keyForField(FIELD_URIS), new ArrayList<@NullableType Uri>(Arrays.asList(uris)));
bundle.putIntArray(keyForField(FIELD_STATES), states);
@@ -393,6 +508,8 @@ public Bundle toBundle() {
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 @@ private static AdGroup fromBundle(Bundle bundle) {
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 @@ private static String keyForField(@AdGroup.FieldNumber int field) {
*/
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 AdPlaybackState withAdCount(
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}.
+ *
+ *
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 AdPlaybackState withPlayedAd(
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 AdPlaybackState withSkippedAd(
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 AdPlaybackState withContentResumeOffsetUs(
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 static AdPlaybackState fromAdPlaybackState(Object adsId, AdPlaybackState
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),
diff --git a/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java b/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java
index b962fdc89ab..11a8ef15bd0 100644
--- a/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java
+++ b/libraries/common/src/main/java/androidx/media3/common/AudioAttributes.java
@@ -24,6 +24,7 @@
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 Builder() {
}
/** 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;
diff --git a/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java
index fc5a96c96ed..74b144baa38 100644
--- a/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java
+++ b/libraries/common/src/main/java/androidx/media3/common/BasePlayer.java
@@ -21,7 +21,8 @@
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 @@ protected BasePlayer() {
@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 final void setMediaItems(List mediaItems) {
@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 final void seekToPreviousWindow() {
@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 final void seekToNextWindow() {
@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 final long getContentDuration() {
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
}
+ /**
+ * Repeat the current media item.
+ *
+ *
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;
diff --git a/libraries/common/src/main/java/androidx/media3/common/C.java b/libraries/common/src/main/java/androidx/media3/common/C.java
index fa8c8288b05..968253e4b33 100644
--- a/libraries/common/src/main/java/androidx/media3/common/C.java
+++ b/libraries/common/src/main/java/androidx/media3/common/C.java
@@ -1044,29 +1044,31 @@ private 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 @@ private 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}.
diff --git a/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java b/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java
index 829262bb88b..aae29250d18 100644
--- a/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java
+++ b/libraries/common/src/main/java/androidx/media3/common/ColorInfo.java
@@ -28,10 +28,23 @@
import java.util.Arrays;
import org.checkerframework.dataflow.qual.Pure;
-/** Stores color info. */
+/**
+ * Stores color info.
+ *
+ *
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.
diff --git a/libraries/common/src/main/java/androidx/media3/common/DebugViewProvider.java b/libraries/common/src/main/java/androidx/media3/common/DebugViewProvider.java
new file mode 100644
index 00000000000..83f39fa81fe
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/DebugViewProvider.java
@@ -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.
+ *
+ *
This method may be called on an arbitrary thread.
+ */
+ @Nullable
+ SurfaceView getDebugPreviewSurfaceView(int width, int height);
+}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffect.java b/libraries/common/src/main/java/androidx/media3/common/Effect.java
similarity index 53%
rename from libraries/transformer/src/main/java/androidx/media3/transformer/GlEffect.java
rename to libraries/common/src/main/java/androidx/media3/common/Effect.java
index 854446d6a14..5504b5d9ef7 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlEffect.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Effect.java
@@ -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.
- *
- *
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 {}
diff --git a/libraries/common/src/main/java/androidx/media3/common/FlagSet.java b/libraries/common/src/main/java/androidx/media3/common/FlagSet.java
index 4d9b908432d..e67e4a6402a 100644
--- a/libraries/common/src/main/java/androidx/media3/common/FlagSet.java
+++ b/libraries/common/src/main/java/androidx/media3/common/FlagSet.java
@@ -22,6 +22,7 @@
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 Builder() {
* @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 Builder add(int flag) {
* @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 Builder addIf(int flag, boolean condition) {
* @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 Builder addAll(int... flags) {
* @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 Builder addAll(FlagSet flags) {
* @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 Builder remove(int flag) {
* @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 Builder removeIf(int flag, boolean condition) {
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
+ @CanIgnoreReturnValue
public Builder removeAll(int... flags) {
for (int flag : flags) {
remove(flag);
diff --git a/libraries/common/src/main/java/androidx/media3/common/Format.java b/libraries/common/src/main/java/androidx/media3/common/Format.java
index b24b0430092..bb712e24723 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Format.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Format.java
@@ -24,6 +24,7 @@
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 @@ private Builder(Format format) {
* @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 Builder setId(@Nullable String id) {
* @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 Builder setId(int id) {
* @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 Builder setLabel(@Nullable String label) {
* @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 Builder setLanguage(@Nullable String language) {
* @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 Builder setSelectionFlags(@C.SelectionFlags int selectionFlags) {
* @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 Builder setRoleFlags(@C.RoleFlags int roleFlags) {
* @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 Builder setAverageBitrate(int averageBitrate) {
* @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 Builder setPeakBitrate(int peakBitrate) {
* @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 Builder setCodecs(@Nullable String codecs) {
* @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 Builder setMetadata(@Nullable Metadata metadata) {
* @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 Builder setContainerMimeType(@Nullable String containerMimeType) {
* @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 Builder setSampleMimeType(@Nullable String sampleMimeType) {
* @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 Builder setMaxInputSize(int maxInputSize) {
* @param initializationData The {@link Format#initializationData}.
* @return The builder.
*/
+ @CanIgnoreReturnValue
public Builder setInitializationData(@Nullable List initializationData) {
this.initializationData = initializationData;
return this;
@@ -407,6 +422,7 @@ public Builder setInitializationData(@Nullable List initializationData)
* @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 Builder setDrmInitData(@Nullable DrmInitData drmInitData) {
* @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 Builder setSubsampleOffsetUs(long subsampleOffsetUs) {
* @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 Builder setWidth(int width) {
* @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 Builder setHeight(int height) {
* @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 Builder setFrameRate(float frameRate) {
* @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 Builder setRotationDegrees(int rotationDegrees) {
* @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 Builder setPixelWidthHeightRatio(float pixelWidthHeightRatio) {
* @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 Builder setProjectionData(@Nullable byte[] projectionData) {
* @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 Builder setStereoMode(@C.StereoMode int stereoMode) {
* @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 Builder setColorInfo(@Nullable ColorInfo colorInfo) {
* @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 Builder setChannelCount(int channelCount) {
* @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 Builder setSampleRate(int sampleRate) {
* @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 Builder setPcmEncoding(@C.PcmEncoding int pcmEncoding) {
* @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 Builder setEncoderDelay(int encoderDelay) {
* @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 Builder setEncoderPadding(int encoderPadding) {
* @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 Builder setAccessibilityChannel(int accessibilityChannel) {
* @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 static String toLogString(@Nullable Format format) {
@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 Bundle toBundle() {
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.
diff --git a/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java b/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java
index 5975897b405..2640a0a7e46 100644
--- a/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java
+++ b/libraries/common/src/main/java/androidx/media3/common/ForwardingPlayer.java
@@ -23,6 +23,7 @@
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 VideoSize getVideoSize() {
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() {
diff --git a/libraries/common/src/main/java/androidx/media3/common/FrameInfo.java b/libraries/common/src/main/java/androidx/media3/common/FrameInfo.java
new file mode 100644
index 00000000000..6a7301f797d
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/FrameInfo.java
@@ -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.
+ *
+ *
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;
+ }
+}
diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessingException.java b/libraries/common/src/main/java/androidx/media3/common/FrameProcessingException.java
similarity index 76%
rename from libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessingException.java
rename to libraries/common/src/main/java/androidx/media3/common/FrameProcessingException.java
index 6d413fd38ad..1ec4f476dfd 100644
--- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessingException.java
+++ b/libraries/common/src/main/java/androidx/media3/common/FrameProcessingException.java
@@ -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.
diff --git a/libraries/common/src/main/java/androidx/media3/common/FrameProcessor.java b/libraries/common/src/main/java/androidx/media3/common/FrameProcessor.java
new file mode 100644
index 00000000000..fb3bfd94211
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/FrameProcessor.java
@@ -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.
+ *
+ *
The changes are specified by {@link Effect} instances passed to {@link Factory#create}.
+ *
+ *
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)}.
+ *
+ *
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 effects,
+ DebugViewProvider debugViewProvider,
+ ColorInfo colorInfo,
+ boolean releaseFramesAutomatically)
+ throws FrameProcessingException;
+ }
+
+ /**
+ * Listener for asynchronous frame processing events.
+ *
+ *
All listener methods must be called from the same thread.
+ */
+ interface Listener {
+
+ /**
+ * Called when the output size changes.
+ *
+ *
The output size is the frame size in pixels after applying all {@linkplain Effect
+ * effects}.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
The new input information is applied from the next frame {@linkplain #registerInputFrame()
+ * registered} onwards.
+ *
+ *
Pixels are expanded using the {@link FrameInfo#pixelWidthHeightRatio} so that the output
+ * frames' pixels have a ratio of 1.
+ *
+ *
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.
+ *
+ *
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}.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
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}.
+ *
+ *
This will either render the output frame to the {@linkplain #setOutputSurfaceInfo output
+ * surface}, or drop the frame, per {@code releaseTimeNs}.
+ *
+ *
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}.
+ *
+ *
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.
+ *
+ *
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.
+ *
+ *
This method blocks until all resources are released or releasing times out.
+ */
+ void release();
+}
diff --git a/libraries/common/src/main/java/androidx/media3/common/LegacyMediaPlayerWrapper.java b/libraries/common/src/main/java/androidx/media3/common/LegacyMediaPlayerWrapper.java
new file mode 100644
index 00000000000..c432238a40a
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/LegacyMediaPlayerWrapper.java
@@ -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();
+ }
+}
diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java
index 8d85132c19d..7770a8c2765 100644
--- a/libraries/common/src/main/java/androidx/media3/common/MediaItem.java
+++ b/libraries/common/src/main/java/androidx/media3/common/MediaItem.java
@@ -29,6 +29,7 @@
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 @@ private Builder(MediaItem mediaItem) {
*
*
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 Builder setMediaId(String mediaId) {
* 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 Builder setUri(@Nullable String uri) {
* 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 Builder setUri(@Nullable Uri uri) {
*
* @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 Builder setClippingConfiguration(ClippingConfiguration clippingConfigurat
* @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 Builder setClipStartPositionMs(@IntRange(from = 0) long startPositionMs)
* @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 Builder setClipEndPositionMs(long endPositionMs) {
* @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 Builder setClipRelativeToLiveWindow(boolean relativeToLiveWindow) {
* @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 Builder setClipRelativeToDefaultPosition(boolean relativeToDefaultPositio
* @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 Builder setClipStartsAtKeyFrame(boolean startsAtKeyFrame) {
}
/** 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 Builder setDrmConfiguration(@Nullable DrmConfiguration drmConfiguration)
* @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 Builder setDrmLicenseUri(@Nullable Uri licenseUri) {
* @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 Builder setDrmLicenseUri(@Nullable String licenseUri) {
* 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 Builder setDrmLicenseRequestHeaders(
* @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 Builder setDrmUuid(@Nullable UUID uuid) {
* @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 Builder setDrmMultiSession(boolean multiSession) {
* @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 Builder setDrmForceDefaultLicenseUri(boolean forceDefaultLicenseUri) {
* @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 Builder setDrmPlayClearContentWithoutKey(boolean playClearContentWithoutK
* @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 Builder setDrmSessionForClearPeriods(boolean sessionForClearPeriods) {
* 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 Builder setDrmSessionForClearTypes(
* @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 Builder setDrmKeySetId(@Nullable byte[] keySetId) {
*
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 streamKeys) {
this.streamKeys =
@@ -377,6 +400,7 @@ public Builder setStreamKeys(@Nullable List streamKeys) {
*
*
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 Builder setCustomCacheKey(@Nullable String customCacheKey) {
* #setSubtitleConfigurations(List)} doesn't accept null, use an empty list to clear the
* contents.
*/
+ @CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setSubtitles(@Nullable List subtitles) {
@@ -401,6 +426,7 @@ public Builder setSubtitles(@Nullable List subtitles) {
*
*
This method should only be called if {@link #setUri} is passed a non-null value.
*/
+ @CanIgnoreReturnValue
public Builder setSubtitleConfigurations(List subtitleConfigurations) {
this.subtitleConfigurations = ImmutableList.copyOf(subtitleConfigurations);
return this;
@@ -411,6 +437,7 @@ public Builder setSubtitleConfigurations(List subtitleCon
*
*
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 Builder setAdsConfiguration(@Nullable AdsConfiguration adsConfiguration)
* 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 Builder setAdTagUri(@Nullable String adTagUri) {
* @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 Builder setAdTagUri(@Nullable Uri adTagUri) {
* {@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 Builder setAdTagUri(@Nullable Uri adTagUri, @Nullable Object adsId) {
}
/** 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 Builder setLiveConfiguration(LiveConfiguration liveConfiguration) {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setTargetOffsetMs(long)}.
*/
+ @CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveTargetOffsetMs(long liveTargetOffsetMs) {
@@ -471,6 +503,7 @@ public Builder setLiveTargetOffsetMs(long liveTargetOffsetMs) {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setMinOffsetMs(long)}.
*/
+ @CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveMinOffsetMs(long liveMinOffsetMs) {
@@ -482,6 +515,7 @@ public Builder setLiveMinOffsetMs(long liveMinOffsetMs) {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setMaxOffsetMs(long)}.
*/
+ @CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveMaxOffsetMs(long liveMaxOffsetMs) {
@@ -493,6 +527,7 @@ public Builder setLiveMaxOffsetMs(long liveMaxOffsetMs) {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setMinPlaybackSpeed(float)}.
*/
+ @CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveMinPlaybackSpeed(float minPlaybackSpeed) {
@@ -504,6 +539,7 @@ public Builder setLiveMinPlaybackSpeed(float minPlaybackSpeed) {
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
* LiveConfiguration.Builder#setMaxPlaybackSpeed(float)}.
*/
+ @CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setLiveMaxPlaybackSpeed(float maxPlaybackSpeed) {
@@ -518,18 +554,21 @@ public Builder setLiveMaxPlaybackSpeed(float maxPlaybackSpeed) {
*
*
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 @@ private Builder(DrmConfiguration drmConfiguration) {
}
/** Sets the {@link UUID} of the protection scheme. */
+ @CanIgnoreReturnValue
public Builder setScheme(UUID scheme) {
this.scheme = scheme;
return this;
@@ -622,6 +662,7 @@ public Builder setScheme(UUID scheme) {
* @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 @@ private Builder setNullableScheme(@Nullable UUID scheme) {
}
/** 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 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 Builder setMultiSession(boolean multiSession) {
* 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 Builder setForceDefaultLicenseUri(boolean forceDefaultLicenseUri) {
* 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 Builder setPlayClearContentWithoutKey(boolean playClearContentWithoutKey)
/**
* @deprecated Use {@link #setForceSessionsForAudioAndVideoTracks(boolean)} instead.
*/
+ @CanIgnoreReturnValue
@UnstableApi
@Deprecated
@InlineMe(
@@ -690,6 +738,7 @@ public Builder forceSessionsForAudioAndVideoTracks(
*
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 Builder setForceSessionsForAudioAndVideoTracks(
*
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 Builder setForcedSessionTrackTypes(
* 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 Builder(Uri adTagUri) {
}
/** Sets the ad tag URI to load. */
+ @CanIgnoreReturnValue
public Builder setAdTagUri(Uri adTagUri) {
this.adTagUri = adTagUri;
return this;
@@ -875,6 +927,7 @@ public Builder setAdTagUri(Uri adTagUri) {
*
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 @@ private Builder(LiveConfiguration liveConfiguration) {
*
*
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 Builder setTargetOffsetMs(long targetOffsetMs) {
*
*
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 Builder setMinOffsetMs(long minOffsetMs) {
*
*
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 Builder setMaxOffsetMs(long maxOffsetMs) {
*
*
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 Builder setMinPlaybackSpeed(float minPlaybackSpeed) {
*
*
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 @@ private Builder(SubtitleConfiguration subtitleConfiguration) {
}
/** 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 @@ private Builder(ClippingConfiguration clippingConfiguration) {
* 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 Builder setStartPositionMs(@IntRange(from = 0) long startPositionMs) {
* 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 Builder setEndPositionMs(long endPositionMs) {
* {@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 Builder setRelativeToLiveWindow(boolean relativeToLiveWindow) {
* 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 Builder setRelativeToDefaultPosition(boolean relativeToDefaultPosition) {
* 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 @@ private ClippingProperties(Builder builder) {
* MediaItem}.
*
*
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 @@ private Builder(RequestMetadata requestMetadata) {
}
/** 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;
diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java
index 62be209a9b8..603392d3696 100644
--- a/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java
+++ b/libraries/common/src/main/java/androidx/media3/common/MediaLibraryInfo.java
@@ -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;
diff --git a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java
index e32584785e4..05d37b29de6 100644
--- a/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java
+++ b/libraries/common/src/main/java/androidx/media3/common/MediaMetadata.java
@@ -29,6 +29,7 @@
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 @@ private Builder(MediaMetadata mediaMetadata) {
}
/** 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 Builder setDisplayTitle(@Nullable CharSequence displayTitle) {
*
*
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 Builder setOverallRating(@Nullable Rating overallRating) {
* @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 Builder setArtworkData(@Nullable byte[] artworkData) {
* 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 Builder setArtworkData(
*
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 Builder maybeSetArtworkData(byte[] artworkData, @PictureType int artworkD
}
/** 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 Builder setIsPlayable(@Nullable Boolean isPlayable) {
/**
* @deprecated Use {@link #setRecordingYear(Integer)} instead.
*/
+ @CanIgnoreReturnValue
@UnstableApi
@Deprecated
public Builder setYear(@Nullable Integer year) {
@@ -250,6 +269,7 @@ public Builder setYear(@Nullable Integer year) {
}
/** Sets the year of the recording date. */
+ @CanIgnoreReturnValue
public Builder setRecordingYear(@Nullable Integer recordingYear) {
this.recordingYear = recordingYear;
return this;
@@ -260,6 +280,7 @@ public Builder setRecordingYear(@Nullable Integer recordingYear) {
*
*
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 Builder setRecordingMonth(
*
*
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 Builder setReleaseYear(@Nullable Integer releaseYear) {
*
*
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 Builder setReleaseMonth(@Nullable @IntRange(from = 1, to = 12) Integer re
*
*
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 Builder setExtras(@Nullable Bundle extras) {
*
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 Builder populateFromMetadata(Metadata metadata) {
*
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 metadataList) {
for (int i = 0; i < metadataList.size(); i++) {
@@ -397,6 +433,7 @@ public Builder populateFromMetadata(List metadataList) {
}
/** Populates all the fields from {@code mediaMetadata}, provided they are non-null. */
+ @CanIgnoreReturnValue
@UnstableApi
public Builder populate(@Nullable MediaMetadata mediaMetadata) {
if (mediaMetadata == null) {
diff --git a/libraries/common/src/main/java/androidx/media3/common/Metadata.java b/libraries/common/src/main/java/androidx/media3/common/Metadata.java
index 1af08378f7e..6a558425518 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Metadata.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Metadata.java
@@ -20,6 +20,7 @@
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 @@ default void populateMediaMetadata(MediaMetadata.Builder builder) {}
}
private final Entry[] entries;
+ /**
+ * The presentation time of the metadata, in microseconds.
+ *
+ *
This time is an offset from the start of the current {@link Timeline.Period}.
+ *
+ *
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 Metadata(Entry... entries) {
* @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 Metadata(List extends Entry> entries) {
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 Metadata copyWithAppendedEntries(Entry... entriesToAppend) {
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 boolean equals(@Nullable Object obj) {
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 void writeToParcel(Parcel dest, int flags) {
for (Entry entry : entries) {
dest.writeParcelable(entry, 0);
}
+ dest.writeLong(presentationTimeUs);
}
public static final Parcelable.Creator CREATOR =
diff --git a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java
index 28928ba15a1..859773b2a66 100644
--- a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java
+++ b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java
@@ -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
diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java
index 4f2834b1ef2..5e05ae63014 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Player.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Player.java
@@ -33,9 +33,11 @@
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 @@ private Builder(Commands commands) {
* @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 Builder add(@Command int command) {
* @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 Builder addIf(@Command int command, boolean condition) {
* @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 Builder addAll(@Command int... commands) {
* @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 Builder addAll(Commands commands) {
* @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 Builder addAllCommands() {
* @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 Builder remove(@Command int command) {
* @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 Builder removeIf(@Command int command, boolean condition) {
* @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 @@ default void onMetadata(Metadata metadata) {}
*/
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();
diff --git a/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java
new file mode 100644
index 00000000000..f3a073be7fc
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/SimpleBasePlayer.java
@@ -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.
+ *
+ *
Implementation notes:
+ *
+ *
+ *
Subclasses must override {@link #getState()} to populate the current player state on
+ * request.
+ *
The {@link State} should set the {@linkplain State.Builder#setAvailableCommands available
+ * commands} to indicate which {@link Player} methods are supported.
+ *
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()}.
+ *
Subclasses can manually trigger state updates with {@link #invalidateState}, for example if
+ * something changes independent of {@link Player} method calls.
+ *
+ *
+ * This base class handles various aspects of the player implementation to simplify the subclass:
+ *
+ *
+ *
The {@link State} can only be created with allowed combinations of state values, avoiding
+ * any invalid player states.
+ *
Only functionality that is declared as {@linkplain Player.Command available} needs to be
+ * implemented. Other methods are automatically ignored.
+ *
Listener handling and informing listeners of state changes is handled automatically.
+ *
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.
+ *
+ */
+@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 listeners;
+ private final Looper applicationLooper;
+ private final HandlerWrapper applicationHandler;
+ private final HashSet> 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 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 mediaItems, boolean resetPosition) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void setMediaItems(
+ List mediaItems, int startIndex, long startPositionMs) {
+ // TODO: implement.
+ throw new IllegalStateException();
+ }
+
+ @Override
+ public final void addMediaItems(int index, List 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.
+ *
+ *
Triggers a call to {@link #getState()} and informs listeners if the state changed.
+ *
+ *
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.
+ *
+ *
The {@link State} should include all {@linkplain
+ * State.Builder#setAvailableCommands(Commands) available commands} indicating which player
+ * methods are allowed to be called.
+ *
+ *
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.
+ *
+ *
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}.
+ *
+ *
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 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
+ }
+}
diff --git a/libraries/common/src/main/java/androidx/media3/common/SurfaceInfo.java b/libraries/common/src/main/java/androidx/media3/common/SurfaceInfo.java
new file mode 100644
index 00000000000..42477cc132c
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/SurfaceInfo.java
@@ -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}.
+ *
+ *
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;
+ }
+}
diff --git a/libraries/common/src/main/java/androidx/media3/common/Timeline.java b/libraries/common/src/main/java/androidx/media3/common/Timeline.java
index 14b04065f50..43dc1aed117 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Timeline.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Timeline.java
@@ -34,6 +34,7 @@
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 Window() {
}
/** Sets the data held by this window. */
+ @CanIgnoreReturnValue
@UnstableApi
@SuppressWarnings("deprecation")
public Window set(
@@ -626,6 +628,7 @@ public Period() {
* period is not within the window.
* @return This period, for convenience.
*/
+ @CanIgnoreReturnValue
@UnstableApi
public Period set(
@Nullable Object id,
@@ -662,6 +665,7 @@ public Period set(
* information has yet to be loaded.
* @return This period, for convenience.
*/
+ @CanIgnoreReturnValue
@UnstableApi
public Period set(
@Nullable Object id,
diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java
index 6cb679368a4..ce934111d56 100644
--- a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java
+++ b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java
@@ -26,11 +26,11 @@
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 boolean equals(@Nullable Object obj) {
@Override
public Bundle toBundle() {
Bundle bundle = new Bundle();
- bundle.putParcelableArrayList(
- keyForField(FIELD_FORMATS), BundleableUtil.toBundleArrayList(Lists.newArrayList(formats)));
+ ArrayList 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;
}
diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java
index b7fe8176e3d..1c2f7a633a7 100644
--- a/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java
+++ b/libraries/common/src/main/java/androidx/media3/common/TrackSelectionParameters.java
@@ -33,6 +33,7 @@
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 @@ private void init(@UnknownInitialization Builder this, TrackSelectionParameters
}
/** 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 @@ protected Builder set(TrackSelectionParameters parameters) {
*
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder setMaxVideoSizeSd() {
return setMaxVideoSize(1279, 719);
}
@@ -328,6 +331,7 @@ public Builder setMaxVideoSizeSd() {
*
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder clearVideoSizeConstraints() {
return setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
@@ -339,6 +343,7 @@ public Builder clearVideoSizeConstraints() {
* @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 Builder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {
* @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 Builder setMaxVideoFrameRate(int maxVideoFrameRate) {
* @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 Builder setMaxVideoBitrate(int maxVideoBitrate) {
* @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 Builder setMinVideoSize(int minVideoWidth, int minVideoHeight) {
* @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 Builder setMinVideoFrameRate(int minVideoFrameRate) {
* @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 Builder setMinVideoBitrate(int minVideoBitrate) {
* playback.
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder setViewportSizeToPhysicalDisplaySize(
Context context, boolean viewportOrientationMayChange) {
// Assume the viewport is fullscreen.
@@ -424,6 +435,7 @@ public Builder setViewportSizeToPhysicalDisplaySize(
*
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder clearViewportSizeConstraints() {
return setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true);
}
@@ -438,6 +450,7 @@ public Builder clearViewportSizeConstraints() {
* playback.
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder setViewportSize(
int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
this.viewportWidth = viewportWidth;
@@ -464,6 +477,7 @@ public Builder setPreferredVideoMimeType(@Nullable String mimeType) {
* 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 Builder setPreferredVideoMimeTypes(String... mimeTypes) {
* @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 Builder setPreferredAudioLanguage(@Nullable String preferredAudioLanguage
* there's no default.
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder setPreferredAudioLanguages(String... preferredAudioLanguages) {
this.preferredAudioLanguages = normalizeLanguageCodes(preferredAudioLanguages);
return this;
@@ -514,6 +530,7 @@ public Builder setPreferredAudioLanguages(String... preferredAudioLanguages) {
* @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 Builder setPreferredAudioRoleFlags(@C.RoleFlags int preferredAudioRoleFla
* @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 Builder setMaxAudioChannelCount(int maxAudioChannelCount) {
* @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 Builder setPreferredAudioMimeType(@Nullable String mimeType) {
* 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 Builder setPreferredAudioMimeTypes(String... mimeTypes) {
* @param context A {@link Context}.
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(
Context context) {
if (Util.SDK_INT >= 19) {
@@ -604,6 +625,7 @@ public Builder setPreferredTextLanguage(@Nullable String preferredTextLanguage)
* track otherwise.
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder setPreferredTextLanguages(String... preferredTextLanguages) {
this.preferredTextLanguages = normalizeLanguageCodes(preferredTextLanguages);
return this;
@@ -615,6 +637,7 @@ public Builder setPreferredTextLanguages(String... preferredTextLanguages) {
* @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 Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags
* text track selections.
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder setIgnoredTextSelectionFlags(@C.SelectionFlags int ignoredTextSelectionFlags) {
this.ignoredTextSelectionFlags = ignoredTextSelectionFlags;
return this;
@@ -641,6 +665,7 @@ public Builder setIgnoredTextSelectionFlags(@C.SelectionFlags int ignoredTextSel
* 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 Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextL
* video tracks.
* @return This builder.
*/
+ @CanIgnoreReturnValue
public Builder setForceLowestBitrate(boolean forceLowestBitrate) {
this.forceLowestBitrate = forceLowestBitrate;
return this;
@@ -669,18 +695,21 @@ public Builder setForceLowestBitrate(boolean forceLowestBitrate) {
* 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 Builder setOverrideForType(TrackSelectionOverride override) {
}
/** 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 it = overrides.values().iterator();
while (it.hasNext()) {
@@ -706,6 +737,7 @@ public Builder clearOverridesOfType(@C.TrackType int trackType) {
}
/** Removes all overrides. */
+ @CanIgnoreReturnValue
public Builder clearOverrides() {
overrides.clear();
return this;
@@ -719,6 +751,7 @@ public Builder clearOverrides() {
* @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 Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackType
* @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);
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioProcessor.java b/libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessor.java
similarity index 88%
rename from libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioProcessor.java
rename to libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessor.java
index ed7b23813ce..506e2d4266a 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioProcessor.java
+++ b/libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessor.java
@@ -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 String toString() {
+ 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 UnhandledAudioFormatException(AudioFormat inputAudioFormat) {
* @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 UnhandledAudioFormatException(AudioFormat inputAudioFormat) {
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();
diff --git a/libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessorChain.java b/libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessorChain.java
new file mode 100644
index 00000000000..0c776c727fe
--- /dev/null
+++ b/libraries/common/src/main/java/androidx/media3/common/audio/AudioProcessorChain.java
@@ -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.
+ *
+ *