Converted PlayerWrapper, MediaPlayerWrapper, ExoPlayerWrapper to Kotlin

to mess around. The rest of the project will probably follow over the
next few weeks.
This commit is contained in:
casey langen 2017-06-06 23:39:49 -07:00
parent 1e93ab2162
commit a60f33fa19
9 changed files with 727 additions and 768 deletions

View File

@ -1,366 +0,0 @@
package io.casey.musikcube.remote.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.net.Uri;
import android.util.Base64;
import com.danikula.videocache.CacheListener;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import java.io.File;
import io.casey.musikcube.remote.Application;
import io.casey.musikcube.remote.util.NetworkUtil;
import io.casey.musikcube.remote.util.Preconditions;
import io.casey.musikcube.remote.websocket.Prefs;
import okhttp3.Cache;
import okhttp3.OkHttpClient;
import okhttp3.Request;
public class ExoPlayerWrapper extends PlayerWrapper {
private static OkHttpClient audioStreamHttpClient = null;
private final SharedPreferences prefs;
private DataSource.Factory datasources;
private ExtractorsFactory extractors;
private MediaSource source;
private SimpleExoPlayer player;
private boolean prefetch;
private Context context;
private long lastPosition = -1;
private int percentAvailable = 0;
private String originalUri, resolvedUri;
private boolean transcoding;
private void initHttpClient(final String uri) {
if (StreamProxy.ENABLED) {
return;
}
synchronized (ExoPlayerWrapper.class) {
if (audioStreamHttpClient == null) {
final File path = new File(context.getExternalCacheDir(), "audio");
int diskCacheIndex = prefs.getInt(
Prefs.Key.DISK_CACHE_SIZE_INDEX, Prefs.Default.DISK_CACHE_SIZE_INDEX);
if (diskCacheIndex < 0 || diskCacheIndex > StreamProxy.CACHE_SETTING_TO_BYTES.size()) {
diskCacheIndex = 0;
}
final OkHttpClient.Builder builder = new OkHttpClient.Builder()
.cache(new Cache(path, StreamProxy.CACHE_SETTING_TO_BYTES.get(diskCacheIndex)))
.addInterceptor((chain) -> {
Request request = chain.request();
final String userPass = "default:" + prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD);
final String encoded = Base64.encodeToString(userPass.getBytes(), Base64.NO_WRAP);
request = request.newBuilder().addHeader("Authorization", "Basic " + encoded).build();
return chain.proceed(request);
});
if (prefs.getBoolean(Prefs.Key.CERT_VALIDATION_DISABLED, Prefs.Default.CERT_VALIDATION_DISABLED)) {
NetworkUtil.disableCertificateValidation(builder);
}
audioStreamHttpClient = builder.build();
}
}
if (uri.startsWith("http")) {
this.datasources = new OkHttpDataSourceFactory(
audioStreamHttpClient,
Util.getUserAgent(context, "musikdroid"),
new DefaultBandwidthMeter());
}
else {
this.datasources = new DefaultDataSourceFactory(
context, Util.getUserAgent(context, "musikdroid"));
}
}
public ExoPlayerWrapper() {
this.context = Application.getInstance();
final DefaultBandwidthMeter bandwidth = new DefaultBandwidthMeter();
final TrackSelection.Factory trackFactory = new AdaptiveTrackSelection.Factory(bandwidth);
final TrackSelector trackSelector = new DefaultTrackSelector(trackFactory);
this.player = ExoPlayerFactory.newSimpleInstance(this.context, trackSelector);
this.extractors = new DefaultExtractorsFactory();
this.player.addListener(eventListener);
this.datasources = new DefaultDataSourceFactory(context, Util.getUserAgent(context, "musikdroid"));
this.prefs = Application.getInstance().getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
this.transcoding = this.prefs.getInt(Prefs.Key.TRANSCODER_BITRATE_INDEX, 0) != 0;
}
@Override
public void play(String uri) {
Preconditions.throwIfNotOnMainThread();
if (!dead()) {
initHttpClient(uri);
this.originalUri = uri;
this.resolvedUri = StreamProxy.getProxyUrl(context, uri);
addCacheListener();
this.source = new ExtractorMediaSource(Uri.parse(resolvedUri), datasources, extractors, null, null);
this.player.setPlayWhenReady(true);
this.player.prepare(this.source);
addActivePlayer(this);
setState(State.Preparing);
}
}
@Override
public void prefetch(String uri) {
Preconditions.throwIfNotOnMainThread();
if (!dead()) {
initHttpClient(uri);
this.originalUri = uri;
this.prefetch = true;
this.resolvedUri = StreamProxy.getProxyUrl(context, uri);
addCacheListener();
this.source = new ExtractorMediaSource(Uri.parse(resolvedUri), datasources, extractors, null, null);
this.player.setPlayWhenReady(false);
this.player.prepare(this.source);
addActivePlayer(this);
setState(State.Preparing);
}
}
@Override
public void pause() {
Preconditions.throwIfNotOnMainThread();
this.prefetch = true;
if (this.getState() == State.Playing) {
this.player.setPlayWhenReady(false);
setState(State.Paused);
}
}
@Override
public void resume() {
Preconditions.throwIfNotOnMainThread();
switch (this.getState()) {
case Paused:
case Prepared:
this.player.setPlayWhenReady(true);
setState(State.Playing);
break;
case Error:
this.player.setPlayWhenReady(this.lastPosition == -1);
this.player.prepare(this.source);
setState(State.Preparing);
break;
}
this.prefetch = false;
}
@Override
public void setPosition(int millis) {
Preconditions.throwIfNotOnMainThread();
this.lastPosition = -1;
if (this.player.getPlaybackState() != ExoPlayer.STATE_IDLE) {
if (this.player.isCurrentWindowSeekable()) {
long offset = millis;
/* if we're transcoding we don't want to seek arbitrarily because it may put
a lot of pressure on the backend. just allow seeking up to what we currently
have buffered! */
if (transcoding && percentAvailable != 100) {
/* give ourselves 2% wiggle room! */
float percent = (float) Math.max(0, percentAvailable - 2) / 100.0f;
long totalMs = this.player.getDuration();
long available = (long) ((float) totalMs * percent);
offset = Math.min(millis, available);
}
this.player.seekTo(offset);
}
}
}
@Override
public int getPosition() {
Preconditions.throwIfNotOnMainThread();
return (int) this.player.getCurrentPosition();
}
@Override
public int getDuration() {
Preconditions.throwIfNotOnMainThread();
return (int) this.player.getDuration();
}
@Override
public void updateVolume() {
Preconditions.throwIfNotOnMainThread();
this.player.setVolume(getGlobalVolume());
}
@Override
public void setNextMediaPlayer(PlayerWrapper wrapper) {
Preconditions.throwIfNotOnMainThread();
}
@Override
public int getBufferedPercent() {
return transcoding ? percentAvailable : 100;
}
@Override
public void dispose() {
Preconditions.throwIfNotOnMainThread();
if (!dead()) {
setState(State.Killing);
removeActivePlayer(this);
removeCacheListener();
if (this.player != null) {
this.player.setPlayWhenReady(false);
this.player.removeListener(eventListener);
this.player.stop();
this.player.release();
}
setState(State.Disposed);
}
}
@Override
public void setOnStateChangedListener(OnStateChangedListener listener) {
super.setOnStateChangedListener(listener);
}
private boolean dead() {
final State state = getState();
return (state == State.Killing || state == State.Disposed);
}
private void addCacheListener() {
if (StreamProxy.ENABLED) {
if (StreamProxy.isCached(this.originalUri)) {
percentAvailable = 100;
}
else {
StreamProxy.registerCacheListener(this.cacheListener, this.originalUri);
}
}
else {
percentAvailable = 100;
}
}
private void removeCacheListener() {
if (StreamProxy.ENABLED) {
StreamProxy.unregisterCacheListener(this.cacheListener);
}
}
private CacheListener cacheListener = (file, uri, percent) -> {
//Log.e("CLCLCL", String.format("%d", percent));
percentAvailable = percent;
};
private ExoPlayer.EventListener eventListener = new ExoPlayer.EventListener() {
@Override
public void onTimelineChanged(Timeline timeline, Object manifest) {
}
@Override
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
}
@Override
public void onLoadingChanged(boolean isLoading) {
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
Preconditions.throwIfNotOnMainThread();
if (playbackState == ExoPlayer.STATE_BUFFERING) {
setState(State.Buffering);
}
else if (playbackState == ExoPlayer.STATE_READY) {
if (dead()) {
dispose();
}
else {
setState(State.Prepared);
player.setVolume(getGlobalVolume());
if (lastPosition != -1) {
player.seekTo(lastPosition);
lastPosition = -1;
}
if (!prefetch) {
player.setPlayWhenReady(true);
setState(State.Playing);
}
else {
setState(State.Paused);
}
}
}
else if (playbackState == ExoPlayer.STATE_ENDED) {
setState(State.Finished);
dispose();
}
}
@Override
public void onPlayerError(ExoPlaybackException error) {
Preconditions.throwIfNotOnMainThread();
lastPosition = player.getCurrentPosition();
switch (getState()) {
case Preparing:
case Prepared:
case Playing:
case Paused:
setState(State.Error);
break;
}
}
@Override
public void onPositionDiscontinuity() {
}
@Override
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
}
};
}

