Showing Subtitles/Lyrics on Android Exoplayer

Most of time we implement functionality when we badly requires it.It happens with the most of us. Last Sunday When I was working on the one of my pet project I come with a new idea to show subtitle as the audio plays.

One option was to create a youtube video with subtitle and play with Exoplayer but it was cheap kind of way.So I googled about how to implement it with audio and I found the way to do it so I am going to share with you people.

Step 1

You need to create a subtitle file (In SRT or many supported format by Exo)which contain all text with timings and a logic to display in perfect way.

Step 2

When you are playing just attach one media source to audio player but when you want to display subtitle then you need one extra media source to fetch that subtitle from link.Then you need to combine both media sources.

Step 3

When you are fetching two sources like from web links and it may be the case that these can be large enough to load so you must go for progress bar during buffering so it good to implement it using player callbacks you can find code below.

You may find full code on my github repo.

// Import you package
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;

import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MergingMediaSource;
import com.google.android.exoplayer2.source.SingleSampleMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.TextRenderer;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.ui.SubtitleView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import com.google.android.gms.ads.AdRequest;
import com.google.android.gms.ads.AdView;
import com.google.android.gms.ads.InterstitialAd;
import java.util.ArrayList;
import java.util.List;
import stayrocks.jambh.vani.R;

public class AudioPlayerActivity extends AppCompatActivity {
    private SimpleExoPlayerView simpleExoPlayerView;
    private SimpleExoPlayer player;
    private ProgressBar progressBar;
    private DataSource.Factory mediaDataSourceFactory;
    private DefaultTrackSelector trackSelector;
    private boolean shouldAutoPlay;
    private Handler mainHandler;
    private BandwidthMeter bandwidthMeter;
    private List<String> songsList = new ArrayList<>();
    private SubtitleView subtitles;
    private String srt_link;
    private InterstitialAd mInterstitialAd;
    AdRequest adRequestheader;
    Boolean showAd=false;
    AdView adView;

    @Override
    protected void onStart() {
        super.onStart();
        adRequestheader = new AdRequest.Builder()
                .build();
        mInterstitialAd = new InterstitialAd(this);
        mInterstitialAd.setAdUnitId(getResources().getString(R.string.splash));
        mInterstitialAd.loadAd(adRequestheader);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_audio_player);

