ExoPlayer (Java)
Android ships a built-in MediaPlayer class, but it is limited and clunky for anything beyond the simplest playback. ExoPlayer (now distributed as part of Media3) is Google's own recommended replacement — it is more flexible, easier to customize, and actively maintained, while MediaPlayer has stayed mostly frozen for years.
In RecipeBox we will use ExoPlayer for a very ordinary feature: some recipes include a short demonstration video (say, "how to fold the dumpling" or "knife technique for dicing onions") stored alongside the Recipe's photoUri. Tapping a "Watch demo" button on the recipe detail screen should play that clip inline.
Adding the Dependency
ExoPlayer is added like any other library, in the app module's build.gradle:
dependencies {
implementation 'androidx.media3:media3-exoplayer:1.3.1'
implementation 'androidx.media3:media3-ui:1.3.1'
}
Adding PlayerView to the Layout
The recipe detail screen gets a PlayerView where the video surface lives:
<androidx.media3.ui.PlayerView
android:id="@+id/playerView"
android:layout_width="match_parent"
android:layout_height="220dp"
app:use_controller="true"
app:show_shuffle_button="false" />
Building the Player and Playing a MediaItem
In the recipe detail Activity, we create an ExoPlayer instance, attach it to the PlayerView, then hand it a MediaItem built from the recipe's demo video URI:
// RecipeDetailActivity.java
public class RecipeDetailActivity extends AppCompatActivity {
private PlayerView playerView;
private ExoPlayer player;
private Recipe recipe; // loaded earlier, has demoVideoUri
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recipe_detail);
playerView = findViewById(R.id.playerView);
}
private void initializePlayer() {
player = new ExoPlayer.Builder(this).build();
playerView.setPlayer(player);
MediaItem mediaItem = MediaItem.fromUri(recipe.getDemoVideoUri());
player.setMediaItem(mediaItem);
player.prepare();
player.play();
}
}
- ExoPlayer.Builder(this).build() creates the player engine itself.
- playerView.setPlayer(player) connects the engine to the on-screen surface and controls.
- MediaItem.fromUri(...) wraps a local file, raw resource, or remote URL — ExoPlayer doesn't care which, as long as the URI is valid.
- prepare() loads enough of the media to start playback; play() actually starts it. They are deliberately separate steps.
Releasing the Player in the Lifecycle
An ExoPlayer instance holds onto real system resources — decoders, surfaces, sometimes a background thread. If you never call release(), those resources stay allocated even after the Activity is gone, and repeatedly opening/closing the recipe detail screen without releasing will eventually exhaust decoder resources or leak memory. Always tie release() to a lifecycle callback:
@Override
protected void onStart() {
super.onStart();
initializePlayer();
}
@Override
protected void onStop() {
super.onStop();
if (player != null) {
player.release();
player = null;
}
}
Listening for Playback State
To drive a custom play/pause icon or show a loading spinner, attach a Player.Listener:
player.addListener(new Player.Listener() {
@Override
public void onPlaybackStateChanged(int state) {
if (state == Player.STATE_BUFFERING) {
progressBar.setVisibility(View.VISIBLE);
} else if (state == Player.STATE_READY) {
progressBar.setVisibility(View.GONE);
}
}
@Override
public void onIsPlayingChanged(boolean isPlaying) {
playPauseButton.setImageResource(
isPlaying ? R.drawable.ic_pause : R.drawable.ic_play);
}
});
Beyond RecipeBox: Adaptive Streaming
RecipeBox's demo clips are short, simple video files — a single MediaItem.fromUri() call is all we need. For apps that stream longer video at varying quality levels depending on network conditions, ExoPlayer has built-in support for adaptive streaming formats like HLS and DASH, automatically switching between bitrates as bandwidth changes. That's a deeper topic on its own and not something RecipeBox needs, but it's worth knowing ExoPlayer already handles it if a future feature calls for it.
Quick Reference
| Call | Purpose |
|---|---|
| new ExoPlayer.Builder(context).build() | Create the player engine |
| playerView.setPlayer(player) | Attach engine to the visible surface/controls |
| MediaItem.fromUri(uri) | Describe what to play |
| setMediaItem() / prepare() / play() | Queue, load, and start playback |
| addListener(...) | React to state/playing changes for custom UI |
| release() | Free decoder resources — call in onStop()/onDestroy() |