View File

@ -0,0 +1,335 @@
package io.casey.musikcube.remote.playback
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.util.Base64
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
import com.google.android.exoplayer2.extractor.ExtractorsFactory
import com.google.android.exoplayer2.source.ExtractorMediaSource
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.util.Util
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.util.NetworkUtil
import io.casey.musikcube.remote.util.Preconditions
import io.casey.musikcube.remote.websocket.Prefs
import io.casey.musikcube.remote.playback.StreamProxy.*
import okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File
class ExoPlayerWrapper : PlayerWrapper() {
private val prefs: SharedPreferences
private var datasources: DataSource.Factory? = null
private val extractors: ExtractorsFactory
private var source: MediaSource? = null
private val player: SimpleExoPlayer?
private var prefetch: Boolean = false
private val context: Context
private var lastPosition: Long = -1
private var percentAvailable = 0
private var originalUri: String? = null
private var resolvedUri: String? = null
private val transcoding: Boolean
private fun initHttpClient(uri: String) {
if (StreamProxy.ENABLED) {
return
}
synchronized(ExoPlayerWrapper::class.java) {
if (audioStreamHttpClient == null) {
val path = File(context.externalCacheDir, "audio")
var diskCacheIndex = prefs.getInt(
Prefs.Key.DISK_CACHE_SIZE_INDEX, Prefs.Default.DISK_CACHE_SIZE_INDEX)
if (diskCacheIndex < 0 || diskCacheIndex > StreamProxy.CACHE_SETTING_TO_BYTES.size) {
diskCacheIndex = 0
}
val builder = OkHttpClient.Builder()
.cache(Cache(path, CACHE_SETTING_TO_BYTES[diskCacheIndex] ?: MINIMUM_CACHE_SIZE_BYTES))
.addInterceptor { chain ->
var request = chain.request()
val userPass = "default:" + prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD)!!
val encoded = Base64.encodeToString(userPass.toByteArray(), Base64.NO_WRAP)
request = request.newBuilder().addHeader("Authorization", "Basic " + encoded).build()
chain.proceed(request)
}
if (prefs.getBoolean(Prefs.Key.CERT_VALIDATION_DISABLED, Prefs.Default.CERT_VALIDATION_DISABLED)) {
NetworkUtil.disableCertificateValidation(builder)
}
audioStreamHttpClient = builder.build()
}
}
if (uri.startsWith("http")) {
this.datasources = OkHttpDataSourceFactory(
audioStreamHttpClient,
Util.getUserAgent(context, "musikdroid"),
DefaultBandwidthMeter())
}
else {
this.datasources = DefaultDataSourceFactory(
context, Util.getUserAgent(context, "musikdroid"))
}
}
init {
this.context = Application.getInstance()
val bandwidth = DefaultBandwidthMeter()
val trackFactory = AdaptiveTrackSelection.Factory(bandwidth)
val trackSelector = DefaultTrackSelector(trackFactory)
this.player = ExoPlayerFactory.newSimpleInstance(this.context, trackSelector)
this.extractors = DefaultExtractorsFactory()
this.datasources = DefaultDataSourceFactory(context, Util.getUserAgent(context, "musikdroid"))
this.prefs = Application.getInstance().getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
this.transcoding = this.prefs.getInt(Prefs.Key.TRANSCODER_BITRATE_INDEX, 0) != 0
}
override fun play(uri: String) {
Preconditions.throwIfNotOnMainThread()
if (!dead()) {
initHttpClient(uri)
this.originalUri = uri
this.resolvedUri = StreamProxy.getProxyUrl(context, uri)
addCacheListener()
this.source = ExtractorMediaSource(Uri.parse(resolvedUri), datasources, extractors, null, null)
this.player!!.playWhenReady = true
this.player.prepare(this.source)
PlayerWrapper.addActivePlayer(this)
state = State.Preparing
}
}
override fun prefetch(uri: String) {
Preconditions.throwIfNotOnMainThread()
if (!dead()) {
initHttpClient(uri)
this.originalUri = uri
this.prefetch = true
this.resolvedUri = StreamProxy.getProxyUrl(context, uri)
addCacheListener()
this.source = ExtractorMediaSource(Uri.parse(resolvedUri), datasources, extractors, null, null)
this.player!!.playWhenReady = false
this.player.prepare(this.source)
PlayerWrapper.addActivePlayer(this)
state = State.Preparing
}
}
override fun pause() {
Preconditions.throwIfNotOnMainThread()
this.prefetch = true
if (this.state == State.Playing) {
this.player!!.playWhenReady = false
state = State.Paused
}
}
override fun resume() {
Preconditions.throwIfNotOnMainThread()
when (this.state) {
State.Paused, State.Prepared -> {
this.player!!.playWhenReady = true
state = State.Playing
}
State.Error -> {
this.player!!.playWhenReady = this.lastPosition == -1L
this.player.prepare(this.source)
state = State.Preparing
}
}
this.prefetch = false
}
override var position: Int
get(): Int {
Preconditions.throwIfNotOnMainThread()
return this.player!!.currentPosition.toInt()
}
set(millis) {
Preconditions.throwIfNotOnMainThread()
this.lastPosition = -1
if (this.player!!.playbackState != ExoPlayer.STATE_IDLE) {
if (this.player.isCurrentWindowSeekable) {
var offset = millis.toLong()
/* if we're transcoding we don't want to seek arbitrarily because it may put
a lot of pressure on the backend. just allow seeking up to what we currently
have buffered! */
if (transcoding && percentAvailable != 100) {
/* give ourselves 2% wiggle room! */
val percent = Math.max(0, percentAvailable - 2).toFloat() / 100.0f
val totalMs = this.player.duration
val available = (totalMs.toFloat() * percent).toLong()
offset = Math.min(millis.toLong(), available)
}
this.player.seekTo(offset)
}
}
}
override val duration: Int
get() {
Preconditions.throwIfNotOnMainThread()
return this.player!!.duration.toInt()
}
override val bufferedPercent: Int
get() {
return if (transcoding) percentAvailable else 100
}
override fun updateVolume() {
Preconditions.throwIfNotOnMainThread()
this.player!!.volume = PlayerWrapper.getVolume()
}
override fun setNextMediaPlayer(wrapper: PlayerWrapper?) {
Preconditions.throwIfNotOnMainThread()
}
override fun dispose() {
Preconditions.throwIfNotOnMainThread()
if (!dead()) {
state = State.Killing
PlayerWrapper.removeActivePlayer(this)
removeCacheListener()
this.player?.playWhenReady = false
this.player?.removeListener(eventListener)
this.player?.stop()
this.player?.release()
state = State.Disposed
}
}
override fun setOnStateChangedListener(listener: PlayerWrapper.OnStateChangedListener?) {
super.setOnStateChangedListener(listener)
}
private fun dead(): Boolean {
val state = state
return state == State.Killing || state == State.Disposed
}
private fun addCacheListener() {
if (StreamProxy.ENABLED) {
if (StreamProxy.isCached(this.originalUri)) {
percentAvailable = 100
}
else {
StreamProxy.registerCacheListener(this.cacheListener, this.originalUri)
}
}
else {
percentAvailable = 100
}
}
private fun removeCacheListener() {
if (StreamProxy.ENABLED) {
StreamProxy.unregisterCacheListener(this.cacheListener)
}
}
private val cacheListener = { _: File, _: String, percent: Int ->
//Log.e("CLCLCL", String.format("%d", percent));
percentAvailable = percent
}
private var eventListener = object : ExoPlayer.EventListener {
override fun onTimelineChanged(timeline: Timeline, manifest: Any?) {
}
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
}
override fun onLoadingChanged(isLoading: Boolean) {
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
Preconditions.throwIfNotOnMainThread()
if (playbackState == ExoPlayer.STATE_BUFFERING) {
state = State.Buffering
}
else if (playbackState == ExoPlayer.STATE_READY) {
if (dead()) {
dispose()
}
else {
state = State.Prepared
player!!.volume = PlayerWrapper.getVolume()
if (lastPosition != -1L) {
player.seekTo(lastPosition)
lastPosition = -1
}
if (!prefetch) {
player.playWhenReady = true
state = State.Playing
} else {
state = State.Paused
}
}
}
else if (playbackState == ExoPlayer.STATE_ENDED) {
state = State.Finished
dispose()
}
}
override fun onPlayerError(error: ExoPlaybackException) {
Preconditions.throwIfNotOnMainThread()
lastPosition = player!!.currentPosition
when (state) {
State.Preparing,
State.Prepared,
State.Playing,
State.Paused ->
state = State.Error
}
}
override fun onPositionDiscontinuity() {
}
override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
}
}
companion object {
private var audioStreamHttpClient: OkHttpClient? = null
}
init {
this.player!!.addListener(eventListener)
}
}

