diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index 25383cd8dd..0000000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: Bug report -about: Issue template for a bug report. -title: '' -labels: bug, needs triage -assignees: '' ---- - -We can only process bug reports that are actionable. Unclear bug reports or -reports with insufficient information may not get attention. - -Before filing a bug: -------------------------- - -- Search existing issues, including issues that are closed: - https://github.com/androidx/media/issues?q=is%3Aissue -- For ExoPlayer-related bugs, please also check the ExoPlayer tracker: - https://github.com/google/ExoPlayer/issues?q=is%3Aissue - -When reporting a bug: -------------------------- - -Describe how the issue can be reproduced, ideally using one of the demo apps -or a small sample app that you’re able to share as source code on GitHub. To -increase the chance of your issue getting attention, please also include: - -- Clear reproduction steps including observed and expected behavior -- Output of running "adb bugreport" in the console shortly after encountering - the issue -- URI to test content for reproduction -- For protected content: - - DRM scheme and license server URL - - Authentication HTTP headers - -- AndroidX Media version number -- Android version -- Android device - -If there's something you don't want to post publicly, please submit the issue, -then email the link/bug report to dev.exoplayer@gmail.com using a subject in the -format "Issue #1234", where #1234 is your issue number (we don't reply to -emails). diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..41d4528ced --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,99 @@ +name: Bug Report +description: Report a bug in the Media3 library +labels: ["bug", "needs triage"] +body: + - type: markdown + attributes: + value: | + We can only process bug reports that are actionable. Unclear bug reports or reports with insufficient information may not get attention. + + Before filing a bug: + ------------------------- + + - Search existing issues, including issues that are closed: https://github.com/androidx/media/issues?q=is%3Aissue + - For ExoPlayer-related bugs, please also check the ExoPlayer tracker: https://github.com/google/ExoPlayer/issues?q=is%3Aissue + - type: dropdown + attributes: + label: Media3 Version + description: What version of Media3 are you using? + options: + - 1.0.0-alpha03 + - 1.0.0-alpha02 + - 1.0.0-alpha01 + validations: + required: true + - type: textarea + attributes: + label: Devices that reproduce the issue + placeholder: | + Example: + * Pixel 4 running Android 12 + * Samsung S21 running Android 11 + validations: + required: true + - type: textarea + attributes: + label: Devices that do not reproduce the issue + placeholder: | + Example: + * Pixel 3 running Android Pie + - type: dropdown + attributes: + label: Reproducible in the demo app? + description: Please try and reproduce the issue in the [Media3 demo app](https://github.com/androidx/media/tree/release/demos/main). + options: + - "Yes" + - "No" + - Not tested + validations: + required: true + - type: textarea + attributes: + label: Reproduction steps + description: Clear and complete steps we can use to reproduce the problem + placeholder: | + Example: + 1. Play the attached media in the demo app + 2. Seek forward 10s + validations: + required: true + - type: textarea + attributes: + label: Expected result + placeholder: | + Example: + The media plays successfully + validations: + required: true + - type: textarea + attributes: + label: Actual result + placeholder: | + Example: + Playback crashes with the following stack trace: + ... + validations: + required: true + - type: textarea + attributes: + label: Media + description: | + Media we can use to reproduce the problem. Either: + * Attach a file here + * Include a media URL + * Refer to a piece of media from the demo app (e.g. `Misc > Dizzy (MP4)`) + * If you don't want to post media publicly please email the info to dev.exoplayer@gmail.com with subject 'Issue #\' after filing this issue, and note that you will do this here. + * If you are certain the issue does not depend on the media being played, enter "Not applicable" here. + + For DRM-protected media please also include the scheme and license server URL. + validations: + required: true + - type: checkboxes + attributes: + label: Bug Report + description: | + After filing this issue please run `adb bugreport` shortly after reproducing the problem (ideally in the [demo app](https://github.com/androidx/media/tree/release/demos/main)) to capture a zip file, and email this to dev.exoplayer@gmail.com with subject 'Issue #\'. + + **Note:** Logcat output is **not** the same as a full bug report, and is often missing information that's useful for diagnosing issues. Please ensure you're sending a full bug report zip file. + options: + - label: You will email the zip file produced by `adb bugreport` to dev.exoplayer@gmail.com after filing this issue. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3ba13e0cec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0232b429ab..33f1165450 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,10 +15,21 @@ * Track selection: * Flatten `TrackSelectionOverrides` class into `TrackSelectionParameters`, and promote `TrackSelectionOverride` to a top level class. + * Rename `TracksInfo` to `Tracks` and `TracksInfo.TrackGroupInfo` to + `Tracks.Group`. `Player.getCurrentTracksInfo` and + `Player.Listener.onTracksInfoChanged` have also been renamed to + `Player.getCurrentTracks` and `Player.Listener.onTracksChanged`. +* Video: + * Rename `DummySurface` to `PlaceHolderSurface`. * Audio: * Use LG AC3 audio decoder advertising non-standard MIME type. +* Ad playback / IMA: + * Decrease ad polling rate from every 100ms to every 200ms, to line up with + Media Rating Council (MRC) recommendations. * Extractors: * Matroska: Parse `DiscardPadding` for Opus tracks. + * Parse bitrates from `esds` boxes. + * MP4: Parse initialization data from AV1 tracks. * UI: * Fix delivery of events to `OnClickListener`s set on `PlayerView` and `LegacyPlayerView`, in the case that `useController=false` @@ -35,9 +46,34 @@ views to be used with other `Player` implementations, and removes the dependency from the UI module to the ExoPlayer module. This is a breaking change. + * Don't show forced text tracks in the `PlayerView` track selector, and + keep a suitable forced text track selected if "None" is selected + ([#9432](https://github.com/google/ExoPlayer/issues/9432)). +* HLS: + * Fallback to chunkful preparation if the playlist CODECS attribute + does not contain the audio codec + ([#10065](https://github.com/google/ExoPlayer/issues/10065)). * RTSP: + * Add RTP reader for MPEG4 + ([#35](https://github.com/androidx/media/pull/35)) * Add RTP reader for HEVC ([#36](https://github.com/androidx/media/pull/36)). + * Add RTP reader for AMR. Currently only mono-channel, non-interleaved + AMR streams are supported. Compound AMR RTP payload is not supported. + ([#46](https://github.com/androidx/media/pull/46)) + * Add RTP reader for VP8 + ([#47](https://github.com/androidx/media/pull/47)). + * Add RTP reader for WAV + ([#56](https://github.com/androidx/media/pull/56)). + * Fix RTSP basic authorization header. + ([#9544](https://github.com/google/ExoPlayer/issues/9544)). + * Throw checked exception when parsing RTSP timing + ([#10165](https://github.com/google/ExoPlayer/issues/10165)). +* Session: + * Fix NPE in MediaControllerImplLegacy + ([#59](https://github.com/androidx/media/pull/59)) +* Data sources: + * Rename `DummyDataSource` to `PlaceHolderDataSource`. * Remove deprecated symbols: * Remove `Player.Listener.onTracksChanged`. Use `Player.Listener.onTracksInfoChanged` instead. diff --git a/constants.gradle b/constants.gradle index d016c32571..5545b5f4ab 100644 --- a/constants.gradle +++ b/constants.gradle @@ -19,7 +19,7 @@ project.ext { // Upgrading this requires [Internal ref: b/193254928] to be fixed, or some // additional robolectric config. targetSdkVersion = 30 - compileSdkVersion = 31 + compileSdkVersion = 32 dexmakerVersion = '2.28.1' junitVersion = '4.13.2' // Use the same Guava version as the Android repo: diff --git a/core_settings.gradle b/core_settings.gradle index d8760a1709..baca421753 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -87,3 +87,7 @@ include modulePrefix + 'test-data' project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data') include modulePrefix + 'test-utils' project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils') +include modulePrefix + 'test-session-common' +project(modulePrefix + 'test-session-common').projectDir = new File(rootDir, 'libraries/test_session_common') +include modulePrefix + 'test-session-current' +project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'libraries/test_session_current') diff --git a/demos/cast/src/main/java/androidx/media3/demo/cast/PlayerManager.java b/demos/cast/src/main/java/androidx/media3/demo/cast/PlayerManager.java index 8f4d122f92..db29871600 100644 --- a/demos/cast/src/main/java/androidx/media3/demo/cast/PlayerManager.java +++ b/demos/cast/src/main/java/androidx/media3/demo/cast/PlayerManager.java @@ -26,7 +26,7 @@ import androidx.media3.common.Player; import androidx.media3.common.Player.DiscontinuityReason; import androidx.media3.common.Player.TimelineChangeReason; import androidx.media3.common.Timeline; -import androidx.media3.common.TracksInfo; +import androidx.media3.common.Tracks; import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.ui.PlayerControlView; import androidx.media3.ui.PlayerView; @@ -57,7 +57,7 @@ import java.util.ArrayList; private final ArrayList mediaQueue; private final Listener listener; - private TracksInfo lastSeenTrackGroupInfo; + private Tracks lastSeenTracks; private int currentItemIndex; private Player currentPlayer; @@ -219,19 +219,19 @@ import java.util.ArrayList; } @Override - public void onTracksInfoChanged(TracksInfo tracksInfo) { - if (currentPlayer != localPlayer || tracksInfo == lastSeenTrackGroupInfo) { + public void onTracksChanged(Tracks tracks) { + if (currentPlayer != localPlayer || tracks == lastSeenTracks) { return; } - if (tracksInfo.containsType(C.TRACK_TYPE_VIDEO) - && !tracksInfo.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) { + if (tracks.containsType(C.TRACK_TYPE_VIDEO) + && !tracks.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) { listener.onUnsupportedTrack(C.TRACK_TYPE_VIDEO); } - if (tracksInfo.containsType(C.TRACK_TYPE_AUDIO) - && !tracksInfo.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) { + if (tracks.containsType(C.TRACK_TYPE_AUDIO) + && !tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) { listener.onUnsupportedTrack(C.TRACK_TYPE_AUDIO); } - lastSeenTrackGroupInfo = tracksInfo; + lastSeenTracks = tracks; } // CastPlayer.SessionAvailabilityListener implementation. diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java b/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java index 21078d8545..c14bfc4ba9 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java @@ -20,6 +20,7 @@ import static androidx.media3.demo.main.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_I import android.app.Notification; import android.content.Context; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.media3.common.util.NotificationUtil; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.offline.Download; @@ -32,6 +33,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler; import java.util.List; /** A service for downloading media. */ +@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public class DemoDownloadService extends DownloadService { private static final int JOB_ID = 1; diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java b/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java index 2c31ae19ee..2a5a4bfbbe 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java @@ -16,6 +16,7 @@ package androidx.media3.demo.main; import android.content.Context; +import androidx.annotation.OptIn; import androidx.media3.database.DatabaseProvider; import androidx.media3.database.StandaloneDatabaseProvider; import androidx.media3.datasource.DataSource; @@ -71,6 +72,7 @@ public final class DemoUtil { return BuildConfig.USE_DECODER_EXTENSIONS; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public static RenderersFactory buildRenderersFactory( Context context, boolean preferExtensionRenderer) { @DefaultRenderersFactory.ExtensionRendererMode @@ -116,6 +118,7 @@ public final class DemoUtil { return dataSourceFactory; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public static synchronized DownloadNotificationHelper getDownloadNotificationHelper( Context context) { if (downloadNotificationHelper == null) { @@ -135,6 +138,7 @@ public final class DemoUtil { return downloadTracker; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static synchronized Cache getDownloadCache(Context context) { if (downloadCache == null) { File downloadContentDirectory = @@ -146,6 +150,7 @@ public final class DemoUtil { return downloadCache; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static synchronized void ensureDownloadManagerInitialized(Context context) { if (downloadManager == null) { downloadManager = @@ -160,6 +165,7 @@ public final class DemoUtil { } } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static synchronized DatabaseProvider getDatabaseProvider(Context context) { if (databaseProvider == null) { databaseProvider = new StandaloneDatabaseProvider(context); @@ -177,6 +183,7 @@ public final class DemoUtil { return downloadDirectory; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static CacheDataSource.Factory buildReadOnlyCacheDataSource( DataSource.Factory upstreamFactory, Cache cache) { return new CacheDataSource.Factory() diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java b/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java index 92349a575e..089644cbc9 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java @@ -23,6 +23,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.annotation.RequiresApi; import androidx.fragment.app.FragmentManager; import androidx.media3.common.DrmInitData; @@ -30,7 +31,7 @@ import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackSelectionParameters; -import androidx.media3.common.TracksInfo; +import androidx.media3.common.Tracks; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; @@ -53,6 +54,7 @@ import java.util.HashMap; import java.util.concurrent.CopyOnWriteArraySet; /** Tracks media that has been downloaded. */ +@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public class DownloadTracker { /** Listens for changes in the tracked downloads. */ @@ -301,17 +303,17 @@ public class DownloadTracker { return; } - TracksInfo tracksInfo = downloadHelper.getTracksInfo(/* periodIndex= */ 0); - if (!TrackSelectionDialog.willHaveContent(tracksInfo)) { + Tracks tracks = downloadHelper.getTracks(/* periodIndex= */ 0); + if (!TrackSelectionDialog.willHaveContent(tracks)) { Log.d(TAG, "No dialog content. Downloading entire stream."); startDownload(); downloadHelper.release(); return; } trackSelectionDialog = - TrackSelectionDialog.createForTracksInfoAndParameters( + TrackSelectionDialog.createForTracksAndParameters( /* titleId= */ R.string.exo_download_description, - tracksInfo, + tracks, DownloadHelper.getDefaultTrackSelectorParameters(context), /* allowAdaptiveSelections= */ false, /* allowMultipleOverrides= */ true, diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index d158059a9f..df7eb8c40b 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -17,6 +17,7 @@ package androidx.media3.demo.main; import android.content.Intent; import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; import android.util.Pair; import android.view.KeyEvent; @@ -27,6 +28,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; @@ -35,7 +37,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.PlaybackException; import androidx.media3.common.Player; import androidx.media3.common.TrackSelectionParameters; -import androidx.media3.common.TracksInfo; +import androidx.media3.common.Tracks; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.exoplayer.ExoPlayer; @@ -81,7 +83,7 @@ public class PlayerActivity extends AppCompatActivity private List mediaItems; private TrackSelectionParameters trackSelectionParameters; private DebugTextViewHelper debugViewHelper; - private TracksInfo lastSeenTracksInfo; + private Tracks lastSeenTracks; private boolean startAutoPlay; private int startItemIndex; private long startPosition; @@ -142,7 +144,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onStart() { super.onStart(); - if (Util.SDK_INT > 23) { + if (Build.VERSION.SDK_INT > 23) { initializePlayer(); if (playerView != null) { playerView.onResume(); @@ -153,7 +155,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onResume() { super.onResume(); - if (Util.SDK_INT <= 23 || player == null) { + if (Build.VERSION.SDK_INT <= 23 || player == null) { initializePlayer(); if (playerView != null) { playerView.onResume(); @@ -164,7 +166,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onPause() { super.onPause(); - if (Util.SDK_INT <= 23) { + if (Build.VERSION.SDK_INT <= 23) { if (playerView != null) { playerView.onPause(); } @@ -175,7 +177,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onStop() { super.onStop(); - if (Util.SDK_INT > 23) { + if (Build.VERSION.SDK_INT > 23) { if (playerView != null) { playerView.onPause(); } @@ -274,7 +276,7 @@ public class PlayerActivity extends AppCompatActivity RenderersFactory renderersFactory = DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders); - lastSeenTracksInfo = TracksInfo.EMPTY; + lastSeenTracks = Tracks.EMPTY; player = new ExoPlayer.Builder(/* context= */ this) .setRenderersFactory(renderersFactory) @@ -342,7 +344,7 @@ public class PlayerActivity extends AppCompatActivity MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration; if (drmConfiguration != null) { - if (Util.SDK_INT < 18) { + if (Build.VERSION.SDK_INT < 18) { showToast(R.string.error_drm_unsupported_before_api_18); finish(); return Collections.emptyList(); @@ -454,22 +456,20 @@ public class PlayerActivity extends AppCompatActivity @Override @SuppressWarnings("ReferenceEquality") - public void onTracksInfoChanged(TracksInfo tracksInfo) { + public void onTracksChanged(Tracks tracks) { updateButtonVisibility(); - if (tracksInfo == lastSeenTracksInfo) { + if (tracks == lastSeenTracks) { return; } - if (tracksInfo.containsType(C.TRACK_TYPE_VIDEO) - && !tracksInfo.isTypeSupported( - C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) { + if (tracks.containsType(C.TRACK_TYPE_VIDEO) + && !tracks.isTypeSupported(C.TRACK_TYPE_VIDEO, /* allowExceedsCapabilities= */ true)) { showToast(R.string.error_unsupported_video); } - if (tracksInfo.containsType(C.TRACK_TYPE_AUDIO) - && !tracksInfo.isTypeSupported( - C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) { + if (tracks.containsType(C.TRACK_TYPE_AUDIO) + && !tracks.isTypeSupported(C.TRACK_TYPE_AUDIO, /* allowExceedsCapabilities= */ true)) { showToast(R.string.error_unsupported_audio); } - lastSeenTracksInfo = tracksInfo; + lastSeenTracks = tracks; } } @@ -508,29 +508,32 @@ public class PlayerActivity extends AppCompatActivity private static List createMediaItems(Intent intent, DownloadTracker downloadTracker) { List mediaItems = new ArrayList<>(); for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) { - @Nullable - DownloadRequest downloadRequest = - downloadTracker.getDownloadRequest(item.localConfiguration.uri); - if (downloadRequest != null) { - MediaItem.Builder builder = item.buildUpon(); - builder - .setMediaId(downloadRequest.id) - .setUri(downloadRequest.uri) - .setCustomCacheKey(downloadRequest.customCacheKey) - .setMimeType(downloadRequest.mimeType) - .setStreamKeys(downloadRequest.streamKeys); - @Nullable - MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration; - if (drmConfiguration != null) { - builder.setDrmConfiguration( - drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build()); - } - - mediaItems.add(builder.build()); - } else { - mediaItems.add(item); - } + mediaItems.add( + maybeSetDownloadProperties( + item, downloadTracker.getDownloadRequest(item.localConfiguration.uri))); } return mediaItems; } + + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) + private static MediaItem maybeSetDownloadProperties( + MediaItem item, @Nullable DownloadRequest downloadRequest) { + if (downloadRequest == null) { + return item; + } + MediaItem.Builder builder = item.buildUpon(); + builder + .setMediaId(downloadRequest.id) + .setUri(downloadRequest.uri) + .setCustomCacheKey(downloadRequest.customCacheKey) + .setMimeType(downloadRequest.mimeType) + .setStreamKeys(downloadRequest.streamKeys); + @Nullable + MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration; + if (drmConfiguration != null) { + builder.setDrmConfiguration( + drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build()); + } + return builder.build(); + } } diff --git a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java index 6b765679ad..fa61a3ba44 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java @@ -41,6 +41,7 @@ import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem.ClippingConfiguration; @@ -53,6 +54,7 @@ import androidx.media3.datasource.DataSourceUtil; import androidx.media3.datasource.DataSpec; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.offline.DownloadService; +import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; @@ -120,6 +122,7 @@ public class SampleChooserActivity extends AppCompatActivity } /** Start the download service if it should be running but it's not currently. */ + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private void startDownloadService() { // Starting the service in the foreground causes notification flicker if there is no scheduled // action. Starting it in the background throws an exception if the app is in the background too @@ -274,6 +277,7 @@ public class SampleChooserActivity extends AppCompatActivity private boolean sawError; + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) @Override protected List doInBackground(String... uris) { List result = new ArrayList<>(); @@ -484,7 +488,7 @@ public class SampleChooserActivity extends AppCompatActivity private PlaylistGroup getGroup(String groupName, List groups) { for (int i = 0; i < groups.size(); i++) { - if (Util.areEqual(groupName, groups.get(i).title)) { + if (Objects.equal(groupName, groups.get(i).title)) { return groups.get(i); } } diff --git a/demos/main/src/main/java/androidx/media3/demo/main/TrackSelectionDialog.java b/demos/main/src/main/java/androidx/media3/demo/main/TrackSelectionDialog.java index 8dbb003104..0c741ec057 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/TrackSelectionDialog.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/TrackSelectionDialog.java @@ -35,8 +35,7 @@ import androidx.media3.common.Player; import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TrackSelectionParameters; -import androidx.media3.common.TracksInfo; -import androidx.media3.common.TracksInfo.TrackGroupInfo; +import androidx.media3.common.Tracks; import androidx.media3.ui.TrackSelectionView; import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; @@ -79,16 +78,16 @@ public final class TrackSelectionDialog extends DialogFragment { * specified {@link Player}. */ public static boolean willHaveContent(Player player) { - return willHaveContent(player.getCurrentTracksInfo()); + return willHaveContent(player.getCurrentTracks()); } /** * Returns whether a track selection dialog will have content to display if initialized with the - * specified {@link TracksInfo}. + * specified {@link Tracks}. */ - public static boolean willHaveContent(TracksInfo tracksInfo) { - for (TrackGroupInfo trackGroupInfo : tracksInfo.getTrackGroupInfos()) { - if (SUPPORTED_TRACK_TYPES.contains(trackGroupInfo.getTrackType())) { + public static boolean willHaveContent(Tracks tracks) { + for (Tracks.Group trackGroup : tracks.getGroups()) { + if (SUPPORTED_TRACK_TYPES.contains(trackGroup.getType())) { return true; } } @@ -105,9 +104,9 @@ public final class TrackSelectionDialog extends DialogFragment { */ public static TrackSelectionDialog createForPlayer( Player player, DialogInterface.OnDismissListener onDismissListener) { - return createForTracksInfoAndParameters( + return createForTracksAndParameters( R.string.track_selection_title, - player.getCurrentTracksInfo(), + player.getCurrentTracks(), player.getTrackSelectionParameters(), /* allowAdaptiveSelections= */ true, /* allowMultipleOverrides= */ false, @@ -116,10 +115,10 @@ public final class TrackSelectionDialog extends DialogFragment { } /** - * Creates a dialog for given {@link TracksInfo} and {@link TrackSelectionParameters}. + * Creates a dialog for given {@link Tracks} and {@link TrackSelectionParameters}. * * @param titleId The resource id of the dialog title. - * @param tracksInfo The {@link TracksInfo} describing the tracks to display. + * @param tracks The {@link Tracks} describing the tracks to display. * @param trackSelectionParameters The initial {@link TrackSelectionParameters}. * @param allowAdaptiveSelections Whether adaptive selections (consisting of more than one track) * can be made. @@ -128,9 +127,9 @@ public final class TrackSelectionDialog extends DialogFragment { * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is * dismissed. */ - public static TrackSelectionDialog createForTracksInfoAndParameters( + public static TrackSelectionDialog createForTracksAndParameters( int titleId, - TracksInfo tracksInfo, + Tracks tracks, TrackSelectionParameters trackSelectionParameters, boolean allowAdaptiveSelections, boolean allowMultipleOverrides, @@ -138,7 +137,7 @@ public final class TrackSelectionDialog extends DialogFragment { DialogInterface.OnDismissListener onDismissListener) { TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); trackSelectionDialog.init( - tracksInfo, + tracks, trackSelectionParameters, titleId, allowAdaptiveSelections, @@ -169,7 +168,7 @@ public final class TrackSelectionDialog extends DialogFragment { } private void init( - TracksInfo tracksInfo, + Tracks tracks, TrackSelectionParameters trackSelectionParameters, int titleId, boolean allowAdaptiveSelections, @@ -182,16 +181,16 @@ public final class TrackSelectionDialog extends DialogFragment { for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) { @C.TrackType int trackType = SUPPORTED_TRACK_TYPES.get(i); - ArrayList trackGroupInfos = new ArrayList<>(); - for (TrackGroupInfo trackGroupInfo : tracksInfo.getTrackGroupInfos()) { - if (trackGroupInfo.getTrackType() == trackType) { - trackGroupInfos.add(trackGroupInfo); + ArrayList trackGroups = new ArrayList<>(); + for (Tracks.Group trackGroup : tracks.getGroups()) { + if (trackGroup.getType() == trackType) { + trackGroups.add(trackGroup); } } - if (!trackGroupInfos.isEmpty()) { + if (!trackGroups.isEmpty()) { TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); tabFragment.init( - trackGroupInfos, + trackGroups, trackSelectionParameters.disabledTrackTypes.contains(trackType), trackSelectionParameters.overrides, allowAdaptiveSelections, @@ -300,7 +299,7 @@ public final class TrackSelectionDialog extends DialogFragment { public static final class TrackSelectionViewFragment extends Fragment implements TrackSelectionView.TrackSelectionListener { - private List trackGroupInfos; + private List trackGroups; private boolean allowAdaptiveSelections; private boolean allowMultipleOverrides; @@ -313,12 +312,12 @@ public final class TrackSelectionDialog extends DialogFragment { } public void init( - List trackGroupInfos, + List trackGroups, boolean isDisabled, Map overrides, boolean allowAdaptiveSelections, boolean allowMultipleOverrides) { - this.trackGroupInfos = trackGroupInfos; + this.trackGroups = trackGroups; this.isDisabled = isDisabled; this.allowAdaptiveSelections = allowAdaptiveSelections; this.allowMultipleOverrides = allowMultipleOverrides; @@ -326,8 +325,7 @@ public final class TrackSelectionDialog extends DialogFragment { // handle the case where the TrackSelectionView is never created. this.overrides = new HashMap<>( - TrackSelectionView.filterOverrides( - overrides, trackGroupInfos, allowMultipleOverrides)); + TrackSelectionView.filterOverrides(overrides, trackGroups, allowMultipleOverrides)); } @Override @@ -343,7 +341,7 @@ public final class TrackSelectionDialog extends DialogFragment { trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); trackSelectionView.init( - trackGroupInfos, + trackGroups, isDisabled, overrides, /* trackFormatComparator= */ null, diff --git a/demos/transformer/src/main/assets/fragment_shader_bitmap_overlay_es2.glsl b/demos/transformer/src/main/assets/fragment_shader_bitmap_overlay_es2.glsl new file mode 100644 index 0000000000..90ff827132 --- /dev/null +++ b/demos/transformer/src/main/assets/fragment_shader_bitmap_overlay_es2.glsl @@ -0,0 +1,37 @@ +#version 100 +// 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. + +// ES 2 fragment shader that overlays the bitmap from uTexSampler1 over a video +// frame from uTexSampler0. + +precision mediump float; +// Texture containing an input video frame. +uniform sampler2D uTexSampler0; +// Texture containing the overlap bitmap. +uniform sampler2D uTexSampler1; +// Horizontal scaling factor for the overlap bitmap. +uniform float uScaleX; +// Vertical scaling factory for the overlap bitmap. +uniform float uScaleY; +varying vec2 vTexSamplingCoord; +void main() { + vec4 videoColor = texture2D(uTexSampler0, vTexSamplingCoord); + vec4 overlayColor = texture2D(uTexSampler1, + vec2(vTexSamplingCoord.x * uScaleX, + vTexSamplingCoord.y * uScaleY)); + // Blend the video decoder output and the overlay bitmap. + gl_FragColor = videoColor * (1.0 - overlayColor.a) + + overlayColor * overlayColor.a; +} diff --git a/demos/transformer/src/main/assets/fragment_shader_vignette_es2.glsl b/demos/transformer/src/main/assets/fragment_shader_vignette_es2.glsl new file mode 100644 index 0000000000..55dea952a5 --- /dev/null +++ b/demos/transformer/src/main/assets/fragment_shader_vignette_es2.glsl @@ -0,0 +1,31 @@ +#version 100 +// 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. + +// ES 2 fragment shader that samples from a (non-external) texture with uTexSampler, +// copying from this texture to the current output while applying a vignette effect +// by linearly darkening the pixels between uInnerRadius and uOuterRadius. + +precision mediump float; +uniform sampler2D uTexSampler; +uniform vec2 uCenter; +uniform float uInnerRadius; +uniform float uOuterRadius; +varying vec2 vTexSamplingCoord; +void main() { + vec3 src = texture2D(uTexSampler, vTexSamplingCoord).xyz; + float dist = distance(vTexSamplingCoord, uCenter); + float scale = clamp(1.0 - (dist - uInnerRadius) / (uOuterRadius - uInnerRadius), 0.0, 1.0); + gl_FragColor = vec4(src.r * scale, src.g * scale, src.b * scale, 1.0); +} diff --git a/demos/transformer/src/main/assets/vertex_shader_copy_es2.glsl b/demos/transformer/src/main/assets/vertex_shader_copy_es2.glsl new file mode 100644 index 0000000000..b4c1673d25 --- /dev/null +++ b/demos/transformer/src/main/assets/vertex_shader_copy_es2.glsl @@ -0,0 +1,24 @@ +#version 100 +// 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. + +// ES 2 vertex shader that leaves the coordinates unchanged. + +attribute vec4 aFramePosition; +attribute vec4 aTexSamplingCoord; +varying vec2 vTexSamplingCoord; +void main() { + gl_Position = aFramePosition; + vTexSamplingCoord = aTexSamplingCoord.xy; +} diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/AdvancedFrameProcessorFactory.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/AdvancedFrameProcessorFactory.java new file mode 100644 index 0000000000..d833273a55 --- /dev/null +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/AdvancedFrameProcessorFactory.java @@ -0,0 +1,94 @@ +/* + * 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.demo.transformer; + +import android.graphics.Matrix; +import androidx.media3.common.C; +import androidx.media3.common.util.Util; +import androidx.media3.transformer.AdvancedFrameProcessor; +import androidx.media3.transformer.GlFrameProcessor; + +/** + * Factory for {@link GlFrameProcessor GlFrameProcessors} that create video effects by applying + * transformation matrices to the individual video frames using {@link AdvancedFrameProcessor}. + */ +/* package */ final class AdvancedFrameProcessorFactory { + /** + * Returns a {@link GlFrameProcessor} that rescales the frames over the first {@value + * #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases + * linearly in size from a single point to filling the full output frame. + */ + public static GlFrameProcessor createZoomInTransitionFrameProcessor() { + return new AdvancedFrameProcessor( + /* matrixProvider= */ AdvancedFrameProcessorFactory::calculateZoomInTransitionMatrix); + } + + /** + * Returns a {@link GlFrameProcessor} that crops frames to a rectangle that moves on an ellipse. + */ + public static GlFrameProcessor createDizzyCropFrameProcessor() { + return new AdvancedFrameProcessor( + /* matrixProvider= */ AdvancedFrameProcessorFactory::calculateDizzyCropMatrix); + } + + /** + * Returns a {@link GlFrameProcessor} that rotates a frame in 3D around the y-axis and applies + * perspective projection to 2D. + */ + public static GlFrameProcessor createSpin3dFrameProcessor() { + return new AdvancedFrameProcessor( + /* matrixProvider= */ AdvancedFrameProcessorFactory::calculate3dSpinMatrix); + } + + private static final float ZOOM_DURATION_SECONDS = 2f; + private static final float DIZZY_CROP_ROTATION_PERIOD_US = 1_500_000f; + + private static Matrix calculateZoomInTransitionMatrix(long presentationTimeUs) { + Matrix transformationMatrix = new Matrix(); + float scale = Math.min(1, presentationTimeUs / (C.MICROS_PER_SECOND * ZOOM_DURATION_SECONDS)); + transformationMatrix.postScale(/* sx= */ scale, /* sy= */ scale); + return transformationMatrix; + } + + private static android.graphics.Matrix calculateDizzyCropMatrix(long presentationTimeUs) { + double theta = presentationTimeUs * 2 * Math.PI / DIZZY_CROP_ROTATION_PERIOD_US; + float centerX = 0.5f * (float) Math.cos(theta); + float centerY = 0.5f * (float) Math.sin(theta); + android.graphics.Matrix transformationMatrix = new android.graphics.Matrix(); + transformationMatrix.postTranslate(/* dx= */ centerX, /* dy= */ centerY); + transformationMatrix.postScale(/* sx= */ 2f, /* sy= */ 2f); + return transformationMatrix; + } + + private static float[] calculate3dSpinMatrix(long presentationTimeUs) { + float[] transformationMatrix = new float[16]; + android.opengl.Matrix.frustumM( + transformationMatrix, + /* offset= */ 0, + /* left= */ -1f, + /* right= */ 1f, + /* bottom= */ -1f, + /* top= */ 1f, + /* near= */ 3f, + /* far= */ 5f); + android.opengl.Matrix.translateM( + transformationMatrix, /* mOffset= */ 0, /* x= */ 0f, /* y= */ 0f, /* z= */ -4f); + float theta = Util.usToMs(presentationTimeUs) / 10f; + android.opengl.Matrix.rotateM( + transformationMatrix, /* mOffset= */ 0, theta, /* x= */ 0f, /* y= */ 1f, /* z= */ 0f); + return transformationMatrix; + } +} diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/BitmapOverlayFrameProcessor.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/BitmapOverlayFrameProcessor.java new file mode 100644 index 0000000000..ca9d71f18c --- /dev/null +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/BitmapOverlayFrameProcessor.java @@ -0,0 +1,162 @@ +/* + * 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.demo.transformer; + +import static androidx.media3.common.util.Assertions.checkStateNotNull; + +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.drawable.BitmapDrawable; +import android.opengl.GLES20; +import android.opengl.GLUtils; +import android.util.Size; +import androidx.media3.common.C; +import androidx.media3.common.util.GlProgram; +import androidx.media3.common.util.GlUtil; +import androidx.media3.transformer.GlFrameProcessor; +import java.io.IOException; +import java.util.Locale; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A {@link GlFrameProcessor} that overlays a bitmap with a logo and timer on each frame. + * + *

The bitmap is drawn using an Android {@link Canvas}. + */ +// TODO(b/227625365): Delete this class and use a frame processor from the Transformer library, once +// overlaying a bitmap and text is supported in Transformer. +/* package */ final class BitmapOverlayFrameProcessor implements GlFrameProcessor { + static { + GlUtil.glAssertionsEnabled = true; + } + + private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl"; + private static final String FRAGMENT_SHADER_PATH = "fragment_shader_bitmap_overlay_es2.glsl"; + + private static final int BITMAP_WIDTH_HEIGHT = 512; + + private final Paint paint; + private final Bitmap overlayBitmap; + private final Canvas overlayCanvas; + + private float bitmapScaleX; + private float bitmapScaleY; + private int bitmapTexId; + private @MonotonicNonNull Size outputSize; + private @MonotonicNonNull Bitmap logoBitmap; + private @MonotonicNonNull GlProgram glProgram; + + public BitmapOverlayFrameProcessor() { + paint = new Paint(); + paint.setTextSize(64); + paint.setAntiAlias(true); + paint.setARGB(0xFF, 0xFF, 0xFF, 0xFF); + paint.setColor(Color.GRAY); + 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 = + ((BitmapDrawable) + context.getPackageManager().getApplicationIcon(context.getPackageName())) + .getBitmap(); + } 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); + // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. + glProgram.setBufferAttribute( + "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + glProgram.setBufferAttribute( + "aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + 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); + } + + @Override + public void drawFrame(long presentationTimeUs) { + checkStateNotNull(glProgram); + glProgram.use(); + + // Draw to the canvas and store it in a texture. + String text = + String.format(Locale.US, "%.02f", presentationTimeUs / (float) C.MICROS_PER_SECOND); + overlayBitmap.eraseColor(Color.TRANSPARENT); + overlayCanvas.drawBitmap(checkStateNotNull(logoBitmap), /* left= */ 3, /* top= */ 378, paint); + overlayCanvas.drawText(text, /* x= */ 160, /* y= */ 466, paint); + GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, bitmapTexId); + GLUtils.texSubImage2D( + GLES20.GL_TEXTURE_2D, + /* level= */ 0, + /* xoffset= */ 0, + /* yoffset= */ 0, + flipBitmapVertically(overlayBitmap)); + GlUtil.checkGlError(); + + glProgram.bindAttributesAndUniforms(); + // The four-vertex triangle strip forms a quad. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + } + + @Override + public void release() { + if (glProgram != null) { + glProgram.delete(); + } + } + + private static Bitmap flipBitmapVertically(Bitmap bitmap) { + Matrix flip = new Matrix(); + flip.postScale(1f, -1f); + return Bitmap.createBitmap( + bitmap, + /* x= */ 0, + /* y= */ 0, + bitmap.getWidth(), + bitmap.getHeight(), + flip, + /* filter= */ true); + } +} diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index eaa51847e5..8c078a180d 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -34,6 +34,8 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.MimeTypes; import androidx.media3.common.util.Util; +import com.google.android.material.slider.RangeSlider; +import com.google.android.material.slider.Slider; import java.util.Arrays; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -54,25 +56,48 @@ public final class ConfigurationActivity extends AppCompatActivity { public static final String SCALE_Y = "scale_y"; public static final String ROTATE_DEGREES = "rotate_degrees"; public static final String ENABLE_FALLBACK = "enable_fallback"; + public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping"; public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; + public static final String DEMO_FRAME_PROCESSORS_SELECTIONS = "demo_frame_processors_selections"; + public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x"; + public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y"; + public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius"; + public static final String PERIODIC_VIGNETTE_OUTER_RADIUS = "periodic_vignette_outer_radius"; private static final String[] INPUT_URIS = { "https://html5demos.com/assets/dizzy.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/1920w_1080h_4s.mp4", "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4", "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4", "https://html5demos.com/assets/dizzy.webm", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-hdr-hdr10.mp4", }; private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS "MP4 with H264 video and AAC audio", + "Short MP4 with H265 video and AAC audio", "MP4 with H265 video and AAC audio", "Long MP4 with H264 video and AAC audio", "WebM with VP8 video and Vorbis audio", "4K 60fps MP4 with H264 video and AAC audio (portrait, timestamps always increase)", + "8k 24fps MP4 with H265 video and AAC audio", + "MP4 with H264 video and AAC audio (portrait, H > W, 0\u00B0)", + "MP4 with H264 video and AAC audio (portrait, H < W, 90\u00B0)", + "SEF slow motion with 240 fps", + "MP4 with HDR (HDR10) H265 video (encoding may fail)", }; + private static final String[] DEMO_FRAME_PROCESSORS = { + "Dizzy crop", "Periodic vignette", "3D spin", "Overlay logo & timer", "Zoom in start" + }; + private static final int PERIODIC_VIGNETTE_INDEX = 1; private static final String SAME_AS_INPUT_OPTION = "same as input"; + private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2); - private @MonotonicNonNull Button chooseFileButton; - private @MonotonicNonNull TextView chosenFileTextView; + private @MonotonicNonNull Button selectFileButton; + private @MonotonicNonNull TextView selectedFileTextView; private @MonotonicNonNull CheckBox removeAudioCheckbox; private @MonotonicNonNull CheckBox removeVideoCheckbox; private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox; @@ -82,8 +107,15 @@ public final class ConfigurationActivity extends AppCompatActivity { private @MonotonicNonNull Spinner scaleSpinner; private @MonotonicNonNull Spinner rotateSpinner; private @MonotonicNonNull CheckBox enableFallbackCheckBox; + private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; + private @MonotonicNonNull Button selectDemoFrameProcessorsButton; + private boolean @MonotonicNonNull [] demoFrameProcessorsSelections; private int inputUriPosition; + private float periodicVignetteCenterX; + private float periodicVignetteCenterY; + private float periodicVignetteInnerRadius; + private float periodicVignetteOuterRadius; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -92,11 +124,11 @@ public final class ConfigurationActivity extends AppCompatActivity { findViewById(R.id.transform_button).setOnClickListener(this::startTransformation); - chooseFileButton = findViewById(R.id.choose_file_button); - chooseFileButton.setOnClickListener(this::chooseFile); + selectFileButton = findViewById(R.id.select_file_button); + selectFileButton.setOnClickListener(this::selectFile); - chosenFileTextView = findViewById(R.id.chosen_file_text_view); - chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + selectedFileTextView = findViewById(R.id.selected_file_text_view); + selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox); removeAudioCheckbox.setOnClickListener(this::onRemoveAudio); @@ -148,7 +180,14 @@ public final class ConfigurationActivity extends AppCompatActivity { rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180"); enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox); + enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox); + enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported()); + findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported()); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); + + demoFrameProcessorsSelections = new boolean[DEMO_FRAME_PROCESSORS.length]; + selectDemoFrameProcessorsButton = findViewById(R.id.select_demo_frameprocessors_button); + selectDemoFrameProcessorsButton.setOnClickListener(this::selectFrameProcessors); } @Override @@ -156,8 +195,8 @@ public final class ConfigurationActivity extends AppCompatActivity { super.onResume(); @Nullable Uri intentUri = getIntent().getData(); if (intentUri != null) { - checkNotNull(chooseFileButton).setEnabled(false); - checkNotNull(chosenFileTextView).setText(intentUri.toString()); + checkNotNull(selectFileButton).setEnabled(false); + checkNotNull(selectedFileTextView).setText(intentUri.toString()); } } @@ -177,7 +216,9 @@ public final class ConfigurationActivity extends AppCompatActivity { "scaleSpinner", "rotateSpinner", "enableFallbackCheckBox", - "enableHdrEditingCheckBox" + "enableRequestSdrToneMappingCheckBox", + "enableHdrEditingCheckBox", + "demoFrameProcessorsSelections" }) private void startTransformation(View view) { Intent transformerIntent = new Intent(this, TransformerActivity.class); @@ -209,7 +250,14 @@ public final class ConfigurationActivity extends AppCompatActivity { bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate)); } bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked()); + bundle.putBoolean( + ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked()); bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); + bundle.putBooleanArray(DEMO_FRAME_PROCESSORS_SELECTIONS, demoFrameProcessorsSelections); + bundle.putFloat(PERIODIC_VIGNETTE_CENTER_X, periodicVignetteCenterX); + bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY); + bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius); + bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius); transformerIntent.putExtras(bundle); @Nullable Uri intentUri = getIntent().getData(); @@ -219,19 +267,63 @@ public final class ConfigurationActivity extends AppCompatActivity { startActivity(transformerIntent); } - private void chooseFile(View view) { + private void selectFile(View view) { new AlertDialog.Builder(/* context= */ this) - .setTitle(R.string.choose_file_title) + .setTitle(R.string.select_file_title) .setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog) .setPositiveButton(android.R.string.ok, /* listener= */ null) .create() .show(); } - @RequiresNonNull("chosenFileTextView") + private void selectFrameProcessors(View view) { + new AlertDialog.Builder(/* context= */ this) + .setTitle(R.string.select_demo_frameprocessors) + .setMultiChoiceItems( + DEMO_FRAME_PROCESSORS, + checkNotNull(demoFrameProcessorsSelections), + this::selectFrameProcessor) + .setPositiveButton(android.R.string.ok, /* listener= */ null) + .create() + .show(); + } + + @RequiresNonNull("selectedFileTextView") private void selectFileInDialog(DialogInterface dialog, int which) { inputUriPosition = which; - chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + } + + @RequiresNonNull("demoFrameProcessorsSelections") + private void selectFrameProcessor(DialogInterface dialog, int which, boolean isChecked) { + demoFrameProcessorsSelections[which] = isChecked; + if (!isChecked || which != PERIODIC_VIGNETTE_INDEX) { + return; + } + + View dialogView = + getLayoutInflater().inflate(R.layout.periodic_vignette_options, /* root= */ null); + Slider centerXSlider = + checkNotNull(dialogView.findViewById(R.id.periodic_vignette_center_x_slider)); + Slider centerYSlider = + checkNotNull(dialogView.findViewById(R.id.periodic_vignette_center_y_slider)); + RangeSlider radiusRangeSlider = + checkNotNull(dialogView.findViewById(R.id.periodic_vignette_radius_range_slider)); + radiusRangeSlider.setValues(0f, HALF_DIAGONAL); + new AlertDialog.Builder(/* context= */ this) + .setTitle(R.string.periodic_vignette_options) + .setView(dialogView) + .setPositiveButton( + android.R.string.ok, + (DialogInterface dialogInterface, int i) -> { + periodicVignetteCenterX = centerXSlider.getValue(); + periodicVignetteCenterY = centerYSlider.getValue(); + List radiusRange = radiusRangeSlider.getValues(); + periodicVignetteInnerRadius = radiusRange.get(0); + periodicVignetteOuterRadius = radiusRange.get(1); + }) + .create() + .show(); } @RequiresNonNull({ @@ -241,7 +333,9 @@ public final class ConfigurationActivity extends AppCompatActivity { "resolutionHeightSpinner", "scaleSpinner", "rotateSpinner", - "enableHdrEditingCheckBox" + "enableRequestSdrToneMappingCheckBox", + "enableHdrEditingCheckBox", + "selectDemoFrameProcessorsButton" }) private void onRemoveAudio(View view) { if (((CheckBox) view).isChecked()) { @@ -259,7 +353,9 @@ public final class ConfigurationActivity extends AppCompatActivity { "resolutionHeightSpinner", "scaleSpinner", "rotateSpinner", - "enableHdrEditingCheckBox" + "enableRequestSdrToneMappingCheckBox", + "enableHdrEditingCheckBox", + "selectDemoFrameProcessorsButton" }) private void onRemoveVideo(View view) { if (((CheckBox) view).isChecked()) { @@ -276,7 +372,9 @@ public final class ConfigurationActivity extends AppCompatActivity { "resolutionHeightSpinner", "scaleSpinner", "rotateSpinner", - "enableHdrEditingCheckBox" + "enableRequestSdrToneMappingCheckBox", + "enableHdrEditingCheckBox", + "selectDemoFrameProcessorsButton" }) private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { audioMimeSpinner.setEnabled(isAudioEnabled); @@ -284,13 +382,22 @@ public final class ConfigurationActivity extends AppCompatActivity { resolutionHeightSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled); + enableRequestSdrToneMappingCheckBox.setEnabled( + isRequestSdrToneMappingSupported() && isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled); + selectDemoFrameProcessorsButton.setEnabled(isVideoEnabled); findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled); findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled); findViewById(R.id.scale).setEnabled(isVideoEnabled); findViewById(R.id.rotate).setEnabled(isVideoEnabled); + findViewById(R.id.request_sdr_tone_mapping) + .setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled); findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); } + + private static boolean isRequestSdrToneMappingSupported() { + return Util.SDK_INT >= 31; + } } diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/PeriodicVignetteFrameProcessor.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/PeriodicVignetteFrameProcessor.java new file mode 100644 index 0000000000..650accf7d3 --- /dev/null +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/PeriodicVignetteFrameProcessor.java @@ -0,0 +1,118 @@ +/* + * 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.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 androidx.media3.common.util.GlProgram; +import androidx.media3.common.util.GlUtil; +import androidx.media3.transformer.GlFrameProcessor; +import java.io.IOException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * A {@link GlFrameProcessor} that periodically dims the frames such that pixels are darker the + * further they are away from the frame center. + */ +/* package */ final class PeriodicVignetteFrameProcessor implements GlFrameProcessor { + static { + GlUtil.glAssertionsEnabled = true; + } + + 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; + + /** + * Creates a new instance. + * + *

The inner radius of the vignette effect oscillates smoothly between {@code minInnerRadius} + * and {@code maxInnerRadius}. + * + *

The pixels between the inner radius and the {@code outerRadius} are darkened linearly based + * on their distance from {@code innerRadius}. All pixels outside {@code outerRadius} are black. + * + *

The parameters are given in normalized texture coordinates from 0 to 1. + * + * @param context The {@link Context}. + * @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. + */ + public PeriodicVignetteFrameProcessor( + float centerX, float centerY, float minInnerRadius, float maxInnerRadius, float outerRadius) { + 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); + 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. + glProgram.setBufferAttribute( + "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + glProgram.setBufferAttribute( + "aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(outputSize); + } + + @Override + public void drawFrame(long presentationTimeUs) { + checkStateNotNull(glProgram).use(); + double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US; + float innerRadius = minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta)); + glProgram.setFloatsUniform("uInnerRadius", new float[] {innerRadius}); + glProgram.bindAttributesAndUniforms(); + // The four-vertex triangle strip forms a quad. + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); + } + + @Override + public void release() { + if (glProgram != null) { + glProgram.delete(); + } + } +} diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java index ad83ce75f0..b6559c6c86 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -40,6 +40,7 @@ import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.util.DebugTextViewHelper; import androidx.media3.transformer.DefaultEncoderFactory; import androidx.media3.transformer.EncoderSelector; +import androidx.media3.transformer.GlFrameProcessor; import androidx.media3.transformer.ProgressHolder; import androidx.media3.transformer.TransformationException; import androidx.media3.transformer.TransformationRequest; @@ -50,6 +51,7 @@ import androidx.media3.ui.PlayerView; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.common.base.Stopwatch; import com.google.common.base.Ticker; +import com.google.common.collect.ImmutableList; import java.io.File; import java.io.IOException; import java.util.concurrent.CountDownLatch; @@ -225,6 +227,8 @@ public final class TransformerActivity extends AppCompatActivity { bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0); requestBuilder.setRotationDegrees(rotateDegrees); + requestBuilder.setEnableRequestSdrToneMapping( + bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING)); requestBuilder.experimental_setEnableHdrEditing( bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING)); transformerBuilder @@ -235,6 +239,37 @@ public final class TransformerActivity extends AppCompatActivity { new DefaultEncoderFactory( EncoderSelector.DEFAULT, /* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))); + + ImmutableList.Builder frameProcessors = new ImmutableList.Builder<>(); + @Nullable + boolean[] selectedFrameProcessors = + bundle.getBooleanArray(ConfigurationActivity.DEMO_FRAME_PROCESSORS_SELECTIONS); + if (selectedFrameProcessors != null) { + if (selectedFrameProcessors[0]) { + frameProcessors.add(AdvancedFrameProcessorFactory.createDizzyCropFrameProcessor()); + } + if (selectedFrameProcessors[1]) { + frameProcessors.add( + new PeriodicVignetteFrameProcessor( + bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X), + bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y), + /* minInnerRadius= */ bundle.getFloat( + ConfigurationActivity.PERIODIC_VIGNETTE_INNER_RADIUS), + /* maxInnerRadius= */ bundle.getFloat( + ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS), + bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS))); + } + if (selectedFrameProcessors[2]) { + frameProcessors.add(AdvancedFrameProcessorFactory.createSpin3dFrameProcessor()); + } + if (selectedFrameProcessors[3]) { + frameProcessors.add(new BitmapOverlayFrameProcessor()); + } + if (selectedFrameProcessors[4]) { + frameProcessors.add(AdvancedFrameProcessorFactory.createZoomInTransitionFrameProcessor()); + } + transformerBuilder.setFrameProcessors(frameProcessors.build()); + } } return transformerBuilder .addListener( diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml index 1ff3cafc6b..7d080b7351 100644 --- a/demos/transformer/src/main/res/layout/configuration_activity.xml +++ b/demos/transformer/src/main/res/layout/configuration_activity.xml @@ -34,18 +34,18 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />