Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android MediaPlayer playback stutters over wired headphones, not over Bluetooth

I have a simple music player app (source) which has had playback issues in Lollipop when using headphones. Music will play normally for anywhere from 30 seconds to 5 minutes, then will pause for ~2-4 seconds, then resume.

The behavior seems to generally occur while the screen is off, but acquiring a CPU wakelock didn't help.

The frequency of the pauses seems to accelerate over time. At first it's once per hour, but then the time between pauses decreases by about half each time, until it's pausing almost every minute.

I've observed this behavior with iTunes encoded aac files, others have observed it with mp3s.

This has only been observed while playing over wired headphones. I have never experienced this behavior on a Bluetooth headset.

What could be causing this? It seems like a process priority issue, but I don't know how to address that kind of problem.

I haven't experienced this on Android 4.x.

Here's the Github ticket for this issue.

Here are some relevant bits of source code:

Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.smithdtyler.prettygoodmusicplayer"
    android:versionCode="65"
    android:versionName="3.2.14" >

    <uses-sdk
        android:minSdkVersion="16"
        android:targetSdkVersion="19" />

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_pgmp_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppBaseTheme" >

        <!-- Set the artist list to launch mode single task to prevent multiple instances -->
        <!-- This fixes an error where exiting the application just brings up another instance -->
        <!-- See https://developer.android.com/guide/topics/manifest/activity-element.html#lmode -->
        <activity
            android:name="com.smithdtyler.prettygoodmusicplayer.ArtistList"
            android:label="@string/app_name"
            android:launchMode="singleTask" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
                <category android:name="android.intent.category.CATEGORY_APP_MUSIC " />
            </intent-filter>
        </activity>
        <activity
            android:name="com.smithdtyler.prettygoodmusicplayer.SettingsActivity"
            android:label="@string/title_activity_settings" >
        </activity>
        <activity
            android:name="com.smithdtyler.prettygoodmusicplayer.AlbumList"
            android:label="@string/title_activity_album_list" >
        </activity>
        <activity
            android:name="com.smithdtyler.prettygoodmusicplayer.SongList"
            android:label="@string/title_activity_song_list" >
        </activity>
        <activity
            android:name="com.smithdtyler.prettygoodmusicplayer.NowPlaying"
            android:exported="true"
            android:label="@string/title_activity_now_playing" >
        </activity>
        <!--
        The service has android:exported="true" because that's needed for
        control from the notification. Not sure why it causes a warning...
        -->
        <service
            android:name="com.smithdtyler.prettygoodmusicplayer.MusicPlaybackService"
            android:exported="true"
            android:icon="@drawable/ic_pgmp_launcher" >
        </service>

        <receiver
            android:name="com.smithdtyler.prettygoodmusicplayer.MusicBroadcastReceiver"
            android:enabled="true" >
            <intent-filter android:priority="2147483647" >
                <action android:name="android.intent.action.MEDIA_BUTTON" />
            </intent-filter>
        </receiver>
    </application>

</manifest>

MusicPlaybackService.onCreate()

@Override
public synchronized void onCreate() {
    Log.i(TAG, "Music Playback Service Created!");
    isRunning = true;
    sharedPref = PreferenceManager.getDefaultSharedPreferences(this);

    powerManager =(PowerManager) getSystemService(POWER_SERVICE);
    wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
            "PGMPWakeLock");

    random = new Random();

    mp = new MediaPlayer();

    mp.setOnCompletionListener(new OnCompletionListener() {

        @Override
        public void onCompletion(MediaPlayer mp) {
            Log.i(TAG, "Song complete");
            next();
        }

    });

    // https://developer.android.com/training/managing-audio/audio-focus.html
    audioFocusListener = new PrettyGoodAudioFocusChangeListener();

    // Get permission to play audio
    am = (AudioManager) getBaseContext().getSystemService(
            Context.AUDIO_SERVICE);

    HandlerThread thread = new HandlerThread("ServiceStartArguments");
    thread.start();

    // Get the HandlerThread's Looper and use it for our Handler
    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);

    // https://stackoverflow.com/questions/19474116/the-constructor-notification-is-deprecated
    // https://stackoverflow.com/questions/6406730/updating-an-ongoing-notification-quietly/15538209#15538209
    Intent resultIntent = new Intent(this, NowPlaying.class);
    resultIntent.putExtra("From_Notification", true);
    resultIntent.putExtra(AlbumList.ALBUM_NAME, album);
    resultIntent.putExtra(ArtistList.ARTIST_NAME, artist);
    resultIntent.putExtra(ArtistList.ARTIST_ABS_PATH_NAME, artistAbsPath);

    // Use the FLAG_ACTIVITY_CLEAR_TOP to prevent launching a second
    // NowPlaying if one already exists.
    resultIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
    PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
            resultIntent, 0);

    Builder builder = new NotificationCompat.Builder(
            this.getApplicationContext());

    String contentText = getResources().getString(R.string.ticker_text);
    if (songFile != null) {
        contentText = Utils.getPrettySongName(songFile);
    }

    Notification notification = builder
            .setContentText(contentText)
            .setSmallIcon(R.drawable.ic_pgmp_launcher)
            .setWhen(System.currentTimeMillis())
            .setContentIntent(pendingIntent)
            .setContentTitle(
                    getResources().getString(R.string.notification_title))
                    .build();

    startForeground(uniqueid, notification);

    timer = new Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
        public void run() {
            onTimerTick();
        }
    }, 0, 500L);

    Log.i(TAG, "Registering event receiver");
    mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
    // Apparently audio registration is persistent across lots of things...
    // restarts, installs, etc.
    mAudioManager.registerMediaButtonEventReceiver(cn);
    // I tried to register this in the manifest, but it doesn't seen to
    // accept it, so I'll do it this way.
    getApplicationContext().registerReceiver(receiver, filter);

    headphoneReceiver = new HeadphoneBroadcastReceiver();
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction("android.intent.action.HEADSET_PLUG");
    registerReceiver(headphoneReceiver, filter);
}

