- Added buffering indicator in seekbar for transcoding streams; disallow

seeks past what's been buffered.
- Unified position of buffering indicator in MainMetadataView. Removed
  fades, they were super subtle and causing layout issues.
- Don't show the volume% unless we're in remoting mote
This commit is contained in:
casey langen 2017-06-04 12:00:44 -07:00
parent 3051ebf813
commit c5d88afd1c
12 changed files with 152 additions and 87 deletions

View File

@ -7,7 +7,6 @@ import android.os.Bundle;
import android.os.Handler;
import android.support.design.widget.Snackbar;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -360,6 +359,7 @@ public class MainActivity extends WebSocketActivityBase {
seekbar.setMax((int) duration);
seekbar.setProgress((int) current);
seekbar.setSecondaryProgress((int) playback.getBufferedTime());
scheduleUpdateTime(false);
};

View File

@ -5,6 +5,7 @@ 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;
@ -40,6 +41,7 @@ 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;
@ -47,7 +49,9 @@ public class ExoPlayerWrapper extends PlayerWrapper {
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) {
@ -56,9 +60,6 @@ public class ExoPlayerWrapper extends PlayerWrapper {
synchronized (ExoPlayerWrapper.class) {
if (audioStreamHttpClient == null) {
final SharedPreferences prefs = Application.getInstance()
.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE);
final File path = new File(context.getExternalCacheDir(), "audio");
int diskCacheIndex = prefs.getInt(
@ -107,6 +108,8 @@ public class ExoPlayerWrapper extends PlayerWrapper {
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
@ -117,6 +120,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
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);
@ -134,6 +138,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
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);
@ -182,8 +187,20 @@ public class ExoPlayerWrapper extends PlayerWrapper {
this.lastPosition = -1;
if (this.player.getPlaybackState() != ExoPlayer.STATE_IDLE) {
if (this.player.isCurrentWindowSeekable()) {
this.lastPosition = millis;
this.player.seekTo(millis);
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);
}
}
}
@ -191,21 +208,18 @@ public class ExoPlayerWrapper extends PlayerWrapper {
@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());
}
@ -214,6 +228,11 @@ public class ExoPlayerWrapper extends PlayerWrapper {
Preconditions.throwIfNotOnMainThread();
}
@Override
public int getBufferedPercent() {
return transcoding ? percentAvailable : 100;
}
@Override
public void dispose() {
Preconditions.throwIfNotOnMainThread();
@ -221,6 +240,7 @@ public class ExoPlayerWrapper extends PlayerWrapper {
if (!dead()) {
setState(State.Killing);
removeActivePlayer(this);
removeCacheListener();
if (this.player != null) {
this.player.setPlayWhenReady(false);
this.player.removeListener(eventListener);
@ -241,6 +261,31 @@ public class ExoPlayerWrapper extends PlayerWrapper {
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) {

View File

@ -25,6 +25,7 @@ public class MediaPlayerWrapper extends PlayerWrapper {
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);
@ -51,6 +52,7 @@ public class MediaPlayerWrapper extends PlayerWrapper {
player.setOnPreparedListener(onPrepared);
player.setOnErrorListener(onError);
player.setOnCompletionListener(onCompleted);
player.setOnBufferingUpdateListener(onBuffering);
player.setWakeMode(Application.getInstance(), PowerManager.PARTIAL_WAKE_LOCK);
player.prepareAsync();
}
@ -149,6 +151,11 @@ public class MediaPlayerWrapper extends PlayerWrapper {
}
}
@Override
public int getBufferedPercent() {
return bufferedPercent;
}
private boolean isPreparedOrPlaying() {
final State state = getState();
return state == State.Playing || state == State.Prepared;
@ -228,4 +235,8 @@ public class MediaPlayerWrapper extends PlayerWrapper {
setState(State.Finished);
dispose();
};
private MediaPlayer.OnBufferingUpdateListener onBuffering = (mp, percent) -> {
bufferedPercent = percent;
};
}

View File

@ -43,6 +43,7 @@ public interface PlaybackService {
double getVolume();
double getDuration();
double getCurrentTime();
double getBufferedTime();
PlaybackState getPlaybackState();

View File

@ -122,6 +122,7 @@ public abstract class PlayerWrapper {
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();

View File

@ -306,6 +306,11 @@ public class RemotePlaybackService implements PlaybackService {
return currentTime.get(track);
}
@Override
public double getBufferedTime() {
return getDuration();
}
@Override
public String getTrackString(String key, String defaultValue) {
if (track.has(key)) {

View File

@ -104,6 +104,10 @@ public class StreamProxy {
}
}
public static synchronized boolean isCached(final String url) {
return INSTANCE != null && INSTANCE.proxy.isCached(url);
}
public static synchronized String getProxyUrl(final Context context, final String url) {
init(context);
return ENABLED ? INSTANCE.proxy.getProxyUrl(url) : url;

View File

@ -416,6 +416,15 @@ public class StreamingPlaybackService implements PlaybackService {
return defaultValue;
}
@Override
public double getBufferedTime() {
if (context.currentPlayer != null) {
float percent = (float) context.currentPlayer.getBufferedPercent() / 100.0f;
return percent * (float) context.currentPlayer.getDuration() / 1000.0f; /* ms -> sec */
}
return 0;
}
@Override
public TrackListSlidingWindow.QueryFactory getPlaylistQueryFactory() {
return this.queryFactory;

View File

@ -10,8 +10,6 @@ import java.io.IOException;
import java.net.SocketTimeoutException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;

View File

@ -13,7 +13,6 @@ import android.support.v7.widget.SearchView;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewPropertyAnimator;
import android.widget.CheckBox;
import android.widget.EditText;
@ -122,12 +121,6 @@ public final class Views {
editText.setSelection(editText.getText().length());
}
public static ViewPropertyAnimator animateAlpha(final View view, final float value) {
final ViewPropertyAnimator animator = view.animate().alpha(value).setDuration(300);
animator.start();
return animator;
}
public static void enableUpNavigation(final AppCompatActivity activity) {
final ActionBar ab = activity.getSupportActionBar();
if (ab != null) {

View File

@ -16,7 +16,6 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@ -35,10 +34,10 @@ import io.casey.musikcube.remote.playback.Metadata;
import io.casey.musikcube.remote.playback.PlaybackService;
import io.casey.musikcube.remote.playback.PlaybackServiceFactory;
import io.casey.musikcube.remote.playback.PlaybackState;
import io.casey.musikcube.remote.playback.StreamingPlaybackService;
import io.casey.musikcube.remote.ui.activity.AlbumBrowseActivity;
import io.casey.musikcube.remote.ui.activity.TrackListActivity;
import io.casey.musikcube.remote.ui.model.AlbumArtModel;
import io.casey.musikcube.remote.ui.util.Views;
import io.casey.musikcube.remote.util.Strings;
import io.casey.musikcube.remote.websocket.Messages;
import io.casey.musikcube.remote.websocket.Prefs;
@ -55,10 +54,9 @@ public class MainMetadataView extends FrameLayout {
private TextView title, artist, album, volume;
private TextView titleWithArt, artistAndAlbumWithArt, volumeWithArt;
private View mainTrackMetadataWithAlbumArt, mainTrackMetadataNoAlbumArt;
private View buffering, bufferingWithArt;
private View buffering;
private ImageView albumArtImageView;
private ViewPropertyAnimator metadataAnim1, metadataAnim2;
private enum DisplayMode { Artwork, NoArtwork, Stopped }
private AlbumArtModel albumArtModel = AlbumArtModel.empty();
private DisplayMode lastDisplayMode = DisplayMode.Stopped;
@ -107,23 +105,34 @@ public class MainMetadataView extends FrameLayout {
final PlaybackService playback = getPlaybackService();
final boolean buffering = playback.getPlaybackState() == PlaybackState.Buffering;
final boolean streaming = playback instanceof StreamingPlaybackService;
final String artist = playback.getTrackString(Metadata.Track.ARTIST, "");
final String album = playback.getTrackString(Metadata.Track.ALBUM, "");
final String title = playback.getTrackString(Metadata.Track.TITLE, "");
final String volume = getString(R.string.status_volume, Math.round(playback.getVolume() * 100));
/* we don't display the volume amount when we're streaming -- the system has
overlays for drawing volume. */
if (streaming) {
this.volume.setVisibility(View.GONE);
this.volumeWithArt.setVisibility(View.GONE);
}
else {
final String volume = getString(R.string.status_volume, Math.round(playback.getVolume() * 100));
this.volume.setVisibility(View.VISIBLE);
this.volumeWithArt.setVisibility(View.VISIBLE);
this.volume.setText(volume);
this.volumeWithArt.setText(volume);
}
this.title.setText(Strings.empty(title) ? getString(buffering ? R.string.buffering : R.string.unknown_title) : title);
this.artist.setText(Strings.empty(artist) ? getString(buffering ? R.string.buffering : R.string.unknown_artist) : artist);
this.album.setText(Strings.empty(album) ? getString(buffering ? R.string.buffering : R.string.unknown_album) : album);
this.volume.setText(volume);
this.rebindAlbumArtistWithArtTextView(playback);
this.titleWithArt.setText(Strings.empty(title) ? getString(buffering ? R.string.buffering : R.string.unknown_title) : title);
this.volumeWithArt.setText(volume);
this.buffering.setVisibility(buffering ? View.VISIBLE : View.GONE);
this.bufferingWithArt.setVisibility(buffering ? View.VISIBLE : View.GONE);
boolean albumArtEnabledInSettings = this.prefs.getBoolean(
Prefs.Key.ALBUM_ART_ENABLED, Prefs.Default.ALBUM_ART_ENABLED);
@ -131,7 +140,8 @@ public class MainMetadataView extends FrameLayout {
if (!albumArtEnabledInSettings || Strings.empty(artist) || Strings.empty(album)) {
this.albumArtModel = AlbumArtModel.empty();
setMetadataDisplayMode(DisplayMode.NoArtwork);
} else {
}
else {
if (!this.albumArtModel.is(artist, album)) {
this.albumArtModel.destroy();
@ -158,24 +168,19 @@ public class MainMetadataView extends FrameLayout {
private void setMetadataDisplayMode(DisplayMode mode) {
lastDisplayMode = mode;
if (metadataAnim1 != null) {
metadataAnim1.cancel();
metadataAnim2.cancel();
}
if (mode == DisplayMode.Stopped) {
albumArtImageView.setImageDrawable(null);
metadataAnim1 = Views.animateAlpha(mainTrackMetadataWithAlbumArt, 0.0f);
metadataAnim2 = Views.animateAlpha(mainTrackMetadataNoAlbumArt, 0.0f);
mainTrackMetadataWithAlbumArt.setVisibility(View.GONE);
mainTrackMetadataNoAlbumArt.setVisibility(View.GONE);
}
else if (mode == DisplayMode.Artwork) {
metadataAnim1 = Views.animateAlpha(mainTrackMetadataWithAlbumArt, 1.0f);
metadataAnim2 = Views.animateAlpha(mainTrackMetadataNoAlbumArt, 0.0f);
mainTrackMetadataWithAlbumArt.setVisibility(View.VISIBLE);
mainTrackMetadataNoAlbumArt.setVisibility(View.GONE);
}
else {
albumArtImageView.setImageDrawable(null);
metadataAnim1 = Views.animateAlpha(mainTrackMetadataWithAlbumArt, 0.0f);
metadataAnim2 = Views.animateAlpha(mainTrackMetadataNoAlbumArt, 1.0f);
mainTrackMetadataWithAlbumArt.setVisibility(View.GONE);
mainTrackMetadataNoAlbumArt.setVisibility(View.VISIBLE);
}
}
@ -326,16 +331,11 @@ public class MainMetadataView extends FrameLayout {
this.titleWithArt = (TextView) findViewById(R.id.with_art_track_title);
this.artistAndAlbumWithArt = (TextView) findViewById(R.id.with_art_artist_and_album);
this.volumeWithArt = (TextView) findViewById(R.id.with_art_volume);
this.bufferingWithArt = findViewById(R.id.with_art_buffering);
this.mainTrackMetadataWithAlbumArt = findViewById(R.id.main_track_metadata_with_art);
this.mainTrackMetadataNoAlbumArt = findViewById(R.id.main_track_metadata_without_art);
this.albumArtImageView = (ImageView) findViewById(R.id.album_art);
/* these will get faded in as appropriate */
this.mainTrackMetadataNoAlbumArt.setAlpha(0.0f);
this.mainTrackMetadataWithAlbumArt.setAlpha(0.0f);
this.album.setOnClickListener((view) -> navigateToCurrentAlbum());
this.artist.setOnClickListener((view) -> navigateToCurrentArtist());
}

View File

@ -12,55 +12,61 @@
android:scaleType="centerCrop"/>
<LinearLayout
android:id="@+id/main_track_metadata_with_art"
android:background="@color/main_playback_metadata"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingTop="2dp"
android:paddingBottom="2dp">
android:orientation="vertical">
<ProgressBar
android:id="@+id/with_art_buffering"
android:id="@+id/buffering"
style="?android:attr/progressBarStyleSmall"
android:layout_marginTop="2dp"
android:padding="8dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<TextView
style="@style/LightDropShadow"
android:id="@+id/with_art_track_title"
android:layout_width="wrap_content"
<LinearLayout
android:id="@+id/main_track_metadata_with_art"
android:background="@color/main_playback_metadata"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:textColor="@color/theme_green"
android:text="-"/>
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingRight="24dp"
android:paddingTop="2dp"
android:paddingBottom="2dp">
<TextView
style="@style/LightDropShadow"
android:id="@+id/with_art_artist_and_album"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:textColor="@color/theme_yellow"
android:text="-"/>
<TextView
style="@style/LightDropShadow"
android:id="@+id/with_art_track_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:textColor="@color/theme_green"
android:text="-"/>
<TextView
style="@style/LightDropShadow"
android:id="@+id/with_art_volume"
android:textSize="@dimen/text_size_small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:layout_marginBottom="2dp"
android:text=""/>
<TextView
style="@style/LightDropShadow"
android:id="@+id/with_art_artist_and_album"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:textColor="@color/theme_yellow"
android:text="-"/>
<TextView
style="@style/LightDropShadow"
android:id="@+id/with_art_volume"
android:textSize="@dimen/text_size_small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_gravity="center"
android:layout_marginBottom="2dp"
android:text=""/>
</LinearLayout>
</LinearLayout>
<LinearLayout
@ -74,14 +80,6 @@
android:paddingTop="6dp"
android:paddingBottom="6dp">
<ProgressBar
android:id="@+id/buffering"
style="?android:attr/progressBarStyleSmall"
android:layout_marginTop="2dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"/>
<TextView
android:id="@+id/track_title"
android:layout_width="wrap_content"