mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Merge pull request #210 from androidx/release-1.0.0-beta03
1.0.0-beta03
This commit is contained in:
commit
c2cbb6370a
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
1
.github/ISSUE_TEMPLATE/bug.yml
vendored
@ -17,6 +17,7 @@ body:
|
||||
label: Media3 Version
|
||||
description: What version of Media3 are you using?
|
||||
options:
|
||||
- 1.0.0-beta03
|
||||
- 1.0.0-beta02
|
||||
- 1.0.0-beta01
|
||||
- 1.0.0-alpha03
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -76,3 +76,6 @@ extensions/cronet/jniLibs/*
|
||||
!extensions/cronet/jniLibs/README.md
|
||||
extensions/cronet/libs/*
|
||||
!extensions/cronet/libs/README.md
|
||||
|
||||
# MIDI extension
|
||||
extensions/midi/lib
|
||||
|
@ -21,7 +21,7 @@ all of the information requested in the issue template.
|
||||
|
||||
## Pull requests
|
||||
|
||||
We will also consider high quality pull requests. These should normally merge
|
||||
We will also consider high quality pull requests. These should merge
|
||||
into the `main` branch. Before a pull request can be accepted you must submit
|
||||
a Contributor License Agreement, as described below.
|
||||
|
||||
|
147
RELEASENOTES.md
147
RELEASENOTES.md
@ -1,4 +1,145 @@
|
||||
Release notes
|
||||
Release notes
|
||||
|
||||
### 1.0.0-beta03 (2022-11-22)
|
||||
|
||||
This release corresponds to the
|
||||
[ExoPlayer 2.18.2 release](https://github.com/google/ExoPlayer/releases/tag/r2.18.2).
|
||||
|
||||
* Core library:
|
||||
* Add `ExoPlayer.isTunnelingEnabled` to check if tunneling is enabled for
|
||||
the currently selected tracks
|
||||
([#2518](https://github.com/google/ExoPlayer/issues/2518)).
|
||||
* Add `WrappingMediaSource` to simplify wrapping a single `MediaSource`
|
||||
([#7279](https://github.com/google/ExoPlayer/issues/7279)).
|
||||
* Discard back buffer before playback gets stuck due to insufficient
|
||||
available memory.
|
||||
* Close the Tracing "doSomeWork" block when offload is enabled.
|
||||
* Fix session tracking problem with fast seeks in `PlaybackStatsListener`
|
||||
([#180](https://github.com/androidx/media/issues/180)).
|
||||
* Send missing `onMediaItemTransition` callback when calling `seekToNext`
|
||||
or `seekToPrevious` in a single-item playlist
|
||||
([#10667](https://github.com/google/ExoPlayer/issues/10667)).
|
||||
* Add `Player.getSurfaceSize` that returns the size of the surface on
|
||||
which the video is rendered.
|
||||
* Fix bug where removing listeners during the player release can cause an
|
||||
`IllegalStateException`
|
||||
([#10758](https://github.com/google/ExoPlayer/issues/10758)).
|
||||
* Build:
|
||||
* Enforce minimum `compileSdkVersion` to avoid compilation errors
|
||||
([#10684](https://github.com/google/ExoPlayer/issues/10684)).
|
||||
* Avoid publishing block when included in another gradle build.
|
||||
* Track selection:
|
||||
* Prefer other tracks to Dolby Vision if display does not support it.
|
||||
([#8944](https://github.com/google/ExoPlayer/issues/8944)).
|
||||
* Downloads:
|
||||
* Fix potential infinite loop in `ProgressiveDownloader` caused by
|
||||
simultaneous download and playback with the same `PriorityTaskManager`
|
||||
([#10570](https://github.com/google/ExoPlayer/pull/10570)).
|
||||
* Make download notification appear immediately
|
||||
([#183](https://github.com/androidx/media/pull/183)).
|
||||
* Limit parallel download removals to 1 to avoid excessive thread creation
|
||||
([#10458](https://github.com/google/ExoPlayer/issues/10458)).
|
||||
* Video:
|
||||
* Try alternative decoder for Dolby Vision if display does not support it.
|
||||
([#9794](https://github.com/google/ExoPlayer/issues/9794)).
|
||||
* Audio:
|
||||
* Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid
|
||||
OutOfMemory errors when releasing multiple players at the same time
|
||||
([#10057](https://github.com/google/ExoPlayer/issues/10057)).
|
||||
* Adds `AudioOffloadListener.onExperimentalOffloadedPlayback` for the
|
||||
AudioTrack offload state.
|
||||
([#134](https://github.com/androidx/media/issues/134)).
|
||||
* Make `AudioTrackBufferSizeProvider` a public interface.
|
||||
* Add `ExoPlayer.setPreferredAudioDevice` to set the preferred audio
|
||||
output device ([#135](https://github.com/androidx/media/issues/135)).
|
||||
* Rename `androidx.media3.exoplayer.audio.AudioProcessor` to
|
||||
`androidx.media3.common.audio.AudioProcessor`.
|
||||
* Map 8-channel and 12-channel audio to the 7.1 and 7.1.4 channel masks
|
||||
respectively on all Android versions
|
||||
([#10701](https://github.com/google/ExoPlayer/issues/10701)).
|
||||
* Metadata:
|
||||
* `MetadataRenderer` can now be configured to render metadata as soon as
|
||||
they are available. Create an instance with
|
||||
`MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory,
|
||||
boolean)` to specify whether the renderer will output metadata early or
|
||||
in sync with the player position.
|
||||
* DRM:
|
||||
* Work around a bug in the Android 13 ClearKey implementation that returns
|
||||
a non-empty but invalid license URL.
|
||||
* Fix `setMediaDrmSession failed: session not opened` error when switching
|
||||
between DRM schemes in a playlist (e.g. Widevine to ClearKey).
|
||||
* Text:
|
||||
* CEA-608: Ensure service switch commands on field 2 are handled correctly
|
||||
([#10666](https://github.com/google/ExoPlayer/issues/10666)).
|
||||
* DASH:
|
||||
* Parse `EventStream.presentationTimeOffset` from manifests
|
||||
([#10460](https://github.com/google/ExoPlayer/issues/10460)).
|
||||
* UI:
|
||||
* Use current overrides of the player as preset in
|
||||
`TrackSelectionDialogBuilder`
|
||||
([#10429](https://github.com/google/ExoPlayer/issues/10429)).
|
||||
* Session:
|
||||
* Ensure commands are always executed in the correct order even if some
|
||||
require asynchronous resolution
|
||||
([#85](https://github.com/androidx/media/issues/85)).
|
||||
* Add `DefaultMediaNotificationProvider.Builder` to build
|
||||
`DefaultMediaNotificationProvider` instances. The builder can configure
|
||||
the notification ID, the notification channel ID and the notification
|
||||
channel name used by the provider. Also, add method
|
||||
`DefaultMediaNotificationProvider.setSmallIcon(int)` to set the
|
||||
notifications small icon.
|
||||
([#104](https://github.com/androidx/media/issues/104)).
|
||||
* Ensure commands sent before `MediaController.release()` are not dropped
|
||||
([#99](https://github.com/androidx/media/issues/99)).
|
||||
* `SimpleBitmapLoader` can load bitmap from `file://` URIs
|
||||
([#108](https://github.com/androidx/media/issues/108)).
|
||||
* Fix assertion that prevents `MediaController` to seek over an ad in a
|
||||
period ([#122](https://github.com/androidx/media/issues/122)).
|
||||
* When playback ends, the `MediaSessionService` is stopped from the
|
||||
foreground and a notification is shown to restart playback of the last
|
||||
played media item
|
||||
([#112](https://github.com/androidx/media/issues/112)).
|
||||
* Don't start a foreground service with a pending intent for pause
|
||||
([#167](https://github.com/androidx/media/issues/167)).
|
||||
* Manually hide the 'badge' associated with the notification created by
|
||||
`DefaultNotificationProvider` on API 26 and API 27 (the badge is
|
||||
automatically hidden on API 28+)
|
||||
([#131](https://github.com/androidx/media/issues/131)).
|
||||
* Fix bug where a second binder connection from a legacy MediaSession to a
|
||||
Media3 MediaController causes IllegalStateExceptions
|
||||
([#49](https://github.com/androidx/media/issues/49)).
|
||||
* RTSP:
|
||||
* Add H263 fragmented packet handling
|
||||
([#119](https://github.com/androidx/media/pull/119)).
|
||||
* Add support for MP4A-LATM
|
||||
([#162](https://github.com/androidx/media/pull/162)).
|
||||
* IMA:
|
||||
* Add timeout for loading ad information to handle cases where the IMA SDK
|
||||
gets stuck loading an ad
|
||||
([#10510](https://github.com/google/ExoPlayer/issues/10510)).
|
||||
* Prevent skipping mid-roll ads when seeking to the end of the content
|
||||
([#10685](https://github.com/google/ExoPlayer/issues/10685)).
|
||||
* Correctly calculate window duration for live streams with server-side
|
||||
inserted ads, for example IMA DAI
|
||||
([#10764](https://github.com/google/ExoPlayer/issues/10764)).
|
||||
* FFmpeg extension:
|
||||
* Add newly required flags to link FFmpeg libraries with NDK 23.1.7779620
|
||||
and above ([#9933](https://github.com/google/ExoPlayer/issues/9933)).
|
||||
* AV1 extension:
|
||||
* Update CMake version to avoid incompatibilities with the latest Android
|
||||
Studio releases
|
||||
([#9933](https://github.com/google/ExoPlayer/issues/9933)).
|
||||
* Cast extension:
|
||||
* Implement `getDeviceInfo()` to be able to identify `CastPlayer` when
|
||||
controlling playback with a `MediaController`
|
||||
([#142](https://github.com/androidx/media/issues/142)).
|
||||
* Transformer:
|
||||
* Add muxer watchdog timer to detect when generating an output sample is
|
||||
too slow.
|
||||
* Remove deprecated symbols:
|
||||
* Remove `Transformer.Builder.setOutputMimeType(String)`. This feature has
|
||||
been removed. The MIME type will always be MP4 when the default muxer is
|
||||
used.
|
||||
|
||||
### 1.0.0-beta02 (2022-07-21)
|
||||
|
||||
@ -32,6 +173,8 @@ This release corresponds to the
|
||||
* RTSP:
|
||||
* Add VP8 fragmented packet handling
|
||||
([#110](https://github.com/androidx/media/pull/110)).
|
||||
* Support frames/fragments in VP9
|
||||
([#115](https://github.com/androidx/media/pull/115)).
|
||||
* Leanback extension:
|
||||
* Listen to `playWhenReady` changes in `LeanbackAdapter`
|
||||
([10420](https://github.com/google/ExoPlayer/issues/10420)).
|
||||
@ -266,6 +409,8 @@ This release corresponds to the
|
||||
`DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT` otherwise.
|
||||
* Remove constructor `DefaultTrackSelector(ExoTrackSelection.Factory)`.
|
||||
Use `DefaultTrackSelector(Context, ExoTrackSelection.Factory)` instead.
|
||||
* Remove `Transformer.Builder.setContext`. The `Context` should be passed
|
||||
to the `Transformer.Builder` constructor instead.
|
||||
|
||||
### 1.0.0-alpha03 (2022-03-14)
|
||||
|
||||
|
@ -22,6 +22,9 @@ android {
|
||||
targetSdkVersion project.ext.targetSdkVersion
|
||||
consumerProguardFiles 'proguard-rules.txt'
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
aarMetadata {
|
||||
minCompileSdk = project.ext.compileSdkVersion
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
|
@ -12,15 +12,18 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
releaseVersion = '1.0.0-beta02'
|
||||
releaseVersionCode = 1_000_000_1_02
|
||||
releaseVersion = '1.0.0-beta03'
|
||||
releaseVersionCode = 1_000_000_1_03
|
||||
minSdkVersion = 16
|
||||
appTargetSdkVersion = 29
|
||||
appTargetSdkVersion = 33
|
||||
// API version before restricting local file access.
|
||||
// https://developer.android.com/training/data-storage/app-specific
|
||||
mainDemoAppTargetSdkVersion = 29
|
||||
// Upgrading this requires [Internal ref: b/193254928] to be fixed, or some
|
||||
// additional robolectric config.
|
||||
targetSdkVersion = 30
|
||||
compileSdkVersion = 32
|
||||
dexmakerVersion = '2.28.1'
|
||||
compileSdkVersion = 33
|
||||
dexmakerVersion = '2.28.3'
|
||||
junitVersion = '4.13.2'
|
||||
// Use the same Guava version as the Android repo:
|
||||
// https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA
|
||||
@ -40,7 +43,7 @@ project.ext {
|
||||
androidxConstraintLayoutVersion = '2.0.4'
|
||||
androidxCoreVersion = '1.7.0'
|
||||
androidxFuturesVersion = '1.1.0'
|
||||
androidxMediaVersion = '1.4.3'
|
||||
androidxMediaVersion = '1.6.0'
|
||||
androidxMedia2Version = '1.2.0'
|
||||
androidxMultidexVersion = '2.0.1'
|
||||
androidxRecyclerViewVersion = '1.2.1'
|
||||
|
@ -78,6 +78,9 @@ project(modulePrefix + 'lib-extractor').projectDir = new File(rootDir, 'librarie
|
||||
include modulePrefix + 'lib-cast'
|
||||
project(modulePrefix + 'lib-cast').projectDir = new File(rootDir, 'libraries/cast')
|
||||
|
||||
include modulePrefix + 'lib-effect'
|
||||
project(modulePrefix + 'lib-effect').projectDir = new File(rootDir, 'libraries/effect')
|
||||
|
||||
include modulePrefix + 'lib-transformer'
|
||||
project(modulePrefix + 'lib-transformer').projectDir = new File(rootDir, 'libraries/transformer')
|
||||
|
||||
|
@ -22,8 +22,13 @@
|
||||
|
||||
<uses-sdk/>
|
||||
|
||||
<application android:label="@string/application_name" android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true" android:allowBackup="false">
|
||||
<application
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
android:label="@string/application_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:largeHeap="true"
|
||||
android:allowBackup="false"
|
||||
android:taskAffinity="">
|
||||
|
||||
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||
android:value="androidx.media3.cast.DefaultCastOptionsProvider"/>
|
||||
|
@ -52,6 +52,7 @@ dependencies {
|
||||
implementation project(modulePrefix + 'lib-exoplayer-smoothstreaming')
|
||||
implementation project(modulePrefix + 'lib-ui')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
}
|
||||
|
@ -22,6 +22,7 @@
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/application_name">
|
||||
|
@ -29,6 +29,7 @@ import android.opengl.GLUtils;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.GlProgram;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.common.util.Log;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import javax.microedition.khronos.opengles.GL10;
|
||||
@ -41,6 +42,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
/* package */ final class BitmapOverlayVideoProcessor
|
||||
implements VideoProcessingGLSurfaceView.VideoProcessor {
|
||||
|
||||
private static final String TAG = "BitmapOverlayVP";
|
||||
private static final int OVERLAY_WIDTH = 512;
|
||||
private static final int OVERLAY_HEIGHT = 256;
|
||||
|
||||
@ -85,6 +87,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
/* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl");
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (GlUtil.GlException e) {
|
||||
Log.e(TAG, "Failed to initialize the shader program", e);
|
||||
return;
|
||||
}
|
||||
program.setBufferAttribute(
|
||||
"aFramePosition",
|
||||
@ -119,7 +124,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);
|
||||
GLUtils.texSubImage2D(
|
||||
GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap);
|
||||
GlUtil.checkGlError();
|
||||
try {
|
||||
GlUtil.checkGlError();
|
||||
} catch (GlUtil.GlException e) {
|
||||
Log.e(TAG, "Failed to populate the texture", e);
|
||||
}
|
||||
|
||||
// Run the shader program.
|
||||
GlProgram program = checkNotNull(this.program);
|
||||
@ -128,16 +137,28 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
program.setFloatUniform("uScaleX", bitmapScaleX);
|
||||
program.setFloatUniform("uScaleY", bitmapScaleY);
|
||||
program.setFloatsUniform("uTexTransform", transformMatrix);
|
||||
program.bindAttributesAndUniforms();
|
||||
try {
|
||||
program.bindAttributesAndUniforms();
|
||||
} catch (GlUtil.GlException e) {
|
||||
Log.e(TAG, "Failed to update the shader program", e);
|
||||
}
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
GlUtil.checkGlError();
|
||||
try {
|
||||
GlUtil.checkGlError();
|
||||
} catch (GlUtil.GlException e) {
|
||||
Log.e(TAG, "Failed to draw a frame", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (program != null) {
|
||||
program.delete();
|
||||
try {
|
||||
program.delete();
|
||||
} catch (GlUtil.GlException e) {
|
||||
Log.e(TAG, "Failed to delete the shader program", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package androidx.media3.demo.gl;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
@ -83,7 +85,8 @@ public final class MainActivity extends Activity {
|
||||
VideoProcessingGLSurfaceView videoProcessingGLSurfaceView =
|
||||
new VideoProcessingGLSurfaceView(
|
||||
context, requestSecureSurface, new BitmapOverlayVideoProcessor(context));
|
||||
FrameLayout contentFrame = findViewById(R.id.exo_content_frame);
|
||||
checkNotNull(playerView);
|
||||
FrameLayout contentFrame = playerView.findViewById(R.id.exo_content_frame);
|
||||
contentFrame.addView(videoProcessingGLSurfaceView);
|
||||
this.videoProcessingGLSurfaceView = videoProcessingGLSurfaceView;
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.TimedValueQueue;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.video.VideoFrameMetadataListener;
|
||||
@ -70,6 +71,7 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
||||
}
|
||||
|
||||
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
|
||||
private static final String TAG = "VPGlSurfaceView";
|
||||
|
||||
private final VideoRenderer renderer;
|
||||
private final Handler mainHandler;
|
||||
@ -239,7 +241,11 @@ public final class VideoProcessingGLSurfaceView extends GLSurfaceView {
|
||||
|
||||
@Override
|
||||
public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) {
|
||||
texture = GlUtil.createExternalTexture();
|
||||
try {
|
||||
texture = GlUtil.createExternalTexture();
|
||||
} catch (GlUtil.GlException e) {
|
||||
Log.e(TAG, "Failed to create an external texture", e);
|
||||
}
|
||||
surfaceTexture = new SurfaceTexture(texture);
|
||||
surfaceTexture.setOnFrameAvailableListener(
|
||||
surfaceTexture -> {
|
||||
|
@ -11,6 +11,7 @@
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
apply from: '../../constants.gradle'
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
@ -26,7 +27,9 @@ android {
|
||||
versionName project.ext.releaseVersion
|
||||
versionCode project.ext.releaseVersionCode
|
||||
minSdkVersion project.ext.minSdkVersion
|
||||
targetSdkVersion project.ext.appTargetSdkVersion
|
||||
// Not using appTargetSDKVersion to allow local file access on API 29
|
||||
// and higher [Internal ref: b/191644662]
|
||||
targetSdkVersion project.ext.mainDemoAppTargetSdkVersion
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
|
@ -399,7 +399,7 @@
|
||||
"uri": "ssai://dai.google.com/?contentSourceId=2528370&videoId=tears-of-steel&format=2&adsId=1"
|
||||
},
|
||||
{
|
||||
"name": "HLS Live: Big Buck Bunny (mid), 3 ads each [10 s]",
|
||||
"name": "HLS Live: Big Buck Bunny (mid), 3 ads [10/10/10s]",
|
||||
"uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3"
|
||||
},
|
||||
{
|
||||
|
@ -14,6 +14,7 @@
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="androidx.media3.demo.session">
|
||||
|
||||
<uses-sdk/>
|
||||
@ -21,10 +22,12 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.Media3Demo">
|
||||
android:theme="@style/Theme.Media3Demo"
|
||||
tools:replace="android:name">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
|
@ -13,10 +13,25 @@
|
||||
"id": "video_02",
|
||||
"title": "TTML Netflix Japanese examples (IMSC1.1)",
|
||||
"source": "https://storage.googleapis.com/exoplayer-test-media-1/gen-3/screens/dash-vod-single-segment/video-avc-baseline-480.mp4",
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_japanese_ttml.xml",
|
||||
"album": "Video with subtitle",
|
||||
"artist": "Netflix",
|
||||
"artist": "Subtitles",
|
||||
"genre": "Video",
|
||||
"image": "https://cdn.pixabay.com/photo/2014/10/09/13/14/video-481821_960_720.png",
|
||||
"subtitles": [
|
||||
{
|
||||
"subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/netflix_japanese_ttml.xml",
|
||||
"subtitle_mime_type": "application/ttml+xml",
|
||||
"subtitle_lang": "ja"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "video_03",
|
||||
"title": "MPEG-4 Timed Text",
|
||||
"album": "Video with subtitle",
|
||||
"artist": "Subtitles",
|
||||
"genre": "Video",
|
||||
"source": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4",
|
||||
"image": "https://cdn.pixabay.com/photo/2014/10/09/13/14/video-481821_960_720.png"
|
||||
},
|
||||
{
|
||||
|
@ -34,7 +34,6 @@ import androidx.media3.session.MediaBrowser
|
||||
import androidx.media3.session.SessionToken
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
|
||||
@ -105,7 +104,7 @@ class MainActivity : AppCompatActivity() {
|
||||
SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
||||
)
|
||||
.buildAsync()
|
||||
browserFuture.addListener({ pushRoot() }, MoreExecutors.directExecutor())
|
||||
browserFuture.addListener({ pushRoot() }, ContextCompat.getMainExecutor(this))
|
||||
}
|
||||
|
||||
private fun releaseBrowser() {
|
||||
@ -132,7 +131,7 @@ class MainActivity : AppCompatActivity() {
|
||||
subItemMediaList.addAll(children)
|
||||
mediaListAdapter.notifyDataSetChanged()
|
||||
},
|
||||
MoreExecutors.directExecutor()
|
||||
ContextCompat.getMainExecutor(this)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -18,10 +18,13 @@ package androidx.media3.demo.session
|
||||
import android.content.res.AssetManager
|
||||
import android.net.Uri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaItem.SubtitleConfiguration
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ALBUMS
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_ARTISTS
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_GENRES
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
||||
import androidx.media3.common.util.Util
|
||||
import com.google.common.collect.ImmutableList
|
||||
import org.json.JSONObject
|
||||
@ -65,13 +68,13 @@ object MediaItemTree {
|
||||
mediaId: String,
|
||||
isPlayable: Boolean,
|
||||
@MediaMetadata.FolderType folderType: Int,
|
||||
subtitleConfigurations: List<SubtitleConfiguration> = mutableListOf(),
|
||||
album: String? = null,
|
||||
artist: String? = null,
|
||||
genre: String? = null,
|
||||
sourceUri: Uri? = null,
|
||||
imageUri: Uri? = null,
|
||||
imageUri: Uri? = null
|
||||
): MediaItem {
|
||||
// TODO(b/194280027): add artwork
|
||||
val metadata =
|
||||
MediaMetadata.Builder()
|
||||
.setAlbumTitle(album)
|
||||
@ -82,8 +85,10 @@ object MediaItemTree {
|
||||
.setIsPlayable(isPlayable)
|
||||
.setArtworkUri(imageUri)
|
||||
.build()
|
||||
|
||||
return MediaItem.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setSubtitleConfigurations(subtitleConfigurations)
|
||||
.setMediaMetadata(metadata)
|
||||
.setUri(sourceUri)
|
||||
.build()
|
||||
@ -156,6 +161,19 @@ object MediaItemTree {
|
||||
val title = mediaObject.getString("title")
|
||||
val artist = mediaObject.getString("artist")
|
||||
val genre = mediaObject.getString("genre")
|
||||
val subtitleConfigurations: MutableList<SubtitleConfiguration> = mutableListOf()
|
||||
if (mediaObject.has("subtitles")) {
|
||||
val subtitlesJson = mediaObject.getJSONArray("subtitles")
|
||||
for (i in 0 until subtitlesJson.length()) {
|
||||
val subtitleObject = subtitlesJson.getJSONObject(i)
|
||||
subtitleConfigurations.add(
|
||||
SubtitleConfiguration.Builder(Uri.parse(subtitleObject.getString("subtitle_uri")))
|
||||
.setMimeType(subtitleObject.getString("subtitle_mime_type"))
|
||||
.setLanguage(subtitleObject.getString("subtitle_lang"))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
val sourceUri = Uri.parse(mediaObject.getString("source"))
|
||||
val imageUri = Uri.parse(mediaObject.getString("image"))
|
||||
// key of such items in tree
|
||||
@ -170,12 +188,13 @@ object MediaItemTree {
|
||||
title = title,
|
||||
mediaId = idInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_NONE,
|
||||
subtitleConfigurations,
|
||||
album = album,
|
||||
artist = artist,
|
||||
genre = genre,
|
||||
sourceUri = sourceUri,
|
||||
imageUri = imageUri,
|
||||
folderType = FOLDER_TYPE_NONE
|
||||
imageUri = imageUri
|
||||
)
|
||||
)
|
||||
|
||||
@ -188,7 +207,8 @@ object MediaItemTree {
|
||||
title = album,
|
||||
mediaId = albumFolderIdInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS
|
||||
folderType = FOLDER_TYPE_ALBUMS,
|
||||
subtitleConfigurations
|
||||
)
|
||||
)
|
||||
treeNodes[ALBUM_ID]!!.addChild(albumFolderIdInTree)
|
||||
@ -203,7 +223,8 @@ object MediaItemTree {
|
||||
title = artist,
|
||||
mediaId = artistFolderIdInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS
|
||||
folderType = FOLDER_TYPE_ARTISTS,
|
||||
subtitleConfigurations
|
||||
)
|
||||
)
|
||||
treeNodes[ARTIST_ID]!!.addChild(artistFolderIdInTree)
|
||||
@ -218,7 +239,8 @@ object MediaItemTree {
|
||||
title = genre,
|
||||
mediaId = genreFolderIdInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS
|
||||
folderType = FOLDER_TYPE_GENRES,
|
||||
subtitleConfigurations
|
||||
)
|
||||
)
|
||||
treeNodes[GENRE_ID]!!.addChild(genreFolderIdInTree)
|
||||
|
@ -30,6 +30,7 @@ import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.session.MediaBrowser
|
||||
@ -38,7 +39,6 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu
|
||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.MoreExecutors
|
||||
|
||||
class PlayableFolderActivity : AppCompatActivity() {
|
||||
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
|
||||
@ -69,10 +69,13 @@ class PlayableFolderActivity : AppCompatActivity() {
|
||||
mediaList.setOnItemClickListener { _, _, position, _ ->
|
||||
run {
|
||||
val browser = this.browser ?: return@run
|
||||
browser.setMediaItems(subItemMediaList)
|
||||
browser.setMediaItems(
|
||||
subItemMediaList,
|
||||
/* startIndex= */ position,
|
||||
/* startPositionMs= */ C.TIME_UNSET
|
||||
)
|
||||
browser.shuffleModeEnabled = false
|
||||
browser.prepare()
|
||||
browser.seekToDefaultPosition(/* windowIndex= */ position)
|
||||
browser.play()
|
||||
val intent = Intent(this, PlayerActivity::class.java)
|
||||
startActivity(intent)
|
||||
@ -132,7 +135,7 @@ class PlayableFolderActivity : AppCompatActivity() {
|
||||
SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
||||
)
|
||||
.buildAsync()
|
||||
browserFuture.addListener({ displayFolder() }, MoreExecutors.directExecutor())
|
||||
browserFuture.addListener({ displayFolder() }, ContextCompat.getMainExecutor(this))
|
||||
}
|
||||
|
||||
private fun releaseBrowser() {
|
||||
|
@ -29,9 +29,11 @@ import android.widget.ListView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.C.TRACK_TYPE_TEXT
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.session.MediaController
|
||||
import androidx.media3.session.SessionToken
|
||||
import androidx.media3.ui.PlayerView
|
||||
@ -147,6 +149,10 @@ class PlayerActivity : AppCompatActivity() {
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
updateRepeatSwitchUI(repeatMode)
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
playerView.setShowSubtitleButton(tracks.isTypeSupported(TRACK_TYPE_TEXT))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ android {
|
||||
compileSdkVersion project.ext.compileSdkVersion
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
@ -76,11 +77,14 @@ dependencies {
|
||||
implementation 'androidx.constraintlayout:constraintlayout:' + androidxConstraintLayoutVersion
|
||||
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
implementation 'com.google.android.material:material:' + androidxMaterialVersion
|
||||
implementation project(modulePrefix + 'lib-effect')
|
||||
implementation project(modulePrefix + 'lib-exoplayer')
|
||||
implementation project(modulePrefix + 'lib-exoplayer-dash')
|
||||
implementation project(modulePrefix + 'lib-transformer')
|
||||
implementation project(modulePrefix + 'lib-ui')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
// For MediaPipe and its dependencies:
|
||||
withMediaPipeImplementation fileTree(dir: 'libs', include: ['*.aar'])
|
||||
withMediaPipeImplementation 'com.google.flogger:flogger:latest.release'
|
||||
|
@ -29,6 +29,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/Theme.AppCompat"
|
||||
android:taskAffinity=""
|
||||
android:requestLegacyExternalStorage="true"
|
||||
tools:targetApi="29">
|
||||
<activity android:name=".ConfigurationActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package androidx.media3.demo.transformer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
@ -27,15 +28,14 @@ import android.graphics.Paint;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLUtils;
|
||||
import android.util.Size;
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.FrameProcessingException;
|
||||
import androidx.media3.common.util.GlProgram;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.transformer.FrameProcessingException;
|
||||
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
|
||||
import androidx.media3.effect.SingleFrameGlTextureProcessor;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* A {@link SingleFrameGlTextureProcessor} that overlays a bitmap with a logo and timer on each
|
||||
@ -45,10 +45,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
*/
|
||||
// TODO(b/227625365): Delete this class and use a texture processor from the Transformer library,
|
||||
// once overlaying a bitmap and text is supported in Transformer.
|
||||
/* package */ final class BitmapOverlayProcessor implements SingleFrameGlTextureProcessor {
|
||||
static {
|
||||
GlUtil.glAssertionsEnabled = true;
|
||||
}
|
||||
/* package */ final class BitmapOverlayProcessor extends SingleFrameGlTextureProcessor {
|
||||
|
||||
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
|
||||
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl";
|
||||
@ -57,16 +54,25 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
private final Paint paint;
|
||||
private final Bitmap overlayBitmap;
|
||||
private final Bitmap logoBitmap;
|
||||
private final Canvas overlayCanvas;
|
||||
private final GlProgram glProgram;
|
||||
|
||||
private float bitmapScaleX;
|
||||
private float bitmapScaleY;
|
||||
private int bitmapTexId;
|
||||
private @MonotonicNonNull Size outputSize;
|
||||
private @MonotonicNonNull Bitmap logoBitmap;
|
||||
private @MonotonicNonNull GlProgram glProgram;
|
||||
|
||||
public BitmapOverlayProcessor() {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param context The {@link Context}.
|
||||
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
|
||||
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
|
||||
* @throws FrameProcessingException If a problem occurs while reading shader files.
|
||||
*/
|
||||
public BitmapOverlayProcessor(Context context, boolean useHdr) throws FrameProcessingException {
|
||||
super(useHdr);
|
||||
checkArgument(!useHdr, "BitmapOverlayProcessor does not support HDR colors.");
|
||||
paint = new Paint();
|
||||
paint.setTextSize(64);
|
||||
paint.setAntiAlias(true);
|
||||
@ -75,19 +81,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
overlayBitmap =
|
||||
Bitmap.createBitmap(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT, Bitmap.Config.ARGB_8888);
|
||||
overlayCanvas = new Canvas(overlayBitmap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
|
||||
throws IOException {
|
||||
if (inputWidth > inputHeight) {
|
||||
bitmapScaleX = inputWidth / (float) inputHeight;
|
||||
bitmapScaleY = 1f;
|
||||
} else {
|
||||
bitmapScaleX = 1f;
|
||||
bitmapScaleY = inputHeight / (float) inputWidth;
|
||||
}
|
||||
outputSize = new Size(inputWidth, inputHeight);
|
||||
|
||||
try {
|
||||
logoBitmap =
|
||||
@ -97,30 +90,46 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
bitmapTexId = GlUtil.createTexture(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT);
|
||||
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
|
||||
try {
|
||||
bitmapTexId =
|
||||
GlUtil.createTexture(
|
||||
BITMAP_WIDTH_HEIGHT,
|
||||
BITMAP_WIDTH_HEIGHT,
|
||||
/* useHighPrecisionColorComponents= */ false);
|
||||
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
|
||||
|
||||
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
|
||||
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
|
||||
} catch (GlUtil.GlException | IOException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
}
|
||||
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
|
||||
glProgram.setBufferAttribute(
|
||||
"aFramePosition",
|
||||
GlUtil.getNormalizedCoordinateBounds(),
|
||||
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
|
||||
if (inputWidth > inputHeight) {
|
||||
bitmapScaleX = inputWidth / (float) inputHeight;
|
||||
bitmapScaleY = 1f;
|
||||
} else {
|
||||
bitmapScaleX = 1f;
|
||||
bitmapScaleY = inputHeight / (float) inputWidth;
|
||||
}
|
||||
|
||||
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
|
||||
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
|
||||
|
||||
return Pair.create(inputWidth, inputHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Size getOutputSize() {
|
||||
return checkStateNotNull(outputSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
|
||||
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
|
||||
try {
|
||||
checkStateNotNull(glProgram).use();
|
||||
glProgram.use();
|
||||
|
||||
// Draw to the canvas and store it in a texture.
|
||||
String text =
|
||||
@ -137,19 +146,23 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
flipBitmapVertically(overlayBitmap));
|
||||
GlUtil.checkGlError();
|
||||
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
|
||||
glProgram.bindAttributesAndUniforms();
|
||||
// The four-vertex triangle strip forms a quad.
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
GlUtil.checkGlError();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
throw new FrameProcessingException(e, presentationTimeUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (glProgram != null) {
|
||||
public void release() throws FrameProcessingException {
|
||||
super.release();
|
||||
try {
|
||||
glProgram.delete();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,9 +18,11 @@ package androidx.media3.demo.transformer;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
@ -29,9 +31,14 @@ import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
import androidx.media3.common.util.Util;
|
||||
@ -59,14 +66,28 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
public static final String TRIM_START_MS = "trim_start_ms";
|
||||
public static final String TRIM_END_MS = "trim_end_ms";
|
||||
public static final String ENABLE_FALLBACK = "enable_fallback";
|
||||
public static final String ENABLE_DEBUG_PREVIEW = "enable_debug_preview";
|
||||
public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping";
|
||||
public static final String FORCE_INTERPRET_HDR_VIDEO_AS_SDR = "force_interpret_hdr_video_as_sdr";
|
||||
public static final String ENABLE_HDR_EDITING = "enable_hdr_editing";
|
||||
public static final String DEMO_EFFECTS_SELECTIONS = "demo_effects_selections";
|
||||
public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x";
|
||||
public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y";
|
||||
public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius";
|
||||
public static final String PERIODIC_VIGNETTE_OUTER_RADIUS = "periodic_vignette_outer_radius";
|
||||
private static final String[] INPUT_URIS = {
|
||||
public static final String COLOR_FILTER_SELECTION = "color_filter_selection";
|
||||
public static final String CONTRAST_VALUE = "contrast_value";
|
||||
public static final String RGB_ADJUSTMENT_RED_SCALE = "rgb_adjustment_red_scale";
|
||||
public static final String RGB_ADJUSTMENT_GREEN_SCALE = "rgb_adjustment_green_scale";
|
||||
public static final String RGB_ADJUSTMENT_BLUE_SCALE = "rgb_adjustment_blue_scale";
|
||||
public static final String HSL_ADJUSTMENTS_HUE = "hsl_adjustments_hue";
|
||||
public static final String HSL_ADJUSTMENTS_SATURATION = "hsl_adjustments_saturation";
|
||||
public static final String HSL_ADJUSTMENTS_LIGHTNESS = "hsl_adjustments_lightness";
|
||||
public static final int COLOR_FILTER_GRAYSCALE = 0;
|
||||
public static final int COLOR_FILTER_INVERTED = 1;
|
||||
public static final int COLOR_FILTER_SEPIA = 2;
|
||||
public static final int FILE_PERMISSION_REQUEST_CODE = 1;
|
||||
private static final String[] PRESET_FILE_URIS = {
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4",
|
||||
"https://html5demos.com/assets/dizzy.mp4",
|
||||
@ -79,9 +100,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/gen/screens/dash-vod-single-segment/manifest-baseline.mpd",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-hdr-hdr10.mp4",
|
||||
"https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4",
|
||||
};
|
||||
private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS
|
||||
private static final String[] PRESET_FILE_URI_DESCRIPTIONS = { // same order as PRESET_FILE_URIS
|
||||
"720p H264 video and AAC audio",
|
||||
"1080p H265 video and AAC audio",
|
||||
"360p H264 video and AAC audio",
|
||||
@ -94,21 +115,32 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"H264 video and AAC audio (portrait, H < W, 90\u00B0)",
|
||||
"SEF slow motion with 240 fps",
|
||||
"480p DASH (non-square pixels)",
|
||||
"HDR (HDR10) H265 video (encoding may fail)",
|
||||
"HDR (HDR10) H265 limited range video (encoding may fail)",
|
||||
};
|
||||
private static final String[] DEMO_EFFECTS = {
|
||||
"Dizzy crop",
|
||||
"Edge detector (Media Pipe)",
|
||||
"Color filters",
|
||||
"Map White to Green Color Lookup Table",
|
||||
"RGB Adjustments",
|
||||
"HSL Adjustments",
|
||||
"Contrast",
|
||||
"Periodic vignette",
|
||||
"3D spin",
|
||||
"Overlay logo & timer",
|
||||
"Zoom in start",
|
||||
};
|
||||
private static final int PERIODIC_VIGNETTE_INDEX = 2;
|
||||
private static final int COLOR_FILTERS_INDEX = 2;
|
||||
private static final int RGB_ADJUSTMENTS_INDEX = 4;
|
||||
private static final int HSL_ADJUSTMENT_INDEX = 5;
|
||||
private static final int CONTRAST_INDEX = 6;
|
||||
private static final int PERIODIC_VIGNETTE_INDEX = 7;
|
||||
private static final String SAME_AS_INPUT_OPTION = "same as input";
|
||||
private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2);
|
||||
|
||||
private @MonotonicNonNull Button selectFileButton;
|
||||
private @MonotonicNonNull ActivityResultLauncher<Intent> localFilePickerLauncher;
|
||||
private @MonotonicNonNull Button selectPresetFileButton;
|
||||
private @MonotonicNonNull Button selectLocalFileButton;
|
||||
private @MonotonicNonNull TextView selectedFileTextView;
|
||||
private @MonotonicNonNull CheckBox removeAudioCheckbox;
|
||||
private @MonotonicNonNull CheckBox removeVideoCheckbox;
|
||||
@ -120,13 +152,24 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
private @MonotonicNonNull Spinner rotateSpinner;
|
||||
private @MonotonicNonNull CheckBox trimCheckBox;
|
||||
private @MonotonicNonNull CheckBox enableFallbackCheckBox;
|
||||
private @MonotonicNonNull CheckBox enableDebugPreviewCheckBox;
|
||||
private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox;
|
||||
private @MonotonicNonNull CheckBox forceInterpretHdrVideoAsSdrCheckBox;
|
||||
private @MonotonicNonNull CheckBox enableHdrEditingCheckBox;
|
||||
private @MonotonicNonNull Button selectDemoEffectsButton;
|
||||
private boolean @MonotonicNonNull [] demoEffectsSelections;
|
||||
private @Nullable Uri localFileUri;
|
||||
private int inputUriPosition;
|
||||
private long trimStartMs;
|
||||
private long trimEndMs;
|
||||
private int colorFilterSelection;
|
||||
private float rgbAdjustmentRedScale;
|
||||
private float rgbAdjustmentGreenScale;
|
||||
private float rgbAdjustmentBlueScale;
|
||||
private float contrastValue;
|
||||
private float hueAdjustment;
|
||||
private float saturationAdjustment;
|
||||
private float lightnessAdjustment;
|
||||
private float periodicVignetteCenterX;
|
||||
private float periodicVignetteCenterY;
|
||||
private float periodicVignetteInnerRadius;
|
||||
@ -139,11 +182,10 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
|
||||
findViewById(R.id.transform_button).setOnClickListener(this::startTransformation);
|
||||
|
||||
selectFileButton = findViewById(R.id.select_file_button);
|
||||
selectFileButton.setOnClickListener(this::selectFile);
|
||||
flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox);
|
||||
|
||||
selectedFileTextView = findViewById(R.id.selected_file_text_view);
|
||||
selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
|
||||
selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]);
|
||||
|
||||
removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox);
|
||||
removeAudioCheckbox.setOnClickListener(this::onRemoveAudio);
|
||||
@ -151,7 +193,11 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
removeVideoCheckbox = findViewById(R.id.remove_video_checkbox);
|
||||
removeVideoCheckbox.setOnClickListener(this::onRemoveVideo);
|
||||
|
||||
flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox);
|
||||
selectPresetFileButton = findViewById(R.id.select_preset_file_button);
|
||||
selectPresetFileButton.setOnClickListener(this::selectPresetFile);
|
||||
|
||||
selectLocalFileButton = findViewById(R.id.select_local_file_button);
|
||||
selectLocalFileButton.setOnClickListener(this::selectLocalFile);
|
||||
|
||||
ArrayAdapter<String> audioMimeAdapter =
|
||||
new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item);
|
||||
@ -200,14 +246,38 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
trimEndMs = C.TIME_UNSET;
|
||||
|
||||
enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox);
|
||||
enableDebugPreviewCheckBox = findViewById(R.id.enable_debug_preview_checkbox);
|
||||
enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox);
|
||||
enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported());
|
||||
findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported());
|
||||
forceInterpretHdrVideoAsSdrCheckBox =
|
||||
findViewById(R.id.force_interpret_hdr_video_as_sdr_checkbox);
|
||||
enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox);
|
||||
|
||||
demoEffectsSelections = new boolean[DEMO_EFFECTS.length];
|
||||
selectDemoEffectsButton = findViewById(R.id.select_demo_effects_button);
|
||||
selectDemoEffectsButton.setOnClickListener(this::selectDemoEffects);
|
||||
|
||||
localFilePickerLauncher =
|
||||
registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
this::localFilePickerLauncherResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, String[] permissions, int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
|
||||
if (requestCode == FILE_PERMISSION_REQUEST_CODE
|
||||
&& grantResults.length == 1
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
launchLocalFilePicker();
|
||||
} else {
|
||||
Toast.makeText(
|
||||
getApplicationContext(), getString(R.string.permission_denied), Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -215,7 +285,8 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
super.onResume();
|
||||
@Nullable Uri intentUri = getIntent().getData();
|
||||
if (intentUri != null) {
|
||||
checkNotNull(selectFileButton).setEnabled(false);
|
||||
checkNotNull(selectPresetFileButton).setEnabled(false);
|
||||
checkNotNull(selectLocalFileButton).setEnabled(false);
|
||||
checkNotNull(selectedFileTextView).setText(intentUri.toString());
|
||||
}
|
||||
}
|
||||
@ -237,7 +308,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"rotateSpinner",
|
||||
"trimCheckBox",
|
||||
"enableFallbackCheckBox",
|
||||
"enableDebugPreviewCheckBox",
|
||||
"enableRequestSdrToneMappingCheckBox",
|
||||
"forceInterpretHdrVideoAsSdrCheckBox",
|
||||
"enableHdrEditingCheckBox",
|
||||
"demoEffectsSelections"
|
||||
})
|
||||
@ -275,32 +348,85 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
bundle.putLong(TRIM_END_MS, trimEndMs);
|
||||
}
|
||||
bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked());
|
||||
bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked());
|
||||
bundle.putBoolean(
|
||||
ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked());
|
||||
bundle.putBoolean(
|
||||
FORCE_INTERPRET_HDR_VIDEO_AS_SDR, forceInterpretHdrVideoAsSdrCheckBox.isChecked());
|
||||
bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked());
|
||||
bundle.putBooleanArray(DEMO_EFFECTS_SELECTIONS, demoEffectsSelections);
|
||||
bundle.putInt(COLOR_FILTER_SELECTION, colorFilterSelection);
|
||||
bundle.putFloat(CONTRAST_VALUE, contrastValue);
|
||||
bundle.putFloat(RGB_ADJUSTMENT_RED_SCALE, rgbAdjustmentRedScale);
|
||||
bundle.putFloat(RGB_ADJUSTMENT_GREEN_SCALE, rgbAdjustmentGreenScale);
|
||||
bundle.putFloat(RGB_ADJUSTMENT_BLUE_SCALE, rgbAdjustmentBlueScale);
|
||||
bundle.putFloat(HSL_ADJUSTMENTS_HUE, hueAdjustment);
|
||||
bundle.putFloat(HSL_ADJUSTMENTS_SATURATION, saturationAdjustment);
|
||||
bundle.putFloat(HSL_ADJUSTMENTS_LIGHTNESS, lightnessAdjustment);
|
||||
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_X, periodicVignetteCenterX);
|
||||
bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY);
|
||||
bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius);
|
||||
bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius);
|
||||
transformerIntent.putExtras(bundle);
|
||||
|
||||
@Nullable Uri intentUri = getIntent().getData();
|
||||
transformerIntent.setData(
|
||||
intentUri != null ? intentUri : Uri.parse(INPUT_URIS[inputUriPosition]));
|
||||
@Nullable Uri intentUri;
|
||||
if (getIntent().getData() != null) {
|
||||
intentUri = getIntent().getData();
|
||||
} else if (localFileUri != null) {
|
||||
intentUri = localFileUri;
|
||||
} else {
|
||||
intentUri = Uri.parse(PRESET_FILE_URIS[inputUriPosition]);
|
||||
}
|
||||
transformerIntent.setData(intentUri);
|
||||
|
||||
startActivity(transformerIntent);
|
||||
}
|
||||
|
||||
private void selectFile(View view) {
|
||||
private void selectPresetFile(View view) {
|
||||
new AlertDialog.Builder(/* context= */ this)
|
||||
.setTitle(R.string.select_file_title)
|
||||
.setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog)
|
||||
.setTitle(R.string.select_preset_file_title)
|
||||
.setSingleChoiceItems(
|
||||
PRESET_FILE_URI_DESCRIPTIONS, inputUriPosition, this::selectPresetFileInDialog)
|
||||
.setPositiveButton(android.R.string.ok, /* listener= */ null)
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
@RequiresNonNull("selectedFileTextView")
|
||||
private void selectPresetFileInDialog(DialogInterface dialog, int which) {
|
||||
inputUriPosition = which;
|
||||
localFileUri = null;
|
||||
selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]);
|
||||
}
|
||||
|
||||
private void selectLocalFile(View view) {
|
||||
int permissionStatus =
|
||||
ActivityCompat.checkSelfPermission(
|
||||
ConfigurationActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
if (permissionStatus != PackageManager.PERMISSION_GRANTED) {
|
||||
String[] neededPermissions = {Manifest.permission.READ_EXTERNAL_STORAGE};
|
||||
ActivityCompat.requestPermissions(
|
||||
ConfigurationActivity.this, neededPermissions, FILE_PERMISSION_REQUEST_CODE);
|
||||
} else {
|
||||
launchLocalFilePicker();
|
||||
}
|
||||
}
|
||||
|
||||
private void launchLocalFilePicker() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("video/*");
|
||||
checkNotNull(localFilePickerLauncher).launch(intent);
|
||||
}
|
||||
|
||||
@RequiresNonNull("selectedFileTextView")
|
||||
private void localFilePickerLauncherResult(ActivityResult result) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
localFileUri = checkNotNull(data.getData());
|
||||
selectedFileTextView.setText(localFileUri.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private void selectDemoEffects(View view) {
|
||||
new AlertDialog.Builder(/* context= */ this)
|
||||
.setTitle(R.string.select_demo_effects)
|
||||
@ -316,35 +442,122 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
return;
|
||||
}
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null);
|
||||
RangeSlider radiusRangeSlider =
|
||||
RangeSlider trimRangeSlider =
|
||||
checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider));
|
||||
radiusRangeSlider.setValues(0f, 60f); // seconds
|
||||
trimRangeSlider.setValues(0f, 10f); // seconds
|
||||
new AlertDialog.Builder(/* context= */ this)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
(DialogInterface dialogInterface, int i) -> {
|
||||
List<Float> radiusRange = radiusRangeSlider.getValues();
|
||||
trimStartMs = 1000 * radiusRange.get(0).longValue();
|
||||
trimEndMs = 1000 * radiusRange.get(1).longValue();
|
||||
List<Float> trimRange = trimRangeSlider.getValues();
|
||||
trimStartMs = Math.round(1000 * trimRange.get(0));
|
||||
trimEndMs = Math.round(1000 * trimRange.get(1));
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
@RequiresNonNull("selectedFileTextView")
|
||||
private void selectFileInDialog(DialogInterface dialog, int which) {
|
||||
inputUriPosition = which;
|
||||
selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]);
|
||||
}
|
||||
|
||||
@RequiresNonNull("demoEffectsSelections")
|
||||
private void selectDemoEffect(DialogInterface dialog, int which, boolean isChecked) {
|
||||
demoEffectsSelections[which] = isChecked;
|
||||
if (!isChecked || which != PERIODIC_VIGNETTE_INDEX) {
|
||||
if (!isChecked) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (which) {
|
||||
case COLOR_FILTERS_INDEX:
|
||||
controlColorFiltersSettings();
|
||||
break;
|
||||
case RGB_ADJUSTMENTS_INDEX:
|
||||
controlRgbAdjustmentsScale();
|
||||
break;
|
||||
case CONTRAST_INDEX:
|
||||
controlContrastSettings();
|
||||
break;
|
||||
case HSL_ADJUSTMENT_INDEX:
|
||||
controlHslAdjustmentSettings();
|
||||
break;
|
||||
case PERIODIC_VIGNETTE_INDEX:
|
||||
controlPeriodicVignetteSettings();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void controlColorFiltersSettings() {
|
||||
new AlertDialog.Builder(/* context= */ this)
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> dialogInterface.dismiss())
|
||||
.setSingleChoiceItems(
|
||||
this.getResources().getStringArray(R.array.color_filter_options),
|
||||
colorFilterSelection,
|
||||
(DialogInterface dialogInterface, int i) -> {
|
||||
checkState(
|
||||
i == COLOR_FILTER_GRAYSCALE
|
||||
|| i == COLOR_FILTER_INVERTED
|
||||
|| i == COLOR_FILTER_SEPIA);
|
||||
colorFilterSelection = i;
|
||||
dialogInterface.dismiss();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void controlRgbAdjustmentsScale() {
|
||||
View dialogView =
|
||||
getLayoutInflater().inflate(R.layout.rgb_adjustment_options, /* root= */ null);
|
||||
Slider redScaleSlider = checkNotNull(dialogView.findViewById(R.id.rgb_adjustment_red_scale));
|
||||
Slider greenScaleSlider =
|
||||
checkNotNull(dialogView.findViewById(R.id.rgb_adjustment_green_scale));
|
||||
Slider blueScaleSlider = checkNotNull(dialogView.findViewById(R.id.rgb_adjustment_blue_scale));
|
||||
new AlertDialog.Builder(/* context= */ this)
|
||||
.setTitle(R.string.rgb_adjustment_options)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
(DialogInterface dialogInterface, int i) -> {
|
||||
rgbAdjustmentRedScale = redScaleSlider.getValue();
|
||||
rgbAdjustmentGreenScale = greenScaleSlider.getValue();
|
||||
rgbAdjustmentBlueScale = blueScaleSlider.getValue();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void controlContrastSettings() {
|
||||
View dialogView = getLayoutInflater().inflate(R.layout.contrast_options, /* root= */ null);
|
||||
Slider contrastSlider = checkNotNull(dialogView.findViewById(R.id.contrast_slider));
|
||||
new AlertDialog.Builder(/* context= */ this)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
(DialogInterface dialogInterface, int i) -> contrastValue = contrastSlider.getValue())
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void controlHslAdjustmentSettings() {
|
||||
View dialogView =
|
||||
getLayoutInflater().inflate(R.layout.hsl_adjustment_options, /* root= */ null);
|
||||
Slider hueAdjustmentSlider = checkNotNull(dialogView.findViewById(R.id.hsl_adjustments_hue));
|
||||
Slider saturationAdjustmentSlider =
|
||||
checkNotNull(dialogView.findViewById(R.id.hsl_adjustments_saturation));
|
||||
Slider lightnessAdjustmentSlider =
|
||||
checkNotNull(dialogView.findViewById(R.id.hsl_adjustment_lightness));
|
||||
new AlertDialog.Builder(/* context= */ this)
|
||||
.setTitle(R.string.hsl_adjustment_options)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
(DialogInterface dialogInterface, int i) -> {
|
||||
hueAdjustment = hueAdjustmentSlider.getValue();
|
||||
saturationAdjustment = saturationAdjustmentSlider.getValue();
|
||||
lightnessAdjustment = lightnessAdjustmentSlider.getValue();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void controlPeriodicVignetteSettings() {
|
||||
View dialogView =
|
||||
getLayoutInflater().inflate(R.layout.periodic_vignette_options, /* root= */ null);
|
||||
Slider centerXSlider =
|
||||
@ -377,7 +590,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"resolutionHeightSpinner",
|
||||
"scaleSpinner",
|
||||
"rotateSpinner",
|
||||
"enableDebugPreviewCheckBox",
|
||||
"enableRequestSdrToneMappingCheckBox",
|
||||
"forceInterpretHdrVideoAsSdrCheckBox",
|
||||
"enableHdrEditingCheckBox",
|
||||
"selectDemoEffectsButton"
|
||||
})
|
||||
@ -397,7 +612,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"resolutionHeightSpinner",
|
||||
"scaleSpinner",
|
||||
"rotateSpinner",
|
||||
"enableDebugPreviewCheckBox",
|
||||
"enableRequestSdrToneMappingCheckBox",
|
||||
"forceInterpretHdrVideoAsSdrCheckBox",
|
||||
"enableHdrEditingCheckBox",
|
||||
"selectDemoEffectsButton"
|
||||
})
|
||||
@ -416,7 +633,9 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
"resolutionHeightSpinner",
|
||||
"scaleSpinner",
|
||||
"rotateSpinner",
|
||||
"enableDebugPreviewCheckBox",
|
||||
"enableRequestSdrToneMappingCheckBox",
|
||||
"forceInterpretHdrVideoAsSdrCheckBox",
|
||||
"enableHdrEditingCheckBox",
|
||||
"selectDemoEffectsButton"
|
||||
})
|
||||
@ -426,8 +645,10 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
resolutionHeightSpinner.setEnabled(isVideoEnabled);
|
||||
scaleSpinner.setEnabled(isVideoEnabled);
|
||||
rotateSpinner.setEnabled(isVideoEnabled);
|
||||
enableDebugPreviewCheckBox.setEnabled(isVideoEnabled);
|
||||
enableRequestSdrToneMappingCheckBox.setEnabled(
|
||||
isRequestSdrToneMappingSupported() && isVideoEnabled);
|
||||
forceInterpretHdrVideoAsSdrCheckBox.setEnabled(isVideoEnabled);
|
||||
enableHdrEditingCheckBox.setEnabled(isVideoEnabled);
|
||||
selectDemoEffectsButton.setEnabled(isVideoEnabled);
|
||||
|
||||
@ -438,6 +659,7 @@ public final class ConfigurationActivity extends AppCompatActivity {
|
||||
findViewById(R.id.rotate).setEnabled(isVideoEnabled);
|
||||
findViewById(R.id.request_sdr_tone_mapping)
|
||||
.setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled);
|
||||
findViewById(R.id.force_interpret_hdr_video_as_sdr).setEnabled(isVideoEnabled);
|
||||
findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled);
|
||||
}
|
||||
|
||||
|
@ -18,8 +18,8 @@ package androidx.media3.demo.transformer;
|
||||
import android.graphics.Matrix;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.transformer.GlMatrixTransformation;
|
||||
import androidx.media3.transformer.MatrixTransformation;
|
||||
import androidx.media3.effect.GlMatrixTransformation;
|
||||
import androidx.media3.effect.MatrixTransformation;
|
||||
|
||||
/**
|
||||
* Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link
|
||||
|
@ -16,39 +16,29 @@
|
||||
package androidx.media3.demo.transformer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
|
||||
import android.content.Context;
|
||||
import android.opengl.GLES20;
|
||||
import android.util.Size;
|
||||
import android.util.Pair;
|
||||
import androidx.media3.common.FrameProcessingException;
|
||||
import androidx.media3.common.util.GlProgram;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.media3.transformer.FrameProcessingException;
|
||||
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
|
||||
import androidx.media3.effect.SingleFrameGlTextureProcessor;
|
||||
import java.io.IOException;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
|
||||
/**
|
||||
* A {@link SingleFrameGlTextureProcessor} that periodically dims the frames such that pixels are
|
||||
* darker the further they are away from the frame center.
|
||||
*/
|
||||
/* package */ final class PeriodicVignetteProcessor implements SingleFrameGlTextureProcessor {
|
||||
static {
|
||||
GlUtil.glAssertionsEnabled = true;
|
||||
}
|
||||
/* package */ final class PeriodicVignetteProcessor extends SingleFrameGlTextureProcessor {
|
||||
|
||||
private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl";
|
||||
private static final String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl";
|
||||
private static final float DIMMING_PERIOD_US = 5_600_000f;
|
||||
|
||||
private float centerX;
|
||||
private float centerY;
|
||||
private float minInnerRadius;
|
||||
private float deltaInnerRadius;
|
||||
private float outerRadius;
|
||||
|
||||
private @MonotonicNonNull Size outputSize;
|
||||
private @MonotonicNonNull GlProgram glProgram;
|
||||
private final GlProgram glProgram;
|
||||
private final float minInnerRadius;
|
||||
private final float deltaInnerRadius;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
@ -61,29 +51,35 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
*
|
||||
* <p>The parameters are given in normalized texture coordinates from 0 to 1.
|
||||
*
|
||||
* @param context The {@link Context}.
|
||||
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
|
||||
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
|
||||
* @param centerX The x-coordinate of the center of the effect.
|
||||
* @param centerY The y-coordinate of the center of the effect.
|
||||
* @param minInnerRadius The lower bound of the radius that is unaffected by the effect.
|
||||
* @param maxInnerRadius The upper bound of the radius that is unaffected by the effect.
|
||||
* @param outerRadius The radius after which all pixels are black.
|
||||
* @throws FrameProcessingException If a problem occurs while reading shader files.
|
||||
*/
|
||||
public PeriodicVignetteProcessor(
|
||||
float centerX, float centerY, float minInnerRadius, float maxInnerRadius, float outerRadius) {
|
||||
Context context,
|
||||
boolean useHdr,
|
||||
float centerX,
|
||||
float centerY,
|
||||
float minInnerRadius,
|
||||
float maxInnerRadius,
|
||||
float outerRadius)
|
||||
throws FrameProcessingException {
|
||||
super(useHdr);
|
||||
checkArgument(minInnerRadius <= maxInnerRadius);
|
||||
checkArgument(maxInnerRadius <= outerRadius);
|
||||
this.centerX = centerX;
|
||||
this.centerY = centerY;
|
||||
this.minInnerRadius = minInnerRadius;
|
||||
this.deltaInnerRadius = maxInnerRadius - minInnerRadius;
|
||||
this.outerRadius = outerRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
|
||||
throws IOException {
|
||||
outputSize = new Size(inputWidth, inputHeight);
|
||||
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
|
||||
try {
|
||||
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
|
||||
} catch (IOException | GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
}
|
||||
glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY});
|
||||
glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius});
|
||||
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
|
||||
@ -94,14 +90,15 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Size getOutputSize() {
|
||||
return checkStateNotNull(outputSize);
|
||||
public Pair<Integer, Integer> configure(int inputWidth, int inputHeight) {
|
||||
return Pair.create(inputWidth, inputHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
|
||||
public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException {
|
||||
try {
|
||||
checkStateNotNull(glProgram).use();
|
||||
glProgram.use();
|
||||
glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0);
|
||||
double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US;
|
||||
float innerRadius =
|
||||
minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta));
|
||||
@ -110,14 +107,17 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
// The four-vertex triangle strip forms a quad.
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
throw new FrameProcessingException(e, presentationTimeUs);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
if (glProgram != null) {
|
||||
public void release() throws FrameProcessingException {
|
||||
super.release();
|
||||
try {
|
||||
glProgram.delete();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,10 +17,13 @@ package androidx.media3.demo.transformer;
|
||||
|
||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||
import static androidx.media3.common.util.Assertions.checkNotNull;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
@ -28,28 +31,37 @@ import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.DebugViewProvider;
|
||||
import androidx.media3.common.Effect;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.effect.Contrast;
|
||||
import androidx.media3.effect.GlEffect;
|
||||
import androidx.media3.effect.GlTextureProcessor;
|
||||
import androidx.media3.effect.HslAdjustment;
|
||||
import androidx.media3.effect.RgbAdjustment;
|
||||
import androidx.media3.effect.RgbFilter;
|
||||
import androidx.media3.effect.RgbMatrix;
|
||||
import androidx.media3.effect.SingleColorLut;
|
||||
import androidx.media3.exoplayer.ExoPlayer;
|
||||
import androidx.media3.exoplayer.util.DebugTextViewHelper;
|
||||
import androidx.media3.transformer.DefaultEncoderFactory;
|
||||
import androidx.media3.transformer.EncoderSelector;
|
||||
import androidx.media3.transformer.GlEffect;
|
||||
import androidx.media3.transformer.ProgressHolder;
|
||||
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
|
||||
import androidx.media3.transformer.TransformationException;
|
||||
import androidx.media3.transformer.TransformationRequest;
|
||||
import androidx.media3.transformer.TransformationResult;
|
||||
import androidx.media3.transformer.Transformer;
|
||||
import androidx.media3.ui.AspectRatioFrameLayout;
|
||||
import androidx.media3.ui.PlayerView;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator;
|
||||
import com.google.common.base.Stopwatch;
|
||||
import com.google.common.base.Ticker;
|
||||
@ -66,7 +78,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
public final class TransformerActivity extends AppCompatActivity {
|
||||
private static final String TAG = "TransformerActivity";
|
||||
|
||||
private @MonotonicNonNull PlayerView playerView;
|
||||
private @MonotonicNonNull Button displayInputButton;
|
||||
private @MonotonicNonNull MaterialCardView inputCardView;
|
||||
private @MonotonicNonNull PlayerView inputPlayerView;
|
||||
private @MonotonicNonNull PlayerView outputPlayerView;
|
||||
private @MonotonicNonNull TextView debugTextView;
|
||||
private @MonotonicNonNull TextView informationTextView;
|
||||
private @MonotonicNonNull ViewGroup progressViewGroup;
|
||||
@ -75,7 +90,8 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
private @MonotonicNonNull AspectRatioFrameLayout debugFrame;
|
||||
|
||||
@Nullable private DebugTextViewHelper debugTextViewHelper;
|
||||
@Nullable private ExoPlayer player;
|
||||
@Nullable private ExoPlayer inputPlayer;
|
||||
@Nullable private ExoPlayer outputPlayer;
|
||||
@Nullable private Transformer transformer;
|
||||
@Nullable private File externalCacheFile;
|
||||
|
||||
@ -84,16 +100,21 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.transformer_activity);
|
||||
|
||||
playerView = findViewById(R.id.player_view);
|
||||
inputCardView = findViewById(R.id.input_card_view);
|
||||
inputPlayerView = findViewById(R.id.input_player_view);
|
||||
outputPlayerView = findViewById(R.id.output_player_view);
|
||||
debugTextView = findViewById(R.id.debug_text_view);
|
||||
informationTextView = findViewById(R.id.information_text_view);
|
||||
progressViewGroup = findViewById(R.id.progress_view_group);
|
||||
progressIndicator = findViewById(R.id.progress_indicator);
|
||||
debugFrame = findViewById(R.id.debug_aspect_ratio_frame_layout);
|
||||
displayInputButton = findViewById(R.id.display_input_button);
|
||||
displayInputButton.setOnClickListener(this::toggleInputVideoDisplay);
|
||||
|
||||
transformationStopwatch =
|
||||
Stopwatch.createUnstarted(
|
||||
new Ticker() {
|
||||
@Override
|
||||
public long read() {
|
||||
return android.os.SystemClock.elapsedRealtimeNanos();
|
||||
}
|
||||
@ -107,13 +128,17 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
checkNotNull(progressIndicator);
|
||||
checkNotNull(informationTextView);
|
||||
checkNotNull(transformationStopwatch);
|
||||
checkNotNull(playerView);
|
||||
checkNotNull(inputCardView);
|
||||
checkNotNull(inputPlayerView);
|
||||
checkNotNull(outputPlayerView);
|
||||
checkNotNull(debugTextView);
|
||||
checkNotNull(progressViewGroup);
|
||||
checkNotNull(debugFrame);
|
||||
checkNotNull(displayInputButton);
|
||||
startTransformation();
|
||||
|
||||
playerView.onResume();
|
||||
inputPlayerView.onResume();
|
||||
outputPlayerView.onResume();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -127,7 +152,8 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
// stop watch to be stopped in a transformer callback.
|
||||
checkNotNull(transformationStopwatch).reset();
|
||||
|
||||
checkNotNull(playerView).onPause();
|
||||
checkNotNull(inputPlayerView).onPause();
|
||||
checkNotNull(outputPlayerView).onPause();
|
||||
releasePlayer();
|
||||
|
||||
checkNotNull(externalCacheFile).delete();
|
||||
@ -135,7 +161,10 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@RequiresNonNull({
|
||||
"playerView",
|
||||
"inputCardView",
|
||||
"inputPlayerView",
|
||||
"outputPlayerView",
|
||||
"displayInputButton",
|
||||
"debugTextView",
|
||||
"informationTextView",
|
||||
"progressIndicator",
|
||||
@ -161,7 +190,8 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
informationTextView.setText(R.string.transformation_started);
|
||||
playerView.setVisibility(View.GONE);
|
||||
inputCardView.setVisibility(View.GONE);
|
||||
outputPlayerView.setVisibility(View.GONE);
|
||||
Handler mainHandler = new Handler(getMainLooper());
|
||||
ProgressHolder progressHolder = new ProgressHolder();
|
||||
mainHandler.post(
|
||||
@ -200,20 +230,11 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
return mediaItemBuilder.build();
|
||||
}
|
||||
|
||||
// Create a cache file, resetting it if it already exists.
|
||||
private File createExternalCacheFile(String fileName) throws IOException {
|
||||
File file = new File(getExternalCacheDir(), fileName);
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IllegalStateException("Could not delete the previous transformer output file");
|
||||
}
|
||||
if (!file.createNewFile()) {
|
||||
throw new IllegalStateException("Could not create the transformer output file");
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
@RequiresNonNull({
|
||||
"playerView",
|
||||
"inputCardView",
|
||||
"inputPlayerView",
|
||||
"outputPlayerView",
|
||||
"displayInputButton",
|
||||
"debugTextView",
|
||||
"informationTextView",
|
||||
"transformationStopwatch",
|
||||
@ -251,6 +272,8 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
|
||||
requestBuilder.setEnableRequestSdrToneMapping(
|
||||
bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING));
|
||||
requestBuilder.experimental_setForceInterpretHdrVideoAsSdr(
|
||||
bundle.getBoolean(ConfigurationActivity.FORCE_INTERPRET_HDR_VIDEO_AS_SDR));
|
||||
requestBuilder.experimental_setEnableHdrEditing(
|
||||
bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING));
|
||||
transformerBuilder
|
||||
@ -258,62 +281,14 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
.setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO))
|
||||
.setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO))
|
||||
.setEncoderFactory(
|
||||
new DefaultEncoderFactory(
|
||||
EncoderSelector.DEFAULT,
|
||||
/* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK)));
|
||||
new DefaultEncoderFactory.Builder(this.getApplicationContext())
|
||||
.setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))
|
||||
.build());
|
||||
|
||||
ImmutableList.Builder<GlEffect> effects = new ImmutableList.Builder<>();
|
||||
@Nullable
|
||||
boolean[] selectedEffects =
|
||||
bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS);
|
||||
if (selectedEffects != null) {
|
||||
if (selectedEffects[0]) {
|
||||
effects.add(MatrixTransformationFactory.createDizzyCropEffect());
|
||||
}
|
||||
if (selectedEffects[1]) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("androidx.media3.demo.transformer.MediaPipeProcessor");
|
||||
Constructor<?> constructor =
|
||||
clazz.getConstructor(String.class, String.class, String.class);
|
||||
effects.add(
|
||||
() -> {
|
||||
try {
|
||||
return (SingleFrameGlTextureProcessor)
|
||||
constructor.newInstance(
|
||||
/* graphName= */ "edge_detector_mediapipe_graph.binarypb",
|
||||
/* inputStreamName= */ "input_video",
|
||||
/* outputStreamName= */ "output_video");
|
||||
} catch (Exception e) {
|
||||
runOnUiThread(() -> showToast(R.string.no_media_pipe_error));
|
||||
throw new RuntimeException("Failed to load MediaPipe processor", e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
showToast(R.string.no_media_pipe_error);
|
||||
}
|
||||
}
|
||||
if (selectedEffects[2]) {
|
||||
effects.add(
|
||||
() ->
|
||||
new PeriodicVignetteProcessor(
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y),
|
||||
/* minInnerRadius= */ bundle.getFloat(
|
||||
ConfigurationActivity.PERIODIC_VIGNETTE_INNER_RADIUS),
|
||||
/* maxInnerRadius= */ bundle.getFloat(
|
||||
ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS),
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
|
||||
}
|
||||
if (selectedEffects[3]) {
|
||||
effects.add(MatrixTransformationFactory.createSpin3dEffect());
|
||||
}
|
||||
if (selectedEffects[4]) {
|
||||
effects.add(BitmapOverlayProcessor::new);
|
||||
}
|
||||
if (selectedEffects[5]) {
|
||||
effects.add(MatrixTransformationFactory.createZoomInTransition());
|
||||
}
|
||||
transformerBuilder.setVideoFrameEffects(effects.build());
|
||||
transformerBuilder.setVideoEffects(createVideoEffectsListFromBundle(bundle));
|
||||
|
||||
if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) {
|
||||
transformerBuilder.setDebugViewProvider(new DemoDebugViewProvider());
|
||||
}
|
||||
}
|
||||
return transformerBuilder
|
||||
@ -322,7 +297,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onTransformationCompleted(
|
||||
MediaItem mediaItem, TransformationResult transformationResult) {
|
||||
TransformerActivity.this.onTransformationCompleted(filePath);
|
||||
TransformerActivity.this.onTransformationCompleted(filePath, mediaItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -331,10 +306,151 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
TransformerActivity.this.onTransformationError(exception);
|
||||
}
|
||||
})
|
||||
.setDebugViewProvider(new DemoDebugViewProvider())
|
||||
.build();
|
||||
}
|
||||
|
||||
/** Creates a cache file, resetting it if it already exists. */
|
||||
private File createExternalCacheFile(String fileName) throws IOException {
|
||||
File file = new File(getExternalCacheDir(), fileName);
|
||||
if (file.exists() && !file.delete()) {
|
||||
throw new IllegalStateException("Could not delete the previous transformer output file");
|
||||
}
|
||||
if (!file.createNewFile()) {
|
||||
throw new IllegalStateException("Could not create the transformer output file");
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private ImmutableList<Effect> createVideoEffectsListFromBundle(Bundle bundle) {
|
||||
@Nullable
|
||||
boolean[] selectedEffects =
|
||||
bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS);
|
||||
if (selectedEffects == null) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
ImmutableList.Builder<Effect> effects = new ImmutableList.Builder<>();
|
||||
if (selectedEffects[0]) {
|
||||
effects.add(MatrixTransformationFactory.createDizzyCropEffect());
|
||||
}
|
||||
if (selectedEffects[1]) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName("androidx.media3.demo.transformer.MediaPipeProcessor");
|
||||
Constructor<?> constructor =
|
||||
clazz.getConstructor(
|
||||
Context.class,
|
||||
boolean.class,
|
||||
String.class,
|
||||
boolean.class,
|
||||
String.class,
|
||||
String.class);
|
||||
effects.add(
|
||||
(GlEffect)
|
||||
(Context context, boolean useHdr) -> {
|
||||
try {
|
||||
return (GlTextureProcessor)
|
||||
constructor.newInstance(
|
||||
context,
|
||||
useHdr,
|
||||
/* graphName= */ "edge_detector_mediapipe_graph.binarypb",
|
||||
/* isSingleFrameGraph= */ true,
|
||||
/* inputStreamName= */ "input_video",
|
||||
/* outputStreamName= */ "output_video");
|
||||
} catch (Exception e) {
|
||||
runOnUiThread(() -> showToast(R.string.no_media_pipe_error));
|
||||
throw new RuntimeException("Failed to load MediaPipe processor", e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
showToast(R.string.no_media_pipe_error);
|
||||
}
|
||||
}
|
||||
if (selectedEffects[2]) {
|
||||
switch (bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION)) {
|
||||
case ConfigurationActivity.COLOR_FILTER_GRAYSCALE:
|
||||
effects.add(RgbFilter.createGrayscaleFilter());
|
||||
break;
|
||||
case ConfigurationActivity.COLOR_FILTER_INVERTED:
|
||||
effects.add(RgbFilter.createInvertedFilter());
|
||||
break;
|
||||
case ConfigurationActivity.COLOR_FILTER_SEPIA:
|
||||
// W3C Sepia RGBA matrix with sRGB as a target color space:
|
||||
// https://www.w3.org/TR/filter-effects-1/#sepiaEquivalent
|
||||
// The matrix is defined for the sRGB color space and the Transformer library
|
||||
// uses a linear RGB color space internally. Meaning this is only for demonstration
|
||||
// purposes and it does not display a correct sepia frame.
|
||||
float[] sepiaMatrix = {
|
||||
0.393f, 0.349f, 0.272f, 0, 0.769f, 0.686f, 0.534f, 0, 0.189f, 0.168f, 0.131f, 0, 0, 0,
|
||||
0, 1
|
||||
};
|
||||
effects.add((RgbMatrix) (presentationTimeUs, useHdr) -> sepiaMatrix);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException(
|
||||
"Unexpected color filter "
|
||||
+ bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION));
|
||||
}
|
||||
}
|
||||
if (selectedEffects[3]) {
|
||||
int length = 3;
|
||||
int[][][] mapWhiteToGreenLut = new int[length][length][length];
|
||||
int scale = 255 / (length - 1);
|
||||
for (int r = 0; r < length; r++) {
|
||||
for (int g = 0; g < length; g++) {
|
||||
for (int b = 0; b < length; b++) {
|
||||
mapWhiteToGreenLut[r][g][b] =
|
||||
Color.rgb(/* red= */ r * scale, /* green= */ g * scale, /* blue= */ b * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
mapWhiteToGreenLut[length - 1][length - 1][length - 1] = Color.GREEN;
|
||||
effects.add(SingleColorLut.createFromCube(mapWhiteToGreenLut));
|
||||
}
|
||||
if (selectedEffects[4]) {
|
||||
effects.add(
|
||||
new RgbAdjustment.Builder()
|
||||
.setRedScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_RED_SCALE))
|
||||
.setGreenScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_GREEN_SCALE))
|
||||
.setBlueScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_BLUE_SCALE))
|
||||
.build());
|
||||
}
|
||||
if (selectedEffects[5]) {
|
||||
effects.add(
|
||||
new HslAdjustment.Builder()
|
||||
.adjustHue(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_HUE))
|
||||
.adjustSaturation(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_SATURATION))
|
||||
.adjustLightness(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_LIGHTNESS))
|
||||
.build());
|
||||
}
|
||||
if (selectedEffects[6]) {
|
||||
effects.add(new Contrast(bundle.getFloat(ConfigurationActivity.CONTRAST_VALUE)));
|
||||
}
|
||||
if (selectedEffects[7]) {
|
||||
effects.add(
|
||||
(GlEffect)
|
||||
(Context context, boolean useHdr) ->
|
||||
new PeriodicVignetteProcessor(
|
||||
context,
|
||||
useHdr,
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X),
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y),
|
||||
/* minInnerRadius= */ bundle.getFloat(
|
||||
ConfigurationActivity.PERIODIC_VIGNETTE_INNER_RADIUS),
|
||||
/* maxInnerRadius= */ bundle.getFloat(
|
||||
ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS),
|
||||
bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS)));
|
||||
}
|
||||
if (selectedEffects[8]) {
|
||||
effects.add(MatrixTransformationFactory.createSpin3dEffect());
|
||||
}
|
||||
if (selectedEffects[9]) {
|
||||
effects.add((GlEffect) BitmapOverlayProcessor::new);
|
||||
}
|
||||
if (selectedEffects[10]) {
|
||||
effects.add(MatrixTransformationFactory.createZoomInTransition());
|
||||
}
|
||||
return effects.build();
|
||||
}
|
||||
|
||||
@RequiresNonNull({
|
||||
"informationTextView",
|
||||
"progressViewGroup",
|
||||
@ -346,44 +462,66 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
informationTextView.setText(R.string.transformation_error);
|
||||
progressViewGroup.setVisibility(View.GONE);
|
||||
debugFrame.removeAllViews();
|
||||
Toast.makeText(
|
||||
TransformerActivity.this, "Transformation error: " + exception, Toast.LENGTH_LONG)
|
||||
Toast.makeText(getApplicationContext(), "Transformation error: " + exception, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Transformation error", exception);
|
||||
}
|
||||
|
||||
@RequiresNonNull({
|
||||
"playerView",
|
||||
"inputCardView",
|
||||
"inputPlayerView",
|
||||
"outputPlayerView",
|
||||
"displayInputButton",
|
||||
"debugTextView",
|
||||
"informationTextView",
|
||||
"progressViewGroup",
|
||||
"debugFrame",
|
||||
"transformationStopwatch",
|
||||
})
|
||||
private void onTransformationCompleted(String filePath) {
|
||||
private void onTransformationCompleted(String filePath, MediaItem inputMediaItem) {
|
||||
transformationStopwatch.stop();
|
||||
informationTextView.setText(
|
||||
getString(
|
||||
R.string.transformation_completed, transformationStopwatch.elapsed(TimeUnit.SECONDS)));
|
||||
progressViewGroup.setVisibility(View.GONE);
|
||||
debugFrame.removeAllViews();
|
||||
playerView.setVisibility(View.VISIBLE);
|
||||
playMediaItem(MediaItem.fromUri("file://" + filePath));
|
||||
inputCardView.setVisibility(View.VISIBLE);
|
||||
outputPlayerView.setVisibility(View.VISIBLE);
|
||||
displayInputButton.setVisibility(View.VISIBLE);
|
||||
playMediaItems(inputMediaItem, MediaItem.fromUri("file://" + filePath));
|
||||
Log.d(TAG, "Output file path: file://" + filePath);
|
||||
}
|
||||
|
||||
@RequiresNonNull({"playerView", "debugTextView"})
|
||||
private void playMediaItem(MediaItem mediaItem) {
|
||||
playerView.setPlayer(null);
|
||||
@RequiresNonNull({
|
||||
"inputCardView",
|
||||
"inputPlayerView",
|
||||
"outputPlayerView",
|
||||
"debugTextView",
|
||||
})
|
||||
private void playMediaItems(MediaItem inputMediaItem, MediaItem outputMediaItem) {
|
||||
inputPlayerView.setPlayer(null);
|
||||
outputPlayerView.setPlayer(null);
|
||||
releasePlayer();
|
||||
|
||||
ExoPlayer player = new ExoPlayer.Builder(/* context= */ this).build();
|
||||
playerView.setPlayer(player);
|
||||
player.setMediaItem(mediaItem);
|
||||
player.play();
|
||||
player.prepare();
|
||||
this.player = player;
|
||||
debugTextViewHelper = new DebugTextViewHelper(player, debugTextView);
|
||||
ExoPlayer inputPlayer = new ExoPlayer.Builder(/* context= */ this).build();
|
||||
inputPlayerView.setPlayer(inputPlayer);
|
||||
inputPlayerView.setControllerAutoShow(false);
|
||||
inputPlayer.setMediaItem(inputMediaItem);
|
||||
inputPlayer.prepare();
|
||||
this.inputPlayer = inputPlayer;
|
||||
inputPlayer.setVolume(0f);
|
||||
|
||||
ExoPlayer outputPlayer = new ExoPlayer.Builder(/* context= */ this).build();
|
||||
outputPlayerView.setPlayer(outputPlayer);
|
||||
outputPlayerView.setControllerAutoShow(false);
|
||||
outputPlayer.setMediaItem(outputMediaItem);
|
||||
outputPlayer.prepare();
|
||||
this.outputPlayer = outputPlayer;
|
||||
|
||||
inputPlayer.play();
|
||||
outputPlayer.play();
|
||||
|
||||
debugTextViewHelper = new DebugTextViewHelper(outputPlayer, debugTextView);
|
||||
debugTextViewHelper.start();
|
||||
}
|
||||
|
||||
@ -392,9 +530,13 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
debugTextViewHelper.stop();
|
||||
debugTextViewHelper = null;
|
||||
}
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
if (inputPlayer != null) {
|
||||
inputPlayer.release();
|
||||
inputPlayer = null;
|
||||
}
|
||||
if (outputPlayer != null) {
|
||||
outputPlayer.release();
|
||||
outputPlayer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,11 +553,45 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
Toast.makeText(getApplicationContext(), getString(messageResource), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private final class DemoDebugViewProvider implements Transformer.DebugViewProvider {
|
||||
@RequiresNonNull({
|
||||
"inputCardView",
|
||||
"displayInputButton",
|
||||
})
|
||||
private void toggleInputVideoDisplay(View view) {
|
||||
if (inputCardView.getVisibility() == View.GONE) {
|
||||
inputCardView.setVisibility(View.VISIBLE);
|
||||
displayInputButton.setText(getString(R.string.hide_input_video));
|
||||
} else if (inputCardView.getVisibility() == View.VISIBLE) {
|
||||
checkNotNull(inputPlayer).pause();
|
||||
inputCardView.setVisibility(View.GONE);
|
||||
displayInputButton.setText(getString(R.string.show_input_video));
|
||||
}
|
||||
}
|
||||
|
||||
private final class DemoDebugViewProvider implements DebugViewProvider {
|
||||
|
||||
private @MonotonicNonNull SurfaceView surfaceView;
|
||||
private int width;
|
||||
private int height;
|
||||
|
||||
public DemoDebugViewProvider() {
|
||||
width = C.LENGTH_UNSET;
|
||||
height = C.LENGTH_UNSET;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SurfaceView getDebugPreviewSurfaceView(int width, int height) {
|
||||
checkState(
|
||||
surfaceView == null || (this.width == width && this.height == height),
|
||||
"Transformer should not change the output size mid-transformation.");
|
||||
if (surfaceView != null) {
|
||||
return surfaceView;
|
||||
}
|
||||
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
// Update the UI on the main thread and wait for the output surface to be available.
|
||||
CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1);
|
||||
SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this);
|
||||
@ -452,6 +628,7 @@ public final class TransformerActivity extends AppCompatActivity {
|
||||
Thread.currentThread().interrupt();
|
||||
return null;
|
||||
}
|
||||
this.surfaceView = surfaceView;
|
||||
return surfaceView;
|
||||
}
|
||||
}
|
||||
|
@ -34,16 +34,26 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
<Button
|
||||
android:id="@+id/select_file_button"
|
||||
android:id="@+id/select_preset_file_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:text="@string/select_file_title"
|
||||
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view"
|
||||
android:layout_marginStart="8dp"
|
||||
android:text="@string/select_preset_file_title"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/select_local_file_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/select_local_file_title"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/configuration_text_view" />
|
||||
<TextView
|
||||
android:id="@+id/selected_file_text_view"
|
||||
android:layout_width="0dp"
|
||||
@ -57,52 +67,50 @@
|
||||
android:gravity="center"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/select_file_button" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/select_preset_file_button" />
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:padding="4dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/selected_file_text_view"
|
||||
app:layout_constraintBottom_toTopOf="@+id/select_demo_effects_button">
|
||||
<TableLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:stretchColumns="1"
|
||||
android:stretchColumns="0"
|
||||
android:layout_marginTop="32dp"
|
||||
android:measureWithLargestChild="true"
|
||||
android:paddingLeft="24dp"
|
||||
android:paddingRight="12dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
<TableRow
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical" >
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/remove_audio" />
|
||||
<CheckBox
|
||||
android:id="@+id/remove_audio_checkbox"
|
||||
android:layout_gravity="right"/>
|
||||
android:layout_gravity="end"
|
||||
android:id="@+id/remove_audio_checkbox"/>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical" >
|
||||
<TableRow android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/remove_video"/>
|
||||
<CheckBox
|
||||
android:id="@+id/remove_video_checkbox"
|
||||
android:layout_gravity="right" />
|
||||
android:layout_gravity="end" />
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical" >
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/flatten_for_slow_motion"/>
|
||||
<CheckBox
|
||||
android:id="@+id/flatten_for_slow_motion_checkbox"
|
||||
android:layout_gravity="right" />
|
||||
android:layout_gravity="end" />
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1"
|
||||
@ -160,44 +168,64 @@
|
||||
android:gravity="right" />
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical" >
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/trim"
|
||||
android:text="@string/trim" />
|
||||
<CheckBox
|
||||
android:id="@+id/trim_checkbox"
|
||||
android:layout_gravity="right" />
|
||||
android:layout_gravity="end" />
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical" >
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/enable_fallback" />
|
||||
<CheckBox
|
||||
android:id="@+id/enable_fallback_checkbox"
|
||||
android:layout_gravity="right"
|
||||
android:layout_gravity="end"
|
||||
android:checked="true"/>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical" >
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:text="@string/enable_debug_preview" />
|
||||
<CheckBox
|
||||
android:id="@+id/enable_debug_preview_checkbox"
|
||||
android:layout_gravity="end"
|
||||
android:checked="true"/>
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/request_sdr_tone_mapping"
|
||||
android:text="@string/request_sdr_tone_mapping" />
|
||||
<CheckBox
|
||||
android:id="@+id/request_sdr_tone_mapping_checkbox"
|
||||
android:layout_gravity="right" />
|
||||
android:layout_gravity="end" />
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical" >
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/hdr_editing"
|
||||
android:text="@string/hdr_editing" />
|
||||
<CheckBox
|
||||
android:id="@+id/hdr_editing_checkbox"
|
||||
android:layout_gravity="right" />
|
||||
android:layout_gravity="end" />
|
||||
</TableRow>
|
||||
<TableRow
|
||||
android:layout_weight="1">
|
||||
<TextView
|
||||
android:layout_gravity="center_vertical"
|
||||
android:id="@+id/force_interpret_hdr_video_as_sdr"
|
||||
android:text="@string/force_interpret_hdr_video_as_sdr" />
|
||||
<CheckBox
|
||||
android:id="@+id/force_interpret_hdr_video_as_sdr_checkbox"
|
||||
android:layout_gravity="end" />
|
||||
</TableRow>
|
||||
</TableLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
49
demos/transformer/src/main/res/layout/contrast_options.xml
Normal file
49
demos/transformer/src/main/res/layout/contrast_options.xml
Normal 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>
|
@ -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>
|
@ -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>
|
@ -29,42 +29,113 @@
|
||||
app:cardElevation="2dp"
|
||||
android:gravity="center_vertical" >
|
||||
|
||||
<TextView
|
||||
android:id="@+id/information_text_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp" />
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/information_text_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/display_input_button"
|
||||
android:visibility="gone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hide_input_video"
|
||||
android:layout_margin="8dp" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/input_card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_margin="16dp"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="2dp">
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/player_view"
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:padding="8dp"
|
||||
android:text="@string/input_video" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/input_player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.media3.ui.AspectRatioFrameLayout
|
||||
android:id="@+id/input_debug_aspect_ratio_frame_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_text_view"
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/output_card_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_margin="16dp"
|
||||
app:cardCornerRadius="4dp"
|
||||
app:cardElevation="2dp">
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:text="@string/output_video" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/debug_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:textSize="10sp"
|
||||
tools:ignore="SmallSp"/>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.media3.ui.PlayerView
|
||||
android:id="@+id/output_player_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
tools:ignore="SmallSp"/>
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/progress_view_group"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_gravity="bottom"
|
||||
android:padding="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
@ -96,5 +167,9 @@
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
@ -17,7 +17,8 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="app_name" translatable="false">Transformer Demo</string>
|
||||
<string name="configuration" translatable="false">Configuration</string>
|
||||
<string name="select_file_title" translatable="false">Choose file</string>
|
||||
<string name="select_preset_file_title" translatable="false">Choose preset file</string>
|
||||
<string name="select_local_file_title">Choose local file</string>
|
||||
<string name="remove_audio" translatable="false">Remove audio</string>
|
||||
<string name="remove_video" translatable="false">Remove video</string>
|
||||
<string name="flatten_for_slow_motion" translatable="false">Flatten for slow motion</string>
|
||||
@ -27,9 +28,11 @@
|
||||
<string name="scale" translatable="false">Scale video</string>
|
||||
<string name="rotate" translatable="false">Rotate video (degrees)</string>
|
||||
<string name="enable_fallback" translatable="false">Enable fallback</string>
|
||||
<string name="enable_debug_preview" translatable="false">Enable debug preview</string>
|
||||
<string name="trim" translatable="false">Trim</string>
|
||||
<string name="request_sdr_tone_mapping" translatable="false">Request SDR tone-mapping (API 31+)</string>
|
||||
<string name="hdr_editing" translatable="false">[Experimental] HDR editing</string>
|
||||
<string name="force_interpret_hdr_video_as_sdr" translatable="false">[Experimental] Force interpret HDR video as SDR (API 29+)</string>
|
||||
<string name="hdr_editing" translatable="false">[Experimental] HDR editing (API 31+)</string>
|
||||
<string name="select_demo_effects" translatable="false">Add demo effects</string>
|
||||
<string name="periodic_vignette_options" translatable="false">Periodic vignette options</string>
|
||||
<string name="no_media_pipe_error" translatable="false">Failed to load MediaPipe processor. Check the README for instructions.</string>
|
||||
@ -40,8 +43,27 @@
|
||||
<string name="transformation_timer" translatable="false">Transformation started %d seconds ago.</string>
|
||||
<string name="transformation_completed" translatable="false">Transformation completed in %d seconds.</string>
|
||||
<string name="transformation_error" translatable="false">Transformation error</string>
|
||||
<string name="trim_range">Bounds in seconds</string>
|
||||
<string-array name="color_filter_options">
|
||||
<item>Grayscale</item>
|
||||
<item>Inverted</item>
|
||||
<item>Sepia</item>
|
||||
</string-array>
|
||||
<string name="contrast_value" translatable="false">Contrast value</string>
|
||||
<string name="rgb_adjustment_options" translatable="false">Scale RGB Channels individually</string>
|
||||
<string name="rgb_adjustment_scale_red" translatable="false">Scale red</string>
|
||||
<string name="rgb_adjustment_scale_green" translatable="false">Scale green</string>
|
||||
<string name="rgb_adjustment_scale_blue" translatable="false">Scale blue</string>
|
||||
<string name="center_x">Center X</string>
|
||||
<string name="center_y">Center Y</string>
|
||||
<string name="radius_range">Radius range</string>
|
||||
<string name="trim_range">Bounds in seconds</string>
|
||||
<string name="hsl_adjustment_options" translatable="false">HSL adjustment options</string>
|
||||
<string name="hue_adjustment">Hue adjustment</string>
|
||||
<string name="saturation_adjustment">Saturation adjustment</string>
|
||||
<string name="lightness_adjustment">Lightness adjustment</string>
|
||||
<string name="input_video">Input video:</string>
|
||||
<string name="output_video">Output video:</string>
|
||||
<string name="permission_denied">Permission Denied</string>
|
||||
<string name="hide_input_video">Hide input video</string>
|
||||
<string name="show_input_video">Show input video</string>
|
||||
</resources>
|
||||
|
@ -15,32 +15,37 @@
|
||||
*/
|
||||
package androidx.media3.demo.transformer;
|
||||
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
import static androidx.media3.common.util.Assertions.checkStateNotNull;
|
||||
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||
|
||||
import android.content.Context;
|
||||
import android.opengl.EGL14;
|
||||
import android.opengl.GLES20;
|
||||
import android.util.Size;
|
||||
import androidx.media3.common.util.ConditionVariable;
|
||||
import androidx.media3.common.util.GlProgram;
|
||||
import androidx.media3.common.util.GlUtil;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.FrameProcessingException;
|
||||
import androidx.media3.common.util.LibraryLoader;
|
||||
import androidx.media3.transformer.FrameProcessingException;
|
||||
import androidx.media3.transformer.SingleFrameGlTextureProcessor;
|
||||
import androidx.media3.common.util.Util;
|
||||
import androidx.media3.effect.GlTextureProcessor;
|
||||
import androidx.media3.effect.TextureInfo;
|
||||
import com.google.mediapipe.components.FrameProcessor;
|
||||
import com.google.mediapipe.framework.AndroidAssetUtil;
|
||||
import com.google.mediapipe.framework.AppTextureFrame;
|
||||
import com.google.mediapipe.framework.TextureFrame;
|
||||
import com.google.mediapipe.glutil.EglManager;
|
||||
import java.io.IOException;
|
||||
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
* Runs a MediaPipe graph on input frames. The implementation is currently limited to graphs that
|
||||
* can immediately produce one output frame per input frame.
|
||||
*/
|
||||
/* package */ final class MediaPipeProcessor implements SingleFrameGlTextureProcessor {
|
||||
/** Runs a MediaPipe graph on input frames. */
|
||||
/* package */ final class MediaPipeProcessor implements GlTextureProcessor {
|
||||
|
||||
private static final String THREAD_NAME = "Demo:MediaPipeProcessor";
|
||||
private static final long RELEASE_WAIT_TIME_MS = 100;
|
||||
private static final long RETRY_WAIT_TIME_MS = 1;
|
||||
|
||||
private static final LibraryLoader LOADER =
|
||||
new LibraryLoader("mediapipe_jni") {
|
||||
@ -60,116 +65,218 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
}
|
||||
}
|
||||
|
||||
private static final String COPY_VERTEX_SHADER_NAME = "vertex_shader_copy_es2.glsl";
|
||||
private static final String COPY_FRAGMENT_SHADER_NAME = "shaders/fragment_shader_copy_es2.glsl";
|
||||
private final FrameProcessor frameProcessor;
|
||||
private final ConcurrentHashMap<TextureInfo, TextureFrame> outputFrames;
|
||||
private final boolean isSingleFrameGraph;
|
||||
@Nullable private final ExecutorService singleThreadExecutorService;
|
||||
private final Queue<Future<?>> futures;
|
||||
|
||||
private final String graphName;
|
||||
private final String inputStreamName;
|
||||
private final String outputStreamName;
|
||||
private final ConditionVariable frameProcessorConditionVariable;
|
||||
|
||||
private @MonotonicNonNull FrameProcessor frameProcessor;
|
||||
private int inputWidth;
|
||||
private int inputHeight;
|
||||
private int inputTexId;
|
||||
private @MonotonicNonNull GlProgram glProgram;
|
||||
private @MonotonicNonNull TextureFrame outputFrame;
|
||||
private @MonotonicNonNull RuntimeException frameProcessorPendingError;
|
||||
private InputListener inputListener;
|
||||
private OutputListener outputListener;
|
||||
private ErrorListener errorListener;
|
||||
private boolean acceptedFrame;
|
||||
|
||||
/**
|
||||
* Creates a new texture processor that wraps a MediaPipe graph.
|
||||
*
|
||||
* <p>If {@code isSingleFrameGraph} is {@code false}, the {@code MediaPipeProcessor} may waste CPU
|
||||
* time by continuously attempting to queue input frames to MediaPipe until they are accepted or
|
||||
* waste memory if MediaPipe accepts and stores many frames internally.
|
||||
*
|
||||
* @param context The {@link Context}.
|
||||
* @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be
|
||||
* in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709.
|
||||
* @param graphName Name of a MediaPipe graph asset to load.
|
||||
* @param isSingleFrameGraph Whether the MediaPipe graph will eventually produce one output frame
|
||||
* each time an input frame (and no other input) has been queued.
|
||||
* @param inputStreamName Name of the input video stream in the graph.
|
||||
* @param outputStreamName Name of the input video stream in the graph.
|
||||
*/
|
||||
public MediaPipeProcessor(String graphName, String inputStreamName, String outputStreamName) {
|
||||
public MediaPipeProcessor(
|
||||
Context context,
|
||||
boolean useHdr,
|
||||
String graphName,
|
||||
boolean isSingleFrameGraph,
|
||||
String inputStreamName,
|
||||
String outputStreamName) {
|
||||
checkState(LOADER.isAvailable());
|
||||
this.graphName = graphName;
|
||||
this.inputStreamName = inputStreamName;
|
||||
this.outputStreamName = outputStreamName;
|
||||
frameProcessorConditionVariable = new ConditionVariable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight)
|
||||
throws IOException {
|
||||
this.inputTexId = inputTexId;
|
||||
this.inputWidth = inputWidth;
|
||||
this.inputHeight = inputHeight;
|
||||
glProgram = new GlProgram(context, COPY_VERTEX_SHADER_NAME, COPY_FRAGMENT_SHADER_NAME);
|
||||
|
||||
AndroidAssetUtil.initializeNativeAssetManager(context);
|
||||
// TODO(b/227624622): Confirm whether MediaPipeProcessor could support HDR colors.
|
||||
checkArgument(!useHdr, "MediaPipeProcessor does not support HDR colors.");
|
||||
|
||||
this.isSingleFrameGraph = isSingleFrameGraph;
|
||||
singleThreadExecutorService =
|
||||
isSingleFrameGraph ? null : Util.newSingleThreadExecutor(THREAD_NAME);
|
||||
futures = new ArrayDeque<>();
|
||||
inputListener = new InputListener() {};
|
||||
outputListener = new OutputListener() {};
|
||||
errorListener = (frameProcessingException) -> {};
|
||||
EglManager eglManager = new EglManager(EGL14.eglGetCurrentContext());
|
||||
frameProcessor =
|
||||
new FrameProcessor(
|
||||
context, eglManager.getNativeContext(), graphName, inputStreamName, outputStreamName);
|
||||
outputFrames = new ConcurrentHashMap<>();
|
||||
// OnWillAddFrameListener is called on the same thread as frameProcessor.onNewFrame(...), so no
|
||||
// synchronization is needed for acceptedFrame.
|
||||
frameProcessor.setOnWillAddFrameListener((long timestamp) -> acceptedFrame = true);
|
||||
}
|
||||
|
||||
// Unblock drawFrame when there is an output frame or an error.
|
||||
@Override
|
||||
public void setInputListener(InputListener inputListener) {
|
||||
this.inputListener = inputListener;
|
||||
if (!isSingleFrameGraph || outputFrames.isEmpty()) {
|
||||
inputListener.onReadyToAcceptInputFrame();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOutputListener(OutputListener outputListener) {
|
||||
this.outputListener = outputListener;
|
||||
frameProcessor.setConsumer(
|
||||
frame -> {
|
||||
outputFrame = frame;
|
||||
frameProcessorConditionVariable.open();
|
||||
TextureInfo texture =
|
||||
new TextureInfo(
|
||||
frame.getTextureName(),
|
||||
/* fboId= */ C.INDEX_UNSET,
|
||||
frame.getWidth(),
|
||||
frame.getHeight());
|
||||
outputFrames.put(texture, frame);
|
||||
outputListener.onOutputFrameAvailable(texture, frame.getTimestamp());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setErrorListener(ErrorListener errorListener) {
|
||||
this.errorListener = errorListener;
|
||||
frameProcessor.setAsynchronousErrorListener(
|
||||
error -> {
|
||||
frameProcessorPendingError = error;
|
||||
frameProcessorConditionVariable.open();
|
||||
});
|
||||
error -> errorListener.onFrameProcessingError(new FrameProcessingException(error)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Size getOutputSize() {
|
||||
return new Size(inputWidth, inputHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void drawFrame(long presentationTimeUs) throws FrameProcessingException {
|
||||
frameProcessorConditionVariable.close();
|
||||
|
||||
// Pass the input frame to MediaPipe.
|
||||
AppTextureFrame appTextureFrame = new AppTextureFrame(inputTexId, inputWidth, inputHeight);
|
||||
public void queueInputFrame(TextureInfo inputTexture, long presentationTimeUs) {
|
||||
AppTextureFrame appTextureFrame =
|
||||
new AppTextureFrame(inputTexture.texId, inputTexture.width, inputTexture.height);
|
||||
// TODO(b/238302213): Handle timestamps restarting from 0 when applying effects to a playlist.
|
||||
// MediaPipe will fail if the timestamps are not monotonically increasing.
|
||||
// Also make sure that a MediaPipe graph producing additional frames only starts producing
|
||||
// frames for the next MediaItem after receiving the first frame of that MediaItem as input
|
||||
// to avoid MediaPipe producing extra frames after the last MediaItem has ended.
|
||||
appTextureFrame.setTimestamp(presentationTimeUs);
|
||||
checkStateNotNull(frameProcessor).onNewFrame(appTextureFrame);
|
||||
|
||||
// Wait for output to be passed to the consumer.
|
||||
try {
|
||||
frameProcessorConditionVariable.block();
|
||||
} catch (InterruptedException e) {
|
||||
// Propagate the interrupted flag so the next blocking operation will throw.
|
||||
// TODO(b/230469581): The next processor that runs will not have valid input due to returning
|
||||
// early here. This could be fixed by checking for interruption in the outer loop that runs
|
||||
// through the texture processors.
|
||||
Thread.currentThread().interrupt();
|
||||
if (isSingleFrameGraph) {
|
||||
boolean acceptedFrame = maybeQueueInputFrameSynchronous(appTextureFrame, inputTexture);
|
||||
checkState(
|
||||
acceptedFrame,
|
||||
"queueInputFrame must only be called when a new input frame can be accepted");
|
||||
return;
|
||||
}
|
||||
|
||||
if (frameProcessorPendingError != null) {
|
||||
throw new FrameProcessingException(frameProcessorPendingError);
|
||||
}
|
||||
// TODO(b/241782273): Avoid retrying continuously until the frame is accepted by using a
|
||||
// currently non-existent MediaPipe API to be notified when MediaPipe has capacity to accept a
|
||||
// new frame.
|
||||
queueInputFrameAsynchronous(appTextureFrame, inputTexture);
|
||||
}
|
||||
|
||||
// Copy from MediaPipe's output texture to the current output.
|
||||
private boolean maybeQueueInputFrameSynchronous(
|
||||
AppTextureFrame appTextureFrame, TextureInfo inputTexture) {
|
||||
acceptedFrame = false;
|
||||
frameProcessor.onNewFrame(appTextureFrame);
|
||||
try {
|
||||
checkStateNotNull(glProgram).use();
|
||||
glProgram.setSamplerTexIdUniform(
|
||||
"uTexSampler", checkStateNotNull(outputFrame).getTextureName(), /* texUnitIndex= */ 0);
|
||||
glProgram.setBufferAttribute(
|
||||
"aFramePosition",
|
||||
GlUtil.getNormalizedCoordinateBounds(),
|
||||
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
|
||||
glProgram.bindAttributesAndUniforms();
|
||||
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4);
|
||||
GlUtil.checkGlError();
|
||||
} catch (GlUtil.GlException e) {
|
||||
throw new FrameProcessingException(e);
|
||||
} finally {
|
||||
checkStateNotNull(outputFrame).release();
|
||||
appTextureFrame.waitUntilReleasedWithGpuSync();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException(e));
|
||||
}
|
||||
if (acceptedFrame) {
|
||||
inputListener.onInputFrameProcessed(inputTexture);
|
||||
}
|
||||
return acceptedFrame;
|
||||
}
|
||||
|
||||
private void queueInputFrameAsynchronous(
|
||||
AppTextureFrame appTextureFrame, TextureInfo inputTexture) {
|
||||
removeFinishedFutures();
|
||||
futures.add(
|
||||
checkStateNotNull(singleThreadExecutorService)
|
||||
.submit(
|
||||
() -> {
|
||||
while (!maybeQueueInputFrameSynchronous(appTextureFrame, inputTexture)) {
|
||||
try {
|
||||
Thread.sleep(RETRY_WAIT_TIME_MS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
if (errorListener != null) {
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
inputListener.onReadyToAcceptInputFrame();
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void releaseOutputFrame(TextureInfo outputTexture) {
|
||||
checkStateNotNull(outputFrames.get(outputTexture)).release();
|
||||
if (isSingleFrameGraph) {
|
||||
inputListener.onReadyToAcceptInputFrame();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release() {
|
||||
checkStateNotNull(frameProcessor).close();
|
||||
if (isSingleFrameGraph) {
|
||||
frameProcessor.close();
|
||||
return;
|
||||
}
|
||||
|
||||
Queue<Future<?>> futures = checkStateNotNull(this.futures);
|
||||
while (!futures.isEmpty()) {
|
||||
futures.remove().cancel(/* mayInterruptIfRunning= */ false);
|
||||
}
|
||||
ExecutorService singleThreadExecutorService =
|
||||
checkStateNotNull(this.singleThreadExecutorService);
|
||||
singleThreadExecutorService.shutdown();
|
||||
try {
|
||||
if (!singleThreadExecutorService.awaitTermination(RELEASE_WAIT_TIME_MS, MILLISECONDS)) {
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException("Release timed out"));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException(e));
|
||||
}
|
||||
|
||||
frameProcessor.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void signalEndOfCurrentInputStream() {
|
||||
if (isSingleFrameGraph) {
|
||||
frameProcessor.waitUntilIdle();
|
||||
outputListener.onCurrentOutputStreamEnded();
|
||||
return;
|
||||
}
|
||||
|
||||
removeFinishedFutures();
|
||||
futures.add(
|
||||
checkStateNotNull(singleThreadExecutorService)
|
||||
.submit(
|
||||
() -> {
|
||||
frameProcessor.waitUntilIdle();
|
||||
outputListener.onCurrentOutputStreamEnded();
|
||||
}));
|
||||
}
|
||||
|
||||
private void removeFinishedFutures() {
|
||||
while (!futures.isEmpty()) {
|
||||
if (!futures.element().isDone()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
futures.remove().get();
|
||||
} catch (ExecutionException e) {
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException(e));
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
errorListener.onFrameProcessingError(new FrameProcessingException(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class CombinedJavadocPlugin implements Plugin<Project> {
|
||||
|
||||
// Dackka snapshots are listed at https://androidx.dev/dackka/builds.
|
||||
static final String DACKKA_JAR_URL =
|
||||
"https://androidx.dev/dackka/builds/8003564/artifacts/dackka-0.0.14.jar"
|
||||
"https://androidx.dev/dackka/builds/9221390/artifacts/dackka-1.0.4-all.jar"
|
||||
|
||||
@Override
|
||||
void apply(Project project) {
|
||||
@ -58,6 +58,11 @@ class CombinedJavadocPlugin implements Plugin<Project> {
|
||||
"media-" + project.ext.androidxMediaVersion + "-api.jar")) {
|
||||
return false;
|
||||
}
|
||||
if (file ==~ /.*\/core-.\..\..-api.jar$/
|
||||
&& !file.path.endsWith(
|
||||
"core-" + project.ext.androidxCoreVersion + "-api.jar")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
classpath +=
|
||||
@ -115,11 +120,16 @@ class CombinedJavadocPlugin implements Plugin<Project> {
|
||||
def sourcesString = project.files(sources.flatten())
|
||||
.filter({ f -> project.file(f).exists() }).join(";")
|
||||
def dependenciesString = project.files(dependencies).asPath.replace(':', ';')
|
||||
def sourceSet = [
|
||||
"-src", sourcesString,
|
||||
"-classpath", dependenciesString,
|
||||
"-documentedVisibilities", "PUBLIC;PROTECTED"
|
||||
].join(" ")
|
||||
args("-moduleName", "",
|
||||
"-outputDir", "$dackkaOutputDir",
|
||||
"-globalLinks", "$globalLinksString",
|
||||
"-loggingLevel", "WARN",
|
||||
"-sourceSet", "-src $sourcesString -classpath $dependenciesString",
|
||||
"-sourceSet", "$sourceSet",
|
||||
"-offlineMode")
|
||||
environment("DEVSITE_TENANT", "androidx/media3")
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Clock;
|
||||
import androidx.media3.common.util.ListenerSet;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.Size;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.android.gms.cast.CastStatusCodes;
|
||||
@ -82,6 +83,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
@UnstableApi
|
||||
public final class CastPlayer extends BasePlayer {
|
||||
|
||||
/** The {@link DeviceInfo} returned by {@link #getDeviceInfo() this player}. */
|
||||
public static final DeviceInfo DEVICE_INFO =
|
||||
new DeviceInfo(DeviceInfo.PLAYBACK_TYPE_REMOTE, /* minVolume= */ 0, /* maxVolume= */ 0);
|
||||
|
||||
static {
|
||||
MediaLibraryInfo.registerModule("media3.cast");
|
||||
}
|
||||
@ -723,16 +728,22 @@ public final class CastPlayer extends BasePlayer {
|
||||
return VideoSize.UNKNOWN;
|
||||
}
|
||||
|
||||
/** This method is not supported and returns {@link Size#UNKNOWN}. */
|
||||
@Override
|
||||
public Size getSurfaceSize() {
|
||||
return Size.UNKNOWN;
|
||||
}
|
||||
|
||||
/** This method is not supported and returns an empty {@link CueGroup}. */
|
||||
@Override
|
||||
public CueGroup getCurrentCues() {
|
||||
return CueGroup.EMPTY;
|
||||
return CueGroup.EMPTY_TIME_ZERO;
|
||||
}
|
||||
|
||||
/** This method is not supported and always returns {@link DeviceInfo#UNKNOWN}. */
|
||||
/** This method always returns {@link CastPlayer#DEVICE_INFO}. */
|
||||
@Override
|
||||
public DeviceInfo getDeviceInfo() {
|
||||
return DeviceInfo.UNKNOWN;
|
||||
return DEVICE_INFO;
|
||||
}
|
||||
|
||||
/** This method is not supported and always returns {@code 0}. */
|
||||
|
@ -62,6 +62,7 @@ import static org.mockito.MockitoAnnotations.initMocks;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.DeviceInfo;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.MimeTypes;
|
||||
@ -1864,6 +1865,14 @@ public class CastPlayerTest {
|
||||
verify(mockListener, never()).onMediaMetadataChanged(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDeviceInfo_returnsCorrectDeviceInfoWithPlaybackTypeRemote() {
|
||||
DeviceInfo deviceInfo = castPlayer.getDeviceInfo();
|
||||
|
||||
assertThat(deviceInfo).isEqualTo(CastPlayer.DEVICE_INFO);
|
||||
assertThat(deviceInfo.playbackType).isEqualTo(DeviceInfo.PLAYBACK_TYPE_REMOTE);
|
||||
}
|
||||
|
||||
private int[] createMediaQueueItemIds(int numberOfIds) {
|
||||
int[] mediaQueueItemIds = new int[numberOfIds];
|
||||
for (int i = 0; i < numberOfIds; i++) {
|
||||
|
@ -25,6 +25,7 @@ import android.view.View;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@ -77,6 +78,7 @@ public final class AdOverlayInfo {
|
||||
*
|
||||
* @return This builder, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setDetailedReason(@Nullable String detailedReason) {
|
||||
this.detailedReason = detailedReason;
|
||||
return this;
|
||||
|
@ -64,6 +64,13 @@ public final class AdPlaybackState implements Bundleable {
|
||||
public final long timeUs;
|
||||
/** The number of ads in the ad group, or {@link C#LENGTH_UNSET} if unknown. */
|
||||
public final int count;
|
||||
/**
|
||||
* The original number of ads in the ad group in case the ad group is only partially available,
|
||||
* or {@link C#LENGTH_UNSET} if unknown. An ad can be partially available when a server side
|
||||
* inserted ad live stream is joined while an ad is already playing and some ad information is
|
||||
* missing.
|
||||
*/
|
||||
public final int originalCount;
|
||||
/** The URI of each ad in the ad group. */
|
||||
public final @NullableType Uri[] uris;
|
||||
/** The state of each ad in the ad group. */
|
||||
@ -88,6 +95,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
this(
|
||||
timeUs,
|
||||
/* count= */ C.LENGTH_UNSET,
|
||||
/* originalCount= */ C.LENGTH_UNSET,
|
||||
/* states= */ new int[0],
|
||||
/* uris= */ new Uri[0],
|
||||
/* durationsUs= */ new long[0],
|
||||
@ -98,6 +106,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
private AdGroup(
|
||||
long timeUs,
|
||||
int count,
|
||||
int originalCount,
|
||||
@AdState int[] states,
|
||||
@NullableType Uri[] uris,
|
||||
long[] durationsUs,
|
||||
@ -106,6 +115,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
checkArgument(states.length == uris.length);
|
||||
this.timeUs = timeUs;
|
||||
this.count = count;
|
||||
this.originalCount = originalCount;
|
||||
this.states = states;
|
||||
this.uris = uris;
|
||||
this.durationsUs = durationsUs;
|
||||
@ -125,6 +135,9 @@ public final class AdPlaybackState implements Bundleable {
|
||||
* Returns the index of the next ad in the ad group that should be played after playing {@code
|
||||
* lastPlayedAdIndex}, or {@link #count} if no later ads should be played. If no ads have been
|
||||
* played, pass -1 to get the index of the first ad to play.
|
||||
*
|
||||
* <p>Note: {@linkplain #isServerSideInserted Server side inserted ads} are always considered
|
||||
* playable.
|
||||
*/
|
||||
public int getNextAdIndexToPlay(@IntRange(from = -1) int lastPlayedAdIndex) {
|
||||
int nextAdIndexToPlay = lastPlayedAdIndex + 1;
|
||||
@ -170,6 +183,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
AdGroup adGroup = (AdGroup) o;
|
||||
return timeUs == adGroup.timeUs
|
||||
&& count == adGroup.count
|
||||
&& originalCount == adGroup.originalCount
|
||||
&& Arrays.equals(uris, adGroup.uris)
|
||||
&& Arrays.equals(states, adGroup.states)
|
||||
&& Arrays.equals(durationsUs, adGroup.durationsUs)
|
||||
@ -180,6 +194,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = count;
|
||||
result = 31 * result + originalCount;
|
||||
result = 31 * result + (int) (timeUs ^ (timeUs >>> 32));
|
||||
result = 31 * result + Arrays.hashCode(uris);
|
||||
result = 31 * result + Arrays.hashCode(states);
|
||||
@ -193,7 +208,14 @@ public final class AdPlaybackState implements Bundleable {
|
||||
@CheckResult
|
||||
public AdGroup withTimeUs(long timeUs) {
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/** Returns a new instance with the ad count set to {@code count}. */
|
||||
@ -203,7 +225,14 @@ public final class AdPlaybackState implements Bundleable {
|
||||
long[] durationsUs = copyDurationsUsWithSpaceForAdCount(this.durationsUs, count);
|
||||
@NullableType Uri[] uris = Arrays.copyOf(this.uris, count);
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -221,7 +250,14 @@ public final class AdPlaybackState implements Bundleable {
|
||||
uris[index] = uri;
|
||||
states[index] = AD_STATE_AVAILABLE;
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -235,7 +271,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
@CheckResult
|
||||
public AdGroup withAdState(@AdState int state, @IntRange(from = 0) int index) {
|
||||
checkArgument(count == C.LENGTH_UNSET || index < count);
|
||||
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, index + 1);
|
||||
@AdState int[] states = copyStatesWithSpaceForAdCount(this.states, /* count= */ index + 1);
|
||||
checkArgument(
|
||||
states[index] == AD_STATE_UNAVAILABLE
|
||||
|| states[index] == AD_STATE_AVAILABLE
|
||||
@ -249,7 +285,14 @@ public final class AdPlaybackState implements Bundleable {
|
||||
this.uris.length == states.length ? this.uris : Arrays.copyOf(this.uris, states.length);
|
||||
states[index] = state;
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/** Returns a new instance with the specified ad durations, in microseconds. */
|
||||
@ -261,21 +304,75 @@ public final class AdPlaybackState implements Bundleable {
|
||||
durationsUs = Arrays.copyOf(durationsUs, uris.length);
|
||||
}
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified {@link #contentResumeOffsetUs}. */
|
||||
@CheckResult
|
||||
public AdGroup withContentResumeOffsetUs(long contentResumeOffsetUs) {
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified value for {@link #isServerSideInserted}. */
|
||||
@CheckResult
|
||||
public AdGroup withIsServerSideInserted(boolean isServerSideInserted) {
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified value for {@link #originalCount}. */
|
||||
public AdGroup withOriginalAdCount(int originalCount) {
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/** Removes the last ad from the ad group. */
|
||||
public AdGroup withLastAdRemoved() {
|
||||
int newCount = states.length - 1;
|
||||
@AdState int[] newStates = Arrays.copyOf(states, newCount);
|
||||
@NullableType Uri[] newUris = Arrays.copyOf(uris, newCount);
|
||||
long[] newDurationsUs = durationsUs;
|
||||
if (durationsUs.length > newCount) {
|
||||
newDurationsUs = Arrays.copyOf(durationsUs, newCount);
|
||||
}
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
newCount,
|
||||
originalCount,
|
||||
newStates,
|
||||
newUris,
|
||||
newDurationsUs,
|
||||
/* contentResumeOffsetUs= */ Util.sum(newDurationsUs),
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -288,6 +385,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
/* count= */ 0,
|
||||
originalCount,
|
||||
/* states= */ new int[0],
|
||||
/* uris= */ new Uri[0],
|
||||
/* durationsUs= */ new long[0],
|
||||
@ -302,7 +400,14 @@ public final class AdPlaybackState implements Bundleable {
|
||||
}
|
||||
}
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -324,7 +429,14 @@ public final class AdPlaybackState implements Bundleable {
|
||||
}
|
||||
}
|
||||
return new AdGroup(
|
||||
timeUs, count, states, uris, durationsUs, contentResumeOffsetUs, isServerSideInserted);
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states,
|
||||
uris,
|
||||
durationsUs,
|
||||
contentResumeOffsetUs,
|
||||
isServerSideInserted);
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
@ -358,6 +470,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
FIELD_DURATIONS_US,
|
||||
FIELD_CONTENT_RESUME_OFFSET_US,
|
||||
FIELD_IS_SERVER_SIDE_INSERTED,
|
||||
FIELD_ORIGINAL_COUNT
|
||||
})
|
||||
private @interface FieldNumber {}
|
||||
|
||||
@ -368,6 +481,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
private static final int FIELD_DURATIONS_US = 4;
|
||||
private static final int FIELD_CONTENT_RESUME_OFFSET_US = 5;
|
||||
private static final int FIELD_IS_SERVER_SIDE_INSERTED = 6;
|
||||
private static final int FIELD_ORIGINAL_COUNT = 7;
|
||||
|
||||
// putParcelableArrayList actually supports null elements.
|
||||
@SuppressWarnings("nullness:argument")
|
||||
@ -376,6 +490,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLong(keyForField(FIELD_TIME_US), timeUs);
|
||||
bundle.putInt(keyForField(FIELD_COUNT), count);
|
||||
bundle.putInt(keyForField(FIELD_ORIGINAL_COUNT), originalCount);
|
||||
bundle.putParcelableArrayList(
|
||||
keyForField(FIELD_URIS), new ArrayList<@NullableType Uri>(Arrays.asList(uris)));
|
||||
bundle.putIntArray(keyForField(FIELD_STATES), states);
|
||||
@ -393,6 +508,8 @@ public final class AdPlaybackState implements Bundleable {
|
||||
private static AdGroup fromBundle(Bundle bundle) {
|
||||
long timeUs = bundle.getLong(keyForField(FIELD_TIME_US));
|
||||
int count = bundle.getInt(keyForField(FIELD_COUNT), /* defaultValue= */ C.LENGTH_UNSET);
|
||||
int originalCount =
|
||||
bundle.getInt(keyForField(FIELD_ORIGINAL_COUNT), /* defaultValue= */ C.LENGTH_UNSET);
|
||||
@Nullable
|
||||
ArrayList<@NullableType Uri> uriList = bundle.getParcelableArrayList(keyForField(FIELD_URIS));
|
||||
@Nullable
|
||||
@ -404,6 +521,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
return new AdGroup(
|
||||
timeUs,
|
||||
count,
|
||||
originalCount,
|
||||
states == null ? new int[0] : states,
|
||||
uriList == null ? new Uri[0] : uriList.toArray(new Uri[0]),
|
||||
durationsUs == null ? new long[0] : durationsUs,
|
||||
@ -470,7 +588,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
*/
|
||||
public final long contentDurationUs;
|
||||
/**
|
||||
* The number of ad groups the have been removed. Ad groups with indices between {@code 0}
|
||||
* The number of ad groups that have been removed. Ad groups with indices between {@code 0}
|
||||
* (inclusive) and {@code removedAdGroupCount} (exclusive) will be empty and must not be modified
|
||||
* by any of the {@code with*} methods.
|
||||
*/
|
||||
@ -639,18 +757,40 @@ public final class AdPlaybackState implements Bundleable {
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified ad URI. */
|
||||
/**
|
||||
* Returns an instance with the specified ad URI and the ad marked as {@linkplain
|
||||
* #AD_STATE_AVAILABLE available}.
|
||||
*
|
||||
* @throws IllegalStateException If {@link Uri#EMPTY} is passed as argument for a client-side
|
||||
* inserted ad group.
|
||||
*/
|
||||
@CheckResult
|
||||
public AdPlaybackState withAdUri(
|
||||
public AdPlaybackState withAvailableAdUri(
|
||||
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup, Uri uri) {
|
||||
int adjustedIndex = adGroupIndex - removedAdGroupCount;
|
||||
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
|
||||
checkState(!Uri.EMPTY.equals(uri) || adGroups[adjustedIndex].isServerSideInserted);
|
||||
adGroups[adjustedIndex] = adGroups[adjustedIndex].withAdUri(uri, adIndexInAdGroup);
|
||||
return new AdPlaybackState(
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified ad marked as played. */
|
||||
/**
|
||||
* Returns an instance with the specified ad marked as {@linkplain #AD_STATE_AVAILABLE available}.
|
||||
*
|
||||
* <p>Must not be called with client side inserted ad groups. Client side inserted ads should use
|
||||
* {@link #withAvailableAdUri}.
|
||||
*
|
||||
* @throws IllegalStateException in case this methods is called on an ad group that {@linkplain
|
||||
* AdGroup#isServerSideInserted is not server side inserted}.
|
||||
*/
|
||||
@CheckResult
|
||||
public AdPlaybackState withAvailableAd(
|
||||
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup) {
|
||||
return withAvailableAdUri(adGroupIndex, adIndexInAdGroup, Uri.EMPTY);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified ad marked as {@linkplain #AD_STATE_PLAYED played}. */
|
||||
@CheckResult
|
||||
public AdPlaybackState withPlayedAd(
|
||||
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup) {
|
||||
@ -662,7 +802,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified ad marked as skipped. */
|
||||
/** Returns an instance with the specified ad marked as {@linkplain #AD_STATE_SKIPPED skipped}. */
|
||||
@CheckResult
|
||||
public AdPlaybackState withSkippedAd(
|
||||
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup) {
|
||||
@ -674,7 +814,20 @@ public final class AdPlaybackState implements Bundleable {
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/** Returns an instance with the specified ad marked as having a load error. */
|
||||
/** Returns an instance with the last ad of the given ad group removed. */
|
||||
@CheckResult
|
||||
public AdPlaybackState withLastAdRemoved(@IntRange(from = 0) int adGroupIndex) {
|
||||
int adjustedIndex = adGroupIndex - removedAdGroupCount;
|
||||
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
|
||||
adGroups[adjustedIndex] = adGroups[adjustedIndex].withLastAdRemoved();
|
||||
return new AdPlaybackState(
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the specified ad marked {@linkplain #AD_STATE_ERROR as having a load
|
||||
* error}.
|
||||
*/
|
||||
@CheckResult
|
||||
public AdPlaybackState withAdLoadError(
|
||||
@IntRange(from = 0) int adGroupIndex, @IntRange(from = 0) int adIndexInAdGroup) {
|
||||
@ -796,6 +949,23 @@ public final class AdPlaybackState implements Bundleable {
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the specified value for {@link AdGroup#originalCount} in the specified
|
||||
* ad group.
|
||||
*/
|
||||
@CheckResult
|
||||
public AdPlaybackState withOriginalAdCount(
|
||||
@IntRange(from = 0) int adGroupIndex, int originalAdCount) {
|
||||
int adjustedIndex = adGroupIndex - removedAdGroupCount;
|
||||
if (adGroups[adjustedIndex].originalCount == originalAdCount) {
|
||||
return this;
|
||||
}
|
||||
AdGroup[] adGroups = Util.nullSafeArrayCopy(this.adGroups, this.adGroups.length);
|
||||
adGroups[adjustedIndex] = adGroups[adjustedIndex].withOriginalAdCount(originalAdCount);
|
||||
return new AdPlaybackState(
|
||||
adsId, adGroups, adResumePositionUs, contentDurationUs, removedAdGroupCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance with the specified value for {@link AdGroup#isServerSideInserted} in the
|
||||
* specified ad group.
|
||||
@ -843,6 +1013,7 @@ public final class AdPlaybackState implements Bundleable {
|
||||
new AdGroup(
|
||||
adGroup.timeUs,
|
||||
adGroup.count,
|
||||
adGroup.originalCount,
|
||||
Arrays.copyOf(adGroup.states, adGroup.states.length),
|
||||
Arrays.copyOf(adGroup.uris, adGroup.uris.length),
|
||||
Arrays.copyOf(adGroup.durationsUs, adGroup.durationsUs.length),
|
||||
|
@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@ -94,30 +95,35 @@ public final class AudioAttributes implements Bundleable {
|
||||
}
|
||||
|
||||
/** See {@link android.media.AudioAttributes.Builder#setContentType(int)} */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setContentType(@C.AudioContentType int contentType) {
|
||||
this.contentType = contentType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link android.media.AudioAttributes.Builder#setFlags(int)} */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setFlags(@C.AudioFlags int flags) {
|
||||
this.flags = flags;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link android.media.AudioAttributes.Builder#setUsage(int)} */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setUsage(@C.AudioUsage int usage) {
|
||||
this.usage = usage;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link android.media.AudioAttributes.Builder#setAllowedCapturePolicy(int)}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAllowedCapturePolicy(@C.AudioAllowedCapturePolicy int allowedCapturePolicy) {
|
||||
this.allowedCapturePolicy = allowedCapturePolicy;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** See {@link android.media.AudioAttributes.Builder#setSpatializationBehavior(int)}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSpatializationBehavior(@C.SpatializationBehavior int spatializationBehavior) {
|
||||
this.spatializationBehavior = spatializationBehavior;
|
||||
return this;
|
||||
|
@ -21,7 +21,8 @@ import static java.lang.Math.min;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import java.util.Collections;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.annotations.ForOverride;
|
||||
import java.util.List;
|
||||
|
||||
/** Abstract base {@link Player} which implements common implementation independent methods. */
|
||||
@ -36,17 +37,17 @@ public abstract class BasePlayer implements Player {
|
||||
|
||||
@Override
|
||||
public final void setMediaItem(MediaItem mediaItem) {
|
||||
setMediaItems(Collections.singletonList(mediaItem));
|
||||
setMediaItems(ImmutableList.of(mediaItem));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setMediaItem(MediaItem mediaItem, long startPositionMs) {
|
||||
setMediaItems(Collections.singletonList(mediaItem), /* startWindowIndex= */ 0, startPositionMs);
|
||||
setMediaItems(ImmutableList.of(mediaItem), /* startIndex= */ 0, startPositionMs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void setMediaItem(MediaItem mediaItem, boolean resetPosition) {
|
||||
setMediaItems(Collections.singletonList(mediaItem), resetPosition);
|
||||
setMediaItems(ImmutableList.of(mediaItem), resetPosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -56,12 +57,12 @@ public abstract class BasePlayer implements Player {
|
||||
|
||||
@Override
|
||||
public final void addMediaItem(int index, MediaItem mediaItem) {
|
||||
addMediaItems(index, Collections.singletonList(mediaItem));
|
||||
addMediaItems(index, ImmutableList.of(mediaItem));
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void addMediaItem(MediaItem mediaItem) {
|
||||
addMediaItems(Collections.singletonList(mediaItem));
|
||||
addMediaItems(ImmutableList.of(mediaItem));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -187,7 +188,12 @@ public abstract class BasePlayer implements Player {
|
||||
@Override
|
||||
public final void seekToPreviousMediaItem() {
|
||||
int previousMediaItemIndex = getPreviousMediaItemIndex();
|
||||
if (previousMediaItemIndex != C.INDEX_UNSET) {
|
||||
if (previousMediaItemIndex == C.INDEX_UNSET) {
|
||||
return;
|
||||
}
|
||||
if (previousMediaItemIndex == getCurrentMediaItemIndex()) {
|
||||
repeatCurrentMediaItem();
|
||||
} else {
|
||||
seekToDefaultPosition(previousMediaItemIndex);
|
||||
}
|
||||
}
|
||||
@ -254,7 +260,12 @@ public abstract class BasePlayer implements Player {
|
||||
@Override
|
||||
public final void seekToNextMediaItem() {
|
||||
int nextMediaItemIndex = getNextMediaItemIndex();
|
||||
if (nextMediaItemIndex != C.INDEX_UNSET) {
|
||||
if (nextMediaItemIndex == C.INDEX_UNSET) {
|
||||
return;
|
||||
}
|
||||
if (nextMediaItemIndex == getCurrentMediaItemIndex()) {
|
||||
repeatCurrentMediaItem();
|
||||
} else {
|
||||
seekToDefaultPosition(nextMediaItemIndex);
|
||||
}
|
||||
}
|
||||
@ -426,6 +437,17 @@ public abstract class BasePlayer implements Player {
|
||||
: timeline.getWindow(getCurrentMediaItemIndex(), window).getDurationMs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeat the current media item.
|
||||
*
|
||||
* <p>The default implementation seeks to the default position in the current item, which can be
|
||||
* overridden for additional handling.
|
||||
*/
|
||||
@ForOverride
|
||||
protected void repeatCurrentMediaItem() {
|
||||
seekToDefaultPosition();
|
||||
}
|
||||
|
||||
private @RepeatMode int getRepeatModeForNavigation() {
|
||||
@RepeatMode int repeatMode = getRepeatMode();
|
||||
return repeatMode == REPEAT_MODE_ONE ? REPEAT_MODE_OFF : repeatMode;
|
||||
|
@ -1044,29 +1044,31 @@ public final class C {
|
||||
*/
|
||||
@UnstableApi public static final int STEREO_MODE_STEREO_MESH = 3;
|
||||
|
||||
// LINT.IfChange(color_space)
|
||||
/**
|
||||
* Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT709}, {@link
|
||||
* #COLOR_SPACE_BT601} or {@link #COLOR_SPACE_BT2020}.
|
||||
* Video colorspaces. One of {@link Format#NO_VALUE}, {@link #COLOR_SPACE_BT601}, {@link
|
||||
* #COLOR_SPACE_BT709} or {@link #COLOR_SPACE_BT2020}.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef({Format.NO_VALUE, COLOR_SPACE_BT709, COLOR_SPACE_BT601, COLOR_SPACE_BT2020})
|
||||
@IntDef({Format.NO_VALUE, COLOR_SPACE_BT601, COLOR_SPACE_BT709, COLOR_SPACE_BT2020})
|
||||
public @interface ColorSpace {}
|
||||
/**
|
||||
* @see MediaFormat#COLOR_STANDARD_BT709
|
||||
*/
|
||||
@UnstableApi public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
|
||||
/**
|
||||
* @see MediaFormat#COLOR_STANDARD_BT601_PAL
|
||||
*/
|
||||
@UnstableApi public static final int COLOR_SPACE_BT601 = MediaFormat.COLOR_STANDARD_BT601_PAL;
|
||||
/**
|
||||
* @see MediaFormat#COLOR_STANDARD_BT709
|
||||
*/
|
||||
@UnstableApi public static final int COLOR_SPACE_BT709 = MediaFormat.COLOR_STANDARD_BT709;
|
||||
/**
|
||||
* @see MediaFormat#COLOR_STANDARD_BT2020
|
||||
*/
|
||||
@UnstableApi public static final int COLOR_SPACE_BT2020 = MediaFormat.COLOR_STANDARD_BT2020;
|
||||
|
||||
// LINT.IfChange(color_transfer)
|
||||
/**
|
||||
* Video color transfer characteristics. One of {@link Format#NO_VALUE}, {@link
|
||||
* #COLOR_TRANSFER_SDR}, {@link #COLOR_TRANSFER_ST2084} or {@link #COLOR_TRANSFER_HLG}.
|
||||
@ -1090,6 +1092,7 @@ public final class C {
|
||||
*/
|
||||
@UnstableApi public static final int COLOR_TRANSFER_HLG = MediaFormat.COLOR_TRANSFER_HLG;
|
||||
|
||||
// LINT.IfChange(color_range)
|
||||
/**
|
||||
* Video color range. One of {@link Format#NO_VALUE}, {@link #COLOR_RANGE_LIMITED} or {@link
|
||||
* #COLOR_RANGE_FULL}.
|
||||
|
@ -28,10 +28,23 @@ import java.lang.annotation.Target;
|
||||
import java.util.Arrays;
|
||||
import org.checkerframework.dataflow.qual.Pure;
|
||||
|
||||
/** Stores color info. */
|
||||
/**
|
||||
* Stores color info.
|
||||
*
|
||||
* <p>When a {@code null} {@code ColorInfo} instance is used, this often represents a generic {@link
|
||||
* #SDR_BT709_LIMITED} instance.
|
||||
*/
|
||||
@UnstableApi
|
||||
public final class ColorInfo implements Bundleable {
|
||||
|
||||
/** Color info representing SDR BT.709 limited range, which is a common SDR video color format. */
|
||||
public static final ColorInfo SDR_BT709_LIMITED =
|
||||
new ColorInfo(
|
||||
C.COLOR_SPACE_BT709,
|
||||
C.COLOR_RANGE_LIMITED,
|
||||
C.COLOR_TRANSFER_SDR,
|
||||
/* hdrStaticInfo= */ null);
|
||||
|
||||
/**
|
||||
* Returns the {@link C.ColorSpace} corresponding to the given ISO color primary code, as per
|
||||
* table A.7.21.1 in Rec. ITU-T T.832 (03/2009), or {@link Format#NO_VALUE} if no mapping can be
|
||||
@ -76,6 +89,13 @@ public final class ColorInfo implements Bundleable {
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns whether the {@code ColorInfo} uses an HDR {@link C.ColorTransfer}. */
|
||||
public static boolean isTransferHdr(@Nullable ColorInfo colorInfo) {
|
||||
return colorInfo != null
|
||||
&& colorInfo.colorTransfer != Format.NO_VALUE
|
||||
&& colorInfo.colorTransfer != C.COLOR_TRANSFER_SDR;
|
||||
}
|
||||
|
||||
/**
|
||||
* The color space of the video. Valid values are {@link C#COLOR_SPACE_BT601}, {@link
|
||||
* C#COLOR_SPACE_BT709}, {@link C#COLOR_SPACE_BT2020} or {@link Format#NO_VALUE} if unknown.
|
||||
|
@ -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);
|
||||
}
|
@ -13,21 +13,11 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.transformer;
|
||||
|
||||
package androidx.media3.common;
|
||||
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/**
|
||||
* Interface for a video frame effect with a {@link SingleFrameGlTextureProcessor} implementation.
|
||||
*
|
||||
* <p>Implementations contain information specifying the effect and can be {@linkplain
|
||||
* #toGlTextureProcessor() converted} to a {@link SingleFrameGlTextureProcessor} which applies the
|
||||
* effect.
|
||||
*/
|
||||
/** Marker interface for a video frame effect. */
|
||||
@UnstableApi
|
||||
public interface GlEffect {
|
||||
|
||||
/** Returns a {@link SingleFrameGlTextureProcessor} that applies the effect. */
|
||||
// TODO(b/227625423): use GlTextureProcessor here once this interface exists.
|
||||
SingleFrameGlTextureProcessor toGlTextureProcessor();
|
||||
}
|
||||
public interface Effect {}
|
@ -22,6 +22,7 @@ import android.util.SparseBooleanArray;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
|
||||
/**
|
||||
* A set of integer flags.
|
||||
@ -53,6 +54,7 @@ public final class FlagSet {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder add(int flag) {
|
||||
checkState(!buildCalled);
|
||||
flags.append(flag, /* value= */ true);
|
||||
@ -67,6 +69,7 @@ public final class FlagSet {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder addIf(int flag, boolean condition) {
|
||||
if (condition) {
|
||||
return add(flag);
|
||||
@ -81,6 +84,7 @@ public final class FlagSet {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder addAll(int... flags) {
|
||||
for (int flag : flags) {
|
||||
add(flag);
|
||||
@ -95,6 +99,7 @@ public final class FlagSet {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder addAll(FlagSet flags) {
|
||||
for (int i = 0; i < flags.size(); i++) {
|
||||
add(flags.get(i));
|
||||
@ -109,6 +114,7 @@ public final class FlagSet {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder remove(int flag) {
|
||||
checkState(!buildCalled);
|
||||
flags.delete(flag);
|
||||
@ -123,6 +129,7 @@ public final class FlagSet {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder removeIf(int flag, boolean condition) {
|
||||
if (condition) {
|
||||
return remove(flag);
|
||||
@ -137,6 +144,7 @@ public final class FlagSet {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder removeAll(int... flags) {
|
||||
for (int flag : flags) {
|
||||
remove(flag);
|
||||
|
@ -24,6 +24,7 @@ import androidx.media3.common.util.BundleableUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@ -248,6 +249,7 @@ public final class Format implements Bundleable {
|
||||
* @param id The {@link Format#id}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setId(@Nullable String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
@ -260,6 +262,7 @@ public final class Format implements Bundleable {
|
||||
* @param id The {@link Format#id}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setId(int id) {
|
||||
this.id = Integer.toString(id);
|
||||
return this;
|
||||
@ -271,6 +274,7 @@ public final class Format implements Bundleable {
|
||||
* @param label The {@link Format#label}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLabel(@Nullable String label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
@ -282,6 +286,7 @@ public final class Format implements Bundleable {
|
||||
* @param language The {@link Format#language}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLanguage(@Nullable String language) {
|
||||
this.language = language;
|
||||
return this;
|
||||
@ -293,6 +298,7 @@ public final class Format implements Bundleable {
|
||||
* @param selectionFlags The {@link Format#selectionFlags}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSelectionFlags(@C.SelectionFlags int selectionFlags) {
|
||||
this.selectionFlags = selectionFlags;
|
||||
return this;
|
||||
@ -304,6 +310,7 @@ public final class Format implements Bundleable {
|
||||
* @param roleFlags The {@link Format#roleFlags}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setRoleFlags(@C.RoleFlags int roleFlags) {
|
||||
this.roleFlags = roleFlags;
|
||||
return this;
|
||||
@ -315,6 +322,7 @@ public final class Format implements Bundleable {
|
||||
* @param averageBitrate The {@link Format#averageBitrate}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAverageBitrate(int averageBitrate) {
|
||||
this.averageBitrate = averageBitrate;
|
||||
return this;
|
||||
@ -326,6 +334,7 @@ public final class Format implements Bundleable {
|
||||
* @param peakBitrate The {@link Format#peakBitrate}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPeakBitrate(int peakBitrate) {
|
||||
this.peakBitrate = peakBitrate;
|
||||
return this;
|
||||
@ -337,6 +346,7 @@ public final class Format implements Bundleable {
|
||||
* @param codecs The {@link Format#codecs}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setCodecs(@Nullable String codecs) {
|
||||
this.codecs = codecs;
|
||||
return this;
|
||||
@ -348,6 +358,7 @@ public final class Format implements Bundleable {
|
||||
* @param metadata The {@link Format#metadata}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMetadata(@Nullable Metadata metadata) {
|
||||
this.metadata = metadata;
|
||||
return this;
|
||||
@ -361,6 +372,7 @@ public final class Format implements Bundleable {
|
||||
* @param containerMimeType The {@link Format#containerMimeType}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setContainerMimeType(@Nullable String containerMimeType) {
|
||||
this.containerMimeType = containerMimeType;
|
||||
return this;
|
||||
@ -374,6 +386,7 @@ public final class Format implements Bundleable {
|
||||
* @param sampleMimeType {@link Format#sampleMimeType}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSampleMimeType(@Nullable String sampleMimeType) {
|
||||
this.sampleMimeType = sampleMimeType;
|
||||
return this;
|
||||
@ -385,6 +398,7 @@ public final class Format implements Bundleable {
|
||||
* @param maxInputSize The {@link Format#maxInputSize}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxInputSize(int maxInputSize) {
|
||||
this.maxInputSize = maxInputSize;
|
||||
return this;
|
||||
@ -396,6 +410,7 @@ public final class Format implements Bundleable {
|
||||
* @param initializationData The {@link Format#initializationData}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setInitializationData(@Nullable List<byte[]> initializationData) {
|
||||
this.initializationData = initializationData;
|
||||
return this;
|
||||
@ -407,6 +422,7 @@ public final class Format implements Bundleable {
|
||||
* @param drmInitData The {@link Format#drmInitData}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setDrmInitData(@Nullable DrmInitData drmInitData) {
|
||||
this.drmInitData = drmInitData;
|
||||
return this;
|
||||
@ -418,6 +434,7 @@ public final class Format implements Bundleable {
|
||||
* @param subsampleOffsetUs The {@link Format#subsampleOffsetUs}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSubsampleOffsetUs(long subsampleOffsetUs) {
|
||||
this.subsampleOffsetUs = subsampleOffsetUs;
|
||||
return this;
|
||||
@ -431,6 +448,7 @@ public final class Format implements Bundleable {
|
||||
* @param width The {@link Format#width}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setWidth(int width) {
|
||||
this.width = width;
|
||||
return this;
|
||||
@ -442,6 +460,7 @@ public final class Format implements Bundleable {
|
||||
* @param height The {@link Format#height}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setHeight(int height) {
|
||||
this.height = height;
|
||||
return this;
|
||||
@ -453,6 +472,7 @@ public final class Format implements Bundleable {
|
||||
* @param frameRate The {@link Format#frameRate}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setFrameRate(float frameRate) {
|
||||
this.frameRate = frameRate;
|
||||
return this;
|
||||
@ -464,6 +484,7 @@ public final class Format implements Bundleable {
|
||||
* @param rotationDegrees The {@link Format#rotationDegrees}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setRotationDegrees(int rotationDegrees) {
|
||||
this.rotationDegrees = rotationDegrees;
|
||||
return this;
|
||||
@ -475,6 +496,7 @@ public final class Format implements Bundleable {
|
||||
* @param pixelWidthHeightRatio The {@link Format#pixelWidthHeightRatio}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPixelWidthHeightRatio(float pixelWidthHeightRatio) {
|
||||
this.pixelWidthHeightRatio = pixelWidthHeightRatio;
|
||||
return this;
|
||||
@ -486,6 +508,7 @@ public final class Format implements Bundleable {
|
||||
* @param projectionData The {@link Format#projectionData}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setProjectionData(@Nullable byte[] projectionData) {
|
||||
this.projectionData = projectionData;
|
||||
return this;
|
||||
@ -497,6 +520,7 @@ public final class Format implements Bundleable {
|
||||
* @param stereoMode The {@link Format#stereoMode}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setStereoMode(@C.StereoMode int stereoMode) {
|
||||
this.stereoMode = stereoMode;
|
||||
return this;
|
||||
@ -508,6 +532,7 @@ public final class Format implements Bundleable {
|
||||
* @param colorInfo The {@link Format#colorInfo}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setColorInfo(@Nullable ColorInfo colorInfo) {
|
||||
this.colorInfo = colorInfo;
|
||||
return this;
|
||||
@ -521,6 +546,7 @@ public final class Format implements Bundleable {
|
||||
* @param channelCount The {@link Format#channelCount}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setChannelCount(int channelCount) {
|
||||
this.channelCount = channelCount;
|
||||
return this;
|
||||
@ -532,6 +558,7 @@ public final class Format implements Bundleable {
|
||||
* @param sampleRate The {@link Format#sampleRate}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSampleRate(int sampleRate) {
|
||||
this.sampleRate = sampleRate;
|
||||
return this;
|
||||
@ -543,6 +570,7 @@ public final class Format implements Bundleable {
|
||||
* @param pcmEncoding The {@link Format#pcmEncoding}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPcmEncoding(@C.PcmEncoding int pcmEncoding) {
|
||||
this.pcmEncoding = pcmEncoding;
|
||||
return this;
|
||||
@ -554,6 +582,7 @@ public final class Format implements Bundleable {
|
||||
* @param encoderDelay The {@link Format#encoderDelay}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setEncoderDelay(int encoderDelay) {
|
||||
this.encoderDelay = encoderDelay;
|
||||
return this;
|
||||
@ -565,6 +594,7 @@ public final class Format implements Bundleable {
|
||||
* @param encoderPadding The {@link Format#encoderPadding}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setEncoderPadding(int encoderPadding) {
|
||||
this.encoderPadding = encoderPadding;
|
||||
return this;
|
||||
@ -578,6 +608,7 @@ public final class Format implements Bundleable {
|
||||
* @param accessibilityChannel The {@link Format#accessibilityChannel}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAccessibilityChannel(int accessibilityChannel) {
|
||||
this.accessibilityChannel = accessibilityChannel;
|
||||
return this;
|
||||
@ -591,6 +622,7 @@ public final class Format implements Bundleable {
|
||||
* @param cryptoType The {@link C.CryptoType}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setCryptoType(@C.CryptoType int cryptoType) {
|
||||
this.cryptoType = cryptoType;
|
||||
return this;
|
||||
@ -1515,6 +1547,15 @@ public final class Format implements Bundleable {
|
||||
@UnstableApi
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
return toBundle(/* excludeMetadata= */ false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Bundle} representing the information stored in this object. If {@code
|
||||
* excludeMetadata} is true, {@linkplain Format#metadata metadata} is excluded.
|
||||
*/
|
||||
@UnstableApi
|
||||
public Bundle toBundle(boolean excludeMetadata) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(keyForField(FIELD_ID), id);
|
||||
bundle.putString(keyForField(FIELD_LABEL), label);
|
||||
@ -1524,10 +1565,10 @@ public final class Format implements Bundleable {
|
||||
bundle.putInt(keyForField(FIELD_AVERAGE_BITRATE), averageBitrate);
|
||||
bundle.putInt(keyForField(FIELD_PEAK_BITRATE), peakBitrate);
|
||||
bundle.putString(keyForField(FIELD_CODECS), codecs);
|
||||
// Metadata is currently not Bundleable because Metadata.Entry is an Interface,
|
||||
// which would be difficult to unbundle in a backward compatible way.
|
||||
// The entries are additionally of limited usefulness to remote processes.
|
||||
bundle.putParcelable(keyForField(FIELD_METADATA), metadata);
|
||||
if (!excludeMetadata) {
|
||||
// TODO (internal ref: b/239701618)
|
||||
bundle.putParcelable(keyForField(FIELD_METADATA), metadata);
|
||||
}
|
||||
// Container specific.
|
||||
bundle.putString(keyForField(FIELD_CONTAINER_MIME_TYPE), containerMimeType);
|
||||
// Sample specific.
|
||||
|
@ -23,6 +23,7 @@ import android.view.TextureView;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.Size;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import java.util.List;
|
||||
|
||||
@ -759,6 +760,12 @@ public class ForwardingPlayer implements Player {
|
||||
return player.getVideoSize();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#getSurfaceSize()} on the delegate and returns the result. */
|
||||
@Override
|
||||
public Size getSurfaceSize() {
|
||||
return player.getSurfaceSize();
|
||||
}
|
||||
|
||||
/** Calls {@link Player#clearVideoSurface()} on the delegate. */
|
||||
@Override
|
||||
public void clearVideoSurface() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -13,15 +13,34 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.transformer;
|
||||
package androidx.media3.common;
|
||||
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
/** Thrown when an exception occurs while applying effects to video frames. */
|
||||
@UnstableApi
|
||||
public final class FrameProcessingException extends Exception {
|
||||
|
||||
/**
|
||||
* Wraps the given exception in a {@code FrameProcessingException} if it is not already a {@code
|
||||
* FrameProcessingException} and returns the exception otherwise.
|
||||
*/
|
||||
public static FrameProcessingException from(Exception exception) {
|
||||
return from(exception, /* presentationTimeUs= */ C.TIME_UNSET);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the given exception in a {@code FrameProcessingException} with the given timestamp if it
|
||||
* is not already a {@code FrameProcessingException} and returns the exception otherwise.
|
||||
*/
|
||||
public static FrameProcessingException from(Exception exception, long presentationTimeUs) {
|
||||
if (exception instanceof FrameProcessingException) {
|
||||
return (FrameProcessingException) exception;
|
||||
} else {
|
||||
return new FrameProcessingException(exception, presentationTimeUs);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The microsecond timestamp of the frame being processed while the exception occurred or {@link
|
||||
* C#TIME_UNSET} if unknown.
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import com.google.errorprone.annotations.InlineMe;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
@ -126,6 +127,7 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>By default {@link #DEFAULT_MEDIA_ID} is used.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMediaId(String mediaId) {
|
||||
this.mediaId = checkNotNull(mediaId);
|
||||
return this;
|
||||
@ -138,6 +140,7 @@ public final class MediaItem implements Bundleable {
|
||||
* during {@link #build()} and no other {@code Builder} methods that would populate {@link
|
||||
* MediaItem#localConfiguration} should be called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setUri(@Nullable String uri) {
|
||||
return setUri(uri == null ? null : Uri.parse(uri));
|
||||
}
|
||||
@ -149,6 +152,7 @@ public final class MediaItem implements Bundleable {
|
||||
* during {@link #build()} and no other {@code Builder} methods that would populate {@link
|
||||
* MediaItem#localConfiguration} should be called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setUri(@Nullable Uri uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
@ -163,12 +167,14 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* @param mimeType The MIME type.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMimeType(@Nullable String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the {@link ClippingConfiguration}, defaults to {@link ClippingConfiguration#UNSET}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setClippingConfiguration(ClippingConfiguration clippingConfiguration) {
|
||||
this.clippingConfiguration = clippingConfiguration.buildUpon();
|
||||
return this;
|
||||
@ -178,6 +184,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
|
||||
* ClippingConfiguration.Builder#setStartPositionMs(long)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setClipStartPositionMs(@IntRange(from = 0) long startPositionMs) {
|
||||
@ -189,6 +196,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
|
||||
* ClippingConfiguration.Builder#setEndPositionMs(long)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setClipEndPositionMs(long endPositionMs) {
|
||||
@ -200,6 +208,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
|
||||
* ClippingConfiguration.Builder#setRelativeToLiveWindow(boolean)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setClipRelativeToLiveWindow(boolean relativeToLiveWindow) {
|
||||
@ -211,6 +220,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
|
||||
* ClippingConfiguration.Builder#setRelativeToDefaultPosition(boolean)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setClipRelativeToDefaultPosition(boolean relativeToDefaultPosition) {
|
||||
@ -222,6 +232,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setClippingConfiguration(ClippingConfiguration)} and {@link
|
||||
* ClippingConfiguration.Builder#setStartsAtKeyFrame(boolean)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setClipStartsAtKeyFrame(boolean startsAtKeyFrame) {
|
||||
@ -230,6 +241,7 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the optional DRM configuration. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setDrmConfiguration(@Nullable DrmConfiguration drmConfiguration) {
|
||||
this.drmConfiguration =
|
||||
drmConfiguration != null ? drmConfiguration.buildUpon() : new DrmConfiguration.Builder();
|
||||
@ -240,6 +252,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
|
||||
* DrmConfiguration.Builder#setLicenseUri(Uri)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmLicenseUri(@Nullable Uri licenseUri) {
|
||||
@ -251,6 +264,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
|
||||
* DrmConfiguration.Builder#setLicenseUri(String)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmLicenseUri(@Nullable String licenseUri) {
|
||||
@ -264,6 +278,7 @@ public final class MediaItem implements Bundleable {
|
||||
* DrmConfiguration.Builder#setLicenseRequestHeaders(Map)} doesn't accept null, use an empty
|
||||
* map to clear the headers.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmLicenseRequestHeaders(
|
||||
@ -277,6 +292,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and pass the {@code uuid} to
|
||||
* {@link DrmConfiguration.Builder#Builder(UUID)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmUuid(@Nullable UUID uuid) {
|
||||
@ -288,6 +304,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
|
||||
* DrmConfiguration.Builder#setMultiSession(boolean)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmMultiSession(boolean multiSession) {
|
||||
@ -299,6 +316,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
|
||||
* DrmConfiguration.Builder#setForceDefaultLicenseUri(boolean)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmForceDefaultLicenseUri(boolean forceDefaultLicenseUri) {
|
||||
@ -310,6 +328,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
|
||||
* DrmConfiguration.Builder#setPlayClearContentWithoutKey(boolean)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmPlayClearContentWithoutKey(boolean playClearContentWithoutKey) {
|
||||
@ -321,6 +340,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
|
||||
* DrmConfiguration.Builder#setForceSessionsForAudioAndVideoTracks(boolean)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmSessionForClearPeriods(boolean sessionForClearPeriods) {
|
||||
@ -334,6 +354,7 @@ public final class MediaItem implements Bundleable {
|
||||
* DrmConfiguration.Builder#setForcedSessionTrackTypes(List)} doesn't accept null, use an
|
||||
* empty list to clear the contents.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmSessionForClearTypes(
|
||||
@ -347,6 +368,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setDrmConfiguration(DrmConfiguration)} and {@link
|
||||
* DrmConfiguration.Builder#setKeySetId(byte[])} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setDrmKeySetId(@Nullable byte[] keySetId) {
|
||||
@ -363,6 +385,7 @@ public final class MediaItem implements Bundleable {
|
||||
* <p>If {@link #setUri} is passed a non-null {@code uri}, the stream keys are used to create a
|
||||
* {@link LocalConfiguration} object. Otherwise they will be ignored.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Builder setStreamKeys(@Nullable List<StreamKey> streamKeys) {
|
||||
this.streamKeys =
|
||||
@ -377,6 +400,7 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Builder setCustomCacheKey(@Nullable String customCacheKey) {
|
||||
this.customCacheKey = customCacheKey;
|
||||
@ -388,6 +412,7 @@ public final class MediaItem implements Bundleable {
|
||||
* #setSubtitleConfigurations(List)} doesn't accept null, use an empty list to clear the
|
||||
* contents.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setSubtitles(@Nullable List<Subtitle> subtitles) {
|
||||
@ -401,6 +426,7 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSubtitleConfigurations(List<SubtitleConfiguration> subtitleConfigurations) {
|
||||
this.subtitleConfigurations = ImmutableList.copyOf(subtitleConfigurations);
|
||||
return this;
|
||||
@ -411,6 +437,7 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAdsConfiguration(@Nullable AdsConfiguration adsConfiguration) {
|
||||
this.adsConfiguration = adsConfiguration;
|
||||
return this;
|
||||
@ -421,6 +448,7 @@ public final class MediaItem implements Bundleable {
|
||||
* with {@link Uri#parse(String)} and pass the result to {@link
|
||||
* AdsConfiguration.Builder#Builder(Uri)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setAdTagUri(@Nullable String adTagUri) {
|
||||
@ -431,6 +459,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setAdsConfiguration(AdsConfiguration)} and pass the {@code adTagUri}
|
||||
* to {@link AdsConfiguration.Builder#Builder(Uri)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setAdTagUri(@Nullable Uri adTagUri) {
|
||||
@ -442,6 +471,7 @@ public final class MediaItem implements Bundleable {
|
||||
* {@link AdsConfiguration.Builder#Builder(Uri)} and the {@code adsId} to {@link
|
||||
* AdsConfiguration.Builder#setAdsId(Object)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setAdTagUri(@Nullable Uri adTagUri, @Nullable Object adsId) {
|
||||
@ -451,6 +481,7 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the {@link LiveConfiguration}. Defaults to {@link LiveConfiguration#UNSET}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLiveConfiguration(LiveConfiguration liveConfiguration) {
|
||||
this.liveConfiguration = liveConfiguration.buildUpon();
|
||||
return this;
|
||||
@ -460,6 +491,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
|
||||
* LiveConfiguration.Builder#setTargetOffsetMs(long)}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setLiveTargetOffsetMs(long liveTargetOffsetMs) {
|
||||
@ -471,6 +503,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
|
||||
* LiveConfiguration.Builder#setMinOffsetMs(long)}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setLiveMinOffsetMs(long liveMinOffsetMs) {
|
||||
@ -482,6 +515,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
|
||||
* LiveConfiguration.Builder#setMaxOffsetMs(long)}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setLiveMaxOffsetMs(long liveMaxOffsetMs) {
|
||||
@ -493,6 +527,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
|
||||
* LiveConfiguration.Builder#setMinPlaybackSpeed(float)}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setLiveMinPlaybackSpeed(float minPlaybackSpeed) {
|
||||
@ -504,6 +539,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated Use {@link #setLiveConfiguration(LiveConfiguration)} and {@link
|
||||
* LiveConfiguration.Builder#setMaxPlaybackSpeed(float)}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setLiveMaxPlaybackSpeed(float maxPlaybackSpeed) {
|
||||
@ -518,18 +554,21 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>This method should only be called if {@link #setUri} is passed a non-null value.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setTag(@Nullable Object tag) {
|
||||
this.tag = tag;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the media metadata. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMediaMetadata(MediaMetadata mediaMetadata) {
|
||||
this.mediaMetadata = mediaMetadata;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the request metadata. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setRequestMetadata(RequestMetadata requestMetadata) {
|
||||
this.requestMetadata = requestMetadata;
|
||||
return this;
|
||||
@ -613,6 +652,7 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the {@link UUID} of the protection scheme. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setScheme(UUID scheme) {
|
||||
this.scheme = scheme;
|
||||
return this;
|
||||
@ -622,6 +662,7 @@ public final class MediaItem implements Bundleable {
|
||||
* @deprecated This only exists to support the deprecated {@link
|
||||
* MediaItem.Builder#setDrmUuid(UUID)}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@Deprecated
|
||||
private Builder setNullableScheme(@Nullable UUID scheme) {
|
||||
this.scheme = scheme;
|
||||
@ -629,24 +670,28 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the optional default DRM license server URI. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLicenseUri(@Nullable Uri licenseUri) {
|
||||
this.licenseUri = licenseUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the optional default DRM license server URI. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLicenseUri(@Nullable String licenseUri) {
|
||||
this.licenseUri = licenseUri == null ? null : Uri.parse(licenseUri);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the optional request headers attached to DRM license requests. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLicenseRequestHeaders(Map<String, String> licenseRequestHeaders) {
|
||||
this.licenseRequestHeaders = ImmutableMap.copyOf(licenseRequestHeaders);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets whether multi session is enabled. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMultiSession(boolean multiSession) {
|
||||
this.multiSession = multiSession;
|
||||
return this;
|
||||
@ -656,6 +701,7 @@ public final class MediaItem implements Bundleable {
|
||||
* Sets whether to always use the default DRM license server URI even if the media specifies
|
||||
* its own DRM license server URI.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setForceDefaultLicenseUri(boolean forceDefaultLicenseUri) {
|
||||
this.forceDefaultLicenseUri = forceDefaultLicenseUri;
|
||||
return this;
|
||||
@ -665,6 +711,7 @@ public final class MediaItem implements Bundleable {
|
||||
* Sets whether clear samples within protected content should be played when keys for the
|
||||
* encrypted part of the content have yet to be loaded.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPlayClearContentWithoutKey(boolean playClearContentWithoutKey) {
|
||||
this.playClearContentWithoutKey = playClearContentWithoutKey;
|
||||
return this;
|
||||
@ -673,6 +720,7 @@ public final class MediaItem implements Bundleable {
|
||||
/**
|
||||
* @deprecated Use {@link #setForceSessionsForAudioAndVideoTracks(boolean)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
@InlineMe(
|
||||
@ -690,6 +738,7 @@ public final class MediaItem implements Bundleable {
|
||||
* <p>This method overrides what has been set by previously calling {@link
|
||||
* #setForcedSessionTrackTypes(List)}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setForceSessionsForAudioAndVideoTracks(
|
||||
boolean forceSessionsForAudioAndVideoTracks) {
|
||||
this.setForcedSessionTrackTypes(
|
||||
@ -709,6 +758,7 @@ public final class MediaItem implements Bundleable {
|
||||
* <p>This method overrides what has been set by previously calling {@link
|
||||
* #setForceSessionsForAudioAndVideoTracks(boolean)}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setForcedSessionTrackTypes(
|
||||
List<@C.TrackType Integer> forcedSessionTrackTypes) {
|
||||
this.forcedSessionTrackTypes = ImmutableList.copyOf(forcedSessionTrackTypes);
|
||||
@ -722,6 +772,7 @@ public final class MediaItem implements Bundleable {
|
||||
* release an existing offline license (see {@code DefaultDrmSessionManager#setMode(int
|
||||
* mode,byte[] offlineLicenseKeySetId)}).
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setKeySetId(@Nullable byte[] keySetId) {
|
||||
this.keySetId = keySetId != null ? Arrays.copyOf(keySetId, keySetId.length) : null;
|
||||
return this;
|
||||
@ -864,6 +915,7 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the ad tag URI to load. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAdTagUri(Uri adTagUri) {
|
||||
this.adTagUri = adTagUri;
|
||||
return this;
|
||||
@ -875,6 +927,7 @@ public final class MediaItem implements Bundleable {
|
||||
* <p>See details on {@link AdsConfiguration#adsId} for how the ads identifier is used and how
|
||||
* it's calculated if not explicitly set.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAdsId(@Nullable Object adsId) {
|
||||
this.adsId = adsId;
|
||||
return this;
|
||||
@ -1093,6 +1146,7 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>Defaults to {@link C#TIME_UNSET}, indicating the media-defined default will be used.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setTargetOffsetMs(long targetOffsetMs) {
|
||||
this.targetOffsetMs = targetOffsetMs;
|
||||
return this;
|
||||
@ -1105,6 +1159,7 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>Defaults to {@link C#TIME_UNSET}, indicating the media-defined default will be used.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMinOffsetMs(long minOffsetMs) {
|
||||
this.minOffsetMs = minOffsetMs;
|
||||
return this;
|
||||
@ -1117,6 +1172,7 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>Defaults to {@link C#TIME_UNSET}, indicating the media-defined default will be used.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxOffsetMs(long maxOffsetMs) {
|
||||
this.maxOffsetMs = maxOffsetMs;
|
||||
return this;
|
||||
@ -1127,6 +1183,7 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>Defaults to {@link C#RATE_UNSET}, indicating the media-defined default will be used.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMinPlaybackSpeed(float minPlaybackSpeed) {
|
||||
this.minPlaybackSpeed = minPlaybackSpeed;
|
||||
return this;
|
||||
@ -1137,6 +1194,7 @@ public final class MediaItem implements Bundleable {
|
||||
*
|
||||
* <p>Defaults to {@link C#RATE_UNSET}, indicating the media-defined default will be used.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxPlaybackSpeed(float maxPlaybackSpeed) {
|
||||
this.maxPlaybackSpeed = maxPlaybackSpeed;
|
||||
return this;
|
||||
@ -1329,42 +1387,49 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the {@link Uri} to the subtitle file. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setUri(Uri uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the MIME type. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMimeType(@Nullable String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the optional language of the subtitle file. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLanguage(@Nullable String language) {
|
||||
this.language = language;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the flags used for track selection. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSelectionFlags(@C.SelectionFlags int selectionFlags) {
|
||||
this.selectionFlags = selectionFlags;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the role flags. These are used for track selection. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setRoleFlags(@C.RoleFlags int roleFlags) {
|
||||
this.roleFlags = roleFlags;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the optional label for this subtitle track. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLabel(@Nullable String label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the optional ID for this subtitle track. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setId(@Nullable String id) {
|
||||
this.id = id;
|
||||
return this;
|
||||
@ -1541,6 +1606,7 @@ public final class MediaItem implements Bundleable {
|
||||
* Sets the optional start position in milliseconds which must be a value larger than or equal
|
||||
* to zero (Default: 0).
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setStartPositionMs(@IntRange(from = 0) long startPositionMs) {
|
||||
Assertions.checkArgument(startPositionMs >= 0);
|
||||
this.startPositionMs = startPositionMs;
|
||||
@ -1552,6 +1618,7 @@ public final class MediaItem implements Bundleable {
|
||||
* to zero, or {@link C#TIME_END_OF_SOURCE} to end when playback reaches the end of media
|
||||
* (Default: {@link C#TIME_END_OF_SOURCE}).
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setEndPositionMs(long endPositionMs) {
|
||||
Assertions.checkArgument(endPositionMs == C.TIME_END_OF_SOURCE || endPositionMs >= 0);
|
||||
this.endPositionMs = endPositionMs;
|
||||
@ -1563,6 +1630,7 @@ public final class MediaItem implements Bundleable {
|
||||
* {@code false}, live streams end when playback reaches the end position in live window seen
|
||||
* when the media is first loaded (Default: {@code false}).
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setRelativeToLiveWindow(boolean relativeToLiveWindow) {
|
||||
this.relativeToLiveWindow = relativeToLiveWindow;
|
||||
return this;
|
||||
@ -1572,6 +1640,7 @@ public final class MediaItem implements Bundleable {
|
||||
* Sets whether the start position and the end position are relative to the default position
|
||||
* in the window (Default: {@code false}).
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setRelativeToDefaultPosition(boolean relativeToDefaultPosition) {
|
||||
this.relativeToDefaultPosition = relativeToDefaultPosition;
|
||||
return this;
|
||||
@ -1581,6 +1650,7 @@ public final class MediaItem implements Bundleable {
|
||||
* Sets whether the start point is guaranteed to be a key frame. If {@code false}, the
|
||||
* playback transition into the clip may not be seamless (Default: {@code false}).
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setStartsAtKeyFrame(boolean startsAtKeyFrame) {
|
||||
this.startsAtKeyFrame = startsAtKeyFrame;
|
||||
return this;
|
||||
@ -1745,7 +1815,7 @@ public final class MediaItem implements Bundleable {
|
||||
* MediaItem}.
|
||||
*
|
||||
* <p>This metadata is most useful for cases where playback requests are forwarded to other player
|
||||
* instances (e.g. from a {@link android.media.session.MediaController}) and the player creating
|
||||
* instances (e.g. from a {@code androidx.media3.session.MediaController}) and the player creating
|
||||
* the request doesn't know the required {@link LocalConfiguration} for playback.
|
||||
*/
|
||||
public static final class RequestMetadata implements Bundleable {
|
||||
@ -1770,18 +1840,21 @@ public final class MediaItem implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the URI of the requested media, or null if not known or applicable. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMediaUri(@Nullable Uri mediaUri) {
|
||||
this.mediaUri = mediaUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the search query for the requested media, or null if not applicable. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSearchQuery(@Nullable String searchQuery) {
|
||||
this.searchQuery = searchQuery;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets optional extras {@link Bundle}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setExtras(@Nullable Bundle extras) {
|
||||
this.extras = extras;
|
||||
return this;
|
||||
|
@ -29,11 +29,11 @@ public final class MediaLibraryInfo {
|
||||
|
||||
/** The version of the library expressed as a string, for example "1.2.3" or "1.2.3-beta01". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "1.0.0-beta02";
|
||||
public static final String VERSION = "1.0.0-beta03";
|
||||
|
||||
/** The version of the library expressed as {@code TAG + "/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta02";
|
||||
public static final String VERSION_SLASHY = "AndroidXMedia3/1.0.0-beta03";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as an integer, for example 1002003300.
|
||||
@ -47,7 +47,7 @@ public final class MediaLibraryInfo {
|
||||
* (123-045-006-3-00).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 1_000_000_1_02;
|
||||
public static final int VERSION_INT = 1_000_000_1_03;
|
||||
|
||||
/** Whether the library was compiled with {@link Assertions} checks enabled. */
|
||||
public static final boolean ASSERTIONS_ENABLED = true;
|
||||
|
@ -29,6 +29,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@ -114,30 +115,35 @@ public final class MediaMetadata implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the title. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setTitle(@Nullable CharSequence title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the artist. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setArtist(@Nullable CharSequence artist) {
|
||||
this.artist = artist;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the album title. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAlbumTitle(@Nullable CharSequence albumTitle) {
|
||||
this.albumTitle = albumTitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the album artist. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setAlbumArtist(@Nullable CharSequence albumArtist) {
|
||||
this.albumArtist = albumArtist;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the display title. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setDisplayTitle(@Nullable CharSequence displayTitle) {
|
||||
this.displayTitle = displayTitle;
|
||||
return this;
|
||||
@ -148,24 +154,28 @@ public final class MediaMetadata implements Bundleable {
|
||||
*
|
||||
* <p>This is the secondary title of the media, unrelated to closed captions.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSubtitle(@Nullable CharSequence subtitle) {
|
||||
this.subtitle = subtitle;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the description. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setDescription(@Nullable CharSequence description) {
|
||||
this.description = description;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the user {@link Rating}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setUserRating(@Nullable Rating userRating) {
|
||||
this.userRating = userRating;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the overall {@link Rating}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setOverallRating(@Nullable Rating overallRating) {
|
||||
this.overallRating = overallRating;
|
||||
return this;
|
||||
@ -175,6 +185,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
* @deprecated Use {@link #setArtworkData(byte[] data, Integer pictureType)} or {@link
|
||||
* #maybeSetArtworkData(byte[] data, int pictureType)}, providing a {@link PictureType}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setArtworkData(@Nullable byte[] artworkData) {
|
||||
@ -185,6 +196,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
* Sets the artwork data as a compressed byte array with an associated {@link PictureType
|
||||
* artworkDataType}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setArtworkData(
|
||||
@Nullable byte[] artworkData, @Nullable @PictureType Integer artworkDataType) {
|
||||
this.artworkData = artworkData == null ? null : artworkData.clone();
|
||||
@ -200,6 +212,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
* <p>Use {@link #setArtworkData(byte[], Integer)} to set the artwork data without checking the
|
||||
* {@link PictureType}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder maybeSetArtworkData(byte[] artworkData, @PictureType int artworkDataType) {
|
||||
if (this.artworkData == null
|
||||
|| Util.areEqual(artworkDataType, PICTURE_TYPE_FRONT_COVER)
|
||||
@ -211,30 +224,35 @@ public final class MediaMetadata implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the artwork {@link Uri}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setArtworkUri(@Nullable Uri artworkUri) {
|
||||
this.artworkUri = artworkUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the track number. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setTrackNumber(@Nullable Integer trackNumber) {
|
||||
this.trackNumber = trackNumber;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the total number of tracks. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setTotalTrackCount(@Nullable Integer totalTrackCount) {
|
||||
this.totalTrackCount = totalTrackCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the {@link FolderType}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setFolderType(@Nullable @FolderType Integer folderType) {
|
||||
this.folderType = folderType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets whether the media is playable. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setIsPlayable(@Nullable Boolean isPlayable) {
|
||||
this.isPlayable = isPlayable;
|
||||
return this;
|
||||
@ -243,6 +261,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
/**
|
||||
* @deprecated Use {@link #setRecordingYear(Integer)} instead.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Builder setYear(@Nullable Integer year) {
|
||||
@ -250,6 +269,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the year of the recording date. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setRecordingYear(@Nullable Integer recordingYear) {
|
||||
this.recordingYear = recordingYear;
|
||||
return this;
|
||||
@ -260,6 +280,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
*
|
||||
* <p>Value should be between 1 and 12.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setRecordingMonth(
|
||||
@Nullable @IntRange(from = 1, to = 12) Integer recordingMonth) {
|
||||
this.recordingMonth = recordingMonth;
|
||||
@ -271,12 +292,14 @@ public final class MediaMetadata implements Bundleable {
|
||||
*
|
||||
* <p>Value should be between 1 and 31.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setRecordingDay(@Nullable @IntRange(from = 1, to = 31) Integer recordingDay) {
|
||||
this.recordingDay = recordingDay;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the year of the release date. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setReleaseYear(@Nullable Integer releaseYear) {
|
||||
this.releaseYear = releaseYear;
|
||||
return this;
|
||||
@ -287,6 +310,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
*
|
||||
* <p>Value should be between 1 and 12.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setReleaseMonth(@Nullable @IntRange(from = 1, to = 12) Integer releaseMonth) {
|
||||
this.releaseMonth = releaseMonth;
|
||||
return this;
|
||||
@ -297,60 +321,70 @@ public final class MediaMetadata implements Bundleable {
|
||||
*
|
||||
* <p>Value should be between 1 and 31.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setReleaseDay(@Nullable @IntRange(from = 1, to = 31) Integer releaseDay) {
|
||||
this.releaseDay = releaseDay;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the writer. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setWriter(@Nullable CharSequence writer) {
|
||||
this.writer = writer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the composer. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setComposer(@Nullable CharSequence composer) {
|
||||
this.composer = composer;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the conductor. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setConductor(@Nullable CharSequence conductor) {
|
||||
this.conductor = conductor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the disc number. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setDiscNumber(@Nullable Integer discNumber) {
|
||||
this.discNumber = discNumber;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the total number of discs. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setTotalDiscCount(@Nullable Integer totalDiscCount) {
|
||||
this.totalDiscCount = totalDiscCount;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the genre. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setGenre(@Nullable CharSequence genre) {
|
||||
this.genre = genre;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the compilation. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setCompilation(@Nullable CharSequence compilation) {
|
||||
this.compilation = compilation;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the name of the station streaming the media. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setStation(@Nullable CharSequence station) {
|
||||
this.station = station;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the extras {@link Bundle}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setExtras(@Nullable Bundle extras) {
|
||||
this.extras = extras;
|
||||
return this;
|
||||
@ -365,6 +399,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
* <p>In the event that multiple {@link Metadata.Entry} objects within the {@link Metadata}
|
||||
* relate to the same {@link MediaMetadata} field, then the last one will be used.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Builder populateFromMetadata(Metadata metadata) {
|
||||
for (int i = 0; i < metadata.length(); i++) {
|
||||
@ -384,6 +419,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
* <p>In the event that multiple {@link Metadata.Entry} objects within any of the {@link
|
||||
* Metadata} relate to the same {@link MediaMetadata} field, then the last one will be used.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Builder populateFromMetadata(List<Metadata> metadataList) {
|
||||
for (int i = 0; i < metadataList.size(); i++) {
|
||||
@ -397,6 +433,7 @@ public final class MediaMetadata implements Bundleable {
|
||||
}
|
||||
|
||||
/** Populates all the fields from {@code mediaMetadata}, provided they are non-null. */
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Builder populate(@Nullable MediaMetadata mediaMetadata) {
|
||||
if (mediaMetadata == null) {
|
||||
|
@ -20,6 +20,7 @@ import android.os.Parcelable;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.primitives.Longs;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@ -61,11 +62,28 @@ public final class Metadata implements Parcelable {
|
||||
}
|
||||
|
||||
private final Entry[] entries;
|
||||
/**
|
||||
* The presentation time of the metadata, in microseconds.
|
||||
*
|
||||
* <p>This time is an offset from the start of the current {@link Timeline.Period}.
|
||||
*
|
||||
* <p>This time is {@link C#TIME_UNSET} when not known or undefined.
|
||||
*/
|
||||
public final long presentationTimeUs;
|
||||
|
||||
/**
|
||||
* @param entries The metadata entries.
|
||||
*/
|
||||
public Metadata(Entry... entries) {
|
||||
this(/* presentationTimeUs= */ C.TIME_UNSET, entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param presentationTimeUs The presentation time for the metadata entries.
|
||||
* @param entries The metadata entries.
|
||||
*/
|
||||
public Metadata(long presentationTimeUs, Entry... entries) {
|
||||
this.presentationTimeUs = presentationTimeUs;
|
||||
this.entries = entries;
|
||||
}
|
||||
|
||||
@ -73,7 +91,15 @@ public final class Metadata implements Parcelable {
|
||||
* @param entries The metadata entries.
|
||||
*/
|
||||
public Metadata(List<? extends Entry> entries) {
|
||||
this.entries = entries.toArray(new Entry[0]);
|
||||
this(entries.toArray(new Entry[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param presentationTimeUs The presentation time for the metadata entries.
|
||||
* @param entries The metadata entries.
|
||||
*/
|
||||
public Metadata(long presentationTimeUs, List<? extends Entry> entries) {
|
||||
this(presentationTimeUs, entries.toArray(new Entry[0]));
|
||||
}
|
||||
|
||||
/* package */ Metadata(Parcel in) {
|
||||
@ -81,6 +107,7 @@ public final class Metadata implements Parcelable {
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
entries[i] = in.readParcelable(Entry.class.getClassLoader());
|
||||
}
|
||||
presentationTimeUs = in.readLong();
|
||||
}
|
||||
|
||||
/** Returns the number of metadata entries. */
|
||||
@ -123,7 +150,21 @@ public final class Metadata implements Parcelable {
|
||||
if (entriesToAppend.length == 0) {
|
||||
return this;
|
||||
}
|
||||
return new Metadata(Util.nullSafeArrayConcatenation(entries, entriesToAppend));
|
||||
return new Metadata(
|
||||
presentationTimeUs, Util.nullSafeArrayConcatenation(entries, entriesToAppend));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of this metadata with the specified presentation time.
|
||||
*
|
||||
* @param presentationTimeUs The new presentation time, in microseconds.
|
||||
* @return The metadata instance with the new presentation time.
|
||||
*/
|
||||
public Metadata copyWithPresentationTimeUs(long presentationTimeUs) {
|
||||
if (this.presentationTimeUs == presentationTimeUs) {
|
||||
return this;
|
||||
}
|
||||
return new Metadata(presentationTimeUs, entries);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -135,17 +176,21 @@ public final class Metadata implements Parcelable {
|
||||
return false;
|
||||
}
|
||||
Metadata other = (Metadata) obj;
|
||||
return Arrays.equals(entries, other.entries);
|
||||
return Arrays.equals(entries, other.entries) && presentationTimeUs == other.presentationTimeUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(entries);
|
||||
int result = Arrays.hashCode(entries);
|
||||
result = 31 * result + Longs.hashCode(presentationTimeUs);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "entries=" + Arrays.toString(entries);
|
||||
return "entries="
|
||||
+ Arrays.toString(entries)
|
||||
+ (presentationTimeUs == C.TIME_UNSET ? "" : ", presentationTimeUs=" + presentationTimeUs);
|
||||
}
|
||||
|
||||
// Parcelable implementation.
|
||||
@ -161,6 +206,7 @@ public final class Metadata implements Parcelable {
|
||||
for (Entry entry : entries) {
|
||||
dest.writeParcelable(entry, 0);
|
||||
}
|
||||
dest.writeLong(presentationTimeUs);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Metadata> CREATOR =
|
||||
|
@ -91,11 +91,15 @@ public final class MimeTypes {
|
||||
public static final String AUDIO_AMR_NB = BASE_TYPE_AUDIO + "/3gpp";
|
||||
public static final String AUDIO_AMR_WB = BASE_TYPE_AUDIO + "/amr-wb";
|
||||
public static final String AUDIO_FLAC = BASE_TYPE_AUDIO + "/flac";
|
||||
public static final String AUDIO_MIDI = BASE_TYPE_AUDIO + "/midi";
|
||||
public static final String AUDIO_ALAC = BASE_TYPE_AUDIO + "/alac";
|
||||
public static final String AUDIO_MSGSM = BASE_TYPE_AUDIO + "/gsm";
|
||||
public static final String AUDIO_OGG = BASE_TYPE_AUDIO + "/ogg";
|
||||
public static final String AUDIO_WAV = BASE_TYPE_AUDIO + "/wav";
|
||||
public static final String AUDIO_MIDI = BASE_TYPE_AUDIO + "/midi";
|
||||
|
||||
@UnstableApi
|
||||
public static final String AUDIO_EXOPLAYER_MIDI = BASE_TYPE_AUDIO + "/x-exoplayer-midi";
|
||||
|
||||
@UnstableApi public static final String AUDIO_UNKNOWN = BASE_TYPE_AUDIO + "/x-unknown";
|
||||
|
||||
// text/ MIME types
|
||||
|
@ -33,9 +33,11 @@ import androidx.annotation.IntRange;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.text.Cue;
|
||||
import androidx.media3.common.text.CueGroup;
|
||||
import androidx.media3.common.util.Size;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@ -406,6 +408,7 @@ public interface Player {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder add(@Command int command) {
|
||||
flagsBuilder.add(command);
|
||||
return this;
|
||||
@ -419,6 +422,7 @@ public interface Player {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder addIf(@Command int command, boolean condition) {
|
||||
flagsBuilder.addIf(command, condition);
|
||||
return this;
|
||||
@ -431,6 +435,7 @@ public interface Player {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder addAll(@Command int... commands) {
|
||||
flagsBuilder.addAll(commands);
|
||||
return this;
|
||||
@ -443,6 +448,7 @@ public interface Player {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder addAll(Commands commands) {
|
||||
flagsBuilder.addAll(commands.flags);
|
||||
return this;
|
||||
@ -454,6 +460,7 @@ public interface Player {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder addAllCommands() {
|
||||
flagsBuilder.addAll(SUPPORTED_COMMANDS);
|
||||
return this;
|
||||
@ -466,6 +473,7 @@ public interface Player {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder remove(@Command int command) {
|
||||
flagsBuilder.remove(command);
|
||||
return this;
|
||||
@ -479,6 +487,7 @@ public interface Player {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder removeIf(@Command int command, boolean condition) {
|
||||
flagsBuilder.removeIf(command, condition);
|
||||
return this;
|
||||
@ -491,6 +500,7 @@ public interface Player {
|
||||
* @return This builder.
|
||||
* @throws IllegalStateException If {@link #build()} has already been called.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder removeAll(@Command int... commands) {
|
||||
flagsBuilder.removeAll(commands);
|
||||
return this;
|
||||
@ -2489,6 +2499,14 @@ public interface Player {
|
||||
*/
|
||||
VideoSize getVideoSize();
|
||||
|
||||
/**
|
||||
* Gets the size of the surface on which the video is rendered.
|
||||
*
|
||||
* @see Listener#onSurfaceSizeChanged(int, int)
|
||||
*/
|
||||
@UnstableApi
|
||||
Size getSurfaceSize();
|
||||
|
||||
/** Returns the current {@link CueGroup}. */
|
||||
CueGroup getCurrentCues();
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -34,6 +34,7 @@ import androidx.media3.common.util.BundleUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import com.google.errorprone.annotations.InlineMe;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
@ -261,6 +262,7 @@ public abstract class Timeline implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets the data held by this window. */
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@SuppressWarnings("deprecation")
|
||||
public Window set(
|
||||
@ -626,6 +628,7 @@ public abstract class Timeline implements Bundleable {
|
||||
* period is not within the window.
|
||||
* @return This period, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Period set(
|
||||
@Nullable Object id,
|
||||
@ -662,6 +665,7 @@ public abstract class Timeline implements Bundleable {
|
||||
* information has yet to be loaded.
|
||||
* @return This period, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Period set(
|
||||
@Nullable Object id,
|
||||
|
@ -26,11 +26,11 @@ import androidx.media3.common.util.BundleableUtil;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.Lists;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@ -179,8 +179,11 @@ public final class TrackGroup implements Bundleable {
|
||||
@Override
|
||||
public Bundle toBundle() {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(
|
||||
keyForField(FIELD_FORMATS), BundleableUtil.toBundleArrayList(Lists.newArrayList(formats)));
|
||||
ArrayList<Bundle> arrayList = new ArrayList<>(formats.length);
|
||||
for (Format format : formats) {
|
||||
arrayList.add(format.toBundle(/* excludeMetadata= */ true));
|
||||
}
|
||||
bundle.putParcelableArrayList(keyForField(FIELD_FORMATS), arrayList);
|
||||
bundle.putString(keyForField(FIELD_ID), id);
|
||||
return bundle;
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
@ -306,6 +307,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
}
|
||||
|
||||
/** Overrides the value of the builder with the value of {@link TrackSelectionParameters}. */
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
protected Builder set(TrackSelectionParameters parameters) {
|
||||
init(parameters);
|
||||
@ -319,6 +321,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
*
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxVideoSizeSd() {
|
||||
return setMaxVideoSize(1279, 719);
|
||||
}
|
||||
@ -328,6 +331,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
*
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder clearVideoSizeConstraints() {
|
||||
return setMaxVideoSize(Integer.MAX_VALUE, Integer.MAX_VALUE);
|
||||
}
|
||||
@ -339,6 +343,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param maxVideoHeight Maximum allowed video height in pixels.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxVideoSize(int maxVideoWidth, int maxVideoHeight) {
|
||||
this.maxVideoWidth = maxVideoWidth;
|
||||
this.maxVideoHeight = maxVideoHeight;
|
||||
@ -351,6 +356,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param maxVideoFrameRate Maximum allowed video frame rate in hertz.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxVideoFrameRate(int maxVideoFrameRate) {
|
||||
this.maxVideoFrameRate = maxVideoFrameRate;
|
||||
return this;
|
||||
@ -362,6 +368,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param maxVideoBitrate Maximum allowed video bitrate in bits per second.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxVideoBitrate(int maxVideoBitrate) {
|
||||
this.maxVideoBitrate = maxVideoBitrate;
|
||||
return this;
|
||||
@ -374,6 +381,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param minVideoHeight Minimum allowed video height in pixels.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMinVideoSize(int minVideoWidth, int minVideoHeight) {
|
||||
this.minVideoWidth = minVideoWidth;
|
||||
this.minVideoHeight = minVideoHeight;
|
||||
@ -386,6 +394,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param minVideoFrameRate Minimum allowed video frame rate in hertz.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMinVideoFrameRate(int minVideoFrameRate) {
|
||||
this.minVideoFrameRate = minVideoFrameRate;
|
||||
return this;
|
||||
@ -397,6 +406,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param minVideoBitrate Minimum allowed video bitrate in bits per second.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMinVideoBitrate(int minVideoBitrate) {
|
||||
this.minVideoBitrate = minVideoBitrate;
|
||||
return this;
|
||||
@ -411,6 +421,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* playback.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setViewportSizeToPhysicalDisplaySize(
|
||||
Context context, boolean viewportOrientationMayChange) {
|
||||
// Assume the viewport is fullscreen.
|
||||
@ -424,6 +435,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
*
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder clearViewportSizeConstraints() {
|
||||
return setViewportSize(Integer.MAX_VALUE, Integer.MAX_VALUE, true);
|
||||
}
|
||||
@ -438,6 +450,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* playback.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setViewportSize(
|
||||
int viewportWidth, int viewportHeight, boolean viewportOrientationMayChange) {
|
||||
this.viewportWidth = viewportWidth;
|
||||
@ -464,6 +477,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* empty list for no preference.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredVideoMimeTypes(String... mimeTypes) {
|
||||
preferredVideoMimeTypes = ImmutableList.copyOf(mimeTypes);
|
||||
return this;
|
||||
@ -475,6 +489,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param preferredVideoRoleFlags Preferred video role flags.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredVideoRoleFlags(@C.RoleFlags int preferredVideoRoleFlags) {
|
||||
this.preferredVideoRoleFlags = preferredVideoRoleFlags;
|
||||
return this;
|
||||
@ -503,6 +518,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* there's no default.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredAudioLanguages(String... preferredAudioLanguages) {
|
||||
this.preferredAudioLanguages = normalizeLanguageCodes(preferredAudioLanguages);
|
||||
return this;
|
||||
@ -514,6 +530,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param preferredAudioRoleFlags Preferred audio role flags.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredAudioRoleFlags(@C.RoleFlags int preferredAudioRoleFlags) {
|
||||
this.preferredAudioRoleFlags = preferredAudioRoleFlags;
|
||||
return this;
|
||||
@ -525,6 +542,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param maxAudioChannelCount Maximum allowed audio channel count.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxAudioChannelCount(int maxAudioChannelCount) {
|
||||
this.maxAudioChannelCount = maxAudioChannelCount;
|
||||
return this;
|
||||
@ -536,6 +554,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param maxAudioBitrate Maximum allowed audio bitrate in bits per second.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMaxAudioBitrate(int maxAudioBitrate) {
|
||||
this.maxAudioBitrate = maxAudioBitrate;
|
||||
return this;
|
||||
@ -559,6 +578,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* empty list for no preference.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredAudioMimeTypes(String... mimeTypes) {
|
||||
preferredAudioMimeTypes = ImmutableList.copyOf(mimeTypes);
|
||||
return this;
|
||||
@ -575,6 +595,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param context A {@link Context}.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredTextLanguageAndRoleFlagsToCaptioningManagerSettings(
|
||||
Context context) {
|
||||
if (Util.SDK_INT >= 19) {
|
||||
@ -604,6 +625,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* track otherwise.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredTextLanguages(String... preferredTextLanguages) {
|
||||
this.preferredTextLanguages = normalizeLanguageCodes(preferredTextLanguages);
|
||||
return this;
|
||||
@ -615,6 +637,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param preferredTextRoleFlags Preferred text role flags.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPreferredTextRoleFlags(@C.RoleFlags int preferredTextRoleFlags) {
|
||||
this.preferredTextRoleFlags = preferredTextRoleFlags;
|
||||
return this;
|
||||
@ -627,6 +650,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* text track selections.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setIgnoredTextSelectionFlags(@C.SelectionFlags int ignoredTextSelectionFlags) {
|
||||
this.ignoredTextSelectionFlags = ignoredTextSelectionFlags;
|
||||
return this;
|
||||
@ -641,6 +665,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* be selected if no preferred language track is available.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSelectUndeterminedTextLanguage(boolean selectUndeterminedTextLanguage) {
|
||||
this.selectUndeterminedTextLanguage = selectUndeterminedTextLanguage;
|
||||
return this;
|
||||
@ -656,6 +681,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* video tracks.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setForceLowestBitrate(boolean forceLowestBitrate) {
|
||||
this.forceLowestBitrate = forceLowestBitrate;
|
||||
return this;
|
||||
@ -669,18 +695,21 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* and video tracks.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setForceHighestSupportedBitrate(boolean forceHighestSupportedBitrate) {
|
||||
this.forceHighestSupportedBitrate = forceHighestSupportedBitrate;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Adds an override, replacing any override for the same {@link TrackGroup}. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder addOverride(TrackSelectionOverride override) {
|
||||
overrides.put(override.mediaTrackGroup, override);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets an override, replacing all existing overrides with the same track type. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setOverrideForType(TrackSelectionOverride override) {
|
||||
clearOverridesOfType(override.getType());
|
||||
overrides.put(override.mediaTrackGroup, override);
|
||||
@ -688,12 +717,14 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
}
|
||||
|
||||
/** Removes the override for the provided media {@link TrackGroup}, if there is one. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder clearOverride(TrackGroup mediaTrackGroup) {
|
||||
overrides.remove(mediaTrackGroup);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Removes all overrides of the provided track type. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder clearOverridesOfType(@C.TrackType int trackType) {
|
||||
Iterator<TrackSelectionOverride> it = overrides.values().iterator();
|
||||
while (it.hasNext()) {
|
||||
@ -706,6 +737,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
}
|
||||
|
||||
/** Removes all overrides. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder clearOverrides() {
|
||||
overrides.clear();
|
||||
return this;
|
||||
@ -719,6 +751,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @return This builder.
|
||||
* @deprecated Use {@link #setTrackTypeDisabled(int, boolean)}.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@Deprecated
|
||||
@UnstableApi
|
||||
public Builder setDisabledTrackTypes(Set<@C.TrackType Integer> disabledTrackTypes) {
|
||||
@ -735,6 +768,7 @@ public class TrackSelectionParameters implements Bundleable {
|
||||
* @param disabled Whether the track type should be disabled.
|
||||
* @return This builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setTrackTypeDisabled(@C.TrackType int trackType, boolean disabled) {
|
||||
if (disabled) {
|
||||
disabledTrackTypes.add(trackType);
|
||||
|
@ -13,12 +13,15 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package androidx.media3.exoplayer.audio;
|
||||
package androidx.media3.common.audio;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.Format;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
@ -70,6 +73,25 @@ public interface AudioProcessor {
|
||||
+ encoding
|
||||
+ ']';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof AudioFormat)) {
|
||||
return false;
|
||||
}
|
||||
AudioFormat that = (AudioFormat) o;
|
||||
return sampleRate == that.sampleRate
|
||||
&& channelCount == that.channelCount
|
||||
&& encoding == that.encoding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(sampleRate, channelCount, encoding);
|
||||
}
|
||||
}
|
||||
|
||||
/** Exception thrown when a processor can't be configured for a given input audio format. */
|
||||
@ -98,6 +120,7 @@ public interface AudioProcessor {
|
||||
* @return The configured output audio format if this instance is {@link #isActive() active}.
|
||||
* @throws UnhandledAudioFormatException Thrown if the specified format can't be handled as input.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
AudioFormat configure(AudioFormat inputAudioFormat) throws UnhandledAudioFormatException;
|
||||
|
||||
/** Returns whether the processor is configured and will process input buffers. */
|
||||
@ -134,8 +157,8 @@ public interface AudioProcessor {
|
||||
ByteBuffer getOutput();
|
||||
|
||||
/**
|
||||
* Returns whether this processor will return no more output from {@link #getOutput()} until it
|
||||
* has been {@link #flush()}ed and more input has been queued.
|
||||
* Returns whether this processor will return no more output from {@link #getOutput()} until
|
||||
* {@link #flush()} has been called and more input has been queued.
|
||||
*/
|
||||
boolean isEnded();
|
||||
|
@ -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();
|
||||
}
|
@ -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;
|
@ -36,6 +36,7 @@ import androidx.media3.common.Bundleable;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@ -628,6 +629,7 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#text
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setText(CharSequence text) {
|
||||
this.text = text;
|
||||
return this;
|
||||
@ -649,6 +651,7 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#bitmap
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setBitmap(Bitmap bitmap) {
|
||||
this.bitmap = bitmap;
|
||||
return this;
|
||||
@ -672,6 +675,7 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#textAlignment
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setTextAlignment(@Nullable Layout.Alignment textAlignment) {
|
||||
this.textAlignment = textAlignment;
|
||||
return this;
|
||||
@ -695,6 +699,7 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#multiRowAlignment
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setMultiRowAlignment(@Nullable Layout.Alignment multiRowAlignment) {
|
||||
this.multiRowAlignment = multiRowAlignment;
|
||||
return this;
|
||||
@ -707,6 +712,7 @@ public final class Cue implements Bundleable {
|
||||
* @see Cue#line
|
||||
* @see Cue#lineType
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLine(float line, @LineType int lineType) {
|
||||
this.line = line;
|
||||
this.lineType = lineType;
|
||||
@ -739,6 +745,7 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#lineAnchor
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLineAnchor(@AnchorType int lineAnchor) {
|
||||
this.lineAnchor = lineAnchor;
|
||||
return this;
|
||||
@ -760,6 +767,7 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#position
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPosition(float position) {
|
||||
this.position = position;
|
||||
return this;
|
||||
@ -781,6 +789,7 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#positionAnchor
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPositionAnchor(@AnchorType int positionAnchor) {
|
||||
this.positionAnchor = positionAnchor;
|
||||
return this;
|
||||
@ -802,6 +811,7 @@ public final class Cue implements Bundleable {
|
||||
* @see Cue#textSize
|
||||
* @see Cue#textSizeType
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setTextSize(float textSize, @TextSizeType int textSizeType) {
|
||||
this.textSize = textSize;
|
||||
this.textSizeType = textSizeType;
|
||||
@ -834,6 +844,7 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#size
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setSize(float size) {
|
||||
this.size = size;
|
||||
return this;
|
||||
@ -855,6 +866,7 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#bitmapHeight
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setBitmapHeight(float bitmapHeight) {
|
||||
this.bitmapHeight = bitmapHeight;
|
||||
return this;
|
||||
@ -878,6 +890,7 @@ public final class Cue implements Bundleable {
|
||||
* @see Cue#windowColor
|
||||
* @see Cue#windowColorSet
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setWindowColor(@ColorInt int windowColor) {
|
||||
this.windowColor = windowColor;
|
||||
this.windowColorSet = true;
|
||||
@ -885,6 +898,7 @@ public final class Cue implements Bundleable {
|
||||
}
|
||||
|
||||
/** Sets {@link Cue#windowColorSet} to false. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder clearWindowColor() {
|
||||
this.windowColorSet = false;
|
||||
return this;
|
||||
@ -915,12 +929,14 @@ public final class Cue implements Bundleable {
|
||||
*
|
||||
* @see Cue#verticalType
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setVerticalType(@VerticalType int verticalType) {
|
||||
this.verticalType = verticalType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Sets the shear angle for this Cue. */
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setShearDegrees(float shearDegrees) {
|
||||
this.shearDegrees = shearDegrees;
|
||||
return this;
|
||||
|
@ -22,6 +22,7 @@ import android.os.Bundle;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.Bundleable;
|
||||
import androidx.media3.common.Timeline;
|
||||
import androidx.media3.common.util.BundleableUtil;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
@ -35,8 +36,10 @@ import java.util.List;
|
||||
/** Class to represent the state of active {@link Cue Cues} at a particular time. */
|
||||
public final class CueGroup implements Bundleable {
|
||||
|
||||
/** Empty {@link CueGroup}. */
|
||||
@UnstableApi public static final CueGroup EMPTY = new CueGroup(ImmutableList.of());
|
||||
/** An empty group with no {@link Cue Cues} and presentation time of zero. */
|
||||
@UnstableApi
|
||||
public static final CueGroup EMPTY_TIME_ZERO =
|
||||
new CueGroup(ImmutableList.of(), /* presentationTimeUs= */ 0);
|
||||
|
||||
/**
|
||||
* The cues in this group.
|
||||
@ -47,11 +50,18 @@ public final class CueGroup implements Bundleable {
|
||||
* <p>This list may be empty if the group represents a state with no cues.
|
||||
*/
|
||||
public final ImmutableList<Cue> cues;
|
||||
/**
|
||||
* The presentation time of the {@link #cues}, in microseconds.
|
||||
*
|
||||
* <p>This time is an offset from the start of the current {@link Timeline.Period}.
|
||||
*/
|
||||
@UnstableApi public final long presentationTimeUs;
|
||||
|
||||
/** Creates a CueGroup. */
|
||||
@UnstableApi
|
||||
public CueGroup(List<Cue> cues) {
|
||||
public CueGroup(List<Cue> cues, long presentationTimeUs) {
|
||||
this.cues = ImmutableList.copyOf(cues);
|
||||
this.presentationTimeUs = presentationTimeUs;
|
||||
}
|
||||
|
||||
// Bundleable implementation.
|
||||
@ -59,10 +69,11 @@ public final class CueGroup implements Bundleable {
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.SOURCE)
|
||||
@Target(TYPE_USE)
|
||||
@IntDef({FIELD_CUES})
|
||||
@IntDef({FIELD_CUES, FIELD_PRESENTATION_TIME_US})
|
||||
private @interface FieldNumber {}
|
||||
|
||||
private static final int FIELD_CUES = 0;
|
||||
private static final int FIELD_PRESENTATION_TIME_US = 1;
|
||||
|
||||
@UnstableApi
|
||||
@Override
|
||||
@ -70,6 +81,7 @@ public final class CueGroup implements Bundleable {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(
|
||||
keyForField(FIELD_CUES), BundleableUtil.toBundleArrayList(filterOutBitmapCues(cues)));
|
||||
bundle.putLong(keyForField(FIELD_PRESENTATION_TIME_US), presentationTimeUs);
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@ -81,7 +93,8 @@ public final class CueGroup implements Bundleable {
|
||||
cueBundles == null
|
||||
? ImmutableList.of()
|
||||
: BundleableUtil.fromBundleList(Cue.CREATOR, cueBundles);
|
||||
return new CueGroup(cues);
|
||||
long presentationTimeUs = bundle.getLong(keyForField(FIELD_PRESENTATION_TIME_US));
|
||||
return new CueGroup(cues, presentationTimeUs);
|
||||
}
|
||||
|
||||
private static String keyForField(@FieldNumber int field) {
|
||||
|
@ -79,13 +79,6 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
|
||||
|
||||
private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0;
|
||||
|
||||
/** A runtime exception to be thrown if some EGL operations failed. */
|
||||
public static final class GlException extends RuntimeException {
|
||||
private GlException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private final Handler handler;
|
||||
private final int[] textureIdHolder;
|
||||
@Nullable private final TextureImageListener callback;
|
||||
@ -125,7 +118,7 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
|
||||
*
|
||||
* @param secureMode The {@link SecureMode} to be used for EGL surface.
|
||||
*/
|
||||
public void init(@SecureMode int secureMode) {
|
||||
public void init(@SecureMode int secureMode) throws GlUtil.GlException {
|
||||
display = getDefaultDisplay();
|
||||
EGLConfig config = chooseEGLConfig(display);
|
||||
context = createEGLContext(display, config, secureMode);
|
||||
@ -206,22 +199,18 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
|
||||
}
|
||||
}
|
||||
|
||||
private static EGLDisplay getDefaultDisplay() {
|
||||
private static EGLDisplay getDefaultDisplay() throws GlUtil.GlException {
|
||||
EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
|
||||
if (display == null) {
|
||||
throw new GlException("eglGetDisplay failed");
|
||||
}
|
||||
GlUtil.checkGlException(display != null, "eglGetDisplay failed");
|
||||
|
||||
int[] version = new int[2];
|
||||
boolean eglInitialized =
|
||||
EGL14.eglInitialize(display, version, /* majorOffset= */ 0, version, /* minorOffset= */ 1);
|
||||
if (!eglInitialized) {
|
||||
throw new GlException("eglInitialize failed");
|
||||
}
|
||||
GlUtil.checkGlException(eglInitialized, "eglInitialize failed");
|
||||
return display;
|
||||
}
|
||||
|
||||
private static EGLConfig chooseEGLConfig(EGLDisplay display) {
|
||||
private static EGLConfig chooseEGLConfig(EGLDisplay display) throws GlUtil.GlException {
|
||||
EGLConfig[] configs = new EGLConfig[1];
|
||||
int[] numConfigs = new int[1];
|
||||
boolean success =
|
||||
@ -234,18 +223,17 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
|
||||
/* config_size= */ 1,
|
||||
numConfigs,
|
||||
/* num_configOffset= */ 0);
|
||||
if (!success || numConfigs[0] <= 0 || configs[0] == null) {
|
||||
throw new GlException(
|
||||
Util.formatInvariant(
|
||||
/* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
|
||||
success, numConfigs[0], configs[0]));
|
||||
}
|
||||
GlUtil.checkGlException(
|
||||
success && numConfigs[0] > 0 && configs[0] != null,
|
||||
Util.formatInvariant(
|
||||
/* format= */ "eglChooseConfig failed: success=%b, numConfigs[0]=%d, configs[0]=%s",
|
||||
success, numConfigs[0], configs[0]));
|
||||
|
||||
return configs[0];
|
||||
}
|
||||
|
||||
private static EGLContext createEGLContext(
|
||||
EGLDisplay display, EGLConfig config, @SecureMode int secureMode) {
|
||||
EGLDisplay display, EGLConfig config, @SecureMode int secureMode) throws GlUtil.GlException {
|
||||
int[] glAttributes;
|
||||
if (secureMode == SECURE_MODE_NONE) {
|
||||
glAttributes = new int[] {EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE};
|
||||
@ -262,14 +250,13 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
|
||||
EGLContext context =
|
||||
EGL14.eglCreateContext(
|
||||
display, config, android.opengl.EGL14.EGL_NO_CONTEXT, glAttributes, 0);
|
||||
if (context == null) {
|
||||
throw new GlException("eglCreateContext failed");
|
||||
}
|
||||
GlUtil.checkGlException(context != null, "eglCreateContext failed");
|
||||
return context;
|
||||
}
|
||||
|
||||
private static EGLSurface createEGLSurface(
|
||||
EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode) {
|
||||
EGLDisplay display, EGLConfig config, EGLContext context, @SecureMode int secureMode)
|
||||
throws GlUtil.GlException {
|
||||
EGLSurface surface;
|
||||
if (secureMode == SECURE_MODE_SURFACELESS_CONTEXT) {
|
||||
surface = EGL14.EGL_NO_SURFACE;
|
||||
@ -297,20 +284,16 @@ public final class EGLSurfaceTexture implements SurfaceTexture.OnFrameAvailableL
|
||||
};
|
||||
}
|
||||
surface = EGL14.eglCreatePbufferSurface(display, config, pbufferAttributes, /* offset= */ 0);
|
||||
if (surface == null) {
|
||||
throw new GlException("eglCreatePbufferSurface failed");
|
||||
}
|
||||
GlUtil.checkGlException(surface != null, "eglCreatePbufferSurface failed");
|
||||
}
|
||||
|
||||
boolean eglMadeCurrent =
|
||||
EGL14.eglMakeCurrent(display, /* draw= */ surface, /* read= */ surface, context);
|
||||
if (!eglMadeCurrent) {
|
||||
throw new GlException("eglMakeCurrent failed");
|
||||
}
|
||||
GlUtil.checkGlException(eglMadeCurrent, "eglMakeCurrent failed");
|
||||
return surface;
|
||||
}
|
||||
|
||||
private static void generateTextureIds(int[] textureIdHolder) {
|
||||
private static void generateTextureIds(int[] textureIdHolder) throws GlUtil.GlException {
|
||||
GLES20.glGenTextures(/* n= */ 1, textureIdHolder, /* offset= */ 0);
|
||||
GlUtil.checkGlError();
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import android.opengl.GLES11Ext;
|
||||
import android.opengl.GLES20;
|
||||
import androidx.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.Buffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@ -54,10 +55,26 @@ public final class GlProgram {
|
||||
* @throws IOException When failing to read shader files.
|
||||
*/
|
||||
public GlProgram(Context context, String vertexShaderFilePath, String fragmentShaderFilePath)
|
||||
throws IOException {
|
||||
this(
|
||||
GlUtil.loadAsset(context, vertexShaderFilePath),
|
||||
GlUtil.loadAsset(context, fragmentShaderFilePath));
|
||||
throws IOException, GlUtil.GlException {
|
||||
this(loadAsset(context, vertexShaderFilePath), loadAsset(context, fragmentShaderFilePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file from the assets folder.
|
||||
*
|
||||
* @param context The {@link Context}.
|
||||
* @param assetPath The path to the file to load, from the assets folder.
|
||||
* @return The content of the file to load.
|
||||
* @throws IOException If the file couldn't be read.
|
||||
*/
|
||||
public static String loadAsset(Context context, String assetPath) throws IOException {
|
||||
@Nullable InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetPath);
|
||||
return Util.fromUtf8Bytes(Util.toByteArray(inputStream));
|
||||
} finally {
|
||||
Util.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,7 +86,7 @@ public final class GlProgram {
|
||||
* @param vertexShaderGlsl The vertex shader program.
|
||||
* @param fragmentShaderGlsl The fragment shader program.
|
||||
*/
|
||||
public GlProgram(String vertexShaderGlsl, String fragmentShaderGlsl) {
|
||||
public GlProgram(String vertexShaderGlsl, String fragmentShaderGlsl) throws GlUtil.GlException {
|
||||
programId = GLES20.glCreateProgram();
|
||||
GlUtil.checkGlError();
|
||||
|
||||
@ -81,10 +98,9 @@ public final class GlProgram {
|
||||
GLES20.glLinkProgram(programId);
|
||||
int[] linkStatus = new int[] {GLES20.GL_FALSE};
|
||||
GLES20.glGetProgramiv(programId, GLES20.GL_LINK_STATUS, linkStatus, /* offset= */ 0);
|
||||
if (linkStatus[0] != GLES20.GL_TRUE) {
|
||||
GlUtil.throwGlException(
|
||||
"Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(programId));
|
||||
}
|
||||
GlUtil.checkGlException(
|
||||
linkStatus[0] == GLES20.GL_TRUE,
|
||||
"Unable to link shader program: \n" + GLES20.glGetProgramInfoLog(programId));
|
||||
GLES20.glUseProgram(programId);
|
||||
attributeByName = new HashMap<>();
|
||||
int[] attributeCount = new int[1];
|
||||
@ -107,16 +123,15 @@ public final class GlProgram {
|
||||
GlUtil.checkGlError();
|
||||
}
|
||||
|
||||
private static void addShader(int programId, int type, String glsl) {
|
||||
private static void addShader(int programId, int type, String glsl) throws GlUtil.GlException {
|
||||
int shader = GLES20.glCreateShader(type);
|
||||
GLES20.glShaderSource(shader, glsl);
|
||||
GLES20.glCompileShader(shader);
|
||||
|
||||
int[] result = new int[] {GLES20.GL_FALSE};
|
||||
GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, result, /* offset= */ 0);
|
||||
if (result[0] != GLES20.GL_TRUE) {
|
||||
GlUtil.throwGlException(GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
|
||||
}
|
||||
GlUtil.checkGlException(
|
||||
result[0] == GLES20.GL_TRUE, GLES20.glGetShaderInfoLog(shader) + ", source: " + glsl);
|
||||
|
||||
GLES20.glAttachShader(programId, shader);
|
||||
GLES20.glDeleteShader(shader);
|
||||
@ -146,13 +161,13 @@ public final class GlProgram {
|
||||
*
|
||||
* <p>Call this in the rendering loop to switch between different programs.
|
||||
*/
|
||||
public void use() {
|
||||
public void use() throws GlUtil.GlException {
|
||||
GLES20.glUseProgram(programId);
|
||||
GlUtil.checkGlError();
|
||||
}
|
||||
|
||||
/** Deletes the program. Deleted programs cannot be used again. */
|
||||
public void delete() {
|
||||
public void delete() throws GlUtil.GlException {
|
||||
GLES20.glDeleteProgram(programId);
|
||||
GlUtil.checkGlError();
|
||||
}
|
||||
@ -161,7 +176,7 @@ public final class GlProgram {
|
||||
* Returns the location of an {@link Attribute}, which has been enabled as a vertex attribute
|
||||
* array.
|
||||
*/
|
||||
public int getAttributeArrayLocationAndEnable(String attributeName) {
|
||||
public int getAttributeArrayLocationAndEnable(String attributeName) throws GlUtil.GlException {
|
||||
int location = getAttributeLocation(attributeName);
|
||||
GLES20.glEnableVertexAttribArray(location);
|
||||
GlUtil.checkGlError();
|
||||
@ -185,18 +200,23 @@ public final class GlProgram {
|
||||
checkNotNull(uniformByName.get(name)).setSamplerTexId(texId, texUnitIndex);
|
||||
}
|
||||
|
||||
/** Sets a float type uniform. */
|
||||
/** Sets an {@code int} type uniform. */
|
||||
public void setIntUniform(String name, int value) {
|
||||
checkNotNull(uniformByName.get(name)).setInt(value);
|
||||
}
|
||||
|
||||
/** Sets a {@code float} type uniform. */
|
||||
public void setFloatUniform(String name, float value) {
|
||||
checkNotNull(uniformByName.get(name)).setFloat(value);
|
||||
}
|
||||
|
||||
/** Sets a float array type uniform. */
|
||||
/** Sets a {@code float[]} type uniform. */
|
||||
public void setFloatsUniform(String name, float[] value) {
|
||||
checkNotNull(uniformByName.get(name)).setFloats(value);
|
||||
}
|
||||
|
||||
/** Binds all attributes and uniforms in the program. */
|
||||
public void bindAttributesAndUniforms() {
|
||||
public void bindAttributesAndUniforms() throws GlUtil.GlException {
|
||||
for (Attribute attribute : attributes) {
|
||||
attribute.bind();
|
||||
}
|
||||
@ -277,7 +297,7 @@ public final class GlProgram {
|
||||
*
|
||||
* <p>Should be called before each drawing call.
|
||||
*/
|
||||
public void bind() {
|
||||
public void bind() throws GlUtil.GlException {
|
||||
Buffer buffer = checkNotNull(this.buffer, "call setBuffer before bind");
|
||||
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, /* buffer= */ 0);
|
||||
GLES20.glVertexAttribPointer(
|
||||
@ -324,16 +344,17 @@ public final class GlProgram {
|
||||
|
||||
private final int location;
|
||||
private final int type;
|
||||
private final float[] value;
|
||||
private final float[] floatValue;
|
||||
|
||||
private int texId;
|
||||
private int intValue;
|
||||
private int texIdValue;
|
||||
private int texUnitIndex;
|
||||
|
||||
private Uniform(String name, int location, int type) {
|
||||
this.name = name;
|
||||
this.location = location;
|
||||
this.type = type;
|
||||
this.value = new float[16];
|
||||
this.floatValue = new float[16];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -343,18 +364,22 @@ public final class GlProgram {
|
||||
* @param texUnitIndex The GL texture unit index.
|
||||
*/
|
||||
public void setSamplerTexId(int texId, int texUnitIndex) {
|
||||
this.texId = texId;
|
||||
this.texIdValue = texId;
|
||||
this.texUnitIndex = texUnitIndex;
|
||||
}
|
||||
|
||||
/** Configures {@link #bind()} to use the specified float {@code value} for this uniform. */
|
||||
public void setFloat(float value) {
|
||||
this.value[0] = value;
|
||||
/** Configures {@link #bind()} to use the specified {@code int} {@code value}. */
|
||||
public void setInt(int value) {
|
||||
this.intValue = value;
|
||||
}
|
||||
|
||||
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
|
||||
/** Configures {@link #bind()} to use the specified {@code float} {@code value}. */
|
||||
public void setFloat(float value) {
|
||||
this.floatValue[0] = value;
|
||||
}
|
||||
|
||||
/** Configures {@link #bind()} to use the specified {@code float[]} {@code value}. */
|
||||
public void setFloats(float[] value) {
|
||||
System.arraycopy(value, /* srcPos= */ 0, this.value, /* destPos= */ 0, value.length);
|
||||
System.arraycopy(value, /* srcPos= */ 0, this.floatValue, /* destPos= */ 0, value.length);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -363,34 +388,37 @@ public final class GlProgram {
|
||||
*
|
||||
* <p>Should be called before each drawing call.
|
||||
*/
|
||||
public void bind() {
|
||||
public void bind() throws GlUtil.GlException {
|
||||
switch (type) {
|
||||
case GLES20.GL_INT:
|
||||
GLES20.glUniform1i(location, intValue);
|
||||
break;
|
||||
case GLES20.GL_FLOAT:
|
||||
GLES20.glUniform1fv(location, /* count= */ 1, value, /* offset= */ 0);
|
||||
GLES20.glUniform1fv(location, /* count= */ 1, floatValue, /* offset= */ 0);
|
||||
GlUtil.checkGlError();
|
||||
break;
|
||||
case GLES20.GL_FLOAT_VEC2:
|
||||
GLES20.glUniform2fv(location, /* count= */ 1, value, /* offset= */ 0);
|
||||
GLES20.glUniform2fv(location, /* count= */ 1, floatValue, /* offset= */ 0);
|
||||
GlUtil.checkGlError();
|
||||
break;
|
||||
case GLES20.GL_FLOAT_VEC3:
|
||||
GLES20.glUniform3fv(location, /* count= */ 1, value, /* offset= */ 0);
|
||||
GLES20.glUniform3fv(location, /* count= */ 1, floatValue, /* offset= */ 0);
|
||||
GlUtil.checkGlError();
|
||||
break;
|
||||
case GLES20.GL_FLOAT_MAT3:
|
||||
GLES20.glUniformMatrix3fv(
|
||||
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
|
||||
location, /* count= */ 1, /* transpose= */ false, floatValue, /* offset= */ 0);
|
||||
GlUtil.checkGlError();
|
||||
break;
|
||||
case GLES20.GL_FLOAT_MAT4:
|
||||
GLES20.glUniformMatrix4fv(
|
||||
location, /* count= */ 1, /* transpose= */ false, value, /* offset= */ 0);
|
||||
location, /* count= */ 1, /* transpose= */ false, floatValue, /* offset= */ 0);
|
||||
GlUtil.checkGlError();
|
||||
break;
|
||||
case GLES20.GL_SAMPLER_2D:
|
||||
case GLES11Ext.GL_SAMPLER_EXTERNAL_OES:
|
||||
case GL_SAMPLER_EXTERNAL_2D_Y2Y_EXT:
|
||||
if (texId == 0) {
|
||||
if (texIdValue == 0) {
|
||||
throw new IllegalStateException("No call to setSamplerTexId() before bind.");
|
||||
}
|
||||
GLES20.glActiveTexture(GLES20.GL_TEXTURE0 + texUnitIndex);
|
||||
@ -399,7 +427,7 @@ public final class GlProgram {
|
||||
type == GLES20.GL_SAMPLER_2D
|
||||
? GLES20.GL_TEXTURE_2D
|
||||
: GLES11Ext.GL_TEXTURE_EXTERNAL_OES,
|
||||
texId);
|
||||
texIdValue);
|
||||
GLES20.glUniform1i(location, texUnitIndex);
|
||||
GlUtil.checkGlError();
|
||||
break;
|
||||
|
@ -16,6 +16,8 @@
|
||||
package androidx.media3.common.util;
|
||||
|
||||
import static android.opengl.GLU.gluErrorString;
|
||||
import static androidx.media3.common.util.Assertions.checkArgument;
|
||||
import static androidx.media3.common.util.Assertions.checkState;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
@ -26,15 +28,16 @@ import android.opengl.EGLDisplay;
|
||||
import android.opengl.EGLSurface;
|
||||
import android.opengl.GLES11Ext;
|
||||
import android.opengl.GLES20;
|
||||
import android.opengl.GLES30;
|
||||
import android.opengl.Matrix;
|
||||
import androidx.annotation.DoNotInline;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.media3.common.C;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
|
||||
@ -43,41 +46,21 @@ import javax.microedition.khronos.egl.EGL10;
|
||||
@UnstableApi
|
||||
public final class GlUtil {
|
||||
|
||||
/** Thrown when an OpenGL error occurs and {@link #glAssertionsEnabled} is {@code true}. */
|
||||
public static final class GlException extends RuntimeException {
|
||||
/** Thrown when an OpenGL error occurs. */
|
||||
public static final class GlException extends Exception {
|
||||
/** Creates an instance with the specified error message. */
|
||||
public GlException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(b/231937416): Consider removing this flag, enabling assertions by default, and making
|
||||
// GlException checked.
|
||||
/** Whether to throw a {@link GlException} in case of an OpenGL error. */
|
||||
public static boolean glAssertionsEnabled = false;
|
||||
|
||||
/** Number of elements in a 3d homogeneous coordinate vector describing a vertex. */
|
||||
public static final int HOMOGENEOUS_COORDINATE_VECTOR_SIZE = 4;
|
||||
|
||||
/** Length of the normalized device coordinate (NDC) space, which spans from -1 to 1. */
|
||||
public static final float LENGTH_NDC = 2f;
|
||||
|
||||
private static final String TAG = "GlUtil";
|
||||
|
||||
// https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_protected_content.txt
|
||||
private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
|
||||
// https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_surfaceless_context.txt
|
||||
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
|
||||
|
||||
// https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_gl_colorspace.txt
|
||||
private static final int EGL_GL_COLORSPACE_KHR = 0x309D;
|
||||
// https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_gl_colorspace_bt2020_linear.txt
|
||||
private static final int EGL_GL_COLORSPACE_BT2020_PQ_EXT = 0x3340;
|
||||
|
||||
private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_NONE = new int[] {EGL14.EGL_NONE};
|
||||
private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ =
|
||||
new int[] {EGL_GL_COLORSPACE_KHR, EGL_GL_COLORSPACE_BT2020_PQ_EXT, EGL14.EGL_NONE};
|
||||
private static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_8888 =
|
||||
public static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_8888 =
|
||||
new int[] {
|
||||
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
|
||||
EGL14.EGL_RED_SIZE, /* redSize= */ 8,
|
||||
@ -88,7 +71,7 @@ public final class GlUtil {
|
||||
EGL14.EGL_STENCIL_SIZE, /* stencilSize= */ 0,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
private static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_1010102 =
|
||||
public static final int[] EGL_CONFIG_ATTRIBUTES_RGBA_1010102 =
|
||||
new int[] {
|
||||
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
|
||||
EGL14.EGL_RED_SIZE, /* redSize= */ 10,
|
||||
@ -100,6 +83,15 @@ public final class GlUtil {
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
|
||||
// https://www.khronos.org/registry/EGL/extensions/EXT/EGL_EXT_protected_content.txt
|
||||
private static final String EXTENSION_PROTECTED_CONTENT = "EGL_EXT_protected_content";
|
||||
// https://www.khronos.org/registry/EGL/extensions/KHR/EGL_KHR_surfaceless_context.txt
|
||||
private static final String EXTENSION_SURFACELESS_CONTEXT = "EGL_KHR_surfaceless_context";
|
||||
// https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_YUV_target.txt
|
||||
private static final String EXTENSION_YUV_TARGET = "GL_EXT_YUV_target";
|
||||
|
||||
private static final int[] EGL_WINDOW_SURFACE_ATTRIBUTES_NONE = new int[] {EGL14.EGL_NONE};
|
||||
|
||||
/** Class only contains static methods. */
|
||||
private GlUtil() {}
|
||||
|
||||
@ -123,6 +115,18 @@ public final class GlUtil {
|
||||
};
|
||||
}
|
||||
|
||||
/** Creates a 4x4 identity matrix. */
|
||||
public static float[] create4x4IdentityMatrix() {
|
||||
float[] matrix = new float[16];
|
||||
setToIdentity(matrix);
|
||||
return matrix;
|
||||
}
|
||||
|
||||
/** Sets the input {@code matrix} to an identity matrix. */
|
||||
public static void setToIdentity(float[] matrix) {
|
||||
Matrix.setIdentityM(matrix, /* smOffset= */ 0);
|
||||
}
|
||||
|
||||
/** Flattens the list of 4 element NDC coordinate vectors into a buffer. */
|
||||
public static float[] createVertexBuffer(List<float[]> vertexList) {
|
||||
float[] vertexBuffer = new float[HOMOGENEOUS_COORDINATE_VECTOR_SIZE * vertexList.size()];
|
||||
@ -182,25 +186,88 @@ public final class GlUtil {
|
||||
return eglExtensions != null && eglExtensions.contains(EXTENSION_SURFACELESS_CONTEXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the {@value #EXTENSION_YUV_TARGET} extension is supported.
|
||||
*
|
||||
* <p>This extension allows sampling raw YUV values from an external texture, which is required
|
||||
* for HDR.
|
||||
*/
|
||||
public static boolean isYuvTargetExtensionSupported() {
|
||||
if (Util.SDK_INT < 17) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable String glExtensions;
|
||||
if (Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT)) {
|
||||
// Create a placeholder context and make it current to allow calling GLES20.glGetString().
|
||||
try {
|
||||
EGLDisplay eglDisplay = createEglDisplay();
|
||||
EGLContext eglContext = createEglContext(eglDisplay);
|
||||
focusPlaceholderEglSurface(eglContext, eglDisplay);
|
||||
glExtensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);
|
||||
destroyEglContext(eglDisplay, eglContext);
|
||||
} catch (GlException e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
glExtensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);
|
||||
}
|
||||
|
||||
return glExtensions != null && glExtensions.contains(EXTENSION_YUV_TARGET);
|
||||
}
|
||||
|
||||
/** Returns an initialized default {@link EGLDisplay}. */
|
||||
@RequiresApi(17)
|
||||
public static EGLDisplay createEglDisplay() {
|
||||
public static EGLDisplay createEglDisplay() throws GlException {
|
||||
return Api17.createEglDisplay();
|
||||
}
|
||||
|
||||
/** Returns a new {@link EGLContext} for the specified {@link EGLDisplay}. */
|
||||
/**
|
||||
* Creates a new {@link EGLContext} for the specified {@link EGLDisplay}.
|
||||
*
|
||||
* <p>Configures the {@link EGLContext} with {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888} and OpenGL
|
||||
* ES 2.0.
|
||||
*
|
||||
* @param eglDisplay The {@link EGLDisplay} to create an {@link EGLContext} for.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static EGLContext createEglContext(EGLDisplay eglDisplay) {
|
||||
return Api17.createEglContext(eglDisplay, /* version= */ 2, EGL_CONFIG_ATTRIBUTES_RGBA_8888);
|
||||
public static EGLContext createEglContext(EGLDisplay eglDisplay) throws GlException {
|
||||
return createEglContext(eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link EGLContext} for the specified {@link EGLDisplay}, requesting ES 3 and an
|
||||
* RGBA 1010102 config.
|
||||
* Creates a new {@link EGLContext} for the specified {@link EGLDisplay}.
|
||||
*
|
||||
* @param eglDisplay The {@link EGLDisplay} to create an {@link EGLContext} for.
|
||||
* @param configAttributes The attributes to configure EGL with. Accepts either {@link
|
||||
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102}, which will request OpenGL ES 3.0, or {@link
|
||||
* #EGL_CONFIG_ATTRIBUTES_RGBA_8888}, which will request OpenGL ES 2.0.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static EGLContext createEglContextEs3Rgba1010102(EGLDisplay eglDisplay) {
|
||||
return Api17.createEglContext(eglDisplay, /* version= */ 3, EGL_CONFIG_ATTRIBUTES_RGBA_1010102);
|
||||
public static EGLContext createEglContext(EGLDisplay eglDisplay, int[] configAttributes)
|
||||
throws GlException {
|
||||
checkArgument(
|
||||
Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_8888)
|
||||
|| Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_1010102));
|
||||
return Api17.createEglContext(
|
||||
eglDisplay,
|
||||
/* version= */ Arrays.equals(configAttributes, EGL_CONFIG_ATTRIBUTES_RGBA_1010102) ? 3 : 2,
|
||||
configAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link EGLSurface} wrapping the specified {@code surface}.
|
||||
*
|
||||
* <p>The {@link EGLSurface} will configure with {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888} and
|
||||
* OpenGL ES 2.0.
|
||||
*
|
||||
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
|
||||
* @param surface The surface to wrap; must be a surface, surface texture or surface holder.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) throws GlException {
|
||||
return Api17.getEglSurface(
|
||||
eglDisplay, surface, EGL_CONFIG_ATTRIBUTES_RGBA_8888, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -208,27 +275,14 @@ public final class GlUtil {
|
||||
*
|
||||
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
|
||||
* @param surface The surface to wrap; must be a surface, surface texture or surface holder.
|
||||
* @param configAttributes The attributes to configure EGL with. Accepts {@link
|
||||
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static EGLSurface getEglSurface(EGLDisplay eglDisplay, Object surface) {
|
||||
public static EGLSurface getEglSurface(
|
||||
EGLDisplay eglDisplay, Object surface, int[] configAttributes) throws GlException {
|
||||
return Api17.getEglSurface(
|
||||
eglDisplay, surface, EGL_CONFIG_ATTRIBUTES_RGBA_8888, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new {@link EGLSurface} wrapping the specified {@code surface}, for HDR rendering with
|
||||
* Rec. 2020 color primaries and using the PQ transfer function.
|
||||
*
|
||||
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
|
||||
* @param surface The surface to wrap; must be a surface, surface texture or surface holder.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static EGLSurface getEglSurfaceBt2020Pq(EGLDisplay eglDisplay, Object surface) {
|
||||
return Api17.getEglSurface(
|
||||
eglDisplay,
|
||||
surface,
|
||||
EGL_CONFIG_ATTRIBUTES_RGBA_1010102,
|
||||
EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ);
|
||||
eglDisplay, surface, configAttributes, EGL_WINDOW_SURFACE_ATTRIBUTES_NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -237,84 +291,81 @@ public final class GlUtil {
|
||||
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
|
||||
* @param width The width of the pixel buffer.
|
||||
* @param height The height of the pixel buffer.
|
||||
* @param configAttributes EGL configuration attributes. Valid arguments include {@link
|
||||
* #EGL_CONFIG_ATTRIBUTES_RGBA_8888} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_1010102}.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
private static EGLSurface createPbufferSurface(EGLDisplay eglDisplay, int width, int height) {
|
||||
private static EGLSurface createPbufferSurface(
|
||||
EGLDisplay eglDisplay, int width, int height, int[] configAttributes) throws GlException {
|
||||
int[] pbufferAttributes =
|
||||
new int[] {
|
||||
EGL14.EGL_WIDTH, width,
|
||||
EGL14.EGL_HEIGHT, height,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
return Api17.createEglPbufferSurface(
|
||||
eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888, pbufferAttributes);
|
||||
return Api17.createEglPbufferSurface(eglDisplay, configAttributes, pbufferAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a placeholder {@link EGLSurface} to use when reading and writing to the surface is not
|
||||
* required.
|
||||
* Creates and focuses a placeholder {@link EGLSurface}.
|
||||
*
|
||||
* <p>This makes a {@link EGLContext} current when reading and writing to a surface is not
|
||||
* required, configured with {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}.
|
||||
*
|
||||
* @param eglContext The {@link EGLContext} to make current.
|
||||
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
|
||||
* @return {@link EGL14#EGL_NO_SURFACE} if supported and a 1x1 pixel buffer surface otherwise.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static EGLSurface createPlaceholderEglSurface(EGLDisplay eglDisplay) {
|
||||
return isSurfacelessContextExtensionSupported()
|
||||
? EGL14.EGL_NO_SURFACE
|
||||
: createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1);
|
||||
public static EGLSurface focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay)
|
||||
throws GlException {
|
||||
return createFocusedPlaceholderEglSurface(
|
||||
eglContext, eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer.
|
||||
* Creates and focuses a placeholder {@link EGLSurface}.
|
||||
*
|
||||
* <p>This makes a {@link EGLContext} current when reading and writing to a surface is not
|
||||
* required.
|
||||
*
|
||||
* @param eglContext The {@link EGLContext} to make current.
|
||||
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
|
||||
* @param configAttributes The attributes to configure EGL with. Accepts {@link
|
||||
* #EGL_CONFIG_ATTRIBUTES_RGBA_1010102} and {@link #EGL_CONFIG_ATTRIBUTES_RGBA_8888}.
|
||||
* @return A placeholder {@link EGLSurface} that has been focused to allow rendering to take
|
||||
* place, or {@link EGL14#EGL_NO_SURFACE} if the current context supports rendering without a
|
||||
* surface.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static void focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay) {
|
||||
EGLSurface eglSurface = createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1);
|
||||
focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer, for HDR rendering
|
||||
* with Rec. 2020 color primaries and using the PQ transfer function.
|
||||
*
|
||||
* @param eglContext The {@link EGLContext} to make current.
|
||||
* @param eglDisplay The {@link EGLDisplay} to attach the surface to.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static void focusPlaceholderEglSurfaceBt2020Pq(
|
||||
EGLContext eglContext, EGLDisplay eglDisplay) {
|
||||
int[] pbufferAttributes =
|
||||
new int[] {
|
||||
EGL14.EGL_WIDTH,
|
||||
/* width= */ 1,
|
||||
EGL14.EGL_HEIGHT,
|
||||
/* height= */ 1,
|
||||
EGL_GL_COLORSPACE_KHR,
|
||||
EGL_GL_COLORSPACE_BT2020_PQ_EXT,
|
||||
EGL14.EGL_NONE
|
||||
};
|
||||
public static EGLSurface createFocusedPlaceholderEglSurface(
|
||||
EGLContext eglContext, EGLDisplay eglDisplay, int[] configAttributes) throws GlException {
|
||||
EGLSurface eglSurface =
|
||||
Api17.createEglPbufferSurface(
|
||||
eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_1010102, pbufferAttributes);
|
||||
isSurfacelessContextExtensionSupported()
|
||||
? EGL14.EGL_NO_SURFACE
|
||||
: createPbufferSurface(eglDisplay, /* width= */ 1, /* height= */ 1, configAttributes);
|
||||
|
||||
focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1);
|
||||
return eglSurface;
|
||||
}
|
||||
|
||||
/**
|
||||
* If there is an OpenGl error, logs the error and if {@link #glAssertionsEnabled} is true throws
|
||||
* a {@link GlException}.
|
||||
* Collects all OpenGL errors that occurred since this method was last called and throws a {@link
|
||||
* GlException} with the combined error message.
|
||||
*/
|
||||
public static void checkGlError() {
|
||||
int lastError = GLES20.GL_NO_ERROR;
|
||||
public static void checkGlError() throws GlException {
|
||||
StringBuilder errorMessageBuilder = new StringBuilder();
|
||||
boolean foundError = false;
|
||||
int error;
|
||||
while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
|
||||
Log.e(TAG, "glError: " + gluErrorString(error));
|
||||
lastError = error;
|
||||
if (foundError) {
|
||||
errorMessageBuilder.append('\n');
|
||||
}
|
||||
errorMessageBuilder.append("glError: ").append(gluErrorString(error));
|
||||
foundError = true;
|
||||
}
|
||||
if (lastError != GLES20.GL_NO_ERROR) {
|
||||
throwGlException("glError: " + gluErrorString(lastError));
|
||||
if (foundError) {
|
||||
throw new GlException(errorMessageBuilder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@ -325,31 +376,43 @@ public final class GlUtil {
|
||||
* @param height The height for a texture.
|
||||
* @throws GlException If the texture width or height is invalid.
|
||||
*/
|
||||
public static void assertValidTextureSize(int width, int height) {
|
||||
private static void assertValidTextureSize(int width, int height) throws GlException {
|
||||
// TODO(b/201293185): Consider handling adjustments for sizes > GL_MAX_TEXTURE_SIZE
|
||||
// (ex. downscaling appropriately) in a texture processor instead of asserting incorrect
|
||||
// values.
|
||||
|
||||
// For valid GL sizes, see:
|
||||
// https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml
|
||||
int[] maxTextureSizeBuffer = new int[1];
|
||||
GLES20.glGetIntegerv(GLES20.GL_MAX_TEXTURE_SIZE, maxTextureSizeBuffer, 0);
|
||||
int maxTextureSize = maxTextureSizeBuffer[0];
|
||||
checkState(
|
||||
maxTextureSize > 0,
|
||||
"Create a OpenGL context first or run the GL methods on an OpenGL thread.");
|
||||
|
||||
if (width < 0 || height < 0) {
|
||||
throwGlException("width or height is less than 0");
|
||||
throw new GlException("width or height is less than 0");
|
||||
}
|
||||
if (width > maxTextureSize || height > maxTextureSize) {
|
||||
throwGlException("width or height is greater than GL_MAX_TEXTURE_SIZE " + maxTextureSize);
|
||||
throw new GlException(
|
||||
"width or height is greater than GL_MAX_TEXTURE_SIZE " + maxTextureSize);
|
||||
}
|
||||
}
|
||||
|
||||
/** Fills the pixels in the current output render target with (r=0, g=0, b=0, a=0). */
|
||||
public static void clearOutputFrame() throws GlException {
|
||||
GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0);
|
||||
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
|
||||
GlUtil.checkGlError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the specified {@code eglSurface} the render target, using a viewport of {@code width} by
|
||||
* {@code height} pixels.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static void focusEglSurface(
|
||||
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface eglSurface, int width, int height) {
|
||||
EGLDisplay eglDisplay, EGLContext eglContext, EGLSurface eglSurface, int width, int height)
|
||||
throws GlException {
|
||||
Api17.focusRenderTarget(
|
||||
eglDisplay, eglContext, eglSurface, /* framebuffer= */ 0, width, height);
|
||||
}
|
||||
@ -365,16 +428,34 @@ public final class GlUtil {
|
||||
EGLSurface eglSurface,
|
||||
int framebuffer,
|
||||
int width,
|
||||
int height) {
|
||||
int height)
|
||||
throws GlException {
|
||||
Api17.focusRenderTarget(eglDisplay, eglContext, eglSurface, framebuffer, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the specified {@code framebuffer} the render target, using a viewport of {@code width} by
|
||||
* {@code height} pixels.
|
||||
*
|
||||
* <p>The caller must ensure that there is a current OpenGL context before calling this method.
|
||||
*
|
||||
* @param framebuffer The identifier of the framebuffer object to bind as the output render
|
||||
* target.
|
||||
* @param width The viewport width, in pixels.
|
||||
* @param height The viewport height, in pixels.
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static void focusFramebufferUsingCurrentContext(int framebuffer, int width, int height)
|
||||
throws GlException {
|
||||
Api17.focusFramebufferUsingCurrentContext(framebuffer, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a GL texture.
|
||||
*
|
||||
* @param textureId The ID of the texture to delete.
|
||||
*/
|
||||
public static void deleteTexture(int textureId) {
|
||||
public static void deleteTexture(int textureId) throws GlException {
|
||||
GLES20.glDeleteTextures(/* n= */ 1, new int[] {textureId}, /* offset= */ 0);
|
||||
checkGlError();
|
||||
}
|
||||
@ -385,7 +466,7 @@ public final class GlUtil {
|
||||
*/
|
||||
@RequiresApi(17)
|
||||
public static void destroyEglContext(
|
||||
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) {
|
||||
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) throws GlException {
|
||||
Api17.destroyEglContext(eglDisplay, eglContext);
|
||||
}
|
||||
|
||||
@ -403,46 +484,53 @@ public final class GlUtil {
|
||||
*
|
||||
* @param capacity The new buffer's capacity, in floats.
|
||||
*/
|
||||
public static FloatBuffer createBuffer(int capacity) {
|
||||
private static FloatBuffer createBuffer(int capacity) {
|
||||
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(capacity * C.BYTES_PER_FLOAT);
|
||||
return byteBuffer.order(ByteOrder.nativeOrder()).asFloatBuffer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a file from the assets folder.
|
||||
*
|
||||
* @param context The {@link Context}.
|
||||
* @param assetPath The path to the file to load, from the assets folder.
|
||||
* @return The content of the file to load.
|
||||
* @throws IOException If the file couldn't be read.
|
||||
*/
|
||||
public static String loadAsset(Context context, String assetPath) throws IOException {
|
||||
@Nullable InputStream inputStream = null;
|
||||
try {
|
||||
inputStream = context.getAssets().open(assetPath);
|
||||
return Util.fromUtf8Bytes(Util.toByteArray(inputStream));
|
||||
} finally {
|
||||
Util.closeQuietly(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a GL_TEXTURE_EXTERNAL_OES with default configuration of GL_LINEAR filtering and
|
||||
* GL_CLAMP_TO_EDGE wrapping.
|
||||
*/
|
||||
public static int createExternalTexture() {
|
||||
public static int createExternalTexture() throws GlException {
|
||||
int texId = generateTexture();
|
||||
bindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId);
|
||||
return texId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the texture identifier for a newly-allocated texture with the specified dimensions.
|
||||
* Allocates a new RGBA texture with the specified dimensions and color component precision.
|
||||
*
|
||||
* @param width of the new texture in pixels
|
||||
* @param height of the new texture in pixels
|
||||
* @param width The width of the new texture in pixels.
|
||||
* @param height The height of the new texture in pixels.
|
||||
* @param useHighPrecisionColorComponents If {@code false}, uses 8-bit unsigned bytes. If {@code
|
||||
* true}, use 16-bit (half-precision) floating-point.
|
||||
* @throws GlException If the texture allocation fails.
|
||||
* @return The texture identifier for the newly-allocated texture.
|
||||
*/
|
||||
public static int createTexture(int width, int height) {
|
||||
public static int createTexture(int width, int height, boolean useHighPrecisionColorComponents)
|
||||
throws GlException {
|
||||
// TODO(227624622): Implement a pixel test that confirms 16f has less posterization.
|
||||
if (useHighPrecisionColorComponents) {
|
||||
checkState(Util.SDK_INT >= 18, "GLES30 extensions are not supported below API 18.");
|
||||
return createTexture(width, height, GLES30.GL_RGBA16F, GLES30.GL_HALF_FLOAT);
|
||||
}
|
||||
return createTexture(width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocates a new RGBA texture with the specified dimensions and color component precision.
|
||||
*
|
||||
* @param width The width of the new texture in pixels.
|
||||
* @param height The height of the new texture in pixels.
|
||||
* @param internalFormat The number of color components in the texture, as well as their format.
|
||||
* @param type The data type of the pixel data.
|
||||
* @throws GlException If the texture allocation fails.
|
||||
* @return The texture identifier for the newly-allocated texture.
|
||||
*/
|
||||
private static int createTexture(int width, int height, int internalFormat, int type)
|
||||
throws GlException {
|
||||
assertValidTextureSize(width, height);
|
||||
int texId = generateTexture();
|
||||
bindTexture(GLES20.GL_TEXTURE_2D, texId);
|
||||
@ -450,20 +538,20 @@ public final class GlUtil {
|
||||
GLES20.glTexImage2D(
|
||||
GLES20.GL_TEXTURE_2D,
|
||||
/* level= */ 0,
|
||||
GLES20.GL_RGBA,
|
||||
internalFormat,
|
||||
width,
|
||||
height,
|
||||
/* border= */ 0,
|
||||
GLES20.GL_RGBA,
|
||||
GLES20.GL_UNSIGNED_BYTE,
|
||||
type,
|
||||
byteBuffer);
|
||||
checkGlError();
|
||||
return texId;
|
||||
}
|
||||
|
||||
/** Returns a new GL texture identifier. */
|
||||
private static int generateTexture() {
|
||||
checkEglException(
|
||||
private static int generateTexture() throws GlException {
|
||||
checkGlException(
|
||||
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
|
||||
|
||||
int[] texId = new int[1];
|
||||
@ -476,12 +564,12 @@ public final class GlUtil {
|
||||
* Binds the texture of the given type with default configuration of GL_LINEAR filtering and
|
||||
* GL_CLAMP_TO_EDGE wrapping.
|
||||
*
|
||||
* @param texId The texture identifier.
|
||||
* @param textureTarget The target to which the texture is bound, e.g. {@link
|
||||
* GLES20#GL_TEXTURE_2D} for a two-dimensional texture or {@link
|
||||
* GLES11Ext#GL_TEXTURE_EXTERNAL_OES} for an external texture.
|
||||
* @param texId The texture identifier.
|
||||
*/
|
||||
public static void bindTexture(int textureTarget, int texId) {
|
||||
public static void bindTexture(int textureTarget, int texId) throws GlException {
|
||||
GLES20.glBindTexture(textureTarget, texId);
|
||||
checkGlError();
|
||||
GLES20.glTexParameteri(textureTarget, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
|
||||
@ -499,8 +587,8 @@ public final class GlUtil {
|
||||
*
|
||||
* @param texId The identifier of the texture to attach to the framebuffer.
|
||||
*/
|
||||
public static int createFboForTexture(int texId) {
|
||||
checkEglException(
|
||||
public static int createFboForTexture(int texId) throws GlException {
|
||||
checkGlException(
|
||||
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
|
||||
|
||||
int[] fboId = new int[1];
|
||||
@ -514,23 +602,19 @@ public final class GlUtil {
|
||||
return fboId[0];
|
||||
}
|
||||
|
||||
/* package */ static void throwGlException(String errorMsg) {
|
||||
if (glAssertionsEnabled) {
|
||||
throw new GlException(errorMsg);
|
||||
} else {
|
||||
Log.e(TAG, errorMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkEglException(boolean expression, String errorMessage) {
|
||||
/**
|
||||
* Throws a {@link GlException} with the given message if {@code expression} evaluates to {@code
|
||||
* false}.
|
||||
*/
|
||||
public static void checkGlException(boolean expression, String errorMessage) throws GlException {
|
||||
if (!expression) {
|
||||
throwGlException(errorMessage);
|
||||
throw new GlException(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkEglException(String errorMessage) {
|
||||
private static void checkEglException(String errorMessage) throws GlException {
|
||||
int error = EGL14.eglGetError();
|
||||
checkEglException(error == EGL14.EGL_SUCCESS, errorMessage + ", error code: " + error);
|
||||
checkGlException(error == EGL14.EGL_SUCCESS, errorMessage + ", error code: " + error);
|
||||
}
|
||||
|
||||
@RequiresApi(17)
|
||||
@ -538,24 +622,24 @@ public final class GlUtil {
|
||||
private Api17() {}
|
||||
|
||||
@DoNotInline
|
||||
public static EGLDisplay createEglDisplay() {
|
||||
public static EGLDisplay createEglDisplay() throws GlException {
|
||||
EGLDisplay eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
|
||||
checkEglException(!eglDisplay.equals(EGL14.EGL_NO_DISPLAY), "No EGL display.");
|
||||
if (!EGL14.eglInitialize(
|
||||
eglDisplay,
|
||||
/* unusedMajor */ new int[1],
|
||||
/* majorOffset= */ 0,
|
||||
/* unusedMinor */ new int[1],
|
||||
/* minorOffset= */ 0)) {
|
||||
throwGlException("Error in eglInitialize.");
|
||||
}
|
||||
checkGlException(!eglDisplay.equals(EGL14.EGL_NO_DISPLAY), "No EGL display.");
|
||||
checkGlException(
|
||||
EGL14.eglInitialize(
|
||||
eglDisplay,
|
||||
/* unusedMajor */ new int[1],
|
||||
/* majorOffset= */ 0,
|
||||
/* unusedMinor */ new int[1],
|
||||
/* minorOffset= */ 0),
|
||||
"Error in eglInitialize.");
|
||||
checkGlError();
|
||||
return eglDisplay;
|
||||
}
|
||||
|
||||
@DoNotInline
|
||||
public static EGLContext createEglContext(
|
||||
EGLDisplay eglDisplay, int version, int[] configAttributes) {
|
||||
EGLDisplay eglDisplay, int version, int[] configAttributes) throws GlException {
|
||||
int[] contextAttributes = {EGL14.EGL_CONTEXT_CLIENT_VERSION, version, EGL14.EGL_NONE};
|
||||
EGLContext eglContext =
|
||||
EGL14.eglCreateContext(
|
||||
@ -566,7 +650,7 @@ public final class GlUtil {
|
||||
/* offset= */ 0);
|
||||
if (eglContext == null) {
|
||||
EGL14.eglTerminate(eglDisplay);
|
||||
throwGlException(
|
||||
throw new GlException(
|
||||
"eglCreateContext() failed to create a valid context. The device may not support EGL"
|
||||
+ " version "
|
||||
+ version);
|
||||
@ -580,7 +664,8 @@ public final class GlUtil {
|
||||
EGLDisplay eglDisplay,
|
||||
Object surface,
|
||||
int[] configAttributes,
|
||||
int[] windowSurfaceAttributes) {
|
||||
int[] windowSurfaceAttributes)
|
||||
throws GlException {
|
||||
EGLSurface eglSurface =
|
||||
EGL14.eglCreateWindowSurface(
|
||||
eglDisplay,
|
||||
@ -594,7 +679,7 @@ public final class GlUtil {
|
||||
|
||||
@DoNotInline
|
||||
public static EGLSurface createEglPbufferSurface(
|
||||
EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) {
|
||||
EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) throws GlException {
|
||||
EGLSurface eglSurface =
|
||||
EGL14.eglCreatePbufferSurface(
|
||||
eglDisplay,
|
||||
@ -612,22 +697,32 @@ public final class GlUtil {
|
||||
EGLSurface eglSurface,
|
||||
int framebuffer,
|
||||
int width,
|
||||
int height) {
|
||||
int height)
|
||||
throws GlException {
|
||||
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
|
||||
checkEglException("Error making context current");
|
||||
focusFramebufferUsingCurrentContext(framebuffer, width, height);
|
||||
}
|
||||
|
||||
@DoNotInline
|
||||
public static void focusFramebufferUsingCurrentContext(int framebuffer, int width, int height)
|
||||
throws GlException {
|
||||
checkGlException(
|
||||
!Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context");
|
||||
|
||||
int[] boundFramebuffer = new int[1];
|
||||
GLES20.glGetIntegerv(GLES20.GL_FRAMEBUFFER_BINDING, boundFramebuffer, /* offset= */ 0);
|
||||
if (boundFramebuffer[0] != framebuffer) {
|
||||
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, framebuffer);
|
||||
}
|
||||
checkGlError();
|
||||
EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext);
|
||||
checkEglException("Error making context current");
|
||||
GLES20.glViewport(/* x= */ 0, /* y= */ 0, width, height);
|
||||
checkGlError();
|
||||
}
|
||||
|
||||
@DoNotInline
|
||||
public static void destroyEglContext(
|
||||
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) {
|
||||
@Nullable EGLDisplay eglDisplay, @Nullable EGLContext eglContext) throws GlException {
|
||||
if (eglDisplay == null) {
|
||||
return;
|
||||
}
|
||||
@ -645,7 +740,8 @@ public final class GlUtil {
|
||||
}
|
||||
|
||||
@DoNotInline
|
||||
private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes) {
|
||||
private static EGLConfig getEglConfig(EGLDisplay eglDisplay, int[] attributes)
|
||||
throws GlException {
|
||||
EGLConfig[] eglConfigs = new EGLConfig[1];
|
||||
if (!EGL14.eglChooseConfig(
|
||||
eglDisplay,
|
||||
@ -656,7 +752,7 @@ public final class GlUtil {
|
||||
/* config_size= */ 1,
|
||||
/* unusedNumConfig */ new int[1],
|
||||
/* num_configOffset= */ 0)) {
|
||||
throwGlException("eglChooseConfig failed.");
|
||||
throw new GlException("eglChooseConfig failed.");
|
||||
}
|
||||
return eglConfigs[0];
|
||||
}
|
||||
|
@ -270,6 +270,7 @@ public final class ListenerSet<T extends @NonNull Object> {
|
||||
public void release(IterationFinishedEvent<T> event) {
|
||||
released = true;
|
||||
if (needsIterationFinishedEvent) {
|
||||
needsIterationFinishedEvent = false;
|
||||
event.invoke(listener, flagsBuilder.build());
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
package androidx.media3.common.util;
|
||||
|
||||
import static androidx.media3.common.util.Util.SDK_INT;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.media.AudioFormat;
|
||||
import android.media.MediaFormat;
|
||||
@ -191,6 +193,53 @@ public final class MediaFormatUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a {@code ColorInfo}, if a valid instance is described in the {@link
|
||||
* MediaFormat}.
|
||||
*/
|
||||
@Nullable
|
||||
public static ColorInfo getColorInfo(MediaFormat mediaFormat) {
|
||||
if (SDK_INT < 29) {
|
||||
return null;
|
||||
}
|
||||
int colorSpace =
|
||||
mediaFormat.getInteger(MediaFormat.KEY_COLOR_STANDARD, /* defaultValue= */ Format.NO_VALUE);
|
||||
int colorRange =
|
||||
mediaFormat.getInteger(MediaFormat.KEY_COLOR_RANGE, /* defaultValue= */ Format.NO_VALUE);
|
||||
int colorTransfer =
|
||||
mediaFormat.getInteger(MediaFormat.KEY_COLOR_TRANSFER, /* defaultValue= */ Format.NO_VALUE);
|
||||
@Nullable
|
||||
ByteBuffer hdrStaticInfoByteBuffer = mediaFormat.getByteBuffer(MediaFormat.KEY_HDR_STATIC_INFO);
|
||||
@Nullable
|
||||
byte[] hdrStaticInfo =
|
||||
hdrStaticInfoByteBuffer != null ? getArray(hdrStaticInfoByteBuffer) : null;
|
||||
// Some devices may produce invalid values from MediaFormat#getInteger.
|
||||
// See b/239435670 for more information.
|
||||
if (!isValidColorSpace(colorSpace)) {
|
||||
colorSpace = Format.NO_VALUE;
|
||||
}
|
||||
if (!isValidColorRange(colorRange)) {
|
||||
colorRange = Format.NO_VALUE;
|
||||
}
|
||||
if (!isValidColorTransfer(colorTransfer)) {
|
||||
colorTransfer = Format.NO_VALUE;
|
||||
}
|
||||
|
||||
if (colorSpace != Format.NO_VALUE
|
||||
|| colorRange != Format.NO_VALUE
|
||||
|| colorTransfer != Format.NO_VALUE
|
||||
|| hdrStaticInfo != null) {
|
||||
return new ColorInfo(colorSpace, colorRange, colorTransfer, hdrStaticInfo);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static byte[] getArray(ByteBuffer byteBuffer) {
|
||||
byte[] array = new byte[byteBuffer.remaining()];
|
||||
byteBuffer.get(array);
|
||||
return array;
|
||||
}
|
||||
|
||||
// Internal methods.
|
||||
|
||||
private static void setBooleanAsInt(MediaFormat format, String key, int value) {
|
||||
@ -253,5 +302,31 @@ public final class MediaFormatUtil {
|
||||
mediaFormat.setInteger(MediaFormat.KEY_PCM_ENCODING, mediaFormatPcmEncoding);
|
||||
}
|
||||
|
||||
/** Whether this is a valid {@link C.ColorSpace} instance. */
|
||||
private static boolean isValidColorSpace(int colorSpace) {
|
||||
// LINT.IfChange(color_space)
|
||||
return colorSpace == C.COLOR_SPACE_BT601
|
||||
|| colorSpace == C.COLOR_SPACE_BT709
|
||||
|| colorSpace == C.COLOR_SPACE_BT2020
|
||||
|| colorSpace == Format.NO_VALUE;
|
||||
}
|
||||
|
||||
/** Whether this is a valid {@link C.ColorRange} instance. */
|
||||
private static boolean isValidColorRange(int colorRange) {
|
||||
// LINT.IfChange(color_range)
|
||||
return colorRange == C.COLOR_RANGE_LIMITED
|
||||
|| colorRange == C.COLOR_RANGE_FULL
|
||||
|| colorRange == Format.NO_VALUE;
|
||||
}
|
||||
|
||||
/** Whether this is a valid {@link C.ColorTransfer} instance. */
|
||||
private static boolean isValidColorTransfer(int colorTransfer) {
|
||||
// LINT.IfChange(color_transfer)
|
||||
return colorTransfer == C.COLOR_TRANSFER_SDR
|
||||
|| colorTransfer == C.COLOR_TRANSFER_ST2084
|
||||
|| colorTransfer == C.COLOR_TRANSFER_HLG
|
||||
|| colorTransfer == Format.NO_VALUE;
|
||||
}
|
||||
|
||||
private MediaFormatUtil() {}
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ public final class NetworkTypeObserver {
|
||||
networkType = C.NETWORK_TYPE_UNKNOWN;
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ConnectivityManager.CONNECTIVITY_ACTION);
|
||||
context.registerReceiver(/* receiver= */ new Receiver(), filter);
|
||||
Util.registerReceiverNotExported(context, new Receiver(), filter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
@ -21,6 +21,7 @@ import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ -136,6 +137,7 @@ import java.util.List;
|
||||
@Nullable private android.os.Message message;
|
||||
@Nullable private SystemHandlerWrapper handler;
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
public SystemMessage setMessage(android.os.Message message, SystemHandlerWrapper handler) {
|
||||
this.message = message;
|
||||
this.handler = handler;
|
||||
|
@ -66,6 +66,8 @@ public final class TimestampAdjuster {
|
||||
* Next sample timestamps for calling threads in shared mode when {@link #timestampOffsetUs} has
|
||||
* not yet been set.
|
||||
*/
|
||||
// incompatible type argument for type parameter T of ThreadLocal.
|
||||
@SuppressWarnings("nullness:type.argument.type.incompatible")
|
||||
private final ThreadLocal<Long> nextSampleTimestampUs;
|
||||
|
||||
/**
|
||||
@ -73,6 +75,8 @@ public final class TimestampAdjuster {
|
||||
* microseconds, or {@link #MODE_NO_OFFSET} if timestamps should not be offset, or {@link
|
||||
* #MODE_SHARED} if the adjuster will be used in shared mode.
|
||||
*/
|
||||
// incompatible types in assignment.
|
||||
@SuppressWarnings("nullness:assignment.type.incompatible")
|
||||
public TimestampAdjuster(long firstSampleTimestampUs) {
|
||||
nextSampleTimestampUs = new ThreadLocal<>();
|
||||
reset(firstSampleTimestampUs);
|
||||
|
@ -34,9 +34,11 @@ import android.Manifest.permission;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.UiModeManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
@ -78,6 +80,11 @@ import androidx.media3.common.Player;
|
||||
import androidx.media3.common.Player.Commands;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.util.concurrent.AsyncFunction;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
@ -100,6 +107,8 @@ import java.util.MissingResourceException;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.regex.Matcher;
|
||||
@ -116,8 +125,8 @@ import org.checkerframework.checker.nullness.qual.PolyNull;
|
||||
public final class Util {
|
||||
|
||||
/**
|
||||
* Like {@link android.os.Build.VERSION#SDK_INT}, but in a place where it can be conveniently
|
||||
* overridden for local testing.
|
||||
* Like {@link Build.VERSION#SDK_INT}, but in a place where it can be conveniently overridden for
|
||||
* local testing.
|
||||
*/
|
||||
@UnstableApi public static final int SDK_INT = Build.VERSION.SDK_INT;
|
||||
|
||||
@ -189,6 +198,54 @@ public final class Util {
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link BroadcastReceiver} that's not intended to receive broadcasts from other
|
||||
* apps. This will be enforced by specifying {@link Context#RECEIVER_NOT_EXPORTED} if {@link
|
||||
* #SDK_INT} is 33 or above.
|
||||
*
|
||||
* @param context The context on which {@link Context#registerReceiver} will be called.
|
||||
* @param receiver The {@link BroadcastReceiver} to register. This value may be null.
|
||||
* @param filter Selects the Intent broadcasts to be received.
|
||||
* @return The first sticky intent found that matches {@code filter}, or null if there are none.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Nullable
|
||||
public static Intent registerReceiverNotExported(
|
||||
Context context, @Nullable BroadcastReceiver receiver, IntentFilter filter) {
|
||||
if (SDK_INT < 33) {
|
||||
return context.registerReceiver(receiver, filter);
|
||||
} else {
|
||||
return context.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link BroadcastReceiver} that's not intended to receive broadcasts from other
|
||||
* apps. This will be enforced by specifying {@link Context#RECEIVER_NOT_EXPORTED} if {@link
|
||||
* #SDK_INT} is 33 or above.
|
||||
*
|
||||
* @param context The context on which {@link Context#registerReceiver} will be called.
|
||||
* @param receiver The {@link BroadcastReceiver} to register. This value may be null.
|
||||
* @param filter Selects the Intent broadcasts to be received.
|
||||
* @param handler Handler identifying the thread that will receive the Intent.
|
||||
* @return The first sticky intent found that matches {@code filter}, or null if there are none.
|
||||
*/
|
||||
@UnstableApi
|
||||
@Nullable
|
||||
public static Intent registerReceiverNotExported(
|
||||
Context context, BroadcastReceiver receiver, IntentFilter filter, Handler handler) {
|
||||
if (SDK_INT < 33) {
|
||||
return context.registerReceiver(receiver, filter, /* broadcastPermission= */ null, handler);
|
||||
} else {
|
||||
return context.registerReceiver(
|
||||
receiver,
|
||||
filter,
|
||||
/* broadcastPermission= */ null,
|
||||
handler,
|
||||
Context.RECEIVER_NOT_EXPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls {@link Context#startForegroundService(Intent)} if {@link #SDK_INT} is 26 or higher, or
|
||||
* {@link Context#startService(Intent)} otherwise.
|
||||
@ -574,6 +631,94 @@ public final class Util {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Posts the {@link Runnable} if the calling thread differs with the {@link Looper} of the {@link
|
||||
* Handler}. Otherwise, runs the {@link Runnable} directly. Also returns a {@link
|
||||
* ListenableFuture} for when the {@link Runnable} has run.
|
||||
*
|
||||
* @param handler The handler to which the {@link Runnable} will be posted.
|
||||
* @param runnable The runnable to either post or run.
|
||||
* @param successValue The value to set in the {@link ListenableFuture} once the runnable
|
||||
* completes.
|
||||
* @param <T> The type of {@code successValue}.
|
||||
* @return A {@link ListenableFuture} for when the {@link Runnable} has run.
|
||||
*/
|
||||
@UnstableApi
|
||||
public static <T> ListenableFuture<T> postOrRunWithCompletion(
|
||||
Handler handler, Runnable runnable, T successValue) {
|
||||
SettableFuture<T> outputFuture = SettableFuture.create();
|
||||
postOrRun(
|
||||
handler,
|
||||
() -> {
|
||||
try {
|
||||
if (outputFuture.isCancelled()) {
|
||||
return;
|
||||
}
|
||||
runnable.run();
|
||||
outputFuture.set(successValue);
|
||||
} catch (Throwable e) {
|
||||
outputFuture.setException(e);
|
||||
}
|
||||
});
|
||||
return outputFuture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously transforms the result of a {@link ListenableFuture}.
|
||||
*
|
||||
* <p>The transformation function is called using a {@linkplain MoreExecutors#directExecutor()
|
||||
* direct executor}.
|
||||
*
|
||||
* <p>The returned Future attempts to keep its cancellation state in sync with that of the input
|
||||
* future and that of the future returned by the transform function. That is, if the returned
|
||||
* Future is cancelled, it will attempt to cancel the other two, and if either of the other two is
|
||||
* cancelled, the returned Future will also be cancelled. All forwarded cancellations will not
|
||||
* attempt to interrupt.
|
||||
*
|
||||
* @param future The input {@link ListenableFuture}.
|
||||
* @param transformFunction The function transforming the result of the input future.
|
||||
* @param <T> The result type of the input future.
|
||||
* @param <U> The result type of the transformation function.
|
||||
* @return A {@link ListenableFuture} for the transformed result.
|
||||
*/
|
||||
@UnstableApi
|
||||
public static <T, U> ListenableFuture<T> transformFutureAsync(
|
||||
ListenableFuture<U> future, AsyncFunction<U, T> transformFunction) {
|
||||
// This is a simplified copy of Guava's Futures.transformAsync.
|
||||
SettableFuture<T> outputFuture = SettableFuture.create();
|
||||
outputFuture.addListener(
|
||||
() -> {
|
||||
if (outputFuture.isCancelled()) {
|
||||
future.cancel(/* mayInterruptIfRunning= */ false);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
future.addListener(
|
||||
() -> {
|
||||
U inputFutureResult;
|
||||
try {
|
||||
inputFutureResult = Futures.getDone(future);
|
||||
} catch (CancellationException cancellationException) {
|
||||
outputFuture.cancel(/* mayInterruptIfRunning= */ false);
|
||||
return;
|
||||
} catch (ExecutionException exception) {
|
||||
@Nullable Throwable cause = exception.getCause();
|
||||
outputFuture.setException(cause == null ? exception : cause);
|
||||
return;
|
||||
} catch (RuntimeException | Error error) {
|
||||
outputFuture.setException(error);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
outputFuture.setFuture(transformFunction.apply(inputFutureResult));
|
||||
} catch (Throwable exception) {
|
||||
outputFuture.setException(exception);
|
||||
}
|
||||
},
|
||||
MoreExecutors.directExecutor());
|
||||
return outputFuture;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link Looper} associated with the current thread, or the {@link Looper} of the
|
||||
* application's main thread if the current thread doesn't have a {@link Looper}.
|
||||
@ -1716,6 +1861,7 @@ public final class Util {
|
||||
* @return The channel configuration or {@link AudioFormat#CHANNEL_INVALID} if output is not
|
||||
* possible.
|
||||
*/
|
||||
@SuppressLint("InlinedApi") // Inlined AudioFormat constants.
|
||||
@UnstableApi
|
||||
public static int getAudioTrackChannelConfig(int channelCount) {
|
||||
switch (channelCount) {
|
||||
@ -1734,21 +1880,9 @@ public final class Util {
|
||||
case 7:
|
||||
return AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
|
||||
case 8:
|
||||
if (SDK_INT >= 23) {
|
||||
return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
|
||||
} else if (SDK_INT >= 21) {
|
||||
// Equal to AudioFormat.CHANNEL_OUT_7POINT1_SURROUND, which is hidden before Android M.
|
||||
return AudioFormat.CHANNEL_OUT_5POINT1
|
||||
| AudioFormat.CHANNEL_OUT_SIDE_LEFT
|
||||
| AudioFormat.CHANNEL_OUT_SIDE_RIGHT;
|
||||
} else {
|
||||
// 8 ch output is not supported before Android L.
|
||||
return AudioFormat.CHANNEL_INVALID;
|
||||
}
|
||||
return AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
|
||||
case 12:
|
||||
return Util.SDK_INT >= 32
|
||||
? AudioFormat.CHANNEL_OUT_7POINT1POINT4
|
||||
: AudioFormat.CHANNEL_INVALID;
|
||||
return AudioFormat.CHANNEL_OUT_7POINT1POINT4;
|
||||
default:
|
||||
return AudioFormat.CHANNEL_INVALID;
|
||||
}
|
||||
@ -2604,6 +2738,7 @@ public final class Util {
|
||||
* @param newFromIndex The new from index.
|
||||
*/
|
||||
@UnstableApi
|
||||
@SuppressWarnings("ExtendsObject") // See go/lsc-extends-object
|
||||
public static <T extends Object> void moveItems(
|
||||
List<T> items, int fromIndex, int toIndex, int newFromIndex) {
|
||||
ArrayDeque<T> removedItems = new ArrayDeque<>();
|
||||
|
@ -25,6 +25,7 @@ import static org.junit.Assert.fail;
|
||||
|
||||
import android.net.Uri;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@ -33,7 +34,7 @@ import org.junit.runner.RunWith;
|
||||
public class AdPlaybackStateTest {
|
||||
|
||||
private static final long[] TEST_AD_GROUP_TIMES_US = new long[] {0, 5_000_000, 10_000_000};
|
||||
private static final Uri TEST_URI = Uri.EMPTY;
|
||||
private static final Uri TEST_URI = Uri.parse("http://www.google.com");
|
||||
private static final Object TEST_ADS_ID = new Object();
|
||||
|
||||
@Test
|
||||
@ -52,7 +53,7 @@ public class AdPlaybackStateTest {
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
|
||||
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
|
||||
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 2);
|
||||
|
||||
assertThat(state.getAdGroup(1).uris[0]).isNull();
|
||||
@ -99,7 +100,7 @@ public class AdPlaybackStateTest {
|
||||
.withRemovedAdGroupCount(1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 2)
|
||||
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 1)
|
||||
.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI)
|
||||
.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI)
|
||||
.withSkippedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0);
|
||||
|
||||
state =
|
||||
@ -139,8 +140,8 @@ public class AdPlaybackStateTest {
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
|
||||
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
|
||||
assertThat(state.getAdGroup(1).getFirstAdIndexToPlay()).isEqualTo(0);
|
||||
}
|
||||
@ -150,8 +151,8 @@ public class AdPlaybackStateTest {
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
|
||||
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
|
||||
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
|
||||
|
||||
@ -165,8 +166,8 @@ public class AdPlaybackStateTest {
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
|
||||
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
|
||||
state = state.withSkippedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
|
||||
|
||||
@ -180,8 +181,8 @@ public class AdPlaybackStateTest {
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
|
||||
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
|
||||
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
|
||||
state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1);
|
||||
@ -194,7 +195,7 @@ public class AdPlaybackStateTest {
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
|
||||
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
|
||||
|
||||
state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1);
|
||||
|
||||
@ -207,9 +208,9 @@ public class AdPlaybackStateTest {
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
|
||||
state = state.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true);
|
||||
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
|
||||
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
|
||||
|
||||
@ -222,9 +223,9 @@ public class AdPlaybackStateTest {
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US).withRemovedAdGroupCount(1);
|
||||
state = state.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true);
|
||||
state = state.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 3);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
|
||||
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0);
|
||||
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1);
|
||||
@ -247,6 +248,51 @@ public class AdPlaybackStateTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withAvailableAd() {
|
||||
int adGroupIndex = 2;
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US)
|
||||
.withRemovedAdGroupCount(2)
|
||||
.withAdCount(adGroupIndex, 3)
|
||||
.withAdDurationsUs(adGroupIndex, /* adDurationsUs...*/ 10, 20, 30)
|
||||
.withIsServerSideInserted(adGroupIndex, true);
|
||||
|
||||
state = state.withAvailableAd(adGroupIndex, /* adIndexInAdGroup= */ 2);
|
||||
|
||||
assertThat(state.getAdGroup(adGroupIndex).states)
|
||||
.asList()
|
||||
.containsExactly(AD_STATE_UNAVAILABLE, AD_STATE_UNAVAILABLE, AD_STATE_AVAILABLE)
|
||||
.inOrder();
|
||||
assertThat(state.getAdGroup(adGroupIndex).uris)
|
||||
.asList()
|
||||
.containsExactly(null, null, Uri.EMPTY)
|
||||
.inOrder();
|
||||
|
||||
state =
|
||||
state
|
||||
.withAvailableAd(adGroupIndex, /* adIndexInAdGroup= */ 0)
|
||||
.withAvailableAd(adGroupIndex, /* adIndexInAdGroup= */ 1)
|
||||
.withAvailableAd(adGroupIndex, /* adIndexInAdGroup= */ 2);
|
||||
|
||||
assertThat(state.getAdGroup(adGroupIndex).states)
|
||||
.asList()
|
||||
.containsExactly(AD_STATE_AVAILABLE, AD_STATE_AVAILABLE, AD_STATE_AVAILABLE)
|
||||
.inOrder();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withAvailableAd_forClientSideAdGroup_throwsRuntimeException() {
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US)
|
||||
.withRemovedAdGroupCount(2)
|
||||
.withAdCount(/* adGroupIndex= */ 2, 3)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 2, /* adDurationsUs...*/ 10, 20, 30);
|
||||
|
||||
Assert.assertThrows(
|
||||
IllegalStateException.class, () -> state.withAvailableAd(/* adGroupIndex= */ 2, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void skipAllWithoutAdCount() {
|
||||
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US);
|
||||
@ -265,6 +311,51 @@ public class AdPlaybackStateTest {
|
||||
assertThat(state.getAdGroup(1).count).isEqualTo(C.LENGTH_UNSET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withOriginalAdCount() {
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 5_000_000)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2);
|
||||
|
||||
state = state.withOriginalAdCount(/* adGroupIndex= */ 0, /* originalAdCount= */ 3);
|
||||
|
||||
assertThat(state.getAdGroup(0).count).isEqualTo(2);
|
||||
assertThat(state.getAdGroup(0).originalCount).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withOriginalAdCount_unsetValue_defaultsToIndexUnset() {
|
||||
AdPlaybackState state =
|
||||
new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 5_000_000)
|
||||
.withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 2);
|
||||
|
||||
assertThat(state.getAdGroup(0).count).isEqualTo(2);
|
||||
assertThat(state.getAdGroup(0).originalCount).isEqualTo(C.INDEX_UNSET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withLastAdGroupRemoved() {
|
||||
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, /* adGroupTimesUs...= */ 5_000_000);
|
||||
state =
|
||||
state
|
||||
.withAdCount(/* adGroupIndex= */ 0, 3)
|
||||
.withAdDurationsUs(/* adGroupIndex= */ 0, 10_000L, 20_000L, 30_000L)
|
||||
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)
|
||||
.withPlayedAd(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 1)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 0, true);
|
||||
|
||||
state = state.withLastAdRemoved(0);
|
||||
|
||||
assertThat(state.getAdGroup(/* adGroupIndex= */ 0).states).asList().hasSize(2);
|
||||
assertThat(state.getAdGroup(/* adGroupIndex= */ 0).durationsUs)
|
||||
.asList()
|
||||
.containsExactly(10_000L, 20_000L)
|
||||
.inOrder();
|
||||
assertThat(state.getAdGroup(/* adGroupIndex= */ 0).states)
|
||||
.asList()
|
||||
.containsExactly(AD_STATE_PLAYED, AD_STATE_PLAYED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withResetAdGroup_resetsAdsInFinalStates() {
|
||||
AdPlaybackState state = new AdPlaybackState(TEST_ADS_ID, TEST_AD_GROUP_TIMES_US);
|
||||
@ -272,10 +363,10 @@ public class AdPlaybackStateTest {
|
||||
state =
|
||||
state.withAdDurationsUs(
|
||||
/* adGroupIndex= */ 1, /* adDurationsUs...= */ 1_000L, 2_000L, 3_000L, 4_000L, 5_000L);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, Uri.EMPTY);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, Uri.EMPTY);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3, Uri.EMPTY);
|
||||
state = state.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4, Uri.EMPTY);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 1, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3, TEST_URI);
|
||||
state = state.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4, TEST_URI);
|
||||
state = state.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 2);
|
||||
state = state.withSkippedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 3);
|
||||
state = state.withAdLoadError(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 4);
|
||||
@ -303,7 +394,7 @@ public class AdPlaybackStateTest {
|
||||
.inOrder();
|
||||
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).uris)
|
||||
.asList()
|
||||
.containsExactly(null, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY, Uri.EMPTY)
|
||||
.containsExactly(null, TEST_URI, TEST_URI, TEST_URI, TEST_URI)
|
||||
.inOrder();
|
||||
assertThat(state.getAdGroup(/* adGroupIndex= */ 1).durationsUs)
|
||||
.asList()
|
||||
@ -317,12 +408,12 @@ public class AdPlaybackStateTest {
|
||||
.withRemovedAdGroupCount(1)
|
||||
.withAdCount(/* adGroupIndex= */ 1, /* adCount= */ 1)
|
||||
.withPlayedAd(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0)
|
||||
.withAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI)
|
||||
.withAvailableAdUri(/* adGroupIndex= */ 1, /* adIndexInAdGroup= */ 0, TEST_URI)
|
||||
.withAdCount(/* adGroupIndex= */ 2, /* adCount= */ 2)
|
||||
.withSkippedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0)
|
||||
.withPlayedAd(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 1)
|
||||
.withAdUri(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, TEST_URI)
|
||||
.withAdUri(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 1, TEST_URI)
|
||||
.withAvailableAdUri(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 0, TEST_URI)
|
||||
.withAvailableAdUri(/* adGroupIndex= */ 2, /* adIndexInAdGroup= */ 1, TEST_URI)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 1, /* contentResumeOffsetUs= */ 4444)
|
||||
.withContentResumeOffsetUs(/* adGroupIndex= */ 2, /* contentResumeOffsetUs= */ 3333)
|
||||
.withIsServerSideInserted(/* adGroupIndex= */ 1, /* isServerSideInserted= */ true)
|
||||
|
@ -20,6 +20,7 @@ import static androidx.media3.common.MimeTypes.VIDEO_MP4;
|
||||
import static androidx.media3.common.MimeTypes.VIDEO_WEBM;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import android.os.Bundle;
|
||||
import androidx.media3.test.utils.FakeMetadataEntry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import java.util.ArrayList;
|
||||
@ -46,6 +47,16 @@ public final class FormatTest {
|
||||
assertThat(formatFromBundle).isEqualTo(formatToBundle);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void roundTripViaBundle_excludeMetadata_hasMetadataExcluded() {
|
||||
Format format = createTestFormat();
|
||||
|
||||
Bundle bundleWithMetadataExcluded = format.toBundle(/* excludeMetadata= */ true);
|
||||
|
||||
Format formatWithMetadataExcluded = Format.CREATOR.fromBundle(bundleWithMetadataExcluded);
|
||||
assertThat(formatWithMetadataExcluded).isEqualTo(format.buildUpon().setMetadata(null).build());
|
||||
}
|
||||
|
||||
private static Format createTestFormat() {
|
||||
byte[] initData1 = new byte[] {1, 2, 3};
|
||||
byte[] initData2 = new byte[] {4, 5, 6};
|
||||
@ -60,7 +71,6 @@ public final class FormatTest {
|
||||
DrmInitData drmInitData = new DrmInitData(drmData1, drmData2);
|
||||
|
||||
byte[] projectionData = new byte[] {1, 2, 3};
|
||||
|
||||
Metadata metadata = new Metadata(new FakeMetadataEntry("id1"), new FakeMetadataEntry("id2"));
|
||||
|
||||
ColorInfo colorInfo =
|
||||
|
@ -30,7 +30,10 @@ public class MetadataTest {
|
||||
@Test
|
||||
public void parcelable() {
|
||||
Metadata metadataToParcel =
|
||||
new Metadata(new FakeMetadataEntry("id1"), new FakeMetadataEntry("id2"));
|
||||
new Metadata(
|
||||
/* presentationTimeUs= */ 1_230_000,
|
||||
new FakeMetadataEntry("id1"),
|
||||
new FakeMetadataEntry("id2"));
|
||||
|
||||
Parcel parcel = Parcel.obtain();
|
||||
metadataToParcel.writeToParcel(parcel, 0);
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -37,7 +37,7 @@ public class CueGroupTest {
|
||||
Cue bitmapCue =
|
||||
new Cue.Builder().setBitmap(Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888)).build();
|
||||
ImmutableList<Cue> cues = ImmutableList.of(textCue, bitmapCue);
|
||||
CueGroup cueGroup = new CueGroup(cues);
|
||||
CueGroup cueGroup = new CueGroup(cues, /* presentationTimeUs= */ 1_230_000);
|
||||
|
||||
Parcel parcel = Parcel.obtain();
|
||||
try {
|
||||
|
@ -49,7 +49,7 @@ public class ListenerSetTest {
|
||||
|
||||
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
|
||||
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
verifyNoMoreInteractions(listener);
|
||||
}
|
||||
@ -67,6 +67,7 @@ public class ListenerSetTest {
|
||||
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
|
||||
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
|
||||
listenerSet.flushEvents();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(listener1, listener2);
|
||||
inOrder.verify(listener1).callback1();
|
||||
@ -75,6 +76,8 @@ public class ListenerSetTest {
|
||||
inOrder.verify(listener2).callback2();
|
||||
inOrder.verify(listener1).callback1();
|
||||
inOrder.verify(listener2).callback1();
|
||||
inOrder.verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1, EVENT_ID_2));
|
||||
inOrder.verify(listener2).iterationFinished(createFlagSet(EVENT_ID_1, EVENT_ID_2));
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
}
|
||||
|
||||
@ -99,6 +102,7 @@ public class ListenerSetTest {
|
||||
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
|
||||
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
|
||||
listenerSet.flushEvents();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(listener1, listener2);
|
||||
inOrder.verify(listener1).callback1();
|
||||
@ -107,6 +111,8 @@ public class ListenerSetTest {
|
||||
inOrder.verify(listener2).callback2();
|
||||
inOrder.verify(listener1).callback3();
|
||||
inOrder.verify(listener2).callback3();
|
||||
inOrder.verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1, EVENT_ID_2, EVENT_ID_3));
|
||||
inOrder.verify(listener2).iterationFinished(createFlagSet(EVENT_ID_1, EVENT_ID_2, EVENT_ID_3));
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
}
|
||||
|
||||
@ -131,7 +137,7 @@ public class ListenerSetTest {
|
||||
// Iteration with single flush.
|
||||
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
|
||||
listenerSet.flushEvents();
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
// Iteration with multiple flushes.
|
||||
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
|
||||
@ -139,11 +145,11 @@ public class ListenerSetTest {
|
||||
listenerSet.flushEvents();
|
||||
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
|
||||
listenerSet.flushEvents();
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
// Iteration with recursive call.
|
||||
listenerSet.sendEvent(EVENT_ID_3, TestListener::callback3);
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(listener1, listener2);
|
||||
inOrder.verify(listener1).callback2();
|
||||
@ -192,7 +198,7 @@ public class ListenerSetTest {
|
||||
listenerSet.add(listener3);
|
||||
|
||||
listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2);
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(listener1, listener2, listener3);
|
||||
inOrder.verify(listener1).callback2();
|
||||
@ -216,7 +222,7 @@ public class ListenerSetTest {
|
||||
|
||||
listenerSet.queueEvent(/* eventFlag= */ C.INDEX_UNSET, TestListener::callback1);
|
||||
listenerSet.flushEvents();
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
// Asserts that negative event flag (INDEX_UNSET) can be used without throwing.
|
||||
}
|
||||
@ -242,7 +248,7 @@ public class ListenerSetTest {
|
||||
// listener2 was added.
|
||||
listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1);
|
||||
listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2);
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(listener1, listener2);
|
||||
inOrder.verify(listener1).callback1();
|
||||
@ -267,7 +273,7 @@ public class ListenerSetTest {
|
||||
listenerSet.add(listener2);
|
||||
listenerSet.queueEvent(EVENT_ID_2, TestListener::callback2);
|
||||
listenerSet.flushEvents();
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
InOrder inOrder = Mockito.inOrder(listener1, listener2);
|
||||
inOrder.verify(listener1).callback1();
|
||||
@ -299,7 +305,7 @@ public class ListenerSetTest {
|
||||
listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1);
|
||||
listenerSet.remove(listener1);
|
||||
listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2);
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
verify(listener1).callback1();
|
||||
verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1));
|
||||
@ -320,7 +326,7 @@ public class ListenerSetTest {
|
||||
listenerSet.remove(listener1);
|
||||
listenerSet.queueEvent(EVENT_ID_1, TestListener::callback1);
|
||||
listenerSet.flushEvents();
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
verify(listener2, times(2)).callback1();
|
||||
verify(listener2).iterationFinished(createFlagSet(EVENT_ID_1));
|
||||
@ -347,13 +353,43 @@ public class ListenerSetTest {
|
||||
// Listener2 shouldn't even get this event as it's released before the event can be invoked.
|
||||
listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1);
|
||||
listenerSet.sendEvent(EVENT_ID_2, TestListener::callback2);
|
||||
ShadowLooper.runMainLooperToNextTask();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
verify(listener1).callback1();
|
||||
verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1));
|
||||
verifyNoMoreInteractions(listener1, listener2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remove_withRecursionDuringRelease_callsAllPendingEventsAndIterationFinished() {
|
||||
ListenerSet<TestListener> listenerSet =
|
||||
new ListenerSet<>(Looper.myLooper(), Clock.DEFAULT, TestListener::iterationFinished);
|
||||
TestListener listener2 = mock(TestListener.class);
|
||||
// Listener1 removes Listener2 from within the callback triggered by release().
|
||||
TestListener listener1 =
|
||||
spy(
|
||||
new TestListener() {
|
||||
@Override
|
||||
public void iterationFinished(FlagSet flags) {
|
||||
listenerSet.remove(listener2);
|
||||
}
|
||||
});
|
||||
listenerSet.add(listener1);
|
||||
listenerSet.add(listener2);
|
||||
|
||||
// Listener2 should still get the event and iterationFinished callback because it was triggered
|
||||
// before the release and the listener removal.
|
||||
listenerSet.sendEvent(EVENT_ID_1, TestListener::callback1);
|
||||
listenerSet.release();
|
||||
ShadowLooper.idleMainLooper();
|
||||
|
||||
verify(listener1).callback1();
|
||||
verify(listener1).iterationFinished(createFlagSet(EVENT_ID_1));
|
||||
verify(listener2).callback1();
|
||||
verify(listener2).iterationFinished(createFlagSet(EVENT_ID_1));
|
||||
verifyNoMoreInteractions(listener1, listener2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void release_preventsRegisteringNewListeners() {
|
||||
ListenerSet<TestListener> listenerSet =
|
||||
|
@ -28,10 +28,16 @@ import static androidx.media3.common.util.Util.parseXsDuration;
|
||||
import static androidx.media3.common.util.Util.unescapeFileName;
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Looper;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
@ -41,6 +47,9 @@ import androidx.media3.common.C;
|
||||
import androidx.media3.test.utils.TestUtil;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.SettableFuture;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
@ -49,16 +58,21 @@ import java.util.Arrays;
|
||||
import java.util.Formatter;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.shadows.ShadowLooper;
|
||||
|
||||
/** Unit tests for {@link Util}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class UtilTest {
|
||||
|
||||
private static final int TIMEOUT_MS = 10000;
|
||||
|
||||
@Test
|
||||
public void addWithOverflowDefault_withoutOverFlow_returnsSum() {
|
||||
long res = Util.addWithOverflowDefault(5, 10, /* overflowResult= */ 0);
|
||||
@ -1238,6 +1252,246 @@ public class UtilTest {
|
||||
.isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postOrRun_withMatchingThread_runsInline() {
|
||||
Runnable mockRunnable = mock(Runnable.class);
|
||||
|
||||
Util.postOrRun(new Handler(Looper.myLooper()), mockRunnable);
|
||||
|
||||
verify(mockRunnable).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postOrRun_fromDifferentThread_posts() throws Exception {
|
||||
Runnable mockRunnable = mock(Runnable.class);
|
||||
HandlerThread handlerThread = new HandlerThread("TestThread");
|
||||
handlerThread.start();
|
||||
Handler handler = new Handler(handlerThread.getLooper());
|
||||
|
||||
ConditionVariable postedCondition = new ConditionVariable();
|
||||
handler.post(
|
||||
() -> {
|
||||
Util.postOrRun(new Handler(Looper.getMainLooper()), mockRunnable);
|
||||
postedCondition.open();
|
||||
});
|
||||
postedCondition.block(TIMEOUT_MS);
|
||||
handlerThread.quit();
|
||||
|
||||
verify(mockRunnable, never()).run();
|
||||
|
||||
ShadowLooper.idleMainLooper();
|
||||
verify(mockRunnable).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postOrRunWithCompletion_withMatchingThread_runsInline() throws Exception {
|
||||
Runnable mockRunnable = mock(Runnable.class);
|
||||
Object expectedResult = new Object();
|
||||
|
||||
ListenableFuture<Object> future =
|
||||
Util.postOrRunWithCompletion(new Handler(Looper.myLooper()), mockRunnable, expectedResult);
|
||||
|
||||
assertThat(future.isDone()).isTrue();
|
||||
assertThat(future.get()).isEqualTo(expectedResult);
|
||||
verify(mockRunnable).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postOrRunWithCompletion_withException_hasException() throws Exception {
|
||||
Object expectedResult = new Object();
|
||||
|
||||
ListenableFuture<Object> future =
|
||||
Util.postOrRunWithCompletion(
|
||||
new Handler(Looper.myLooper()),
|
||||
() -> {
|
||||
throw new IllegalStateException();
|
||||
},
|
||||
expectedResult);
|
||||
|
||||
assertThat(future.isDone()).isTrue();
|
||||
ExecutionException executionException = assertThrows(ExecutionException.class, future::get);
|
||||
assertThat(executionException).hasCauseThat().isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postOrRunWithCompletion_fromDifferentThread_posts() throws Exception {
|
||||
Runnable mockRunnable = mock(Runnable.class);
|
||||
Object expectedResult = new Object();
|
||||
HandlerThread handlerThread = new HandlerThread("TestThread");
|
||||
handlerThread.start();
|
||||
Handler handler = new Handler(handlerThread.getLooper());
|
||||
|
||||
ConditionVariable postedCondition = new ConditionVariable();
|
||||
AtomicReference<ListenableFuture<Object>> futureReference = new AtomicReference<>();
|
||||
handler.post(
|
||||
() -> {
|
||||
futureReference.set(
|
||||
Util.postOrRunWithCompletion(
|
||||
new Handler(Looper.getMainLooper()), mockRunnable, expectedResult));
|
||||
postedCondition.open();
|
||||
});
|
||||
postedCondition.block(TIMEOUT_MS);
|
||||
handlerThread.quit();
|
||||
|
||||
ListenableFuture<Object> future = futureReference.get();
|
||||
verify(mockRunnable, never()).run();
|
||||
assertThat(future.isDone()).isFalse();
|
||||
|
||||
ShadowLooper.idleMainLooper();
|
||||
assertThat(future.isDone()).isTrue();
|
||||
assertThat(future.get()).isEqualTo(expectedResult);
|
||||
verify(mockRunnable).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void postOrRunWithCompletion_withCancel_isNeverRun() throws Exception {
|
||||
Runnable mockRunnable = mock(Runnable.class);
|
||||
Object expectedResult = new Object();
|
||||
HandlerThread handlerThread = new HandlerThread("TestThread");
|
||||
handlerThread.start();
|
||||
Handler handler = new Handler(handlerThread.getLooper());
|
||||
|
||||
ConditionVariable postedCondition = new ConditionVariable();
|
||||
AtomicReference<ListenableFuture<Object>> futureReference = new AtomicReference<>();
|
||||
handler.post(
|
||||
() -> {
|
||||
futureReference.set(
|
||||
Util.postOrRunWithCompletion(
|
||||
new Handler(Looper.getMainLooper()), mockRunnable, expectedResult));
|
||||
postedCondition.open();
|
||||
});
|
||||
postedCondition.block(TIMEOUT_MS);
|
||||
handlerThread.quit();
|
||||
ListenableFuture<Object> future = futureReference.get();
|
||||
future.cancel(/* mayInterruptIfRunning= */ false);
|
||||
|
||||
ShadowLooper.idleMainLooper();
|
||||
verify(mockRunnable, never()).run();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformFutureAsync_withCancelledInput_isCancelled() {
|
||||
SettableFuture<Object> inputFuture = SettableFuture.create();
|
||||
inputFuture.cancel(/* mayInterruptIfRunning= */ false);
|
||||
|
||||
ListenableFuture<Object> outputFuture =
|
||||
Util.transformFutureAsync(inputFuture, input -> Futures.immediateFuture(new Object()));
|
||||
|
||||
assertThat(outputFuture.isCancelled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformFutureAsync_withExceptionInput_hasException() {
|
||||
SettableFuture<Object> inputFuture = SettableFuture.create();
|
||||
Exception expectedException = new Exception();
|
||||
inputFuture.setException(expectedException);
|
||||
|
||||
ListenableFuture<Object> outputFuture =
|
||||
Util.transformFutureAsync(inputFuture, input -> Futures.immediateFuture(new Object()));
|
||||
|
||||
assertThat(outputFuture.isDone()).isTrue();
|
||||
ExecutionException executionException =
|
||||
assertThrows(ExecutionException.class, outputFuture::get);
|
||||
assertThat(executionException).hasCauseThat().isEqualTo(expectedException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformFutureAsync_withCancelledTransform_isCancelled() {
|
||||
SettableFuture<Object> inputFuture = SettableFuture.create();
|
||||
SettableFuture<Object> transformFuture = SettableFuture.create();
|
||||
|
||||
ListenableFuture<Object> outputFuture =
|
||||
Util.transformFutureAsync(inputFuture, input -> transformFuture);
|
||||
assertThat(outputFuture.isDone()).isFalse();
|
||||
inputFuture.set(new Object());
|
||||
assertThat(outputFuture.isDone()).isFalse();
|
||||
transformFuture.cancel(/* mayInterruptIfRunning= */ false);
|
||||
|
||||
assertThat(outputFuture.isCancelled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformFutureAsync_withExceptionInTransformFunction_hasException() {
|
||||
SettableFuture<Object> inputFuture = SettableFuture.create();
|
||||
Exception expectedException = new Exception();
|
||||
|
||||
ListenableFuture<Object> outputFuture =
|
||||
Util.transformFutureAsync(
|
||||
inputFuture,
|
||||
input -> {
|
||||
throw expectedException;
|
||||
});
|
||||
assertThat(outputFuture.isDone()).isFalse();
|
||||
inputFuture.set(new Object());
|
||||
|
||||
assertThat(outputFuture.isDone()).isTrue();
|
||||
ExecutionException executionException =
|
||||
assertThrows(ExecutionException.class, outputFuture::get);
|
||||
assertThat(executionException).hasCauseThat().isEqualTo(expectedException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformFutureAsync_withExceptionDuringTransform_hasException() {
|
||||
SettableFuture<Object> inputFuture = SettableFuture.create();
|
||||
Exception expectedException = new Exception();
|
||||
|
||||
ListenableFuture<Object> outputFuture =
|
||||
Util.transformFutureAsync(
|
||||
inputFuture, input -> Futures.immediateFailedFuture(expectedException));
|
||||
assertThat(outputFuture.isDone()).isFalse();
|
||||
inputFuture.set(new Object());
|
||||
|
||||
assertThat(outputFuture.isDone()).isTrue();
|
||||
ExecutionException executionException =
|
||||
assertThrows(ExecutionException.class, outputFuture::get);
|
||||
assertThat(executionException).hasCauseThat().isEqualTo(expectedException);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformFutureAsync_cancelDuringInput_inputIsCancelled() {
|
||||
SettableFuture<Object> inputFuture = SettableFuture.create();
|
||||
|
||||
ListenableFuture<Object> outputFuture =
|
||||
Util.transformFutureAsync(inputFuture, input -> Futures.immediateFuture(new Object()));
|
||||
assertThat(outputFuture.isDone()).isFalse();
|
||||
outputFuture.cancel(/* mayInterruptIfRunning= */ true);
|
||||
|
||||
assertThat(inputFuture.isCancelled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformFutureAsync_cancelDuringTransform_transformIsCancelled() {
|
||||
SettableFuture<Object> inputFuture = SettableFuture.create();
|
||||
SettableFuture<Object> transformFuture = SettableFuture.create();
|
||||
|
||||
ListenableFuture<Object> outputFuture =
|
||||
Util.transformFutureAsync(inputFuture, input -> transformFuture);
|
||||
assertThat(outputFuture.isDone()).isFalse();
|
||||
inputFuture.set(new Object());
|
||||
assertThat(outputFuture.isDone()).isFalse();
|
||||
outputFuture.cancel(/* mayInterruptIfRunning= */ true);
|
||||
|
||||
assertThat(transformFuture.isCancelled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transformFutureAsync_withSuccessfulTransform_returnsTransformedResult()
|
||||
throws Exception {
|
||||
SettableFuture<Object> inputFuture = SettableFuture.create();
|
||||
SettableFuture<Object> transformFuture = SettableFuture.create();
|
||||
Object expectedOutput = new Object();
|
||||
|
||||
ListenableFuture<Object> outputFuture =
|
||||
Util.transformFutureAsync(inputFuture, input -> transformFuture);
|
||||
assertThat(outputFuture.isDone()).isFalse();
|
||||
inputFuture.set(new Object());
|
||||
assertThat(outputFuture.isDone()).isFalse();
|
||||
transformFuture.set(expectedOutput);
|
||||
|
||||
assertThat(outputFuture.isDone()).isTrue();
|
||||
assertThat(outputFuture.get()).isEqualTo(expectedOutput);
|
||||
}
|
||||
|
||||
private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) {
|
||||
assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName);
|
||||
assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName);
|
||||
|
@ -39,6 +39,7 @@ dependencies {
|
||||
compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||
androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion
|
||||
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
|
@ -22,6 +22,7 @@
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
android:allowBackup="false"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode">
|
||||
|
@ -24,6 +24,7 @@ import androidx.media3.common.C;
|
||||
import androidx.media3.common.MediaLibraryInfo;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
@ -89,6 +90,7 @@ public final class DataSpec {
|
||||
* @param uriString The {@link DataSpec#uri}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setUri(String uriString) {
|
||||
this.uri = Uri.parse(uriString);
|
||||
return this;
|
||||
@ -100,6 +102,7 @@ public final class DataSpec {
|
||||
* @param uri The {@link DataSpec#uri}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setUri(Uri uri) {
|
||||
this.uri = uri;
|
||||
return this;
|
||||
@ -111,6 +114,7 @@ public final class DataSpec {
|
||||
* @param uriPositionOffset The {@link DataSpec#uriPositionOffset}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setUriPositionOffset(long uriPositionOffset) {
|
||||
this.uriPositionOffset = uriPositionOffset;
|
||||
return this;
|
||||
@ -122,6 +126,7 @@ public final class DataSpec {
|
||||
* @param httpMethod The {@link DataSpec#httpMethod}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setHttpMethod(@HttpMethod int httpMethod) {
|
||||
this.httpMethod = httpMethod;
|
||||
return this;
|
||||
@ -133,6 +138,7 @@ public final class DataSpec {
|
||||
* @param httpBody The {@link DataSpec#httpBody}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setHttpBody(@Nullable byte[] httpBody) {
|
||||
this.httpBody = httpBody;
|
||||
return this;
|
||||
@ -148,6 +154,7 @@ public final class DataSpec {
|
||||
* @param httpRequestHeaders The {@link DataSpec#httpRequestHeaders}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setHttpRequestHeaders(Map<String, String> httpRequestHeaders) {
|
||||
this.httpRequestHeaders = httpRequestHeaders;
|
||||
return this;
|
||||
@ -159,6 +166,7 @@ public final class DataSpec {
|
||||
* @param position The {@link DataSpec#position}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setPosition(long position) {
|
||||
this.position = position;
|
||||
return this;
|
||||
@ -170,6 +178,7 @@ public final class DataSpec {
|
||||
* @param length The {@link DataSpec#length}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setLength(long length) {
|
||||
this.length = length;
|
||||
return this;
|
||||
@ -181,6 +190,7 @@ public final class DataSpec {
|
||||
* @param key The {@link DataSpec#key}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setKey(@Nullable String key) {
|
||||
this.key = key;
|
||||
return this;
|
||||
@ -192,6 +202,7 @@ public final class DataSpec {
|
||||
* @param flags The {@link DataSpec#flags}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setFlags(@Flags int flags) {
|
||||
this.flags = flags;
|
||||
return this;
|
||||
@ -203,6 +214,7 @@ public final class DataSpec {
|
||||
* @param customData The {@link DataSpec#customData}.
|
||||
* @return The builder.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Builder setCustomData(@Nullable Object customData) {
|
||||
this.customData = customData;
|
||||
return this;
|
||||
|
@ -23,6 +23,7 @@ import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.Log;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@ -33,25 +34,27 @@ import java.util.Map;
|
||||
* A {@link DataSource} that supports multiple URI schemes. The supported schemes are:
|
||||
*
|
||||
* <ul>
|
||||
* <li>file: For fetching data from a local file (e.g. file:///path/to/media/media.mp4, or just
|
||||
* /path/to/media/media.mp4 because the implementation assumes that a URI without a scheme is
|
||||
* a local file URI).
|
||||
* <li>asset: For fetching data from an asset in the application's apk (e.g. asset:///media.mp4).
|
||||
* <li>rawresource: For fetching data from a raw resource in the application's apk (e.g.
|
||||
* rawresource:///resourceId, where rawResourceId is the integer identifier of the raw
|
||||
* resource).
|
||||
* <li>android.resource: For fetching data in the application's apk (e.g.
|
||||
* android.resource:///resourceId or android.resource://resourceType/resourceName). See {@link
|
||||
* RawResourceDataSource} for more information about the URI form.
|
||||
* <li>content: For fetching data from a content URI (e.g. content://authority/path/123).
|
||||
* <li>rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an
|
||||
* explicit dependency on ExoPlayer's RTMP extension.
|
||||
* <li>data: For parsing data inlined in the URI as defined in RFC 2397.
|
||||
* <li>udp: For fetching data over UDP (e.g. udp://something.com/media).
|
||||
* <li>http(s): For fetching data over HTTP and HTTPS (e.g. https://www.something.com/media.mp4),
|
||||
* if constructed using {@link #DefaultDataSource(Context, String, boolean)}, or any other
|
||||
* schemes supported by a base data source if constructed using {@link
|
||||
* #DefaultDataSource(Context, DataSource)}.
|
||||
* <li>{@code file}: For fetching data from a local file (e.g. {@code
|
||||
* file:///path/to/media/media.mp4}, or just {@code /path/to/media/media.mp4} because the
|
||||
* implementation assumes that a URI without a scheme is a local file URI).
|
||||
* <li>{@code asset}: For fetching data from an asset in the application's APK (e.g. {@code
|
||||
* asset:///media.mp4}).
|
||||
* <li>{@code rawresource}: For fetching data from a raw resource in the application's APK (e.g.
|
||||
* {@code rawresource:///resourceId}, where {@code rawResourceId} is the integer identifier of
|
||||
* the raw resource).
|
||||
* <li>{@code android.resource}: For fetching data in the application's APK (e.g. {@code
|
||||
* android.resource:///resourceId} or {@code android.resource://resourceType/resourceName}).
|
||||
* See {@link RawResourceDataSource} for more information about the URI form.
|
||||
* <li>{@code content}: For fetching data from a content URI (e.g. {@code
|
||||
* content://authority/path/123}).
|
||||
* <li>{@code rtmp}: For fetching data over RTMP. Only supported if the project using ExoPlayer
|
||||
* has an explicit dependency on ExoPlayer's RTMP extension.
|
||||
* <li>{@code data}: For parsing data inlined in the URI as defined in RFC 2397.
|
||||
* <li>{@code udp}: For fetching data over UDP (e.g. {@code udp://something.com/media}).
|
||||
* <li>{@code http(s)}: For fetching data over HTTP and HTTPS (e.g. {@code
|
||||
* https://www.something.com/media.mp4}), if constructed using {@link
|
||||
* #DefaultDataSource(Context, String, boolean)}, or any other schemes supported by a base
|
||||
* data source if constructed using {@link #DefaultDataSource(Context, DataSource)}.
|
||||
* </ul>
|
||||
*/
|
||||
public final class DefaultDataSource implements DataSource {
|
||||
@ -97,6 +100,7 @@ public final class DefaultDataSource implements DataSource {
|
||||
* @param transferListener The listener that will be used.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setTransferListener(@Nullable TransferListener transferListener) {
|
||||
this.transferListener = transferListener;
|
||||
|
@ -34,6 +34,7 @@ import com.google.common.collect.ForwardingMap;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InterruptedIOException;
|
||||
@ -82,6 +83,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
||||
readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Override
|
||||
public final Factory setDefaultRequestProperties(Map<String, String> defaultRequestProperties) {
|
||||
@ -99,6 +101,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
||||
* agent of the underlying platform.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setUserAgent(@Nullable String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
@ -113,6 +116,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
||||
* @param connectTimeoutMs The connect timeout, in milliseconds, that will be used.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setConnectTimeoutMs(int connectTimeoutMs) {
|
||||
this.connectTimeoutMs = connectTimeoutMs;
|
||||
@ -127,6 +131,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
||||
* @param readTimeoutMs The connect timeout, in milliseconds, that will be used.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setReadTimeoutMs(int readTimeoutMs) {
|
||||
this.readTimeoutMs = readTimeoutMs;
|
||||
@ -141,6 +146,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
||||
* @param allowCrossProtocolRedirects Whether to allow cross protocol redirects.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setAllowCrossProtocolRedirects(boolean allowCrossProtocolRedirects) {
|
||||
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
|
||||
@ -158,6 +164,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
||||
* predicate that was previously set.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
@ -174,6 +181,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
||||
* @param transferListener The listener that will be used.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setTransferListener(@Nullable TransferListener transferListener) {
|
||||
this.transferListener = transferListener;
|
||||
@ -184,6 +192,7 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
||||
* Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a
|
||||
* POST request.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) {
|
||||
this.keepPostFor302Redirects = keepPostFor302Redirects;
|
||||
|
@ -30,6 +30,7 @@ import androidx.media3.common.PlaybackException;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
@ -82,6 +83,7 @@ public final class FileDataSource extends BaseDataSource {
|
||||
* @param listener The {@link TransferListener}.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setListener(@Nullable TransferListener listener) {
|
||||
this.listener = listener;
|
||||
return this;
|
||||
|
@ -25,6 +25,7 @@ import androidx.media3.common.util.UnstableApi;
|
||||
import androidx.media3.common.util.Util;
|
||||
import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.lang.annotation.Documented;
|
||||
@ -157,6 +158,7 @@ public interface HttpDataSource extends DataSource {
|
||||
return createDataSourceInternal(defaultRequestProperties);
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@Override
|
||||
public final Factory setDefaultRequestProperties(Map<String, String> defaultRequestProperties) {
|
||||
this.defaultRequestProperties.clearAndSet(defaultRequestProperties);
|
||||
|
@ -28,6 +28,7 @@ import androidx.media3.common.util.Util;
|
||||
import androidx.media3.datasource.DataSink;
|
||||
import androidx.media3.datasource.DataSpec;
|
||||
import androidx.media3.datasource.cache.Cache.CacheException;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
@ -65,6 +66,7 @@ public final class CacheDataSink implements DataSink {
|
||||
* @param cache The cache to which data will be written.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setCache(Cache cache) {
|
||||
this.cache = cache;
|
||||
return this;
|
||||
@ -83,6 +85,7 @@ public final class CacheDataSink implements DataSink {
|
||||
* fragmentation.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setFragmentSize(long fragmentSize) {
|
||||
this.fragmentSize = fragmentSize;
|
||||
return this;
|
||||
@ -97,6 +100,7 @@ public final class CacheDataSink implements DataSink {
|
||||
* @param bufferSize The buffer size in bytes.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setBufferSize(int bufferSize) {
|
||||
this.bufferSize = bufferSize;
|
||||
return this;
|
||||
|
@ -42,6 +42,7 @@ import androidx.media3.datasource.PriorityDataSource;
|
||||
import androidx.media3.datasource.TeeDataSource;
|
||||
import androidx.media3.datasource.TransferListener;
|
||||
import androidx.media3.datasource.cache.Cache.CacheException;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.lang.annotation.Documented;
|
||||
@ -88,6 +89,7 @@ public final class CacheDataSource implements DataSource {
|
||||
* @param cache The cache that will be used.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setCache(Cache cache) {
|
||||
this.cache = cache;
|
||||
return this;
|
||||
@ -111,6 +113,7 @@ public final class CacheDataSource implements DataSource {
|
||||
* @param cacheReadDataSourceFactory The {@link DataSource.Factory} for reading from the cache.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setCacheReadDataSourceFactory(DataSource.Factory cacheReadDataSourceFactory) {
|
||||
this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;
|
||||
return this;
|
||||
@ -126,6 +129,7 @@ public final class CacheDataSource implements DataSource {
|
||||
* DataSinks} for writing data to the cache, or {@code null} to disable writing.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setCacheWriteDataSinkFactory(
|
||||
@Nullable DataSink.Factory cacheWriteDataSinkFactory) {
|
||||
this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory;
|
||||
@ -141,6 +145,7 @@ public final class CacheDataSource implements DataSource {
|
||||
* @param cacheKeyFactory The {@link CacheKeyFactory}.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setCacheKeyFactory(CacheKeyFactory cacheKeyFactory) {
|
||||
this.cacheKeyFactory = cacheKeyFactory;
|
||||
return this;
|
||||
@ -162,6 +167,7 @@ public final class CacheDataSource implements DataSource {
|
||||
* cache, or {@code null} to cause failure in the case of a cache miss.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setUpstreamDataSourceFactory(
|
||||
@Nullable DataSource.Factory upstreamDataSourceFactory) {
|
||||
this.upstreamDataSourceFactory = upstreamDataSourceFactory;
|
||||
@ -186,6 +192,7 @@ public final class CacheDataSource implements DataSource {
|
||||
* @param upstreamPriorityTaskManager The upstream {@link PriorityTaskManager}.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setUpstreamPriorityTaskManager(
|
||||
@Nullable PriorityTaskManager upstreamPriorityTaskManager) {
|
||||
this.upstreamPriorityTaskManager = upstreamPriorityTaskManager;
|
||||
@ -210,6 +217,7 @@ public final class CacheDataSource implements DataSource {
|
||||
* @param upstreamPriority The priority to use when requesting data from upstream.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setUpstreamPriority(int upstreamPriority) {
|
||||
this.upstreamPriority = upstreamPriority;
|
||||
return this;
|
||||
@ -223,6 +231,7 @@ public final class CacheDataSource implements DataSource {
|
||||
* @param flags The {@link CacheDataSource.Flags}.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setFlags(@CacheDataSource.Flags int flags) {
|
||||
this.flags = flags;
|
||||
return this;
|
||||
@ -236,6 +245,7 @@ public final class CacheDataSource implements DataSource {
|
||||
* @param eventListener The {@link EventListener}.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public Factory setEventListener(@Nullable EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
return this;
|
||||
|
@ -20,6 +20,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.media3.common.C;
|
||||
import androidx.media3.common.util.Assertions;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@ -81,6 +82,7 @@ public class ContentMetadataMutations {
|
||||
* @param value The value to be set.
|
||||
* @return This instance, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public ContentMetadataMutations set(String name, String value) {
|
||||
return checkAndSet(name, value);
|
||||
}
|
||||
@ -92,6 +94,7 @@ public class ContentMetadataMutations {
|
||||
* @param value The value to be set.
|
||||
* @return This instance, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public ContentMetadataMutations set(String name, long value) {
|
||||
return checkAndSet(name, value);
|
||||
}
|
||||
@ -103,6 +106,7 @@ public class ContentMetadataMutations {
|
||||
* @param value The value to be set.
|
||||
* @return This instance, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public ContentMetadataMutations set(String name, byte[] value) {
|
||||
return checkAndSet(name, Arrays.copyOf(value, value.length));
|
||||
}
|
||||
@ -113,6 +117,7 @@ public class ContentMetadataMutations {
|
||||
* @param name The name of the metadata value.
|
||||
* @return This instance, for convenience.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
public ContentMetadataMutations remove(String name) {
|
||||
removedValues.add(name);
|
||||
editedValues.remove(name);
|
||||
@ -137,6 +142,7 @@ public class ContentMetadataMutations {
|
||||
return Collections.unmodifiableMap(hashMap);
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
private ContentMetadataMutations checkAndSet(String name, Object value) {
|
||||
editedValues.put(Assertions.checkNotNull(name), Assertions.checkNotNull(value));
|
||||
removedValues.remove(name);
|
||||
|
@ -24,11 +24,12 @@ dependencies {
|
||||
implementation project(modulePrefix + 'lib-common')
|
||||
implementation project(modulePrefix + 'lib-datasource')
|
||||
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
|
||||
compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion
|
||||
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
|
||||
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
|
||||
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion
|
||||
androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion
|
||||
androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion
|
||||
androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion
|
||||
// Instrumentation tests assume that an app-packaged version of cronet is
|
||||
// available.
|
||||
|
@ -23,6 +23,7 @@
|
||||
<uses-sdk/>
|
||||
|
||||
<application
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
android:allowBackup="false"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="MissingApplicationIcon,HardcodedDebugMode"/>
|
||||
|
@ -42,6 +42,7 @@ import com.google.common.base.Ascii;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import com.google.common.primitives.Longs;
|
||||
import com.google.errorprone.annotations.CanIgnoreReturnValue;
|
||||
import java.io.IOException;
|
||||
import java.io.InterruptedIOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
@ -142,6 +143,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
|
||||
}
|
||||
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Override
|
||||
public final Factory setDefaultRequestProperties(Map<String, String> defaultRequestProperties) {
|
||||
@ -162,6 +164,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* agent of the underlying {@link CronetEngine}.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setUserAgent(@Nullable String userAgent) {
|
||||
this.userAgent = userAgent;
|
||||
@ -181,6 +184,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* UrlRequest.Builder#REQUEST_PRIORITY_*} constants.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setRequestPriority(int requestPriority) {
|
||||
this.requestPriority = requestPriority;
|
||||
@ -195,6 +199,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @param connectTimeoutMs The connect timeout, in milliseconds, that will be used.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setConnectionTimeoutMs(int connectTimeoutMs) {
|
||||
this.connectTimeoutMs = connectTimeoutMs;
|
||||
@ -212,6 +217,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @param resetTimeoutOnRedirects Whether the connect timeout is reset when a redirect occurs.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setResetTimeoutOnRedirects(boolean resetTimeoutOnRedirects) {
|
||||
this.resetTimeoutOnRedirects = resetTimeoutOnRedirects;
|
||||
@ -228,6 +234,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* to the redirect url in the "Cookie" header.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setHandleSetCookieRequests(boolean handleSetCookieRequests) {
|
||||
this.handleSetCookieRequests = handleSetCookieRequests;
|
||||
@ -242,6 +249,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @param readTimeoutMs The connect timeout, in milliseconds, that will be used.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setReadTimeoutMs(int readTimeoutMs) {
|
||||
this.readTimeoutMs = readTimeoutMs;
|
||||
@ -261,6 +269,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* predicate that was previously set.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
|
||||
this.contentTypePredicate = contentTypePredicate;
|
||||
@ -274,6 +283,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* Sets whether we should keep the POST method and body when we have HTTP 302 redirects for a
|
||||
* POST request.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setKeepPostFor302Redirects(boolean keepPostFor302Redirects) {
|
||||
this.keepPostFor302Redirects = keepPostFor302Redirects;
|
||||
@ -293,6 +303,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @param transferListener The listener that will be used.
|
||||
* @return This factory.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
public Factory setTransferListener(@Nullable TransferListener transferListener) {
|
||||
this.transferListener = transferListener;
|
||||
@ -313,6 +324,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* @deprecated Do not use {@link CronetDataSource} or its factory in cases where a suitable
|
||||
* {@link CronetEngine} is not available. Use the fallback factory directly in such cases.
|
||||
*/
|
||||
@CanIgnoreReturnValue
|
||||
@UnstableApi
|
||||
@Deprecated
|
||||
public Factory setFallbackFactory(@Nullable HttpDataSource.Factory fallbackFactory) {
|
||||
|
@ -145,7 +145,7 @@ public final class CronetDataSourceTest {
|
||||
testResponseHeader.put("Content-Type", TEST_CONTENT_TYPE);
|
||||
// This value can be anything since the DataSpec is unset.
|
||||
testResponseHeader.put("Content-Length", Long.toString(TEST_CONTENT_LENGTH));
|
||||
testUrlResponseInfo = createUrlResponseInfo(200); // statusCode
|
||||
testUrlResponseInfo = createUrlResponseInfo(/* statusCode= */ 200);
|
||||
}
|
||||
|
||||
@After
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user