diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 97e431f75b1..62889430ef2 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.2 - 2.18.1 - 2.18.0 - 2.17.1 diff --git a/.gitignore b/.gitignore index 3ab16a94fdf..ce7cddb44b4 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,6 @@ extensions/cronet/jniLibs/* !extensions/cronet/jniLibs/README.md extensions/cronet/libs/* !extensions/cronet/libs/README.md + +# MIDI extension +extensions/midi/lib diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 388b2ed2599..b685679cef8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ all of the information requested in the issue template. ## Pull requests ## -We will also consider high quality pull requests. These should normally merge +We will also consider high quality pull requests. These should merge into the `dev-v2` branch. Before a pull request can be accepted you must submit a Contributor License Agreement, as described below. diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d8194abf8f4..ab5aec86e2f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,116 @@ # Release notes +### 2.18.2 (2022-11-22) + +This release corresponds to the +[AndroidX Media3 1.0.0-beta03 release](https://github.com/androidx/media/releases/tag/1.0.0-beta03). + +* Core library: + * Add `ExoPlayer.isTunnelingEnabled` to check if tunneling is enabled for + the currently selected tracks + ([#2518](https://github.com/google/ExoPlayer/issues/2518)). + * Add `WrappingMediaSource` to simplify wrapping a single `MediaSource` + ([#7279](https://github.com/google/ExoPlayer/issues/7279)). + * Discard back buffer before playback gets stuck due to insufficient + available memory. + * Close the Tracing "doSomeWork" block when offload is enabled. + * Fix session tracking problem with fast seeks in `PlaybackStatsListener` + ([#180](https://github.com/androidx/media/issues/180)). + * Send missing `onMediaItemTransition` callback when calling `seekToNext` + or `seekToPrevious` in a single-item playlist + ([#10667](https://github.com/google/ExoPlayer/issues/10667)). + * Add `Player.getSurfaceSize` that returns the size of the surface on + which the video is rendered. + * Fix bug where removing listeners during the player release can cause an + `IllegalStateException` + ([#10758](https://github.com/google/ExoPlayer/issues/10758)). +* Build: + * Enforce minimum `compileSdkVersion` to avoid compilation errors + ([#10684](https://github.com/google/ExoPlayer/issues/10684)). + * Avoid publishing block when included in another gradle build. +* Track selection: + * Prefer other tracks to Dolby Vision if display does not support it. + ([#8944](https://github.com/google/ExoPlayer/issues/8944)). +* Downloads: + * Fix potential infinite loop in `ProgressiveDownloader` caused by + simultaneous download and playback with the same `PriorityTaskManager` + ([#10570](https://github.com/google/ExoPlayer/pull/10570)). + * Make download notification appear immediately + ([#183](https://github.com/androidx/media/pull/183)). + * Limit parallel download removals to 1 to avoid excessive thread creation + ([#10458](https://github.com/google/ExoPlayer/issues/10458)). +* Video: + * Try alternative decoder for Dolby Vision if display does not support it. + ([#9794](https://github.com/google/ExoPlayer/issues/9794)). +* Audio: + * Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid + OutOfMemory errors when releasing multiple players at the same time + ([#10057](https://github.com/google/ExoPlayer/issues/10057)). + * Adds `AudioOffloadListener.onExperimentalOffloadedPlayback` for the + AudioTrack offload state. + ([#134](https://github.com/androidx/media/issues/134)). + * Make `AudioTrackBufferSizeProvider` a public interface. + * Add `ExoPlayer.setPreferredAudioDevice` to set the preferred audio + output device ([#135](https://github.com/androidx/media/issues/135)). + * Rename `com.google.android.exoplayer2.audio.AudioProcessor` to + `com.google.android.exoplayer2.audio.AudioProcessor`. + * Map 8-channel and 12-channel audio to the 7.1 and 7.1.4 channel masks + respectively on all Android versions + ([#10701](https://github.com/google/ExoPlayer/issues/10701)). +* Metadata: + * `MetadataRenderer` can now be configured to render metadata as soon as + they are available. Create an instance with + `MetadataRenderer(MetadataOutput, Looper, MetadataDecoderFactory, + boolean)` to specify whether the renderer will output metadata early or + in sync with the player position. +* DRM: + * Work around a bug in the Android 13 ClearKey implementation that returns + a non-empty but invalid license URL. + * Fix `setMediaDrmSession failed: session not opened` error when switching + between DRM schemes in a playlist (e.g. Widevine to ClearKey). +* Text: + * CEA-608: Ensure service switch commands on field 2 are handled correctly + ([#10666](https://github.com/google/ExoPlayer/issues/10666)). +* DASH: + * Parse `EventStream.presentationTimeOffset` from manifests + ([#10460](https://github.com/google/ExoPlayer/issues/10460)). +* UI: + * Use current overrides of the player as preset in + `TrackSelectionDialogBuilder` + ([#10429](https://github.com/google/ExoPlayer/issues/10429)). +* RTSP: + * Add H263 fragmented packet handling + ([#119](https://github.com/androidx/media/pull/119)). + * Add support for MP4A-LATM + ([#162](https://github.com/androidx/media/pull/162)). +* IMA: + * Add timeout for loading ad information to handle cases where the IMA SDK + gets stuck loading an ad + ([#10510](https://github.com/google/ExoPlayer/issues/10510)). + * Prevent skipping mid-roll ads when seeking to the end of the content + ([#10685](https://github.com/google/ExoPlayer/issues/10685)). + * Correctly calculate window duration for live streams with server-side + inserted ads, for example IMA DAI + ([#10764](https://github.com/google/ExoPlayer/issues/10764)). +* FFmpeg extension: + * Add newly required flags to link FFmpeg libraries with NDK 23.1.7779620 + and above ([#9933](https://github.com/google/ExoPlayer/issues/9933)). +* AV1 extension: + * Update CMake version to avoid incompatibilities with the latest Android + Studio releases + ([#9933](https://github.com/google/ExoPlayer/issues/9933)). +* Cast extension: + * Implement `getDeviceInfo()` to be able to identify `CastPlayer` when + controlling playback with a `MediaController` + ([#142](https://github.com/androidx/media/issues/142)). +* Transformer: + * Add muxer watchdog timer to detect when generating an output sample is + too slow. +* Remove deprecated symbols: + * Remove `Transformer.Builder.setOutputMimeType(String)`. This feature has + been removed. The MIME type will always be MP4 when the default muxer is + used. + ### 2.18.1 (2022-07-21) This release corresponds to the diff --git a/common_library_config.gradle b/common_library_config.gradle index 51773ca0e18..002502299b1 100644 --- a/common_library_config.gradle +++ b/common_library_config.gradle @@ -22,6 +22,9 @@ android { targetSdkVersion project.ext.targetSdkVersion consumerProguardFiles 'proguard-rules.txt' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + aarMetadata { + minCompileSdk = project.ext.compileSdkVersion + } } compileOptions { diff --git a/constants.gradle b/constants.gradle index e71175d1a49..3068e7646b5 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,15 +13,18 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.18.1' - releaseVersionCode = 2_018_001 + releaseVersion = '2.18.2' + releaseVersionCode = 2_018_002 minSdkVersion = 16 - appTargetSdkVersion = 29 + appTargetSdkVersion = 33 + // API version before restricting local file access. + // https://developer.android.com/training/data-storage/app-specific + mainDemoAppTargetSdkVersion = 29 // Upgrading this requires [Internal ref: b/193254928] to be fixed, or some // additional robolectric config. targetSdkVersion = 30 - compileSdkVersion = 32 - dexmakerVersion = '2.28.1' + compileSdkVersion = 33 + dexmakerVersion = '2.28.3' junitVersion = '4.13.2' // Use the same Guava version as the Android repo: // https://cs.android.com/android/platform/superproject/+/master:external/guava/METADATA @@ -40,7 +43,7 @@ project.ext { androidxConstraintLayoutVersion = '2.0.4' androidxCoreVersion = '1.7.0' androidxFuturesVersion = '1.1.0' - androidxMediaVersion = '1.4.3' + androidxMediaVersion = '1.6.0' androidxMedia2Version = '1.2.0' androidxMultidexVersion = '2.0.1' androidxRecyclerViewVersion = '1.2.1' diff --git a/core_settings.gradle b/core_settings.gradle index 83ec79ef72f..65300d3a674 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -82,6 +82,9 @@ project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'libr include modulePrefix + 'extension-cast' project(modulePrefix + 'extension-cast').projectDir = new File(rootDir, 'extensions/cast') +include modulePrefix + 'library-effect' +project(modulePrefix + 'library-effect').projectDir = new File(rootDir, 'library/effect') + include modulePrefix + 'library-transformer' project(modulePrefix + 'library-transformer').projectDir = new File(rootDir, 'library/transformer') diff --git a/demos/cast/src/main/AndroidManifest.xml b/demos/cast/src/main/AndroidManifest.xml index ebaab7bb240..fe17b9d6394 100644 --- a/demos/cast/src/main/AndroidManifest.xml +++ b/demos/cast/src/main/AndroidManifest.xml @@ -22,8 +22,13 @@ - + diff --git a/demos/gl/build.gradle b/demos/gl/build.gradle index 01b5808fe70..b08f5e48169 100644 --- a/demos/gl/build.gradle +++ b/demos/gl/build.gradle @@ -52,6 +52,7 @@ dependencies { implementation project(modulePrefix + 'library-smoothstreaming') implementation project(modulePrefix + 'library-ui') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'androidx.multidex:multidex:' + androidxMultidexVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion } diff --git a/demos/gl/src/main/AndroidManifest.xml b/demos/gl/src/main/AndroidManifest.xml index 4c95d1ec2f5..6e27e1154f1 100644 --- a/demos/gl/src/main/AndroidManifest.xml +++ b/demos/gl/src/main/AndroidManifest.xml @@ -22,6 +22,7 @@ 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 42dbc5b2b73..f941fefe131 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 @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.GlProgram; import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.Log; import java.io.IOException; import java.util.Locale; import javax.microedition.khronos.opengles.GL10; @@ -41,6 +42,7 @@ /* package */ final class BitmapOverlayVideoProcessor implements VideoProcessingGLSurfaceView.VideoProcessor { + private static final String TAG = "BitmapOverlayVP"; private static final int OVERLAY_WIDTH = 512; private static final int OVERLAY_HEIGHT = 256; @@ -85,6 +87,9 @@ public void initialize() { /* fragmentShaderFilePath= */ "bitmap_overlay_video_processor_fragment.glsl"); } catch (IOException e) { throw new IllegalStateException(e); + } catch (GlUtil.GlException e) { + Log.e(TAG, "Failed to initialize the shader program", e); + return; } program.setBufferAttribute( "aFramePosition", @@ -119,7 +124,11 @@ public void draw(int frameTexture, long frameTimestampUs, float[] transformMatri GLES20.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]); GLUtils.texSubImage2D( GL10.GL_TEXTURE_2D, /* level= */ 0, /* xoffset= */ 0, /* yoffset= */ 0, overlayBitmap); - GlUtil.checkGlError(); + try { + GlUtil.checkGlError(); + } catch (GlUtil.GlException e) { + Log.e(TAG, "Failed to populate the texture", e); + } // Run the shader program. GlProgram program = checkNotNull(this.program); @@ -128,16 +137,28 @@ public void draw(int frameTexture, long frameTimestampUs, float[] transformMatri program.setFloatUniform("uScaleX", bitmapScaleX); program.setFloatUniform("uScaleY", bitmapScaleY); program.setFloatsUniform("uTexTransform", transformMatrix); - program.bindAttributesAndUniforms(); + try { + program.bindAttributesAndUniforms(); + } catch (GlUtil.GlException e) { + Log.e(TAG, "Failed to update the shader program", e); + } GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); - GlUtil.checkGlError(); + try { + GlUtil.checkGlError(); + } catch (GlUtil.GlException e) { + Log.e(TAG, "Failed to draw a frame", e); + } } @Override public void release() { if (program != null) { - program.delete(); + try { + program.delete(); + } catch (GlUtil.GlException e) { + Log.e(TAG, "Failed to delete the shader program", e); + } } } } 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 c1fc42becfa..42c6cd9728a 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 @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.gldemo; +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -83,7 +85,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { VideoProcessingGLSurfaceView videoProcessingGLSurfaceView = new VideoProcessingGLSurfaceView( context, requestSecureSurface, new BitmapOverlayVideoProcessor(context)); - FrameLayout contentFrame = findViewById(R.id.exo_content_frame); + checkNotNull(playerView); + FrameLayout contentFrame = playerView.findViewById(R.id.exo_content_frame); contentFrame.addView(videoProcessingGLSurfaceView); this.videoProcessingGLSurfaceView = videoProcessingGLSurfaceView; } diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java index 2bdd0872518..648609284f6 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/VideoProcessingGLSurfaceView.java @@ -29,6 +29,7 @@ import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.GlUtil; +import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.TimedValueQueue; import com.google.android.exoplayer2.video.VideoFrameMetadataListener; import java.util.concurrent.atomic.AtomicBoolean; @@ -70,6 +71,7 @@ public interface VideoProcessor { } private static final int EGL_PROTECTED_CONTENT_EXT = 0x32C0; + private static final String TAG = "VPGlSurfaceView"; private final VideoRenderer renderer; private final Handler mainHandler; @@ -239,7 +241,11 @@ public VideoRenderer(VideoProcessor videoProcessor) { @Override public synchronized void onSurfaceCreated(GL10 gl, EGLConfig config) { - texture = GlUtil.createExternalTexture(); + try { + texture = GlUtil.createExternalTexture(); + } catch (GlUtil.GlException e) { + Log.e(TAG, "Failed to create an external texture", e); + } surfaceTexture = new SurfaceTexture(texture); surfaceTexture.setOnFrameAvailableListener( surfaceTexture -> { diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 1e29a9ea6f0..296250d5f26 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -11,6 +11,7 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. + apply from: '../../constants.gradle' apply plugin: 'com.android.application' @@ -26,7 +27,9 @@ android { versionName project.ext.releaseVersion versionCode project.ext.releaseVersionCode minSdkVersion project.ext.minSdkVersion - targetSdkVersion project.ext.appTargetSdkVersion + // Not using appTargetSDKVersion to allow local file access on API 29 + // and higher [Internal ref: b/191644662] + targetSdkVersion project.ext.mainDemoAppTargetSdkVersion multiDexEnabled true } diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index 09688fa73ac..ac7b5ce7492 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -399,7 +399,7 @@ "uri": "ssai://dai.google.com/?contentSourceId=2528370&videoId=tears-of-steel&format=2&adsId=1" }, { - "name": "HLS Live: Big Buck Bunny (mid), 3 ads each [10 s]", + "name": "HLS Live: Big Buck Bunny (mid), 3 ads [10/10/10s]", "uri": "ssai://dai.google.com/?assetKey=sN_IYUG8STe1ZzhIIE_ksA&format=2&adsId=3" }, { diff --git a/demos/transformer/build.gradle b/demos/transformer/build.gradle index 3690b2f50f3..234f0bbfafe 100644 --- a/demos/transformer/build.gradle +++ b/demos/transformer/build.gradle @@ -20,6 +20,7 @@ android { compileSdkVersion project.ext.compileSdkVersion compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } @@ -76,11 +77,14 @@ dependencies { implementation 'androidx.constraintlayout:constraintlayout:' + androidxConstraintLayoutVersion implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation 'com.google.android.material:material:' + androidxMaterialVersion + implementation project(modulePrefix + 'library-effect') implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-transformer') implementation project(modulePrefix + 'library-ui') + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + // For MediaPipe and its dependencies: withMediaPipeImplementation fileTree(dir: 'libs', include: ['*.aar']) withMediaPipeImplementation 'com.google.flogger:flogger:latest.release' diff --git a/demos/transformer/src/main/AndroidManifest.xml b/demos/transformer/src/main/AndroidManifest.xml index 780b02ab6db..60a336de272 100644 --- a/demos/transformer/src/main/AndroidManifest.xml +++ b/demos/transformer/src/main/AndroidManifest.xml @@ -29,6 +29,7 @@ android:label="@string/app_name" android:theme="@style/Theme.AppCompat" android:taskAffinity="" + android:requestLegacyExternalStorage="true" tools:targetApi="29"> inputHeight) { - bitmapScaleX = inputWidth / (float) inputHeight; - bitmapScaleY = 1f; - } else { - bitmapScaleX = 1f; - bitmapScaleY = inputHeight / (float) inputWidth; - } - outputSize = new Size(inputWidth, inputHeight); try { logoBitmap = @@ -97,30 +90,46 @@ public void initialize(Context context, int inputTexId, int inputWidth, int inpu } catch (PackageManager.NameNotFoundException e) { throw new IllegalStateException(e); } - bitmapTexId = GlUtil.createTexture(BITMAP_WIDTH_HEIGHT, BITMAP_WIDTH_HEIGHT); - GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0); - - glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); + try { + bitmapTexId = + GlUtil.createTexture( + BITMAP_WIDTH_HEIGHT, + BITMAP_WIDTH_HEIGHT, + /* useHighPrecisionColorComponents= */ false); + GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, /* level= */ 0, overlayBitmap, /* border= */ 0); + + glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); + } catch (GlUtil.GlException | IOException e) { + throw new FrameProcessingException(e); + } // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. glProgram.setBufferAttribute( "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.HOMOGENEOUS_COORDINATE_VECTOR_SIZE); - glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0); glProgram.setSamplerTexIdUniform("uTexSampler1", bitmapTexId, /* texUnitIndex= */ 1); - glProgram.setFloatUniform("uScaleX", bitmapScaleX); - glProgram.setFloatUniform("uScaleY", bitmapScaleY); } @Override - public Size getOutputSize() { - return checkStateNotNull(outputSize); + public Pair configure(int inputWidth, int inputHeight) { + if (inputWidth > inputHeight) { + bitmapScaleX = inputWidth / (float) inputHeight; + bitmapScaleY = 1f; + } else { + bitmapScaleX = 1f; + bitmapScaleY = inputHeight / (float) inputWidth; + } + + glProgram.setFloatUniform("uScaleX", bitmapScaleX); + glProgram.setFloatUniform("uScaleY", bitmapScaleY); + + return Pair.create(inputWidth, inputHeight); } @Override - public void drawFrame(long presentationTimeUs) throws FrameProcessingException { + public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException { try { - checkStateNotNull(glProgram).use(); + glProgram.use(); // Draw to the canvas and store it in a texture. String text = @@ -137,19 +146,23 @@ public void drawFrame(long presentationTimeUs) throws FrameProcessingException { flipBitmapVertically(overlayBitmap)); GlUtil.checkGlError(); + glProgram.setSamplerTexIdUniform("uTexSampler0", inputTexId, /* texUnitIndex= */ 0); glProgram.bindAttributesAndUniforms(); // The four-vertex triangle strip forms a quad. GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); GlUtil.checkGlError(); } catch (GlUtil.GlException e) { - throw new FrameProcessingException(e); + throw new FrameProcessingException(e, presentationTimeUs); } } @Override - public void release() { - if (glProgram != null) { + public void release() throws FrameProcessingException { + super.release(); + try { glProgram.delete(); + } catch (GlUtil.GlException e) { + throw new FrameProcessingException(e); } } 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 d659738ebfd..664014984a9 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 @@ -18,9 +18,11 @@ import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkState; +import android.Manifest; import android.app.Activity; import android.content.DialogInterface; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; import android.view.View; @@ -29,9 +31,14 @@ import android.widget.CheckBox; import android.widget.Spinner; import android.widget.TextView; +import android.widget.Toast; +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.util.MimeTypes; import com.google.android.exoplayer2.util.Util; @@ -59,14 +66,28 @@ public final class ConfigurationActivity extends AppCompatActivity { public static final String TRIM_START_MS = "trim_start_ms"; public static final String TRIM_END_MS = "trim_end_ms"; public static final String ENABLE_FALLBACK = "enable_fallback"; + public static final String ENABLE_DEBUG_PREVIEW = "enable_debug_preview"; public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping"; + public static final String FORCE_INTERPRET_HDR_VIDEO_AS_SDR = "force_interpret_hdr_video_as_sdr"; public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; public static final String DEMO_EFFECTS_SELECTIONS = "demo_effects_selections"; public static final String PERIODIC_VIGNETTE_CENTER_X = "periodic_vignette_center_x"; public static final String PERIODIC_VIGNETTE_CENTER_Y = "periodic_vignette_center_y"; public static final String PERIODIC_VIGNETTE_INNER_RADIUS = "periodic_vignette_inner_radius"; public static final String PERIODIC_VIGNETTE_OUTER_RADIUS = "periodic_vignette_outer_radius"; - private static final String[] INPUT_URIS = { + public static final String COLOR_FILTER_SELECTION = "color_filter_selection"; + public static final String CONTRAST_VALUE = "contrast_value"; + public static final String RGB_ADJUSTMENT_RED_SCALE = "rgb_adjustment_red_scale"; + public static final String RGB_ADJUSTMENT_GREEN_SCALE = "rgb_adjustment_green_scale"; + public static final String RGB_ADJUSTMENT_BLUE_SCALE = "rgb_adjustment_blue_scale"; + public static final String HSL_ADJUSTMENTS_HUE = "hsl_adjustments_hue"; + public static final String HSL_ADJUSTMENTS_SATURATION = "hsl_adjustments_saturation"; + public static final String HSL_ADJUSTMENTS_LIGHTNESS = "hsl_adjustments_lightness"; + public static final int COLOR_FILTER_GRAYSCALE = 0; + public static final int COLOR_FILTER_INVERTED = 1; + public static final int COLOR_FILTER_SEPIA = 2; + public static final int FILE_PERMISSION_REQUEST_CODE = 1; + private static final String[] PRESET_FILE_URIS = { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/android-screens-10s.mp4", "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4", "https://html5demos.com/assets/dizzy.mp4", @@ -79,9 +100,9 @@ public final class ConfigurationActivity extends AppCompatActivity { "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4", "https://storage.googleapis.com/exoplayer-test-media-1/gen/screens/dash-vod-single-segment/manifest-baseline.mpd", - "https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-hdr-hdr10.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-s21-hdr-hdr10.mp4", }; - private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS + private static final String[] PRESET_FILE_URI_DESCRIPTIONS = { // same order as PRESET_FILE_URIS "720p H264 video and AAC audio", "1080p H265 video and AAC audio", "360p H264 video and AAC audio", @@ -94,21 +115,32 @@ public final class ConfigurationActivity extends AppCompatActivity { "H264 video and AAC audio (portrait, H < W, 90\u00B0)", "SEF slow motion with 240 fps", "480p DASH (non-square pixels)", - "HDR (HDR10) H265 video (encoding may fail)", + "HDR (HDR10) H265 limited range video (encoding may fail)", }; private static final String[] DEMO_EFFECTS = { "Dizzy crop", "Edge detector (Media Pipe)", + "Color filters", + "Map White to Green Color Lookup Table", + "RGB Adjustments", + "HSL Adjustments", + "Contrast", "Periodic vignette", "3D spin", "Overlay logo & timer", "Zoom in start", }; - private static final int PERIODIC_VIGNETTE_INDEX = 2; + private static final int COLOR_FILTERS_INDEX = 2; + private static final int RGB_ADJUSTMENTS_INDEX = 4; + private static final int HSL_ADJUSTMENT_INDEX = 5; + private static final int CONTRAST_INDEX = 6; + private static final int PERIODIC_VIGNETTE_INDEX = 7; private static final String SAME_AS_INPUT_OPTION = "same as input"; private static final float HALF_DIAGONAL = 1f / (float) Math.sqrt(2); - private @MonotonicNonNull Button selectFileButton; + private @MonotonicNonNull ActivityResultLauncher localFilePickerLauncher; + private @MonotonicNonNull Button selectPresetFileButton; + private @MonotonicNonNull Button selectLocalFileButton; private @MonotonicNonNull TextView selectedFileTextView; private @MonotonicNonNull CheckBox removeAudioCheckbox; private @MonotonicNonNull CheckBox removeVideoCheckbox; @@ -120,13 +152,24 @@ public final class ConfigurationActivity extends AppCompatActivity { private @MonotonicNonNull Spinner rotateSpinner; private @MonotonicNonNull CheckBox trimCheckBox; private @MonotonicNonNull CheckBox enableFallbackCheckBox; + private @MonotonicNonNull CheckBox enableDebugPreviewCheckBox; private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; + private @MonotonicNonNull CheckBox forceInterpretHdrVideoAsSdrCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; private @MonotonicNonNull Button selectDemoEffectsButton; private boolean @MonotonicNonNull [] demoEffectsSelections; + private @Nullable Uri localFileUri; private int inputUriPosition; private long trimStartMs; private long trimEndMs; + private int colorFilterSelection; + private float rgbAdjustmentRedScale; + private float rgbAdjustmentGreenScale; + private float rgbAdjustmentBlueScale; + private float contrastValue; + private float hueAdjustment; + private float saturationAdjustment; + private float lightnessAdjustment; private float periodicVignetteCenterX; private float periodicVignetteCenterY; private float periodicVignetteInnerRadius; @@ -139,11 +182,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { findViewById(R.id.transform_button).setOnClickListener(this::startTransformation); - selectFileButton = findViewById(R.id.select_file_button); - selectFileButton.setOnClickListener(this::selectFile); + flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox); selectedFileTextView = findViewById(R.id.selected_file_text_view); - selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]); removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox); removeAudioCheckbox.setOnClickListener(this::onRemoveAudio); @@ -151,7 +193,11 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { removeVideoCheckbox = findViewById(R.id.remove_video_checkbox); removeVideoCheckbox.setOnClickListener(this::onRemoveVideo); - flattenForSlowMotionCheckbox = findViewById(R.id.flatten_for_slow_motion_checkbox); + selectPresetFileButton = findViewById(R.id.select_preset_file_button); + selectPresetFileButton.setOnClickListener(this::selectPresetFile); + + selectLocalFileButton = findViewById(R.id.select_local_file_button); + selectLocalFileButton.setOnClickListener(this::selectLocalFile); ArrayAdapter audioMimeAdapter = new ArrayAdapter<>(/* context= */ this, R.layout.spinner_item); @@ -200,14 +246,38 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { trimEndMs = C.TIME_UNSET; enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox); + enableDebugPreviewCheckBox = findViewById(R.id.enable_debug_preview_checkbox); enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox); enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported()); findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported()); + forceInterpretHdrVideoAsSdrCheckBox = + findViewById(R.id.force_interpret_hdr_video_as_sdr_checkbox); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); demoEffectsSelections = new boolean[DEMO_EFFECTS.length]; selectDemoEffectsButton = findViewById(R.id.select_demo_effects_button); selectDemoEffectsButton.setOnClickListener(this::selectDemoEffects); + + localFilePickerLauncher = + registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + this::localFilePickerLauncherResult); + } + + @Override + public void onRequestPermissionsResult( + int requestCode, String[] permissions, int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + + if (requestCode == FILE_PERMISSION_REQUEST_CODE + && grantResults.length == 1 + && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + launchLocalFilePicker(); + } else { + Toast.makeText( + getApplicationContext(), getString(R.string.permission_denied), Toast.LENGTH_LONG) + .show(); + } } @Override @@ -215,7 +285,8 @@ protected void onResume() { super.onResume(); @Nullable Uri intentUri = getIntent().getData(); if (intentUri != null) { - checkNotNull(selectFileButton).setEnabled(false); + checkNotNull(selectPresetFileButton).setEnabled(false); + checkNotNull(selectLocalFileButton).setEnabled(false); checkNotNull(selectedFileTextView).setText(intentUri.toString()); } } @@ -237,7 +308,9 @@ protected void onNewIntent(Intent intent) { "rotateSpinner", "trimCheckBox", "enableFallbackCheckBox", + "enableDebugPreviewCheckBox", "enableRequestSdrToneMappingCheckBox", + "forceInterpretHdrVideoAsSdrCheckBox", "enableHdrEditingCheckBox", "demoEffectsSelections" }) @@ -275,32 +348,85 @@ private void startTransformation(View view) { bundle.putLong(TRIM_END_MS, trimEndMs); } bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked()); + bundle.putBoolean(ENABLE_DEBUG_PREVIEW, enableDebugPreviewCheckBox.isChecked()); bundle.putBoolean( ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked()); + bundle.putBoolean( + FORCE_INTERPRET_HDR_VIDEO_AS_SDR, forceInterpretHdrVideoAsSdrCheckBox.isChecked()); bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); bundle.putBooleanArray(DEMO_EFFECTS_SELECTIONS, demoEffectsSelections); + bundle.putInt(COLOR_FILTER_SELECTION, colorFilterSelection); + bundle.putFloat(CONTRAST_VALUE, contrastValue); + bundle.putFloat(RGB_ADJUSTMENT_RED_SCALE, rgbAdjustmentRedScale); + bundle.putFloat(RGB_ADJUSTMENT_GREEN_SCALE, rgbAdjustmentGreenScale); + bundle.putFloat(RGB_ADJUSTMENT_BLUE_SCALE, rgbAdjustmentBlueScale); + bundle.putFloat(HSL_ADJUSTMENTS_HUE, hueAdjustment); + bundle.putFloat(HSL_ADJUSTMENTS_SATURATION, saturationAdjustment); + bundle.putFloat(HSL_ADJUSTMENTS_LIGHTNESS, lightnessAdjustment); bundle.putFloat(PERIODIC_VIGNETTE_CENTER_X, periodicVignetteCenterX); bundle.putFloat(PERIODIC_VIGNETTE_CENTER_Y, periodicVignetteCenterY); bundle.putFloat(PERIODIC_VIGNETTE_INNER_RADIUS, periodicVignetteInnerRadius); bundle.putFloat(PERIODIC_VIGNETTE_OUTER_RADIUS, periodicVignetteOuterRadius); transformerIntent.putExtras(bundle); - @Nullable Uri intentUri = getIntent().getData(); - transformerIntent.setData( - intentUri != null ? intentUri : Uri.parse(INPUT_URIS[inputUriPosition])); + @Nullable Uri intentUri; + if (getIntent().getData() != null) { + intentUri = getIntent().getData(); + } else if (localFileUri != null) { + intentUri = localFileUri; + } else { + intentUri = Uri.parse(PRESET_FILE_URIS[inputUriPosition]); + } + transformerIntent.setData(intentUri); startActivity(transformerIntent); } - private void selectFile(View view) { + private void selectPresetFile(View view) { new AlertDialog.Builder(/* context= */ this) - .setTitle(R.string.select_file_title) - .setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog) + .setTitle(R.string.select_preset_file_title) + .setSingleChoiceItems( + PRESET_FILE_URI_DESCRIPTIONS, inputUriPosition, this::selectPresetFileInDialog) .setPositiveButton(android.R.string.ok, /* listener= */ null) .create() .show(); } + @RequiresNonNull("selectedFileTextView") + private void selectPresetFileInDialog(DialogInterface dialog, int which) { + inputUriPosition = which; + localFileUri = null; + selectedFileTextView.setText(PRESET_FILE_URI_DESCRIPTIONS[inputUriPosition]); + } + + private void selectLocalFile(View view) { + int permissionStatus = + ActivityCompat.checkSelfPermission( + ConfigurationActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE); + if (permissionStatus != PackageManager.PERMISSION_GRANTED) { + String[] neededPermissions = {Manifest.permission.READ_EXTERNAL_STORAGE}; + ActivityCompat.requestPermissions( + ConfigurationActivity.this, neededPermissions, FILE_PERMISSION_REQUEST_CODE); + } else { + launchLocalFilePicker(); + } + } + + private void launchLocalFilePicker() { + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.setType("video/*"); + checkNotNull(localFilePickerLauncher).launch(intent); + } + + @RequiresNonNull("selectedFileTextView") + private void localFilePickerLauncherResult(ActivityResult result) { + Intent data = result.getData(); + if (data != null) { + localFileUri = checkNotNull(data.getData()); + selectedFileTextView.setText(localFileUri.toString()); + } + } + private void selectDemoEffects(View view) { new AlertDialog.Builder(/* context= */ this) .setTitle(R.string.select_demo_effects) @@ -316,35 +442,122 @@ private void selectTrimBounds(View view, boolean isChecked) { return; } View dialogView = getLayoutInflater().inflate(R.layout.trim_options, /* root= */ null); - RangeSlider radiusRangeSlider = + RangeSlider trimRangeSlider = checkNotNull(dialogView.findViewById(R.id.trim_bounds_range_slider)); - radiusRangeSlider.setValues(0f, 60f); // seconds + trimRangeSlider.setValues(0f, 10f); // seconds new AlertDialog.Builder(/* context= */ this) .setView(dialogView) .setPositiveButton( android.R.string.ok, (DialogInterface dialogInterface, int i) -> { - List radiusRange = radiusRangeSlider.getValues(); - trimStartMs = 1000 * radiusRange.get(0).longValue(); - trimEndMs = 1000 * radiusRange.get(1).longValue(); + List trimRange = trimRangeSlider.getValues(); + trimStartMs = Math.round(1000 * trimRange.get(0)); + trimEndMs = Math.round(1000 * trimRange.get(1)); }) .create() .show(); } - @RequiresNonNull("selectedFileTextView") - private void selectFileInDialog(DialogInterface dialog, int which) { - inputUriPosition = which; - selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); - } - @RequiresNonNull("demoEffectsSelections") private void selectDemoEffect(DialogInterface dialog, int which, boolean isChecked) { demoEffectsSelections[which] = isChecked; - if (!isChecked || which != PERIODIC_VIGNETTE_INDEX) { + if (!isChecked) { return; } + switch (which) { + case COLOR_FILTERS_INDEX: + controlColorFiltersSettings(); + break; + case RGB_ADJUSTMENTS_INDEX: + controlRgbAdjustmentsScale(); + break; + case CONTRAST_INDEX: + controlContrastSettings(); + break; + case HSL_ADJUSTMENT_INDEX: + controlHslAdjustmentSettings(); + break; + case PERIODIC_VIGNETTE_INDEX: + controlPeriodicVignetteSettings(); + break; + } + } + + private void controlColorFiltersSettings() { + new AlertDialog.Builder(/* context= */ this) + .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> dialogInterface.dismiss()) + .setSingleChoiceItems( + this.getResources().getStringArray(R.array.color_filter_options), + colorFilterSelection, + (DialogInterface dialogInterface, int i) -> { + checkState( + i == COLOR_FILTER_GRAYSCALE + || i == COLOR_FILTER_INVERTED + || i == COLOR_FILTER_SEPIA); + colorFilterSelection = i; + dialogInterface.dismiss(); + }) + .create() + .show(); + } + + private void controlRgbAdjustmentsScale() { + View dialogView = + getLayoutInflater().inflate(R.layout.rgb_adjustment_options, /* root= */ null); + Slider redScaleSlider = checkNotNull(dialogView.findViewById(R.id.rgb_adjustment_red_scale)); + Slider greenScaleSlider = + checkNotNull(dialogView.findViewById(R.id.rgb_adjustment_green_scale)); + Slider blueScaleSlider = checkNotNull(dialogView.findViewById(R.id.rgb_adjustment_blue_scale)); + new AlertDialog.Builder(/* context= */ this) + .setTitle(R.string.rgb_adjustment_options) + .setView(dialogView) + .setPositiveButton( + android.R.string.ok, + (DialogInterface dialogInterface, int i) -> { + rgbAdjustmentRedScale = redScaleSlider.getValue(); + rgbAdjustmentGreenScale = greenScaleSlider.getValue(); + rgbAdjustmentBlueScale = blueScaleSlider.getValue(); + }) + .create() + .show(); + } + + private void controlContrastSettings() { + View dialogView = getLayoutInflater().inflate(R.layout.contrast_options, /* root= */ null); + Slider contrastSlider = checkNotNull(dialogView.findViewById(R.id.contrast_slider)); + new AlertDialog.Builder(/* context= */ this) + .setView(dialogView) + .setPositiveButton( + android.R.string.ok, + (DialogInterface dialogInterface, int i) -> contrastValue = contrastSlider.getValue()) + .create() + .show(); + } + + private void controlHslAdjustmentSettings() { + View dialogView = + getLayoutInflater().inflate(R.layout.hsl_adjustment_options, /* root= */ null); + Slider hueAdjustmentSlider = checkNotNull(dialogView.findViewById(R.id.hsl_adjustments_hue)); + Slider saturationAdjustmentSlider = + checkNotNull(dialogView.findViewById(R.id.hsl_adjustments_saturation)); + Slider lightnessAdjustmentSlider = + checkNotNull(dialogView.findViewById(R.id.hsl_adjustment_lightness)); + new AlertDialog.Builder(/* context= */ this) + .setTitle(R.string.hsl_adjustment_options) + .setView(dialogView) + .setPositiveButton( + android.R.string.ok, + (DialogInterface dialogInterface, int i) -> { + hueAdjustment = hueAdjustmentSlider.getValue(); + saturationAdjustment = saturationAdjustmentSlider.getValue(); + lightnessAdjustment = lightnessAdjustmentSlider.getValue(); + }) + .create() + .show(); + } + + private void controlPeriodicVignetteSettings() { View dialogView = getLayoutInflater().inflate(R.layout.periodic_vignette_options, /* root= */ null); Slider centerXSlider = @@ -377,7 +590,9 @@ private void selectDemoEffect(DialogInterface dialog, int which, boolean isCheck "resolutionHeightSpinner", "scaleSpinner", "rotateSpinner", + "enableDebugPreviewCheckBox", "enableRequestSdrToneMappingCheckBox", + "forceInterpretHdrVideoAsSdrCheckBox", "enableHdrEditingCheckBox", "selectDemoEffectsButton" }) @@ -397,7 +612,9 @@ private void onRemoveAudio(View view) { "resolutionHeightSpinner", "scaleSpinner", "rotateSpinner", + "enableDebugPreviewCheckBox", "enableRequestSdrToneMappingCheckBox", + "forceInterpretHdrVideoAsSdrCheckBox", "enableHdrEditingCheckBox", "selectDemoEffectsButton" }) @@ -416,7 +633,9 @@ private void onRemoveVideo(View view) { "resolutionHeightSpinner", "scaleSpinner", "rotateSpinner", + "enableDebugPreviewCheckBox", "enableRequestSdrToneMappingCheckBox", + "forceInterpretHdrVideoAsSdrCheckBox", "enableHdrEditingCheckBox", "selectDemoEffectsButton" }) @@ -426,8 +645,10 @@ private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoE resolutionHeightSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled); + enableDebugPreviewCheckBox.setEnabled(isVideoEnabled); enableRequestSdrToneMappingCheckBox.setEnabled( isRequestSdrToneMappingSupported() && isVideoEnabled); + forceInterpretHdrVideoAsSdrCheckBox.setEnabled(isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled); selectDemoEffectsButton.setEnabled(isVideoEnabled); @@ -438,6 +659,7 @@ private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoE findViewById(R.id.rotate).setEnabled(isVideoEnabled); findViewById(R.id.request_sdr_tone_mapping) .setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled); + findViewById(R.id.force_interpret_hdr_video_as_sdr).setEnabled(isVideoEnabled); findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); } 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 index 93a993c812a..042dd88c756 100644 --- 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 @@ -17,8 +17,8 @@ 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.effect.GlMatrixTransformation; +import com.google.android.exoplayer2.effect.MatrixTransformation; import com.google.android.exoplayer2.util.Util; /** 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 index 3ea704ae513..57209af87c8 100644 --- 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 @@ -16,39 +16,29 @@ 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 android.util.Pair; +import com.google.android.exoplayer2.effect.SingleFrameGlTextureProcessor; +import com.google.android.exoplayer2.util.FrameProcessingException; 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; - } +/* package */ final class PeriodicVignetteProcessor extends SingleFrameGlTextureProcessor { private static final String VERTEX_SHADER_PATH = "vertex_shader_copy_es2.glsl"; private static final String FRAGMENT_SHADER_PATH = "fragment_shader_vignette_es2.glsl"; private static final float DIMMING_PERIOD_US = 5_600_000f; - private float centerX; - private float centerY; - private float minInnerRadius; - private float deltaInnerRadius; - private float outerRadius; - - private @MonotonicNonNull Size outputSize; - private @MonotonicNonNull GlProgram glProgram; + private final GlProgram glProgram; + private final float minInnerRadius; + private final float deltaInnerRadius; /** * Creates a new instance. @@ -61,29 +51,35 @@ * *

The parameters are given in normalized texture coordinates from 0 to 1. * + * @param context The {@link Context}. + * @param useHdr Whether input textures come from an HDR source. If {@code true}, colors will be + * in linear RGB BT.2020. If {@code false}, colors will be in linear RGB BT.709. * @param centerX The x-coordinate of the center of the effect. * @param centerY The y-coordinate of the center of the effect. * @param minInnerRadius The lower bound of the radius that is unaffected by the effect. * @param maxInnerRadius The upper bound of the radius that is unaffected by the effect. * @param outerRadius The radius after which all pixels are black. + * @throws FrameProcessingException If a problem occurs while reading shader files. */ public PeriodicVignetteProcessor( - float centerX, float centerY, float minInnerRadius, float maxInnerRadius, float outerRadius) { + Context context, + boolean useHdr, + float centerX, + float centerY, + float minInnerRadius, + float maxInnerRadius, + float outerRadius) + throws FrameProcessingException { + super(useHdr); checkArgument(minInnerRadius <= maxInnerRadius); checkArgument(maxInnerRadius <= outerRadius); - this.centerX = centerX; - this.centerY = centerY; this.minInnerRadius = minInnerRadius; this.deltaInnerRadius = maxInnerRadius - minInnerRadius; - this.outerRadius = outerRadius; - } - - @Override - public void initialize(Context context, int inputTexId, int inputWidth, int inputHeight) - throws IOException { - outputSize = new Size(inputWidth, inputHeight); - glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); - glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0); + try { + glProgram = new GlProgram(context, VERTEX_SHADER_PATH, FRAGMENT_SHADER_PATH); + } catch (IOException | GlUtil.GlException e) { + throw new FrameProcessingException(e); + } glProgram.setFloatsUniform("uCenter", new float[] {centerX, centerY}); glProgram.setFloatsUniform("uOuterRadius", new float[] {outerRadius}); // Draw the frame on the entire normalized device coordinate space, from -1 to 1, for x and y. @@ -94,14 +90,15 @@ public void initialize(Context context, int inputTexId, int inputWidth, int inpu } @Override - public Size getOutputSize() { - return checkStateNotNull(outputSize); + public Pair configure(int inputWidth, int inputHeight) { + return Pair.create(inputWidth, inputHeight); } @Override - public void drawFrame(long presentationTimeUs) throws FrameProcessingException { + public void drawFrame(int inputTexId, long presentationTimeUs) throws FrameProcessingException { try { - checkStateNotNull(glProgram).use(); + glProgram.use(); + glProgram.setSamplerTexIdUniform("uTexSampler", inputTexId, /* texUnitIndex= */ 0); double theta = presentationTimeUs * 2 * Math.PI / DIMMING_PERIOD_US; float innerRadius = minInnerRadius + deltaInnerRadius * (0.5f - 0.5f * (float) Math.cos(theta)); @@ -110,14 +107,17 @@ public void drawFrame(long presentationTimeUs) throws FrameProcessingException { // The four-vertex triangle strip forms a quad. GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); } catch (GlUtil.GlException e) { - throw new FrameProcessingException(e); + throw new FrameProcessingException(e, presentationTimeUs); } } @Override - public void release() { - if (glProgram != null) { + public void release() throws FrameProcessingException { + super.release(); + try { glProgram.delete(); + } catch (GlUtil.GlException e) { + throw new FrameProcessingException(e); } } } 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 2e4d6512012..a742c66aaaf 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 @@ -17,10 +17,13 @@ import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static com.google.android.exoplayer2.util.Assertions.checkState; import android.app.Activity; +import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.Color; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -28,6 +31,7 @@ import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; +import android.widget.Button; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; @@ -36,11 +40,16 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.effect.Contrast; +import com.google.android.exoplayer2.effect.GlEffect; +import com.google.android.exoplayer2.effect.GlTextureProcessor; +import com.google.android.exoplayer2.effect.HslAdjustment; +import com.google.android.exoplayer2.effect.RgbAdjustment; +import com.google.android.exoplayer2.effect.RgbFilter; +import com.google.android.exoplayer2.effect.RgbMatrix; +import com.google.android.exoplayer2.effect.SingleColorLut; 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; @@ -48,8 +57,11 @@ import com.google.android.exoplayer2.ui.AspectRatioFrameLayout; import com.google.android.exoplayer2.ui.StyledPlayerView; import com.google.android.exoplayer2.util.DebugTextViewHelper; +import com.google.android.exoplayer2.util.DebugViewProvider; +import com.google.android.exoplayer2.util.Effect; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; +import com.google.android.material.card.MaterialCardView; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.common.base.Stopwatch; import com.google.common.base.Ticker; @@ -66,7 +78,10 @@ public final class TransformerActivity extends AppCompatActivity { private static final String TAG = "TransformerActivity"; - private @MonotonicNonNull StyledPlayerView playerView; + private @MonotonicNonNull Button displayInputButton; + private @MonotonicNonNull MaterialCardView inputCardView; + private @MonotonicNonNull StyledPlayerView inputPlayerView; + private @MonotonicNonNull StyledPlayerView outputPlayerView; private @MonotonicNonNull TextView debugTextView; private @MonotonicNonNull TextView informationTextView; private @MonotonicNonNull ViewGroup progressViewGroup; @@ -75,7 +90,8 @@ public final class TransformerActivity extends AppCompatActivity { private @MonotonicNonNull AspectRatioFrameLayout debugFrame; @Nullable private DebugTextViewHelper debugTextViewHelper; - @Nullable private ExoPlayer player; + @Nullable private ExoPlayer inputPlayer; + @Nullable private ExoPlayer outputPlayer; @Nullable private Transformer transformer; @Nullable private File externalCacheFile; @@ -84,16 +100,21 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.transformer_activity); - playerView = findViewById(R.id.player_view); + inputCardView = findViewById(R.id.input_card_view); + inputPlayerView = findViewById(R.id.input_player_view); + outputPlayerView = findViewById(R.id.output_player_view); debugTextView = findViewById(R.id.debug_text_view); informationTextView = findViewById(R.id.information_text_view); progressViewGroup = findViewById(R.id.progress_view_group); progressIndicator = findViewById(R.id.progress_indicator); debugFrame = findViewById(R.id.debug_aspect_ratio_frame_layout); + displayInputButton = findViewById(R.id.display_input_button); + displayInputButton.setOnClickListener(this::toggleInputVideoDisplay); transformationStopwatch = Stopwatch.createUnstarted( new Ticker() { + @Override public long read() { return android.os.SystemClock.elapsedRealtimeNanos(); } @@ -107,13 +128,17 @@ protected void onStart() { checkNotNull(progressIndicator); checkNotNull(informationTextView); checkNotNull(transformationStopwatch); - checkNotNull(playerView); + checkNotNull(inputCardView); + checkNotNull(inputPlayerView); + checkNotNull(outputPlayerView); checkNotNull(debugTextView); checkNotNull(progressViewGroup); checkNotNull(debugFrame); + checkNotNull(displayInputButton); startTransformation(); - playerView.onResume(); + inputPlayerView.onResume(); + outputPlayerView.onResume(); } @Override @@ -127,7 +152,8 @@ protected void onStop() { // stop watch to be stopped in a transformer callback. checkNotNull(transformationStopwatch).reset(); - checkNotNull(playerView).onPause(); + checkNotNull(inputPlayerView).onPause(); + checkNotNull(outputPlayerView).onPause(); releasePlayer(); checkNotNull(externalCacheFile).delete(); @@ -135,7 +161,10 @@ protected void onStop() { } @RequiresNonNull({ - "playerView", + "inputCardView", + "inputPlayerView", + "outputPlayerView", + "displayInputButton", "debugTextView", "informationTextView", "progressIndicator", @@ -161,7 +190,8 @@ private void startTransformation() { throw new IllegalStateException(e); } informationTextView.setText(R.string.transformation_started); - playerView.setVisibility(View.GONE); + inputCardView.setVisibility(View.GONE); + outputPlayerView.setVisibility(View.GONE); Handler mainHandler = new Handler(getMainLooper()); ProgressHolder progressHolder = new ProgressHolder(); mainHandler.post( @@ -200,20 +230,11 @@ private MediaItem createMediaItem(@Nullable Bundle bundle, Uri uri) { return mediaItemBuilder.build(); } - // Create a cache file, resetting it if it already exists. - private File createExternalCacheFile(String fileName) throws IOException { - File file = new File(getExternalCacheDir(), fileName); - if (file.exists() && !file.delete()) { - throw new IllegalStateException("Could not delete the previous transformer output file"); - } - if (!file.createNewFile()) { - throw new IllegalStateException("Could not create the transformer output file"); - } - return file; - } - @RequiresNonNull({ - "playerView", + "inputCardView", + "inputPlayerView", + "outputPlayerView", + "displayInputButton", "debugTextView", "informationTextView", "transformationStopwatch", @@ -251,6 +272,8 @@ private Transformer createTransformer(@Nullable Bundle bundle, String filePath) requestBuilder.setEnableRequestSdrToneMapping( bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING)); + requestBuilder.experimental_setForceInterpretHdrVideoAsSdr( + bundle.getBoolean(ConfigurationActivity.FORCE_INTERPRET_HDR_VIDEO_AS_SDR)); requestBuilder.experimental_setEnableHdrEditing( bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING)); transformerBuilder @@ -258,30 +281,79 @@ private Transformer createTransformer(@Nullable Bundle bundle, String filePath) .setRemoveAudio(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_AUDIO)) .setRemoveVideo(bundle.getBoolean(ConfigurationActivity.SHOULD_REMOVE_VIDEO)) .setEncoderFactory( - new DefaultEncoderFactory( - EncoderSelector.DEFAULT, - /* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))); - - 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( - () -> { + new DefaultEncoderFactory.Builder(this.getApplicationContext()) + .setEnableFallback(bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK)) + .build()); + + transformerBuilder.setVideoEffects(createVideoEffectsListFromBundle(bundle)); + + if (bundle.getBoolean(ConfigurationActivity.ENABLE_DEBUG_PREVIEW)) { + transformerBuilder.setDebugViewProvider(new DemoDebugViewProvider()); + } + } + return transformerBuilder + .addListener( + new Transformer.Listener() { + @Override + public void onTransformationCompleted( + MediaItem mediaItem, TransformationResult transformationResult) { + TransformerActivity.this.onTransformationCompleted(filePath, mediaItem); + } + + @Override + public void onTransformationError( + MediaItem mediaItem, TransformationException exception) { + TransformerActivity.this.onTransformationError(exception); + } + }) + .build(); + } + + /** Creates a cache file, resetting it if it already exists. */ + private File createExternalCacheFile(String fileName) throws IOException { + File file = new File(getExternalCacheDir(), fileName); + if (file.exists() && !file.delete()) { + throw new IllegalStateException("Could not delete the previous transformer output file"); + } + if (!file.createNewFile()) { + throw new IllegalStateException("Could not create the transformer output file"); + } + return file; + } + + private ImmutableList createVideoEffectsListFromBundle(Bundle bundle) { + @Nullable + boolean[] selectedEffects = + bundle.getBooleanArray(ConfigurationActivity.DEMO_EFFECTS_SELECTIONS); + if (selectedEffects == null) { + return ImmutableList.of(); + } + ImmutableList.Builder effects = new ImmutableList.Builder<>(); + 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( + Context.class, + boolean.class, + String.class, + boolean.class, + String.class, + String.class); + effects.add( + (GlEffect) + (Context context, boolean useHdr) -> { try { - return (SingleFrameGlTextureProcessor) + return (GlTextureProcessor) constructor.newInstance( + context, + useHdr, /* graphName= */ "edge_detector_mediapipe_graph.binarypb", + /* isSingleFrameGraph= */ true, /* inputStreamName= */ "input_video", /* outputStreamName= */ "output_video"); } catch (Exception e) { @@ -289,14 +361,77 @@ private Transformer createTransformer(@Nullable Bundle bundle, String filePath) throw new RuntimeException("Failed to load MediaPipe processor", e); } }); - } catch (Exception e) { - showToast(R.string.no_media_pipe_error); + } catch (Exception e) { + showToast(R.string.no_media_pipe_error); + } + } + if (selectedEffects[2]) { + switch (bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION)) { + case ConfigurationActivity.COLOR_FILTER_GRAYSCALE: + effects.add(RgbFilter.createGrayscaleFilter()); + break; + case ConfigurationActivity.COLOR_FILTER_INVERTED: + effects.add(RgbFilter.createInvertedFilter()); + break; + case ConfigurationActivity.COLOR_FILTER_SEPIA: + // W3C Sepia RGBA matrix with sRGB as a target color space: + // https://www.w3.org/TR/filter-effects-1/#sepiaEquivalent + // The matrix is defined for the sRGB color space and the Transformer library + // uses a linear RGB color space internally. Meaning this is only for demonstration + // purposes and it does not display a correct sepia frame. + float[] sepiaMatrix = { + 0.393f, 0.349f, 0.272f, 0, 0.769f, 0.686f, 0.534f, 0, 0.189f, 0.168f, 0.131f, 0, 0, 0, + 0, 1 + }; + effects.add((RgbMatrix) (presentationTimeUs, useHdr) -> sepiaMatrix); + break; + default: + throw new IllegalStateException( + "Unexpected color filter " + + bundle.getInt(ConfigurationActivity.COLOR_FILTER_SELECTION)); + } + } + if (selectedEffects[3]) { + int length = 3; + int[][][] mapWhiteToGreenLut = new int[length][length][length]; + int scale = 255 / (length - 1); + for (int r = 0; r < length; r++) { + for (int g = 0; g < length; g++) { + for (int b = 0; b < length; b++) { + mapWhiteToGreenLut[r][g][b] = + Color.rgb(/* red= */ r * scale, /* green= */ g * scale, /* blue= */ b * scale); } } - if (selectedEffects[2]) { - effects.add( - () -> + } + mapWhiteToGreenLut[length - 1][length - 1][length - 1] = Color.GREEN; + effects.add(SingleColorLut.createFromCube(mapWhiteToGreenLut)); + } + if (selectedEffects[4]) { + effects.add( + new RgbAdjustment.Builder() + .setRedScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_RED_SCALE)) + .setGreenScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_GREEN_SCALE)) + .setBlueScale(bundle.getFloat(ConfigurationActivity.RGB_ADJUSTMENT_BLUE_SCALE)) + .build()); + } + if (selectedEffects[5]) { + effects.add( + new HslAdjustment.Builder() + .adjustHue(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_HUE)) + .adjustSaturation(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_SATURATION)) + .adjustLightness(bundle.getFloat(ConfigurationActivity.HSL_ADJUSTMENTS_LIGHTNESS)) + .build()); + } + if (selectedEffects[6]) { + effects.add(new Contrast(bundle.getFloat(ConfigurationActivity.CONTRAST_VALUE))); + } + if (selectedEffects[7]) { + effects.add( + (GlEffect) + (Context context, boolean useHdr) -> new PeriodicVignetteProcessor( + context, + useHdr, bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_X), bundle.getFloat(ConfigurationActivity.PERIODIC_VIGNETTE_CENTER_Y), /* minInnerRadius= */ bundle.getFloat( @@ -304,36 +439,17 @@ private Transformer createTransformer(@Nullable Bundle bundle, String filePath) /* 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, TransformationResult transformationResult) { - TransformerActivity.this.onTransformationCompleted(filePath); - } - - @Override - public void onTransformationError( - MediaItem mediaItem, TransformationException exception) { - TransformerActivity.this.onTransformationError(exception); - } - }) - .setDebugViewProvider(new DemoDebugViewProvider()) - .build(); + if (selectedEffects[8]) { + effects.add(MatrixTransformationFactory.createSpin3dEffect()); + } + if (selectedEffects[9]) { + effects.add((GlEffect) BitmapOverlayProcessor::new); + } + if (selectedEffects[10]) { + effects.add(MatrixTransformationFactory.createZoomInTransition()); + } + return effects.build(); } @RequiresNonNull({ @@ -347,44 +463,66 @@ private void onTransformationError(TransformationException exception) { informationTextView.setText(R.string.transformation_error); progressViewGroup.setVisibility(View.GONE); debugFrame.removeAllViews(); - Toast.makeText( - TransformerActivity.this, "Transformation error: " + exception, Toast.LENGTH_LONG) + Toast.makeText(getApplicationContext(), "Transformation error: " + exception, Toast.LENGTH_LONG) .show(); Log.e(TAG, "Transformation error", exception); } @RequiresNonNull({ - "playerView", + "inputCardView", + "inputPlayerView", + "outputPlayerView", + "displayInputButton", "debugTextView", "informationTextView", "progressViewGroup", "debugFrame", "transformationStopwatch", }) - private void onTransformationCompleted(String filePath) { + private void onTransformationCompleted(String filePath, MediaItem inputMediaItem) { transformationStopwatch.stop(); informationTextView.setText( getString( R.string.transformation_completed, transformationStopwatch.elapsed(TimeUnit.SECONDS))); progressViewGroup.setVisibility(View.GONE); debugFrame.removeAllViews(); - playerView.setVisibility(View.VISIBLE); - playMediaItem(MediaItem.fromUri("file://" + filePath)); + inputCardView.setVisibility(View.VISIBLE); + outputPlayerView.setVisibility(View.VISIBLE); + displayInputButton.setVisibility(View.VISIBLE); + playMediaItems(inputMediaItem, MediaItem.fromUri("file://" + filePath)); Log.d(TAG, "Output file path: file://" + filePath); } - @RequiresNonNull({"playerView", "debugTextView"}) - private void playMediaItem(MediaItem mediaItem) { - playerView.setPlayer(null); + @RequiresNonNull({ + "inputCardView", + "inputPlayerView", + "outputPlayerView", + "debugTextView", + }) + private void playMediaItems(MediaItem inputMediaItem, MediaItem outputMediaItem) { + inputPlayerView.setPlayer(null); + outputPlayerView.setPlayer(null); releasePlayer(); - ExoPlayer player = new ExoPlayer.Builder(/* context= */ this).build(); - playerView.setPlayer(player); - player.setMediaItem(mediaItem); - player.play(); - player.prepare(); - this.player = player; - debugTextViewHelper = new DebugTextViewHelper(player, debugTextView); + ExoPlayer inputPlayer = new ExoPlayer.Builder(/* context= */ this).build(); + inputPlayerView.setPlayer(inputPlayer); + inputPlayerView.setControllerAutoShow(false); + inputPlayer.setMediaItem(inputMediaItem); + inputPlayer.prepare(); + this.inputPlayer = inputPlayer; + inputPlayer.setVolume(0f); + + ExoPlayer outputPlayer = new ExoPlayer.Builder(/* context= */ this).build(); + outputPlayerView.setPlayer(outputPlayer); + outputPlayerView.setControllerAutoShow(false); + outputPlayer.setMediaItem(outputMediaItem); + outputPlayer.prepare(); + this.outputPlayer = outputPlayer; + + inputPlayer.play(); + outputPlayer.play(); + + debugTextViewHelper = new DebugTextViewHelper(outputPlayer, debugTextView); debugTextViewHelper.start(); } @@ -393,9 +531,13 @@ private void releasePlayer() { debugTextViewHelper.stop(); debugTextViewHelper = null; } - if (player != null) { - player.release(); - player = null; + if (inputPlayer != null) { + inputPlayer.release(); + inputPlayer = null; + } + if (outputPlayer != null) { + outputPlayer.release(); + outputPlayer = null; } } @@ -412,11 +554,45 @@ private void showToast(@StringRes int messageResource) { Toast.makeText(getApplicationContext(), getString(messageResource), Toast.LENGTH_LONG).show(); } - private final class DemoDebugViewProvider implements Transformer.DebugViewProvider { + @RequiresNonNull({ + "inputCardView", + "displayInputButton", + }) + private void toggleInputVideoDisplay(View view) { + if (inputCardView.getVisibility() == View.GONE) { + inputCardView.setVisibility(View.VISIBLE); + displayInputButton.setText(getString(R.string.hide_input_video)); + } else if (inputCardView.getVisibility() == View.VISIBLE) { + checkNotNull(inputPlayer).pause(); + inputCardView.setVisibility(View.GONE); + displayInputButton.setText(getString(R.string.show_input_video)); + } + } + + private final class DemoDebugViewProvider implements DebugViewProvider { + + private @MonotonicNonNull SurfaceView surfaceView; + private int width; + private int height; + + public DemoDebugViewProvider() { + width = C.LENGTH_UNSET; + height = C.LENGTH_UNSET; + } @Nullable @Override public SurfaceView getDebugPreviewSurfaceView(int width, int height) { + checkState( + surfaceView == null || (this.width == width && this.height == height), + "Transformer should not change the output size mid-transformation."); + if (surfaceView != null) { + return surfaceView; + } + + this.width = width; + this.height = height; + // Update the UI on the main thread and wait for the output surface to be available. CountDownLatch surfaceCreatedCountDownLatch = new CountDownLatch(1); SurfaceView surfaceView = new SurfaceView(/* context= */ TransformerActivity.this); @@ -453,6 +629,7 @@ public void surfaceDestroyed(SurfaceHolder surfaceHolder) { Thread.currentThread().interrupt(); return null; } + this.surfaceView = surfaceView; return surfaceView; } } diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml index 2879d6a637a..2a481bea698 100644 --- a/demos/transformer/src/main/res/layout/configuration_activity.xml +++ b/demos/transformer/src/main/res/layout/configuration_activity.xml @@ -34,16 +34,26 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />