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

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

View File

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

3
.gitignore vendored
View File

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

View File

@ -21,7 +21,7 @@ all of the information requested in the issue template.
## Pull requests ## 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 into the `main` branch. Before a pull request can be accepted you must submit
a Contributor License Agreement, as described below. a Contributor License Agreement, as described below.

View File

@ -1,5 +1,146 @@
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) ### 1.0.0-beta02 (2022-07-21)
This release corresponds to the This release corresponds to the
@ -32,6 +173,8 @@ This release corresponds to the
* RTSP: * RTSP:
* Add VP8 fragmented packet handling * Add VP8 fragmented packet handling
([#110](https://github.com/androidx/media/pull/110)). ([#110](https://github.com/androidx/media/pull/110)).
* Support frames/fragments in VP9
([#115](https://github.com/androidx/media/pull/115)).
* Leanback extension: * Leanback extension:
* Listen to `playWhenReady` changes in `LeanbackAdapter` * Listen to `playWhenReady` changes in `LeanbackAdapter`
([10420](https://github.com/google/ExoPlayer/issues/10420)). ([10420](https://github.com/google/ExoPlayer/issues/10420)).
@ -266,6 +409,8 @@ This release corresponds to the
`DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise. `DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise.
* Remove constructor `DefaultTrackSelector(ExoTrackSelection.Factory)`. * Remove constructor `DefaultTrackSelector(ExoTrackSelection.Factory)`.
Use `DefaultTrackSelector(Context, ExoTrackSelection.Factory)` instead. 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) ### 1.0.0-alpha03 (2022-03-14)

View File

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

View File

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

View File

@ -78,6 +78,9 @@ project(modulePrefix + 'lib-extractor').projectDir = new File(rootDir, 'librarie
include modulePrefix + 'lib-cast' include modulePrefix + 'lib-cast'
project(modulePrefix + 'lib-cast').projectDir = new File(rootDir, 'libraries/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' include modulePrefix + 'lib-transformer'
project(modulePrefix + 'lib-transformer').projectDir = new File(rootDir, 'libraries/transformer') project(modulePrefix + 'lib-transformer').projectDir = new File(rootDir, 'libraries/transformer')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
apply from: '../../constants.gradle' apply from: '../../constants.gradle'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
@ -26,7 +27,9 @@ android {
versionName project.ext.releaseVersion versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion // Not using appTargetSDKVersion to allow local file access on API 29
// and higher [Internal ref: b/191644662]
targetSdkVersion project.ext.mainDemoAppTargetSdkVersion
multiDexEnabled true multiDexEnabled true
} }

View File

@ -399,7 +399,7 @@
"uri": "ssai://dai.google.com/?contentSourceId=2528370&videoId=tears-of-steel&format=2&adsId=1" "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" "uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
}, },
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@
*/ */
package androidx.media3.demo.transformer; package androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context; import android.content.Context;
@ -27,15 +28,14 @@ import android.graphics.Paint;
import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.BitmapDrawable;
import android.opengl.GLES20; import android.opengl.GLES20;
import android.opengl.GLUtils; import android.opengl.GLUtils;
import android.util.Size; import android.util.Pair;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.FrameProcessingException;
import androidx.media3.common.util.GlProgram; import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.FrameProcessingException; import androidx.media3.effect.SingleFrameGlTextureProcessor;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import java.io.IOException; import java.io.IOException;
import java.util.Locale; import java.util.Locale;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* A {@link SingleFrameGlTextureProcessor} that overlays a bitmap with a logo and timer on each * A {@link SingleFrameGlTextureProcessor} that overlays a bitmap with a logo and timer on each
@ -45,10 +45,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
*/ */
// TODO(b/227625365): Delete this class and use a texture processor from the Transformer library, // TODO(b/227625365): Delete this class and use a texture processor from the Transformer library,
// once overlaying a bitmap and text is supported in Transformer. // once overlaying a bitmap and text is supported in Transformer.
/* package */ final class BitmapOverlayProcessor implements SingleFrameGlTextureProcessor { /* package */ final class BitmapOverlayProcessor extends SingleFrameGlTextureProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl"; private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl"; private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl";
@ -57,16 +54,25 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
private final Paint paint; private final Paint paint;
private final Bitmap overlayBitmap; private final Bitmap overlayBitmap;
private final Bitmap logoBitmap;
private final Canvas overlayCanvas; private final Canvas overlayCanvas;
private final GlProgram glProgram;
private float bitmapScaleX; private float bitmapScaleX;
private float bitmapScaleY; private float bitmapScaleY;
private int bitmapTexId; private int bitmapTexId;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull Bitmap logoBitmap;
private @MonotonicNonNull GlProgram glProgram;
public BitmapOverlayProcessor() { /**
* Creates a new instance.
*
* @param context The {@link Context}.
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
* @throws FrameProcessingException If a problem occurs while reading shader files.
*/
public BitmapOverlayProcessor(Context context, boolean useHdr) throws FrameProcessingException {
super(useHdr);
checkArgument(!useHdr, "BitmapOverlayProcessor does not support HDR colors.");
paint = new Paint(); paint = new Paint();
paint.setTextSize(64); paint.setTextSize(64);
paint.setAntiAlias(true); paint.setAntiAlias(true);
@ -75,19 +81,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
overlayBitmap = overlayBitmap =
Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888); Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888);
overlayCanvas = new Canvas(overlayBitmap); overlayCanvas = new Canvas(overlayBitmap);
}
@Override
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
throws IOException {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
outputSize = new Size(inputWidth, inputHeight);
try { try {
logoBitmap = logoBitmap =
@ -97,30 +90,46 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(e); throw new IllegalStateException(e);
} }
bitmapTexId = GlUtil.createTexture(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT); try {
bitmapTexId =
GlUtil.createTexture(
BITMAP_WIDTH_HEIGHT,
BITMAP_WIDTH_HEIGHT,
/* useHighPrecisionColorComponents= */ false);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0); GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
} catch (GlUtil.GlException | IOException e) {
throw new FrameProcessingException(e);
}
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute( glProgram.setBufferAttribute(
"aFramePosition", "aFramePosition",
GlUtil.getNormalizedCoordinateBounds(), GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1); glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
}
@Override
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
if (inputWidth > inputHeight) {
bitmapScaleX = inputWidth / (float) inputHeight;
bitmapScaleY = 1f;
} else {
bitmapScaleX = 1f;
bitmapScaleY = inputHeight / (float) inputWidth;
}
glProgram.setFloatUniform("uScaleX", bitmapScaleX); glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY); glProgram.setFloatUniform("uScaleY", bitmapScaleY);
return Pair.create(inputWidth, inputHeight);
} }
@Override @Override
public Size getOutputSize() { public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
return checkStateNotNull(outputSize);
}
@Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
try { try {
checkStateNotNull(glProgram).use(); glProgram.use();
// Draw to the canvas and store it in a texture. // Draw to the canvas and store it in a texture.
String text = String text =
@ -137,19 +146,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
flipBitmapVertically(overlayBitmap)); flipBitmapVertically(overlayBitmap));
GlUtil.checkGlError(); GlUtil.checkGlError();
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.bindAttributesAndUniforms(); glProgram.bindAttributesAndUniforms();
// The four-vertex triangle strip forms a quad. // The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
GlUtil.checkGlError(); GlUtil.checkGlError();
} catch (GlUtil.GlException e) { } catch (GlUtil.GlException e) {
throw new FrameProcessingException(e); throw new FrameProcessingException(e, presentationTimeUs);
} }
} }
@Override @Override
public void release() { public void release() throws FrameProcessingException {
if (glProgram != null) { super.release();
try {
glProgram.delete(); glProgram.delete();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
} }
} }

View File

@ -18,9 +18,11 @@ package androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import android.Manifest;
import android.app.Activity; import android.app.Activity;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
@ -29,9 +31,14 @@ import android.widget.Button;
import android.widget.CheckBox; import android.widget.CheckBox;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.TextView; 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.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util; 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_START_MS = "trim_start_ms";
public static final String TRIM_END_MS = "trim_end_ms"; public static final String TRIM_END_MS = "trim_end_ms";
public static final String ENABLE_FALLBACK = "enable_fallback"; 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 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 ENABLE_HDR_EDITING = "enable_hdr_editing";
public static final String DEMO_EFFECTS_SELECTIONS = "demo_effects_selections"; 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_X = "periodic_vignette_center_x";
public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y"; 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_INNER_RADIUS = "periodic_vignette_inner_radius";
public static final String PERIODIC_VIGNETTE_OUTER_RADIUS = "periodic_vignette_outer_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-1/mp4/android-screens-10s.mp4",
"https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4", "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4",
"https://html5demos.com/assets/dizzy.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/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/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/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", "720p H264 video and AAC audio",
"1080p H265 video and AAC audio", "1080p H265 video and AAC audio",
"360p H264 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)", "H264 video and AAC audio (portrait, H < W, 90\u00B0)",
"SEF slow motion with 240 fps", "SEF slow motion with 240 fps",
"480p DASH (non-square pixels)", "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 = { private static final String[] DEMO_EFFECTS = {
"Dizzy crop", "Dizzy crop",
"Edge detector (Media Pipe)", "Edge detector (Media Pipe)",
"Color filters",
"Map White to Green Color Lookup Table",
"RGB Adjustments",
"HSL Adjustments",
"Contrast",
"Periodic vignette", "Periodic vignette",
"3D spin", "3D spin",
"Overlay logo & timer", "Overlay logo & timer",
"Zoom in start", "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 String SAME_AS_INPUT_OPTION = "same as input";
private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2); private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2);
private @MonotonicNonNull Button selectFileButton; private @MonotonicNonNull ActivityResultLauncher<Intent> localFilePickerLauncher;
private @MonotonicNonNull Button selectPresetFileButton;
private @MonotonicNonNull Button selectLocalFileButton;
private @MonotonicNonNull TextView selectedFileTextView; private @MonotonicNonNull TextView selectedFileTextView;
private @MonotonicNonNull CheckBox removeAudioCheckbox; private @MonotonicNonNull CheckBox removeAudioCheckbox;
private @MonotonicNonNull CheckBox removeVideoCheckbox; private @MonotonicNonNull CheckBox removeVideoCheckbox;
@ -120,13 +152,24 @@ public final class ConfigurationActivity extends AppCompatActivity {
private @MonotonicNonNull Spinner rotateSpinner; private @MonotonicNonNull Spinner rotateSpinner;
private @MonotonicNonNull CheckBox trimCheckBox; private @MonotonicNonNull CheckBox trimCheckBox;
private @MonotonicNonNull CheckBox enableFallbackCheckBox; private @MonotonicNonNull CheckBox enableFallbackCheckBox;
private @MonotonicNonNull CheckBox enableDebugPreviewCheckBox;
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox;
private @MonotonicNonNull CheckBox forceInterpretHdrVideoAsSdrCheckBox;
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox;
private @MonotonicNonNull Button selectDemoEffectsButton; private @MonotonicNonNull Button selectDemoEffectsButton;
private boolean @MonotonicNonNull [] demoEffectsSelections; private boolean @MonotonicNonNull [] demoEffectsSelections;
private @Nullable Uri localFileUri;
private int inputUriPosition; private int inputUriPosition;
private long trimStartMs; private long trimStartMs;
private long trimEndMs; 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 periodicVignetteCenterX;
private float periodicVignetteCenterY; private float periodicVignetteCenterY;
private float periodicVignetteInnerRadius; private float periodicVignetteInnerRadius;
@ -139,11 +182,10 @@ public final class ConfigurationActivity extends AppCompatActivity {
findViewById(R.id.transform_button).setOnClickListener(this::startTransformation); findViewById(R.id.transform_button).setOnClickListener(this::startTransformation);
selectFileButton = findViewById(R.id.select_file_button); flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox);
selectFileButton.setOnClickListener(this::selectFile);
selectedFileTextView = findViewById(R.id.selected_file_text_view); 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 = findViewById(R.id.remove_audio_checkbox);
removeAudioCheckbox.setOnClickListener(this::onRemoveAudio); removeAudioCheckbox.setOnClickListener(this::onRemoveAudio);
@ -151,7 +193,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
removeVideoCheckbox = findViewById(R.id.remove_video_checkbox); removeVideoCheckbox = findViewById(R.id.remove_video_checkbox);
removeVideoCheckbox.setOnClickListener(this::onRemoveVideo); removeVideoCheckbox.setOnClickListener(this::onRemoveVideo);
flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox); selectPresetFileButton = findViewById(R.id.select_preset_file_button);
selectPresetFileButton.setOnClickListener(this::selectPresetFile);
selectLocalFileButton = findViewById(R.id.select_local_file_button);
selectLocalFileButton.setOnClickListener(this::selectLocalFile);
ArrayAdapter<String> audioMimeAdapter = ArrayAdapter<String> audioMimeAdapter =
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
@ -200,14 +246,38 @@ public final class ConfigurationActivity extends AppCompatActivity {
trimEndMs = C.TIME_UNSET; trimEndMs = C.TIME_UNSET;
enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox); 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 = findViewById(R.id.request_sdr_tone_mapping_checkbox);
enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported()); enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported());
findViewById(R.id.request_sdr_tone_mapping).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); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
demoEffectsSelections = new boolean[DEMO_EFFECTS.length]; demoEffectsSelections = new boolean[DEMO_EFFECTS.length];
selectDemoEffectsButton = findViewById(R.id.select_demo_effects_button); selectDemoEffectsButton = findViewById(R.id.select_demo_effects_button);
selectDemoEffectsButton.setOnClickListener(this::selectDemoEffects); 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 @Override
@ -215,7 +285,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
super.onResume(); super.onResume();
@Nullable Uri intentUri = getIntent().getData(); @Nullable Uri intentUri = getIntent().getData();
if (intentUri != null) { if (intentUri != null) {
checkNotNull(selectFileButton).setEnabled(false); checkNotNull(selectPresetFileButton).setEnabled(false);
checkNotNull(selectLocalFileButton).setEnabled(false);
checkNotNull(selectedFileTextView).setText(intentUri.toString()); checkNotNull(selectedFileTextView).setText(intentUri.toString());
} }
} }
@ -237,7 +308,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"rotateSpinner", "rotateSpinner",
"trimCheckBox", "trimCheckBox",
"enableFallbackCheckBox", "enableFallbackCheckBox",
"enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox", "enableRequestSdrToneMappingCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox", "enableHdrEditingCheckBox",
"demoEffectsSelections" "demoEffectsSelections"
}) })
@ -275,32 +348,85 @@ public final class ConfigurationActivity extends AppCompatActivity {
bundle.putLong(TRIM_END_MS, trimEndMs); bundle.putLong(TRIM_END_MS, trimEndMs);
} }
bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked()); bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked());
bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked());
bundle.putBoolean( bundle.putBoolean(
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked()); 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.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked());
bundle.putBooleanArray(DEMO_EFFECTS_SELECTIONS, demoEffectsSelections); 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_X, periodicVignetteCenterX);
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY); bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY);
bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius); bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius);
bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius); bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius);
transformerIntent.putExtras(bundle); transformerIntent.putExtras(bundle);
@Nullable Uri intentUri = getIntent().getData(); @Nullable Uri intentUri;
transformerIntent.setData( if (getIntent().getData() != null) {
intentUri != null ? intentUri : Uri.parse(INPUT_URIS[inputUriPosition])); intentUri = getIntent().getData();
} else if (localFileUri != null) {
intentUri = localFileUri;
} else {
intentUri = Uri.parse(PRESET_FILE_URIS[inputUriPosition]);
}
transformerIntent.setData(intentUri);
startActivity(transformerIntent); startActivity(transformerIntent);
} }
private void selectFile(View view) { private void selectPresetFile(View view) {
new AlertDialog.Builder(/* context= */ this) new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_file_title) .setTitle(R.string.select_preset_file_title)
.setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog) .setSingleChoiceItems(
PRESET_FILE_URI_DESCRIPTIONS, inputUriPosition, this::selectPresetFileInDialog)
.setPositiveButton(android.R.string.ok, /* listener= */ null) .setPositiveButton(android.R.string.ok, /* listener= */ null)
.create() .create()
.show(); .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) { private void selectDemoEffects(View view) {
new AlertDialog.Builder(/* context= */ this) new AlertDialog.Builder(/* context= */ this)
.setTitle(R.string.select_demo_effects) .setTitle(R.string.select_demo_effects)
@ -316,35 +442,122 @@ public final class ConfigurationActivity extends AppCompatActivity {
return; return;
} }
View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null); View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null);
RangeSlider radiusRangeSlider = RangeSlider trimRangeSlider =
checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider)); checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider));
radiusRangeSlider.setValues(0f, 60f); // seconds trimRangeSlider.setValues(0f, 10f); // seconds
new AlertDialog.Builder(/* context= */ this) new AlertDialog.Builder(/* context= */ this)
.setView(dialogView) .setView(dialogView)
.setPositiveButton( .setPositiveButton(
android.R.string.ok, android.R.string.ok,
(DialogInterface dialogInterface, int i) -> { (DialogInterface dialogInterface, int i) -> {
List<Float> radiusRange = radiusRangeSlider.getValues(); List<Float> trimRange = trimRangeSlider.getValues();
trimStartMs = 1000 * radiusRange.get(0).longValue(); trimStartMs = Math.round(1000 * trimRange.get(0));
trimEndMs = 1000 * radiusRange.get(1).longValue(); trimEndMs = Math.round(1000 * trimRange.get(1));
}) })
.create() .create()
.show(); .show();
} }
@RequiresNonNull("selectedFileTextView")
private void selectFileInDialog(DialogInterface dialog, int which) {
inputUriPosition = which;
selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
}
@RequiresNonNull("demoEffectsSelections") @RequiresNonNull("demoEffectsSelections")
private void selectDemoEffect(DialogInterface dialog, int which, boolean isChecked) { private void selectDemoEffect(DialogInterface dialog, int which, boolean isChecked) {
demoEffectsSelections[which] = isChecked; demoEffectsSelections[which] = isChecked;
if (!isChecked || which != PERIODIC_VIGNETTE_INDEX) { if (!isChecked) {
return; 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 = View dialogView =
getLayoutInflater().inflate(R.layout.periodic_vignette_options, /* root= */ null); getLayoutInflater().inflate(R.layout.periodic_vignette_options, /* root= */ null);
Slider centerXSlider = Slider centerXSlider =
@ -377,7 +590,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner", "resolutionHeightSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox", "enableRequestSdrToneMappingCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox", "enableHdrEditingCheckBox",
"selectDemoEffectsButton" "selectDemoEffectsButton"
}) })
@ -397,7 +612,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner", "resolutionHeightSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox", "enableRequestSdrToneMappingCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox", "enableHdrEditingCheckBox",
"selectDemoEffectsButton" "selectDemoEffectsButton"
}) })
@ -416,7 +633,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
"resolutionHeightSpinner", "resolutionHeightSpinner",
"scaleSpinner", "scaleSpinner",
"rotateSpinner", "rotateSpinner",
"enableDebugPreviewCheckBox",
"enableRequestSdrToneMappingCheckBox", "enableRequestSdrToneMappingCheckBox",
"forceInterpretHdrVideoAsSdrCheckBox",
"enableHdrEditingCheckBox", "enableHdrEditingCheckBox",
"selectDemoEffectsButton" "selectDemoEffectsButton"
}) })
@ -426,8 +645,10 @@ public final class ConfigurationActivity extends AppCompatActivity {
resolutionHeightSpinner.setEnabled(isVideoEnabled); resolutionHeightSpinner.setEnabled(isVideoEnabled);
scaleSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled);
rotateSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled);
enableDebugPreviewCheckBox.setEnabled(isVideoEnabled);
enableRequestSdrToneMappingCheckBox.setEnabled( enableRequestSdrToneMappingCheckBox.setEnabled(
isRequestSdrToneMappingSupported() && isVideoEnabled); isRequestSdrToneMappingSupported() && isVideoEnabled);
forceInterpretHdrVideoAsSdrCheckBox.setEnabled(isVideoEnabled);
enableHdrEditingCheckBox.setEnabled(isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
selectDemoEffectsButton.setEnabled(isVideoEnabled); selectDemoEffectsButton.setEnabled(isVideoEnabled);
@ -438,6 +659,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
findViewById(R.id.rotate).setEnabled(isVideoEnabled); findViewById(R.id.rotate).setEnabled(isVideoEnabled);
findViewById(R.id.request_sdr_tone_mapping) findViewById(R.id.request_sdr_tone_mapping)
.setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled); .setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled);
findViewById(R.id.force_interpret_hdr_video_as_sdr).setEnabled(isVideoEnabled);
findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled);
} }

View File

@ -18,8 +18,8 @@ package androidx.media3.demo.transformer;
import android.graphics.Matrix; import android.graphics.Matrix;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.transformer.GlMatrixTransformation; import androidx.media3.effect.GlMatrixTransformation;
import androidx.media3.transformer.MatrixTransformation; import androidx.media3.effect.MatrixTransformation;
/** /**
* Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link * Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link

View File

@ -16,39 +16,29 @@
package androidx.media3.demo.transformer; package androidx.media3.demo.transformer;
import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.content.Context; import android.content.Context;
import android.opengl.GLES20; 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.GlProgram;
import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.GlUtil;
import androidx.media3.transformer.FrameProcessingException; import androidx.media3.effect.SingleFrameGlTextureProcessor;
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
import java.io.IOException; import java.io.IOException;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* A {@link SingleFrameGlTextureProcessor} that periodically dims the frames such that pixels are * A {@link SingleFrameGlTextureProcessor} that periodically dims the frames such that pixels are
* darker the further they are away from the frame center. * darker the further they are away from the frame center.
*/ */
/* package */ final class PeriodicVignetteProcessor implements SingleFrameGlTextureProcessor { /* package */ final class PeriodicVignetteProcessor extends SingleFrameGlTextureProcessor {
static {
GlUtil.glAssertionsEnabled = true;
}
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl"; 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 String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl";
private static final float DIMMING_PERIOD_US = 5_600_000f; private static final float DIMMING_PERIOD_US = 5_600_000f;
private float centerX; private final GlProgram glProgram;
private float centerY; private final float minInnerRadius;
private float minInnerRadius; private final float deltaInnerRadius;
private float deltaInnerRadius;
private float outerRadius;
private @MonotonicNonNull Size outputSize;
private @MonotonicNonNull GlProgram glProgram;
/** /**
* Creates a new instance. * Creates a new instance.
@ -61,29 +51,35 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
* *
* <p>The parameters are given in normalized texture coordinates from 0 to 1. * <p>The parameters are given in normalized texture coordinates from 0 to 1.
* *
* @param context The {@link Context}.
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
* @param centerX The x-coordinate of the center of the effect. * @param centerX The x-coordinate of the center of the effect.
* @param centerY The y-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 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 maxInnerRadius The upper bound of the radius that is unaffected by the effect.
* @param outerRadius The radius after which all pixels are black. * @param outerRadius The radius after which all pixels are black.
* @throws FrameProcessingException If a problem occurs while reading shader files.
*/ */
public PeriodicVignetteProcessor( 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(minInnerRadius <= maxInnerRadius);
checkArgument(maxInnerRadius <= outerRadius); checkArgument(maxInnerRadius <= outerRadius);
this.centerX = centerX;
this.centerY = centerY;
this.minInnerRadius = minInnerRadius; this.minInnerRadius = minInnerRadius;
this.deltaInnerRadius = maxInnerRadius - minInnerRadius; this.deltaInnerRadius = maxInnerRadius - minInnerRadius;
this.outerRadius = outerRadius; try {
}
@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 = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0); } catch (IOException | GlUtil.GlException e) {
throw new FrameProcessingException(e);
}
glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY}); glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY});
glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius}); glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius});
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
@ -94,14 +90,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
@Override @Override
public Size getOutputSize() { public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
return checkStateNotNull(outputSize); return Pair.create(inputWidth, inputHeight);
} }
@Override @Override
public void drawFrame(long presentationTimeUs) throws FrameProcessingException { public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
try { try {
checkStateNotNull(glProgram).use(); glProgram.use();
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US; double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US;
float innerRadius = float innerRadius =
minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta)); minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta));
@ -110,14 +107,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
// The four-vertex triangle strip forms a quad. // The four-vertex triangle strip forms a quad.
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
} catch (GlUtil.GlException e) { } catch (GlUtil.GlException e) {
throw new FrameProcessingException(e); throw new FrameProcessingException(e, presentationTimeUs);
} }
} }
@Override @Override
public void release() { public void release() throws FrameProcessingException {
if (glProgram != null) { super.release();
try {
glProgram.delete(); glProgram.delete();
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e);
} }
} }
} }

