diff --git a/demos/transformer/build.gradle b/demos/transformer/build.gradle
index a260eee977..d2a6b1e02b 100644
--- a/demos/transformer/build.gradle
+++ b/demos/transformer/build.gradle
@@ -77,6 +77,7 @@ dependencies {
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.constraintlayout:constraintlayout:' + androidxConstraintLayoutVersion
+ implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation project(modulePrefix + 'lib-effect')
diff --git a/demos/transformer/src/main/AndroidManifest.xml b/demos/transformer/src/main/AndroidManifest.xml
index 8c58f1ad6a..058521c85a 100644
--- a/demos/transformer/src/main/AndroidManifest.xml
+++ b/demos/transformer/src/main/AndroidManifest.xml
@@ -64,5 +64,11 @@
android:label="@string/app_name"
android:exported="true"
android:theme="@style/Theme.MaterialComponents.DayNight.NoActionBar"/>
+
diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/AssetItemAdapter.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/AssetItemAdapter.java
new file mode 100644
index 0000000000..ee03cba9d7
--- /dev/null
+++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/AssetItemAdapter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.demo.transformer;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import androidx.recyclerview.widget.RecyclerView;
+import java.util.ArrayList;
+import java.util.List;
+
+/** A {@link RecyclerView.Adapter} that displays assets in a sequence in a {@link RecyclerView}. */
+public final class AssetItemAdapter extends RecyclerView.Adapter {
+ private static final String TAG = "AssetItemAdapter";
+
+ private final List dataSet;
+
+ /**
+ * Creates a new instance
+ *
+ * @param data A list of items to populate RecyclerView with.
+ */
+ public AssetItemAdapter(List data) {
+ this.dataSet = new ArrayList<>(data);
+ }
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.preset_item, parent, false);
+ return new ViewHolder(v);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ holder.getTextView().setText(dataSet.get(position));
+ }
+
+ @Override
+ public int getItemCount() {
+ return dataSet.size();
+ }
+
+ /** A {@link RecyclerView.ViewHolder} used to build {@link AssetItemAdapter}. */
+ public static final class ViewHolder extends RecyclerView.ViewHolder {
+ private final TextView textView;
+
+ private ViewHolder(View view) {
+ super(view);
+ textView = view.findViewById(R.id.preset_name_text);
+ }
+
+ private TextView getTextView() {
+ return textView;
+ }
+ }
+}
diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/CompositionPreviewActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/CompositionPreviewActivity.java
new file mode 100644
index 0000000000..7daab290db
--- /dev/null
+++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/CompositionPreviewActivity.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package androidx.media3.demo.transformer;
+
+import static androidx.media3.common.util.Assertions.checkStateNotNull;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.AppCompatButton;
+import androidx.media3.common.MediaItem;
+import androidx.media3.transformer.Composition;
+import androidx.media3.transformer.CompositionPlayer;
+import androidx.media3.transformer.EditedMediaItem;
+import androidx.media3.transformer.EditedMediaItemSequence;
+import androidx.media3.ui.PlayerView;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
+
+/**
+ * An {@link Activity} that previews compositions, using {@link
+ * androidx.media3.transformer.CompositionPlayer}.
+ */
+public final class CompositionPreviewActivity extends AppCompatActivity {
+ private static final String TAG = "CompPreviewActivity";
+ private static final ImmutableList SEQUENCE_FILE_INDICES = ImmutableList.of(0, 2);
+
+ private @MonotonicNonNull PlayerView playerView;
+ private @MonotonicNonNull RecyclerView presetList;
+ private @MonotonicNonNull AppCompatButton previewButton;
+
+ @Nullable private CompositionPlayer compositionPlayer;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.composition_preview_activity);
+
+ String[] presetFileURIs = getResources().getStringArray(R.array.preset_uris);
+ String[] presetFileDescriptions = getResources().getStringArray(R.array.preset_descriptions);
+
+ playerView = findViewById(R.id.composition_player_view);
+ presetList = findViewById(R.id.composition_preset_list);
+ previewButton = findViewById(R.id.preview_button);
+ previewButton.setOnClickListener(view -> previewComposition(view, presetFileURIs));
+
+ LinearLayoutManager layoutManager =
+ new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, /* reverseLayout= */ false);
+ presetList.setLayoutManager(layoutManager);
+ ArrayList sequenceFiles = new ArrayList<>();
+ for (int i = 0; i < SEQUENCE_FILE_INDICES.size(); i++) {
+ if (SEQUENCE_FILE_INDICES.get(i) < presetFileDescriptions.length) {
+ sequenceFiles.add(presetFileDescriptions[SEQUENCE_FILE_INDICES.get(i)]);
+ }
+ }
+ AssetItemAdapter adapter = new AssetItemAdapter(sequenceFiles);
+ presetList.setAdapter(adapter);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ checkStateNotNull(playerView).onResume();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ checkStateNotNull(playerView).onPause();
+ releasePlayer();
+ }
+
+ private Composition prepareComposition(String[] presetFileURIs) {
+ List mediaItems = new ArrayList<>();
+ for (int i = 0; i < SEQUENCE_FILE_INDICES.size(); i++) {
+ mediaItems.add(
+ new EditedMediaItem.Builder(
+ MediaItem.fromUri(presetFileURIs[SEQUENCE_FILE_INDICES.get(i)]))
+ .build());
+ }
+ EditedMediaItemSequence videoSequence =
+ new EditedMediaItemSequence(Collections.unmodifiableList(mediaItems));
+ return new Composition.Builder(ImmutableList.of(videoSequence)).build();
+ }
+
+ private void previewComposition(View view, String[] presetFileURIs) {
+ releasePlayer();
+ Composition composition = prepareComposition(presetFileURIs);
+ checkStateNotNull(playerView).setPlayer(null);
+
+ CompositionPlayer player = new CompositionPlayer(getApplicationContext(), /* looper= */ null);
+ this.compositionPlayer = player;
+ checkStateNotNull(playerView).setPlayer(compositionPlayer);
+ checkStateNotNull(playerView).setControllerAutoShow(false);
+ player.setComposition(composition);
+ player.prepare();
+ player.play();
+ }
+
+ private void releasePlayer() {
+ if (compositionPlayer != null) {
+ compositionPlayer.release();
+ compositionPlayer = null;
+ }
+ }
+}
diff --git a/demos/transformer/src/main/res/layout/composition_preview_activity.xml b/demos/transformer/src/main/res/layout/composition_preview_activity.xml
new file mode 100644
index 0000000000..14ddd48e6e
--- /dev/null
+++ b/demos/transformer/src/main/res/layout/composition_preview_activity.xml
@@ -0,0 +1,92 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demos/transformer/src/main/res/layout/preset_item.xml b/demos/transformer/src/main/res/layout/preset_item.xml
new file mode 100644
index 0000000000..ece15cc343
--- /dev/null
+++ b/demos/transformer/src/main/res/layout/preset_item.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
diff --git a/demos/transformer/src/main/res/layout/transformer_activity.xml b/demos/transformer/src/main/res/layout/transformer_activity.xml
index d3daac940d..d62e9cfe11 100644
--- a/demos/transformer/src/main/res/layout/transformer_activity.xml
+++ b/demos/transformer/src/main/res/layout/transformer_activity.xml
@@ -1,4 +1,5 @@
-
+
+
+ - 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°)
+ - H264 video and AAC audio (portrait, H < W, 90°)
+ - London JPG image (Plays for 5secs at 30fps)
+ - Tokyo JPG image (Portrait, Plays for 5secs at 30fps)
+ - SEF slow motion with 240 fps
+ - 480p DASH (non-square pixels)
+ - HDR (HDR10) H265 limited range video (encoding may fail)
+ - HDR (HLG) H265 limited range video (encoding may fail)
+ - 720p H264 video with no audio
+
+
+ - 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
+ - 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/jpg/london.jpg
+ - https://storage.googleapis.com/exoplayer-test-media-1/jpg/tokyo.jpg
+ - 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-s21-hdr-hdr10.mp4
+ - https://storage.googleapis.com/exoplayer-test-media-1/mp4/Pixel7Pro_HLG_1080P.mp4
+ - https://storage.googleapis.com/exoplayer-test-media-1/mp4/sample_video_track_only.mp4
+
+
diff --git a/demos/transformer/src/main/res/values/strings.xml b/demos/transformer/src/main/res/values/strings.xml
index 0a3a4d2d16..4ec8f9682f 100644
--- a/demos/transformer/src/main/res/values/strings.xml
+++ b/demos/transformer/src/main/res/values/strings.xml
@@ -80,4 +80,7 @@
Text
Text color
Specify text overlay settings
+ Preview
+ Single sequence preview
+ Single sequence items: