- Improved play queue adapter for selection.
- Fixed media source resolution on background player for streams without an audio only stream. - Fixed background player not updating when screen turns back on. - Fixed background player notification switching to wrong repeat mode icon opacity on click.
This commit is contained in:
parent
bd9ee18e56
commit
a9aee21e58
|
@ -38,6 +38,11 @@
|
|||
android:name=".player.BackgroundPlayer"
|
||||
android:exported="false"/>
|
||||
|
||||
<activity
|
||||
android:name=".player.BackgroundPlayerActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:label="@string/title_activity_background_player"/>
|
||||
|
||||
<service
|
||||
android:name=".player.PopupVideoPlayer"
|
||||
android:exported="false"/>
|
||||
|
|
|
@ -27,6 +27,7 @@ import android.content.Intent;
|
|||
import android.content.IntentFilter;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.Binder;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import android.os.PowerManager;
|
||||
|
@ -36,9 +37,9 @@ import android.support.v4.app.NotificationCompat;
|
|||
import android.util.Log;
|
||||
import android.widget.RemoteViews;
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MergingMediaSource;
|
||||
|
||||
import org.schabi.newpipe.BuildConfig;
|
||||
import org.schabi.newpipe.MainActivity;
|
||||
|
@ -51,9 +52,6 @@ import org.schabi.newpipe.util.Constants;
|
|||
import org.schabi.newpipe.util.ListHelper;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* Base players joining the common properties
|
||||
|
@ -78,13 +76,30 @@ public final class BackgroundPlayer extends Service {
|
|||
private PowerManager.WakeLock wakeLock;
|
||||
private WifiManager.WifiLock wifiLock;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Service-Activity Binder
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public interface PlayerEventListener {
|
||||
void onPlaybackUpdate(int state, int repeatMode, PlaybackParameters parameters);
|
||||
void onProgressUpdate(int currentProgress, int duration, int bufferPercent);
|
||||
void onMetadataUpdate(StreamInfo info);
|
||||
void onServiceStopped();
|
||||
}
|
||||
|
||||
private PlayerEventListener activityListener;
|
||||
private IBinder mBinder;
|
||||
|
||||
class LocalBinder extends Binder {
|
||||
BasePlayerImpl getBackgroundPlayerInstance() {
|
||||
return BackgroundPlayer.this.basePlayerImpl;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Notification
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
private static final int NOTIFICATION_ID = 123789;
|
||||
|
||||
private boolean shouldUpdateNotification;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
private NotificationCompat.Builder notBuilder;
|
||||
private RemoteViews notRemoteView;
|
||||
|
@ -105,6 +120,8 @@ public final class BackgroundPlayer extends Service {
|
|||
ThemeHelper.setTheme(this);
|
||||
basePlayerImpl = new BasePlayerImpl(this);
|
||||
basePlayerImpl.setup();
|
||||
|
||||
mBinder = new LocalBinder();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -124,13 +141,19 @@ public final class BackgroundPlayer extends Service {
|
|||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Actions
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void openControl(final Context context) {
|
||||
final Intent intent = new Intent(context, BackgroundPlayerActivity.class);
|
||||
context.startActivity(intent);
|
||||
context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
|
||||
}
|
||||
|
||||
public void onOpenDetail(Context context, String videoUrl, String videoTitle) {
|
||||
if (DEBUG) Log.d(TAG, "onOpenDetail() called with: context = [" + context + "], videoUrl = [" + videoUrl + "]");
|
||||
Intent i = new Intent(context, MainActivity.class);
|
||||
|
@ -144,7 +167,11 @@ public final class BackgroundPlayer extends Service {
|
|||
}
|
||||
|
||||
private void onClose() {
|
||||
if (basePlayerImpl != null) basePlayerImpl.destroyPlayer();
|
||||
if (basePlayerImpl != null) {
|
||||
basePlayerImpl.stopActivityBinding();
|
||||
basePlayerImpl.destroyPlayer();
|
||||
}
|
||||
|
||||
stopForeground(true);
|
||||
releaseWifiAndCpu();
|
||||
stopSelf();
|
||||
|
@ -152,8 +179,6 @@ public final class BackgroundPlayer extends Service {
|
|||
|
||||
private void onScreenOnOff(boolean on) {
|
||||
if (DEBUG) Log.d(TAG, "onScreenOnOff() called with: on = [" + on + "]");
|
||||
shouldUpdateNotification = on;
|
||||
|
||||
if (on) {
|
||||
if (basePlayerImpl.isPlaying() && !basePlayerImpl.isProgressLoopRunning()) {
|
||||
basePlayerImpl.startProgressLoop();
|
||||
|
@ -168,9 +193,7 @@ public final class BackgroundPlayer extends Service {
|
|||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void resetNotification() {
|
||||
if (shouldUpdateNotification) {
|
||||
notBuilder = createNotification();
|
||||
}
|
||||
notBuilder = createNotification();
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder createNotification() {
|
||||
|
@ -211,7 +234,7 @@ public final class BackgroundPlayer extends Service {
|
|||
break;
|
||||
case Player.REPEAT_MODE_ONE:
|
||||
// todo change image
|
||||
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
|
||||
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 168);
|
||||
break;
|
||||
case Player.REPEAT_MODE_ALL:
|
||||
remoteViews.setInt(R.id.notificationRepeat, setAlphaMethodName, 255);
|
||||
|
@ -227,7 +250,7 @@ public final class BackgroundPlayer extends Service {
|
|||
*/
|
||||
private synchronized void updateNotification(int drawableId) {
|
||||
//if (DEBUG) Log.d(TAG, "updateNotification() called with: drawableId = [" + drawableId + "]");
|
||||
if (notBuilder == null || !shouldUpdateNotification) return;
|
||||
if (notBuilder == null) return;
|
||||
if (drawableId != -1) {
|
||||
if (notRemoteView != null) notRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setImageViewResource(R.id.notificationPlayPause, drawableId);
|
||||
|
@ -270,7 +293,7 @@ public final class BackgroundPlayer extends Service {
|
|||
|
||||
//////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private class BasePlayerImpl extends BasePlayer {
|
||||
protected class BasePlayerImpl extends BasePlayer {
|
||||
|
||||
BasePlayerImpl(Context context) {
|
||||
super(context);
|
||||
|
@ -280,8 +303,7 @@ public final class BackgroundPlayer extends Service {
|
|||
public void handleIntent(Intent intent) {
|
||||
super.handleIntent(intent);
|
||||
|
||||
shouldUpdateNotification = true;
|
||||
notBuilder = createNotification();
|
||||
resetNotification();
|
||||
startForeground(NOTIFICATION_ID, notBuilder.build());
|
||||
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setProgressBar(R.id.notificationProgressBar, 100, 0, false);
|
||||
|
@ -329,23 +351,6 @@ public final class BackgroundPlayer extends Service {
|
|||
@Override
|
||||
public void onRepeatClicked() {
|
||||
super.onRepeatClicked();
|
||||
|
||||
int opacity = 255;
|
||||
switch (simpleExoPlayer.getRepeatMode()) {
|
||||
case Player.REPEAT_MODE_OFF:
|
||||
opacity = 77;
|
||||
break;
|
||||
case Player.REPEAT_MODE_ONE:
|
||||
// todo change image
|
||||
opacity = 168;
|
||||
break;
|
||||
case Player.REPEAT_MODE_ALL:
|
||||
opacity = 255;
|
||||
break;
|
||||
}
|
||||
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
|
||||
updateNotification(-1);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -368,6 +373,7 @@ public final class BackgroundPlayer extends Service {
|
|||
}
|
||||
|
||||
updateNotification(-1);
|
||||
updateProgress(currentProgress, duration, bufferPercent);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -386,16 +392,6 @@ public final class BackgroundPlayer extends Service {
|
|||
triggerProgressUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Disable default behavior
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int i) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
|
@ -408,6 +404,42 @@ public final class BackgroundPlayer extends Service {
|
|||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// ExoPlayer Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void onPlaybackParametersChanged(PlaybackParameters playbackParameters) {
|
||||
super.onPlaybackParametersChanged(playbackParameters);
|
||||
updatePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadingChanged(boolean isLoading) {
|
||||
// Disable default behavior
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRepeatModeChanged(int i) {
|
||||
int opacity = 255;
|
||||
switch (simpleExoPlayer.getRepeatMode()) {
|
||||
case Player.REPEAT_MODE_OFF:
|
||||
opacity = 77;
|
||||
break;
|
||||
case Player.REPEAT_MODE_ONE:
|
||||
// todo change image
|
||||
opacity = 168;
|
||||
break;
|
||||
case Player.REPEAT_MODE_ALL:
|
||||
opacity = 255;
|
||||
break;
|
||||
}
|
||||
if (notRemoteView != null) notRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
|
||||
if (bigNotRemoteView != null) bigNotRemoteView.setInt(R.id.notificationRepeat, setAlphaMethodName, opacity);
|
||||
updateNotification(-1);
|
||||
updatePlayback();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -422,11 +454,14 @@ public final class BackgroundPlayer extends Service {
|
|||
bigNotRemoteView.setTextViewText(R.id.notificationSongName, getVideoTitle());
|
||||
bigNotRemoteView.setTextViewText(R.id.notificationArtist, getUploaderName());
|
||||
updateNotification(-1);
|
||||
updateMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaSource sourceOf(final StreamInfo info) {
|
||||
final int index = ListHelper.getDefaultAudioFormat(context, info.audio_streams);
|
||||
if (index < 0) return null;
|
||||
|
||||
final AudioStream audio = info.audio_streams.get(index);
|
||||
return buildMediaSource(audio.url, MediaFormat.getSuffixById(audio.format));
|
||||
}
|
||||
|
@ -435,6 +470,43 @@ public final class BackgroundPlayer extends Service {
|
|||
public void shutdown() {
|
||||
super.shutdown();
|
||||
stopSelf();
|
||||
stopActivityBinding();
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Activity Event Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
public void setActivityListener(PlayerEventListener listener) {
|
||||
activityListener = listener;
|
||||
updateMetadata();
|
||||
updatePlayback();
|
||||
triggerProgressUpdate();
|
||||
}
|
||||
|
||||
private void updateMetadata() {
|
||||
if (activityListener != null && currentInfo != null) {
|
||||
activityListener.onMetadataUpdate(currentInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePlayback() {
|
||||
if (activityListener != null) {
|
||||
activityListener.onPlaybackUpdate(currentState, simpleExoPlayer.getRepeatMode(), simpleExoPlayer.getPlaybackParameters());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress(int currentProgress, int duration, int bufferPercent) {
|
||||
if (activityListener != null) {
|
||||
activityListener.onProgressUpdate(currentProgress, duration, bufferPercent);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopActivityBinding() {
|
||||
if (activityListener != null) {
|
||||
activityListener.onServiceStopped();
|
||||
activityListener = null;
|
||||
}
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -469,7 +541,7 @@ public final class BackgroundPlayer extends Service {
|
|||
onVideoPlayPause();
|
||||
break;
|
||||
case ACTION_OPEN_DETAIL:
|
||||
onOpenDetail(BackgroundPlayer.this, getVideoUrl(), getVideoTitle());
|
||||
openControl(BackgroundPlayer.this);
|
||||
break;
|
||||
case ACTION_REPEAT:
|
||||
onRepeatClicked();
|
||||
|
@ -493,6 +565,12 @@ public final class BackgroundPlayer extends Service {
|
|||
// States
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
@Override
|
||||
public void changeState(int state) {
|
||||
super.changeState(state);
|
||||
updatePlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlocked() {
|
||||
super.onBlocked();
|
||||
|
|
|
@ -0,0 +1,305 @@
|
|||
package org.schabi.newpipe.player;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItemBuilder;
|
||||
import org.schabi.newpipe.settings.SettingsActivity;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
import org.schabi.newpipe.util.ThemeHelper;
|
||||
|
||||
public class BackgroundPlayerActivity extends AppCompatActivity
|
||||
implements BackgroundPlayer.PlayerEventListener, SeekBar.OnSeekBarChangeListener, View.OnClickListener {
|
||||
|
||||
private static final String TAG = "BGPlayerActivity";
|
||||
|
||||
private boolean isServiceBound;
|
||||
private ServiceConnection serviceConnection;
|
||||
|
||||
private BackgroundPlayer.BasePlayerImpl player;
|
||||
|
||||
private boolean isSeeking;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Views
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private View rootView;
|
||||
|
||||
private RecyclerView itemsList;
|
||||
|
||||
private TextView metadataTitle;
|
||||
private TextView metadataArtist;
|
||||
|
||||
private SeekBar progressSeekBar;
|
||||
private TextView progressCurrentTime;
|
||||
private TextView progressEndTime;
|
||||
|
||||
private ImageButton repeatButton;
|
||||
private ImageButton backwardButton;
|
||||
private ImageButton playPauseButton;
|
||||
private ImageButton forwardButton;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Activity Lifecycle
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeHelper.setTheme(this);
|
||||
setContentView(R.layout.activity_background_player);
|
||||
rootView = findViewById(R.id.main_content);
|
||||
|
||||
final Toolbar toolbar = rootView.findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
getSupportActionBar().setTitle(R.string.title_activity_background_player);
|
||||
|
||||
serviceConnection = backgroundPlayerConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
final Intent mIntent = new Intent(this, BackgroundPlayer.class);
|
||||
final boolean success = bindService(mIntent, serviceConnection, BIND_AUTO_CREATE);
|
||||
if (!success) unbindService(serviceConnection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
finish();
|
||||
return true;
|
||||
case R.id.action_settings:
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
if(isServiceBound) {
|
||||
unbindService(serviceConnection);
|
||||
isServiceBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Service Connection
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private ServiceConnection backgroundPlayerConnection() {
|
||||
return new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
Log.d(TAG, "Background player service is disconnected");
|
||||
isServiceBound = false;
|
||||
player = null;
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
Log.d(TAG, "Background player service is connected");
|
||||
final BackgroundPlayer.LocalBinder mLocalBinder = (BackgroundPlayer.LocalBinder) service;
|
||||
player = mLocalBinder.getBackgroundPlayerInstance();
|
||||
if (player == null) {
|
||||
finish();
|
||||
} else {
|
||||
isServiceBound = true;
|
||||
buildComponents();
|
||||
|
||||
player.setActivityListener(BackgroundPlayerActivity.this);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Component Building
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void buildComponents() {
|
||||
buildQueue();
|
||||
buildMetadata();
|
||||
buildSeekBar();
|
||||
buildControls();
|
||||
}
|
||||
|
||||
private void buildQueue() {
|
||||
itemsList = findViewById(R.id.play_queue);
|
||||
itemsList.setLayoutManager(new LinearLayoutManager(this));
|
||||
itemsList.setAdapter(player.playQueueAdapter);
|
||||
itemsList.setClickable(true);
|
||||
|
||||
player.playQueueAdapter.setSelectedListener(new PlayQueueItemBuilder.OnSelectedListener() {
|
||||
@Override
|
||||
public void selected(PlayQueueItem item) {
|
||||
final int index = player.playQueue.indexOf(item);
|
||||
if (index != -1) player.playQueue.setIndex(index);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void buildMetadata() {
|
||||
metadataTitle = rootView.findViewById(R.id.song_name);
|
||||
metadataArtist = rootView.findViewById(R.id.artist_name);
|
||||
}
|
||||
|
||||
private void buildSeekBar() {
|
||||
progressCurrentTime = rootView.findViewById(R.id.current_time);
|
||||
progressSeekBar = rootView.findViewById(R.id.seek_bar);
|
||||
progressEndTime = rootView.findViewById(R.id.end_time);
|
||||
|
||||
progressSeekBar.setOnSeekBarChangeListener(this);
|
||||
}
|
||||
|
||||
private void buildControls() {
|
||||
repeatButton = rootView.findViewById(R.id.control_repeat);
|
||||
backwardButton = rootView.findViewById(R.id.control_backward);
|
||||
playPauseButton = rootView.findViewById(R.id.control_play_pause);
|
||||
forwardButton = rootView.findViewById(R.id.control_forward);
|
||||
|
||||
repeatButton.setOnClickListener(this);
|
||||
backwardButton.setOnClickListener(this);
|
||||
playPauseButton.setOnClickListener(this);
|
||||
forwardButton.setOnClickListener(this);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Component On-Click Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (view.getId() == repeatButton.getId()) {
|
||||
player.onRepeatClicked();
|
||||
} else if (view.getId() == backwardButton.getId()) {
|
||||
player.onPlayPrevious();
|
||||
} else if (view.getId() == playPauseButton.getId()) {
|
||||
player.onVideoPlayPause();
|
||||
} else if (view.getId() == forwardButton.getId()) {
|
||||
player.onPlayNext();
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Seekbar Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) progressCurrentTime.setText(Localization.getDurationString(progress / 1000));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
isSeeking = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
player.simpleExoPlayer.seekTo(seekBar.getProgress());
|
||||
isSeeking = false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// Binding Service Listener
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onPlaybackUpdate(int state, int repeatMode, PlaybackParameters parameters) {
|
||||
switch (state) {
|
||||
case BasePlayer.STATE_PAUSED:
|
||||
playPauseButton.setImageResource(R.drawable.ic_play_arrow_white);
|
||||
break;
|
||||
case BasePlayer.STATE_PLAYING:
|
||||
playPauseButton.setImageResource(R.drawable.ic_pause_white);
|
||||
break;
|
||||
case BasePlayer.STATE_COMPLETED:
|
||||
playPauseButton.setImageResource(R.drawable.ic_replay_white);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
int alpha = 255;
|
||||
switch (repeatMode) {
|
||||
case Player.REPEAT_MODE_OFF:
|
||||
alpha = 77;
|
||||
break;
|
||||
case Player.REPEAT_MODE_ONE:
|
||||
// todo change image
|
||||
alpha = 168;
|
||||
break;
|
||||
case Player.REPEAT_MODE_ALL:
|
||||
alpha = 255;
|
||||
break;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
repeatButton.setImageAlpha(alpha);
|
||||
} else {
|
||||
repeatButton.setAlpha(alpha);
|
||||
}
|
||||
|
||||
if (parameters != null) {
|
||||
final float speed = parameters.speed;
|
||||
final float pitch = parameters.pitch;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onProgressUpdate(int currentProgress, int duration, int bufferPercent) {
|
||||
// Set buffer progress
|
||||
progressSeekBar.setSecondaryProgress((int) (progressSeekBar.getMax() * ((float) bufferPercent / 100)));
|
||||
|
||||
// Set Duration
|
||||
progressSeekBar.setMax(duration);
|
||||
progressEndTime.setText(Localization.getDurationString(duration / 1000));
|
||||
|
||||
// Set current time if not seeking
|
||||
if (!isSeeking) {
|
||||
progressSeekBar.setProgress(currentProgress);
|
||||
progressCurrentTime.setText(Localization.getDurationString(currentProgress / 1000));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMetadataUpdate(StreamInfo info) {
|
||||
if (info != null) {
|
||||
metadataTitle.setText(info.name);
|
||||
metadataArtist.setText(info.uploader_name);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceStopped() {
|
||||
finish();
|
||||
}
|
||||
}
|
|
@ -27,7 +27,6 @@ import android.content.Context;
|
|||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.media.audiofx.AudioEffect;
|
||||
|
@ -35,7 +34,6 @@ import android.net.Uri;
|
|||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
|
@ -72,28 +70,21 @@ import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvicto
|
|||
import com.google.android.exoplayer2.upstream.cache.SimpleCache;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
import com.nostra13.universalimageloader.core.assist.ImageSize;
|
||||
import com.nostra13.universalimageloader.core.listener.SimpleImageLoadingListener;
|
||||
|
||||
import org.schabi.newpipe.Downloader;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem;
|
||||
import org.schabi.newpipe.player.playback.MediaSourceManager;
|
||||
import org.schabi.newpipe.player.playback.PlaybackListener;
|
||||
import org.schabi.newpipe.playlist.ExternalPlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueueAdapter;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Formatter;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
@ -124,6 +115,8 @@ public abstract class BasePlayer implements Player.EventListener,
|
|||
protected BroadcastReceiver broadcastReceiver;
|
||||
protected IntentFilter intentFilter;
|
||||
|
||||
protected PlayQueueAdapter playQueueAdapter;
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Intent
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -285,6 +278,9 @@ public abstract class BasePlayer implements Player.EventListener,
|
|||
playQueue = queue;
|
||||
playQueue.init();
|
||||
playbackManager = new MediaSourceManager(this, playQueue);
|
||||
|
||||
if (playQueueAdapter != null) playQueueAdapter.dispose();
|
||||
playQueueAdapter = new PlayQueueAdapter(playQueue);
|
||||
}
|
||||
|
||||
public void initThumbnail(final String url) {
|
||||
|
@ -816,6 +812,7 @@ public abstract class BasePlayer implements Player.EventListener,
|
|||
private final Formatter formatter = new Formatter(stringBuilder, Locale.getDefault());
|
||||
private final NumberFormat speedFormatter = new DecimalFormat("0.##x");
|
||||
|
||||
// todo: merge this into Localization
|
||||
public String getTimeString(int milliSeconds) {
|
||||
long seconds = (milliSeconds % 60000L) / 1000L;
|
||||
long minutes = (milliSeconds % 3600000L) / 60000L;
|
||||
|
|
|
@ -64,13 +64,9 @@ import org.schabi.newpipe.R;
|
|||
import org.schabi.newpipe.extractor.MediaFormat;
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||
import org.schabi.newpipe.playlist.PlayQueue;
|
||||
import org.schabi.newpipe.playlist.PlayQueueItem;
|
||||
import org.schabi.newpipe.playlist.SinglePlayQueue;
|
||||
import org.schabi.newpipe.util.AnimationUtils;
|
||||
import org.schabi.newpipe.util.ListHelper;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -111,6 +107,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
|||
private List<TrackGroupInfo> trackGroupInfos;
|
||||
private int videoRendererIndex = -1;
|
||||
private TrackGroupArray videoTrackGroups;
|
||||
private TrackGroup selectedVideoTrackGroup;
|
||||
|
||||
private boolean startedFromNewPipe = true;
|
||||
protected boolean wasPlaying = false;
|
||||
|
@ -211,7 +208,7 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
|||
public void initPlayer() {
|
||||
super.initPlayer();
|
||||
simpleExoPlayer.setVideoSurfaceView(surfaceView);
|
||||
simpleExoPlayer.setVideoListener(this);
|
||||
simpleExoPlayer.addVideoListener(this);
|
||||
|
||||
trackSelector.setTunnelingAudioSessionId(C.generateAudioSessionIdV21(context));
|
||||
}
|
||||
|
@ -229,6 +226,79 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
|||
);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// UI Builders
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private final class TrackGroupInfo {
|
||||
final int track;
|
||||
final int group;
|
||||
final Format format;
|
||||
|
||||
TrackGroupInfo(final int track, final int group, final Format format) {
|
||||
this.track = track;
|
||||
this.group = group;
|
||||
this.format = format;
|
||||
}
|
||||
}
|
||||
|
||||
private void buildQualityMenu() {
|
||||
if (qualityPopupMenu == null || videoTrackGroups == null || selectedVideoTrackGroup == null || videoTrackGroups.length != availableStreams.size()) return;
|
||||
|
||||
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
|
||||
trackGroupInfos = new ArrayList<>();
|
||||
int acc = 0;
|
||||
|
||||
// Each group represent a source in sorted order of how the media source was built
|
||||
for (int groupIndex = 0; groupIndex < videoTrackGroups.length; groupIndex++) {
|
||||
final TrackGroup group = videoTrackGroups.get(groupIndex);
|
||||
final VideoStream stream = availableStreams.get(groupIndex);
|
||||
|
||||
// For each source, there may be one or multiple tracks depending on the source type
|
||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||
final Format format = group.getFormat(trackIndex);
|
||||
final boolean isSetCurrent = selectedVideoTrackGroup.indexOf(format) != -1;
|
||||
|
||||
if (group.length == 1 && videoTrackGroups.length == availableStreams.size()) {
|
||||
// If the source is non-adaptive (extractor source), then we use the resolution contained in the stream
|
||||
if (isSetCurrent) qualityTextView.setText(stream.resolution);
|
||||
|
||||
final String menuItem = MediaFormat.getNameById(stream.format) + " " +
|
||||
stream.resolution + " (" + format.width + "x" + format.height + ")";
|
||||
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem);
|
||||
} else {
|
||||
// Otherwise, we have an adaptive source, which contains multiple formats and
|
||||
// thus have no inherent quality format
|
||||
if (isSetCurrent) qualityTextView.setText(resolutionStringOf(format));
|
||||
|
||||
final MediaFormat mediaFormat = MediaFormat.getFromMimeType(format.sampleMimeType);
|
||||
final String mediaName = mediaFormat == null ? format.sampleMimeType : mediaFormat.name;
|
||||
|
||||
final String menuItem = mediaName + " " + format.width + "x" + format.height;
|
||||
qualityPopupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem);
|
||||
}
|
||||
|
||||
trackGroupInfos.add(new TrackGroupInfo(trackIndex, groupIndex, format));
|
||||
acc++;
|
||||
}
|
||||
}
|
||||
|
||||
qualityPopupMenu.setOnMenuItemClickListener(this);
|
||||
qualityPopupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
private void buildPlaybackSpeedMenu() {
|
||||
if (playbackSpeedPopupMenu == null) return;
|
||||
|
||||
playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId);
|
||||
for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
|
||||
playbackSpeedPopupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i]));
|
||||
}
|
||||
playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
|
||||
playbackSpeedPopupMenu.setOnMenuItemClickListener(this);
|
||||
playbackSpeedPopupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// Playback Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -243,8 +313,8 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
|||
selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, videos);
|
||||
}
|
||||
|
||||
playbackSpeedPopupMenu.getMenu().removeGroup(playbackSpeedPopupMenuGroupId);
|
||||
buildPlaybackSpeedMenu(playbackSpeedPopupMenu);
|
||||
buildPlaybackSpeedMenu();
|
||||
buildQualityMenu();
|
||||
}
|
||||
|
||||
public MediaSource sourceOf(final StreamInfo info) {
|
||||
|
@ -259,15 +329,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
|||
return new MergingMediaSource(sources.toArray(new MediaSource[sources.size()]));
|
||||
}
|
||||
|
||||
private void buildPlaybackSpeedMenu(PopupMenu popupMenu) {
|
||||
for (int i = 0; i < PLAYBACK_SPEEDS.length; i++) {
|
||||
popupMenu.getMenu().add(playbackSpeedPopupMenuGroupId, i, Menu.NONE, formatSpeed(PLAYBACK_SPEEDS[i]));
|
||||
}
|
||||
playbackSpeed.setText(formatSpeed(getPlaybackSpeed()));
|
||||
popupMenu.setOnMenuItemClickListener(this);
|
||||
popupMenu.setOnDismissListener(this);
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
// States Implementation
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
@ -343,22 +404,6 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
|||
// ExoPlayer Video Listener
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private class TrackGroupInfo {
|
||||
final int track;
|
||||
final int group;
|
||||
final String label;
|
||||
final String resolution;
|
||||
final Format format;
|
||||
|
||||
TrackGroupInfo(final int track, final int group, final String label, final String resolution, final Format format) {
|
||||
this.track = track;
|
||||
this.group = group;
|
||||
this.label = label;
|
||||
this.resolution = resolution;
|
||||
this.format = format;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
|
||||
super.onTracksChanged(trackGroups, trackSelections);
|
||||
|
@ -376,52 +421,9 @@ public abstract class VideoPlayer extends BasePlayer implements SimpleExoPlayer.
|
|||
}
|
||||
}
|
||||
videoTrackGroups = trackSelector.getCurrentMappedTrackInfo().getTrackGroups(videoRendererIndex);
|
||||
final TrackGroup selectedTrackGroup = trackSelections.get(videoRendererIndex).getTrackGroup();
|
||||
selectedVideoTrackGroup = trackSelections.get(videoRendererIndex).getTrackGroup();
|
||||
|
||||
qualityPopupMenu.getMenu().removeGroup(qualityPopupMenuGroupId);
|
||||
buildQualityMenu(qualityPopupMenu, videoTrackGroups, selectedTrackGroup);
|
||||
}
|
||||
|
||||
private void buildQualityMenu(PopupMenu popupMenu, TrackGroupArray videoTrackGroups, TrackGroup selectedTrackGroup) {
|
||||
trackGroupInfos = new ArrayList<>();
|
||||
int acc = 0;
|
||||
|
||||
// Each group represent a source in sorted order of how the media source was built
|
||||
for (int groupIndex = 0; groupIndex < videoTrackGroups.length; groupIndex++) {
|
||||
final TrackGroup group = videoTrackGroups.get(groupIndex);
|
||||
final VideoStream stream = availableStreams.get(groupIndex);
|
||||
|
||||
// For each source, there may be one or multiple tracks depending on the source type
|
||||
for (int trackIndex = 0; trackIndex < group.length; trackIndex++) {
|
||||
final Format format = group.getFormat(trackIndex);
|
||||
final boolean isSetCurrent = selectedTrackGroup.indexOf(format) != -1;
|
||||
|
||||
if (group.length == 1 && videoTrackGroups.length == availableStreams.size()) {
|
||||
// If the source is non-adaptive (extractor source), then we use the resolution contained in the stream
|
||||
if (isSetCurrent) qualityTextView.setText(stream.resolution);
|
||||
|
||||
final String menuItem = MediaFormat.getNameById(stream.format) + " " +
|
||||
stream.resolution + " (" + format.width + "x" + format.height + ")";
|
||||
popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem);
|
||||
} else {
|
||||
// Otherwise, we have an adaptive source, which contains multiple formats and
|
||||
// thus have no inherent quality format
|
||||
if (isSetCurrent) qualityTextView.setText(resolutionStringOf(format));
|
||||
|
||||
final MediaFormat mediaFormat = MediaFormat.getFromMimeType(format.sampleMimeType);
|
||||
final String mediaName = mediaFormat == null ? format.sampleMimeType : mediaFormat.name;
|
||||
|
||||
final String menuItem = mediaName + " " + format.width + "x" + format.height;
|
||||
popupMenu.getMenu().add(qualityPopupMenuGroupId, acc, Menu.NONE, menuItem);
|
||||
}
|
||||
|
||||
trackGroupInfos.add(new TrackGroupInfo(trackIndex, groupIndex, MediaFormat.getNameById(stream.format), stream.resolution, format));
|
||||
acc++;
|
||||
}
|
||||
}
|
||||
|
||||
popupMenu.setOnMenuItemClickListener(this);
|
||||
popupMenu.setOnDismissListener(this);
|
||||
buildQualityMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -13,7 +13,6 @@ import org.schabi.newpipe.playlist.PlayQueueItem;
|
|||
|
||||
import java.io.IOException;
|
||||
|
||||
import io.reactivex.SingleObserver;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Consumer;
|
||||
|
@ -86,7 +85,7 @@ public final class DeferredMediaSource implements MediaSource {
|
|||
*
|
||||
* If loading fails here, an error will be propagated out and result in a
|
||||
* {@link com.google.android.exoplayer2.ExoPlaybackException}, which is delegated
|
||||
* out to the player.
|
||||
* to the player.
|
||||
* */
|
||||
public synchronized void load() {
|
||||
if (state != STATE_PREPARED || stream == null || loader != null) return;
|
||||
|
@ -95,15 +94,23 @@ public final class DeferredMediaSource implements MediaSource {
|
|||
final Consumer<StreamInfo> onSuccess = new Consumer<StreamInfo>() {
|
||||
@Override
|
||||
public void accept(StreamInfo streamInfo) throws Exception {
|
||||
if (exoPlayer == null && listener == null) {
|
||||
error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
|
||||
} else {
|
||||
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
|
||||
Log.d(TAG, " Loaded: [" + stream.getTitle() + "] with url: " + stream.getUrl());
|
||||
state = STATE_LOADED;
|
||||
|
||||
mediaSource = callback.sourceOf(streamInfo);
|
||||
mediaSource.prepareSource(exoPlayer, false, listener);
|
||||
state = STATE_LOADED;
|
||||
if (exoPlayer == null || listener == null || streamInfo == null) {
|
||||
error = new Throwable("Stream info loading failed. URL: " + stream.getUrl());
|
||||
return;
|
||||
}
|
||||
|
||||
mediaSource = callback.sourceOf(streamInfo);
|
||||
if (mediaSource == null) {
|
||||
error = new Throwable("Unable to resolve source from stream info. URL: " + stream.getUrl() +
|
||||
", audio count: " + streamInfo.audio_streams.size() +
|
||||
", video count: " + streamInfo.video_only_streams.size() + streamInfo.video_streams.size());
|
||||
return;
|
||||
}
|
||||
|
||||
mediaSource.prepareSource(exoPlayer, false, listener);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
playQueue.append(data);
|
||||
}
|
||||
|
||||
public void add(final PlayQueueItem data) {
|
||||
public void add(final PlayQueueItem... data) {
|
||||
playQueue.append(data);
|
||||
}
|
||||
|
||||
|
@ -136,7 +136,6 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
return count;
|
||||
}
|
||||
|
||||
// don't ask why we have to do that this way... it's android accept it -.-
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if(header != null && position == 0) {
|
||||
|
@ -167,15 +166,17 @@ public class PlayQueueAdapter extends RecyclerView.Adapter<RecyclerView.ViewHold
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int i) {
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
if(holder instanceof PlayQueueItemHolder) {
|
||||
if(header != null) {
|
||||
i--;
|
||||
}
|
||||
playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(i));
|
||||
} else if(holder instanceof HFHolder && i == 0 && header != null) {
|
||||
// Ensure header does not interfere with list building
|
||||
if (header != null) position--;
|
||||
// Build the list item
|
||||
playQueueItemBuilder.buildStreamInfoItem((PlayQueueItemHolder) holder, playQueue.getStreams().get(position));
|
||||
// Check if the current item should be selected/highlighted
|
||||
holder.itemView.setSelected(playQueue.getIndex() == position);
|
||||
} else if(holder instanceof HFHolder && position == 0 && header != null) {
|
||||
((HFHolder) holder).view = header;
|
||||
} else if(holder instanceof HFHolder && i == playQueue.getStreams().size() && footer != null && showFooter) {
|
||||
} else if(holder instanceof HFHolder && position == playQueue.getStreams().size() && footer != null && showFooter) {
|
||||
((HFHolder) holder).view = footer;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ public class PlayQueueItem implements Serializable {
|
|||
final private String url;
|
||||
final private int serviceId;
|
||||
final private long duration;
|
||||
final private String thumbnailUrl;
|
||||
final private String uploader;
|
||||
|
||||
private Throwable error;
|
||||
|
||||
|
@ -30,6 +32,8 @@ public class PlayQueueItem implements Serializable {
|
|||
this.url = streamInfo.url;
|
||||
this.serviceId = streamInfo.service_id;
|
||||
this.duration = streamInfo.duration;
|
||||
this.thumbnailUrl = streamInfo.thumbnail_url;
|
||||
this.uploader = streamInfo.uploader_name;
|
||||
|
||||
this.stream = Single.just(streamInfo);
|
||||
}
|
||||
|
@ -39,6 +43,8 @@ public class PlayQueueItem implements Serializable {
|
|||
this.url = streamInfoItem.url;
|
||||
this.serviceId = streamInfoItem.service_id;
|
||||
this.duration = streamInfoItem.duration;
|
||||
this.thumbnailUrl = streamInfoItem.thumbnail_url;
|
||||
this.uploader = streamInfoItem.uploader_name;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -59,6 +65,14 @@ public class PlayQueueItem implements Serializable {
|
|||
return duration;
|
||||
}
|
||||
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
public String getUploader() {
|
||||
return uploader;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Throwable getError() {
|
||||
return error;
|
||||
|
|
|
@ -5,9 +5,11 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import com.nostra13.universalimageloader.core.DisplayImageOptions;
|
||||
import com.nostra13.universalimageloader.core.ImageLoader;
|
||||
|
||||
import java.util.Locale;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.util.Localization;
|
||||
|
||||
|
||||
public class PlayQueueItemBuilder {
|
||||
|
@ -15,68 +17,44 @@ public class PlayQueueItemBuilder {
|
|||
private static final String TAG = PlayQueueItemBuilder.class.toString();
|
||||
|
||||
public interface OnSelectedListener {
|
||||
void selected(int serviceId, String url, String title);
|
||||
void selected(PlayQueueItem item);
|
||||
}
|
||||
|
||||
private OnSelectedListener onStreamInfoItemSelectedListener;
|
||||
private OnSelectedListener onItemClickListener;
|
||||
|
||||
public PlayQueueItemBuilder() {}
|
||||
|
||||
public void setOnSelectedListener(OnSelectedListener listener) {
|
||||
this.onStreamInfoItemSelectedListener = listener;
|
||||
this.onItemClickListener = listener;
|
||||
}
|
||||
|
||||
public View buildView(ViewGroup parent, final PlayQueueItem item) {
|
||||
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
final View itemView = inflater.inflate(R.layout.play_queue_item, parent, false);
|
||||
final PlayQueueItemHolder holder = new PlayQueueItemHolder(itemView);
|
||||
|
||||
buildStreamInfoItem(holder, item);
|
||||
|
||||
return itemView;
|
||||
}
|
||||
|
||||
|
||||
public void buildStreamInfoItem(PlayQueueItemHolder holder, final PlayQueueItem item) {
|
||||
if (!TextUtils.isEmpty(item.getTitle())) holder.itemVideoTitleView.setText(item.getTitle());
|
||||
if (!TextUtils.isEmpty(item.getUploader())) holder.itemAdditionalDetailsView.setText(item.getUploader());
|
||||
|
||||
if (item.getDuration() > 0) {
|
||||
holder.itemDurationView.setText(getDurationString(item.getDuration()));
|
||||
holder.itemDurationView.setText(Localization.getDurationString(item.getDuration()));
|
||||
} else {
|
||||
holder.itemDurationView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
ImageLoader.getInstance().displayImage(item.getThumbnailUrl(), holder.itemThumbnailView, IMAGE_OPTIONS);
|
||||
|
||||
holder.itemRoot.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if(onStreamInfoItemSelectedListener != null) {
|
||||
onStreamInfoItemSelectedListener.selected(item.getServiceId(), item.getUrl(), item.getTitle());
|
||||
if (onItemClickListener != null) {
|
||||
onItemClickListener.selected(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public static String getDurationString(long duration) {
|
||||
if(duration < 0) {
|
||||
duration = 0;
|
||||
}
|
||||
String output;
|
||||
long days = duration / (24 * 60 * 60); /* greater than a day */
|
||||
duration %= (24 * 60 * 60);
|
||||
long hours = duration / (60 * 60); /* greater than an hour */
|
||||
duration %= (60 * 60);
|
||||
long minutes = duration / 60;
|
||||
long seconds = duration % 60;
|
||||
|
||||
//handle days
|
||||
if (days > 0) {
|
||||
output = String.format(Locale.US, "%d:%02d:%02d:%02d", days, hours, minutes, seconds);
|
||||
} else if(hours > 0) {
|
||||
output = String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds);
|
||||
} else {
|
||||
output = String.format(Locale.US, "%d:%02d", minutes, seconds);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
private static final DisplayImageOptions IMAGE_OPTIONS =
|
||||
new DisplayImageOptions.Builder()
|
||||
.cacheInMemory(true)
|
||||
.showImageOnFail(R.drawable.dummy_thumbnail)
|
||||
.showImageForEmptyUri(R.drawable.dummy_thumbnail)
|
||||
.showImageOnLoading(R.drawable.dummy_thumbnail)
|
||||
.build();
|
||||
}
|
||||
|
|
|
@ -31,13 +31,17 @@ import org.schabi.newpipe.info_list.holder.InfoItemHolder;
|
|||
|
||||
public class PlayQueueItemHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
public final TextView itemVideoTitleView, itemDurationView;
|
||||
public final TextView itemVideoTitleView, itemDurationView, itemAdditionalDetailsView;
|
||||
public final ImageView itemThumbnailView;
|
||||
|
||||
public final View itemRoot;
|
||||
|
||||
public PlayQueueItemHolder(View v) {
|
||||
super(v);
|
||||
itemRoot = v.findViewById(R.id.itemRoot);
|
||||
itemVideoTitleView = (TextView) v.findViewById(R.id.itemVideoTitleView);
|
||||
itemDurationView = (TextView) v.findViewById(R.id.itemDurationView);
|
||||
itemVideoTitleView = v.findViewById(R.id.itemVideoTitleView);
|
||||
itemDurationView = v.findViewById(R.id.itemDurationView);
|
||||
itemAdditionalDetailsView = v.findViewById(R.id.itemAdditionalDetails);
|
||||
itemThumbnailView = v.findViewById(R.id.itemThumbnailView);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true" android:color="@color/dark_youtube_primary_color"/>
|
||||
<item android:color="@color/dark_youtube_accent_color"/>
|
||||
</selector>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true" android:color="@color/light_youtube_primary_color"/>
|
||||
<item android:color="@color/light_youtube_accent_color"/>
|
||||
</selector>
|
|
@ -0,0 +1,186 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/main_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context="org.schabi.newpipe.player.BackgroundPlayerActivity">
|
||||
|
||||
<android.support.design.widget.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/appbar_padding_top"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.ActionBar">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_weight="1"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:title="@string/app_name"/>
|
||||
|
||||
</android.support.design.widget.AppBarLayout>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/play_queue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/appbar"
|
||||
android:layout_above="@+id/metadata"
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="LinearLayoutManager"
|
||||
tools:listitem="@layout/play_queue_item"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/metadata"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/progress_bar"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp"
|
||||
tools:ignore="RtlHardcoded,RtlSymmetry">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/song_name"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent.Title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textSize="14sp"
|
||||
android:textColor="?attr/colorAccent"
|
||||
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis nec aliquam augue, eget cursus est. Ut id tristique enim, ut scelerisque tellus. Sed ultricies ipsum non mauris ultricies, commodo malesuada velit porta."/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/artist_name"
|
||||
style="@android:style/TextAppearance.StatusBar.EventContent"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textSize="12sp"
|
||||
tools:text="Duis posuere arcu condimentum lobortis mattis."/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/playback_controls"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/current_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:minHeight="40dp"
|
||||
android:text="-:--:--"
|
||||
android:textColor="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="1:06:29"/>
|
||||
|
||||
|
||||
<android.support.v7.widget.AppCompatSeekBar
|
||||
android:id="@+id/seek_bar"
|
||||
style="@style/Widget.AppCompat.SeekBar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingTop="8dp"
|
||||
tools:progress="25"
|
||||
tools:secondaryProgress="50"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/end_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="-:--:--"
|
||||
android:textColor="?attr/colorAccent"
|
||||
tools:ignore="HardcodedText"
|
||||
tools:text="1:23:49"/>
|
||||
</LinearLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/playback_controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="60dp"
|
||||
android:paddingTop="10dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/player_controls_bg"
|
||||
tools:ignore="RtlHardcoded">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_repeat"
|
||||
android:layout_width="25dp"
|
||||
android:layout_height="25dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:background="#00000000"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:scaleType="fitXY"
|
||||
android:src="@drawable/ic_repeat_white"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_backward"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="5dp"
|
||||
android:layout_toLeftOf="@+id/control_play_pause"
|
||||
android:background="#00000000"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="2dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_action_av_fast_rewind"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_play_pause"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="5dp"
|
||||
android:layout_toLeftOf="@+id/control_forward"
|
||||
android:background="#00000000"
|
||||
android:padding="2dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_pause_white"
|
||||
tools:ignore="ContentDescription"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/control_forward"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="8dp"
|
||||
android:background="#00000000"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="2dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_action_av_fast_forward"
|
||||
tools:ignore="ContentDescription"/>
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
|
@ -7,6 +7,7 @@
|
|||
android:layout_height="48dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="6dp">
|
||||
|
||||
<ImageView
|
||||
|
@ -54,6 +55,7 @@
|
|||
android:maxLines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textSize="@dimen/video_item_search_title_text_size"
|
||||
android:textColor="?attr/selector_color"
|
||||
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/>
|
||||
|
||||
<TextView
|
||||
|
@ -66,5 +68,6 @@
|
|||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textSize="@dimen/video_item_search_upload_date_text_size"
|
||||
tools:text="Uploader • 2 years ago • 10M views"/>
|
||||
android:textColor="?attr/selector_color"
|
||||
tools:text="Uploader"/>
|
||||
</RelativeLayout>
|
|
@ -1,51 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/itemRoot"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/video_item_search_height"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:padding="@dimen/video_item_search_padding">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemThumbnailView"
|
||||
android:layout_width="@dimen/video_item_search_thumbnail_image_width"
|
||||
android:layout_height="@dimen/video_item_search_thumbnail_image_height"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_marginRight="@dimen/video_item_search_image_right_margin"
|
||||
android:contentDescription="@string/list_thumbnail_view_description"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/dummy_thumbnail"
|
||||
tools:ignore="RtlHardcoded"/>
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemPlaylistTitleView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_toRightOf="@id/itemThumbnailView"
|
||||
android:layout_toEndOf="@id/itemThumbnailView"
|
||||
android:ellipsize="end"
|
||||
android:lines="3"
|
||||
android:maxLines="3"
|
||||
android:textAppearance="?android:attr/textAppearanceLarge"
|
||||
android:textSize="@dimen/video_item_search_title_text_size"
|
||||
tools:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc tristique vitae sem vitae blanditLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsumLorem ipsum"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemAdditionalDetails"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_toRightOf="@id/itemThumbnailView"
|
||||
android:layout_toEndOf="@id/itemThumbnailView"
|
||||
android:lines="1"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textSize="@dimen/video_item_search_upload_date_text_size"
|
||||
android:text="@string/playlist"/>
|
||||
</RelativeLayout>
|
|
@ -22,6 +22,7 @@
|
|||
<!-- Can't refer to colors directly into drawable's xml-->
|
||||
<attr name="toolbar_shadow_drawable" format="reference"/>
|
||||
|
||||
<attr name="selector_color" format="color"/>
|
||||
<attr name="separator_color" format="color"/>
|
||||
<attr name="contrast_background_color" format="color"/>
|
||||
</resources>
|
|
@ -292,4 +292,7 @@
|
|||
<string name="top_50">Top 50</string>
|
||||
<string name="new_and_hot">New & hot</string>
|
||||
<string name="service_kiosk_string" translatable="false">%1$s/%2$s</string>
|
||||
|
||||
<!-- Player -->
|
||||
<string name="title_activity_background_player">Background Player</string>
|
||||
</resources>
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
<item name="language">@drawable/ic_language_black_24dp</item>
|
||||
<item name="history">@drawable/ic_history_black_24dp</item>
|
||||
|
||||
<item name="selector_color">@color/light_selector</item>
|
||||
<item name="separator_color">@color/light_separator_color</item>
|
||||
<item name="contrast_background_color">@color/light_contrast_background_color</item>
|
||||
<item name="toolbar_shadow_drawable">@drawable/toolbar_shadow_light</item>
|
||||
|
@ -60,6 +61,7 @@
|
|||
<item name="language">@drawable/ic_language_white_24dp</item>
|
||||
<item name="history">@drawable/ic_history_white_24dp</item>
|
||||
|
||||
<item name="selector_color">@color/dark_selector</item>
|
||||
<item name="separator_color">@color/dark_separator_color</item>
|
||||
<item name="contrast_background_color">@color/dark_contrast_background_color</item>
|
||||
<item name="toolbar_shadow_drawable">@drawable/toolbar_shadow_dark</item>
|
||||
|
|
Loading…
Reference in New Issue