View File

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

View File

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

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".ConfigurationActivity">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/contrast_value" />
<com.google.android.material.slider.Slider
android:id="@+id/contrast_slider"
android:valueFrom="-1.0"
android:value="0.0"
android:valueTo="1.0"
android:layout_gravity="right"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".ConfigurationActivity">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/hue_adjustment" />
<com.google.android.material.slider.Slider
android:id="@+id/hsl_adjustments_hue"
android:valueFrom="-360"
android:value="0"
android:valueTo="360"
android:layout_gravity="right"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/saturation_adjustment" />
<com.google.android.material.slider.Slider
android:id="@+id/hsl_adjustments_saturation"
android:valueFrom="-100"
android:value="0"
android:valueTo="100"
android:layout_gravity="right"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/lightness_adjustment" />
<com.google.android.material.slider.Slider
android:id="@+id/hsl_adjustment_lightness"
android:valueFrom="-100"
android:value="0"
android:valueTo="100"
android:layout_gravity="right"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2022 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
tools:context=".ConfigurationActivity">
<TableLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:stretchColumns="1"
android:layout_marginTop="32dp"
android:measureWithLargestChild="true"
android:paddingLeft="24dp"
android:paddingRight="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/rgb_adjustment_scale_red" />
<com.google.android.material.slider.Slider
android:id="@+id/rgb_adjustment_red_scale"
android:valueFrom="0"
android:value="1"
android:valueTo="2"
android:layout_gravity="right"
app:labelBehavior="gone"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/rgb_adjustment_scale_green" />
<com.google.android.material.slider.Slider
android:id="@+id/rgb_adjustment_green_scale"
android:valueFrom="0"
android:value="1"
android:valueTo="2"
android:layout_gravity="right"
app:labelBehavior="gone"/>
</TableRow>
<TableRow
android:layout_weight="1"
android:gravity="center_vertical" >
<TextView
android:text="@string/rgb_adjustment_scale_blue" />
<com.google.android.material.slider.Slider
android:id="@+id/rgb_adjustment_blue_scale"
android:valueFrom="0"
android:value="1"
android:valueTo="2"
android:layout_gravity="right"
app:labelBehavior="gone"/>
</TableRow>
</TableLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

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

View File

@ -17,7 +17,8 @@
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name" translatable="false">Transformer Demo</string> <string name="app_name" translatable="false">Transformer Demo</string>
<string name="configuration" translatable="false">Configuration</string> <string name="configuration" translatable="false">Configuration</string>
<string name="select_file_title" translatable="false">Choose file</string> <string name="select_preset_file_title" translatable="false">Choose preset file</string>
<string name="select_local_file_title">Choose local file</string>
<string name="remove_audio" translatable="false">Remove audio</string> <string name="remove_audio" translatable="false">Remove audio</string>
<string name="remove_video" translatable="false">Remove video</string> <string name="remove_video" translatable="false">Remove video</string>
<string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string> <string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string>
@ -27,9 +28,11 @@
<string name="scale" translatable="false">Scale video</string> <string name="scale" translatable="false">Scale video</string>
<string name="rotate" translatable="false">Rotate video (degrees)</string> <string name="rotate" translatable="false">Rotate video (degrees)</string>
<string name="enable_fallback" translatable="false">Enable fallback</string> <string name="enable_fallback" translatable="false">Enable fallback</string>
<string name="enable_debug_preview" translatable="false">Enable debug preview</string>
<string name="trim" translatable="false">Trim</string> <string name="trim" translatable="false">Trim</string>
<string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string> <string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string>
<string name="hdr_editing" translatable="false">[Experimental] HDR editing</string> <string name="force_interpret_hdr_video_as_sdr" translatable="false">[Experimental] Force interpret HDR video as SDR (API 29+)</string>
<string name="hdr_editing" translatable="false">[Experimental] HDR editing (API 31+)</string>
<string name="select_demo_effects" translatable="false">Add demo effects</string> <string name="select_demo_effects" translatable="false">Add demo effects</string>
<string name="periodic_vignette_options" translatable="false">Periodic vignette options</string> <string name="periodic_vignette_options" translatable="false">Periodic vignette options</string>
<string name="no_media_pipe_error" translatable="false">Failed to load MediaPipe processor. Check the README for instructions.</string> <string name="no_media_pipe_error" translatable="false">Failed to load MediaPipe processor. Check the README for instructions.</string>
@ -40,8 +43,27 @@
<string name="transformation_timer" translatable="false">Transformation started %d seconds ago.</string> <string name="transformation_timer" translatable="false">Transformation started %d seconds ago.</string>
<string name="transformation_completed" translatable="false">Transformation completed in %d seconds.</string> <string name="transformation_completed" translatable="false">Transformation completed in %d seconds.</string>
<string name="transformation_error" translatable="false">Transformation error</string> <string name="transformation_error" translatable="false">Transformation error</string>
<string name="trim_range">Bounds in seconds</string>
<string-array name="color_filter_options">
<item>Grayscale</item>
<item>Inverted</item>
<item>Sepia</item>
</string-array>
<string name="contrast_value" translatable="false">Contrast value</string>
<string name="rgb_adjustment_options" translatable="false">Scale RGB Channels individually</string>
<string name="rgb_adjustment_scale_red" translatable="false">Scale red</string>
<string name="rgb_adjustment_scale_green" translatable="false">Scale green</string>
<string name="rgb_adjustment_scale_blue" translatable="false">Scale blue</string>
<string name="center_x">Center X</string> <string name="center_x">Center X</string>
<string name="center_y">Center Y</string> <string name="center_y">Center Y</string>
<string name="radius_range">Radius range</string> <string name="radius_range">Radius range</string>
<string name="trim_range">Bounds in seconds</string> <string name="hsl_adjustment_options" translatable="false">HSL adjustment options</string>
<string name="hue_adjustment">Hue adjustment</string>
<string name="saturation_adjustment">Saturation adjustment</string>
<string name="lightness_adjustment">Lightness adjustment</string>
<string name="input_video">Input video:</string>
<string name="output_video">Output video:</string>
<string name="permission_denied">Permission Denied</string>
<string name="hide_input_video">Hide input video</string>
<string name="show_input_video">Show input video</string>
</resources> </resources>

View File

@ -15,32 +15,37 @@
*/ */
package androidx.media3.demo.transformer; 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.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.Context; import android.content.Context;
import android.opengl.EGL14; import android.opengl.EGL14;
import android.opengl.GLES20; import androidx.annotation.Nullable;
import android.util.Size; import androidx.media3.common.C;
import androidx.media3.common.util.ConditionVariable; import androidx.media3.common.FrameProcessingException;
import androidx.media3.common.util.GlProgram;
import androidx.media3.common.util.GlUtil;
import androidx.media3.common.util.LibraryLoader; import androidx.media3.common.util.LibraryLoader;
import androidx.media3.transformer.FrameProcessingException; import androidx.media3.common.util.Util;
import androidx.media3.transformer.SingleFrameGlTextureProcessor; import androidx.media3.effect.GlTextureProcessor;
import androidx.media3.effect.TextureInfo;
import com.google.mediapipe.components.FrameProcessor; import com.google.mediapipe.components.FrameProcessor;
import com.google.mediapipe.framework.AndroidAssetUtil;
import com.google.mediapipe.framework.AppTextureFrame; import com.google.mediapipe.framework.AppTextureFrame;
import com.google.mediapipe.framework.TextureFrame; import com.google.mediapipe.framework.TextureFrame;
import com.google.mediapipe.glutil.EglManager; import com.google.mediapipe.glutil.EglManager;
import java.io.IOException; import java.util.ArrayDeque;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull; 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. */
* Runs a MediaPipe graph on input frames. The implementation is currently limited to graphs that /* package */ final class MediaPipeProcessor implements GlTextureProcessor {
* can immediately produce one output frame per input frame.
*/ private static final String THREAD_NAME = "Demo:MediaPipeProcessor";
/* package */ final class MediaPipeProcessor implements SingleFrameGlTextureProcessor { private static final long RELEASE_WAIT_TIME_MS = 100;
private static final long RETRY_WAIT_TIME_MS = 1;
private static final LibraryLoader LOADER = private static final LibraryLoader LOADER =
new LibraryLoader("mediapipe_jni") { new LibraryLoader("mediapipe_jni") {
@ -60,116 +65,218 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
} }
} }
private static final String COPY_VERTEX_SHADER_NAME = "vertex_shader_copy_es2.glsl"; private final FrameProcessor frameProcessor;
private static final String COPY_FRAGMENT_SHADER_NAME = "shaders/fragment_shader_copy_es2.glsl"; private final ConcurrentHashMap<TextureInfo, TextureFrame> outputFrames;
private final boolean isSingleFrameGraph;
@Nullable private final ExecutorService singleThreadExecutorService;
private final Queue<Future<?>> futures;
private final String graphName; private InputListener inputListener;
private final String inputStreamName; private OutputListener outputListener;
private final String outputStreamName; private ErrorListener errorListener;
private final ConditionVariable frameProcessorConditionVariable; private boolean acceptedFrame;
private @MonotonicNonNull FrameProcessor frameProcessor;
private int inputWidth;
private int inputHeight;
private int inputTexId;
private @MonotonicNonNull GlProgram glProgram;
private @MonotonicNonNull TextureFrame outputFrame;
private @MonotonicNonNull RuntimeException frameProcessorPendingError;
/** /**
* Creates a new texture processor that wraps a MediaPipe graph. * Creates a new texture processor that wraps a MediaPipe graph.
* *
* <p>If {@code isSingleFrameGraph} is {@code false}, the {@code MediaPipeProcessor} may waste CPU
* time by continuously attempting to queue input frames to MediaPipe until they are accepted or
* waste memory if MediaPipe accepts and stores many frames internally.
*
* @param context The {@link Context}.
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
* @param graphName Name of a MediaPipe graph asset to load. * @param 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 inputStreamName Name of the input video stream in the graph.
* @param outputStreamName 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()); checkState(LOADER.isAvailable());
this.graphName = graphName; // TODO(b/227624622): Confirm whether MediaPipeProcessor could support HDR colors.
this.inputStreamName = inputStreamName; checkArgument(!useHdr, "MediaPipeProcessor does not support HDR colors.");
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);
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()); EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext());
frameProcessor = frameProcessor =
new FrameProcessor( new FrameProcessor(
context, eglManager.getNativeContext(), graphName, inputStreamName, outputStreamName); 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( frameProcessor.setConsumer(
frame -> { frame -> {
outputFrame = frame; TextureInfo texture =
frameProcessorConditionVariable.open(); new TextureInfo(
frame.getTextureName(),
/* fboId= */ C.INDEX_UNSET,
frame.getWidth(),
frame.getHeight());
outputFrames.put(texture, frame);
outputListener.onOutputFrameAvailable(texture, frame.getTimestamp());
}); });
}
@Override
public void setErrorListener(ErrorListener errorListener) {
this.errorListener = errorListener;
frameProcessor.setAsynchronousErrorListener( frameProcessor.setAsynchronousErrorListener(
error -> { error -> errorListener.onFrameProcessingError(new FrameProcessingException(error)));
frameProcessorPendingError = error;
frameProcessorConditionVariable.open();
});
} }
@Override @Override
public Size getOutputSize() { public void queueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
return new Size(inputWidth, inputHeight); AppTextureFrame appTextureFrame =
} new AppTextureFrame(inputTexture.texId, inputTexture.width, inputTexture.height);
// TODO(b/238302213): Handle timestamps restarting from 0 when applying effects to a playlist.
@Override // MediaPipe will fail if the timestamps are not monotonically increasing.
public void drawFrame(long presentationTimeUs) throws FrameProcessingException { // Also make sure that a MediaPipe graph producing additional frames only starts producing
frameProcessorConditionVariable.close(); // 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.
// Pass the input frame to MediaPipe.
AppTextureFrame appTextureFrame = new AppTextureFrame(inputTexId, inputWidth, inputHeight);
appTextureFrame.setTimestamp(presentationTimeUs); appTextureFrame.setTimestamp(presentationTimeUs);
checkStateNotNull(frameProcessor).onNewFrame(appTextureFrame); if (isSingleFrameGraph) {
boolean acceptedFrame = maybeQueueInputFrameSynchronous(appTextureFrame, inputTexture);
// Wait for output to be passed to the consumer. checkState(
try { acceptedFrame,
frameProcessorConditionVariable.block(); "queueInputFrame must only be called when a new input frame can be accepted");
} 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; return;
} }
if (frameProcessorPendingError != null) { // TODO(b/241782273): Avoid retrying continuously until the frame is accepted by using a
throw new FrameProcessingException(frameProcessorPendingError); // currently non-existent MediaPipe API to be notified when MediaPipe has capacity to accept a
// new frame.
queueInputFrameAsynchronous(appTextureFrame, inputTexture);
} }
// Copy from MediaPipe's output texture to the current output. private boolean maybeQueueInputFrameSynchronous(
AppTextureFrame appTextureFrame, TextureInfo inputTexture) {
acceptedFrame = false;
frameProcessor.onNewFrame(appTextureFrame);
try { try {
checkStateNotNull(glProgram).use(); appTextureFrame.waitUntilReleasedWithGpuSync();
glProgram.setSamplerTexIdUniform( } catch (InterruptedException e) {
"uTexSampler", checkStateNotNull(outputFrame).getTextureName(), /* texUnitIndex= */ 0); Thread.currentThread().interrupt();
glProgram.setBufferAttribute( errorListener.onFrameProcessingError(new FrameProcessingException(e));
"aFramePosition", }
GlUtil.getNormalizedCoordinateBounds(), if (acceptedFrame) {
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); inputListener.onInputFrameProcessed(inputTexture);
glProgram.bindAttributesAndUniforms(); }
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); return acceptedFrame;
GlUtil.checkGlError(); }
} catch (GlUtil.GlException e) {
throw new FrameProcessingException(e); private void queueInputFrameAsynchronous(
} finally { AppTextureFrame appTextureFrame, TextureInfo inputTexture) {
checkStateNotNull(outputFrame).release(); removeFinishedFutures();
futures.add(
checkStateNotNull(singleThreadExecutorService)
.submit(
() -> {
while (!maybeQueueInputFrameSynchronous(appTextureFrame, inputTexture)) {
try {
Thread.sleep(RETRY_WAIT_TIME_MS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
if (errorListener != null) {
errorListener.onFrameProcessingError(new FrameProcessingException(e));
}
}
}
inputListener.onReadyToAcceptInputFrame();
}));
}
@Override
public void releaseOutputFrame(TextureInfo outputTexture) {
checkStateNotNull(outputFrames.get(outputTexture)).release();
if (isSingleFrameGraph) {
inputListener.onReadyToAcceptInputFrame();
} }
} }
@Override @Override
public void release() { public void release() {
checkStateNotNull(frameProcessor).close(); if (isSingleFrameGraph) {
frameProcessor.close();
return;
}
Queue<Future<?>> futures = checkStateNotNull(this.futures);
while (!futures.isEmpty()) {
futures.remove().cancel(/* mayInterruptIfRunning= */ false);
}
ExecutorService singleThreadExecutorService =
checkStateNotNull(this.singleThreadExecutorService);
singleThreadExecutorService.shutdown();
try {
if (!singleThreadExecutorService.awaitTermination(RELEASE_WAIT_TIME_MS, MILLISECONDS)) {
errorListener.onFrameProcessingError(new FrameProcessingException("Release timed out"));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
errorListener.onFrameProcessingError(new FrameProcessingException(e));
}
frameProcessor.close();
}
@Override
public final void signalEndOfCurrentInputStream() {
if (isSingleFrameGraph) {
frameProcessor.waitUntilIdle();
outputListener.onCurrentOutputStreamEnded();
return;
}
removeFinishedFutures();
futures.add(
checkStateNotNull(singleThreadExecutorService)
.submit(
() -> {
frameProcessor.waitUntilIdle();
outputListener.onCurrentOutputStreamEnded();
}));
}
private void removeFinishedFutures() {
while (!futures.isEmpty()) {
if (!futures.element().isDone()) {
return;
}
try {
futures.remove().get();
} catch (ExecutionException e) {
errorListener.onFrameProcessingError(new FrameProcessingException(e));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
errorListener.onFrameProcessingError(new FrameProcessingException(e));
}
}
} }
} }

