package de.danoeh.antennapod.core.service.playback;
import android.content.Context;
import android.net.wifi.WifiManager;
import android.support.annotation.NonNull;
import android.support.annotation.StringRes;
import android.util.Log;
import android.util.Pair;
import android.view.SurfaceHolder;
import de.danoeh.antennapod.core.feed.Chapter;
import de.danoeh.antennapod.core.feed.FeedItem;
import de.danoeh.antennapod.core.feed.FeedMedia;
import de.danoeh.antennapod.core.feed.MediaType;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.util.playback.Playable;
/*
* An inconvenience of an implementation like this is that some members and methods that once were
* private are now protected, allowing for access from classes of the same package, namely
* PlaybackService. A workaround would be to move this to a dedicated package.
*/
/**
* Abstract class that allows for different implementations of the PlaybackServiceMediaPlayer for local
* and remote (cast devices) playback.
*/
public abstract class PlaybackServiceMediaPlayer {
public static final String TAG = "PlaybackSvcMediaPlayer";
/**
* Return value of some PSMP methods if the method call failed.
*/
public static final int INVALID_TIME = -1;
protected volatile PlayerStatus playerStatus;
/**
* A wifi-lock that is acquired if the media file is being streamed.
*/
private WifiManager.WifiLock wifiLock;
protected final PSMPCallback callback;
protected final Context context;
public PlaybackServiceMediaPlayer(@NonNull Context context,
@NonNull PSMPCallback callback){
this.context = context;
this.callback = callback;
playerStatus = PlayerStatus.STOPPED;
}
/**
* 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.
*/
public abstract void playMediaObject(@NonNull Playable playable, boolean stream, boolean startWhenPrepared, boolean prepareImmediately);
/**
* 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.
*/
public abstract void resume();
/**
* 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
*/
public abstract void pause(boolean abandonFocus, boolean reinit);
/**
* Prepared media player for playback if the service is in the INITALIZED
* state.
* <p/>
* This method is executed on an internal executor service.
*/
public abstract void prepare();
/**
* Resets the media player and moves it into INITIALIZED state.
* <p/>
* This method is executed on an internal executor service.
*/
public abstract void reinit();
/**
* 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.
*/
public abstract void seekTo(int t);
/**
* Seek a specific position from the current position
*
* @param d offset from current position (positive or negative)
*/
public abstract void seekDelta(int d);
/**
* Seek to the start of the specified chapter.
*/
public void seekToChapter(@NonNull Chapter c) {
seekTo((int) c.getStart());
}
/**
* Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved.
*/
public abstract int getDuration();
/**
* Returns the position of the current media object or INVALID_TIME if the position could not be retrieved.
*/
public abstract int getPosition();
public abstract boolean isStartWhenPrepared();
public abstract void setStartWhenPrepared(boolean startWhenPrepared);
/**
* Returns true if the playback speed can be adjusted.
*/
public abstract boolean canSetSpeed();
/**
* Sets the playback speed.
* This method is executed on an internal executor service.
*/
public abstract void setSpeed(float speed);
/**
* Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned.
*/
public abstract float getPlaybackSpeed();
/**
* Sets the playback volume.
* This method is executed on an internal executor service.
*/
public abstract void setVolume(float volumeLeft, float volumeRight);
/**
* Returns true if the mediaplayer can mix stereo down to mono
*/
public abstract boolean canDownmix();
public abstract void setDownmix(boolean enable);
public abstract MediaType getCurrentMediaType();
public abstract boolean isStreaming();
/**
* Releases internally used resources. This method should only be called when the object is not used anymore.
*/
public abstract void shutdown();
/**
* 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.
*/
public abstract void shutdownQuietly();
public abstract void setVideoSurface(SurfaceHolder surface);
public abstract void resetVideoSurface();
/**
* 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.
*/
public abstract Pair<Integer, Integer> getVideoSize();
/**
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
*
* @return The PSMPInfo object.
*/
public final synchronized PSMPInfo getPSMPInfo() {
return new PSMPInfo(playerStatus, getPlayable());
}
/**
* Returns the current status, 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 player status
*/
public PlayerStatus getPlayerStatus() {
return playerStatus;
}
/**
* 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
*/
public abstract Playable getPlayable();
protected abstract void setPlayable(Playable playable);
public abstract void endPlayback(boolean wasSkipped, boolean switchingPlayers);
/**
* Moves the PSMP 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.
*/
public abstract void stop();
/**
* @return {@code true} if the WifiLock feature should be used, {@code false} otherwise.
*/
protected abstract boolean shouldLockWifi();
protected final synchronized void acquireWifiLockIfNecessary() {
if (shouldLockWifi()) {
if (wifiLock == null) {
wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG);
wifiLock.setReferenceCounted(false);
}
wifiLock.acquire();
}
}
protected final synchronized void releaseWifiLockIfNecessary() {
if (wifiLock != null && wifiLock.isHeld()) {
wifiLock.release();
}
}
/**
* Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time
* so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null).
* <p/>
* This method will notify the callback about the change of the player status (even if the new status is the same
* as the old one).
*
* @param newStatus The new PlayerStatus. This must not be null.
* @param newMedia The new playable object of the PSMP object. This can be null.
*/
protected synchronized final void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) {
Log.d(TAG, this.getClass().getSimpleName() + ": Setting player status to " + newStatus);
this.playerStatus = newStatus;
setPlayable(newMedia);
if (playerStatus != null) {
Log.d(TAG, "playerStatus: " + playerStatus.toString());
}
callback.statusChanged(new PSMPInfo(playerStatus, getPlayable()));
}
protected void smartMarkAsPlayed(Playable media) {
if(media != null && media instanceof FeedMedia) {
FeedMedia oldMedia = (FeedMedia) media;
if(oldMedia.hasAlmostEnded()) {
Log.d(TAG, "smart mark as read");
FeedItem item = oldMedia.getItem();
if (item == null) {
return;
}
DBWriter.markItemPlayed(item, FeedItem.PLAYED, false);
DBWriter.removeQueueItem(context, item, false);
DBWriter.addItemToPlaybackHistory(oldMedia);
if (item.getFeed().getPreferences().getCurrentAutoDelete()) {
Log.d(TAG, "Delete " + oldMedia.toString());
DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId());
}
}
}
}
public interface PSMPCallback {
void statusChanged(PSMPInfo newInfo);
void shouldStop();
void playbackSpeedChanged(float s);
void setSpeedAbilityChanged();
void onBufferingUpdate(int percent);
void onMediaChanged(boolean reloadUI);
boolean onMediaPlayerInfo(int code, @StringRes int resourceId);
boolean onMediaPlayerError(Object inObj, int what, int extra);
boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers);
}
/**
* Holds information about a PSMP object.
*/
public static class PSMPInfo {
public PlayerStatus playerStatus;
public Playable playable;
public PSMPInfo(PlayerStatus playerStatus, Playable playable) {
this.playerStatus = playerStatus;
this.playable = playable;
}
}
}