        if(getIntent()!=null){

            String song=getIntent().getStringExtra("link");
            srt_link=getIntent().getStringExtra("srt_link");
            songsList.add(song);

        }
        initRecyclerView();


    }
    private void initRecyclerView() {

        simpleExoPlayerView = (SimpleExoPlayerView) findViewById(R.id.player_view);
        subtitles=(SubtitleView)findViewById(R.id.subtitle);
        progressBar=(ProgressBar)findViewById(R.id.progress_bar);
        adView=(AdView)findViewById(R.id.adView);
        mainHandler = new Handler();
        bandwidthMeter = new DefaultBandwidthMeter();
        mediaDataSourceFactory = new DefaultDataSourceFactory(this, Util.getUserAgent(this, null));
        initializePlayer();

    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //player.setPlayWhenReady(false);
    }

    private void initializePlayer() {
        simpleExoPlayerView.requestFocus();
        TrackSelection.Factory videoTrackSelectionFactory =
                new AdaptiveTrackSelection.Factory(bandwidthMeter);
        trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);
        player = ExoPlayerFactory.newSimpleInstance(this, trackSelector);
        simpleExoPlayerView.setPlayer(player);
        simpleExoPlayerView.getSubtitleView().setVisibility(View.GONE);
        simpleExoPlayerView.setControllerHideOnTouch(true);
        simpleExoPlayerView.setControllerAutoShow(false);
        simpleExoPlayerView.setControllerShowTimeoutMs(0);
        simpleExoPlayerView.setUseArtwork(false);

        try {
            int l=songsList.size();
            MediaSource[] mediaSources = new MediaSource[l];
            for (int i = 0; i < l; i++) {
                Log.e("uri is", songsList.get(i));
                mediaSources[i] = buildMediaSource(Uri.parse(songsList.get(i)));
                Log.e("Media source is", mediaSources[i].toString());
            }

            //Log.e("Media source is", mediaSources.toString());
            MediaSource mediaSource = mediaSources.length == 1 ? mediaSources[0]
                    : new ConcatenatingMediaSource(mediaSources);
            player.prepare(mediaSource);

            //player.seekTo(0, C.TIME_UNSET);

            player.setPlayWhenReady(true);
           // player.setRepeatMode(Player.REPEAT_MODE_ALL);
            player.addTextOutput(new TextRenderer.Output() {
                @Override
                public void onCues(List< Cue> cues) {
                    Log.e("cues are ",cues.toString());
                    if(subtitles!=null){
                        subtitles.onCues(cues);
                    }
                }
            });
            player.addListener(new Player.EventListener() {
                @Override
                public void onTimelineChanged(Timeline timeline, Object manifest) {

                }

                @Override
                public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {

                }

                @Override
                public void onLoadingChanged(boolean isLoading) {
                    Log.e("",isLoading+" ");

                }

                @Override
                public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {

                    if (playbackState == Player.STATE_BUFFERING){
                        progressBar.setVisibility(View.VISIBLE);
                    } else {
                        progressBar.setVisibility(View.GONE);
                    }

                    switch(playbackState) {
                        case Player.STATE_BUFFERING:

                            break;
                        case Player.STATE_ENDED:
                            if(mInterstitialAd.isLoaded()){
                                mInterstitialAd.show();
                            }
                            //do what you want
                            break;
                        case Player.STATE_IDLE:
                            break;

                        case Player.STATE_READY:
                            try {
                                if (showAd) {
                                    adView.setVisibility(View.VISIBLE);
                                    adView.loadAd(adRequestheader);
                                } else {
                                    adView.setVisibility(View.GONE);
                                }
                            }catch (Exception e){
                                adView.setVisibility(View.GONE);
                            }
                            break;
                        default:
                            break;
                    }

                }

                @Override
                public void onRepeatModeChanged(int repeatMode) {

                }

                @Override
                public void onPlayerError(ExoPlaybackException error) {

                }

                @Override
                public void onPositionDiscontinuity() {

                }

                @Override
                public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {

                }
            });

           // player.setPlayWhenReady(true);
        }catch (Exception e){

            e.printStackTrace();
        }
    }

    private MediaSource buildMediaSource(Uri uri) {

        MediaSource mediaSource = new ExtractorMediaSource(uri, mediaDataSourceFactory, new DefaultExtractorsFactory(),
                mainHandler, null);
        // For subtitles

        if((srt_link==null|| srt_link=="false" || srt_link.isEmpty())){

            showAd=true;
            return mediaSource;
        }

        Format textFormat = Format.createTextSampleFormat(null, MimeTypes.APPLICATION_SUBRIP,
                Format.NO_VALUE,"hi");
        //Log.e("srt link is",srt_link);
        Uri uriSubtitle = Uri.parse(srt_link);
        MediaSource subtitleSource = new SingleSampleMediaSource(uriSubtitle, mediaDataSourceFactory, textFormat, C.TIME_UNSET);
        MergingMediaSource mergedSource = new MergingMediaSource(mediaSource, subtitleSource);

        return mergedSource;
    }

    private void releasePlayer() {
        if (player != null) {
            shouldAutoPlay = player.getPlayWhenReady();
            if(shouldAutoPlay){

                player.stop();
            }
            mainHandler=null;
            player.release();
            player = null;
            trackSelector = null;
        }
        // player.set
    }

    @Override
    public void onBackPressed() {

        releasePlayer();
        super.onBackPressed();
    }



}

Here in code I am doing three main things. Audio I want to play and Subtitle file link is coming from intent.Means from list of songs I am selecting particular song and then passing it’s audio link and subtitle link. As these are two media resources and We will combine them into one. You can go through buildMediaSource implementation .

Using simpleExoPlayerView.getSubtitleView().setVisibility(View.GONE); I am hiding default Subtitle View and using my own Subtitle View for better control.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:background="@color/black"
    android:orientation="vertical"
    android:keepScreenOn="true"
    xmlns:ads="http://schemas.android.com/apk/res-auto"
    >
<FrameLayout
            android:layout_height="0dp"
            android:layout_width="match_parent"
            android:layout_weight="1"
            >

        <com.google.android.exoplayer2.ui.SubtitleView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/subtitle"
            />

            <com.google.android.gms.ads.AdView
                android:id="@+id/adView"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center"
                ads:adSize="LARGE_BANNER"
                ads:adUnitId="@string/banner_ad_header" />

            <ProgressBar
                android:id="@+id/progress_bar"
                android:layout_width="48dp"
                android:layout_height="48dp"
                android:layout_gravity="center"/>

        </FrameLayout>

    <com.google.android.exoplayer2.ui.SimpleExoPlayerView
        android:id="@+id/player_view"
        android:layout_width="match_parent"
        android:layout_height="100dp" />




</LinearLayout>

I am using media player callbacks to show loader when it is taking time and keep user experience better.

Another thing I am doing here is that when audio completes I am showing a Splash Ad to get some revenue out of my hard work.

Thanks for reading this, hope you are going to enjoy it.


Tagged with:
CodeNextGen
Written by Sanjeev Kumar

Get notifications about new posts on Twitter, RSS or Email.