MusicPlaybackService.startPlayingFile()

private synchronized void startPlayingFile(int songProgress) {
    // Have we loaded a file yet?
    if (mp.getDuration() > 0) {
        pause();
        mp.stop();
        mp.reset();
    }

    // open the file, pass it into the mp
    try {
        fis = new FileInputStream(songFile);
        mp.setDataSource(fis.getFD());
        mp.prepare();
        if(songProgress > 0){
            mp.seekTo(songProgress);
        }
        wakeLock.acquire();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IllegalArgumentException e) {
        e.printStackTrace();
    } catch (IllegalStateException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

MusicPlaybackService Timer Task

private void onTimerTick() {
    long currentTime = System.currentTimeMillis();
    if (pauseTime < currentTime) {
        pause();
    }
    updateResumePosition();
    sendUpdateToClients();
}

private void updateResumePosition(){
    long currentTime = System.currentTimeMillis();
    if(currentTime - 10000 > lastResumeUpdateTime){
        if(mp != null && songFile != null && mp.isPlaying()){
            int pos = mp.getCurrentPosition();
            SharedPreferences prefs = getSharedPreferences("PrettyGoodMusicPlayer", MODE_PRIVATE);
            Log.i(TAG,
                    "Preferences update success: "
                            + prefs.edit()
                            .putString(songFile.getParentFile().getAbsolutePath(),songFile.getName() + "~" + pos)
                            .commit());
        }
        lastResumeUpdateTime = currentTime;
    }
}


private void sendUpdateToClients() {
    List<Messenger> toRemove = new ArrayList<Messenger>();
    synchronized (mClients) {
        for (Messenger client : mClients) {
            Message msg = Message.obtain(null, MSG_SERVICE_STATUS);
            Bundle b = new Bundle();
            if (songFile != null) {
                b.putString(PRETTY_SONG_NAME,
                        Utils.getPrettySongName(songFile));
                b.putString(PRETTY_ALBUM_NAME, songFile.getParentFile()
                        .getName());
                b.putString(PRETTY_ARTIST_NAME, songFile.getParentFile()
                        .getParentFile().getName());
            } else {
                // songFile can be null while we're shutting down.
                b.putString(PRETTY_SONG_NAME, " ");
                b.putString(PRETTY_ALBUM_NAME, " ");
                b.putString(PRETTY_ARTIST_NAME, " ");
            }

            b.putBoolean(IS_SHUFFLING, this._shuffle);

            if (mp.isPlaying()) {
                b.putInt(PLAYBACK_STATE, PlaybackState.PLAYING.ordinal());
            } else {
                b.putInt(PLAYBACK_STATE, PlaybackState.PAUSED.ordinal());
            }
            // We might not be able to send the position right away if mp is
            // still being created
            // so instead let's send the last position we knew about.
            if (mp.isPlaying()) {
                lastDuration = mp.getDuration();
                lastPosition = mp.getCurrentPosition();
            }
            b.putInt(TRACK_DURATION, lastDuration);
            b.putInt(TRACK_POSITION, lastPosition);
            msg.setData(b);
            try {
                client.send(msg);
            } catch (RemoteException e) {
                e.printStackTrace();
                toRemove.add(client);
            }
        }

        for (Messenger remove : toRemove) {
            mClients.remove(remove);
        }
    }
}
like image 294
T.D. Smith Avatar asked Oct 13 '15 10:10

T.D. Smith


1 Answers

I got a really helpful response from the developer of the Vanilla Music Player:

We use a separated thread to read-ahead the currently playing file:

-> The thread reads the file with about 256kb/s, so it will read the file faster than mediaserver does -> This gives the file a very good chance to stay in the page/disk cache -> ..and this minimizes the chance for 'drop outs' due to funky sd-cards or other IO-pauses.

The code is located here: https://github.com/vanilla-music/vanilla/blob/master/src/ch/blinkenlights/android/vanilla/ReadaheadThread.java The code does not depend on any parts of vanilla music: if you would like to give it a try, just drop it into your project and do something like:

onCreate {
 ...
 mReadaheadThread = new ReadaheadThread()
 ...
}
...
mMediaPlayer.setDataSource(path);
mReadaheadThread.setDataSource(path);
...

Since implementing this change I haven't encountered the problem.

like image 69
T.D. Smith Avatar answered Nov 11 '22 00:11

T.D. Smith