View File

@ -1,242 +0,0 @@
package io.casey.musikcube.remote.playback;
import android.content.Context;
import android.content.SharedPreferences;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.PowerManager;
import android.util.Base64;
import android.util.Log;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import io.casey.musikcube.remote.Application;
import io.casey.musikcube.remote.util.Preconditions;
import io.casey.musikcube.remote.websocket.Prefs;
public class MediaPlayerWrapper extends PlayerWrapper {
private static final String TAG = "MediaPlayerWrapper";
private MediaPlayer player = new MediaPlayer();
private int seekTo;
private boolean prefetching;
private Context context = Application.getInstance();
private SharedPreferences prefs;
private int bufferedPercent;
public MediaPlayerWrapper() {
this.prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
}
@Override
public void play(final String uri) {
Preconditions.throwIfNotOnMainThread();
try {
setState(State.Preparing);
final String userPass = "default:" + prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD);
final String encoded = Base64.encodeToString(userPass.getBytes(), Base64.NO_WRAP);
final Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Basic " + encoded);
player.setDataSource(
context,
Uri.parse(StreamProxy.getProxyUrl(context, uri)),
headers);
player.setAudioStreamType(AudioManager.STREAM_MUSIC);
player.setOnPreparedListener(onPrepared);
player.setOnErrorListener(onError);
player.setOnCompletionListener(onCompleted);
player.setOnBufferingUpdateListener(onBuffering);
player.setWakeMode(Application.getInstance(), PowerManager.PARTIAL_WAKE_LOCK);
player.prepareAsync();
}
catch (IOException e) {
Log.e(TAG, "setDataSource failed: " + e.toString());
}
}
@Override
public void prefetch(final String uri) {
Preconditions.throwIfNotOnMainThread();
this.prefetching = true;
play(uri);
}
@Override
public void pause() {
Preconditions.throwIfNotOnMainThread();
if (isPreparedOrPlaying()) {
player.pause();
setState(State.Paused);
}
}
@Override
public void setPosition(int millis) {
Preconditions.throwIfNotOnMainThread();
if (isPreparedOrPlaying()) {
this.player.seekTo(millis);
this.seekTo = 0;
}
else {
this.seekTo = millis;
}
}
@Override
public int getPosition() {
Preconditions.throwIfNotOnMainThread();
if (isPreparedOrPlaying()) {
return this.player.getCurrentPosition();
}
return 0;
}
@Override
public int getDuration() {
Preconditions.throwIfNotOnMainThread();
if (isPreparedOrPlaying()) {
return this.player.getDuration();
}
return 0;
}
@Override
public void resume() {
Preconditions.throwIfNotOnMainThread();
final State state = getState();
if (state == State.Prepared || state == State.Paused) {
player.start();
setState(State.Playing);
}
else {
prefetching = false;
}
}
@Override
public void setNextMediaPlayer(final PlayerWrapper wrapper) {
Preconditions.throwIfNotOnMainThread();
if (isPreparedOrPlaying()) {
try {
this.player.setNextMediaPlayer(wrapper != null ? ((MediaPlayerWrapper) wrapper).player : null);
}
catch (IllegalStateException ex) {
Log.d(TAG, "invalid state for setNextMediaPlayer");
}
}
}
@Override
public void updateVolume() {
Preconditions.throwIfNotOnMainThread();
final State state = getState();
if (state != State.Preparing && state != State.Disposed) {
final float volume = getGlobalVolume();
player.setVolume(volume, volume);
}
}
@Override
public int getBufferedPercent() {
return bufferedPercent;
}
private boolean isPreparedOrPlaying() {
final State state = getState();
return state == State.Playing || state == State.Prepared;
}
public void dispose() {
Preconditions.throwIfNotOnMainThread();
removeActivePlayer(this);
if (getState() != State.Preparing) {
try {
this.player.setNextMediaPlayer(null);
}
catch (IllegalStateException ex) {
Log.d(TAG, "failed to setNextMediaPlayer(null)");
}
try {
this.player.stop();
}
catch (IllegalStateException ex) {
Log.d(TAG, "failed to stop()");
}
try {
this.player.reset();
}
catch (IllegalStateException ex) {
Log.d(TAG, "failed to reset()");
}
this.player.release();
setOnStateChangedListener(null);
setState(State.Disposed);
}
else {
setState(State.Killing);
}
}
private MediaPlayer.OnPreparedListener onPrepared = (mediaPlayer) -> {
if (this.getState() == State.Killing) {
dispose();
}
else {
final float volume = getGlobalVolume();
player.setVolume(volume, volume);
addActivePlayer(this);
if (prefetching) {
setState(State.Prepared);
}
else {
this.player.start();
if (this.seekTo != 0) {
setPosition(this.seekTo);
}
setState(State.Playing);
}
this.prefetching = false;
}
};
private MediaPlayer.OnErrorListener onError = (player, what, extra) -> {
setState(State.Error);
dispose();
return true;
};
private MediaPlayer.OnCompletionListener onCompleted = (mp) -> {
setState(State.Finished);
dispose();
};
private MediaPlayer.OnBufferingUpdateListener onBuffering = (mp, percent) -> {
bufferedPercent = percent;
};
}

View File

@ -0,0 +1,232 @@
package io.casey.musikcube.remote.playback
import android.content.Context
import android.content.SharedPreferences
import android.media.AudioManager
import android.media.MediaPlayer
import android.net.Uri
import android.os.PowerManager
import android.util.Base64
import android.util.Log
import java.io.IOException
import java.util.HashMap
import io.casey.musikcube.remote.Application
import io.casey.musikcube.remote.util.Preconditions
import io.casey.musikcube.remote.websocket.Prefs
class MediaPlayerWrapper : PlayerWrapper() {
private val player = MediaPlayer()
private var seekTo: Int = 0
private var prefetching: Boolean = false
private val context = Application.getInstance()
private val prefs: SharedPreferences
override var bufferedPercent: Int = 0
init {
this.prefs = context.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE)
}
override fun play(uri: String) {
Preconditions.throwIfNotOnMainThread()
try {
state = State.Preparing
val userPass = "default:" + prefs.getString(Prefs.Key.PASSWORD, Prefs.Default.PASSWORD)!!
val encoded = Base64.encodeToString(userPass.toByteArray(), Base64.NO_WRAP)
val headers = HashMap<String, String>()
headers.put("Authorization", "Basic " + encoded)
player.setDataSource(
context,
Uri.parse(StreamProxy.getProxyUrl(context, uri)),
headers)
player.setAudioStreamType(AudioManager.STREAM_MUSIC)
player.setOnPreparedListener(onPrepared)
player.setOnErrorListener(onError)
player.setOnCompletionListener(onCompleted)
player.setOnBufferingUpdateListener(onBuffering)
player.setWakeMode(Application.getInstance(), PowerManager.PARTIAL_WAKE_LOCK)
player.prepareAsync()
}
catch (e: IOException) {
Log.e(TAG, "setDataSource failed: " + e.toString())
}
}
override fun prefetch(uri: String) {
Preconditions.throwIfNotOnMainThread()
this.prefetching = true
play(uri)
}
override fun pause() {
Preconditions.throwIfNotOnMainThread()
if (isPreparedOrPlaying) {
player.pause()
state = State.Paused
}
}
override var position: Int
get() {
Preconditions.throwIfNotOnMainThread()
if (isPreparedOrPlaying) {
return this.player.currentPosition
}
return 0
}
set(millis) {
Preconditions.throwIfNotOnMainThread()
if (isPreparedOrPlaying) {
this.player.seekTo(millis)
this.seekTo = 0
}
else {
this.seekTo = millis
}
}
override val duration: Int
get() {
Preconditions.throwIfNotOnMainThread()
if (isPreparedOrPlaying) {
return this.player.duration
}
return 0
}
override fun resume() {
Preconditions.throwIfNotOnMainThread()
if (state === State.Prepared || state === State.Paused) {
player.start()
this.state = State.Playing
}
else {
prefetching = false
}
}
override fun setNextMediaPlayer(wrapper: PlayerWrapper?) {
Preconditions.throwIfNotOnMainThread()
if (isPreparedOrPlaying) {
try {
if (wrapper is MediaPlayerWrapper) {
this.player.setNextMediaPlayer(wrapper.player)
}
}
catch (ex: IllegalStateException) {
Log.d(TAG, "invalid state for setNextMediaPlayer")
}
}
}
override fun updateVolume() {
Preconditions.throwIfNotOnMainThread()
val state = state
if (state !== State.Preparing && state !== State.Disposed) {
val volume = PlayerWrapper.getVolume()
player.setVolume(volume, volume)
}
}
private val isPreparedOrPlaying: Boolean
get() {
return state === State.Playing || state === State.Prepared
}
override fun dispose() {
Preconditions.throwIfNotOnMainThread()
PlayerWrapper.removeActivePlayer(this)
if (state !== State.Preparing) {
try {
this.player.setNextMediaPlayer(null)
}
catch (ex: IllegalStateException) {
Log.d(TAG, "failed to setNextMediaPlayer(null)")
}
try {
this.player.stop()
}
catch (ex: IllegalStateException) {
Log.d(TAG, "failed to stop()")
}
try {
this.player.reset()
}
catch (ex: IllegalStateException) {
Log.d(TAG, "failed to reset()")
}
this.player.release()
setOnStateChangedListener(null)
state = State.Disposed
}
else {
state = State.Killing
}
}
private val onPrepared = { mediaPlayer: MediaPlayer ->
if (this.state === State.Killing) {
dispose()
}
else {
val volume = PlayerWrapper.getVolume()
player.setVolume(volume, volume)
PlayerWrapper.addActivePlayer(this)
if (prefetching) {
state = State.Prepared
}
else {
this.player.start()
if (this.seekTo != 0) {
position = this.seekTo
}
state = State.Playing
}
this.prefetching = false
}
}
private val onError = { _: MediaPlayer, _: Int, _: Int ->
state = State.Error
dispose()
true
}
private val onCompleted = { _: MediaPlayer ->
state = State.Finished
dispose()
}
private val onBuffering = { _: MediaPlayer, percent: Int -> bufferedPercent = percent }
companion object {
private val TAG = "MediaPlayerWrapper"
}
}

