package de.danoeh.antennapod.core.service.playback; import android.content.Context; import android.media.AudioManager; import android.os.PowerManager; import android.support.annotation.NonNull; import android.telephony.TelephonyManager; import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; import org.antennapod.audio.MediaPlayer; import java.io.IOException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.RewindAfterPauseUtils; import de.danoeh.antennapod.core.util.playback.AudioPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.core.util.playback.Playable; import de.danoeh.antennapod.core.util.playback.VideoPlayer; /** * Manages the MediaPlayer object of the PlaybackService. */ public class LocalPSMP extends PlaybackServiceMediaPlayer { public static final String TAG = "LclPlaybackSvcMPlayer"; private final AudioManager audioManager; private volatile PlayerStatus statusBeforeSeeking; private volatile IPlayer mediaPlayer; private volatile Playable media; private volatile boolean stream; private volatile MediaType mediaType; private volatile AtomicBoolean startWhenPrepared; private volatile boolean pausedBecauseOfTransientAudiofocusLoss; private volatile Pair<Integer, Integer> videoSize; /** * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads * have to wait until these operations have finished. */ private final ReentrantLock playerLock; private CountDownLatch seekLatch; private final ThreadPoolExecutor executor; public LocalPSMP(@NonNull Context context, @NonNull PSMPCallback callback) { super(context, callback); this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); this.playerLock = new ReentrantLock(); this.startWhenPrepared = new AtomicBoolean(false); executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<>(), (r, executor) -> Log.d(TAG, "Rejected execution of runnable")); mediaPlayer = null; statusBeforeSeeking = null; pausedBecauseOfTransientAudiofocusLoss = false; mediaType = MediaType.UNKNOWN; videoSize = null; } /** * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will * not do anything. * Whether playback starts immediately depends on the given parameters. See below for more details. * <p/> * States: * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. * <p/> * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. * <p/> * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object * will enter the ERROR state. * <p/> * This method is executed on an internal executor service. * * @param playable The Playable object that is supposed to be played. This parameter must not be null. * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by * the Android MediaPlayer via getStreamUrl. * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared * for playback immediately (see 'prepareImmediately' parameter for more details) * @param prepareImmediately Set to true if the method should also prepare the episode for playback. */ @Override public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { Log.d(TAG, "playMediaObject(...)"); executor.submit(() -> { playerLock.lock(); try { playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); } catch (RuntimeException e) { e.printStackTrace(); throw e; } finally { playerLock.unlock(); } }); } /** * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if * the given playable parameter is the same object as the currently playing media. * <p/> * This method requires the playerLock and is executed on the caller's thread. * * @see #playMediaObject(de.danoeh.antennapod.core.util.playback.Playable, boolean, boolean, boolean) */ private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { if (!playerLock.isHeldByCurrentThread()) { throw new IllegalStateException("method requires playerLock"); } if (media != null) { if (!forceReset && media.getIdentifier().equals(playable.getIdentifier()) && playerStatus == PlayerStatus.PLAYING) { // episode is already playing -> ignore method call Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); return; } else { // stop playback of this episode if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { mediaPlayer.stop(); } // set temporarily to pause in order to update list with current position if (playerStatus == PlayerStatus.PLAYING) { setPlayerStatus(PlayerStatus.PAUSED, media); } smartMarkAsPlayed(media); setPlayerStatus(PlayerStatus.INDETERMINATE, null); } } this.media = playable; this.stream = stream; this.mediaType = media.getMediaType(); this.videoSize = null; createMediaPlayer(); LocalPSMP.this.startWhenPrepared.set(startWhenPrepared); setPlayerStatus(PlayerStatus.INITIALIZING, media); try { media.loadMetadata(); callback.onMediaChanged(false); if (stream) { mediaPlayer.setDataSource(media.getStreamUrl()); } else { mediaPlayer.setDataSource(media.getLocalMediaUrl()); } setPlayerStatus(PlayerStatus.INITIALIZED, media); if (prepareImmediately) { setPlayerStatus(PlayerStatus.PREPARING, media); mediaPlayer.prepare(); onPrepared(startWhenPrepared); } } catch (Playable.PlayableException | IOException | IllegalStateException e) { e.printStackTrace(); setPlayerStatus(PlayerStatus.ERROR, null); } } /** * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. * nothing will happen. * <p/> * This method is executed on an internal executor service. */ @Override public void resume() { executor.submit(() -> { playerLock.lock(); resumeSync(); playerLock.unlock(); }); } private void resumeSync() { if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { int focusGained = audioManager.requestAudioFocus( audioFocusChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { acquireWifiLockIfNecessary(); float speed = 1.0f; try { speed = Float.parseFloat(UserPreferences.getPlaybackSpeed()); } catch(NumberFormatException e) { Log.e(TAG, Log.getStackTraceString(e)); UserPreferences.setPlaybackSpeed(String.valueOf(speed)); } setSpeed(speed); setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume()); if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind( media.getPosition(), media.getLastPlayedTime()); seekToSync(newPosition); } mediaPlayer.start(); setPlayerStatus(PlayerStatus.PLAYING, media); pausedBecauseOfTransientAudiofocusLoss = false; media.onPlaybackStart(); } else { Log.e(TAG, "Failed to request audio focus"); } } else { Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); } } /** * Saves the current position and pauses playback. Note that, if audiofocus * is abandoned, the lockscreen controls will also disapear. * <p/> * This method is executed on an internal executor service. * * @param abandonFocus is true if the service should release audio focus * @param reinit is true if service should reinit after pausing if the media * file is being streamed */ @Override public void pause(final boolean abandonFocus, final boolean reinit) { executor.submit(() -> { playerLock.lock(); releaseWifiLockIfNecessary(); if (playerStatus == PlayerStatus.PLAYING) { Log.d(TAG, "Pausing playback."); mediaPlayer.pause(); setPlayerStatus(PlayerStatus.PAUSED, media); if (abandonFocus) { audioManager.abandonAudioFocus(audioFocusChangeListener); pausedBecauseOfTransientAudiofocusLoss = false; } if (stream && reinit) { reinit(); } } else { Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); } playerLock.unlock(); }); } /** * Prepares media player for playback if the service is in the INITALIZED * state. * <p/> * This method is executed on an internal executor service. */ @Override public void prepare() { executor.submit(() -> { playerLock.lock(); if (playerStatus == PlayerStatus.INITIALIZED) { Log.d(TAG, "Preparing media player"); setPlayerStatus(PlayerStatus.PREPARING, media); try { mediaPlayer.prepare(); onPrepared(startWhenPrepared.get()); } catch (IOException e) { e.printStackTrace(); setPlayerStatus(PlayerStatus.ERROR, null); } } playerLock.unlock(); }); } /** * Called after media player has been prepared. This method is executed on the caller's thread. */ void onPrepared(final boolean startWhenPrepared) { playerLock.lock(); if (playerStatus != PlayerStatus.PREPARING) { playerLock.unlock(); throw new IllegalStateException("Player is not in PREPARING state"); } Log.d(TAG, "Resource prepared"); if (mediaType == MediaType.VIDEO) { VideoPlayer vp = (VideoPlayer) mediaPlayer; videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); } if (media.getPosition() > 0) { seekToSync(media.getPosition()); } if (media.getDuration() == 0) { Log.d(TAG, "Setting duration of media"); media.setDuration(mediaPlayer.getDuration()); } setPlayerStatus(PlayerStatus.PREPARED, media); if (startWhenPrepared) { resumeSync(); } playerLock.unlock(); } /** * Resets the media player and moves it into INITIALIZED state. * <p/> * This method is executed on an internal executor service. */ @Override public void reinit() { executor.submit(() -> { playerLock.lock(); Log.d(TAG, "reinit()"); releaseWifiLockIfNecessary(); if (media != null) { playMediaObject(media, true, stream, startWhenPrepared.get(), false); } else if (mediaPlayer != null) { mediaPlayer.reset(); } else { Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); } playerLock.unlock(); }); } /** * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. * * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0 * <p/> * This method is executed on the caller's thread. */ private void seekToSync(int t) { if (t < 0) { t = 0; } playerLock.lock(); if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { if (!stream) { statusBeforeSeeking = playerStatus; setPlayerStatus(PlayerStatus.SEEKING, media); } if(seekLatch != null && seekLatch.getCount() > 0) { try { seekLatch.await(3, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.e(TAG, Log.getStackTraceString(e)); } } seekLatch = new CountDownLatch(1); mediaPlayer.seekTo(t); try { seekLatch.await(3, TimeUnit.SECONDS); } catch (InterruptedException e) { Log.e(TAG, Log.getStackTraceString(e)); } } else if (playerStatus == PlayerStatus.INITIALIZED) { media.setPosition(t); startWhenPrepared.set(false); prepare(); } playerLock.unlock(); } /** * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. * Invalid time values (< 0) will be ignored. * <p/> * This method is executed on an internal executor service. */ @Override public void seekTo(final int t) { executor.submit(() -> seekToSync(t)); } /** * Seek a specific position from the current position * * @param d offset from current position (positive or negative) */ @Override public void seekDelta(final int d) { executor.submit(() -> { playerLock.lock(); int currentPosition = getPosition(); if (currentPosition != INVALID_TIME) { seekToSync(currentPosition + d); } else { Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); } playerLock.unlock(); }); } /** * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. */ @Override public int getDuration() { if (!playerLock.tryLock()) { return INVALID_TIME; } int retVal = INVALID_TIME; if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { retVal = mediaPlayer.getDuration(); } else if (media != null && media.getDuration() > 0) { retVal = media.getDuration(); } playerLock.unlock(); return retVal; } /** * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. */ @Override public int getPosition() { try { if (!playerLock.tryLock(50, TimeUnit.MILLISECONDS)) { return INVALID_TIME; } } catch (InterruptedException e) { return INVALID_TIME; } int retVal = INVALID_TIME; if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) { retVal = mediaPlayer.getCurrentPosition(); } if (retVal <= 0 && media != null && media.getPosition() >= 0) { retVal = media.getPosition(); } playerLock.unlock(); Log.d(TAG, "getPosition() -> " + retVal); return retVal; } @Override public boolean isStartWhenPrepared() { return startWhenPrepared.get(); } @Override public void setStartWhenPrepared(boolean startWhenPrepared) { this.startWhenPrepared.set(startWhenPrepared); } /** * Returns true if the playback speed can be adjusted. */ @Override public boolean canSetSpeed() { boolean retVal = false; if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { retVal = (mediaPlayer).canSetSpeed(); } return retVal; } /** * Sets the playback speed. * This method is executed on the caller's thread. */ private void setSpeedSync(float speed) { playerLock.lock(); if (media != null && media.getMediaType() == MediaType.AUDIO) { if (mediaPlayer.canSetSpeed()) { mediaPlayer.setPlaybackSpeed(speed); Log.d(TAG, "Playback speed was set to " + speed); callback.playbackSpeedChanged(speed); } } playerLock.unlock(); } /** * Sets the playback speed. * This method is executed on an internal executor service. */ @Override public void setSpeed(final float speed) { executor.submit(() -> setSpeedSync(speed)); } /** * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. */ @Override public float getPlaybackSpeed() { if (!playerLock.tryLock()) { return 1; } float retVal = 1; if ((playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) { retVal = mediaPlayer.getCurrentSpeedMultiplier(); } playerLock.unlock(); return retVal; } /** * Sets the playback volume. * This method is executed on an internal executor service. */ @Override public void setVolume(final float volumeLeft, float volumeRight) { executor.submit(() -> setVolumeSync(volumeLeft, volumeRight)); } /** * Sets the playback volume. * This method is executed on the caller's thread. */ private void setVolumeSync(float volumeLeft, float volumeRight) { playerLock.lock(); if (media != null && media.getMediaType() == MediaType.AUDIO) { mediaPlayer.setVolume(volumeLeft, volumeRight); Log.d(TAG, "Media player volume was set to " + volumeLeft + " " + volumeRight); } playerLock.unlock(); } /** * Returns true if the mediaplayer can mix stereo down to mono */ @Override public boolean canDownmix() { boolean retVal = false; if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { retVal = mediaPlayer.canDownmix(); } return retVal; } @Override public void setDownmix(boolean enable) { playerLock.lock(); if (media != null && media.getMediaType() == MediaType.AUDIO) { mediaPlayer.setDownmix(enable); Log.d(TAG, "Media player downmix was set to " + enable); } playerLock.unlock(); } @Override public MediaType getCurrentMediaType() { return mediaType; } @Override public boolean isStreaming() { return stream; } /** * Releases internally used resources. This method should only be called when the object is not used anymore. */ @Override public void shutdown() { executor.shutdown(); if (mediaPlayer != null) { mediaPlayer.release(); } releaseWifiLockIfNecessary(); } /** * Releases internally used resources. This method should only be called when the object is not used anymore. * This method is executed on an internal executor service. */ @Override public void shutdownQuietly() { executor.submit(this::shutdown); executor.shutdown(); } @Override public void setVideoSurface(final SurfaceHolder surface) { executor.submit(() -> { playerLock.lock(); if (mediaPlayer != null) { mediaPlayer.setDisplay(surface); } playerLock.unlock(); }); } @Override public void resetVideoSurface() { executor.submit(() -> { playerLock.lock(); if (mediaType == MediaType.VIDEO) { Log.d(TAG, "Resetting video surface"); mediaPlayer.setDisplay(null); reinit(); } else { Log.e(TAG, "Resetting video surface for media of Audio type"); } playerLock.unlock(); }); } /** * Return width and height of the currently playing video as a pair. * * @return Width and height as a Pair or null if the video size could not be determined. The method might still * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return * invalid values. */ @Override public Pair<Integer, Integer> getVideoSize() { if (!playerLock.tryLock()) { // use cached value if lock can't be aquired return videoSize; } Pair<Integer, Integer> res; if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { res = null; } else { VideoPlayer vp = (VideoPlayer) mediaPlayer; videoSize = new Pair<>(vp.getVideoWidth(), vp.getVideoHeight()); res = videoSize; } playerLock.unlock(); return res; } /** * Returns the current media, if you need the media and the player status together, you should * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition * could result in nonsensical results (like a status of PLAYING, but a null playable) * @return the current media. May be null */ @Override public Playable getPlayable() { return media; } @Override protected void setPlayable(Playable playable) { media = playable; } private IPlayer createMediaPlayer() { if (mediaPlayer != null) { mediaPlayer.release(); } if (media == null || media.getMediaType() == MediaType.VIDEO) { mediaPlayer = new VideoPlayer(); } else { mediaPlayer = new AudioPlayer(context); } mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); return setMediaPlayerListeners(mediaPlayer); } private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { @Override public void onAudioFocusChange(final int focusChange) { executor.submit(new Runnable() { @Override public void run() { playerLock.lock(); // If there is an incoming call, playback should be paused permanently TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); final int callState = (tm != null) ? tm.getCallState() : 0; Log.i(TAG, "Call state:" + callState); if (focusChange == AudioManager.AUDIOFOCUS_LOSS || (!UserPreferences.shouldResumeAfterCall() && callState != TelephonyManager.CALL_STATE_IDLE)) { Log.d(TAG, "Lost audio focus"); pause(true, false); callback.shouldStop(); } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { Log.d(TAG, "Gained audio focus"); if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now resume(); } else { // we ducked => raise audio level back setVolumeSync(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume()); } } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { if (playerStatus == PlayerStatus.PLAYING) { if (!UserPreferences.shouldPauseForFocusLoss()) { Log.d(TAG, "Lost audio focus temporarily. Ducking..."); final float DUCK_FACTOR = 0.25f; setVolumeSync(DUCK_FACTOR * UserPreferences.getLeftVolume(), DUCK_FACTOR * UserPreferences.getRightVolume()); pausedBecauseOfTransientAudiofocusLoss = false; } else { Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); pause(false, false); pausedBecauseOfTransientAudiofocusLoss = true; } } } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { if (playerStatus == PlayerStatus.PLAYING) { Log.d(TAG, "Lost audio focus temporarily. Pausing..."); pause(false, false); pausedBecauseOfTransientAudiofocusLoss = true; } } playerLock.unlock(); } }); } }; @Override public void endPlayback(final boolean wasSkipped, boolean switchingPlayers) { executor.submit(() -> { playerLock.lock(); releaseWifiLockIfNecessary(); boolean isPlaying = playerStatus == PlayerStatus.PLAYING; if (playerStatus != PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.INDETERMINATE, media); } if (mediaPlayer != null) { mediaPlayer.reset(); } audioManager.abandonAudioFocus(audioFocusChangeListener); callback.endPlayback(media, isPlaying, wasSkipped, switchingPlayers); playerLock.unlock(); }); } /** * Moves the LocalPSMP into STOPPED state. This call is only valid if the player is currently in * INDETERMINATE state, for example after a call to endPlayback. * This method will only take care of changing the PlayerStatus of this object! Other tasks like * abandoning audio focus have to be done with other methods. */ @Override public void stop() { executor.submit(() -> { playerLock.lock(); releaseWifiLockIfNecessary(); if (playerStatus == PlayerStatus.INDETERMINATE) { setPlayerStatus(PlayerStatus.STOPPED, null); } else { Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); } playerLock.unlock(); }); } @Override protected boolean shouldLockWifi(){ return stream; } private IPlayer setMediaPlayerListeners(IPlayer mp) { if (mp != null && media != null) { if (media.getMediaType() == MediaType.AUDIO) { ((AudioPlayer) mp) .setOnCompletionListener(audioCompletionListener); ((AudioPlayer) mp) .setOnSeekCompleteListener(audioSeekCompleteListener); ((AudioPlayer) mp).setOnErrorListener(audioErrorListener); ((AudioPlayer) mp) .setOnBufferingUpdateListener(audioBufferingUpdateListener); ((AudioPlayer) mp).setOnInfoListener(audioInfoListener); ((AudioPlayer) mp).setOnSpeedAdjustmentAvailableChangedListener(audioSetSpeedAbilityListener); } else { ((VideoPlayer) mp) .setOnCompletionListener(videoCompletionListener); ((VideoPlayer) mp) .setOnSeekCompleteListener(videoSeekCompleteListener); ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); ((VideoPlayer) mp) .setOnBufferingUpdateListener(videoBufferingUpdateListener); ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); } } return mp; } private final MediaPlayer.OnCompletionListener audioCompletionListener = mp -> genericOnCompletion(); private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = mp -> genericOnCompletion(); private void genericOnCompletion() { endPlayback(false, false); } private final MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = (mp, percent) -> genericOnBufferingUpdate(percent); private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = (mp, percent) -> genericOnBufferingUpdate(percent); private void genericOnBufferingUpdate(int percent) { callback.onBufferingUpdate(percent); } private final MediaPlayer.OnInfoListener audioInfoListener = (mp, what, extra) -> genericInfoListener(what); private final android.media.MediaPlayer.OnInfoListener videoInfoListener = (mp, what, extra) -> genericInfoListener(what); private boolean genericInfoListener(int what) { return callback.onMediaPlayerInfo(what, 0); } private final MediaPlayer.OnSpeedAdjustmentAvailableChangedListener audioSetSpeedAbilityListener = (arg0, speedAdjustmentAvailable) -> callback.setSpeedAbilityChanged(); private final MediaPlayer.OnErrorListener audioErrorListener = (mp, what, extra) -> { if(mp.canFallback()) { mp.fallback(); return true; } else { return genericOnError(mp, what, extra); } }; private final android.media.MediaPlayer.OnErrorListener videoErrorListener = this::genericOnError; private boolean genericOnError(Object inObj, int what, int extra) { return callback.onMediaPlayerError(inObj, what, extra); } private final MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = mp -> genericSeekCompleteListener(); private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = mp -> genericSeekCompleteListener(); private void genericSeekCompleteListener() { Thread t = new Thread(() -> { Log.d(TAG, "genericSeekCompleteListener"); if(seekLatch != null) { seekLatch.countDown(); } playerLock.lock(); if (playerStatus == PlayerStatus.SEEKING) { setPlayerStatus(statusBeforeSeeking, media); } playerLock.unlock(); }); t.start(); } }