View File

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

View File

@ -47,6 +47,7 @@ import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.Log; import androidx.media3.common.util.Log;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import com.google.android.gms.cast.CastStatusCodes; import com.google.android.gms.cast.CastStatusCodes;
@ -82,6 +83,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@UnstableApi @UnstableApi
public final class CastPlayer extends BasePlayer { 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 { static {
MediaLibraryInfo.registerModule("media3.cast"); MediaLibraryInfo.registerModule("media3.cast");
} }
@ -723,16 +728,22 @@ public final class CastPlayer extends BasePlayer {
return VideoSize.UNKNOWN; 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}. */ /** This method is not supported and returns an empty {@link CueGroup}. */
@Override @Override
public CueGroup getCurrentCues() { 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 @Override
public DeviceInfo getDeviceInfo() { public DeviceInfo getDeviceInfo() {
return DeviceInfo.UNKNOWN; return DEVICE_INFO;
} }
/** This method is not supported and always returns {@code 0}. */ /** This method is not supported and always returns {@code 0}. */

View File

@ -62,6 +62,7 @@ import static org.mockito.MockitoAnnotations.initMocks;
import android.net.Uri; import android.net.Uri;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
import androidx.media3.common.MimeTypes; import androidx.media3.common.MimeTypes;
@ -1864,6 +1865,14 @@ public class CastPlayerTest {
verify(mockListener, never()).onMediaMetadataChanged(any()); 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) { private int[] createMediaQueueItemIds(int numberOfIds) {
int[] mediaQueueItemIds = new int[numberOfIds]; int[] mediaQueueItemIds = new int[numberOfIds];
for (int i = 0; i < numberOfIds; i++) { for (int i = 0; i < numberOfIds; i++) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,10 +28,23 @@ import java.lang.annotation.Target;
import java.util.Arrays; import java.util.Arrays;
import org.checkerframework.dataflow.qual.Pure; import org.checkerframework.dataflow.qual.Pure;
/** Stores color info. */ /**
* Stores color info.
*
* <p>When a {@code null} {@code ColorInfo} instance is used, this often represents a generic {@link
* #SDR_BT709_LIMITED} instance.
*/
@UnstableApi @UnstableApi
public final class ColorInfo implements Bundleable { 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 * 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 * 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 * 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. * C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,15 +13,34 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package androidx.media3.transformer; package androidx.media3.common;
import androidx.media3.common.C;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
/** Thrown when an exception occurs while applying effects to video frames. */ /** Thrown when an exception occurs while applying effects to video frames. */
@UnstableApi @UnstableApi
public final class FrameProcessingException extends Exception { 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 * The microsecond timestamp of the frame being processed while the exception occurred or {@link
* C#TIME_UNSET} if unknown. * C#TIME_UNSET} if unknown.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -91,11 +91,15 @@ public final class MimeTypes {
public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp"; public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp";
public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb"; public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb";
public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac"; public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac";
public static final String AUDIO_MIDI = BASE_TYPE_AUDIO + "/midi";
public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac"; public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm"; public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm";
public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg"; public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg";
public static final String AUDIO_WAV = BASE_TYPE_AUDIO + "/wav"; 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"; @UnstableApi public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown";
// text/ MIME types // text/ MIME types

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,19 @@
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@NonNullApi
package androidx.media3.common.audio;
import androidx.media3.common.util.NonNullApi;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,85 @@
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.common.util;
import static androidx.media3.common.util.Assertions.checkArgument;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
/** Immutable class for describing width and height dimensions in pixels. */
@UnstableApi
public final class Size {
/** A static instance to represent an unknown size value. */
public static final Size UNKNOWN =
new Size(/* width= */ C.LENGTH_UNSET, /* height= */ C.LENGTH_UNSET);
private final int width;
private final int height;
/**
* Creates a new immutable Size instance.
*
* @param width The width of the size, in pixels, or {@link C#LENGTH_UNSET} if unknown.
* @param height The height of the size, in pixels, or {@link C#LENGTH_UNSET} if unknown.
* @throws IllegalArgumentException if an invalid {@code width} or {@code height} is specified.
*/
public Size(int width, int height) {
checkArgument(
(width == C.LENGTH_UNSET || width >= 0) && (height == C.LENGTH_UNSET || height >= 0));
this.width = width;
this.height = height;
}
/** Returns the width of the size (in pixels), or {@link C#LENGTH_UNSET} if unknown. */
public int getWidth() {
return width;
}
/** Returns the height of the size (in pixels), or {@link C#LENGTH_UNSET} if unknown. */
public int getHeight() {
return height;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
if (this == obj) {
return true;
}
if (obj instanceof Size) {
Size other = (Size) obj;
return width == other.width && height == other.height;
}
return false;
}
@Override
public String toString() {
return width + "x" + height;
}
@Override
public int hashCode() {
// assuming most sizes are <2^16, doing a rotate will give us perfect hashing
return height ^ ((width << (Integer.SIZE / 2)) | (width >>> (Integer.SIZE / 2)));
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ public class CueGroupTest {
Cue bitmapCue = Cue bitmapCue =
new Cue.Builder().setBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)).build(); new Cue.Builder().setBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)).build();
ImmutableList<Cue> cues = ImmutableList.of(textCue, bitmapCue); ImmutableList<Cue> cues = ImmutableList.of(textCue, bitmapCue);
CueGroup cueGroup = new CueGroup(cues); CueGroup cueGroup = new CueGroup(cues, /* presentationTimeUs= */ 1_230_000);
Parcel parcel = Parcel.obtain(); Parcel parcel = Parcel.obtain();
try { try {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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