View File

@ -1,149 +0,0 @@
package io.casey.musikcube.remote.playback;
import java.util.HashSet;
import java.util.Set;
import io.casey.musikcube.remote.util.Preconditions;
public abstract class PlayerWrapper {
private enum Type { MediaPlayer, ExoPlayer }
private static final Type TYPE = Type.ExoPlayer;
private static final float DUCK_COEF = 0.2f; /* volume = 20% when ducked */
private static final float DUCK_NONE = -1.0f;
public enum State {
Stopped,
Preparing,
Prepared,
Playing,
Buffering,
Paused,
Error,
Finished,
Killing,
Disposed
}
public interface OnStateChangedListener {
void onStateChanged(PlayerWrapper mpw, State state);
}
private static Set<PlayerWrapper> activePlayers = new HashSet<>();
private static float globalVolume = 1.0f;
private static boolean globalMuted = false;
private static float preDuckGlobalVolume = DUCK_NONE;
public static void duck() {
Preconditions.throwIfNotOnMainThread();
if (preDuckGlobalVolume == DUCK_NONE) {
final float lastVolume = globalVolume;
setGlobalVolume(globalVolume * DUCK_COEF);
preDuckGlobalVolume = lastVolume;
}
}
public static void unduck() {
Preconditions.throwIfNotOnMainThread();
if (preDuckGlobalVolume != DUCK_NONE) {
final float temp = preDuckGlobalVolume;
preDuckGlobalVolume = DUCK_NONE;
setGlobalVolume(temp);
}
}
public static void setGlobalVolume(float volume) {
Preconditions.throwIfNotOnMainThread();
if (preDuckGlobalVolume != DUCK_NONE) {
preDuckGlobalVolume = volume;
volume = volume * DUCK_COEF;
}
if (volume != globalVolume) {
globalVolume = volume;
for (final PlayerWrapper w : activePlayers) {
w.updateVolume();
}
}
}
public static float getGlobalVolume() {
Preconditions.throwIfNotOnMainThread();
if (globalMuted) {
return 0;
}
return (preDuckGlobalVolume == DUCK_NONE) ? globalVolume : preDuckGlobalVolume;
}
public static void setGlobalMute(final boolean muted) {
Preconditions.throwIfNotOnMainThread();
if (PlayerWrapper.globalMuted != muted) {
PlayerWrapper.globalMuted = muted;
for (final PlayerWrapper w : activePlayers) {
w.updateVolume();
}
}
}
public static PlayerWrapper newInstance() {
return TYPE == Type.ExoPlayer
? new ExoPlayerWrapper()
: new MediaPlayerWrapper();
}
protected static void addActivePlayer(final PlayerWrapper player) {
Preconditions.throwIfNotOnMainThread();
activePlayers.add(player);
}
protected static void removeActivePlayer(final PlayerWrapper player) {
Preconditions.throwIfNotOnMainThread();
activePlayers.remove(player);
}
private OnStateChangedListener listener;
private State state = State.Stopped;
public abstract void play(final String uri);
public abstract void prefetch(final String uri);
public abstract void pause();
public abstract void resume();
public abstract void setPosition(int millis);
public abstract int getPosition();
public abstract int getDuration();
public abstract void updateVolume();
public abstract void setNextMediaPlayer(final PlayerWrapper wrapper);
public abstract void dispose();
public abstract int getBufferedPercent();
public void setOnStateChangedListener(OnStateChangedListener listener) {
Preconditions.throwIfNotOnMainThread();
this.listener = listener;
if (listener != null) {
this.listener.onStateChanged(this, state);
}
}
public final State getState() {
return this.state;
}
protected void setState(final PlayerWrapper.State state) {
if (this.state != state) {
this.state = state;
if (listener != null) {
this.listener.onStateChanged(this, state);
}
}
}
}

