Воспроизведение Android MediaPlayer заикается по проводным наушникам, а не по Bluetooth

У меня есть простое приложение для музыкального плеера (источник), у которого были проблемы с воспроизведением в Lollipop при использовании наушников. Музыка будет воспроизводиться в нормальном режиме от 30 секунд до 5 минут, затем пауза в течение ~ 2-4 секунд, затем возобновление.

Поведение, как правило, происходит, когда экран выключен, но приобретение процессора wakelock не помогло.

Частота пауз, по-видимому, ускоряется с течением времени. Сначала это раз в час, но затем время между паузами уменьшается примерно на половину каждый раз, пока оно не остановится почти каждую минуту.

Я наблюдал это поведение с iTunes закодированными файлами aac, другие наблюдали это с помощью mp3.

Это наблюдается только при прослушивании проводных наушников. Я никогда не испытывал такого поведения на Bluetooth-гарнитуре.

Что может быть причиной этого? Это похоже на проблему с приоритетом процесса, но я не знаю, как решить эту проблему.

Я не испытывал этого на Android 4.x.

Здесь билет Github для этой проблемы.

Вот некоторые соответствующие биты исходного кода:

манифеста

<?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 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 Looper and use it for our Handler
    mServiceLooper = thread.getLooper();
    mServiceHandler = new ServiceHandler(mServiceLooper);

    // https://stackoverflow.com/info/19474116/the-constructor-notification-is-deprecated
    // https://stackoverflow.com/info/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

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 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);
        }
    }
}

Ответ 1

Я получил действительно полезный ответ от разработчика Vanilla Music Игрок:

Мы используем выделенный поток для чтения текущего воспроизводимого файла:

- > Поток читает файл примерно с 256 кб/с, поэтому он будет читать файл быстрее, чем медиасервер - > Это дает файлу очень хороший шанс остаться в кеше страницы/диска - > .. и это минимизирует вероятность "выпадений" из-за фанковых sd-карт или других IO-пауз.

Код находится здесь: https://github.com/vanilla-music/vanilla/blob/master/src/ch/blinkenlights/android/vanilla/ReadaheadThread.java Код не зависит от каких-либо частей ванильной музыки: если вы хотите попробовать, просто добавьте его в свой проект и сделайте что-то вроде:

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

После реализации этого изменения я не столкнулся с проблемой.