Adaptive HLS streaming on android with Google’s ExoPlayer

Streaming video and audio using the default media player API of Android can be a pain when it comes to adaptive streaming and customization. Google’s ExoPlayer is an application level media player which provides consistent API and easy customization for playing videos both locally and over the internet. ExoPlayer supports features not currently supported by Android’s MediaPlayer API, including DASH, HLS and SmoothStreaming adaptive playbacks.

Here we’ll look into integrating ExoPlayer into an android application for streaming video using HLS protocol. HLS stands for HTTP Live Streaming which resembles MPEG-DASH in that it works by breaking the overall stream into a sequence of small HTTP-based file downloads, each download loading one short chunk of an overall potentially unbounded transport stream. HLS protocol is widely used by Vimeo, Apple and other companies which provides live streaming of videos.

Let’s get started

Create a new android studio project and add make sure you have JCenter and Google repositories included in the project level build.gradle file.

repositories {
jcenter()
google()
}

Add these dependencies into your app level build.gradle file to include ExoPlayer libraries for HLS streaming.

dependencies {
implementation 'com.google.android.exoplayer:exoplayer-core:2.8.3'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.8.3'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.8.3'
// Other dependencies
}

Additionally if you want to use DASH or Smooth Streaming instead of HLS use the following dash module instead of hls

// For DASH
implementation 'com.google.android.exoplayer:exoplayer-dash:2.8.3'
// For SmoothStreaming
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.8.3'

Customizing layout for ExoPlayer

Whether you are using an Activity, Fragment or Dialog. Insert this into the layout file of the component. Here I’ll be demonstrating on an Activity’s layout file.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/parent_view"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#000000">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/video_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
app:controller_layout_id="@layout/player_custom_control"/>
<ProgressBar
android:id="@+id/loading"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"/>
</FrameLayout>

Additionally, I’ve added a progress indicator to display during initial load and buffering. In the line #14 I have included a custom layout for Exo controller. You can avoid this line if you don’t want custom control customization. We need to define a layout file for customizing and give ids to the element as specified in the original Exo layout. (Hopefully, the android studio Intellisense will help you in that).

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageButton
android:id="@+id/exo_play"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
android:background="@drawable/player_play_button_background"
android:padding="10dp"
android:scaleType="fitXY"
android:src="@drawable/ic_play_arrow_white_24dp" />
<ImageButton
android:id="@+id/exo_pause"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
android:background="@drawable/player_pause_button_background"
android:padding="10dp"
android:scaleType="fitXY"
android:src="@drawable/ic_pause_pause_24dp" />
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@+id/exo_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="15dp"
app:buffered_color="@color/buffered"
app:played_color="@color/played"
app:scrubber_color="@color/scrubber"
app:unplayed_color="@color/unplayed" />
</FrameLayout>

You can specify additional TextViews in the layout for displaying current position and end time of the video with ids exo_position and exo_duration

Initializing and Playing the Video

In the Activity declare the PlayerView we added in the layout as private PlayerView playerView and initialize it inside the onCreate method with the id of the view.

playerView = view.findViewById(R.id.video_view);
loading = findViewById(R.id.loading);

Additionaly we need a few variables initialized to be used to save the player state on orientation changes and activity pause.

private boolean playWhenReady = true;
private int currentWindow = 0;
private long playbackPosition = 0;

we need to initialize the ExoPlayer with the streaming URL and configurations in the onStart method of the activity.

@Override
public void onStart() {
super.onStart();
//--------------------------------------
//Creating default track selector
//and init the player
        TrackSelection.Factory adaptiveTrackSelection = new AdaptiveTrackSelection.Factory(new DefaultBandwidthMeter());
player = ExoPlayerFactory.newSimpleInstance(
new DefaultRenderersFactory(mContext),
new DefaultTrackSelector(adaptiveTrackSelection),
new DefaultLoadControl());

playerView.setPlayer(player);
        DefaultBandwidthMeter defaultBandwidthMeter = new DefaultBandwidthMeter();
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(mContext,
Util.getUserAgent(mContext, "Exo2"), defaultBandwidthMeter);

String hls_url = "YOUR STREAMING URL HERE";
Uri uri = Uri.parse(hls_url);
Handler mainHandler = new Handler();
MediaSource mediaSource = new HlsMediaSource(uri,
dataSourceFactory, mainHandler, null);
player.prepare(mediaSource);
player.setPlayWhenReady(playWhenReady);
player.addListener(new Player.EventListener() {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest, int reason) {
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
}
@Override
public void onLoadingChanged(boolean isLoading) {
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
switch (playbackState) {
case ExoPlayer.STATE_READY:
loading.setVisibility(View.GONE);
break;
case ExoPlayer.STATE_BUFFERING:
loading.setVisibility(View.VISIBLE);
break;
}
}
@Override
public void onRepeatModeChanged(int repeatMode) {
}
@Override
public void onShuffleModeEnabledChanged(boolean shuffleModeEnabled) {
}
@Override
public void onPlayerError(ExoPlaybackException error) {
}
@Override
public void onPositionDiscontinuity(int reason) {
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
}
@Override
public void onSeekProcessed() {
}
});
player.seekTo(currentWindow, playbackPosition);
player.prepare(mediaSource, true, false);
}

Cleaning Up!

We need to release the player when the activity goes to the background or is paused. Create the following method and call it inside the onStop() and onPause() method of your activity.

private void releasePlayer() {
if (player != null) {
playbackPosition = player.getCurrentPosition();
currentWindow = player.getCurrentWindowIndex();
playWhenReady = player.getPlayWhenReady();
player.release();
player = null;
}
}
@Override
public void onPause() {
super.onPause();
if (Util.SDK_INT <= 23) {
releasePlayer();
}
}
@Override
public void onStop() {
super.onStop();
if (Util.SDK_INT > 23) {
releasePlayer();
}
}

That’s it! Your video will be playing now! For additional information on playing DASH videos visit the Google CodeLabs, or for the documentation of the ExoPlayer visit the GitHub repository. Feel free to ask any queries in the comment section below.


Originally published at zocada.