Commit c2cbb637 by Michael Katz Committed by GitHub

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

1.0.0-beta03
parents 50475814 6661b757
Showing with 2118 additions and 300 deletions
......@@ -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
......
......@@ -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.
......
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);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
try {
bitmapTexId =
GlUtil.createTexture(
BITMAP_WIDTH_HEIGHT,
BITMAP_WIDTH_HEIGHT,
/* useHighPrecisionColorComponents= */ false);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0);
glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH);
} catch (GlUtil.GlException | IOException e) {
throw new FrameProcessingException(e);
}
// Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y.
glProgram.setBufferAttribute(
"aFramePosition",
GlUtil.getNormalizedCoordinateBounds(),
GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE);
glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0);
glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1);
glProgram.setFloatUniform("uScaleX", bitmapScaleX);
glProgram.setFloatUniform("uScaleY", bitmapScaleY);
}
@Override
public Size getOutputSize() {
return checkStateNotNull(outputSize);
public Pair<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 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,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);
}
}
}
......@@ -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>
......
<?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>
<?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>
<?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
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content" >
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:padding="8dp"
android:text="@string/input_video" />
<androidx.media3.ui.PlayerView
android:id="@+id/player_view"
<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>
</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"/>
<TextView
android:id="@+id/debug_text_view"
<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>
......@@ -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;
......
......@@ -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.
......
/*
* 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);
}
/*
* 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 androidx.media3.common.util.UnstableApi;
/** Marker interface for a video frame effect. */
@UnstableApi
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);
......
......@@ -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() {
......
/*
* 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,9 +13,8 @@
* 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. */
......@@ -23,6 +22,26 @@ import androidx.media3.common.util.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.
*/
......
/*
* 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();
}
/*
* 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,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();
......
/*
* 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;
}
......
......@@ -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();
......
/*
* 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();
}
/*
* 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 {@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.value[0] = value;
this.floatValue[0] = value;
}
/** Configures {@link #bind()} to use the specified float[] {@code value} for this uniform. */
/** Configures {@link #bind()} to use the specified {@code float[]} {@code value}. */
public void 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;
......
......@@ -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);
}
/**
......
/*
* 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;
......@@ -190,6 +199,54 @@ public final class Util {
}
/**
* 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.
*
......@@ -575,6 +632,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<>();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment