diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 5d7f107c5f2..2983ab53ed6 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -18,6 +18,7 @@ body: label: ExoPlayer Version description: What version of ExoPlayer are you using? options: + - 2.18.0 - 2.17.1 - 2.17.0 - 2.16.1 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d658b522993..b56338615c2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,194 @@ # Release notes +### 2.18.0 (2022-06-16) + +This release corresponds to the +[AndroidX media3 1.0.0-beta01 release](https://github.com/androidx/media/releases/tag/1.0.0-beta01). + +* Core library: + * Enable support for Android platform diagnostics via + `MediaMetricsManager`. ExoPlayer will forward playback events and + performance data to the platform, which helps to provide system + performance and debugging information on the device. This data may also + be collected by Google + [if sharing usage and diagnostics data is enabled](https://support.google.com/accounts/answer/6078260) + by the user of the device. Apps can opt-out of contributing to platform + diagnostics for ExoPlayer with + `ExoPlayer.Builder.setUsePlatformDiagnostics(false)`. + * Fix bug that tracks are reset too often when using `MergingMediaSource`, + for example when side-loading subtitles and changing the selected + subtitle mid-playback + ([#10248](https://github.com/google/ExoPlayer/issues/10248)). + * Stop detecting 5G-NSA network type on API 29 and 30. These playbacks + will assume a 4G network. + * Disallow passing `null` to + `MediaSource.Factory.setDrmSessionManagerProvider` and + `MediaSource.Factory.setLoadErrorHandlingPolicy`. Instances of + `DefaultDrmSessionManagerProvider` and `DefaultLoadErrorHandlingPolicy` + can be passed explicitly if required. + * Add `MediaItem.RequestMetadata` to represent metadata needed to play + media when the exact `LocalConfiguration` is not known. Also remove + `MediaMetadata.mediaUrl` as this is now included in `RequestMetadata`. + * Add `Player.Command.COMMAND_SET_MEDIA_ITEM` to enable players to allow + setting a single item. +* 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`. + * Change `DefaultTrackSelector.buildUponParameters` and + `DefaultTrackSelector.Parameters.buildUpon` to return + `DefaultTrackSelector.Parameters.Builder` instead of the deprecated + `DefaultTrackSelector.ParametersBuilder`. + * Add + `DefaultTrackSelector.Parameters.constrainAudioChannelCountToDeviceCapabilities` + which is enabled by default. When enabled, the `DefaultTrackSelector` + will prefer audio tracks whose channel count does not exceed the device + output capabilities. On handheld devices, the `DefaultTrackSelector` + will prefer stereo/mono over multichannel audio formats, unless the + multichannel format can be + [Spatialized](https://developer.android.com/reference/android/media/Spatializer) + (Android 12L+) or is a Dolby surround sound format. In addition, on + devices that support audio spatialization, the `DefaultTrackSelector` + will monitor for changes in the + [Spatializer properties](https://developer.android.com/reference/android/media/Spatializer.OnSpatializerStateChangedListener) + and trigger a new track selection upon these. Devices with a + `television` + [UI mode](https://developer.android.com/guide/topics/resources/providing-resources#UiModeQualifier) + are excluded from these constraints and the format with the highest + channel count will be preferred. To enable this feature, the + `DefaultTrackSelector` instance must be constructed with a `Context`. +* Video: + * Rename `DummySurface` to `PlaceholderSurface`. + * Add AV1 support to the `MediaCodecVideoRenderer.getCodecMaxInputSize`. +* Audio: + * Use LG AC3 audio decoder advertising non-standard MIME type. + * Change the return type of `AudioAttributes.getAudioAttributesV21()` from + `android.media.AudioAttributes` to a new `AudioAttributesV21` wrapper + class, to prevent slow ART verification on API < 21. + * Query the platform (API 29+) or assume the audio encoding channel count + for audio passthrough when the format audio channel count is unset, + which occurs with HLS chunkless preparation + ([10204](https://github.com/google/ExoPlayer/issues/10204)). + * Configure `AudioTrack` with channel mask + `AudioFormat.CHANNEL_OUT_7POINT1POINT4` if the decoder outputs 12 + channel PCM audio + ([#10322](#https://github.com/google/ExoPlayer/pull/10322). +* DRM + * Ensure the DRM session is always correctly updated when seeking + immediately after a format change + ([10274](https://github.com/google/ExoPlayer/issues/10274)). +* Text: + * Change `Player.getCurrentCues()` to return `CueGroup` instead of + `List`. + * SSA: Support `OutlineColour` style setting when `BorderStyle == 3` (i.e. + `OutlineColour` sets the background of the cue) + ([#8435](https://github.com/google/ExoPlayer/issues/8435)). + * CEA-708: Parse data into multiple service blocks and ignore blocks not + associated with the currently selected service number. + * Remove `RawCcExtractor`, which was only used to handle a Google-internal + subtitle format. +* Extractors: + * Matroska: Parse `DiscardPadding` for Opus tracks. + * MP4: Parse bitrates from `esds` boxes. + * Ogg: Allow duplicate Opus ID and comment headers + ([#10038](https://github.com/google/ExoPlayer/issues/10038)). +* UI: + * Fix delivery of events to `OnClickListener`s set on `StyledPlayerView` + and `PlayerView`, in the case that `useController=false` + ([#9605](https://github.com/google/ExoPlayer/issues/9605)). Also fix + delivery of events to `OnLongClickListener` for all view configurations. + * Fix incorrectly treating a sequence of touch events that exit the bounds + of `StyledPlayerView` and `PlayerView` before `ACTION_UP` as a click + ([#9861](https://github.com/google/ExoPlayer/issues/9861)). + * Fix `PlayerView` accessibility issue where tapping might toggle playback + rather than hiding the controls + ([#8627](https://github.com/google/ExoPlayer/issues/8627)). + * Rewrite `TrackSelectionView` and `TrackSelectionDialogBuilder` to work + with the `Player` interface rather than `ExoPlayer`. This allows the + 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)). +* DASH: + * Parse channel count from DTS `AudioChannelConfiguration` elements. This + re-enables audio passthrough for DTS streams + ([#10159](https://github.com/google/ExoPlayer/issues/10159)). + * Disallow passing `null` to + `DashMediaSource.Factory.setCompositeSequenceableLoaderFactory`. + Instances of `DefaultCompositeSequenceableLoaderFactory` can be passed + explicitly if required. +* HLS: + * Fallback to chunkful preparation if the playlist CODECS attribute does + not contain the audio codec + ([#10065](https://github.com/google/ExoPlayer/issues/10065)). + * Disallow passing `null` to + `HlsMediaSource.Factory.setCompositeSequenceableLoaderFactory`, + `HlsMediaSource.Factory.setPlaylistParserFactory`, and + `HlsMediaSource.Factory.setPlaylistTrackerFactory`. Instances of + `DefaultCompositeSequenceableLoaderFactory`, + `DefaultHlsPlaylistParserFactory`, or a reference to + `DefaultHlsPlaylistTracker.FACTORY` can be passed explicitly if + required. +* Smooth Streaming: + * Disallow passing `null` to + `SsMediaSource.Factory.setCompositeSequenceableLoaderFactory`. Instances + of `DefaultCompositeSequenceableLoaderFactory` can be passed explicitly + if required. +* 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)). + * Stop checking mandatory SDP fields as ExoPlayer doesn't need them + ([#10049](https://github.com/google/ExoPlayer/issues/10049)). + * Throw checked exception when parsing RTSP timing + ([#10165](https://github.com/google/ExoPlayer/issues/10165)). + * Add RTP reader for VP9 + ([#47](https://github.com/androidx/media/pull/64)). + * Add RTP reader for OPUS + ([#53](https://github.com/androidx/media/pull/53)). +* Data sources: + * Rename `DummyDataSource` to `PlaceholderDataSource`. + * Workaround OkHttp interrupt handling. +* Ad playback / IMA: + * Decrease ad polling rate from every 100ms to every 200ms, to line up + with Media Rating Council (MRC) recommendations. +* FFmpeg extension: + * Update CMake version to `3.21.0+` to avoid a CMake bug causing + AndroidStudio's gradle sync to fail + ([#9933](https://github.com/google/ExoPlayer/issues/9933)). +* Remove deprecated symbols: + * Remove `Player.Listener.onTracksChanged`. Use + `Player.Listener.onTracksInfoChanged` instead. + * Remove `Player.getCurrentTrackGroups` and + `Player.getCurrentTrackSelections`. Use `Player.getCurrentTracksInfo` + instead. You can also continue to use `ExoPlayer.getCurrentTrackGroups` + and `ExoPlayer.getCurrentTrackSelections`, although these methods remain + deprecated. + * Remove `DownloadHelper` + `DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_VIEWPORT` and + `DEFAULT_TRACK_SELECTOR_PARAMETERS` constants. Use + `getDefaultTrackSelectorParameters(Context)` instead when possible, and + `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. + ### 2.17.1 (2022-03-10) This release corresponds to the diff --git a/build.gradle b/build.gradle index d362ff785be..3d91579992b 100644 --- a/build.gradle +++ b/build.gradle @@ -17,7 +17,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.3' + classpath 'com.android.tools.build:gradle:7.2.1' classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.2' } } diff --git a/common_library_config.gradle b/common_library_config.gradle index 6164b35e151..51773ca0e18 100644 --- a/common_library_config.gradle +++ b/common_library_config.gradle @@ -29,5 +29,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } - testOptions.unitTests.includeAndroidResources = true + testOptions { + unitTests.all { + jvmArgs "-Xmx2g" + } + unitTests.includeAndroidResources true + } } diff --git a/constants.gradle b/constants.gradle index bf4e665e911..1b6fb224852 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,21 +13,21 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.17.1' - releaseVersionCode = 2_017_001 + releaseVersion = '2.18.0' + releaseVersionCode = 2_018_000 minSdkVersion = 16 appTargetSdkVersion = 29 // 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: // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA guavaVersion = '31.0.1-android' mockitoVersion = '3.12.4' - robolectricVersion = '4.6.1' + robolectricVersion = '4.8.1' // Keep this in sync with Google's internal Checker Framework version. checkerframeworkVersion = '3.13.0' checkerframeworkCompatVersion = '2.5.5' diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java index 172322d260d..ad1224972f4 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/MainActivity.java @@ -230,8 +230,8 @@ public RecyclerViewCallback() { @Override public boolean onMove( RecyclerView list, RecyclerView.ViewHolder origin, RecyclerView.ViewHolder target) { - int fromPosition = origin.getAdapterPosition(); - int toPosition = target.getAdapterPosition(); + int fromPosition = origin.getBindingAdapterPosition(); + int toPosition = target.getBindingAdapterPosition(); if (draggingFromPosition == C.INDEX_UNSET) { // A drag has started, but changes to the media queue will be reflected in clearView(). draggingFromPosition = fromPosition; @@ -243,7 +243,7 @@ public boolean onMove( @Override public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) { - int position = viewHolder.getAdapterPosition(); + int position = viewHolder.getBindingAdapterPosition(); QueueItemViewHolder queueItemHolder = (QueueItemViewHolder) viewHolder; if (playerManager.removeItem(queueItemHolder.item)) { mediaQueueListAdapter.notifyItemRemoved(position); @@ -282,7 +282,7 @@ public QueueItemViewHolder(TextView textView) { @Override public void onClick(View v) { - playerManager.selectQueueItem(getAdapterPosition()); + playerManager.selectQueueItem(getBindingAdapterPosition()); } } diff --git a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java index 8d78ca1112b..256e5128ac1 100644 --- a/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java +++ b/demos/cast/src/main/java/com/google/android/exoplayer2/castdemo/PlayerManager.java @@ -25,7 +25,7 @@ import com.google.android.exoplayer2.Player.DiscontinuityReason; import com.google.android.exoplayer2.Player.TimelineChangeReason; import com.google.android.exoplayer2.Timeline; -import com.google.android.exoplayer2.TracksInfo; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.ext.cast.CastPlayer; import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener; import com.google.android.exoplayer2.ui.StyledPlayerControlView; @@ -57,7 +57,7 @@ interface Listener { private final ArrayList mediaQueue; private final Listener listener; - private TracksInfo lastSeenTrackGroupInfo; + private Tracks lastSeenTracks; private int currentItemIndex; private Player currentPlayer; @@ -219,19 +219,19 @@ public void onTimelineChanged(Timeline timeline, @TimelineChangeReason int reaso } @Override - public void onTracksInfoChanged(TracksInfo tracksInfo) { - if (currentPlayer != localPlayer || tracksInfo == lastSeenTrackGroupInfo) { + public void onTracksChanged(Tracks tracks) { + if (currentPlayer != localPlayer || tracks == lastSeenTracks) { return; } - if (!tracksInfo.isTypeSupportedOrEmpty( - 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.isTypeSupportedOrEmpty( - 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/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java index 8a5d135dee0..42dbc5b2b73 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/BitmapOverlayVideoProcessor.java @@ -27,6 +27,7 @@ import android.opengl.GLES20; import android.opengl.GLUtils; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.util.GlProgram; import com.google.android.exoplayer2.util.GlUtil; import java.io.IOException; import java.util.Locale; @@ -50,7 +51,7 @@ private final Bitmap logoBitmap; private final Canvas overlayCanvas; - private GlUtil.@MonotonicNonNull Program program; + private @MonotonicNonNull GlProgram program; private float bitmapScaleX; private float bitmapScaleY; @@ -78,7 +79,7 @@ public BitmapOverlayVideoProcessor(Context context) { public void initialize() { try { program = - new GlUtil.Program( + new GlProgram( context, /* vertexShaderFilePath= */ "bitmap_overlay_video_processor_vertex.glsl", /* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl"); @@ -86,9 +87,13 @@ public void initialize() { throw new IllegalStateException(e); } program.setBufferAttribute( - "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + "aFramePosition", + GlUtil.getNormalizedCoordinateBounds(), + GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); program.setBufferAttribute( - "aTexCoords", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); + "aTexCoords", + GlUtil.getTextureCoordinateBounds(), + GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); GLES20.glGenTextures(1, textures, 0); GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); GLES20.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); @@ -117,9 +122,9 @@ public void draw(int frameTexture, long frameTimestampUs, float[] transformMatri GlUtil.checkGlError(); // Run the shader program. - GlUtil.Program program = checkNotNull(this.program); - program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* unit= */ 0); - program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* unit= */ 1); + GlProgram program = checkNotNull(this.program); + program.setSamplerTexIdUniform("uTexSampler0", frameTexture, /* texUnitIndex= */ 0); + program.setSamplerTexIdUniform("uTexSampler1", textures[0], /* texUnitIndex= */ 1); program.setFloatUniform("uScaleX", bitmapScaleX); program.setFloatUniform("uScaleY", bitmapScaleY); program.setFloatsUniform("uTexTransform", transformMatrix); diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java index 1f10cd2c59b..c1fc42becfa 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.text.TextUtils; import android.widget.FrameLayout; import android.widget.Toast; import androidx.annotation.Nullable; @@ -38,7 +39,6 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.EventLogger; import com.google.android.exoplayer2.util.GlUtil; @@ -144,7 +144,7 @@ private void initializePlayer() { String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); - HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); + DataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); drmSessionManager = @@ -157,13 +157,18 @@ private void initializePlayer() { DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this); MediaSource mediaSource; - @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); - if (type == C.TYPE_DASH) { + @Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA); + @C.ContentType + int type = + TextUtils.isEmpty(fileExtension) + ? Util.inferContentType(uri) + : Util.inferContentTypeForExtension(fileExtension); + if (type == C.CONTENT_TYPE_DASH) { mediaSource = new DashMediaSource.Factory(dataSourceFactory) .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .createMediaSource(MediaItem.fromUri(uri)); - } else if (type == C.TYPE_OTHER) { + } else if (type == C.CONTENT_TYPE_OTHER) { mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory) .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) @@ -181,7 +186,7 @@ private void initializePlayer() { Assertions.checkNotNull(this.videoProcessingGLSurfaceView); videoProcessingGLSurfaceView.setPlayer(player); Assertions.checkNotNull(playerView).setPlayer(player); - player.addAnalyticsListener(new EventLogger(/* trackSelector= */ null)); + player.addAnalyticsListener(new EventLogger()); this.player = player; } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java index e6a67da5863..4343f74830a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.cache.Cache; import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; @@ -59,7 +58,7 @@ public final class DemoUtil { private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; private static DataSource.@MonotonicNonNull Factory dataSourceFactory; - private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory; + private static DataSource.@MonotonicNonNull Factory httpDataSourceFactory; private static @MonotonicNonNull DatabaseProvider databaseProvider; private static @MonotonicNonNull File downloadDirectory; private static @MonotonicNonNull Cache downloadCache; @@ -85,7 +84,7 @@ public static RenderersFactory buildRenderersFactory( .setExtensionRendererMode(extensionRendererMode); } - public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) { + public static synchronized DataSource.Factory getHttpDataSourceFactory(Context context) { if (httpDataSourceFactory == null) { if (USE_CRONET_FOR_NETWORKING) { context = context.getApplicationContext(); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index 9eb141e6591..ae6f4dde257 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -15,8 +15,7 @@ */ package com.google.android.exoplayer2.demo; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; +import static com.google.common.base.Preconditions.checkNotNull; import android.content.Context; import android.content.DialogInterface; @@ -29,6 +28,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.RenderersFactory; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.drm.DrmInitData; import com.google.android.exoplayer2.drm.DrmSession; import com.google.android.exoplayer2.drm.DrmSessionEventListener; @@ -43,9 +43,9 @@ import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; -import com.google.android.exoplayer2.upstream.HttpDataSource; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; +import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import java.io.IOException; @@ -65,31 +65,26 @@ public interface Listener { private static final String TAG = "DownloadTracker"; private final Context context; - private final HttpDataSource.Factory httpDataSourceFactory; + private final DataSource.Factory dataSourceFactory; private final CopyOnWriteArraySet listeners; private final HashMap downloads; private final DownloadIndex downloadIndex; - private final DefaultTrackSelector.Parameters trackSelectorParameters; @Nullable private StartDownloadDialogHelper startDownloadDialogHelper; public DownloadTracker( - Context context, - HttpDataSource.Factory httpDataSourceFactory, - DownloadManager downloadManager) { + Context context, DataSource.Factory dataSourceFactory, DownloadManager downloadManager) { this.context = context.getApplicationContext(); - this.httpDataSourceFactory = httpDataSourceFactory; + this.dataSourceFactory = dataSourceFactory; listeners = new CopyOnWriteArraySet<>(); downloads = new HashMap<>(); downloadIndex = downloadManager.getDownloadIndex(); - trackSelectorParameters = DownloadHelper.getDefaultTrackSelectorParameters(context); downloadManager.addListener(new DownloadManagerListener()); loadDownloads(); } public void addListener(Listener listener) { - checkNotNull(listener); - listeners.add(listener); + listeners.add(checkNotNull(listener)); } public void removeListener(Listener listener) { @@ -120,8 +115,7 @@ public void toggleDownload( startDownloadDialogHelper = new StartDownloadDialogHelper( fragmentManager, - DownloadHelper.forMediaItem( - context, mediaItem, renderersFactory, httpDataSourceFactory), + DownloadHelper.forMediaItem(context, mediaItem, renderersFactory, dataSourceFactory), mediaItem); } } @@ -159,7 +153,7 @@ public void onDownloadRemoved(DownloadManager downloadManager, Download download private final class StartDownloadDialogHelper implements DownloadHelper.Callback, - DialogInterface.OnClickListener, + TrackSelectionDialog.TrackSelectionListener, DialogInterface.OnDismissListener { private final FragmentManager fragmentManager; @@ -167,7 +161,6 @@ private final class StartDownloadDialogHelper private final MediaItem mediaItem; private TrackSelectionDialog trackSelectionDialog; - private MappedTrackInfo mappedTrackInfo; private WidevineOfflineLicenseFetchTask widevineOfflineLicenseFetchTask; @Nullable private byte[] keySetId; @@ -220,7 +213,7 @@ public void onPrepared(DownloadHelper helper) { new WidevineOfflineLicenseFetchTask( format, mediaItem.localConfiguration.drmConfiguration, - httpDataSourceFactory, + dataSourceFactory, /* dialogHelper= */ this, helper); widevineOfflineLicenseFetchTask.execute(); @@ -237,21 +230,13 @@ public void onPrepareError(DownloadHelper helper, IOException e) { Log.e(TAG, logMessage, e); } - // DialogInterface.OnClickListener implementation. + // TrackSelectionListener implementation. @Override - public void onClick(DialogInterface dialog, int which) { + public void onTracksSelected(TrackSelectionParameters trackSelectionParameters) { for (int periodIndex = 0; periodIndex < downloadHelper.getPeriodCount(); periodIndex++) { downloadHelper.clearTrackSelections(periodIndex); - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - if (!trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)) { - downloadHelper.addTrackSelectionForSingleRenderer( - periodIndex, - /* rendererIndex= */ i, - trackSelectorParameters, - trackSelectionDialog.getOverrides(/* rendererIndex= */ i)); - } - } + downloadHelper.addTrackSelection(periodIndex, trackSelectionParameters); } DownloadRequest downloadRequest = buildDownloadRequest(); if (downloadRequest.streamKeys.isEmpty()) { @@ -316,21 +301,21 @@ private void onDownloadPrepared(DownloadHelper helper) { return; } - mappedTrackInfo = downloadHelper.getMappedTrackInfo(/* periodIndex= */ 0); - if (!TrackSelectionDialog.willHaveContent(mappedTrackInfo)) { + 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.createForMappedTrackInfoAndParameters( + TrackSelectionDialog.createForTracksAndParameters( /* titleId= */ R.string.exo_download_description, - mappedTrackInfo, - trackSelectorParameters, + tracks, + DownloadHelper.getDefaultTrackSelectorParameters(context), /* allowAdaptiveSelections= */ false, /* allowMultipleOverrides= */ true, - /* onClickListener= */ this, + /* onTracksSelectedListener= */ this, /* onDismissListener= */ this); trackSelectionDialog.show(fragmentManager, /* tag= */ null); } @@ -371,7 +356,7 @@ private static final class WidevineOfflineLicenseFetchTask extends AsyncTask createMediaItemsFromIntent(Intent intent) { /** Populates the intent with the given list of {@link MediaItem media items}. */ public static void addToIntent(List mediaItems, Intent intent) { - Assertions.checkArgument(!mediaItems.isEmpty()); + checkArgument(!mediaItems.isEmpty()); if (mediaItems.size() == 1) { MediaItem mediaItem = mediaItems.get(0); MediaItem.LocalConfiguration localConfiguration = checkNotNull(mediaItem.localConfiguration); @@ -178,7 +178,7 @@ private static MediaItem.Builder populateDrmPropertiesFromIntent( headers.put(keyRequestPropertiesArray[i], keyRequestPropertiesArray[i + 1]); } } - @Nullable UUID drmUuid = Util.getDrmUuid(Util.castNonNull(drmSchemeExtra)); + @Nullable UUID drmUuid = Util.getDrmUuid(drmSchemeExtra); if (drmUuid != null) { builder.setDrmConfiguration( new MediaItem.DrmConfiguration.Builder(drmUuid) @@ -189,7 +189,7 @@ private static MediaItem.Builder populateDrmPropertiesFromIntent( intent.getBooleanExtra( DRM_FORCE_DEFAULT_LICENSE_URI_EXTRA + extrasKeySuffix, false)) .setLicenseRequestHeaders(headers) - .forceSessionsForAudioAndVideoTracks( + .setForceSessionsForAudioAndVideoTracks( intent.getBooleanExtra(DRM_SESSION_FOR_CLEAR_CONTENT + extrasKeySuffix, false)) .build()); } @@ -242,7 +242,7 @@ private static void addDrmConfigurationToIntent( drmConfiguration.forcedSessionTrackTypes; if (!forcedDrmSessionTrackTypes.isEmpty()) { // Only video and audio together are supported. - Assertions.checkState( + checkState( forcedDrmSessionTrackTypes.size() == 2 && forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_VIDEO) && forcedDrmSessionTrackTypes.contains(C.TRACK_TYPE_AUDIO)); diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 4e347734efc..8932b0780d9 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -17,6 +17,7 @@ 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; @@ -34,8 +35,9 @@ import com.google.android.exoplayer2.PlaybackException; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RenderersFactory; -import com.google.android.exoplayer2.TracksInfo; +import com.google.android.exoplayer2.Tracks; import com.google.android.exoplayer2.audio.AudioAttributes; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider; import com.google.android.exoplayer2.drm.FrameworkMediaDrm; import com.google.android.exoplayer2.ext.ima.ImaAdsLoader; import com.google.android.exoplayer2.ext.ima.ImaServerSideAdInsertionMediaSource; @@ -45,8 +47,7 @@ import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.ads.AdsLoader; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.ui.StyledPlayerControlView; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; import com.google.android.exoplayer2.ui.StyledPlayerView; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.util.DebugTextViewHelper; @@ -60,7 +61,7 @@ /** An activity that plays media using {@link ExoPlayer}. */ public class PlayerActivity extends AppCompatActivity - implements OnClickListener, StyledPlayerControlView.VisibilityListener { + implements OnClickListener, StyledPlayerView.ControllerVisibilityListener { // Saved instance state keys. @@ -79,10 +80,9 @@ public class PlayerActivity extends AppCompatActivity private Button selectTracksButton; private DataSource.Factory dataSourceFactory; private List mediaItems; - private DefaultTrackSelector trackSelector; - private DefaultTrackSelector.Parameters trackSelectionParameters; + private TrackSelectionParameters trackSelectionParameters; private DebugTextViewHelper debugViewHelper; - private TracksInfo lastSeenTracksInfo; + private Tracks lastSeenTracks; private boolean startAutoPlay; private int startItemIndex; private long startPosition; @@ -90,7 +90,12 @@ public class PlayerActivity extends AppCompatActivity // For ad playback only. @Nullable private AdsLoader clientSideAdsLoader; + + // TODO: Annotate this and serverSideAdsLoaderState below with @OptIn when it can be applied to + // fields (needs http://r.android.com/2004032 to be released into a version of + // androidx.annotation:annotation-experimental). @Nullable private ImaServerSideAdInsertionMediaSource.AdsLoader serverSideAdsLoader; + private ImaServerSideAdInsertionMediaSource.AdsLoader.@MonotonicNonNull State serverSideAdsLoaderState; @@ -113,22 +118,15 @@ public void onCreate(@Nullable Bundle savedInstanceState) { playerView.requestFocus(); if (savedInstanceState != null) { - // Restore as DefaultTrackSelector.Parameters in case ExoPlayer specific parameters were set. trackSelectionParameters = - DefaultTrackSelector.Parameters.CREATOR.fromBundle( + TrackSelectionParameters.fromBundle( savedInstanceState.getBundle(KEY_TRACK_SELECTION_PARAMETERS)); startAutoPlay = savedInstanceState.getBoolean(KEY_AUTO_PLAY); startItemIndex = savedInstanceState.getInt(KEY_ITEM_INDEX); startPosition = savedInstanceState.getLong(KEY_POSITION); - Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE); - if (adsLoaderStateBundle != null) { - serverSideAdsLoaderState = - ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle( - adsLoaderStateBundle); - } + restoreServerSideAdsLoaderState(savedInstanceState); } else { - trackSelectionParameters = - new DefaultTrackSelector.ParametersBuilder(/* context= */ this).build(); + trackSelectionParameters = new TrackSelectionParameters.Builder(/* context= */ this).build(); clearStartPosition(); } } @@ -145,7 +143,7 @@ public void onNewIntent(Intent intent) { @Override public void onStart() { super.onStart(); - if (Util.SDK_INT > 23) { + if (Build.VERSION.SDK_INT > 23) { initializePlayer(); if (playerView != null) { playerView.onResume(); @@ -156,7 +154,7 @@ public void onStart() { @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(); @@ -167,7 +165,7 @@ public void onResume() { @Override public void onPause() { super.onPause(); - if (Util.SDK_INT <= 23) { + if (Build.VERSION.SDK_INT <= 23) { if (playerView != null) { playerView.onPause(); } @@ -178,7 +176,7 @@ public void onPause() { @Override public void onStop() { super.onStop(); - if (Util.SDK_INT > 23) { + if (Build.VERSION.SDK_INT > 23) { if (playerView != null) { playerView.onPause(); } @@ -218,9 +216,7 @@ public void onSaveInstanceState(Bundle outState) { outState.putBoolean(KEY_AUTO_PLAY, startAutoPlay); outState.putInt(KEY_ITEM_INDEX, startItemIndex); outState.putLong(KEY_POSITION, startPosition); - if (serverSideAdsLoaderState != null) { - outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle()); - } + saveServerSideAdsLoaderState(outState); } // Activity input @@ -237,20 +233,20 @@ public boolean dispatchKeyEvent(KeyEvent event) { public void onClick(View view) { if (view == selectTracksButton && !isShowingTrackSelectionDialog - && TrackSelectionDialog.willHaveContent(trackSelector)) { + && TrackSelectionDialog.willHaveContent(player)) { isShowingTrackSelectionDialog = true; TrackSelectionDialog trackSelectionDialog = - TrackSelectionDialog.createForTrackSelector( - trackSelector, + TrackSelectionDialog.createForPlayer( + player, /* onDismissListener= */ dismissedDialog -> isShowingTrackSelectionDialog = false); trackSelectionDialog.show(getSupportFragmentManager(), /* tag= */ null); } } - // StyledPlayerControlView.VisibilityListener implementation + // StyledPlayerView.ControllerVisibilityListener implementation @Override - public void onVisibilityChange(int visibility) { + public void onVisibilityChanged(int visibility) { debugRootView.setVisibility(visibility); } @@ -260,7 +256,9 @@ protected void setContentView() { setContentView(R.layout.player_activity); } - /** @return Whether initialization was successful. */ + /** + * @return Whether initialization was successful. + */ protected boolean initializePlayer() { if (player == null) { Intent intent = getIntent(); @@ -270,26 +268,20 @@ protected boolean initializePlayer() { return false; } - boolean preferExtensionDecoders = - intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false); - RenderersFactory renderersFactory = - DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders); - - trackSelector = new DefaultTrackSelector(/* context= */ this); - lastSeenTracksInfo = TracksInfo.EMPTY; - player = + lastSeenTracks = Tracks.EMPTY; + ExoPlayer.Builder playerBuilder = new ExoPlayer.Builder(/* context= */ this) - .setRenderersFactory(renderersFactory) - .setMediaSourceFactory(createMediaSourceFactory()) - .setTrackSelector(trackSelector) - .build(); + .setMediaSourceFactory(createMediaSourceFactory()); + setRenderersFactory( + playerBuilder, intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false)); + player = playerBuilder.build(); player.setTrackSelectionParameters(trackSelectionParameters); player.addListener(new PlayerEventListener()); - player.addAnalyticsListener(new EventLogger(trackSelector)); + player.addAnalyticsListener(new EventLogger()); player.setAudioAttributes(AudioAttributes.DEFAULT, /* handleAudioFocus= */ true); player.setPlayWhenReady(startAutoPlay); playerView.setPlayer(player); - serverSideAdsLoader.setPlayer(player); + configurePlayerWithServerSideAdsLoader(); debugViewHelper = new DebugTextViewHelper(player, debugTextView); debugViewHelper.start(); } @@ -304,6 +296,10 @@ protected boolean initializePlayer() { } private MediaSource.Factory createMediaSourceFactory() { + DefaultDrmSessionManagerProvider drmSessionManagerProvider = + new DefaultDrmSessionManagerProvider(); + drmSessionManagerProvider.setDrmHttpDataSourceFactory( + DemoUtil.getHttpDataSourceFactory(/* context= */ this)); ImaServerSideAdInsertionMediaSource.AdsLoader.Builder serverSideAdLoaderBuilder = new ImaServerSideAdInsertionMediaSource.AdsLoader.Builder(/* context= */ this, playerView); if (serverSideAdsLoaderState != null) { @@ -312,13 +308,28 @@ private MediaSource.Factory createMediaSourceFactory() { serverSideAdsLoader = serverSideAdLoaderBuilder.build(); ImaServerSideAdInsertionMediaSource.Factory imaServerSideAdInsertionMediaSourceFactory = new ImaServerSideAdInsertionMediaSource.Factory( - serverSideAdsLoader, new DefaultMediaSourceFactory(dataSourceFactory)); - return new DefaultMediaSourceFactory(dataSourceFactory) - .setAdsLoaderProvider(this::getClientSideAdsLoader) - .setAdViewProvider(playerView) + serverSideAdsLoader, + new DefaultMediaSourceFactory(/* context= */ this) + .setDataSourceFactory(dataSourceFactory)); + return new DefaultMediaSourceFactory(/* context= */ this) + .setDataSourceFactory(dataSourceFactory) + .setDrmSessionManagerProvider(drmSessionManagerProvider) + .setLocalAdInsertionComponents( + this::getClientSideAdsLoader, /* adViewProvider= */ playerView) .setServerSideAdInsertionMediaSourceFactory(imaServerSideAdInsertionMediaSourceFactory); } + private void setRenderersFactory( + ExoPlayer.Builder playerBuilder, boolean preferExtensionDecoders) { + RenderersFactory renderersFactory = + DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders); + playerBuilder.setRenderersFactory(renderersFactory); + } + + private void configurePlayerWithServerSideAdsLoader() { + serverSideAdsLoader.setPlayer(player); + } + private List createMediaItems(Intent intent) { String action = intent.getAction(); boolean actionIsListView = IntentUtil.ACTION_VIEW_LIST.equals(action); @@ -345,7 +356,7 @@ private List createMediaItems(Intent intent) { 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(); @@ -372,8 +383,7 @@ protected void releasePlayer() { if (player != null) { updateTrackSelectorParameters(); updateStartPosition(); - serverSideAdsLoaderState = serverSideAdsLoader.release(); - serverSideAdsLoader = null; + releaseServerSideAdsLoader(); debugViewHelper.stop(); debugViewHelper = null; player.release(); @@ -388,6 +398,11 @@ protected void releasePlayer() { } } + private void releaseServerSideAdsLoader() { + serverSideAdsLoaderState = serverSideAdsLoader.release(); + serverSideAdsLoader = null; + } + private void releaseClientSideAdsLoader() { if (clientSideAdsLoader != null) { clientSideAdsLoader.release(); @@ -396,12 +411,24 @@ private void releaseClientSideAdsLoader() { } } + private void saveServerSideAdsLoaderState(Bundle outState) { + if (serverSideAdsLoaderState != null) { + outState.putBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE, serverSideAdsLoaderState.toBundle()); + } + } + + private void restoreServerSideAdsLoaderState(Bundle savedInstanceState) { + Bundle adsLoaderStateBundle = savedInstanceState.getBundle(KEY_SERVER_SIDE_ADS_LOADER_STATE); + if (adsLoaderStateBundle != null) { + serverSideAdsLoaderState = + ImaServerSideAdInsertionMediaSource.AdsLoader.State.CREATOR.fromBundle( + adsLoaderStateBundle); + } + } + private void updateTrackSelectorParameters() { if (player != null) { - // Until the demo app is fully migrated to TrackSelectionParameters, rely on ExoPlayer to use - // DefaultTrackSelector by default. - trackSelectionParameters = - (DefaultTrackSelector.Parameters) player.getTrackSelectionParameters(); + trackSelectionParameters = player.getTrackSelectionParameters(); } } @@ -422,8 +449,7 @@ protected void clearStartPosition() { // User controls private void updateButtonVisibility() { - selectTracksButton.setEnabled( - player != null && TrackSelectionDialog.willHaveContent(trackSelector)); + selectTracksButton.setEnabled(player != null && TrackSelectionDialog.willHaveContent(player)); } private void showControls() { @@ -461,20 +487,20 @@ public void onPlayerError(PlaybackException error) { @Override @SuppressWarnings("ReferenceEquality") - public void onTracksInfoChanged(TracksInfo tracksInfo) { + public void onTracksChanged(Tracks tracks) { updateButtonVisibility(); - if (tracksInfo == lastSeenTracksInfo) { + if (tracks == lastSeenTracks) { return; } - if (!tracksInfo.isTypeSupportedOrEmpty( - 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.isTypeSupportedOrEmpty( - 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; } } @@ -513,29 +539,31 @@ public Pair getErrorMessage(PlaybackException e) { 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; } + + 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/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index b79a7a62ca5..e47c2734554 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -15,9 +15,9 @@ */ package com.google.android.exoplayer2.demo; -import static com.google.android.exoplayer2.util.Assertions.checkArgument; -import static com.google.android.exoplayer2.util.Assertions.checkNotNull; -import static com.google.android.exoplayer2.util.Assertions.checkState; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; import android.content.Context; import android.content.Intent; @@ -27,6 +27,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.text.TextUtils; import android.util.JsonReader; import android.view.Menu; import android.view.MenuInflater; @@ -53,6 +54,7 @@ import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; +import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; @@ -116,8 +118,11 @@ public void onCreate(Bundle savedInstanceState) { useExtensionRenderers = DemoUtil.useExtensionRenderers(); downloadTracker = DemoUtil.getDownloadTracker(/* context= */ this); loadSample(); + startDownloadService(); + } - // Start the download service if it should be running but it's not currently. + /** Start the download service if it should be running but it's not currently. */ + 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 // (e.g. if device screen is locked). @@ -436,7 +441,10 @@ private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) thro } else { @Nullable String adaptiveMimeType = - Util.getAdaptiveMimeTypeForContentType(Util.inferContentType(uri, extension)); + Util.getAdaptiveMimeTypeForContentType( + TextUtils.isEmpty(extension) + ? Util.inferContentType(uri) + : Util.inferContentTypeForExtension(extension)); mediaItem .setUri(uri) .setMediaMetadata(new MediaMetadata.Builder().setTitle(title).build()) @@ -447,7 +455,7 @@ private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) thro new MediaItem.DrmConfiguration.Builder(drmUuid) .setLicenseUri(drmLicenseUri) .setLicenseRequestHeaders(drmLicenseRequestHeaders) - .forceSessionsForAudioAndVideoTracks(drmSessionForClearContent) + .setForceSessionsForAudioAndVideoTracks(drmSessionForClearContent) .setMultiSession(drmMultiSession) .setForceDefaultLicenseUri(drmForceDefaultLicenseUri) .build()); @@ -481,7 +489,7 @@ private PlaylistHolder readEntry(JsonReader reader, boolean insidePlaylist) thro 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/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java index 00d90bbcf5d..0923a64547a 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java @@ -32,20 +32,40 @@ import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.source.TrackGroupArray; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; -import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.Tracks; +import com.google.android.exoplayer2.source.TrackGroup; +import com.google.android.exoplayer2.trackselection.TrackSelectionOverride; +import com.google.android.exoplayer2.trackselection.TrackSelectionParameters; import com.google.android.exoplayer2.ui.TrackSelectionView; -import com.google.android.exoplayer2.util.Assertions; import com.google.android.material.tabs.TabLayout; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** Dialog to select tracks. */ public final class TrackSelectionDialog extends DialogFragment { + /** Called when tracks are selected. */ + public interface TrackSelectionListener { + + /** + * Called when tracks are selected. + * + * @param trackSelectionParameters A {@link TrackSelectionParameters} representing the selected + * tracks. Any manual selections are defined by {@link + * TrackSelectionParameters#disabledTrackTypes} and {@link + * TrackSelectionParameters#overrides}. + */ + void onTracksSelected(TrackSelectionParameters trackSelectionParameters); + } + + public static final ImmutableList SUPPORTED_TRACK_TYPES = + ImmutableList.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO, C.TRACK_TYPE_TEXT); + private final SparseArray tabFragments; private final ArrayList tabTrackTypes; @@ -55,20 +75,19 @@ public final class TrackSelectionDialog extends DialogFragment { /** * Returns whether a track selection dialog will have content to display if initialized with the - * specified {@link DefaultTrackSelector} in its current state. + * specified {@link Player}. */ - public static boolean willHaveContent(DefaultTrackSelector trackSelector) { - MappedTrackInfo mappedTrackInfo = trackSelector.getCurrentMappedTrackInfo(); - return mappedTrackInfo != null && willHaveContent(mappedTrackInfo); + public static boolean willHaveContent(Player player) { + return willHaveContent(player.getCurrentTracks()); } /** * Returns whether a track selection dialog will have content to display if initialized with the - * specified {@link MappedTrackInfo}. + * specified {@link Tracks}. */ - public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) { - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - if (showTabForRenderer(mappedTrackInfo, i)) { + public static boolean willHaveContent(Tracks tracks) { + for (Tracks.Group trackGroup : tracks.getGroups()) { + if (SUPPORTED_TRACK_TYPES.contains(trackGroup.getType())) { return true; } } @@ -76,78 +95,67 @@ public static boolean willHaveContent(MappedTrackInfo mappedTrackInfo) { } /** - * Creates a dialog for a given {@link DefaultTrackSelector}, whose parameters will be - * automatically updated when tracks are selected. + * Creates a dialog for a given {@link Player}, whose parameters will be automatically updated + * when tracks are selected. * - * @param trackSelector The {@link DefaultTrackSelector}. + * @param player The {@link Player}. * @param onDismissListener A {@link DialogInterface.OnDismissListener} to call when the dialog is * dismissed. */ - public static TrackSelectionDialog createForTrackSelector( - DefaultTrackSelector trackSelector, DialogInterface.OnDismissListener onDismissListener) { - MappedTrackInfo mappedTrackInfo = - Assertions.checkNotNull(trackSelector.getCurrentMappedTrackInfo()); - TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); - DefaultTrackSelector.Parameters parameters = trackSelector.getParameters(); - trackSelectionDialog.init( - /* titleId= */ R.string.track_selection_title, - mappedTrackInfo, - /* initialParameters = */ parameters, + public static TrackSelectionDialog createForPlayer( + Player player, DialogInterface.OnDismissListener onDismissListener) { + return createForTracksAndParameters( + R.string.track_selection_title, + player.getCurrentTracks(), + player.getTrackSelectionParameters(), /* allowAdaptiveSelections= */ true, /* allowMultipleOverrides= */ false, - /* onClickListener= */ (dialog, which) -> { - DefaultTrackSelector.ParametersBuilder builder = parameters.buildUpon(); - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - builder - .clearSelectionOverrides(/* rendererIndex= */ i) - .setRendererDisabled( - /* rendererIndex= */ i, - trackSelectionDialog.getIsDisabled(/* rendererIndex= */ i)); - List overrides = - trackSelectionDialog.getOverrides(/* rendererIndex= */ i); - if (!overrides.isEmpty()) { - builder.setSelectionOverride( - /* rendererIndex= */ i, - mappedTrackInfo.getTrackGroups(/* rendererIndex= */ i), - overrides.get(0)); - } - } - trackSelector.setParameters(builder); - }, + player::setTrackSelectionParameters, onDismissListener); - return trackSelectionDialog; } /** - * Creates a dialog for given {@link MappedTrackInfo} and {@link DefaultTrackSelector.Parameters}. + * Creates a dialog for given {@link Tracks} and {@link TrackSelectionParameters}. * * @param titleId The resource id of the dialog title. - * @param mappedTrackInfo The {@link MappedTrackInfo} to display. - * @param initialParameters The {@link DefaultTrackSelector.Parameters} describing the initial - * track selection. + * @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. * @param allowMultipleOverrides Whether tracks from multiple track groups can be selected. - * @param onClickListener {@link DialogInterface.OnClickListener} called when tracks are selected. + * @param trackSelectionListener Called when tracks are selected. * @param onDismissListener {@link DialogInterface.OnDismissListener} called when the dialog is * dismissed. */ - public static TrackSelectionDialog createForMappedTrackInfoAndParameters( + public static TrackSelectionDialog createForTracksAndParameters( int titleId, - MappedTrackInfo mappedTrackInfo, - DefaultTrackSelector.Parameters initialParameters, + Tracks tracks, + TrackSelectionParameters trackSelectionParameters, boolean allowAdaptiveSelections, boolean allowMultipleOverrides, - DialogInterface.OnClickListener onClickListener, + TrackSelectionListener trackSelectionListener, DialogInterface.OnDismissListener onDismissListener) { TrackSelectionDialog trackSelectionDialog = new TrackSelectionDialog(); trackSelectionDialog.init( + tracks, + trackSelectionParameters, titleId, - mappedTrackInfo, - initialParameters, allowAdaptiveSelections, allowMultipleOverrides, - onClickListener, + /* onClickListener= */ (dialog, which) -> { + TrackSelectionParameters.Builder builder = trackSelectionParameters.buildUpon(); + for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) { + int trackType = SUPPORTED_TRACK_TYPES.get(i); + builder.setTrackTypeDisabled(trackType, trackSelectionDialog.getIsDisabled(trackType)); + builder.clearOverridesOfType(trackType); + Map overrides = + trackSelectionDialog.getOverrides(trackType); + for (TrackSelectionOverride override : overrides.values()) { + builder.addOverride(override); + } + } + trackSelectionListener.onTracksSelected(builder.build()); + }, onDismissListener); return trackSelectionDialog; } @@ -160,9 +168,9 @@ public TrackSelectionDialog() { } private void init( + Tracks tracks, + TrackSelectionParameters trackSelectionParameters, int titleId, - MappedTrackInfo mappedTrackInfo, - DefaultTrackSelector.Parameters initialParameters, boolean allowAdaptiveSelections, boolean allowMultipleOverrides, DialogInterface.OnClickListener onClickListener, @@ -170,45 +178,49 @@ private void init( this.titleId = titleId; this.onClickListener = onClickListener; this.onDismissListener = onDismissListener; - for (int i = 0; i < mappedTrackInfo.getRendererCount(); i++) { - if (showTabForRenderer(mappedTrackInfo, i)) { - int trackType = mappedTrackInfo.getRendererType(/* rendererIndex= */ i); - TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(i); + + for (int i = 0; i < SUPPORTED_TRACK_TYPES.size(); i++) { + @C.TrackType int trackType = SUPPORTED_TRACK_TYPES.get(i); + ArrayList trackGroups = new ArrayList<>(); + for (Tracks.Group trackGroup : tracks.getGroups()) { + if (trackGroup.getType() == trackType) { + trackGroups.add(trackGroup); + } + } + if (!trackGroups.isEmpty()) { TrackSelectionViewFragment tabFragment = new TrackSelectionViewFragment(); tabFragment.init( - mappedTrackInfo, - /* rendererIndex= */ i, - initialParameters.getRendererDisabled(/* rendererIndex= */ i), - initialParameters.getSelectionOverride(/* rendererIndex= */ i, trackGroupArray), + trackGroups, + trackSelectionParameters.disabledTrackTypes.contains(trackType), + trackSelectionParameters.overrides, allowAdaptiveSelections, allowMultipleOverrides); - tabFragments.put(i, tabFragment); + tabFragments.put(trackType, tabFragment); tabTrackTypes.add(trackType); } } } /** - * Returns whether a renderer is disabled. + * Returns whether the disabled option is selected for the specified track type. * - * @param rendererIndex Renderer index. - * @return Whether the renderer is disabled. + * @param trackType The track type. + * @return Whether the disabled option is selected for the track type. */ - public boolean getIsDisabled(int rendererIndex) { - TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); - return rendererView != null && rendererView.isDisabled; + public boolean getIsDisabled(int trackType) { + TrackSelectionViewFragment trackView = tabFragments.get(trackType); + return trackView != null && trackView.isDisabled; } /** - * Returns the list of selected track selection overrides for the specified renderer. There will - * be at most one override for each track group. + * Returns the selected track overrides for the specified track type. * - * @param rendererIndex Renderer index. - * @return The list of track selection overrides for this renderer. + * @param trackType The track type. + * @return The track overrides for the track type. */ - public List getOverrides(int rendererIndex) { - TrackSelectionViewFragment rendererView = tabFragments.get(rendererIndex); - return rendererView == null ? Collections.emptyList() : rendererView.overrides; + public Map getOverrides(int trackType) { + TrackSelectionViewFragment trackView = tabFragments.get(trackType); + return trackView == null ? Collections.emptyMap() : trackView.overrides; } @Override @@ -248,27 +260,7 @@ public View onCreateView( return dialogView; } - private static boolean showTabForRenderer(MappedTrackInfo mappedTrackInfo, int rendererIndex) { - TrackGroupArray trackGroupArray = mappedTrackInfo.getTrackGroups(rendererIndex); - if (trackGroupArray.length == 0) { - return false; - } - int trackType = mappedTrackInfo.getRendererType(rendererIndex); - return isSupportedTrackType(trackType); - } - - private static boolean isSupportedTrackType(int trackType) { - switch (trackType) { - case C.TRACK_TYPE_VIDEO: - case C.TRACK_TYPE_AUDIO: - case C.TRACK_TYPE_TEXT: - return true; - default: - return false; - } - } - - private static String getTrackTypeString(Resources resources, int trackType) { + private static String getTrackTypeString(Resources resources, @C.TrackType int trackType) { switch (trackType) { case C.TRACK_TYPE_VIDEO: return resources.getString(R.string.exo_track_selection_title_video); @@ -289,12 +281,12 @@ public FragmentAdapter(FragmentManager fragmentManager) { @Override public Fragment getItem(int position) { - return tabFragments.valueAt(position); + return tabFragments.get(tabTrackTypes.get(position)); } @Override public int getCount() { - return tabFragments.size(); + return tabTrackTypes.size(); } @Override @@ -307,13 +299,12 @@ public CharSequence getPageTitle(int position) { public static final class TrackSelectionViewFragment extends Fragment implements TrackSelectionView.TrackSelectionListener { - private MappedTrackInfo mappedTrackInfo; - private int rendererIndex; + private List trackGroups; private boolean allowAdaptiveSelections; private boolean allowMultipleOverrides; /* package */ boolean isDisabled; - /* package */ List overrides; + /* package */ Map overrides; public TrackSelectionViewFragment() { // Retain instance across activity re-creation to prevent losing access to init data. @@ -321,21 +312,20 @@ public TrackSelectionViewFragment() { } public void init( - MappedTrackInfo mappedTrackInfo, - int rendererIndex, - boolean initialIsDisabled, - @Nullable SelectionOverride initialOverride, + List trackGroups, + boolean isDisabled, + Map overrides, boolean allowAdaptiveSelections, boolean allowMultipleOverrides) { - this.mappedTrackInfo = mappedTrackInfo; - this.rendererIndex = rendererIndex; - this.isDisabled = initialIsDisabled; - this.overrides = - initialOverride == null - ? Collections.emptyList() - : Collections.singletonList(initialOverride); + this.trackGroups = trackGroups; + this.isDisabled = isDisabled; this.allowAdaptiveSelections = allowAdaptiveSelections; this.allowMultipleOverrides = allowMultipleOverrides; + // TrackSelectionView does this filtering internally, but we need to do it here as well to + // handle the case where the TrackSelectionView is never created. + this.overrides = + new HashMap<>( + TrackSelectionView.filterOverrides(overrides, trackGroups, allowMultipleOverrides)); } @Override @@ -351,8 +341,7 @@ public View onCreateView( trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); trackSelectionView.init( - mappedTrackInfo, - rendererIndex, + trackGroups, isDisabled, overrides, /* trackFormatComparator= */ null, @@ -361,7 +350,8 @@ public View onCreateView( } @Override - public void onTrackSelectionChanged(boolean isDisabled, List overrides) { + public void onTrackSelectionChanged( + boolean isDisabled, Map overrides) { this.isDisabled = isDisabled; this.overrides = overrides; } diff --git a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java index 4433cf50e5b..749e0cea289 100644 --- a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java +++ b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java @@ -19,6 +19,7 @@ import android.content.Intent; import android.net.Uri; import android.os.Bundle; +import android.text.TextUtils; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceHolder; @@ -42,7 +43,6 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; -import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Util; import java.util.UUID; @@ -189,7 +189,7 @@ private void initializePlayer() { String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); - HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); + DataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSource.Factory(); HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); drmSessionManager = @@ -202,13 +202,18 @@ private void initializePlayer() { DataSource.Factory dataSourceFactory = new DefaultDataSource.Factory(this); MediaSource mediaSource; - @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); - if (type == C.TYPE_DASH) { + @Nullable String fileExtension = intent.getStringExtra(EXTENSION_EXTRA); + @C.ContentType + int type = + TextUtils.isEmpty(fileExtension) + ? Util.inferContentType(uri) + : Util.inferContentTypeForExtension(fileExtension); + if (type == C.CONTENT_TYPE_DASH) { mediaSource = new DashMediaSource.Factory(dataSourceFactory) .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) .createMediaSource(MediaItem.fromUri(uri)); - } else if (type == C.TYPE_OTHER) { + } else if (type == C.CONTENT_TYPE_OTHER) { mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory) .setDrmSessionManagerProvider(unusedMediaItem -> drmSessionManager) diff --git a/demos/transformer/BUILD.bazel b/demos/transformer/BUILD.bazel new file mode 100644 index 00000000000..dba4a653157 --- /dev/null +++ b/demos/transformer/BUILD.bazel @@ -0,0 +1,22 @@ +# Build targets for a demo MediaPipe graph. +# See README.md for instructions on using MediaPipe in the demo. + +load("//mediapipe/java/com/google/mediapipe:mediapipe_aar.bzl", "mediapipe_aar") +load( + "//mediapipe/framework/tool:mediapipe_graph.bzl", + "mediapipe_binary_graph", +) + +mediapipe_aar( + name = "edge_detector_mediapipe_aar", + calculators = [ + "//mediapipe/calculators/image:luminance_calculator", + "//mediapipe/calculators/image:sobel_edges_calculator", + ], +) + +mediapipe_binary_graph( + name = "edge_detector_binary_graph", + graph = "edge_detector_mediapipe_graph.pbtxt", + output_name = "edge_detector_mediapipe_graph.binarypb", +) diff --git a/demos/transformer/README.md b/demos/transformer/README.md index fb2657001e9..fd767ba6c88 100644 --- a/demos/transformer/README.md +++ b/demos/transformer/README.md @@ -6,4 +6,61 @@ example by removing audio or video. See the [demos README](../README.md) for instructions on how to build and run this demo. +## MediaPipe frame processing demo + +Building the demo app with [MediaPipe][] integration enabled requires some extra +manual steps. + +1. Follow the + [instructions](https://google.github.io/mediapipe/getting_started/install.html) + to install MediaPipe. +1. Copy the Transformer demo's build configuration and MediaPipe graph text + protocol buffer under the MediaPipe source tree. This makes it easy to + [build an AAR][] with bazel by reusing MediaPipe's workspace. + + ```sh + cd "" + MEDIAPIPE_ROOT="$(pwd)" + MEDIAPIPE_TRANSFORMER_ROOT="${MEDIAPIPE_ROOT}/mediapipe/java/com/google/mediapipe/transformer" + cd "" + TRANSFORMER_DEMO_ROOT="$(pwd)" + mkdir -p "${MEDIAPIPE_TRANSFORMER_ROOT}" + mkdir -p "${TRANSFORMER_DEMO_ROOT}/libs" + cp ${TRANSFORMER_DEMO_ROOT}/BUILD.bazel ${MEDIAPIPE_TRANSFORMER_ROOT}/BUILD + cp ${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets/edge_detector_mediapipe_graph.pbtxt \ + ${MEDIAPIPE_TRANSFORMER_ROOT} + ``` + +1. Build the AAR and the binary proto for the demo's MediaPipe graph, then copy + them to Transformer. + + ```sh + cd ${MEDIAPIPE_ROOT} + bazel build -c opt --strip=ALWAYS \ + --host_crosstool_top=@bazel_tools//tools/cpp:toolchain \ + --fat_apk_cpu=arm64-v8a,armeabi-v7a \ + --legacy_whole_archive=0 \ + --features=-legacy_whole_archive \ + --copt=-fvisibility=hidden \ + --copt=-ffunction-sections \ + --copt=-fdata-sections \ + --copt=-fstack-protector \ + --copt=-Oz \ + --copt=-fomit-frame-pointer \ + --copt=-DABSL_MIN_LOG_LEVEL=2 \ + --linkopt=-Wl,--gc-sections,--strip-all \ + mediapipe/java/com/google/mediapipe/transformer:edge_detector_mediapipe_aar.aar + cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_aar.aar \ + ${TRANSFORMER_DEMO_ROOT}/libs + bazel build mediapipe/java/com/google/mediapipe/transformer:edge_detector_binary_graph + cp bazel-bin/mediapipe/java/com/google/mediapipe/transformer/edge_detector_mediapipe_graph.binarypb \ + ${TRANSFORMER_DEMO_ROOT}/src/withMediaPipe/assets + ``` + +1. In Android Studio, gradle sync and select the `withMediaPipe` build variant + (this will only appear if the AAR is present), then build and run the demo + app and select a MediaPipe-based effect. + [Transformer]: https://exoplayer.dev/transforming-media.html +[MediaPipe]: https://google.github.io/mediapipe/ +[build an AAR]: https://google.github.io/mediapipe/getting_started/android_archive_library.html diff --git a/demos/transformer/build.gradle b/demos/transformer/build.gradle index 4501c01c8ba..3690b2f50f3 100644 --- a/demos/transformer/build.gradle +++ b/demos/transformer/build.gradle @@ -45,6 +45,27 @@ android { // This demo app isn't indexed and doesn't have translations. disable 'GoogleAppIndexingWarning','MissingTranslation' } + + flavorDimensions "mediaPipe" + + productFlavors { + noMediaPipe { + dimension "mediaPipe" + } + withMediaPipe { + dimension "mediaPipe" + } + } + + // Ignore the withMediaPipe variant if the MediaPipe AAR is not present. + if (!project.file("libs/edge_detector_mediapipe_aar.aar").exists()) { + variantFilter { variant -> + def names = variant.flavors*.name + if (names.contains("withMediaPipe")) { + setIgnore(true) + } + } + } } dependencies { @@ -56,6 +77,14 @@ dependencies { implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-transformer') implementation project(modulePrefix + 'library-ui') + + // For MediaPipe and its dependencies: + withMediaPipeImplementation fileTree(dir: 'libs', include: ['*.aar']) + withMediaPipeImplementation 'com.google.flogger:flogger:latest.release' + withMediaPipeImplementation 'com.google.flogger:flogger-system-backend:latest.release' + withMediaPipeImplementation 'com.google.code.findbugs:jsr305:latest.release' + withMediaPipeImplementation 'com.google.protobuf:protobuf-javalite:3.19.1' } 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 00000000000..90ff827132b --- /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 00000000000..55dea952a5c --- /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/library/transformer/src/main/assets/shaders/vertex_shader_transformation_es3.glsl b/demos/transformer/src/main/assets/vertex_shader_copy_es2.glsl similarity index 70% rename from library/transformer/src/main/assets/shaders/vertex_shader_transformation_es3.glsl rename to demos/transformer/src/main/assets/vertex_shader_copy_es2.glsl index 9af4dcecd63..c603e252aca 100644 --- a/library/transformer/src/main/assets/shaders/vertex_shader_transformation_es3.glsl +++ b/demos/transformer/src/main/assets/vertex_shader_copy_es2.glsl @@ -1,4 +1,4 @@ -#version 300 es +#version 100 // Copyright 2022 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -12,12 +12,12 @@ // 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. -in vec4 aFramePosition; -in vec4 aTexCoords; -uniform mat4 uTexTransform; -uniform mat4 uTransformationMatrix; -out vec2 vTexCoords; + +// ES 2 vertex shader that leaves the coordinates unchanged. + +attribute vec4 aFramePosition; +varying vec2 vTexSamplingCoord; void main() { - gl_Position = uTransformationMatrix * aFramePosition; - vTexCoords = (uTexTransform * aTexCoords).xy; + gl_Position = aFramePosition; + vTexSamplingCoord = vec2(aFramePosition.x * 0.5 + 0.5, aFramePosition.y * 0.5 + 0.5); } diff --git a/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/BitmapOverlayProcessor.java b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/BitmapOverlayProcessor.java new file mode 100644 index 00000000000..01cab43d8a1 --- /dev/null +++ b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/BitmapOverlayProcessor.java @@ -0,0 +1,168 @@ +/* + * 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 com.google.android.exoplayer2.transformerdemo; + +import static com.google.android.exoplayer2.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 com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.transformer.FrameProcessingException; +import com.google.android.exoplayer2.transformer.SingleFrameGlTextureProcessor; +import com.google.android.exoplayer2.util.GlProgram; +import com.google.android.exoplayer2.util.GlUtil; +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 + * frame. + * + *

The bitmap is drawn using an Android {@link Canvas}. + */ +// 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; + } + + 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 BitmapOverlayProcessor() { + 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.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); + } + + @Override + public void drawFrame(long presentationTimeUs) throws FrameProcessingException { + try { + checkStateNotNull(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); + GlUtil.checkGlError(); + } catch (GlUtil.GlException e) { + throw new FrameProcessingException(e); + } + } + + @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/com/google/android/exoplayer2/transformerdemo/ConfigurationActivity.java b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/ConfigurationActivity.java index 3381935df45..d659738ebfd 100644 --- a/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/ConfigurationActivity.java @@ -32,7 +32,11 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.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; @@ -49,39 +53,84 @@ public final class ConfigurationActivity extends AppCompatActivity { public static final String AUDIO_MIME_TYPE = "audio_mime_type"; public static final String VIDEO_MIME_TYPE = "video_mime_type"; public static final String RESOLUTION_HEIGHT = "resolution_height"; - public static final String TRANSLATE_X = "translate_x"; - public static final String TRANSLATE_Y = "translate_y"; public static final String SCALE_X = "scale_x"; public static final String SCALE_Y = "scale_y"; public static final String ROTATE_DEGREES = "rotate_degrees"; + public static final String TRIM_START_MS = "trim_start_ms"; + public static final String TRIM_END_MS = "trim_end_ms"; + public static final String ENABLE_FALLBACK = "enable_fallback"; + public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping"; public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; + public static final String DEMO_EFFECTS_SELECTIONS = "demo_effects_selections"; + public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x"; + public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y"; + public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius"; + public static final String PERIODIC_VIGNETTE_OUTER_RADIUS = "periodic_vignette_outer_radius"; private static final String[] INPUT_URIS = { - "https://html5demos.com/assets/dizzy.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4", "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4", - "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4", + "https://html5demos.com/assets/dizzy.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/1920w_1080h_4s.mp4", + "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.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/gen/screens/dash-vod-single-segment/manifest-baseline.mpd", + "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", - "MP4 with H265 video and AAC audio", - "Long MP4 with H264 video and AAC audio", - "WebM with VP8 video and Vorbis audio", + "720p H264 video and AAC audio", + "1080p H265 video and AAC audio", + "360p H264 video and AAC audio", + "360p VP8 video and Vorbis audio", + "4K H264 video and AAC audio (portrait, no B-frames)", + "8k H265 video and AAC audio", + "Short 1080p H265 video and AAC audio", + "Long 180p H264 video and AAC audio", + "H264 video and AAC audio (portrait, H > W, 0\u00B0)", + "H264 video and AAC audio (portrait, H < W, 90\u00B0)", + "SEF slow motion with 240 fps", + "480p DASH (non-square pixels)", + "HDR (HDR10) H265 video (encoding may fail)", + }; + private static final String[] DEMO_EFFECTS = { + "Dizzy crop", + "Edge detector (Media Pipe)", + "Periodic vignette", + "3D spin", + "Overlay logo & timer", + "Zoom in start", }; + private static final int PERIODIC_VIGNETTE_INDEX = 2; 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; private @MonotonicNonNull Spinner audioMimeSpinner; private @MonotonicNonNull Spinner videoMimeSpinner; private @MonotonicNonNull Spinner resolutionHeightSpinner; - private @MonotonicNonNull Spinner translateSpinner; private @MonotonicNonNull Spinner scaleSpinner; private @MonotonicNonNull Spinner rotateSpinner; + private @MonotonicNonNull CheckBox trimCheckBox; + private @MonotonicNonNull CheckBox enableFallbackCheckBox; + private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; + private @MonotonicNonNull Button selectDemoEffectsButton; + private boolean @MonotonicNonNull [] demoEffectsSelections; private int inputUriPosition; + private long trimStartMs; + private long trimEndMs; + private float periodicVignetteCenterX; + private float periodicVignetteCenterY; + private float periodicVignetteInnerRadius; + private float periodicVignetteOuterRadius; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -90,11 +139,11 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { 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); @@ -118,11 +167,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { videoMimeSpinner = findViewById(R.id.video_mime_spinner); videoMimeSpinner.setAdapter(videoMimeAdapter); videoMimeAdapter.addAll( - SAME_AS_INPUT_OPTION, - MimeTypes.VIDEO_H263, - MimeTypes.VIDEO_H264, - MimeTypes.VIDEO_H265, - MimeTypes.VIDEO_MP4V); + SAME_AS_INPUT_OPTION, MimeTypes.VIDEO_H263, MimeTypes.VIDEO_H264, MimeTypes.VIDEO_MP4V); + if (Util.SDK_INT >= 24) { + videoMimeAdapter.add(MimeTypes.VIDEO_H265); + } ArrayAdapter resolutionHeightAdapter = new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); @@ -132,14 +180,6 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { resolutionHeightAdapter.addAll( SAME_AS_INPUT_OPTION, "144", "240", "360", "480", "720", "1080", "1440", "2160"); - ArrayAdapter translateAdapter = - new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); - translateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - translateSpinner = findViewById(R.id.translate_spinner); - translateSpinner.setAdapter(translateAdapter); - translateAdapter.addAll( - SAME_AS_INPUT_OPTION, "-.1, -.1", "0, 0", ".5, 0", "0, .5", "1, 1", "1.9, 0", "0, 1.9"); - ArrayAdapter scaleAdapter = new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); scaleAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); @@ -152,9 +192,22 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { rotateAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); rotateSpinner = findViewById(R.id.rotate_spinner); rotateSpinner.setAdapter(rotateAdapter); - rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "90", "180"); + rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180"); + + trimCheckBox = findViewById(R.id.trim_checkbox); + trimCheckBox.setOnCheckedChangeListener(this::selectTrimBounds); + trimStartMs = C.TIME_UNSET; + trimEndMs = C.TIME_UNSET; + 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); + + demoEffectsSelections = new boolean[DEMO_EFFECTS.length]; + selectDemoEffectsButton = findViewById(R.id.select_demo_effects_button); + selectDemoEffectsButton.setOnClickListener(this::selectDemoEffects); } @Override @@ -162,8 +215,8 @@ protected void onResume() { 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()); } } @@ -180,13 +233,16 @@ protected void onNewIntent(Intent intent) { "audioMimeSpinner", "videoMimeSpinner", "resolutionHeightSpinner", - "translateSpinner", "scaleSpinner", "rotateSpinner", - "enableHdrEditingCheckBox" + "trimCheckBox", + "enableFallbackCheckBox", + "enableRequestSdrToneMappingCheckBox", + "enableHdrEditingCheckBox", + "demoEffectsSelections" }) private void startTransformation(View view) { - Intent transformerIntent = new Intent(this, TransformerActivity.class); + Intent transformerIntent = new Intent(/* packageContext= */ this, TransformerActivity.class); Bundle bundle = new Bundle(); bundle.putBoolean(SHOULD_REMOVE_AUDIO, removeAudioCheckbox.isChecked()); bundle.putBoolean(SHOULD_REMOVE_VIDEO, removeVideoCheckbox.isChecked()); @@ -203,13 +259,6 @@ private void startTransformation(View view) { if (!SAME_AS_INPUT_OPTION.equals(selectedResolutionHeight)) { bundle.putInt(RESOLUTION_HEIGHT, Integer.parseInt(selectedResolutionHeight)); } - String selectedTranslate = String.valueOf(translateSpinner.getSelectedItem()); - if (!SAME_AS_INPUT_OPTION.equals(selectedTranslate)) { - List translateXY = Arrays.asList(selectedTranslate.split(", ")); - checkState(translateXY.size() == 2); - bundle.putFloat(TRANSLATE_X, Float.parseFloat(translateXY.get(0))); - bundle.putFloat(TRANSLATE_Y, Float.parseFloat(translateXY.get(1))); - } String selectedScale = String.valueOf(scaleSpinner.getSelectedItem()); if (!SAME_AS_INPUT_OPTION.equals(selectedScale)) { List scaleXY = Arrays.asList(selectedScale.split(", ")); @@ -221,7 +270,19 @@ private void startTransformation(View view) { if (!SAME_AS_INPUT_OPTION.equals(selectedRotate)) { bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate)); } + if (trimCheckBox.isChecked()) { + bundle.putLong(TRIM_START_MS, trimStartMs); + bundle.putLong(TRIM_END_MS, trimEndMs); + } + 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_EFFECTS_SELECTIONS, demoEffectsSelections); + 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(); @@ -231,19 +292,82 @@ private void startTransformation(View view) { 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 selectDemoEffects(View view) { + new AlertDialog.Builder(/* context= */ this) + .setTitle(R.string.select_demo_effects) + .setMultiChoiceItems( + DEMO_EFFECTS, checkNotNull(demoEffectsSelections), this::selectDemoEffect) + .setPositiveButton(android.R.string.ok, /* listener= */ null) + .create() + .show(); + } + + private void selectTrimBounds(View view, boolean isChecked) { + if (!isChecked) { + return; + } + View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null); + RangeSlider radiusRangeSlider = + checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider)); + radiusRangeSlider.setValues(0f, 60f); // seconds + new AlertDialog.Builder(/* context= */ this) + .setView(dialogView) + .setPositiveButton( + android.R.string.ok, + (DialogInterface dialogInterface, int i) -> { + List radiusRange = radiusRangeSlider.getValues(); + trimStartMs = 1000 * radiusRange.get(0).longValue(); + trimEndMs = 1000 * radiusRange.get(1).longValue(); + }) + .create() + .show(); + } + + @RequiresNonNull("selectedFileTextView") private void selectFileInDialog(DialogInterface dialog, int which) { inputUriPosition = which; - chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + } + + @RequiresNonNull("demoEffectsSelections") + private void selectDemoEffect(DialogInterface dialog, int which, boolean isChecked) { + demoEffectsSelections[which] = isChecked; + if (!isChecked || which != PERIODIC_VIGNETTE_INDEX) { + 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({ @@ -251,10 +375,11 @@ private void selectFileInDialog(DialogInterface dialog, int which) { "audioMimeSpinner", "videoMimeSpinner", "resolutionHeightSpinner", - "translateSpinner", "scaleSpinner", "rotateSpinner", - "enableHdrEditingCheckBox" + "enableRequestSdrToneMappingCheckBox", + "enableHdrEditingCheckBox", + "selectDemoEffectsButton" }) private void onRemoveAudio(View view) { if (((CheckBox) view).isChecked()) { @@ -270,10 +395,11 @@ private void onRemoveAudio(View view) { "audioMimeSpinner", "videoMimeSpinner", "resolutionHeightSpinner", - "translateSpinner", "scaleSpinner", "rotateSpinner", - "enableHdrEditingCheckBox" + "enableRequestSdrToneMappingCheckBox", + "enableHdrEditingCheckBox", + "selectDemoEffectsButton" }) private void onRemoveVideo(View view) { if (((CheckBox) view).isChecked()) { @@ -288,26 +414,34 @@ private void onRemoveVideo(View view) { "audioMimeSpinner", "videoMimeSpinner", "resolutionHeightSpinner", - "translateSpinner", "scaleSpinner", "rotateSpinner", - "enableHdrEditingCheckBox" + "enableRequestSdrToneMappingCheckBox", + "enableHdrEditingCheckBox", + "selectDemoEffectsButton" }) private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { audioMimeSpinner.setEnabled(isAudioEnabled); videoMimeSpinner.setEnabled(isVideoEnabled); resolutionHeightSpinner.setEnabled(isVideoEnabled); - translateSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled); + enableRequestSdrToneMappingCheckBox.setEnabled( + isRequestSdrToneMappingSupported() && isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled); + selectDemoEffectsButton.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.translate).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/com/google/android/exoplayer2/transformerdemo/MatrixTransformationFactory.java b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/MatrixTransformationFactory.java new file mode 100644 index 00000000000..93a993c812a --- /dev/null +++ b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/MatrixTransformationFactory.java @@ -0,0 +1,93 @@ +/* + * 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 com.google.android.exoplayer2.transformerdemo; + +import android.graphics.Matrix; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.transformer.GlMatrixTransformation; +import com.google.android.exoplayer2.transformer.MatrixTransformation; +import com.google.android.exoplayer2.util.Util; + +/** + * Factory for {@link GlMatrixTransformation GlMatrixTransformations} and {@link + * MatrixTransformation MatrixTransformations} that create video effects by applying transformation + * matrices to the individual video frames. + */ +/* package */ final class MatrixTransformationFactory { + /** + * Returns a {@link MatrixTransformation} 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 MatrixTransformation createZoomInTransition() { + return MatrixTransformationFactory::calculateZoomInTransitionMatrix; + } + + /** + * Returns a {@link MatrixTransformation} that crops frames to a rectangle that moves on an + * ellipse. + */ + public static MatrixTransformation createDizzyCropEffect() { + return MatrixTransformationFactory::calculateDizzyCropMatrix; + } + + /** + * Returns a {@link GlMatrixTransformation} that rotates a frame in 3D around the y-axis and + * applies perspective projection to 2D. + */ + public static GlMatrixTransformation createSpin3dEffect() { + return MatrixTransformationFactory::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/com/google/android/exoplayer2/transformerdemo/PeriodicVignetteProcessor.java b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/PeriodicVignetteProcessor.java new file mode 100644 index 00000000000..3ea704ae513 --- /dev/null +++ b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/PeriodicVignetteProcessor.java @@ -0,0 +1,123 @@ +/* + * 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 com.google.android.exoplayer2.transformerdemo; + +import static com.google.android.exoplayer2.util.Assertions.checkArgument; +import static com.google.android.exoplayer2.util.Assertions.checkStateNotNull; + +import android.content.Context; +import android.opengl.GLES20; +import android.util.Size; +import com.google.android.exoplayer2.transformer.FrameProcessingException; +import com.google.android.exoplayer2.transformer.SingleFrameGlTextureProcessor; +import com.google.android.exoplayer2.util.GlProgram; +import com.google.android.exoplayer2.util.GlUtil; +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; + } + + 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 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 PeriodicVignetteProcessor( + 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.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(outputSize); + } + + @Override + public void drawFrame(long presentationTimeUs) throws FrameProcessingException { + try { + 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); + } catch (GlUtil.GlException e) { + throw new FrameProcessingException(e); + } + } + + @Override + public void release() { + if (glProgram != null) { + glProgram.delete(); + } + } +} diff --git a/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/TransformerActivity.java b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/TransformerActivity.java index 6dd0a7ff620..2e4d6512012 100644 --- a/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/TransformerActivity.java +++ b/demos/transformer/src/main/java/com/google/android/exoplayer2/transformerdemo/TransformerActivity.java @@ -21,7 +21,6 @@ import android.app.Activity; import android.content.Intent; import android.content.pm.PackageManager; -import android.graphics.Matrix; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -32,13 +31,19 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.appcompat.app.AppCompatActivity; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.transformer.DefaultEncoderFactory; +import com.google.android.exoplayer2.transformer.EncoderSelector; +import com.google.android.exoplayer2.transformer.GlEffect; import com.google.android.exoplayer2.transformer.ProgressHolder; +import com.google.android.exoplayer2.transformer.SingleFrameGlTextureProcessor; import com.google.android.exoplayer2.transformer.TransformationException; import com.google.android.exoplayer2.transformer.TransformationRequest; +import com.google.android.exoplayer2.transformer.TransformationResult; import com.google.android.exoplayer2.transformer.Transformer; import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.StyledPlayerView; @@ -48,8 +53,10 @@ 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.lang.reflect.Constructor; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; @@ -145,9 +152,10 @@ private void startTransformation() { externalCacheFile = createExternalCacheFile("transformer-output.mp4"); String filePath = externalCacheFile.getAbsolutePath(); @Nullable Bundle bundle = intent.getExtras(); + MediaItem mediaItem = createMediaItem(bundle, uri); Transformer transformer = createTransformer(bundle, filePath); transformationStopwatch.start(); - transformer.startTransformation(MediaItem.fromUri(uri), filePath); + transformer.startTransformation(mediaItem, filePath); this.transformer = transformer; } catch (IOException e) { throw new IllegalStateException(e); @@ -174,6 +182,24 @@ public void run() { }); } + private MediaItem createMediaItem(@Nullable Bundle bundle, Uri uri) { + MediaItem.Builder mediaItemBuilder = new MediaItem.Builder().setUri(uri); + if (bundle != null) { + long trimStartMs = + bundle.getLong(ConfigurationActivity.TRIM_START_MS, /* defaultValue= */ C.TIME_UNSET); + long trimEndMs = + bundle.getLong(ConfigurationActivity.TRIM_END_MS, /* defaultValue= */ C.TIME_UNSET); + if (trimStartMs != C.TIME_UNSET && trimEndMs != C.TIME_UNSET) { + mediaItemBuilder.setClippingConfiguration( + new MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(trimStartMs) + .setEndPositionMs(trimEndMs) + .build()); + } + } + return mediaItemBuilder.build(); + } + // Create a cache file, resetting it if it already exists. private File createExternalCacheFile(String fileName) throws IOException { File file = new File(getExternalCacheDir(), fileName); @@ -214,22 +240,89 @@ private Transformer createTransformer(@Nullable Bundle bundle, String filePath) if (resolutionHeight != C.LENGTH_UNSET) { requestBuilder.setResolution(resolutionHeight); } - Matrix transformationMatrix = getTransformationMatrix(bundle); - if (!transformationMatrix.isIdentity()) { - requestBuilder.setTransformationMatrix(transformationMatrix); - } + + float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1); + float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1); + requestBuilder.setScale(scaleX, scaleY); + + float rotateDegrees = + 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 .setTransformationRequest(requestBuilder.build()) .setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO)) - .setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO)); + .setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO)) + .setEncoderFactory( + new DefaultEncoderFactory( + EncoderSelector.DEFAULT, + /* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))); + + ImmutableList.Builder effects = new ImmutableList.Builder<>(); + @Nullable + boolean[] selectedEffects = + bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS); + if (selectedEffects != null) { + if (selectedEffects[0]) { + effects.add(MatrixTransformationFactory.createDizzyCropEffect()); + } + if (selectedEffects[1]) { + try { + Class clazz = + Class.forName("com.google.android.exoplayer2.transformerdemo.MediaPipeProcessor"); + Constructor constructor = + clazz.getConstructor(String.class, String.class, String.class); + effects.add( + () -> { + try { + return (SingleFrameGlTextureProcessor) + constructor.newInstance( + /* graphName= */ "edge_detector_mediapipe_graph.binarypb", + /* inputStreamName= */ "input_video", + /* outputStreamName= */ "output_video"); + } catch (Exception e) { + runOnUiThread(() -> showToast(R.string.no_media_pipe_error)); + throw new RuntimeException("Failed to load MediaPipe processor", e); + } + }); + } catch (Exception e) { + showToast(R.string.no_media_pipe_error); + } + } + if (selectedEffects[2]) { + effects.add( + () -> + new PeriodicVignetteProcessor( + bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X), + bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y), + /* minInnerRadius= */ bundle.getFloat( + ConfigurationActivity.PERIODIC_VIGNETTE_INNER_RADIUS), + /* maxInnerRadius= */ bundle.getFloat( + ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS), + bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_OUTER_RADIUS))); + } + if (selectedEffects[3]) { + effects.add(MatrixTransformationFactory.createSpin3dEffect()); + } + if (selectedEffects[4]) { + effects.add(BitmapOverlayProcessor::new); + } + if (selectedEffects[5]) { + effects.add(MatrixTransformationFactory.createZoomInTransition()); + } + transformerBuilder.setVideoFrameEffects(effects.build()); + } } return transformerBuilder .addListener( new Transformer.Listener() { @Override - public void onTransformationCompleted(MediaItem mediaItem) { + public void onTransformationCompleted( + MediaItem mediaItem, TransformationResult transformationResult) { TransformerActivity.this.onTransformationCompleted(filePath); } @@ -243,26 +336,6 @@ public void onTransformationError( .build(); } - private static Matrix getTransformationMatrix(Bundle bundle) { - Matrix transformationMatrix = new Matrix(); - - float translateX = bundle.getFloat(ConfigurationActivity.TRANSLATE_X, /* defaultValue= */ 0); - float translateY = bundle.getFloat(ConfigurationActivity.TRANSLATE_Y, /* defaultValue= */ 0); - // TODO(b/213198690): Get resolution for aspect ratio and scale all translations' translateX - // by this aspect ratio. - transformationMatrix.postTranslate(translateX, translateY); - - float scaleX = bundle.getFloat(ConfigurationActivity.SCALE_X, /* defaultValue= */ 1); - float scaleY = bundle.getFloat(ConfigurationActivity.SCALE_Y, /* defaultValue= */ 1); - transformationMatrix.postScale(scaleX, scaleY); - - float rotateDegrees = - bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0); - transformationMatrix.postRotate(rotateDegrees); - - return transformationMatrix; - } - @RequiresNonNull({ "informationTextView", "progressViewGroup", @@ -335,6 +408,10 @@ private void requestTransformerPermission() { } } + private void showToast(@StringRes int messageResource) { + Toast.makeText(getApplicationContext(), getString(messageResource), Toast.LENGTH_LONG).show(); + } + private final class DemoDebugViewProvider implements Transformer.DebugViewProvider { @Nullable diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml index b50d5eaba0e..2879d6a637a 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" />