View File

@ -0,0 +1,148 @@
package io.casey.musikcube.remote.playback
import java.util.HashSet
import io.casey.musikcube.remote.util.Preconditions
abstract class PlayerWrapper {
private enum class Type {
MediaPlayer, ExoPlayer
}
enum class State {
Stopped,
Preparing,
Prepared,
Playing,
Buffering,
Paused,
Error,
Finished,
Killing,
Disposed
}
interface OnStateChangedListener {
fun onStateChanged(mpw: PlayerWrapper, state: State)
}
private var listener: OnStateChangedListener? = null
var state = State.Stopped
protected set(state) {
if (this.state != state) {
field = state
if (listener != null) {
this.listener!!.onStateChanged(this, state)
}
}
}
abstract fun play(uri: String)
abstract fun prefetch(uri: String)
abstract fun pause()
abstract fun resume()
abstract fun updateVolume()
abstract fun setNextMediaPlayer(wrapper: PlayerWrapper?)
abstract fun dispose()
abstract var position: Int
abstract val duration: Int
abstract val bufferedPercent: Int
open fun setOnStateChangedListener(listener: OnStateChangedListener?) {
Preconditions.throwIfNotOnMainThread()
this.listener = listener
if (listener != null) {
this.listener!!.onStateChanged(this, this.state)
}
}
companion object {
private val TYPE = Type.ExoPlayer
private val DUCK_COEF = 0.2f /* volume = 20% when ducked */
private val DUCK_NONE = -1.0f
private val activePlayers = HashSet<PlayerWrapper>()
private var globalVolume = 1.0f
private var globalMuted = false
private var preDuckGlobalVolume = DUCK_NONE
fun duck() {
Preconditions.throwIfNotOnMainThread()
if (preDuckGlobalVolume == DUCK_NONE) {
val lastVolume = globalVolume
setVolume(globalVolume * DUCK_COEF)
preDuckGlobalVolume = lastVolume
}
}
fun unduck() {
Preconditions.throwIfNotOnMainThread()
if (preDuckGlobalVolume != DUCK_NONE) {
val temp = preDuckGlobalVolume
preDuckGlobalVolume = DUCK_NONE
setVolume(temp)
}
}
fun setVolume(volume: Float) {
var volume = volume
Preconditions.throwIfNotOnMainThread()
if (preDuckGlobalVolume != DUCK_NONE) {
preDuckGlobalVolume = volume
volume = volume * DUCK_COEF
}
if (volume != globalVolume) {
globalVolume = volume
for (w in activePlayers) {
w.updateVolume()
}
}
}
fun getVolume(): Float {
Preconditions.throwIfNotOnMainThread()
if (globalMuted) {
return 0f
}
return if (preDuckGlobalVolume == DUCK_NONE) globalVolume else preDuckGlobalVolume
}
fun setMute(muted: Boolean) {
Preconditions.throwIfNotOnMainThread()
if (PlayerWrapper.globalMuted != muted) {
PlayerWrapper.globalMuted = muted
for (w in activePlayers) {
w.updateVolume()
}
}
}
fun newInstance(): PlayerWrapper {
return if (TYPE == Type.ExoPlayer)
ExoPlayerWrapper()
else
MediaPlayerWrapper()
}
fun addActivePlayer(player: PlayerWrapper) {
Preconditions.throwIfNotOnMainThread()
activePlayers.add(player)
}
fun removeActivePlayer(player: PlayerWrapper) {
Preconditions.throwIfNotOnMainThread()
activePlayers.remove(player)
}
}
}

View File

@ -22,12 +22,13 @@ public class StreamProxy {
public static final boolean ENABLED = true;
public static final long BYTES_PER_MEGABYTE = 1048576L;
public static final long BYTES_PER_GIGABYTE = 1073741824L;
public static final long MINIMUM_CACHE_SIZE_BYTES = BYTES_PER_MEGABYTE * 32;
public static final Map<Integer, Long> CACHE_SETTING_TO_BYTES;
private static final FileNameGenerator DEFAULT_FILENAME_GENERATOR = new Md5FileNameGenerator();
static {
CACHE_SETTING_TO_BYTES = new HashMap<>();
CACHE_SETTING_TO_BYTES.put(0, BYTES_PER_MEGABYTE * 32);
CACHE_SETTING_TO_BYTES.put(0, MINIMUM_CACHE_SIZE_BYTES);
CACHE_SETTING_TO_BYTES.put(1, BYTES_PER_GIGABYTE / 2);
CACHE_SETTING_TO_BYTES.put(2, BYTES_PER_GIGABYTE);
CACHE_SETTING_TO_BYTES.put(3, BYTES_PER_GIGABYTE * 2);

View File

@ -330,7 +330,7 @@ public class StreamingPlaybackService implements PlaybackService {
@Override
public double getVolume() {
if (prefs.getBoolean(Prefs.Key.SOFTWARE_VOLUME, Prefs.Default.SOFTWARE_VOLUME)) {
return PlayerWrapper.getGlobalVolume();
return PlayerWrapper.Companion.getVolume();
}
return getSystemVolume();
@ -374,7 +374,7 @@ public class StreamingPlaybackService implements PlaybackService {
@Override
public void toggleMute() {
muted = !muted;
PlayerWrapper.setGlobalMute(muted);
PlayerWrapper.Companion.setMute(muted);
notifyEventListeners();
}
@ -451,14 +451,14 @@ public class StreamingPlaybackService implements PlaybackService {
}
final boolean softwareVolume = prefs.getBoolean(Prefs.Key.SOFTWARE_VOLUME, Prefs.Default.SOFTWARE_VOLUME);
float current = softwareVolume ? PlayerWrapper.getGlobalVolume() : getSystemVolume();
float current = softwareVolume ? PlayerWrapper.Companion.getVolume() : getSystemVolume();
current += delta;
if (current > 1.0) current = 1.0f;
if (current < 0.0) current = 0.0f;
if (softwareVolume) {
PlayerWrapper.setGlobalVolume(current);
PlayerWrapper.Companion.setVolume(current);
}
else {
final int actual = Math.round(current * getMaxSystemVolume());
@ -699,7 +699,7 @@ public class StreamingPlaybackService implements PlaybackService {
if (uri != null) {
this.context.reset(this.context.nextPlayer);
this.context.nextPlayer = PlayerWrapper.newInstance();
this.context.nextPlayer = PlayerWrapper.Companion.newInstance();
this.context.nextPlayer.setOnStateChangedListener(onNextPlayerStateChanged);
this.context.nextPlayer.prefetch(uri);
}
@ -792,7 +792,7 @@ public class StreamingPlaybackService implements PlaybackService {
final String uri = getUri(this.context.currentMetadata);
if (uri != null) {
this.context.currentPlayer = PlayerWrapper.newInstance();
this.context.currentPlayer = PlayerWrapper.Companion.newInstance();
this.context.currentPlayer.setOnStateChangedListener(onCurrentPlayerStateChanged);
this.context.currentPlayer.play(uri);
}
@ -912,7 +912,7 @@ public class StreamingPlaybackService implements PlaybackService {
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT:
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE:
case AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK:
PlayerWrapper.unduck();
PlayerWrapper.Companion.unduck();
if (pausedByTransientLoss) {
pausedByTransientLoss = false;
resume();
@ -934,7 +934,7 @@ public class StreamingPlaybackService implements PlaybackService {
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
PlayerWrapper.duck();
PlayerWrapper.Companion.duck();
break;
}
};

View File

@ -22,8 +22,8 @@ import android.widget.Spinner;
import java.util.Locale;
import io.casey.musikcube.remote.R;
import io.casey.musikcube.remote.playback.MediaPlayerWrapper;
import io.casey.musikcube.remote.playback.PlaybackServiceFactory;
import io.casey.musikcube.remote.playback.PlayerWrapper;
import io.casey.musikcube.remote.playback.StreamProxy;
import io.casey.musikcube.remote.ui.util.Views;
import io.casey.musikcube.remote.websocket.Prefs;
@ -204,7 +204,7 @@ public class SettingsActivity extends AppCompatActivity {
.apply();
if (!softwareVolume.isChecked()) {
MediaPlayerWrapper.setGlobalVolume(1.0f);
PlayerWrapper.Companion.setVolume(1.0f);
}
if (wasStreaming && !isStreamingEnabled()) {