Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best practices for audio streaming

I'm writing an application to play audio from remote server. I tried several ways to implement streaming audio, but they all are not good enough for me. That's what I've tried:

Naive using of MediaPlayer

Something like:

MediaPlayer player = new MediaPlayer(); 
player.setDataSource(context, Uri.parse("http://whatever.com/track.mp3"));
player.prepare();
player.start();

(or prepareAsync, no matter)

But standard MediaPlayer is quite unstable when playing remote content. It is often falls or stops playback and I can't process this. On the other side, I want to implement media caching. But I haven't found any way to get buffered content from MediaPlayer to save it somewhere on device.

Implementing custom buffering

Then there became an idea to download media file by chunks, combine them into one local file and play this file. Downloading the whole file can be slow because of bad connection, so it will be fine to download enough initially piece, then start playback and continue downloading and appending local file. Besides, we get caching functionality.

Sounds like a plan, but it didn't always work. It works perfectly on HTC Sensation XE but didn't on 4.1 tablet playback stopped after finishing this initial piece. Don't know, why is so. I've asked question about this, but received no answers.

Using two MediaPlayers

I've created two MediaPlayer instances and tried to make them change each other. The logic is following:

  • Start downloading initial piece of media
  • When it is downloaded, start playback via currentMediaPlayer. The rest of media continues downloading
  • When downloaded piece is almost played (1 sec before finish), prepare secondaryMediaPlayer with the same source file (as it was appended during playback)
  • 261 ms before finish of currentMediaPlayer – pause it, start secondary, set secondary as current, schedule preparing of next secondary player.

The source:

private static final String FILE_NAME="local.mp3";
private static final String URL = ...;
private static final long FILE_SIZE = 7084032;

private static final long PREPARE_NEXT_PLAYER_OFFSET = 1000;
private static final int START_NEXT_OFFSET = 261;

private static final int INIT_PERCENTAGE = 3;

private MediaPlayer mPlayer;
private MediaPlayer mSecondaryPlayer;

private Handler mHandler = new Handler();

public void startDownload() {
    mDownloader = new Mp3Downloader(FILE_NAME, URL, getExternalCacheDir());
    mDownloader.setDownloadListener(mInitDownloadListener);
    mDownloader.startDownload();
}


private Mp3Downloader.DownloadListener mInitDownloadListener = new Mp3Downloader.DownloadListener() {
    public void onDownloaded(long bytes) {
        int percentage = Math.round(bytes * 100f / FILE_SIZE);

        // Start playback when appropriate piece of media downloaded
        if (percentage >= INIT_PERCENTAGE) {
            mPlayer = new MediaPlayer();
            try {
                mPlayer.setDataSource(mDownloader.getDownloadingFile().getAbsolutePath());
                mPlayer.prepare();
                mPlayer.start();

                mHandler.postDelayed(prepareSecondaryPlayerRunnable, mPlayer.getDuration() - PREPARE_NEXT_PLAYER_OFFSET);
                mHandler.postDelayed(startNextPlayerRunnable, mPlayer.getDuration() - START_NEXT_OFFSET);

            } catch (IOException e) {
                Log.e(e);
            }

            mDownloader.setDownloadListener(null);
        }
    }
};

// Starting to prepare secondary MediaPlayer
private Runnable prepareSecondaryPlayerRunnable = new Runnable() {
    public void run() {
        mSecondaryPlayer = new MediaPlayer();
        try {
            mSecondaryPlayer.setDataSource(mDownloader.getDownloadingFile().getAbsolutePath());
            mSecondaryPlayer.prepare();
            mSecondaryPlayer.seekTo(mPlayer.getDuration() - START_NEXT_OFFSET);

        } catch (IOException e) {
            Log.e(e);
        }
    }
};

// Starting secondary MediaPlayer playback, scheduling creating next MediaPlayer
private Runnable startNextPlayerRunnable = new Runnable() {
    public void run() {
        mSecondaryPlayer.start();

        mHandler.postDelayed(prepareSecondaryPlayerRunnable, mSecondaryPlayer.getDuration() - mPlayer.getCurrentPosition() - PREPARE_NEXT_PLAYER_OFFSET);
        mHandler.postDelayed(startNextPlayerRunnable, mSecondaryPlayer.getDuration() - mPlayer.getCurrentPosition() - START_NEXT_OFFSET);

        mPlayer.pause();
        mPlayer.release();
        
        mPlayer = mSecondaryPlayer;

    }
};

Again – sounds, like a plan, but works not perfectly. The moments of switching MediaPlayers are quite hearable. Here I have opposite situation: on 4.1 tablet it's ok, but on HTC Sensation there are evident lags.

I also tried to implement different download techniques. I've implemented download by 10Kb chunks and by MP3 frames. I don't know exactly, but it seems that in case of MP3 frames seekTo and start work better. But it's just a feeling, I don't know explanation.

StreamingMediaPlayer

I saw to this word several times while googling, and found this implementation: https://code.google.com/p/mynpr/source/browse/trunk/mynpr/src/com/webeclubbin/mynpr/StreamingMediaPlayer.java?r=18

It is a solution everybody use?

If yes, it's sad, because it is not working good for me too. And I don't see any fresh ideas in implementation.

So, the question

How do you guys implement audio streaming in your applications? I don't beleive I am the only person who faced problems like this. There should be some good practices.

like image 450
darja Avatar asked May 06 '13 18:05

darja


1 Answers

In my case I use FFMPEG with OpenSL ES. The disadvantage is complexity. You must be familiar with a lot of things: JNI, OpenSL, FFMPEG. It's also hard to debug(comparing with pure java android app). In your case I suggest you to try low level Media API. The only thing is lack of examples. But there is a unit test which shows how you can handle audio(you need to change InputStream reference - line 82).

like image 91
Eugene Avatar answered Sep 22 '22 21:09

Eugene