mirror of
https://github.com/clangen/musikcube.git
synced 2025-03-29 19:20:28 +00:00
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:
parent
1e93ab2162
commit
a60f33fa19
@ -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) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
};
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
@ -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()) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user